Skip to content

Commit d80d26b

Browse files
authored
release of v10.95.2 (#6483)
2 parents 0cd08af + 6328745 commit d80d26b

27 files changed

Lines changed: 136 additions & 77 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@
2828
- Tests should always be written for new functionality and bug fixes.
2929
- Tests act as a contract to verify the intention of the functionality.
3030
- Write tests before implementing a fix - this ensures the test captures the expected behavior and verifies the fix works correctly.
31+
- Prefer document.querySelector over screen from @testing-library/react.

packages/dnb-design-system-portal/src/docs/uilib/usage/first-steps/tools.mdx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,19 @@ order: 3
1212

1313
If your AI coding agent supports the Model Context Protocol (MCP), you can run a small local MCP server that exposes the packaged documentation from `/docs`.
1414

15+
But first, make sure you have installed `@dnb/eufemia` in your project:
16+
17+
```bash
18+
npm install @dnb/eufemia
19+
# or
20+
yarn add @dnb/eufemia
21+
# or
22+
pnpm add @dnb/eufemia
23+
```
24+
1525
Run the server from your project (where `@dnb/eufemia` is installed):
1626

17-
Example MCP config (e.g. `.vscode/mcp.json`):
27+
### Example MCP config (e.g. `.vscode/mcp.json`):
1828

1929
```json
2030
{
@@ -29,12 +39,18 @@ Example MCP config (e.g. `.vscode/mcp.json`):
2939
}
3040
```
3141

32-
CLI (Claude MCP):
42+
### Using Claude CLI with MCP:
3343

3444
```bash
3545
claude mcp add --transport stdio eufemia -- node node_modules/@dnb/eufemia/mcp/mcp-docs-server.js
3646
```
3747

48+
### Using raicode CLI with MCP (using Claude):
49+
50+
```bash
51+
raicode mcp add --transport stdio eufemia -- node node_modules/@dnb/eufemia/mcp/mcp-docs-server.js
52+
```
53+
3854
### How to use
3955

4056
- The MCP server helps AI apply Eufemia patterns more accurately in code, but results can still be imperfect. So always review the output carefully!

packages/dnb-eufemia/scripts/prebuild/tasks/__tests__/__snapshots__/makePropertiesFile.test.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export default {
9797
'--color-emerald-green-50': '#89aaac',
9898
'--color-emerald-green-25': '#c4d4d6',
9999
'--color-emerald-green-10': '#e8eeef',
100+
'--internal-color-background-selected-subtle': '#e4eed7',
100101
'--ca-font-family-heading': '"ArizonaFlare", var(--font-family-default)',
101102
'--ca-color-ocean-green': '#00343e',
102103
'--ca-color-burgundy-red': '#5c0022',
@@ -296,6 +297,7 @@ export default {
296297
'--color-emerald-green-50': '#89aaac',
297298
'--color-emerald-green-25': '#c4d4d6',
298299
'--color-emerald-green-10': '#e8eeef',
300+
'--internal-color-background-selected-subtle': '#e4eed7',
299301
'--ca-font-family-heading': '"ArizonaFlare", var(--font-family-default)',
300302
'--ca-color-ocean-green': '#00343e',
301303
'--ca-color-burgundy-red': '#5c0022',

packages/dnb-eufemia/src/components/autocomplete/Autocomplete.js

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,67 +1376,72 @@ class AutocompleteInstance extends React.PureComponent {
13761376
? '' // when searching numbers, we don't care about word boundaries
13771377
: '^|\\s'
13781378

1379+
// Pre-compile regex patterns for performance
1380+
const searchWordsData = searchWords.map((word, wordIndex) => {
1381+
const processedWord = searchNumbers
1382+
? word.replace(/[^\p{L}\p{N}]+/gu, '')
1383+
: escapeRegexChars(word)
1384+
const wordBoundary = getWordBoundary(wordIndex)
1385+
1386+
return {
1387+
originalWord: word,
1388+
processedWord,
1389+
wordIndex,
1390+
// Pre-compile regex for filter phase
1391+
filterRegex: new RegExp(
1392+
wordIndex >= inWordIndex
1393+
? `${processedWord}`
1394+
: `(${wordBoundary})${processedWord}`,
1395+
'i'
1396+
),
1397+
// Pre-compile regex for scoring phase
1398+
scoreRegex: new RegExp(
1399+
`(${wordBoundary})${escapeRegexChars(word)}`,
1400+
'ig'
1401+
),
1402+
}
1403+
})
1404+
1405+
// Pre-compile first word regex if needed
1406+
const firstWordRegex =
1407+
searchWords.length > 0
1408+
? new RegExp(`^${escapeRegexChars(searchWords[0])}`, 'i')
1409+
: null
1410+
13791411
const findSearchWords = (contentChunk) => {
13801412
if (typeof contentChunk !== 'string') {
13811413
return []
13821414
}
13831415

1384-
return searchWords
1385-
.map((word, wordIndex) => ({ word, wordIndex }))
1386-
.filter(({ word, wordIndex }) => {
1387-
if (searchNumbers) {
1388-
// Remove all other chars, except numbers, so we can compare
1389-
word = word.replace(/[^\p{L}\p{N}]+/gu, '')
1390-
} else {
1391-
// To ensure we escape regex chars
1392-
word = escapeRegexChars(word)
1393-
}
1394-
1395-
const wordBoundary = getWordBoundary(wordIndex)
1396-
1397-
// if the uses reached word 3, then we go inside words as well
1398-
const regexWord = new RegExp(
1399-
wordIndex >= inWordIndex
1400-
? `${word}`
1401-
: `(${wordBoundary})${word}`,
1402-
'i'
1403-
)
1404-
1405-
if (regexWord.test(contentChunk)) {
1416+
return searchWordsData
1417+
.filter(({ filterRegex, processedWord }) => {
1418+
if (filterRegex.test(contentChunk)) {
14061419
return true
14071420
}
14081421

14091422
if (
14101423
searchNumbers &&
1411-
regexWord.test(contentChunk.replace(/[^0-9]/g, ''))
1424+
filterRegex.test(contentChunk.replace(/[^0-9]/g, ''))
14121425
) {
14131426
return true
14141427
}
14151428

14161429
return false
14171430
})
1418-
.map(({ word, wordIndex }) => {
1431+
.map(({ originalWord, wordIndex, scoreRegex }) => {
14191432
// Use 1 to ensure we never have 0, because we filter out words with 0 later
14201433
let wordScore = 0
14211434

14221435
// Check how ofter the current written word is inside the content,
14231436
// and give a score for each one
1424-
wordScore += (
1425-
contentChunk.match(
1426-
new RegExp(
1427-
`(${getWordBoundary(wordIndex)})${escapeRegexChars(word)}`,
1428-
'ig'
1429-
)
1430-
) || []
1431-
).length
1437+
wordScore += (contentChunk.match(scoreRegex) || []).length
14321438

14331439
// Give the first word extra points
1434-
if (wordIndex === 0) {
1440+
if (wordIndex === 0 && firstWordRegex) {
14351441
// Check if the first chunk starts the first written word
1436-
const isFirstWord = new RegExp(
1437-
`^${escapeRegexChars(searchWords[0])}`,
1438-
'i'
1439-
).test(contentChunk.split(' ')[0])
1442+
const isFirstWord = firstWordRegex.test(
1443+
contentChunk.split(' ')[0]
1444+
)
14401445

14411446
// If yes, add the amount of possible words + 1
14421447
if (isFirstWord) {
@@ -1445,7 +1450,7 @@ class AutocompleteInstance extends React.PureComponent {
14451450
}
14461451

14471452
return {
1448-
word,
1453+
word: originalWord,
14491454
wordIndex,
14501455
wordScore,
14511456
}
@@ -1566,14 +1571,17 @@ class AutocompleteInstance extends React.PureComponent {
15661571

15671572
if (segment.includes(strS)) {
15681573
// to make sure we don't have several in a row
1574+
const startRepeatRegex = new RegExp(`(${strS})+`, 'g')
1575+
const endRepeatRegex = new RegExp(`(${strE})+`, 'g')
1576+
const adjacentRegex = new RegExp(`(${strE}${strS})`, 'g')
1577+
const splitRegex = new RegExp(`(${strS}|${strE})`, 'g')
1578+
15691579
const normalized = segment
1570-
.replace(new RegExp(`(${strS})+`, 'g'), strS)
1571-
.replace(new RegExp(`(${strE})+`, 'g'), strE)
1572-
.replace(new RegExp(`(${strE}${strS})`, 'g'), '')
1580+
.replace(startRepeatRegex, strS)
1581+
.replace(endRepeatRegex, strE)
1582+
.replace(adjacentRegex, '')
15731583

1574-
const tokens = normalized
1575-
.split(new RegExp(`(${strS}|${strE})`, 'g'))
1576-
.filter(Boolean)
1584+
const tokens = normalized.split(splitRegex).filter(Boolean)
15771585

15781586
let isHighlighted = false
15791587
let highlightIndex = 0

packages/dnb-eufemia/src/components/copy-on-click/__tests__/CopyOnClick.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ describe('CopyOnClick', () => {
180180
const originalWrite = navigator.clipboard.writeText
181181
navigator.clipboard.writeText = jest
182182
.fn()
183-
.mockRejectedValue(new Error('permission denied'))
183+
.mockRejectedValue(new Error('Permission denied'))
184184

185185
// Ensure fallback does not succeed
186186
document.execCommand = jest.fn(() => false)

packages/dnb-eufemia/src/components/input-masked/addons/emailPipe.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ const dotDot = '..'
1010
const emptyArray = []
1111
const allDotsRegExp = /\./g
1212

13+
function createEmptyEmailRegex(placeholderChar: string) {
14+
return new RegExp(`[^@\\s.${placeholderChar}]`)
15+
}
16+
1317
export default function emailPipe(conformedValue, config) {
1418
const {
1519
currentCaretPosition,
@@ -25,7 +29,7 @@ export default function emailPipe(conformedValue, config) {
2529
const indexOfAtDot = value.indexOf(atDot)
2630

2731
const emptyEmail =
28-
rawValue.match(new RegExp(`[^@\\s.${placeholderChar}]`)) === null
32+
rawValue.match(createEmptyEmailRegex(placeholderChar)) === null
2933

3034
if (emptyEmail) {
3135
return emptyString

packages/dnb-eufemia/src/components/number-format/__tests__/NumberFormat.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1738,7 +1738,7 @@ describe('NumberFormat copy tooltip', () => {
17381738
)
17391739

17401740
if (!selection) {
1741-
throw new Error('selection element is missing')
1741+
throw new Error('Selection element is missing')
17421742
}
17431743

17441744
fireEvent.copy(selection)

packages/dnb-eufemia/src/components/popover/PopoverContainer.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,21 @@ function PopoverContainer(props: PopoverContainerProps) {
834834
return null
835835
}
836836

837+
// Pre-compute className variations to avoid multiple .map() calls
838+
const noAnimationClasses = noAnimation
839+
? baseClassNames.map((base) => `${base}--no-animation`)
840+
: null
841+
const fixedClasses = fixedPosition
842+
? baseClassNames.map((base) => `${base}--fixed`)
843+
: null
844+
const activeClasses = isActive
845+
? baseClassNames.map((base) => `${base}--active`)
846+
: null
847+
const hideClasses =
848+
!isActive && wasActive
849+
? baseClassNames.map((base) => `${base}--hide`)
850+
: null
851+
837852
return (
838853
<span
839854
ref={elementRef}
@@ -845,13 +860,10 @@ function PopoverContainer(props: PopoverContainerProps) {
845860
}}
846861
className={classnames(
847862
attributes?.className,
848-
noAnimation &&
849-
baseClassNames.map((base) => `${base}--no-animation`),
850-
fixedPosition && baseClassNames.map((base) => `${base}--fixed`),
851-
isActive && baseClassNames.map((base) => `${base}--active`),
852-
!isActive &&
853-
wasActive &&
854-
baseClassNames.map((base) => `${base}--hide`)
863+
noAnimationClasses,
864+
fixedClasses,
865+
activeClasses,
866+
hideClasses
855867
)}
856868
style={containerStyle}
857869
>

packages/dnb-eufemia/src/components/upload/UploadVerify.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,12 @@ export function extendWithAbbreviation(
183183
abbreviations = { jpg: 'jpeg' }
184184
) {
185185
const list = [...acceptedFileTypes]
186+
const listSet = new Set(list)
186187

187188
Object.entries(abbreviations).forEach(([type, abbr]) => {
188-
if (list.some((t) => t === type) && !list.some((t) => t === abbr)) {
189+
if (listSet.has(type) && !listSet.has(abbr)) {
189190
list.push(abbr)
191+
listSet.add(abbr)
190192
}
191193
})
192194

packages/dnb-eufemia/src/elements/typography/P.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,10 @@ const handleDeprecatedProps = ({
9898
.forEach((modifier) => allModifiers.push(modifier))
9999
}
100100

101-
const remainingModifiers = allModifiers.filter(Boolean).filter((cur) => {
101+
const remainingModifiers = allModifiers.filter((cur) => {
102+
if (!cur) {
103+
return false
104+
}
102105
if (['x-small'].includes(cur)) {
103106
oldSize = 'x-small'
104107
} else if (['small'].includes(cur)) {

0 commit comments

Comments
 (0)