Compare commits

..
Author SHA1 Message Date
Ken 3d566298e4 📝 Colspan update 2026-03-16 09:49:14 +01:00
Ken b5dc86785d 🎉 Fix keyboard-nav 2026-03-13 22:08:29 +01:00
Ken 58e564fed0 Merge branch 'main' into auto-size-table 2026-03-13 21:51:28 +01:00
Ken c4c51dcd37 ⚡ Optimize merge refs 2026-03-13 21:46:45 +01:00
Ken 5c913ae3d6 WIP: Merged auto-generated code 2026-03-13 15:36:12 +01:00
388 changed files with 14802 additions and 21127 deletions

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@navikt/ds-react": patch
---
Dialog: Improved scrollbar handling when opening dialog-popup.
+5
View File
@@ -0,0 +1,5 @@
---
"@navikt/ds-react": patch
---
useDatepicker, useMonthpicker: Add JsDoc to validation object
+5
View File
@@ -0,0 +1,5 @@
---
"@navikt/ds-react": patch
---
DatePicker, MonthPicker: Removed controlled tabindex on Input-button based on open-state.
+1 -1
View File
@@ -13,4 +13,4 @@
- [ ] Exports (`@navikt/core/react/src/index.ts` and `@navikt/core/react/package.json`)
- [ ] New component? CSS import (`@navikt/core/css/index.css`)
- [ ] Breaking change? Update migration guide. Consider codemod.
- [ ] Changeset (Format: `<Component>: <gitmoji?> <Text>` E.g. "Button: :sparkles: Add feature xyz")
- [ ] Changeset (Format: `<Component>: <gitmoji?> <Text>.` E.g. "Button: :sparkles: Add feature xyz.")
+203 -102
View File
@@ -1,145 +1,246 @@
# Aksel - Copilot Instructions
# Aksel – GitHub Copilot Instructions
## Use this file first
This file tells Copilot how to work inside the Aksel monorepo. Prefer existing patterns and symbols, keep changes minimal, and reference real files in this repo.
- Trust these instructions to narrow the search space, but still read the target code and nearest related files before editing.
- Optimize for local work. Prefer known paths, nearby patterns, and existing scripts over broad repo searches.
- Ignore release, deploy, changelog, CMS sync, and other remote/ops scripts unless the user asks.
## How to act in this monorepo (quick)
## How to use this file
- Edit only what’s asked. Don’t invent files/APIs or refactors.
- One file at a time. Provide a single coherent diff per file.
- Use existing exports, tokens, CSS layers, and patterns before adding new ones.
- When touching packages under `@navikt/*`, also update exports and stories nearby.
- Add or adjust tests and stories when behavior/visuals change.
- Link files with repo paths like `@navikt/react/src/...` or `aksel.nav.no/website/...`.
- Keep changes focused and minimal; avoid drive-by rewrites or formatting churn.
- Preserve public APIs unless the request explicitly calls for a breaking change (add a changeset if user-facing).
- Use this file to narrow the search space, not to replace reading code.
- Before editing, read the target file and the nearest related files in the same folder such as `index.ts`, story, test, and package/root export files.
- Search wider only when nearby files do not answer the question.
## Agent mode
## General code guidelines (Copilot)
- Verify before you claim. Don’t speculate.
- Preserve unrelated code and existing structure.
- No apologies, no “I think”, no change summaries.
- Don’t ask to confirm info that’s already in context.
- Prefer explicit, descriptive names and follow current style.
- Favor performance and security; add robust error handling and safe logging.
- Default tone: short and answer-first. Aim for max 0–2 concise sentences where needed.
- Thinking and explanation process: Keep it short, omit whats not strictly necessary, and focus on the next edit.
- Avoid long explanations or multiple steps at once.
- Don't explain thinking process for each step, just do the next edit. Only add explanations when the user explicitly asks for it or when it's necessary to clarify a non-obvious change.
- Keep functions small and focused; remove duplication.
- Replace hardcoded values with named constants when reasonable.
- Prefer composition over deep inheritance; keep modules cohesive.
## Inline chat and editor completions
- Verify before you claim. Don’t speculate.
- Keep functions small and focused; remove duplication (rule of three).
- Default tone: short and answer-first. Aim for 1–3 concise sentences before any code.
- Show only what changes. Provide minimal diffs/snippets; don’t paste whole files or unrelated lines.
- Keep edits scoped to a single file unless explicitly asked to touch more.
- Use existing APIs, utilities, tokens, and patterns. Don’t add new deps unless requested.
- Preserve style: imports, naming, types, JSX patterns, token usage, CSS layers.
- Reference files and symbols with repo-relative paths and backticks, e.g. `@navikt/ds-react/src/...`.
- Extract requirements into a tiny checklist and proceed. Ask a question only when truly blocked.
- Avoid heavy formatting: bullets are OK; skip tables; use code fences only for code.
- For tests/visual changes: add/update the closest test/story next to the changed file.
- For Next.js: don’t mix `app/` and `pages/` in the same hierarchy; prefer server components where used; add "use client" only when required.
- For CSS/tokens: never hard-code colors/spacing; use tokens and existing layers.
- For performance/security: avoid unnecessary re-renders, side effects at module top-level, unsafe HTML, and leaking env vars.
## Repo summary
## Repository overview
- Large Yarn 4 workspaces monorepo for NAV's design system: public React/CSS/tokens/icons/stylelint/CLI packages plus docs site and playroom.
- Stack: TypeScript 6, React 19, Next.js 16, Storybook 10, Vitest 4, ESLint 9, Stylelint 17, Biome 2, Prettier 3.
- Runtime: Node 22+ recommended, Yarn 4.12.0.
- Important constraint: code under `@navikt/*` must stay React 17 API compatible. Do not use React 18/19-only APIs there.
Aksel is NAV’s design system: React components, CSS, design tokens, icons, tooling, and the documentation site. It’s a Yarn 4 workspaces monorepo.
## Start and validate
- Languages: TypeScript, CSS, React, Next.js
- Package manager: Yarn v4 (packageManager: `yarn@4.4.0`)
- Build: Per-package TypeScript builds (run via root scripts)
- Tests: Vitest + React Testing Library
1. `corepack enable`
2. `yarn install`
3. `yarn boot`
Related files: `package.json`, `eslint.config.js`, `biome.json`, `tsconfig.json`.
- Run `yarn boot` after first install and after `yarn clean`.
- Full install may need `NPM_AUTH_TOKEN` for GitHub Packages.
## Prerequisites
Public-package-only install:
- Yarn 4 via Corepack is expected by the repo (`packageManager` field).
- Some maintenance scripts use Deno (for example `scripts/deno/changelog/createMainChangelog.ts`). Install Deno if you run those.
- Most packages are public. If you encounter 401s from GitHub Packages in a custom setup, set `NPM_AUTH_TOKEN` with `read:packages`.
For limited installs (no website), you can focus core packages:
```bash
yarn workspaces focus @navikt/aksel-icons @navikt/ds-tokens @navikt/ds-css @navikt/ds-react @navikt/ds-tailwind @navikt/aksel @navikt/aksel-stylelint
```
## Definition of done
## Repository structure (short)
- Behavior or visual change: update the implementation, closest story, and closest test.
- New public export: update the component `index.ts`, package `src/index.ts`, and `package.json` exports.
- User-facing package change: add a changeset unless the task says otherwise.
- Validate with the narrowest workspace command first; use root commands only for cross-workspace changes.
```
@navikt/
├─ core/react # @navikt/ds-react – React components
├─ core/css # @navikt/ds-css – CSS
├─ core/tokens # @navikt/ds-tokens – tokens
├─ core/tailwind # @navikt/ds-tailwind – Tailwind preset
├─ aksel-icons # @navikt/aksel-icons – icons
├─ aksel-stylelint # @navikt/aksel-stylelint – stylelint rules
└─ aksel # @navikt/aksel – CLI/tooling
### Common local commands
aksel.nav.no/
├─ website # Next.js documentation site
└─ playroom # Component playground
Always prefer using these commands that already exists in workspaces before running custom commands or doing manual steps. These are optimized for local development and validation. For example:
examples/ # Example apps
scripts/ # Build/maintenance scripts
```
- `yarn boot` - build all workspaces
- `yarn test` - run all workspace tests
- `yarn lint` - eslint + stylelint + biome + cycle + version checks
- `yarn lint:eslint` / `yarn lint:css` / `yarn lint:biome`
- `yarn storybook` - root Storybook on `localhost:6006`
- `yarn storybook:aksel` - website/example Storybook on `localhost:6007`
- `yarn storybook:test` - Storybook browser tests, slow, uses Playwright/Firefox
- `yarn dev` - website dev server on `localhost:3000`
- `yarn clean` - remove build artifacts
- `yarn workspace @navikt/ds-react build`
- `yarn workspace @navikt/ds-react test`
- `yarn workspace website test`
- `yarn workspace playroom sync-imports`
## Build and development
### Validated locally
Setup:
- Works: `yarn boot`, `yarn test`, `yarn storybook --ci --smoke-test`, `yarn storybook:aksel --ci --smoke-test`
- Caveat: `yarn lint` and `yarn lint:eslint` can fail if playroom imports are stale. Fix with `yarn workspace playroom sync-imports`.
```bash
yarn && yarn boot
```
## Where to edit
Useful scripts (root `package.json`):
- `@navikt/core/react/` - `@navikt/ds-react`
- `src/<component>/<Component>.tsx`
- `src/<component>/index.ts`
- `src/<component>/*.stories.tsx`
- `src/index.ts`
- `package.json` exports map for new public components
- `@navikt/core/css/src/` - component CSS
- `@navikt/core/tokens/src/` - design token source
- `@navikt/core/tailwind/` - Tailwind preset
- `@navikt/aksel-icons/icons/` - source SVGs; `src/` is generated
- `@navikt/aksel-stylelint/src/` - Stylelint rules/config
- `@navikt/aksel/src/` - CLI and codemods
- `aksel.nav.no/website/` - Next.js docs site
- `aksel.nav.no/playroom/` - playroom and generated imports
- `examples/` - example apps
- `scripts/` - shared repo scripts
- Root references: `README.md`, `CONTRIBUTING.md`, `aksel.nav.no/README.md`, `package.json`, `eslint.config.js`, `stylelint.config.mjs`, `biome.json`, `tsconfig.json`, `tsconfig.build.json`, `.storybook/`, `.github/workflows/build-validation.yml`
```bash
yarn boot # Build all workspaces (topological)
yarn storybook # Storybook dev (port 6006)
yarn storybook:aksel # Storybook with website config (port 6007)
yarn dev # Run documentation website
yarn test # Run workspace tests
yarn lint # ESLint + Stylelint (full install recommended)
yarn clean # Clean generated files
yarn changeset # Create a changeset
yarn changelog # Generate main changelog (Deno)
yarn biome:lint # Optional extra linting with Biome
```
## Coding defaults
Note: `yarn lint` runs across the repo and may require full install (website ESLint plugins).
- Keep diffs small. No drive-by refactors, formatting churn, or new dependencies unless asked.
- Read the full file and copy the nearest existing pattern before changing code.
- Keep public APIs stable. Prefer additive props and exports.
- In `@navikt/*`, import React in TSX files; root JSX runtime is classic.
- Use named exports with explicit type exports.
- Keep `forwardRef`, `className`, `...rest`, and `as`/`OverridableComponent` patterns when the surrounding component uses them.
- Reuse internal helpers instead of re-implementing them: `cl`, `composeEventHandlers`, `useId`, `omit`, `useClientLayoutEffect`, `useEventListener`.
- Use `node:` imports for Node builtins.
- Never import from `esm/` or `cjs/` build output.
- Keep import order consistent: `@navikt/*`, `@/*`, then relative. Let Prettier handle sorting.
- Preserve or add JSDoc for public props/components when changing public APIs.
## CI and validation
## React and ds-react specifics
- Pre-commit (Husky): `lint-staged`, plus internal version sync checks.
- CI typically runs: lint, unit tests, example/website checks, and Chromatic when applicable.
- Component `index.ts` files in `@navikt/core/react/src/**` start with `"use client"`.
- When adding or exposing a public component, keep these in sync:
- component `index.ts`
- `@navikt/core/react/src/index.ts`
- `@navikt/core/react/package.json` exports
- Do not use React 18/19-only APIs under `@navikt/*`. Use internal helpers like `useId`.
Common failure causes:
## CSS and tokens
- Missing dependencies in a focused install when running repo-wide scripts
- TypeScript errors in dependent workspaces
- Stylelint token checks failing due to missing token imports
- Never hardcode colors, spacing, radius, or shadows. Use tokens only.
- Public tokens use `--ax-*`. Internal component variables use `--__axc-*`.
- Follow existing CSS nesting, `data-*` selectors, `focus-visible`, `forced-colors`, and reduced-motion patterns.
- Prettier formats; Biome is lint-only.
Quality gates to check locally before opening a PR:
## Stories, tests, website
- Build: `yarn boot` succeeds without TS errors.
- Lint/format: `yarn lint` (and optional `yarn biome:lint`) passes.
- Tests: `yarn test` for changed workspaces are green.
- Storybook/website (when UI changes): boots locally without errors.
- Update the closest story and test whenever behavior or visuals change.
- Use Testing Library queries by role and name.
- Stories live next to components and use `@storybook/react-vite`.
- For component work, prefer Storybook over `yarn dev`.
- `aksel.nav.no/website/pages/eksempler/**` and `pages/templates/**` feed the website and Storybook. In these files, only import from `@navikt/*` and `react`.
- The website uses both `app/` and `pages/`; stay inside the router already used nearby.
- `yarn storybook:aksel` works for website examples/templates without Sanity secrets. `yarn dev` may need website env values such as `SANITY_READ_NO_DRAFTS`.
## Tests
- Framework: Vitest (jsdom where needed)
- Website config: `aksel.nav.no/website/config/vitest.config.ts`
- Test locations: `**/__tests__/**` and `**/*.test.*`
- Add tests for new behavior and keep existing tests passing
- Prefer small, focused tests. Cover the happy path and 1–2 edge cases.
- Use Testing Library queries that reflect user intent (role/name), avoid brittle selectors.
## Linting and formatting
- ESLint config lives at root: `eslint.config.js` (React, Next.js, Storybook, Testing Library, custom rules)
- Stylelint is configured in root `package.json` with `@navikt/aksel-stylelint`
- Tokens are validated via Stylelint and `csstools/value-no-unknown-custom-properties`
- Biome config: `biome.json` (optional extra lint/format)
- Prettier uses import-sort; website adds Tailwind plugin via override
- Avoid disabling lint rules unless necessary; if you must, comment why.
## Packages and exports
- Synchronized versions: `@navikt/ds-react`, `@navikt/ds-css`, `@navikt/aksel-icons`, `@navikt/ds-tokens`, `@navikt/ds-tailwind`
- Typical package layout:
```
src/
├─ index.ts # Package exports
└─ <component>/
├─ index.ts # Component exports
├─ <component>.tsx # Implementation
└─ <component>.stories.tsx
```
When adding/editing components in `@navikt/react`:
- Update exports in `src/index.ts` and per-component `index.ts`
- Add/maintain stories and tests next to the component
- Follow existing props, theming, and token patterns
- Keep props stable; prefer additive changes. Deprecate before removal when possible.
- Ensure tree-shakeability; avoid side effects at module top-level.
## Common workflows
Add a new component
1. Implement under `@navikt/react/src/...`
2. Add stories and tests
3. Export from the package `index.ts`
4. Run `yarn boot` and fix types/lint
5. Create a changeset (`yarn changeset`)
Component change checklist (quick):
- [ ] Implementation updated under `@navikt/react/src/...`
- [ ] Exports updated (`src/index.ts` and component `index.ts`)
- [ ] Stories updated/added and render without errors
- [ ] Tests updated/added and passing (Vitest)
- [ ] Tokens/CSS use existing variables and layers (no hard-coded values)
- [ ] Changeset added if user-facing (version bump and notes)
Modify an existing component
1. Keep the public API stable unless explicitly requested
2. Update tests if behavior changes; update stories if visuals change
3. Add a changeset for user-facing changes
Troubleshooting
1. `yarn clean && yarn boot`
2. Check workspace dependency build order/issues
3. If focusing workspaces, avoid running repo-wide scripts that depend on the website
## Theming and tokens
- Use design tokens and CSS layers; avoid hard-coded colors/spacing.
- Stylelint enforces token usage; unknown/missing custom properties will fail lint.
## Website (Next.js)
- The website uses Next.js 15 and contains both `app/` and `pages/`. Place new files in the router that matches the surrounding code.
- Prefer server components where already used; mark client components with `"use client"` only when needed.
- Use Next Image, metadata APIs, and established utilities already in `aksel.nav.no/website`.
- Logging uses Next logger/pino; follow patterns in `aksel.nav.no/website` (see `next-logger.config.js`).
- Use existing route conventions and file organization; don’t mix `app/` and `pages/` in the same hierarchy.
- For Tailwind, prefer the shared preset `@navikt/ds-tailwind` and follow website `tailwind.config.js` patterns.
## Versioning and releases
- Use Changesets: `yarn changeset` and the root scripts:
- `yarn create-version` (applies versions, updates changelog, reinstalls)
- `yarn release` (boots, docgen, publish, git release helper)
- Main changelog script: `scripts/deno/changelog/createMainChangelog.ts`
## Answer snippets (for Copilot)
- Build: “Run yarn boot” (see root `package.json`)
- Storybook: “Run yarn storybook” (or `yarn storybook:aksel` for the website config)
- Lint/format: “Run yarn lint” (full install recommended) and optional `yarn biome:lint`
- Versioning: “Run yarn changeset, then yarn create-version”
- Theming: “Import @navikt/ds-css in global CSS”
## Code reviews (for Copilot)
- Summarize the pull-request on maximum two sentences
- Avoid listing all changes, focus on the most important ones
- Check the description against the actual changes
- Ensure the PR title is descriptive and follows the conventional commit format
- Look for: correctness, style, performance, security, tests, documentation
- Check: existing patterns, exports, tokens, CSS layers
- Make sure import paths are correct, updated and use existing aliases where possible
- When components are changed, ensure stories and tests are updated accordingly
- When components are changed, make sure related examples for website and playroom are updated
- Make sure changes made to packages under `@navikt/` has a related changeset where relevant
### Extra review tips
- Watch for unnecessary bundle impact (large deps, dynamic imports, side effects).
- Confirm a11y basics: semantic elements, labels, focus order, keyboard support.
- Check security footguns: unsafe HTML, unescaped data, leaking env vars, weak CSP assumptions.
+12 -2
View File
@@ -8,8 +8,7 @@ updates:
- package-ecosystem: npm
directory: /
exclude-paths:
- "examples/astro/package.json"
- "examples/shadow-dom/package.json"
- "examples/**"
schedule:
interval: weekly
day: monday
@@ -40,6 +39,17 @@ updates:
- "vitest"
- "@vitest/*"
- package-ecosystem: npm
directory: /examples
schedule:
interval: weekly
day: monday
time: "04:00"
groups:
all-deps:
patterns:
- "*"
- package-ecosystem: github-actions
directory: "/"
schedule:
@@ -1,11 +0,0 @@
description: "Use for JS/TS code in this repo."
applyTo: "**/\*.tsx", "**/_.jsx", "\*\*/_.ts", "\*_/_.js"
---
# JS/TS instructions
- Prefer `for...of` over `Array.forEach`.
- Prefer named exports; keep type exports separate from value exports.
- Group exports at the bottom: values first, then types.
- In `*.stories.*`, keep the default export at the top.
@@ -1,13 +0,0 @@
description: "Use for ds-react components and component CSS."
applyTo: "@navikt/core/react/src/**", "@navikt/core/css/src/**"
---
# ds-react and component instructions
- Read the full component folder before editing.
- Preserve the surrounding `forwardRef`, `className`, `...rest`, and `as`/`OverridableComponent` patterns.
- Keep `@navikt/*` code React 17 API compatible.
- Use tokens, not hardcoded component values.
- If behavior or visuals change, update the nearest story and test.
- If a public component changes, keep component exports, package exports, and root exports in sync.
@@ -1,11 +0,0 @@
description: "Use for tests and stories."
applyTo: "**/_.test._", "**/\*.stories.tsx"
---
# Testing and Storybook instructions
- Prefer story `play` functions and `yarn storybook:test` for real user flows; use Vitest for logic and non-browser cases.
- Prefer focused assertions over snapshots unless nearby tests already use snapshots.
- Cover the main path and one or two meaningful edge cases.
- If UI behavior changes, update the nearest story and test in the same change.
@@ -1,14 +0,0 @@
description: "Use for website and playroom work."
applyTo:
- "aksel.nav.no/website/\*\*"
- "aksel.nav.no/playroom/\*\*"
---
# Website and playroom instructions
- Stay inside the router already used nearby; do not mix `app/` and `pages/` patterns in the same area.
- In `aksel.nav.no/website/pages/eksempler/**` and `aksel.nav.no/website/pages/templates/**`, only import from `@navikt/*` and `react`.
- Reuse nearby website utilities and patterns before adding abstractions.
- Prefer `yarn storybook:aksel` for examples/templates; use `yarn dev` only for app-only behavior.
@@ -1,15 +1,10 @@
name: Deploy dev (aksel.ansatt.dev.nav.no)
name: Deploy aksel.ansatt.dev.nav.no
on:
workflow_dispatch:
push:
branches:
- next
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
build_and_deploy:
name: Build and deploy to dev-gcp
@@ -18,7 +13,6 @@ jobs:
packages: "write"
contents: "read"
id-token: "write"
deployments: "write"
steps:
- name: Checkout code
uses: actions/checkout@v6
@@ -35,8 +29,10 @@ jobs:
sanity_read_token: ${{ secrets.SANITY_READ }}
sanity_read_no_drafts_token: ${{ secrets.SANITY_READ_NO_DRAFTS }}
- name: Build and push Docker image
uses: nais/docker-build-push@v0
- name: Get complete tag
run: echo "TAG=DEV-$( date +%s )" >> $GITHUB_ENV
- uses: nais/docker-build-push@v0
id: docker-push
with:
team: designsystem
@@ -1,5 +1,4 @@
name: Deploy prod (aksel.nav.no)
name: Deploy aksel.nav.no
on:
push:
branches:
@@ -7,20 +6,15 @@ on:
paths:
- "aksel.nav.no/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
env:
IMAGE: ghcr.io/navikt/aksel
jobs:
sync_data_with_sanity:
name: Sync data to Sanity
name: Update component data
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- uses: actions/checkout@v6
- name: Setup project
uses: ./.github/actions/setup
@@ -30,7 +24,7 @@ jobs:
- name: Run ts-docgen
run: yarn docgen
- name: Update Sanity
- name: Update sanity sync
env:
SANITY_WRITE: ${{ secrets.SANITY_WRITE }}
run: yarn workspace aksel.nav.no sanity:update
@@ -42,7 +36,6 @@ jobs:
packages: "write"
contents: "read"
id-token: "write"
deployments: "write"
steps:
- name: Checkout code
uses: actions/checkout@v6
@@ -62,8 +55,10 @@ jobs:
sanity_deploy_studio: ${{ secrets.SANITY_DEPLOY_STUDIO }}
next_encryption_key: ${{ secrets.NEXT_ENCRYPTION_KEY }}
- name: Build and push Docker image
uses: nais/docker-build-push@v0
- name: Get complete tag
run: echo "TAG=PROD-$( date +%s )" >> $GITHUB_ENV
- uses: nais/docker-build-push@v0
id: docker-push
with:
team: designsystem
@@ -1,15 +1,10 @@
name: Deploy prod (aksel-v7.nav.no)
name: Deploy aksel-v7.nav.no
on:
workflow_dispatch:
push:
branches:
- v7-aksel.nav.no
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
build_and_deploy:
name: Build and deploy to prod-gcp
@@ -43,8 +38,10 @@ jobs:
run: cd v7-aksel.nav.no && yarn build
shell: bash
- name: Build and push Docker image
uses: nais/docker-build-push@v0
- name: Get complete tag
run: echo "TAG=PROD-V7-$( date +%s )" >> $GITHUB_ENV
- uses: nais/docker-build-push@v0
id: docker-push
with:
team: designsystem
@@ -1,25 +1,18 @@
name: Backup Sanity dataset
name: Backup sanity dateset to gcp
on:
workflow_dispatch:
schedule:
# 12.00 every 3 days
- cron: "0 0 */3 * *"
permissions:
contents: read
jobs:
execute:
name: Export and upload Sanity backup
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- uses: actions/checkout@v6
with:
fetch-depth: 1
fetch-depth: 0 # Required to retrieve git history
- name: Setup Node.js
uses: actions/setup-node@v6
- uses: actions/setup-node@v6
with:
node-version: lts/*
cache: yarn
@@ -35,13 +28,11 @@ jobs:
env:
SANITY_READ: ${{ secrets.SANITY_READ }}
- name: Authenticate with GCP
- name: Upload GCP bucket
uses: "google-github-actions/auth@v3"
with:
credentials_json: "${{ secrets.gcp_credentials_prod }}"
- name: Upload backup to GCS bucket
uses: "google-github-actions/upload-cloud-storage@v3"
- uses: "google-github-actions/upload-cloud-storage@v3"
with:
path: ./aksel.nav.no/website
destination: "aksel-website-prod/backups"
+4 -4
View File
@@ -1,4 +1,4 @@
name: Bundle check (npm)
name: Bundle check against npm
on:
pull_request:
@@ -24,12 +24,12 @@ jobs:
run: echo "should-run=${{ contains(github.event.pull_request.title, 'Version Packages') }}" >> $GITHUB_OUTPUT
analyze:
name: Analyze npm packages
name: "Analyzing npm packages"
runs-on: ubuntu-latest
needs: check-title
if: ${{ needs.check-title.outputs.should-run == 'true' }}
steps:
- name: Checkout code
- name: Checkout main-branch
uses: actions/checkout@v6
with:
fetch-depth: 1
@@ -54,7 +54,7 @@ jobs:
retention-days: 1
update-comment:
name: Post PR comment with results
name: "Create or update PR comment with results"
runs-on: ubuntu-latest
needs: [analyze]
permissions:
@@ -1,4 +1,4 @@
name: Bundle check (main branch)
name: Bundle check against main branch
on:
pull_request:
@@ -24,12 +24,12 @@ jobs:
run: echo "should-run=${{ !contains(github.event.pull_request.title, 'Version Packages') }}" >> $GITHUB_OUTPUT
analyze-trunk:
name: Analyze main branch
name: "Analyzing main branch"
runs-on: ubuntu-latest
needs: check-title
if: ${{ needs.check-title.outputs.should-run == 'true' }}
steps:
- name: Checkout main branch
- name: Checkout main-branch
uses: actions/checkout@v6
with:
fetch-depth: 1
@@ -52,12 +52,12 @@ jobs:
retention-days: 1
analyze-local:
name: Analyze PR branch
name: "Analyzing PR branch"
runs-on: ubuntu-latest
needs: check-title
if: ${{ needs.check-title.outputs.should-run == 'true' }}
steps:
- name: Checkout PR branch
- name: Checkout main-branch
uses: actions/checkout@v6
with:
fetch-depth: 1
@@ -79,7 +79,7 @@ jobs:
retention-days: 1
compare-against-trunk:
name: Compare bundles (PR vs main)
name: "Analyzing PR branch and comparing against main"
runs-on: ubuntu-latest
needs: [analyze-trunk, analyze-local]
steps:
@@ -117,7 +117,7 @@ jobs:
retention-days: 1
update-comment:
name: Post PR comment with results
name: "Create or update PR comment with results"
runs-on: ubuntu-latest
needs: [compare-against-trunk]
permissions:
@@ -1,4 +1,4 @@
name: Visual regression (Chromatic)
name: Visual Regression Tests
on:
push:
@@ -19,7 +19,6 @@ concurrency:
jobs:
chromatic:
name: Run Chromatic
runs-on: ubuntu-latest
permissions:
issues: write
@@ -48,7 +47,7 @@ jobs:
buildScriptName: "build:storybook-chromatic"
onlyChanged: true
untraced: "**/package.json|yarn.lock|**/*.md"
externals: "**/core/css/src/**/*.css"
externals: "**/core/css/*.css|**/core/css/baseline/*.css|**/core/css/form/*.css|**/core/css/primitives/*.css"
traceChanged: true
env:
STORYBOOK_GITHUB_SHA: ${{ github.sha }}
@@ -1,4 +1,4 @@
name: Validation (Lint, Tests)
name: CI
on:
push:
@@ -8,16 +8,9 @@ on:
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
name: Lint (ESLint, Stylelint etc.)
name: "Linter: eslint, stylelint"
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -34,7 +27,7 @@ jobs:
run: yarn lint
validate-examples:
name: Validate component examples
name: "Validate Aksel.nav.no component examples"
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -51,7 +44,7 @@ jobs:
run: yarn validate:eksempler
test:
name: Unit and integration tests
name: "Tests: Unit and integration"
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -68,7 +61,7 @@ jobs:
run: yarn test
ui:
name: UI tests (Storybook)
name: "Tests: UI"
runs-on: ubuntu-latest
timeout-minutes: 10
container:
-26
View File
@@ -1,26 +0,0 @@
name: Cleanup E2E Docker images
on:
schedule:
- cron: "0 4 * * 1"
workflow_dispatch:
permissions:
packages: write
jobs:
cleanup:
runs-on: ubuntu-latest
strategy:
matrix:
package-name:
- aksel-e2e
steps:
- name: Delete old container images
uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # actions/delete-package-versions@v5
with:
package-name: ${{ matrix.package-name }}
package-type: container
delete-only-untagged-versions: false
min-versions-to-keep: 1
token: ${{ secrets.GITHUB_TOKEN }}
@@ -1,4 +1,4 @@
name: E2E tests (Playwright)
name: E2E / Playwright Tests
on:
push:
branches: [main, next]
@@ -21,7 +21,7 @@ permissions:
jobs:
prebuild-app-for-playwright:
name: Build app for Playwright
name: "Prebuild app for Playwright tests"
runs-on: ubuntu-latest
env:
IMAGE_NAME: ${{ github.repository }}-e2e
@@ -59,7 +59,7 @@ jobs:
run-playwright:
needs: [prebuild-app-for-playwright]
name: Playwright shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
name: Playwright shard ${{ matrix.shardIndex }}
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.58.1-noble
@@ -111,7 +111,6 @@ jobs:
retention-days: 1
merge-reports:
name: Merge Playwright reports
if: ${{ !cancelled() }}
needs: [run-playwright]
runs-on: ubuntu-latest
@@ -1,4 +1,4 @@
name: Upload Figma token config to CDN
name: "Update remote token config for figma variables plugin"
on:
workflow_dispatch:
@@ -10,7 +10,6 @@ on:
jobs:
upload:
name: Build and upload token config
runs-on: ubuntu-latest
permissions:
@@ -18,13 +17,11 @@ jobs:
id-token: "write"
steps:
- name: Checkout code
uses: actions/checkout@v6
- uses: actions/checkout@v6
with:
fetch-depth: 1
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
- uses: actions/setup-node@v6
with:
node-version: lts/*
cache: yarn
@@ -38,8 +35,7 @@ jobs:
- name: Build tokens
run: yarn workspace @navikt/ds-tokens build
- name: Upload config to CDN
id: upload
- id: upload
uses: nais/deploy/actions/cdn-upload/v2@master
with:
team: designsystem
@@ -50,6 +46,5 @@ jobs:
source_keep_parent_name: false
no_cache_paths: "@navikt/tokens/figma-config.json"
- name: Log uploaded file
run: echo uploaded file ${{ steps.upload.outputs.uploaded }}
- run: echo uploaded file ${{ steps.upload.outputs.uploaded }}
shell: bash
@@ -1,4 +1,4 @@
name: Upload fonts to CDN
name: "push-to-cdn"
on:
workflow_dispatch:
@@ -10,19 +10,16 @@ on:
jobs:
upload:
name: Upload fonts to CDN
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
contents: "read"
id-token: "write"
steps:
- name: Checkout code
uses: "actions/checkout@v6"
- uses: "actions/checkout@v6"
- name: Upload fonts to CDN
id: upload
- id: upload
uses: nais/deploy/actions/cdn-upload/v2@master
with:
team: aksel
@@ -31,6 +28,5 @@ jobs:
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- name: Log uploaded file
run: echo uploaded file ${{ steps.upload.outputs.uploaded }}
- run: echo uploaded file ${{ steps.upload.outputs.uploaded }}
shell: bash
+7 -10
View File
@@ -1,4 +1,4 @@
name: Release packages
name: Release
on:
push:
@@ -13,8 +13,8 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
build:
name: Release packages
if: ${{ !contains(github.event.head_commit.message, '[ci skip]') }}
name: "Release packages"
if: ${{!contains(github.event.head_commit.message, '[ci skip]')}}
runs-on: ubuntu-latest
permissions:
id-token: "write"
@@ -22,20 +22,17 @@ jobs:
packages: "write"
pull-requests: "write"
steps:
- name: Checkout code
uses: actions/checkout@v6
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
- uses: actions/setup-node@v6
with:
node-version: lts/*
cache: yarn
cache-dependency-path: "yarn.lock"
- name: Setup Deno
uses: denoland/setup-deno@v2
- uses: denoland/setup-deno@v2
with:
deno-version: v1.x
@@ -58,7 +55,7 @@ jobs:
# Needed for push to main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create .npmrc for GPR
- name: Creating .npmrc
run: |
cat << EOF > "$HOME/.npmrc"
@navikt:registry=https://npm.pkg.github.com
+29
View File
@@ -0,0 +1,29 @@
# https://github.com/actions/stale
name: Mark stale issues
on:
schedule:
- cron: "31 3 * * *"
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-issue-stale: 120
days-before-pr-stale: -1
days-before-close: 30
any-of-labels: "Besvart"
stale-issue-message: |-
Hei! Det ser ut som det ikke har vært aktivitet her på en stund. Etter 30 dager vil saken bli lukket automatisk.
Om saken fortsatt er relevant eller du har oppdateringer, vil en kommentar hindre at den lukkes. Du kan alltids åpne den igjen hvis det skulle være nødvendig!
stale-issue-label: "no-issue-activity"
ascending: true
operations-per-run: 200
+5 -11
View File
@@ -1,22 +1,16 @@
name: Update article views (Naisjob)
name: Redeploy Naisjob (update-article-views)
on:
workflow_dispatch:
jobs:
execute:
name: Build and deploy article views job
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
id-token: "write"
steps:
- name: Checkout code
uses: actions/checkout@v6
- uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Build and push Docker image
uses: nais/docker-build-push@v0
- uses: nais/docker-build-push@v0
id: docker-push
with:
team: designsystem
@@ -25,7 +19,7 @@ jobs:
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- name: Deploy Naisjob to prod-gcp
- name: start naisjob prod-gcp
uses: nais/deploy/actions/deploy@master
env:
APIKEY: ${{ secrets.NAIS_DEPLOY_APIKEY }}
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname $0)/_/husky.sh"
yarn lint-staged
yarn lint-staged && yarn tsx ./scripts/in-sync-versions.ts
-13
View File
@@ -1,13 +0,0 @@
#!/bin/sh
. "$(dirname $0)/_/husky.sh"
# Only run tests when pushing to main branch
while read local_ref local_sha remote_ref remote_sha; do
if [ "$remote_ref" = "refs/heads/main" ]; then
echo "Pushing to main branch - running tests...\n"
yarn test
exit $?
fi
done
exit 0
+4 -4
View File
@@ -1,4 +1,4 @@
import { defineMain } from "@storybook/react-vite/node";
import { StorybookConfig } from "@storybook/react-vite";
import FG from "fast-glob";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
@@ -17,7 +17,7 @@ const includeWebsiteStories = process.env.WITH_WEBSITE === "true";
const indexRegex = /export const args = {\s+index: (\d+),/;
export default defineMain({
export default {
experimental_indexers: (indexers) => {
// Changes here might need to be reflected in aksel.nav.no/website/.storybook/main.ts
const customIndexer = async (fileName: string, opts: any) => {
@@ -126,9 +126,9 @@ export default defineMain({
features: {
actions: false,
},
});
} satisfies StorybookConfig;
function getAbsolutePath(value: string) {
function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, "package.json")));
}
+4 -8
View File
@@ -1,8 +1,5 @@
import addonA11y from "@storybook/addon-a11y";
import addonDocs from "@storybook/addon-docs";
import addonThemes, { withThemeByClassName } from "@storybook/addon-themes";
import addonVitest from "@storybook/addon-vitest";
import { definePreview } from "@storybook/react-vite";
import { withThemeByClassName } from "@storybook/addon-themes";
import { Preview } from "@storybook/react-vite";
import React, { useEffect } from "react";
import "../@navikt/core/css/src/data-table.css";
import "../@navikt/core/css/src/data-token-filter.css";
@@ -58,8 +55,7 @@ const TypoDecorator = ({
return <>{children}</>;
};
export default definePreview({
addons: [addonA11y(), addonThemes(), addonDocs(), addonVitest()],
export default {
parameters: {
options: {
storySort: {
@@ -136,4 +132,4 @@ export default definePreview({
defaultTheme: "light",
}),
],
});
} satisfies Preview;
+4 -4
View File
@@ -1,13 +1,13 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
"biomejs.biome",
"esbenp.prettier-vscode",
"denoland.vscode-deno",
"esbenp.prettier-vscode",
"github.vscode-github-actions",
"ms-playwright.playwright",
"sanity-io.vscode-sanity",
"vunguyentuan.vscode-css-variables"
"stylelint.vscode-stylelint",
"vunguyentuan.vscode-css-variables",
"bradlc.vscode-tailwindcss"
]
}
+5 -2
View File
@@ -1,5 +1,8 @@
{
"cssVariables.lookupFiles": ["@navikt/core/tokens/dist/tokens.css"],
"cssVariables.lookupFiles": [
"@navikt/core/tokens/dist/tokens.css",
"@navikt/core/tokens/dist/darkside/tokens.css"
],
"eslint.problems.shortenToSingleLine": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"files.watcherExclude": {
@@ -9,6 +12,6 @@
"**/cjs": true
},
"playwright.reuseBrowser": true,
"js/ts.tsdk.path": "node_modules/typescript/lib",
"typescript.tsdk": "node_modules/typescript/lib",
"deno.enablePaths": ["./scripts/deno"]
}
-2
View File
@@ -12,10 +12,8 @@ npmScopes:
npmAuthToken: "${NPM_AUTH_TOKEN:-}"
npmRegistryServer: "https://npm.pkg.github.com"
enableScripts: false
defaultSemverRangePrefix: ""
npmMinimalAgeGate: "7d"
npmPreapprovedPackages: ["@navikt/*"]
enableTelemetry: false
yarnPath: .yarn/releases/yarn-4.12.0.cjs
-12
View File
@@ -1,17 +1,5 @@
# @navikt/aksel-icons
## 8.10.2
## 8.10.1
## 8.10.0
## 8.9.1
## 8.9.0
## 8.8.0
## 8.7.0
## 8.6.0
+38 -5
View File
@@ -2,11 +2,44 @@
## Foreslå nye ikoner
Om du ikke finner ikonet du leter etter kan du [lage et issue på GitHub](https://github.com/navikt/aksel/issues/new?labels=nytt+✨%2Cikoner+🖼%2Cforespørsel+🥰&template=new-icon.yaml&title=%5BNytt+ikon%5D%3A+).
Om du ikke finner et ikonet du leter etter kan du [åpne et issue på github](https://github.com/navikt/aksel/issues/new?labels=nytt+✨%2Cikoner+🖼%2Cforespørsel+🥰&template&template=new-icon.yaml&title=%5BNytt+ikon%5D%3A+).
## Oppdatere ikon
Hvis du tenker et ikon fortjener en oppdatering eller er utdatert kan [du foreslå det her](https://github.com/navikt/aksel/issues/new?labels=forespørsel+🥰&template=update-icon.yml&title=%5BInnspill+til+ikon%5D%3A+).
Hvis du tenker et ikon fortjener en oppdatering eller er utdatert kan [du foreslå det her.](https://github.com/navikt/aksel/issues/new?labels=forespørsel+🥰&template=update-icon.yml&title=%5BInnspill+til+ikon%5D%3A+)
## Legge til nye ikoner selv
Hvis du selv ønsker å legge til nye ikoner kan du kopiere filenes fra `/template` og legge de inn i `/icons` med egen data. Alle ikonene har naming-format `PascalCase`. Alle ikonene har 1 SVG-fil og 1 tilhørende YML-fil.
### Svg
- Sizing, width og height må være 24x24: `width="24" height="24" `
- Viewbox: `viewBox="0 0 24 24"`
- Fill på path: `fill="#202733"`
### Yml
```yml
name: Template # Samme som filnavn
category: Template category # Hovedkategori
sub_category: Sub category # Underkategori
keywords: # Samling med søkeord
- template1
- template2
- template3
variant: Stroke # Stroke eller Fill
created_at: 01.01.2023 # Dato for opprettelse
```
### Publisere oppdatering
Når du har lagt til nye ikoner kan du opprette en pull request. Denne vil bli automatisk testet og så gått gjennom av Aksel-teamet.
Legg gjerne til en `changeset` ved å kjøre `yarn changeset` i terminalen. Dette vil gi oss bedre oversikt over hva som er endret i endringslogger og automatisk bumpe pakke-versjonen ved merge.
#### Teste nytt ikon
Hvis du ønsker å teste om alt stemmer, både i `svg` og `yml` før du lager en PR kan du kjøre `yarn && cd navikt/aksel-icons && yarn test` i terminalen lokalt.
## Design av nye ikoner
@@ -17,9 +50,9 @@ Hvis du tenker et ikon fortjener en oppdatering eller er utdatert kan [du foresl
### Stroke (linje)
- Ikonet må bestå av kun linjer.
- Linjene skal være 1.5px, center-aligned.
- Ikonet må bestå av kun linjer
- Linjene skal være 1.5px, center-aligned
### Fill (fylt)
- Ikoner hvor shapes kan fylles, bør fylles.
- Ikoner hvor shapes kan fylles, bør fylles
@@ -14,8 +14,8 @@
"@navikt/ds-css": "*",
"@navikt/ds-react": "*",
"fuse.js": "^7.0.0",
"react": "19.2.5",
"react-dom": "19.2.5"
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@figma/plugin-typings": "^1.100.2",
@@ -24,8 +24,8 @@
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "^5.1.0",
"typescript": "6.0.2",
"vite": "^7.3.2",
"typescript": "5.9.3",
"vite": "^7.2.4",
"vite-plugin-singlefile": "^2.3.0",
"vite-plugin-svgr": "^4.2.0"
}
@@ -155,7 +155,7 @@ const App = () => {
<div className="icons-wrapper">
{sub.icons.map((i) => {
// biome-ignore lint/performance/noDynamicNamespaceImportAccess: We do not know which icon will be rendered at build time
const T = Icons[`${i.id}Icon`];
const T = Icons[`${i.id}Icon`]; // eslint-disable-line import/namespace
if (T === undefined) {
return null;
}
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-icons",
"version": "8.10.2",
"version": "8.7.0",
"author": "Aksel | Nav",
"homepage": "https://aksel.nav.no/ikoner",
"repository": {
@@ -82,14 +82,14 @@
"fast-glob": "3.3.3",
"hast-util-select": "6.0.4",
"js-yaml": "^4.1.1",
"lodash": "4.18.1",
"lodash": "4.17.23",
"prettier": "3.8.1",
"react": "19.2.5",
"react": "19.2.4",
"rehype-parse": "9.0.1",
"tsc-alias": "1.8.16",
"tsx": "^4.20.6",
"typescript": "6.0.2",
"typescript": "5.9.3",
"unified": "11.0.5",
"vitest": "4.1.4"
"vitest": "4.0.18"
}
}
-2
View File
@@ -1,8 +1,6 @@
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"ignoreDeprecations": "6.0",
"rootDir": "./src",
"outDir": "./dist/react/cjs",
"moduleResolution": "node",
"module": "commonjs",
+1 -1
View File
@@ -14,7 +14,7 @@ function useGlobalId(idOverride?: string): string | undefined {
// We can't use it server-side.
// If you want to use random values please consider the Birthday Problem: https://en.wikipedia.org/wiki/Birthday_problem
globalId += 1;
setDefaultId(`aksel-icon-${globalId}`); // eslint-disable-line react-hooks/set-state-in-effect
setDefaultId(`aksel-icon-${globalId}`);
}
}, [defaultId]);
return id;
-16
View File
@@ -1,21 +1,5 @@
# @navikt/aksel-stylelint
## 8.10.2
## 8.10.1
## 8.10.0
## 8.9.1
## 8.9.0
## 8.8.0
### Patch Changes
- ReadMore: Deprecated classes `aksel-read-more--open` and `aksel-read-more__content--closed`. Use `data-state` instead to access state. ([#4723](https://github.com/navikt/aksel/pull/4723))
## 8.7.0
## 8.6.0
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-stylelint",
"version": "8.10.2",
"version": "8.7.0",
"type": "module",
"engines": {
"node": ">=18.12.0"
@@ -42,14 +42,14 @@
"dev": "yarn watch:lint"
},
"devDependencies": {
"@navikt/ds-css": "^8.10.2",
"@navikt/ds-tokens": "^8.10.2",
"@navikt/ds-css": "^8.7.0",
"@navikt/ds-tokens": "^8.7.0",
"concurrently": "9.2.1",
"postcss-selector-parser": "^7.1.0",
"postcss-value-parser": "^4.2.0",
"stylelint": "^17.2.0",
"stylelint-test-rule-node": "^1.0.0",
"tsx": "^4.20.6",
"typescript": "6.0.2"
"typescript": "5.9.3"
}
}
@@ -18,9 +18,4 @@ export const deprecations: DeprecatedList = [
"Removed in vX.X.X, see documentation [link] for more information.",
deprecatePrefix: true,
},
{
classes: ["aksel-read-more__content--closed", "aksel-read-more--open"],
message:
"Removed in version ^8.8. Use data-state='open' or 'closed' instead.",
},
];
+10 -13
View File
@@ -8,25 +8,22 @@ const __dirname = dirname(__filename);
const flattenObject = (
obj: Record<string, string | Record<string, string>>,
): string[] => {
const flattened = Object.keys(obj).reduce<(string | string[])[]>(
(acc, key) => {
if (typeof obj[key] === "string") {
acc.push(key);
} else {
acc.push(flattenObject(obj[key]));
}
return acc;
},
[],
);
) => {
const flattened = Object.keys(obj).reduce((acc, key) => {
if (typeof obj[key] === "string") {
acc.push(key);
} else {
acc.push(flattenObject(obj[key]));
}
return acc;
}, []);
return flattened.flat();
};
const tokenCSSFile = "./global-tokens.css";
const tokenJsonFile = "./component-tokens.json";
const allowedTokenNames: string[] = [];
const allowedTokenNames = [];
export const tokenExists = (
controlledPrefixes: string[],
-48
View File
@@ -1,53 +1,5 @@
# @navikt/aksel
## 8.10.2
### Patch Changes
- Updated dependencies [[`90194d7`](https://github.com/navikt/aksel/commit/90194d7d95edd2ad69156528ec7e1e0600daef8a)]:
- @navikt/ds-css@8.10.2
- @navikt/ds-tokens@8.10.2
## 8.10.1
### Patch Changes
- Updated dependencies [[`f66236c`](https://github.com/navikt/aksel/commit/f66236c3710d132f4a2bbe043282a98136b089d9)]:
- @navikt/ds-css@8.10.1
- @navikt/ds-tokens@8.10.1
## 8.10.0
### Patch Changes
- Updated dependencies [[`63f5aaf`](https://github.com/navikt/aksel/commit/63f5aaf2f4bfdc3402afe9617c806309e0169420), [`139805d`](https://github.com/navikt/aksel/commit/139805d5b4b6422e99ee04c1dcde1bdd15c8ddaa), [`2eab5b9`](https://github.com/navikt/aksel/commit/2eab5b956420848ca9fc785bc63263161ac79f85), [`f491aae`](https://github.com/navikt/aksel/commit/f491aaec704314fe97b77d92cc2ed1564454c0e8), [`2eab5b9`](https://github.com/navikt/aksel/commit/2eab5b956420848ca9fc785bc63263161ac79f85), [`c096f46`](https://github.com/navikt/aksel/commit/c096f46beb07f48e2869f7bcc722732e4f7ac31e), [`c096f46`](https://github.com/navikt/aksel/commit/c096f46beb07f48e2869f7bcc722732e4f7ac31e)]:
- @navikt/ds-css@8.10.0
- @navikt/ds-tokens@8.10.0
## 8.9.1
### Patch Changes
- Updated dependencies [[`f7393f7`](https://github.com/navikt/aksel/commit/f7393f75159d3e4cff5fd438fd8e275325f98f1c), [`9346578`](https://github.com/navikt/aksel/commit/93465781c13e91ec41520e1284cba42c452d1386), [`47d74be`](https://github.com/navikt/aksel/commit/47d74be1a0db844d7116e46b0608c42146dcbc01)]:
- @navikt/ds-css@8.9.1
- @navikt/ds-tokens@8.9.1
## 8.9.0
### Patch Changes
- Updated dependencies [[`86f86d3`](https://github.com/navikt/aksel/commit/86f86d37608432160333ed275f76553d936bf63a), [`72a9131`](https://github.com/navikt/aksel/commit/72a9131aeeac41d0242869f5684999e6bc731285)]:
- @navikt/ds-css@8.9.0
- @navikt/ds-tokens@8.9.0
## 8.8.0
### Patch Changes
- Updated dependencies [[`befae1e`](https://github.com/navikt/aksel/commit/befae1e0a27e32b46c45335cf438aa65f4333b9f), [`7388130`](https://github.com/navikt/aksel/commit/738813046ffece51d4670a33297f4333c6bae8d0), [`5756986`](https://github.com/navikt/aksel/commit/57569860700bf81d6eef06ddbc39270933eff528), [`232ed45`](https://github.com/navikt/aksel/commit/232ed4569414e70f5a6f8e7bab584226f191a889)]:
- @navikt/ds-css@8.8.0
- @navikt/ds-tokens@8.8.0
## 8.7.0
### Patch Changes
+6 -15
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel",
"version": "8.10.2",
"version": "8.7.0",
"description": "Aksel command line interface. Codemods and other utilities for Aksel users.",
"author": "Aksel | Nav designsystem team",
"license": "MIT",
@@ -23,13 +23,6 @@
"test:watch": "vitest watch"
},
"bin": "./dist/index.js",
"exports": {
"./package.json": "./package.json",
"./migrations": {
"import": "./dist/migrations-config.js",
"types": "./dist/migrations-config.d.ts"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/navikt/aksel.git",
@@ -40,8 +33,8 @@
"url": "https://github.com/navikt/aksel/issues"
},
"dependencies": {
"@navikt/ds-css": "^8.10.2",
"@navikt/ds-tokens": "^8.10.2",
"@navikt/ds-css": "^8.7.0",
"@navikt/ds-tokens": "^8.7.0",
"chalk": "5.6.2",
"cli-progress": "3.12.0",
"clipboardy": "5.0.1",
@@ -52,15 +45,13 @@
"is-git-clean": "1.1.0",
"jscodeshift": "17.3.0",
"jscodeshift-add-imports": "1.0.11",
"lodash": "4.18.1"
"lodash": "4.17.23"
},
"devDependencies": {
"@types/cli-progress": "3.11.6",
"@types/is-git-clean": "1.1.2",
"@types/jscodeshift": "17.3.0",
"rimraf": "6.1.3",
"typescript": "6.0.2",
"vitest": "4.1.4"
"typescript": "5.9.3",
"vitest": "4.0.18"
},
"sideEffects": false,
"engines": {
+214 -9
View File
@@ -1,5 +1,216 @@
import chalk from "chalk";
import { migrations } from "../migrations-config";
import { SupportedCodemodExtensions } from "./codeshift.utils";
const JS_EXTENSIONS = [
"js",
"jsx",
"ts",
"tsx",
] satisfies SupportedCodemodExtensions[];
const CSS_EXTENSIONS = [
"css",
"scss",
"less",
] satisfies SupportedCodemodExtensions[];
type MigrationT = Record<
string,
{
description: string;
value: string;
path: string;
warning?: string;
/**
* Some transforms are only meant to run on specific file extensions.
* This is used to filter out files that should not be transformed.
*/
ignoredExtensions: SupportedCodemodExtensions[];
}[]
>;
export const migrations: MigrationT = {
v1: [
{
description: "Runs all codemods for beta -> v1 migration",
value: "v1-preset",
path: "v1.0.0/preset/preset",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description: "Fixes breaking API-changes for <Pagination /> component",
value: "v1-pagination",
path: "v1.0.0/pagination/pagination",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description: "Fixes breaking API-changes for <Tabs /> component",
value: "v1-tabs",
path: "v1.0.0/tabs/tabs",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Fixes breaking API-changes for <SpeechBubble /> (now <Chat/>) component",
value: "v1-chat",
path: "v1.0.0/chat/chat",
ignoredExtensions: CSS_EXTENSIONS,
},
],
v2: [
{
description: "Patches changed css-variables",
value: "v2-css",
path: "v2.0.0/update-css-tokens/update-css-tokens",
ignoredExtensions: [],
},
{
description: "Patches changed js-variables",
value: "v2-js",
path: "v2.0.0/update-js-tokens/update-js-tokens",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description: "Patches changed sass-variables",
value: "v2-sass",
path: "v2.0.0/update-sass-tokens/update-sass-tokens",
ignoredExtensions: JS_EXTENSIONS,
},
{
description: "Patches changed less-variables",
value: "v2-less",
path: "v2.0.0/update-less-tokens/update-less-tokens",
ignoredExtensions: JS_EXTENSIONS,
},
],
v3: [
{
description:
"Replaces deprecated <CopyToClipboard /> with <CopyButton />",
value: "v3-copybutton",
path: "v3.0.0/copybutton/copybutton",
warning:
"Remember to remove css-import from '@navikt/ds-css-internal' if no longer needed\nIf non-text was used as children, or different locales were handled, you need to manually fix this",
ignoredExtensions: CSS_EXTENSIONS,
},
],
v4: [
{
description:
"Replaced deprecated 'internal'-component import to 'core'-imports",
value: "v4-internal-react",
path: "v4.0.0/internal-react/internal-react",
warning:
"Remember to also run 'v4-internal-css' if you have overwritten any css-classes!",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Replaced 'navdsi'-prefixes used to overwrite internal-css to 'navds'-prefixes",
value: "v4-internal-css",
path: "v4.0.0/internal-css/internal-css",
warning:
"Remember to remove '@navikt/ds-css-internal' imports if no longer needed! Also run 'v4-internal-react' to convert internal-react imports to core-imports",
ignoredExtensions: [],
},
{
description:
"Removes all instances of UNSAFE_ for datepicker/monthpicker.",
value: "v4-date",
path: "v4.0.0/date/date",
ignoredExtensions: CSS_EXTENSIONS,
},
],
v6: [
{
description:
"Removes `backgroundColor` and `avatarBgColor` properties from `Chat` and `Chat.Bubble`",
value: "v6-chat",
path: "v6.0.0/chat/chat",
warning:
"Remember to update use of `variant`-prop to match previous use of colors. If needed the component exposes css-variables for custom overrides",
ignoredExtensions: CSS_EXTENSIONS,
},
],
v8: [
{
description:
"Updates Box with legacy-tokens to Box using the new token system, and renames already migrated BoxNew/Box.New instances to Box.",
value: "v8-box",
path: "v8.0.0/box/box",
warning:
"Remember to check if 'TODO: Aksel Box migration'-comment was added to any files after migration. This comment will help you find and update Box-instances we couldn't update for you.",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description: "Renames already migrated BoxNew/Box.New instances to Box.",
value: "v8-box-new",
path: "v8.0.0/box-new/box-new",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description: "Removes deprecated props from components.",
value: "v8-prop-deprecate",
path: "v8.0.0/prop-deprecate/prop-deprecate",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Tries to migrate List component by wrapping it with 'Box' and moving 'title' and 'description' props to 'Heading' and 'BodyShort'.",
value: "v8-list",
path: "v8.0.0/list/list",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description: "Updates all Primitives to use new `space`-tokens.",
value: "v8-primitive-spacing",
path: "v8.0.0/primitives-spacing/spacing",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Updates variant + data-color props on Tag based on current variant prop.",
value: "v8-tag-variant",
path: "v8.0.0/tag-variant/tag-variant",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Updates variant + data-color props on ToggleGroup based on current variant prop.",
value: "v8-toggle-group-variant",
path: "v8.0.0/toggle-group-variant/toggle-group-variant",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Updates variant + data-color props on Accordion based on current variant prop.",
value: "v8-accordion-variant",
path: "v8.0.0/accordion-variant/accordion-variant",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Updates variant + data-color props on Chips based on current variant prop.",
value: "v8-chips-variant",
path: "v8.0.0/chips-variant/chips-variant",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Updates variant + data-color props on Button based on current variant prop.",
value: "v8-button-variant",
path: "v8.0.0/button-variant/button-variant",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Updates variant + data-color props on Link based on current variant prop.",
value: "v8-link-variant",
path: "v8.0.0/link-variant/link-variant",
ignoredExtensions: CSS_EXTENSIONS,
},
],
} as const;
/**
* Extracts `path` field for a given migration.
@@ -53,14 +264,8 @@ export function getMigrationsForVersion(version: string) {
/**
* Returns the override migrations available for a specific version.
*/
function isOverrideVersion(
version: string,
): version is keyof typeof migrationStringOverride {
return version in migrationStringOverride;
}
export function getOverridesForVersion(version: string) {
return isOverrideVersion(version) ? migrationStringOverride[version] : [];
return migrationStringOverride[version] ?? [];
}
/**
@@ -87,7 +292,7 @@ export function getMigrationString() {
Object.entries(migrations).forEach(([version, vMigrations]) => {
str += `\n${chalk.underline(version)}\n`;
const overrideMigrations = getOverridesForVersion(version);
const overrideMigrations = migrationStringOverride[version] || [];
overrideMigrations.forEach((migration) => {
str += `${chalk.blue(migration.value)}: ${migration.description}\n`;
});
+1 -6
View File
@@ -80,11 +80,6 @@ export async function runCodeshift(
warning && console.info(`\n${chalk.yellow(warning)}\n`);
} catch (error) {
program.error(
chalk.red(
"Error:",
error instanceof Error ? error.message : String(error),
),
);
program.error(chalk.red("Error:", error.message));
}
}
@@ -50,9 +50,7 @@ async function runVersionMigration(
chalk.greenBright.bold(`\nAvailable migrations for ${version}:\n`),
);
const overrideValues = new Set<string>(
overrideMigrations.map((m) => m.value),
);
const overrideValues = new Set(overrideMigrations.map((m) => m.value));
const choices = [
...overrideMigrations.map((migration) => ({
@@ -363,6 +363,6 @@ const tokens = [
["--navdsi-header-color-hover", "--a-surface-inverted-hover"],
["--navdsi-header-color-active", "--a-surface-inverted-active"],
["--navds-button-icon-adjustment", "--ac-button-icon-margin"]
] as const
]
export { tokens };
module.exports = { tokens };
@@ -1,11 +1,10 @@
import type { API, FileInfo, Options } from "jscodeshift";
import renameProps from "../../utils/rename-props";
export default function transformer(
file: FileInfo,
api: API,
options: Options,
) {
/**
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
export default function transformer(file, api, options) {
const j = api.jscodeshift;
const root = j(file.source);
@@ -1,8 +1,11 @@
import type { API, FileInfo } from "jscodeshift";
import { getLineTerminator } from "../../../utils/lineterminator";
import renameProps from "../../../utils/rename-props";
export default function transformer(file: FileInfo, api: API) {
/**
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
export default function transformer(file, api) {
const j = api.jscodeshift;
let localName = "SpeechBubble";
@@ -13,17 +16,14 @@ export default function transformer(file: FileInfo, api: API) {
.find(j.ImportDeclaration)
.filter((path) => path.node.source.value === "@navikt/ds-react")
.forEach((imp) => {
imp.value.specifiers?.forEach((x) => {
if (x.type !== "ImportSpecifier") return;
imp.value.specifiers.forEach((x) => {
if (x.imported.name === "SpeechBubble") {
if (x.local && x.local.name !== x.imported.name) {
localName = String(x.local.name);
if (x.local.name !== x.imported.name) {
localName = x.local.name;
x.imported.name = "Chat";
} else {
x.imported.name = "Chat";
if (x.local) {
x.local.name = "Chat";
}
x.local.name = "Chat";
}
}
});
@@ -47,34 +47,20 @@ export default function transformer(file: FileInfo, api: API) {
compRoot.forEach((x) => {
if (localName !== "SpeechBubble") return;
const opening = x.node.openingElement.name;
if (opening.type === "JSXIdentifier") {
opening.name = "Chat";
}
const closing = x.node.closingElement?.name;
if (closing?.type === "JSXIdentifier") {
closing.name = "Chat";
}
x.node.openingElement.name.name = "Chat";
x.node.closingElement.name.name = "Chat";
});
/* Need to handle dot-notations differently */
const child = root.find(j.JSXElement);
child.forEach((x) => {
const openingName = x.value.openingElement.name;
if (
openingName.type === "JSXMemberExpression" &&
openingName.object.type === "JSXIdentifier" &&
openingName.object.name === "SpeechBubble"
x.value.openingElement.name.type === "JSXMemberExpression" &&
x.value.openingElement.name.object.name === "SpeechBubble"
) {
openingName.object.name = "Chat";
const closingName = x.value.closingElement?.name;
if (
closingName?.type === "JSXMemberExpression" &&
closingName.object.type === "JSXIdentifier"
) {
closingName.object.name = "Chat";
}
x.value.openingElement.name.object.name = "Chat";
x.value.closingElement.name.object.name = "Chat";
}
});
}
@@ -1,29 +1,25 @@
import type {
API,
FileInfo,
JSXAttribute,
JSXElement,
JSXSpreadAttribute,
} from "jscodeshift";
import { getJSXLiteralValue } from "../../../utils/jsx-value";
import { getLineTerminator } from "../../../utils/lineterminator";
export default function transformer(file: FileInfo, api: API) {
/**
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
export default function transformer(file, api) {
const j = api.jscodeshift;
let localName = "Pagination";
const root = j(file.source);
function addMigrationTag(node: JSXElement) {
function addMigrationTag(node) {
const attributes = node.openingElement.attributes;
const isMigrated = attributes?.find(
(attr: JSXAttribute | JSXSpreadAttribute) =>
const isMigrated = attributes.find(
(attr) =>
attr.type === "JSXAttribute" &&
attr.name.name === "data-version" &&
getJSXLiteralValue(attr.value) === "v1",
attr.value.value === "v1",
);
if (!isMigrated && attributes) {
if (!isMigrated) {
attributes.push(
j.jsxAttribute(j.jsxIdentifier("data-version"), j.literal("v1")),
);
@@ -31,14 +27,13 @@ export default function transformer(file: FileInfo, api: API) {
}
/* https://github.com/mui/material-ui/blob/master/packages/mui-codemod/src/v5.0.0/variant-prop.js */
function addExplicitStandardProp(node: JSXElement) {
function addExplicitStandardProp(node) {
const attributes = node.openingElement.attributes;
const variant = attributes?.find(
(attr: JSXAttribute | JSXSpreadAttribute) =>
attr.type === "JSXAttribute" && attr.name.name === "size",
const variant = attributes.find(
(attr) => attr.type === "JSXAttribute" && attr.name.name === "size",
);
if (!variant && attributes) {
if (!variant) {
attributes.unshift(
j.jsxAttribute(j.jsxIdentifier("size"), j.literal("small")),
);
@@ -51,45 +46,37 @@ export default function transformer(file: FileInfo, api: API) {
.find(j.ImportDeclaration)
.filter((path) => path.node.source.value === "@navikt/ds-react")
.forEach((imp) => {
imp.value.specifiers?.forEach((x) => {
imp.value.specifiers.forEach((x) => {
if (
x.type === "ImportSpecifier" &&
x.imported.type === "Identifier" &&
x.imported.name === "Pagination" &&
x.local?.name !== x.imported.name &&
x.local?.name
x.local.name !== x.imported.name
) {
localName = String(x.local.name);
localName = x.local.name;
}
});
});
if (j(file.source).findJSXElements(localName)) {
root.findJSXElements(`${localName}`).forEach((parent) => {
const skip = !!parent.value.openingElement?.attributes?.find(
(attr: JSXAttribute | JSXSpreadAttribute) =>
attr.type === "JSXAttribute" &&
attr.name.name === "data-version" &&
getJSXLiteralValue(attr.value) === "v1",
const skip = !!parent.value.openingElement?.attributes.find(
(x) => x.name.name === "data-version" && x.value.value === "v1",
);
parent.value.openingElement?.attributes?.forEach(
(x: JSXAttribute | JSXSpreadAttribute) => {
let didUpdate = false;
if (x.type === "JSXAttribute" && x.name?.name === "size" && !skip) {
const value = getJSXLiteralValue(x.value);
if (value === "medium") {
x.value = j.literal("small");
didUpdate = true;
} else if (value === "small") {
x.value = j.literal("xsmall");
didUpdate = true;
}
didUpdate && addMigrationTag(parent.value);
parent.value.openingElement?.attributes.forEach((x) => {
let didUpdate = false;
if (x.name?.name === "size" && x.type === "JSXAttribute" && !skip) {
/* addExplicitStandardProp */
if (x.value.value === "medium") {
x.value = j.literal("small");
didUpdate = true;
} else if (x.value.value === "small") {
x.value = j.literal("xsmall");
didUpdate = true;
}
},
);
didUpdate && addMigrationTag(parent.value);
}
});
addExplicitStandardProp(parent.value);
});
}
@@ -1,11 +0,0 @@
import { Pagination as DsPagination, Button } from "@navikt/ds-react";
export const Demo = () => {
return (
<div>
<DsPagination onPageChange={setPage} />
<DsPagination size={"medium"} onPageChange={setPage} />
<DsPagination size={"small"} onPageChange={setPage} />
</div>
);
};
@@ -1,11 +0,0 @@
import { Pagination as DsPagination, Button } from "@navikt/ds-react";
export const Demo = () => {
return (
<div>
<DsPagination size="small" onPageChange={setPage} data-version="v1" />
<DsPagination size="small" onPageChange={setPage} data-version="v1" />
<DsPagination size="xsmall" onPageChange={setPage} data-version="v1" />
</div>
);
};
@@ -1,7 +1,7 @@
import { check } from "../../../../utils/check";
const migration = "pagination";
const fixtures = ["size", "idempotent", "import", "literal"];
const fixtures = ["size", "idempotent", "import"];
for (const fixture of fixtures) {
check(__dirname, {
@@ -1,9 +1,12 @@
import type { API, FileInfo } from "jscodeshift";
import transformChat from "../chat/chat";
import transformPagination from "../pagination/pagination";
import transformTabs from "../tabs/tabs";
export default function transformer(file: FileInfo, api: API) {
/**
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
export default function transformer(file, api) {
file.source = transformTabs(file, api);
file.source = transformChat(file, api);
file.source = transformPagination(file, api);
@@ -1,11 +1,14 @@
import type { API, FileInfo, JSXAttribute } from "jscodeshift";
import { getLineTerminator } from "../../../utils/lineterminator";
import renameProps from "../../../utils/rename-props";
export default function transformer(file: FileInfo, api: API) {
/**
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
export default function transformer(file, api) {
const j = api.jscodeshift;
let localName = "Tabs";
let iconPositionProp: JSXAttribute | null = null;
let iconPositionProp = null;
const root = j(file.source);
@@ -14,14 +17,9 @@ export default function transformer(file: FileInfo, api: API) {
.find(j.ImportDeclaration)
.filter((path) => path.node.source.value === "@navikt/ds-react")
.forEach((imp) => {
imp.value.specifiers?.forEach((x) => {
if (
x.type === "ImportSpecifier" &&
x.imported.name === "Tabs" &&
x.local &&
x.local.name !== x.imported.name
) {
localName = String(x.local.name);
imp.value.specifiers.forEach((x) => {
if (x.imported.name === "Tabs" && x.local.name !== x.imported.name) {
localName = x.local.name;
}
});
});
@@ -36,52 +34,48 @@ export default function transformer(file: FileInfo, api: API) {
});
root.findJSXElements(`${localName}`).forEach((parent) => {
parent.value.children?.forEach((el) => {
if (el.type !== "JSXElement") return;
parent.value.children.forEach((el) => {
const openingEl = el.openingElement;
if (openingEl.name.type === "JSXMemberExpression") {
if (openingEl?.name && openingEl.name.type === "JSXMemberExpression") {
if (
openingEl.name.object.type === "JSXIdentifier" &&
openingEl.name.object.name === localName &&
openingEl.name.property.name === "List"
) {
/* Move loop-prop */
openingEl.attributes?.forEach((x, index) => {
if (x.type === "JSXAttribute" && x.name.name === "loop") {
parent.value.openingElement.attributes?.push(x);
openingEl.attributes?.splice(index, 1);
el.openingElement.attributes.forEach((x, index) => {
if (x.name?.name === "loop" && x.type === "JSXAttribute") {
parent.value.openingElement.attributes.push(x);
delete el.openingElement.attributes[index];
}
});
/* Find and move iconPosition-prop to <Tabs/> */
el.children?.forEach((tab) => {
if (tab.type !== "JSXElement") return;
el.children.forEach((tab) => {
const tabEl = tab.openingElement;
if (
tabEl.name.type === "JSXMemberExpression" &&
tabEl.name.object.type === "JSXIdentifier" &&
tabEl.name.object.name === localName &&
tabEl.name.property.name === "Tab"
) {
tabEl.attributes?.forEach((x, index) => {
if (
x.type === "JSXAttribute" &&
x.name.name === "iconPosition"
) {
if (!iconPositionProp) {
iconPositionProp = x;
if (tabEl?.name && tabEl.name.type === "JSXMemberExpression") {
if (
tabEl.name.object.name === localName &&
tabEl.name.property.name === "Tab"
) {
tabEl?.attributes.forEach((x, index) => {
if (
x.name?.name === "iconPosition" &&
x.type === "JSXAttribute"
) {
if (!iconPositionProp) {
iconPositionProp = x;
}
delete tabEl.attributes[index];
}
tabEl.attributes?.splice(index, 1);
}
});
});
}
}
});
}
}
});
if (iconPositionProp) {
parent.value.openingElement.attributes?.push(iconPositionProp);
}
iconPositionProp &&
parent.value.openingElement.attributes.push(iconPositionProp);
iconPositionProp = null;
});
}
@@ -1,7 +1,10 @@
import type { FileInfo } from "jscodeshift";
import { tokens } from "../../../tokens-map.js";
export default function transformer(file: FileInfo) {
/**
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
export default function transformer(file) {
let src = file.source;
tokens.forEach((tok) => {
@@ -1,4 +1,4 @@
import type { API, JSCodeshift } from "jscodeshift";
import type { JSCodeshift } from "jscodeshift";
import { tokens } from "../../../tokens-map.js";
import {
getImportSpecifier,
@@ -12,7 +12,7 @@ import { translateToken } from "../../../utils/translate-token";
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
export default function transformer(file: JSCodeshift, api: API) {
export default function transformer(file: JSCodeshift, api) {
const src = file.source;
const j = api.jscodeshift as JSCodeshift;
@@ -37,7 +37,7 @@ export default function transformer(file: JSCodeshift, api: API) {
getImportSpecifier(j, root, name, "@navikt/ds-tokens/dist/tokens").forEach(
(x) => {
foundName = String(x.node.imported.name);
foundName = x.node.imported.name;
},
);
@@ -1,8 +1,11 @@
import type { FileInfo } from "jscodeshift";
import { tokens } from "../../../tokens-map.js";
import { translateToken } from "../../../utils/translate-token";
export default function transformer(file: FileInfo) {
/**
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
export default function transformer(file) {
let src = file.source;
tokens.forEach((tok) => {
@@ -1,8 +1,11 @@
import type { FileInfo } from "jscodeshift";
import { tokens } from "../../../tokens-map.js";
import { translateToken } from "../../../utils/translate-token";
export default function transformer(file: FileInfo) {
/**
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
export default function transformer(file) {
let src = file.source;
tokens.forEach((tok) => {
@@ -1,13 +1,16 @@
import chalk from "chalk";
import type { API, Collection, FileInfo } from "jscodeshift";
import { getLineTerminator } from "../../../utils/lineterminator";
import moveAndRenameImport from "../../../utils/packageImports";
import removePropsFromComponent from "../../../utils/removeProps";
export default function transformer(file: FileInfo, api: API) {
/**
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
export default function transformer(file, api) {
const j = api.jscodeshift;
let root: Collection<any>;
let root: any;
try {
root = j(file.source);
} catch {
@@ -43,14 +46,13 @@ export default function transformer(file: FileInfo, api: API) {
const children = node.node.children;
let flagged = false;
if (
children &&
children.length > 0 &&
!node.node.openingElement.attributes?.some(
(attr) => attr.type === "JSXAttribute" && attr.name.name === "text",
!node.node.openingElement.attributes.some(
(attr) => attr.name.name === "text",
)
) {
if (children.length === 1 && children[0].type === "JSXText") {
node.node.openingElement.attributes?.push(
node.node.openingElement.attributes.push(
j.jsxAttribute(
j.jsxIdentifier("text"),
j.literal(children[0].value.trim()),
@@ -78,15 +80,9 @@ export default function transformer(file: FileInfo, api: API) {
});
compRoot.forEach((x) => {
const openingName = x.node.openingElement.name;
if (openingName.type === "JSXIdentifier") {
openingName.name = "CopyButton";
}
if (x.node.children && x.node.children.length > 0) {
const closingName = x.node.closingElement?.name;
if (closingName?.type === "JSXIdentifier") {
closingName.name = "CopyButton";
}
x.node.openingElement.name.name = "CopyButton";
if (x.node.children.length > 0) {
x.node.closingElement.name.name = "CopyButton";
}
});
}
@@ -1,6 +1,8 @@
import type { FileInfo } from "jscodeshift";
export default function transformer(file: FileInfo) {
/**
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
export default function transformer(file) {
const names = [
"UNSAFE_useDatepicker",
"UNSAFE_DatePicker",
@@ -1,5 +1,3 @@
import type { FileInfo } from "jscodeshift";
const cssConversions = {
".navdsi-dropdown": ".navds-dropdown",
".navdsi-header": ".navds-internalheader",
@@ -10,8 +8,11 @@ const cssConversions = {
"--ac-header-hover-bg": "--ac-internalheader-hover-bg",
"--ac-header-active-bg": "--ac-internalheader-active-bg",
};
export default function transformer(file: FileInfo) {
/**
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
export default function transformer(file) {
let src = file.source;
Object.entries(cssConversions).forEach(([key, value]) => {
@@ -1,8 +1,11 @@
import type { API, FileInfo } from "jscodeshift";
import { getLineTerminator } from "../../../utils/lineterminator";
import moveAndRenameImport from "../../../utils/packageImports";
export default function transformer(file: FileInfo, api: API) {
/**
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
export default function transformer(file, api) {
const j = api.jscodeshift;
let root: any;
@@ -1,7 +1,10 @@
import type { API, FileInfo } from "jscodeshift";
import { getLineTerminator } from "../../../utils/lineterminator";
export default function transformer(file: FileInfo, api: API) {
/**
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
export default function transformer(file, api) {
const j = api.jscodeshift;
let localName = "Chat";
@@ -12,14 +15,9 @@ export default function transformer(file: FileInfo, api: API) {
.find(j.ImportDeclaration)
.filter((path) => path.node.source.value === "@navikt/ds-react")
.forEach((imp) => {
imp.value.specifiers?.forEach((x) => {
if (x.type !== "ImportSpecifier") return;
if (
x.imported.name === "Chat" &&
x.local &&
x.local.name !== x.imported.name
) {
localName = String(x.local.name);
imp.value.specifiers.forEach((x) => {
if (x.imported.name === "Chat" && x.local.name !== x.imported.name) {
localName = x.local.name;
}
});
});
@@ -43,7 +43,7 @@ export default function transformer(file: FileInfo, api: API) {
for (const prop of propsAffected) {
findProps({ j, path: astElement, name: prop }).forEach((attr) => {
const attrvalue = attr.value.value;
if (attrvalue?.type === "StringLiteral") {
if (attrvalue.type === "StringLiteral") {
/**
* Skips if the replacement token already set
*/
@@ -65,7 +65,7 @@ export default function transformer(file: FileInfo, api: API) {
tokenComments.push(tokenComment);
}
} else if (
attrvalue?.type === "JSXExpressionContainer" &&
attrvalue.type === "JSXExpressionContainer" &&
attrvalue.expression.type === "StringLiteral"
) {
const literal = attrvalue.expression;
@@ -96,10 +96,10 @@ export default function transformer(file: FileInfo, api: API) {
findProps({ j, path: astElement, name: "borderRadius" }).forEach((attr) => {
const attrValue = attr.value.value;
if (attrValue?.type === "StringLiteral") {
if (attrValue.type === "StringLiteral") {
/* borderRadius="xlarge" */
attrValue.value = convertBorderRadiusToRadius(attrValue.value);
} else if (attrValue?.type === "JSXExpressionContainer") {
} else if (attrValue.type === "JSXExpressionContainer") {
/* borderRadius={{xs: "xlarge", sm: "large"}} */
const expression = attrValue.expression;
if (expression.type === "ObjectExpression") {
@@ -189,9 +189,7 @@ function convertBorderRadiusToRadius(oldValue: string): string {
console.warn(`Possibly invalid radius token found: ${radiusToken}\n`);
newRadius.push(radiusToken);
} else {
newRadius.push(
`${legacyBorderRadiusNameTokenLookup[radiusToken as keyof typeof legacyBorderRadiusNameTokenLookup]}`,
);
newRadius.push(`${legacyBorderRadiusNameTokenLookup[radiusToken]}`);
}
}
@@ -4,25 +4,23 @@ import type {
FileInfo,
ImportDeclaration,
JSXAttribute,
JSXElement,
JSXSpreadAttribute,
} from "jscodeshift";
import { getJSXStringValue } from "../../../utils/jsx-value";
import { getLineTerminator } from "../../../utils/lineterminator";
const headingSizeMap: Record<string, string> = {
const headingSizeMap = {
small: "xsmall",
medium: "small",
large: "medium",
};
const bodySizeMap: Record<string, string> = {
const bodySizeMap = {
small: "small",
medium: "medium",
large: "large",
};
const boxMarginMap: Record<string, string> = {
const boxMarginMap = {
small: "space-12",
medium: "space-16",
large: "space-16",
@@ -34,7 +32,7 @@ export default function transformer(file: FileInfo, api: API) {
let localListName = "List";
let hasListImport = false;
let dsReactImportPath: ASTPath<ImportDeclaration> | undefined;
let dsReactImportPath: ASTPath<ImportDeclaration> | null = null;
let localHeadingName = "Heading";
let hasHeadingImport = false;
@@ -52,19 +50,19 @@ export default function transformer(file: FileInfo, api: API) {
path.node.specifiers?.forEach((specifier) => {
if (specifier.type === "ImportSpecifier") {
if (specifier.imported.name === "List") {
localListName = String(specifier.local?.name || "List");
localListName = specifier.local?.name || "List";
hasListImport = true;
}
if (specifier.imported.name === "Heading") {
localHeadingName = String(specifier.local?.name || "Heading");
localHeadingName = specifier.local?.name || "Heading";
hasHeadingImport = true;
}
if (specifier.imported.name === "BodyShort") {
localBodyShortName = String(specifier.local?.name || "BodyShort");
localBodyShortName = specifier.local?.name || "BodyShort";
hasBodyShortImport = true;
}
if (specifier.imported.name === "Box") {
localBoxName = String(specifier.local?.name || "Box");
localBoxName = specifier.local?.name || "Box";
hasBoxImport = true;
}
}
@@ -73,7 +71,7 @@ export default function transformer(file: FileInfo, api: API) {
path.node.specifiers?.forEach((specifier) => {
if (specifier.type === "ImportSpecifier") {
if (specifier.imported.name === "List") {
localListName = String(specifier.local?.name || "List");
localListName = specifier.local?.name || "List";
hasListImport = true;
}
}
@@ -94,10 +92,6 @@ export default function transformer(file: FileInfo, api: API) {
.forEach((path) => {
const attributes = path.node.openingElement.attributes;
if (!attributes) {
return;
}
const titleAttr = attributes.find(
(attr) => attr.type === "JSXAttribute" && attr.name.name === "title",
) as JSXAttribute | undefined;
@@ -128,11 +122,15 @@ export default function transformer(file: FileInfo, api: API) {
const titleValue = titleAttr?.value;
const descValue = descAttr?.value;
const extractedSize = getJSXStringValue(sizeAttr?.value);
const sizeValue = extractedSize || "medium";
let sizeValue = "medium";
if (sizeAttr && sizeAttr.value?.type === "StringLiteral") {
sizeValue = sizeAttr.value.value;
}
const extractedHeadingAs = getJSXStringValue(headingTagAttr?.value);
const headingAs = extractedHeadingAs || "h3";
let headingAs = "h3";
if (headingTagAttr && headingTagAttr.value?.type === "StringLiteral") {
headingAs = headingTagAttr.value.value;
}
// Separate attributes
const listAttributes: JSXAttribute[] = [
@@ -170,7 +168,7 @@ export default function transformer(file: FileInfo, api: API) {
}
});
const newNodes: JSXElement[] = [];
const newNodes = [];
// Create Heading
if (titleValue) {
@@ -191,13 +189,11 @@ export default function transformer(file: FileInfo, api: API) {
);
}
let children: (ReturnType<typeof j.jsxText> | typeof titleValue)[];
let children = [];
if (titleValue.type === "StringLiteral") {
children = [j.jsxText(titleValue.value)];
} else if (titleValue.type === "JSXExpressionContainer") {
children = [titleValue];
} else {
children = [];
}
const heading = j.jsxElement(
@@ -213,7 +209,7 @@ export default function transformer(file: FileInfo, api: API) {
if (!hasBodyShortImport) {
newImports.add("BodyShort");
}
const bodyAttrs: JSXAttribute[] = [];
const bodyAttrs = [];
const mappedSize = bodySizeMap[sizeValue];
if (mappedSize && mappedSize !== "medium") {
bodyAttrs.push(
@@ -224,13 +220,11 @@ export default function transformer(file: FileInfo, api: API) {
);
}
let children: (ReturnType<typeof j.jsxText> | typeof descValue)[];
let children = [];
if (descValue.type === "StringLiteral") {
children = [j.jsxText(descValue.value)];
} else if (descValue.type === "JSXExpressionContainer") {
children = [descValue];
} else {
children = [];
}
const body = j.jsxElement(
@@ -291,8 +285,8 @@ export default function transformer(file: FileInfo, api: API) {
);
newImports.forEach((imp) => {
if (!existingSpecifiers.has(imp) && dsReactImportPath) {
dsReactImportPath.node.specifiers?.push(
if (!existingSpecifiers.has(imp)) {
dsReactImportPath!.node.specifiers?.push(
j.importSpecifier(j.identifier(imp)),
);
}
@@ -56,17 +56,17 @@ export default function transformer(file: FileInfo, api: API) {
findProps({ j, path, name: prop }).forEach((attr) => {
const attrValue = attr.value.value;
if (attrValue?.type === "StringLiteral") {
if (attrValue.type === "StringLiteral") {
/* padding="32" */
attrValue.value = convertSpacingToSpace(attrValue.value);
} else if (
attrValue?.type === "JSXExpressionContainer" &&
attrValue.type === "JSXExpressionContainer" &&
attrValue.expression.type === "StringLiteral"
) {
attrValue.expression.value = convertSpacingToSpace(
attrValue.expression.value,
);
} else if (attrValue?.type === "JSXExpressionContainer") {
} else if (attrValue.type === "JSXExpressionContainer") {
/* padding={{xs: "16", sm: "32"}} */
const expression = attrValue.expression;
if (expression.type === "ObjectExpression") {
+4 -4
View File
@@ -33,14 +33,14 @@ function findComponentImport(input: {
return;
}
path.node.specifiers?.forEach((specifier) => {
path.node.specifiers.forEach((specifier) => {
if (
specifier.type === "ImportSpecifier" &&
specifier.imported.name === name
) {
foundName = String(
specifier.local ? specifier.local.name : specifier.imported.name,
);
foundName = specifier.local
? specifier.local.name
: specifier.imported.name;
}
});
});
-1
View File
@@ -1,4 +1,3 @@
/* @ts-expect-error No avaliable types for testUtils */
import { applyTransform } from "jscodeshift/dist/testUtils";
import fs from "node:fs";
import path from "node:path";
@@ -1,88 +0,0 @@
import type { JSXAttribute, JSXExpressionContainer } from "jscodeshift";
type AttributeValue =
| JSXAttribute["value"]
| JSXExpressionContainer["expression"];
type LiteralValue = string | number | boolean;
/**
* Extracts a literal value (string, number, or boolean) from a JSX attribute value.
* Handles the following patterns:
* - `prop="value"` (StringLiteral)
* - `prop={"value"}` (JSXExpressionContainer with StringLiteral)
* - `prop={`value`}` (TemplateLiteral with no expressions)
* - `prop={42}` (NumericLiteral)
* - `prop={true}` / `prop={false}` (BooleanLiteral)
*
* @param node - The JSX attribute value node to extract the value from
* @returns The literal value if it can be extracted, otherwise null
*
* @example
* ```tsx
* getJSXLiteralValue(attr.value) // where attr is prop="hello" => "hello"
* getJSXLiteralValue(attr.value) // where attr is prop={42} => 42
* getJSXLiteralValue(attr.value) // where attr is prop={true} => true
* ```
*/
export function getJSXLiteralValue(node: AttributeValue): LiteralValue | null {
if (!node) return null;
if (node.type === "StringLiteral") {
return node.value;
}
if (node.type === "NumericLiteral") {
return node.value;
}
if (node.type === "UnaryExpression") {
const unary = node;
if (unary.operator === "-" && unary.argument.type === "NumericLiteral") {
return -unary.argument.value;
}
return null;
}
if (node.type === "BooleanLiteral") {
return node.value;
}
if (node.type === "Literal") {
const value = node.value;
if (
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean"
) {
return value;
}
return null;
}
if (node.type === "TemplateLiteral") {
const template = node;
if (template.expressions.length === 0 && template.quasis.length === 1) {
return template.quasis[0].value.cooked ?? null;
}
return null;
}
if (node.type === "JSXExpressionContainer") {
return getJSXLiteralValue(node.expression);
}
return null;
}
/**
* Extracts the string value from a JSX attribute value.
* Convenience wrapper around `getJSXLiteralValue` that only returns strings.
*
* @param node - The JSX attribute value node to extract the string from
* @returns The string value if it can be extracted, otherwise null
*/
export function getJSXStringValue(node: AttributeValue): string | null {
const value = getJSXLiteralValue(node);
return typeof value === "string" ? value : null;
}
@@ -1,10 +1,10 @@
import type {
API,
FileInfo,
JSXAttribute,
JSXExpressionContainer,
Literal,
} from "jscodeshift";
import { getJSXStringValue } from "./jsx-value";
import { getLineTerminator } from "./lineterminator";
export interface MigrationConfig {
@@ -44,7 +44,7 @@ export function moveVariantToDataColor(
specifier.type === "ImportSpecifier" &&
specifier.imported.name === rootComponent
) {
localName = String(specifier.local?.name || specifier.imported.name);
localName = specifier.local?.name || specifier.imported.name;
}
});
});
@@ -77,7 +77,7 @@ export function moveVariantToDataColor(
if (attr.type !== "JSXAttribute") return;
if (attr.name.name === config.prop) {
variantPropIndex = index;
const value = getJSXStringValue(attr.value);
const value = getStringValue(attr.value);
if (value) {
variantValue = value;
}
@@ -134,3 +134,25 @@ export function moveVariantToDataColor(
const toSourceOptions = getLineTerminator(file.source);
return root.toSource(toSourceOptions);
}
type AttributeValue =
| JSXAttribute["value"]
| JSXExpressionContainer["expression"];
function getStringValue(node: AttributeValue): string | null {
if (!node) return null;
if (node.type === "StringLiteral" || node.type === "Literal") {
return node.value as string;
}
if (
node.type === "TemplateLiteral" &&
node.expressions.length === 0 &&
node.quasis.length === 1
) {
return node.quasis[0].value.cooked ?? null;
}
if (node.type === "JSXExpressionContainer") {
return getStringValue(node.expression);
}
return null;
}
@@ -59,8 +59,8 @@ export default function moveAndRenameImport(
const existingFromImportSpecifier = existingFromImport?.find(
j.ImportSpecifier,
(node) => {
if (node.imported.name === fromName && node.local?.name) {
localname = String(node.local.name);
if (node.imported.name === fromName) {
localname = node.local.name;
}
return node.imported.name === fromName;
},
@@ -11,10 +11,6 @@ export default function removePropsFromComponent(
component.forEach((node) => {
const attributes = node.node.openingElement.attributes;
if (!attributes) {
return;
}
for (let i = attributes.length - 1; i >= 0; i--) {
const attribute = attributes[i];
@@ -1,21 +1,8 @@
import type { Collection } from "jscodeshift";
interface RenamePropsOptions {
root: Collection;
componentName: string;
props: Record<string, string>;
}
export default function renameProps({
root,
componentName,
props,
}: RenamePropsOptions) {
export default function renameProps({ root, componentName, props }) {
return root.findJSXElements(componentName).forEach((path) => {
path.node.openingElement.attributes?.forEach((node) => {
path.node.openingElement.attributes.forEach((node) => {
if (
node.type === "JSXAttribute" &&
typeof node.name.name === "string" &&
Object.keys(props).includes(node.name.name)
) {
node.name.name = props[node.name.name];
@@ -1,112 +0,0 @@
import jscodeshift from "jscodeshift";
import { describe, expect, test } from "vitest";
import { getJSXLiteralValue, getJSXStringValue } from "../jsx-value";
const j = jscodeshift.withParser("tsx");
function getAttrValue(source: string) {
const root = j(source);
const attr = root.find(j.JSXAttribute, { name: { name: "prop" } });
return attr.paths()[0]?.value.value ?? null;
}
describe("getJSXLiteralValue", () => {
test("returns null for null/undefined input", () => {
expect(getJSXLiteralValue(null)).toBe(null);
expect(getJSXLiteralValue(undefined)).toBe(null);
});
test('handles StringLiteral: prop="value"', () => {
const node = getAttrValue('<Component prop="hello" />');
expect(getJSXLiteralValue(node)).toBe("hello");
});
test("handles empty StringLiteral", () => {
const node = getAttrValue('<Component prop="" />');
expect(getJSXLiteralValue(node)).toBe("");
});
test('handles JSXExpressionContainer with StringLiteral: prop={"value"}', () => {
const node = getAttrValue('<Component prop={"hello"} />');
expect(getJSXLiteralValue(node)).toBe("hello");
});
test("handles TemplateLiteral without expressions: prop={`value`}", () => {
const node = getAttrValue("<Component prop={`hello`} />");
expect(getJSXLiteralValue(node)).toBe("hello");
});
test("returns null for TemplateLiteral with expressions", () => {
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing only
const node = getAttrValue("<Component prop={`hello ${name}`} />");
expect(getJSXLiteralValue(node)).toBe(null);
});
test("handles numeric values: prop={42}", () => {
const node = getAttrValue("<Component prop={42} />");
expect(getJSXLiteralValue(node)).toBe(42);
});
test("handles negative numbers: prop={-5}", () => {
const node = getAttrValue("<Component prop={-5} />");
expect(getJSXLiteralValue(node)).toBe(-5);
});
test("handles floating point numbers: prop={3.14}", () => {
const node = getAttrValue("<Component prop={3.14} />");
expect(getJSXLiteralValue(node)).toBe(3.14);
});
test("handles boolean true: prop={true}", () => {
const node = getAttrValue("<Component prop={true} />");
expect(getJSXLiteralValue(node)).toBe(true);
});
test("handles boolean false: prop={false}", () => {
const node = getAttrValue("<Component prop={false} />");
expect(getJSXLiteralValue(node)).toBe(false);
});
test("returns null for variable references", () => {
const node = getAttrValue("<Component prop={someVar} />");
expect(getJSXLiteralValue(node)).toBe(null);
});
test("returns null for function calls", () => {
const node = getAttrValue("<Component prop={getValue()} />");
expect(getJSXLiteralValue(node)).toBe(null);
});
test("returns null for object expressions", () => {
const node = getAttrValue('<Component prop={{ key: "value" }} />');
expect(getJSXLiteralValue(node)).toBe(null);
});
test("handles strings with special characters", () => {
const node = getAttrValue('<Component prop={"hello, world! 👋"} />');
expect(getJSXLiteralValue(node)).toBe("hello, world! 👋");
});
test("handles multiline template literal without expressions", () => {
const node = getAttrValue(`<Component prop={\`hello
world\`} />`);
expect(getJSXLiteralValue(node)).toBe("hello\nworld");
});
});
describe("getJSXStringValue", () => {
test("returns string for StringLiteral", () => {
const node = getAttrValue('<Component prop="hello" />');
expect(getJSXStringValue(node)).toBe("hello");
});
test("returns null for numeric values", () => {
const node = getAttrValue("<Component prop={42} />");
expect(getJSXStringValue(node)).toBe(null);
});
test("returns null for boolean values", () => {
const node = getAttrValue("<Component prop={true} />");
expect(getJSXStringValue(node)).toBe(null);
});
});
@@ -29,11 +29,11 @@ class TokenStatus {
status: StatusT;
constructor() {
this.status = this.initStatus();
this.initStatus();
}
initStatus() {
return {
this.status = {
css: { legacy: [], updated: [] },
scss: { legacy: [], updated: [] },
less: { legacy: [], updated: [] },
@@ -44,10 +44,6 @@ class TokenStatus {
};
}
resetStatus() {
this.status = this.initStatus();
}
add({
isLegacy,
type,
@@ -100,9 +96,11 @@ class TokenStatus {
let statusDataObj: StatusDataT;
if (type === "summary") {
statusDataObj = {
legacy: Object.values(this.status).flatMap((_status) => _status.legacy),
updated: Object.values(this.status).flatMap(
(_status) => _status.updated,
legacy: [].concat(
...Object.values(this.status).map((_status) => _status.legacy),
),
updated: [].concat(
...Object.values(this.status).map((_status) => _status.updated),
),
};
@@ -1230,7 +1230,7 @@ const colors = {
type ColorObjWithTwType = ColorObjType & {
twOld: string;
twNew?: string;
twNew: string;
};
const colorWithTailwindConversion = Object.entries(colors).reduce<
@@ -9,7 +9,7 @@ function getFrameworkRegexes({
legacy = false,
}: {
token: string;
twString?: string | null;
twString: string | null;
legacy: boolean;
}): Record<"css" | "scss" | "less" | "js" | "tailwind", RegExp | null> {
const regexes: ReturnType<typeof getFrameworkRegexes> = {
@@ -453,7 +453,7 @@ const newTokensWithoutRegex: TokensWithTailwindTag = {
type TokensWithRegexes = Record<
string,
{ tw: string | null; regexes: ReturnType<typeof getFrameworkRegexes> }
{ tw: string; regexes: ReturnType<typeof getFrameworkRegexes> }
>;
const v8TokenConfig: TokensWithRegexes = Object.entries(
@@ -107,12 +107,7 @@ export async function runTooling(
() => getStatus(filepaths, "no-print"),
);
} catch (error) {
program.error(
chalk.red(
"Error:",
error instanceof Error ? error.message : String(error),
),
);
program.error(chalk.red("Error:", error.message));
}
task = await getNextTask(currentStatus.status);
@@ -266,21 +261,15 @@ function getScopedFilesForTask(
return safeFilepaths.filter((f) => {
switch (task) {
case "css-tokens":
return !!status.css.legacy.find((config: any) => config.fileName === f);
return !!status.css.legacy.find((config) => config.fileName === f);
case "scss-tokens":
return !!status.scss.legacy.find(
(config: any) => config.fileName === f,
);
return !!status.scss.legacy.find((config) => config.fileName === f);
case "less-tokens":
return !!status.less.legacy.find(
(config: any) => config.fileName === f,
);
return !!status.less.legacy.find((config) => config.fileName === f);
case "js-tokens":
return !!status.js.legacy.find((config: any) => config.fileName === f);
return !!status.js.legacy.find((config) => config.fileName === f);
case "tailwind-tokens":
return !!status.tailwind.legacy.find(
(config: any) => config.fileName === f,
);
return !!status.tailwind.legacy.find((config) => config.fileName === f);
default:
return false;
}
@@ -372,12 +361,7 @@ async function getNextTask(status?: any): Promise<TaskName> {
} as any);
return response.task;
} catch (error) {
if (
error != null &&
typeof error === "object" &&
"isTtyError" in error &&
error.isTtyError
) {
if (error.isTtyError) {
console.info(
"Oops, something went wrong! Looks like @navikt/aksel can't run in this terminal. " +
"Contact Aksel for support if this persists, or try another terminal.",
@@ -190,10 +190,7 @@ async function printRemaining(files: string[], status?: TokenStatus["status"]) {
clipboardy.writeSync(JSON.stringify(jsonOutput, null, 2));
console.info("✅ Report (JSON) copied to clipboard!");
} catch (error) {
console.error(
"❌ Failed to copy to clipboard:",
error instanceof Error ? error.message : String(error),
);
console.error("❌ Failed to copy to clipboard:", error.message);
}
}
}
@@ -29,7 +29,7 @@ function getStatus(
progressBar.start(files.length, 0);
}
StatusStore.resetStatus();
StatusStore.initStatus();
/**
* Prepare search terms for legacy and v8 tokens.
@@ -39,7 +39,7 @@ export default function transformer(file: FileInfo, api: API) {
oldJsVar,
"@navikt/ds-tokens/dist/tokens",
).forEach((x) => {
foundName = x.node.imported.name as string;
foundName = x.node.imported.name;
});
if (!foundName) {
@@ -14,7 +14,7 @@ export default function transformer(file: FileInfo) {
let src = file.source;
for (const config of Object.values(legacyTokenConfig)) {
if (config.replacement.length > 0 && config.regexes.less) {
if (config.replacement.length > 0) {
src = src.replace(
config.regexes.less,
translateToken(`--ax-${config.replacement}`, "less"),
@@ -14,7 +14,7 @@ export default function transformer(file: FileInfo) {
let src = file.source;
for (const config of Object.values(legacyTokenConfig)) {
if (config.replacement.length > 0 && config.regexes.scss) {
if (config.replacement.length > 0) {
src = src.replace(
config.regexes.scss,
translateToken(`--ax-${config.replacement}`, "scss"),
-214
View File
@@ -1,214 +0,0 @@
import type { SupportedCodemodExtensions } from "./codemod/codeshift.utils";
const JS_EXTENSIONS = [
"js",
"jsx",
"ts",
"tsx",
] satisfies SupportedCodemodExtensions[];
const CSS_EXTENSIONS = [
"css",
"scss",
"less",
] satisfies SupportedCodemodExtensions[];
type MigrationT = Record<
string,
{
description: string;
value: string;
path: string;
warning?: string;
/**
* Some transforms are only meant to run on specific file extensions.
* This is used to filter out files that should not be transformed.
*/
ignoredExtensions: SupportedCodemodExtensions[];
}[]
>;
const migrations: MigrationT = {
v1: [
{
description: "Runs all codemods for beta -> v1 migration",
value: "v1-preset",
path: "v1.0.0/preset/preset",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description: "Fixes breaking API-changes for <Pagination /> component",
value: "v1-pagination",
path: "v1.0.0/pagination/pagination",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description: "Fixes breaking API-changes for <Tabs /> component",
value: "v1-tabs",
path: "v1.0.0/tabs/tabs",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Fixes breaking API-changes for <SpeechBubble /> (now <Chat/>) component",
value: "v1-chat",
path: "v1.0.0/chat/chat",
ignoredExtensions: CSS_EXTENSIONS,
},
],
v2: [
{
description: "Patches changed css-variables",
value: "v2-css",
path: "v2.0.0/update-css-tokens/update-css-tokens",
ignoredExtensions: [],
},
{
description: "Patches changed js-variables",
value: "v2-js",
path: "v2.0.0/update-js-tokens/update-js-tokens",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description: "Patches changed sass-variables",
value: "v2-sass",
path: "v2.0.0/update-sass-tokens/update-sass-tokens",
ignoredExtensions: JS_EXTENSIONS,
},
{
description: "Patches changed less-variables",
value: "v2-less",
path: "v2.0.0/update-less-tokens/update-less-tokens",
ignoredExtensions: JS_EXTENSIONS,
},
],
v3: [
{
description:
"Replaces deprecated <CopyToClipboard /> with <CopyButton />",
value: "v3-copybutton",
path: "v3.0.0/copybutton/copybutton",
warning:
"Remember to remove css-import from '@navikt/ds-css-internal' if no longer needed\nIf non-text was used as children, or different locales were handled, you need to manually fix this",
ignoredExtensions: CSS_EXTENSIONS,
},
],
v4: [
{
description:
"Replaced deprecated 'internal'-component import to 'core'-imports",
value: "v4-internal-react",
path: "v4.0.0/internal-react/internal-react",
warning:
"Remember to also run 'v4-internal-css' if you have overwritten any css-classes!",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Replaced 'navdsi'-prefixes used to overwrite internal-css to 'navds'-prefixes",
value: "v4-internal-css",
path: "v4.0.0/internal-css/internal-css",
warning:
"Remember to remove '@navikt/ds-css-internal' imports if no longer needed! Also run 'v4-internal-react' to convert internal-react imports to core-imports",
ignoredExtensions: [],
},
{
description:
"Removes all instances of UNSAFE_ for datepicker/monthpicker.",
value: "v4-date",
path: "v4.0.0/date/date",
ignoredExtensions: CSS_EXTENSIONS,
},
],
v6: [
{
description:
"Removes `backgroundColor` and `avatarBgColor` properties from `Chat` and `Chat.Bubble`",
value: "v6-chat",
path: "v6.0.0/chat/chat",
warning:
"Remember to update use of `variant`-prop to match previous use of colors. If needed the component exposes css-variables for custom overrides",
ignoredExtensions: CSS_EXTENSIONS,
},
],
v8: [
{
description:
"Updates Box with legacy-tokens to Box using the new token system, and renames already migrated BoxNew/Box.New instances to Box.",
value: "v8-box",
path: "v8.0.0/box/box",
warning:
"Remember to check if 'TODO: Aksel Box migration'-comment was added to any files after migration. This comment will help you find and update Box-instances we couldn't update for you.",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description: "Renames already migrated BoxNew/Box.New instances to Box.",
value: "v8-box-new",
path: "v8.0.0/box-new/box-new",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description: "Removes deprecated props from components.",
value: "v8-prop-deprecate",
path: "v8.0.0/prop-deprecate/prop-deprecate",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Tries to migrate List component by wrapping it with 'Box' and moving 'title' and 'description' props to 'Heading' and 'BodyShort'.",
value: "v8-list",
path: "v8.0.0/list/list",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description: "Updates all Primitives to use new `space`-tokens.",
value: "v8-primitive-spacing",
path: "v8.0.0/primitives-spacing/spacing",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Updates variant + data-color props on Tag based on current variant prop.",
value: "v8-tag-variant",
path: "v8.0.0/tag-variant/tag-variant",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Updates variant + data-color props on ToggleGroup based on current variant prop.",
value: "v8-toggle-group-variant",
path: "v8.0.0/toggle-group-variant/toggle-group-variant",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Updates variant + data-color props on Accordion based on current variant prop.",
value: "v8-accordion-variant",
path: "v8.0.0/accordion-variant/accordion-variant",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Updates variant + data-color props on Chips based on current variant prop.",
value: "v8-chips-variant",
path: "v8.0.0/chips-variant/chips-variant",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Updates variant + data-color props on Button based on current variant prop.",
value: "v8-button-variant",
path: "v8.0.0/button-variant/button-variant",
ignoredExtensions: CSS_EXTENSIONS,
},
{
description:
"Updates variant + data-color props on Link based on current variant prop.",
value: "v8-link-variant",
path: "v8.0.0/link-variant/link-variant",
ignoredExtensions: CSS_EXTENSIONS,
},
],
} as const;
export { migrations };
+1 -2
View File
@@ -4,9 +4,8 @@
"sourceMap": false,
"esModuleInterop": true,
"target": "es2015",
"lib": ["ES2021"],
"preserveWatchOutput": true,
"declaration": true,
"declaration": false,
"types": ["node"],
"outDir": "dist",
"rootDir": "./src",
-64
View File
@@ -1,69 +1,5 @@
# @navikt/ds-css
## 8.10.2
### Patch Changes
- Dialog: Now aligns to bottom when centered at 480px instead of 768px screen width ([#4831](https://github.com/navikt/aksel/pull/4831))
## 8.10.1
### Patch Changes
- Modal: Changing `size` had no effect before ([#4823](https://github.com/navikt/aksel/pull/4823))
## 8.10.0
### Minor Changes
- DatePicker: Highlight days between start and hovered day ([#4801](https://github.com/navikt/aksel/pull/4801))
### Patch Changes
- Skeleton: Text-variant now has properly rounded edges ([#4808](https://github.com/navikt/aksel/pull/4808))
- ExpandableRow: Fix selected row top border not showing when row above was collapsed ([#4810](https://github.com/navikt/aksel/pull/4810))
- Timeline: Icons in periods now scale based on avaliable space. No longer clips on smaller periods ([#4809](https://github.com/navikt/aksel/pull/4809))
- DatePicker, MonthPicker: Change styling on disabled days/months ([#4801](https://github.com/navikt/aksel/pull/4801))
- InfoCard: Align header with content when no icon is present for small version. ([#4815](https://github.com/navikt/aksel/pull/4815))
- InfoCard, LocalAlert, GlobalAlert: Update content typography use and sizing. ([#4815](https://github.com/navikt/aksel/pull/4815))
## 8.9.1
### Patch Changes
- Button: Use correct text color when inherited `data-color` is "neutral" ([#4772](https://github.com/navikt/aksel/pull/4772))
- Checkbox, Radio: Internal updates. ([#4760](https://github.com/navikt/aksel/pull/4760))
- Datepicker, Monthpicker: Sync background on DateInput button to Input. ([#4781](https://github.com/navikt/aksel/pull/4781))
## 8.9.0
### Minor Changes
- InternalHeader: Added `isActive`-prop to `InternalHeader.Button`. ([#4761](https://github.com/navikt/aksel/pull/4761))
### Patch Changes
- DatePicker, MonthPicker: Make open button more prominent ([#4751](https://github.com/navikt/aksel/pull/4751))
## 8.8.0
### Minor Changes
- ReadMore: Added variant `moderate`. ([#4723](https://github.com/navikt/aksel/pull/4723))
- InfoCard: New sub-component InfoCard.Message. ([#4717](https://github.com/navikt/aksel/pull/4717))
### Patch Changes
- GlobalAlert, LocalAlert: Content outline changed from 4px to 2px. ([#4745](https://github.com/navikt/aksel/pull/4745))
## 8.7.0
### Minor Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "8.10.2",
"version": "8.7.0",
"description": "CSS for Nav Designsystem",
"author": "Aksel | Nav designsystem team",
"keywords": [
@@ -30,9 +30,9 @@
"css:get-version": "node config/get-version.js"
},
"devDependencies": {
"@navikt/ds-tokens": "^8.10.2",
"@navikt/ds-tokens": "^8.7.0",
"browserslist": "^4.25.0",
"esbuild": "^0.27.4",
"esbuild": "^0.27.2",
"fast-glob": "3.3.3",
"lightningcss": "^1.30.1",
"normalize.css": "^8.0.1",
+2 -35
View File
@@ -114,16 +114,11 @@
&[data-variant="moderate"] {
outline: 1px solid var(--ax-border-default);
outline-offset: -1px;
/* Adjust horizontal padding for small moderate alerts */
&[data-size="small"] {
--__axc-base-alert-pi: var(--ax-space-16);
}
}
&[data-variant="strong"] {
outline: 2px solid var(--ax-border-default);
outline-offset: -2px;
outline: 4px solid var(--ax-border-default);
outline-offset: -4px;
&[data-color="neutral"] {
outline-color: var(--ax-border-strong);
@@ -150,9 +145,6 @@
background: var(--ax-bg-moderate);
color: var(--ax-text-default);
border-color: var(--ax-border-subtleA);
/* Optical alignment */
padding-top: var(--ax-space-1);
}
.aksel-base-alert[data-variant="strong"] & {
@@ -180,10 +172,6 @@
padding: var(--__axc-base-alert-content-p);
background-color: var(--ax-bg-default);
&[data-color] {
color: var(--ax-text-neutral);
}
.aksel-base-alert[data-global="true"] & {
padding-inline: var(--__axc-global-alert-alignment);
}
@@ -204,24 +192,3 @@
color: var(--ax-text-neutral-contrast);
}
}
.aksel-base-alert__message {
border-radius: var(--ax-radius-12);
height: fit-content;
outline: 1px solid var(--ax-border-default);
outline-offset: -1px;
background: var(--ax-bg-moderate);
color: var(--ax-text-default);
display: flex;
align-items: flex-start;
gap: var(--ax-space-8);
padding: var(--ax-space-12) var(--ax-space-16);
& > .aksel-base-alert__icon {
margin-top: var(--ax-space-2);
}
.aksel-base-alert[data-size="small"] & > .aksel-base-alert__icon {
margin-top: 0;
}
}
+4 -10
View File
@@ -44,9 +44,7 @@
background-color: transparent;
color: var(--ax-text-subtle);
&[data-color="neutral"],
[data-color="neutral"] > &:not([data-color]),
[data-color="neutral"] :not([data-color]) &:not([data-color]) {
&[data-color="neutral"] {
color: var(--ax-text-default);
}
@@ -64,8 +62,7 @@
color: var(--ax-text-subtle);
background-color: transparent;
&[data-color="neutral"],
[data-color="neutral"] > &:not([data-color]) {
&[data-color="neutral"] {
color: var(--ax-text-default);
}
}
@@ -75,9 +72,7 @@
background-color: transparent;
color: var(--ax-text-subtle);
&[data-color="neutral"],
[data-color="neutral"] > &:not([data-color], [data-pressed="true"]),
[data-color="neutral"] :not([data-color]) &:not([data-color], [data-pressed="true"]) {
&[data-color="neutral"] {
color: var(--ax-text-default);
}
@@ -98,8 +93,7 @@
color: var(--ax-text-subtle);
background-color: transparent;
&[data-color="neutral"],
[data-color="neutral"] > &:not([data-color]) {
&[data-color="neutral"] {
color: var(--ax-text-default);
}
}
+2
View File
@@ -95,6 +95,8 @@
padding-left: var(--ax-space-6);
}
/* -------------------------- start old CSS -------------------------- */
/* ----------------------------- Chips removable ---------------------------- */
.aksel-chips__removable {
gap: 0;
+82 -284
View File
@@ -1,4 +1,3 @@
/* stylelint-disable csstools/value-no-unknown-custom-properties */
/* stylelint-disable no-duplicate-selectors */
.aksel-data-table__border-wrapper {
@@ -49,70 +48,41 @@
background-color: var(--ax-bg-neutral-softA);
}
}
&[data-truncate-content="true"] {
.aksel-data-table__td > div {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
}
.aksel-data-table__thead {
background-color: var(--ax-bg-raised);
.aksel-data-table[data-loading="true"] &::after {
content: "";
position: absolute;
block-size: 2px;
inset-block-end: -2px;
background-color: var(--ax-bg-strong);
animation: animateDataTableLoading 1s linear infinite;
z-index: 11;
}
position: sticky;
top: 0;
}
.aksel-data-table__tbody {
background-color: var(--ax-bg-raised);
position: relative;
&[inert] {
transition: opacity 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
opacity: var(--ax-opacity-disabled);
@starting-style {
opacity: 0.001;
}
}
&[inert]::after {
content: "";
position: absolute;
inset: 0;
background-color: var(--ax-bg-overlay);
transition: opacity 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
z-index: 10;
opacity: 0.2;
/* iOS 26+: Ensure the backdrop covers the entire visible viewport. */
@starting-style {
opacity: 0.001;
}
}
}
.aksel-data-table__tr {
&[data-selected="true"] {
background-color: var(--ax-bg-softA);
.aksel-data-table__tr--selected {
background-color: var(--ax-bg-softA);
box-shadow:
inset 1px 0 0 0 var(--ax-border-default),
inset -1px 0 0 0 var(--ax-border-default);
&:first-of-type {
box-shadow:
inset 1px 0 0 0 var(--ax-border-default),
inset -1px 0 0 0 var(--ax-border-default),
inset 0 1px 0 0 var(--ax-border-default);
}
.aksel-data-table__thead &[data-sticky="true"] {
position: sticky;
top: 0;
z-index: 3;
&::after {
content: "";
position: absolute;
inset-inline: 0;
bottom: -1px;
pointer-events: none;
border-bottom: 1px solid var(--ax-border-neutral-subtle);
}
& :is(.aksel-data-table__td, .aksel-data-table__th) {
border-color: var(--ax-border-default);
}
}
@@ -130,7 +100,6 @@
pointer-events: none;
border: 1px solid var(--ax-border-neutral-subtle);
border-radius: var(--ax-radius-12);
z-index: 3;
}
}
@@ -142,32 +111,16 @@
}
.aksel-data-table__thead {
position: relative; /* Shadow ends up behind loading/empty-state without, consider after-element instead like with sticky */
box-shadow: 0 1px 0 0 var(--ax-border-neutral-subtle);
}
.aksel-data-table__cell {
.aksel-data-table__th,
.aksel-data-table__td {
/* border doesn't work on sticky thead when we have collapsed borders, so using box-shadow instead */
box-shadow: inset 1px 1px 0 0 var(--ax-border-neutral-subtle);
text-align: start;
vertical-align: middle;
scroll-margin: var(--ax-space-0);
padding: 0;
&[data-nested="true"] {
position: relative;
& > div {
margin-inline-start: calc(32px + 8px + calc(var(--__axc-data-table-nested-depth) * var(--ax-space-16)));
}
}
.aksel-data-table__thead & {
background-color: var(--ax-bg-neutral-soft);
}
&[data-align="left"] {
text-align: start;
text-align: left;
}
&[data-align="center"] {
@@ -175,90 +128,18 @@
}
&[data-align="right"] {
text-align: end;
}
&[data-sticky] {
position: sticky;
z-index: 1;
&::after {
content: "";
position: absolute;
inset-block: 0;
pointer-events: none;
border-color: var(--ax-border-neutral-subtle);
border-style: solid;
border-width: 0;
}
&[data-sticky="start"] {
left: 0;
&::after {
inset-inline: 0 -1px;
border-right-width: 1px;
}
}
&[data-sticky="end"] {
right: 0;
}
background-color: var(--ax-bg-raised);
.aksel-data-table__thead & {
background-color: var(--ax-bg-neutral-soft);
}
}
&:focus-visible,
/* Focus instead of focus-visible so that effect of sort-icon staying isnt strange */
/* TODO: Update from th to generic */
&:has(.aksel-data-table__th-sort-button:focus) {
outline: 2px solid var(--ax-border-focus);
outline-offset: -4px;
}
&[data-selectable="true"] {
padding: 0;
& > div {
display: grid;
place-content: center;
}
}
--__axc-data-table-pi-cell: var(--ax-space-16);
&:not([data-selectable="true"]) > div {
padding-block: var(--__axc-data-table-density);
padding-inline: var(--__axc-data-table-pi-cell);
.aksel-data-table[data-truncate-content="true"] & {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
text-align: right;
}
}
/* BORDER STYLING END */
.aksel-data-table__column-header {
/* TODO: Might want this to be dynamic */
--__axc-data-table-density: var(--ax-space-8);
.aksel-data-table__th {
position: relative;
font-weight: var(--ax-font-weight-bold);
&:has(.aksel-data-table__th-sort-button:hover) {
background-color: var(--ax-bg-neutral-moderate-hover);
}
&:has(.aksel-data-table__th-sort-button:active) {
background-color: var(--ax-bg-neutral-moderate-pressed);
}
background-color: var(--ax-bg-neutral-soft);
color: var(--ax-text-neutral);
padding: var(--ax-space-8) var(--ax-space-16);
text-align: start;
&[data-sortable="true"] {
padding: 0;
@@ -270,14 +151,21 @@
display: flex;
align-items: center;
gap: var(--ax-space-2);
padding-inline-start: var(--ax-space-16);
block-size: calc(var(--__axc-data-table-density) + 1.5rem);
width: 100%;
cursor: pointer;
text-align: start;
background-color: transparent;
border: none;
font-weight: inherit;
padding: 0;
&:hover {
background-color: var(--ax-bg-neutral-moderate-hover);
}
&:active {
background-color: var(--ax-bg-neutral-moderate-pressed);
}
&:focus-visible {
outline: none;
@@ -312,7 +200,7 @@
}
.aksel-data-table__th-resize-handle {
z-index: 1;
z-index: 2;
position: absolute;
cursor: col-resize;
margin: 0;
@@ -343,13 +231,6 @@
transition: background 50ms linear;
}
&:hover,
&:focus-visible,
&:active {
/* Avoids clipping when having sticky columns */
z-index: 3;
}
&:hover::after {
/* TODO: Consider if the resize-handle should be visible already when hovering the column header */
background: var(--ax-bg-strong);
@@ -360,7 +241,7 @@
background: var(--ax-bg-strong-pressed);
}
.aksel-data-table__column-header:last-of-type & {
.aksel-data-table__th:last-of-type & {
inset-inline-end: 0;
width: 15px;
@@ -418,6 +299,18 @@
}
}
.aksel-data-table__td {
text-align: start;
vertical-align: middle;
color: var(--ax-text-neutral-subtle);
> div {
padding-block: var(--__axc-data-table-density);
padding-inline: 1rem;
}
}
.aksel-data-table__td,
.aksel-data-table__th {
scroll-margin-block: var(--ax-space-0);
scroll-margin-inline: var(--ax-space-0);
@@ -428,15 +321,6 @@
outline: 2px solid var(--ax-border-focus);
outline-offset: -4px;
}
&[data-selectable="true"] {
padding: 0;
& > div {
display: grid;
place-content: center;
}
}
}
.aksel-data-table__tfoot {
@@ -449,35 +333,15 @@
padding: 0;
}
.aksel-data-drag-and-drop__drag-handler__button {
height: 1.5rem;
position: relative;
background: transparent;
border: none;
padding: 0;
&:hover {
cursor: grab;
}
&:focus-visible,
&[data-drag-handler-active="true"] {
outline: 2px solid var(--ax-border-focus);
outline-offset: -1px;
border-radius: var(--ax-radius-8);
}
}
.aksel-data-table__drag-and-drop-item {
background-color: var(--ax-bg-default);
&[data-dragging="true"] {
background: var(--ax-bg-default);
}
&[data-overlay="true"] {
width: 100%;
&[data-mouse-dragging="true"]:not(:active) {
opacity: 0.8;
box-shadow: var(--ax-shadow-dialog);
}
/* TODO - might be removable */
&[data-keyboard-dragging="true"] {
.aksel-data-drag-and-drop__drag-handler__button {
outline: 2px solid var(--ax-border-focus);
@@ -486,110 +350,44 @@
}
}
&[data-drop-target="true"]:not([data-overlay="true"]) {
&[data-drop-target="true"] {
background: var(--ax-bg-neutral-moderate);
}
}
.aksel-data-drag-and-drop__drag-handler__alt,
.aksel-data-drag-and-drop__drag-handler {
position: relative;
.aksel-data-drag-and-drop__drag-handler__button {
cursor: grab;
height: 1.5rem;
position: relative;
background: transparent;
border: none;
padding: 0;
&:focus-visible {
outline: 2px solid var(--ax-border-focus);
outline-offset: -1px;
border-radius: var(--ax-radius-8);
}
}
.aksel-data-drag-and-drop__drag-handler__arrow {
height: 1.2rem;
}
.aksel-data-drag-and-drop__drag-handler__alt {
position: relative;
.aksel-data-drag-and-drop__drag-handler__arrow {
height: 1.8rem;
position: absolute;
left: 50%;
transform: translate(-50%, -50%);
background: var(--ax-bg-default);
border: none;
cursor: pointer;
padding: 0;
border-radius: 50%;
z-index: 1;
&[data-direction="up"] {
top: -18px;
top: -10px;
}
&[data-direction="down"] {
bottom: calc(-100% - 22px);
}
&:disabled {
opacity: 0.8;
cursor: not-allowed;
bottom: calc(-100% - 6px);
}
}
}
.aksel-data-table__empty-state {
display: grid;
place-content: center;
padding-block: var(--ax-space-32);
padding-inline: var(--ax-space-16);
text-align: center;
gap: var(--ax-space-8);
justify-items: center;
}
.aksel-data-table__loading-state {
display: grid;
place-content: center;
padding-block: var(--ax-space-32);
padding-inline: var(--ax-space-16);
text-align: center;
gap: var(--ax-space-8);
justify-items: center;
position: relative;
}
@keyframes animateDataTableLoading {
0% {
inset-inline-start: 0;
inline-size: 0;
}
20% {
inset-inline-start: 0;
inline-size: 40%;
}
80% {
inset-inline-start: 60%;
inline-size: 40%;
}
100% {
inset-inline-start: 100%;
inline-size: 0;
}
}
/* TODO: Look to handle "inline"-checkbox better than custom CSS-overrides in the future. */
.aksel-data-table tr .aksel-checkbox {
padding: 0;
}
.aksel-data-table tr .aksel-checkbox:focus-within::after {
inset: 0;
}
/* TODO -
* This is needed to prevent the cursor from changing back to default when dragging over interactive elements in the row.
* Not sure if this is the best approach or breaks styling elsewhere */
html[data-dragging-cursor="true"],
html[data-dragging-cursor="true"] *,
html[data-dragging-cursor="true"] *::before,
html[data-dragging-cursor="true"] *::after {
cursor: grabbing !important;
}
/* TODO: alignment not optimal when resizing column with nesting */
.aksel-data-table__nested-toggle {
position: absolute;
inset-block: 0;
left: calc(var(--__axc-data-table-pi-cell) + var(--__axc-data-table-nested-depth) * var(--ax-space-16));
display: flex;
align-items: center;
}
+9 -78
View File
@@ -1,105 +1,36 @@
/* .aksel-property-filter {
flex: 1 1 fit-content;
max-inline-size: 100%;
} */
.aksel-property-filter__popup {
overflow: hidden;
border-radius: var(--ax-radius-8);
border-radius: var(--ax-radius-12);
border: 1px solid var(--ax-border-neutral-subtleA);
box-shadow: var(--ax-shadow-dialog);
/* stylelint-disable-next-line csstools/value-no-unknown-custom-properties */
transform-origin: var(--__axc-floating-transform-origin);
transition: opacity 150ms ease allow-discrete;
transition:
transform 150ms ease allow-discrete,
opacity 150ms ease allow-discrete;
@starting-style {
opacity: 0.6;
opacity: 0.8;
transform: scale3d(0.94, 0.94, 0.94);
}
min-width: 260px;
}
.aksel-property-filter__popup-inner {
--__axc-token-filter-content-p: var(--ax-space-8);
--__axc-token-filter-item-pr: var(--ax-space-8);
--__axc-token-filter-item-pl: var(--ax-space-8);
--__axc-token-filter-item-height: 2rem;
border-radius: var(--ax-radius-8);
border-radius: var(--ax-radius-12);
background-color: var(--ax-bg-raised);
min-width: 128px;
max-width: min(95vw, 640px);
animation-duration: 160ms;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
padding: var(--__axc-token-filter-content-p);
overflow: auto;
/* stylelint-disable-next-line csstools/value-no-unknown-custom-properties */
max-height: calc(var(--__axc-floating-available-height) - 4px);
}
.aksel-property-filter__input {
max-inline-size: 640px;
}
.aksel-property-filter__chips {
display: flex;
flex-wrap: wrap;
gap: var(--ax-space-8);
margin: 0;
margin-block-start: var(--ax-space-12);
padding: 0;
font-size: var(--ax-font-size-medium);
}
.aksel-property-filter__chip {
display: flex;
gap: var(--ax-space-1);
}
.aksel-property-filter__chip-button {
display: flex;
align-items: center;
gap: var(--ax-space-4);
padding: var(--ax-space-4) var(--ax-space-8);
background-color: var(--ax-bg-strong);
color: var(--ax-text-contrast);
border: none;
cursor: pointer;
&:hover {
background-color: var(--ax-bg-strong-hover);
}
&:active {
background-color: var(--ax-bg-strong-pressed);
}
&:focus-visible {
outline: 3px solid var(--ax-border-focus);
outline-offset: 2px;
z-index: 1;
}
&:first-of-type {
border-start-start-radius: var(--ax-radius-8);
border-end-start-radius: var(--ax-radius-8);
}
&:last-child {
border-start-end-radius: var(--ax-radius-8);
border-end-end-radius: var(--ax-radius-8);
}
&[data-type="operation"] {
/* background-color: var(--ax-bg-default);
box-shadow: inset 0 0 0 1px var(--ax-bg-strong);
color: var(--ax-text-default); */
color: var(--ax-text-neutral);
background-color: var(--ax-bg-neutral-moderate-pressed);
}
/* &[data-type="value"] {
} */
&[data-type="remove"] {
padding-inline: var(--ax-space-4);
}
}
+12 -23
View File
@@ -1,29 +1,18 @@
.aksel-data-toolbar {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: var(--ax-space-8);
}
.aksel-data-toolbar__actions {
margin-inline-start: auto;
padding: var(--ax-space-12);
display: flex;
align-items: center;
gap: var(--ax-space-8);
border: 1px solid var(--ax-border-neutral);
border-radius: var(--ax-radius-12);
background-color: var(--ax-bg-raised);
}
> :not(:last-child) {
border-right: 1px solid var(--ax-border-neutral-subtle);
margin-inline-end: var(--ax-space-4);
padding-inline-end: var(--ax-space-4);
.aksel-data-toolbar__toggle-button {
&[aria-pressed="true"] {
--__axc-button-border-width: 0px; /* ??? */
color: var(--ax-text-contrast);
background-color: var(--ax-bg-strong-pressed);
}
}
.aksel-data-toolbar__input {
flex: 1 1 fit-content;
max-inline-size: 100%;
}
.aksel-data-toolbar__pagination {
display: flex;
align-items: center;
gap: var(--ax-space-8);
}
+52 -89
View File
@@ -4,11 +4,11 @@
padding: var(--ax-space-16);
.rdp-table {
border-collapse: collapse;
border-collapse: separate; /* Some CSS-resets sets border-collapse:collapse on tables */
}
.rdp-cell {
padding: 0;
padding: 1px; /* Some CSS-resets sets padding:0 on all items */
&[data-outside="true"] {
width: var(--__axc-date-button-size);
@@ -23,22 +23,18 @@
}
.rdp-day_range_middle {
&.rdp-day_disabled .rdp-day__inner {
&.rdp-day_disabled {
color: var(--ax-text-neutral);
background: var(--ax-bg-neutral-moderateA);
}
&.rdp-day_selected .rdp-day__inner {
&.rdp-day_selected {
background-color: var(--ax-bg-moderate-pressedA);
box-shadow: inset 0 0 0 1px var(--ax-border-subtleA);
color: var(--ax-text-neutral);
}
}
.rdp-button.rdp-day__range-hover-middle .rdp-day__inner {
background-color: var(--ax-bg-moderate-hoverA);
}
.rdp-month,
&.rdp-month {
display: grid;
@@ -56,30 +52,22 @@
.rdp-button {
all: unset;
padding: var(--ax-space-2);
cursor: pointer;
& .rdp-day__inner {
width: var(--__axc-date-button-size);
height: var(--__axc-date-button-size);
}
}
.rdp-day__inner {
display: grid;
place-items: center;
display: block;
width: var(--__axc-date-button-size);
height: var(--__axc-date-button-size);
text-align: center;
border-radius: var(--ax-radius-8);
}
.rdp-day_range_start .rdp-day__inner {
.rdp-day_range_start {
border-radius: 100% var(--ax-radius-12) var(--ax-radius-12) 100%;
}
.rdp-day_range_end:not(.rdp-day_range_start) .rdp-day__inner {
.rdp-day_range_end:not(.rdp-day_range_start) {
border-radius: var(--ax-radius-8) 100% 100% var(--ax-radius-8);
}
.rdp-day_range_start.rdp-day_range_end .rdp-day__inner {
.rdp-day_range_start.rdp-day_range_end {
border-radius: var(--ax-radius-8);
}
@@ -90,23 +78,24 @@
position: relative;
}
.rdp-day_selected .rdp-day__inner {
.rdp-day_selected,
.aksel-monthpicker__month--selected {
color: var(--ax-text-contrast);
background: var(--ax-bg-strong-pressed);
cursor: pointer;
}
.rdp-day_disabled .rdp-day__inner {
.rdp-day_disabled {
cursor: not-allowed;
text-decoration: line-through;
background-color: var(--ax-bg-neutral-moderateA);
color: var(--ax-text-neutral-subtle);
opacity: var(--ax-opacity-disabled);
}
.rdp-button:where(:not(.rdp-day_selected, [disabled])):hover,
.aksel-date__month-button:where(:not(.rdp-day_selected, [disabled])):hover {
.rdp-day__inner {
background: var(--ax-bg-moderate-hoverA);
}
background: var(--ax-bg-moderate-hoverA);
cursor: pointer;
}
.rdp-day_today {
@@ -165,7 +154,7 @@
justify-content: space-between;
align-items: center;
gap: var(--ax-space-4);
padding-inline: var(--ax-space-2);
padding-inline: var(--ax-space-4);
}
.aksel-date__caption-button {
@@ -178,17 +167,15 @@
text-transform: capitalize;
}
/* MonthPicker */
/* Monthpicker */
.aksel-date__month-button {
all: unset;
padding: var(--ax-space-2);
text-align: center;
width: 3rem;
height: 3rem;
text-transform: capitalize;
border-radius: var(--ax-radius-8);
cursor: pointer;
& .rdp-day__inner {
width: 3rem;
height: 3rem;
text-transform: capitalize;
}
}
.aksel-date__year-label {
@@ -203,12 +190,12 @@
.rdp-cell > button.rdp-day,
button.aksel-date__month-button {
&:focus-visible .rdp-day__inner {
&:focus-visible {
outline: 3px solid var(--ax-border-focus);
outline-offset: 3px;
}
&:active:not(:disabled) .rdp-day__inner {
&:active:not(:disabled) {
color: var(--ax-text-contrast);
background-color: var(--ax-bg-strong-pressed);
}
@@ -216,41 +203,35 @@
}
.aksel-date__field-input {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: none;
&:focus-visible {
outline: none;
&.aksel-text-field__input {
padding-right: var(--ax-space-48);
}
.aksel-date__field--error &:not(:disabled) {
box-shadow:
inset -2px 0 0 0 var(--ax-border-danger-strong),
inset 0 0 0 1px var(--ax-border-danger-strong);
.aksel-form-field--small & {
padding-right: var(--ax-space-32);
}
}
.aksel-date__field-button {
--__axc-button-border-width: 1px;
--__axc-button-border-color: var(--ax-border-neutral);
position: absolute;
right: 0.0625rem;
top: 50%;
transform: translateY(-50%);
color: var(--ax-text-neutral);
display: inline-flex;
cursor: pointer;
margin: 0;
text-decoration: none;
border: none;
background: none;
border-radius: calc(var(--ax-radius-8) - 1px);
padding: var(--ax-space-12);
align-items: center;
justify-content: center;
font-size: 1.5rem;
height: calc(100% - 0.125rem);
border-start-start-radius: 0;
border-end-start-radius: 0;
position: relative;
&:focus-visible {
outline: 3px solid var(--ax-border-focus);
outline-offset: 3px;
}
.aksel-date__field:not(.aksel-date__field--readonly):has(.aksel-date__field-input:not(:disabled)) &:hover,
.aksel-date__field:not(.aksel-date__field--readonly, .aksel-date__field--error):has(
.aksel-date__field-input:is(:focus-visible, :hover):not(:disabled)
)
& {
--__axc-button-border-color: var(--ax-border-strong);
}
.aksel-form-field--small & {
padding: var(--ax-space-4);
@@ -263,37 +244,19 @@
/* Readonly */
.aksel-date__field--readonly & {
--__axc-button-border-color: var(--ax-border-neutral-subtleA);
cursor: default;
opacity: 1;
background-color: var(--ax-bg-neutral-moderate);
color: var(--ax-text-neutral-subtle);
}
/* Syncs button-bg with input while respecting hover/active states */
&::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background-color: var(--ax-bg-input);
z-index: -1;
&:focus-visible {
outline-color: var(--ax-border-focus);
}
}
.aksel-date__field-wrapper {
display: inline-flex;
align-items: center;
position: relative;
width: fit-content;
border-radius: var(--ax-radius-8);
/* Initializes stacking context to allow z-index: -1 on button to work. */
z-index: 0;
&:has(.aksel-date__field-input:focus-visible) {
outline: 3px solid var(--ax-border-focus);
outline-offset: 3px;
}
}
.aksel-date__week-row {
+1 -1
View File
@@ -96,7 +96,7 @@
transform: translateY(calc(1rem * var(--__axc-nested-level) * -1)) scale(calc(1 - 0.02 * var(--__axc-nested-level)));
}
@media (max-width: 480px) or (max-height: 479px) {
@media (max-width: 768px) or (max-height: 479px) {
--__axc-dialog-transform: translateY(35%);
max-height: calc(100dvh - max(16px, 10dvh));
+1 -2
View File
@@ -18,8 +18,7 @@
}
/* Applied when hideLegend is applied to fieldset */
.aksel-fieldset > .aksel-sr-only + :not(:first-child, :empty),
.aksel-fieldset > .aksel-typo--visually-hidden + :not(:first-child, :empty) {
.aksel-fieldset > .aksel-sr-only + :not(:first-child, :empty) {
margin-top: 0;
}
Loaded 100 of 388 files, more files were not shown because too many files have changed in this diff. Show more