From ffd15e77f86789610cb3bd1f198986aa34b8d5bd Mon Sep 17 00:00:00 2001 From: Nathan Vasse Date: Thu, 23 Jul 2026 15:27:26 +0200 Subject: [PATCH 1/8] =?UTF-8?q?=E2=AC=86=EF=B8=8F(front)=20upgrade=20cunni?= =?UTF-8?q?ngham-react=20to=204.4.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This version ships minor fixes that we need for the import contact feature. --- cunningham.ts | 11 ++++++++--- package.json | 2 +- src/cunningham-tokens-sass.scss | 30 ++++++++++++++++++++++++++++++ src/cunningham-tokens.css | 18 ++++++++++++++++++ src/cunningham-tokens.ts | 2 +- yarn.lock | 8 ++++---- 6 files changed, 62 insertions(+), 9 deletions(-) diff --git a/cunningham.ts b/cunningham.ts index 93daa073..7914bbc9 100644 --- a/cunningham.ts +++ b/cunningham.ts @@ -27,6 +27,11 @@ export const commonTokenOverrides = { "body--background-color-hover": "ref(contextuals.background.semantic.neutral.tertiary)", }, + "forms-fileuploader": { + "border-style": "solid", + "border-radius": "4px", + "border-width": "1px", + }, "forms-checkbox": { "font-size": "ref(globals.font.sizes.sm)", }, @@ -487,7 +492,7 @@ export const whiteLabelGlobals = { "white-850": "#F8F8F9D9", "white-900": "#F8F8F9E5", "white-950": "#F8F8F9F2", - "white-975": "#F8F8F9F9" + "white-975": "#F8F8F9F9", }, ...commonGlobals, font: { @@ -496,7 +501,7 @@ export const whiteLabelGlobals = { base: "Hanken Grotesk, Inter, Roboto Flex Variable, sans-serif", accent: "Hanken Grotesk, Inter, Roboto Flex Variable, sans-serif", }, - } + }, }; export const dsfrGlobals = { @@ -830,7 +835,7 @@ export const dsfrGlobals = { "white-850": "#F6F8F9D9", "white-900": "#F6F8F9E5", "white-950": "#F6F8F9F2", - "white-975": "#F6F8F9F9" + "white-975": "#F6F8F9F9", }, ...commonGlobals, }; diff --git a/package.json b/package.json index b9612f4b..e58294fa 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@dnd-kit/modifiers": "9.0.0", "@dnd-kit/sortable": "10.0.0", "@fontsource/material-icons": "5.2.5", - "@gouvfr-lasuite/cunningham-react": "4.3.0", + "@gouvfr-lasuite/cunningham-react": "4.4.0", "@gouvfr-lasuite/cunningham-tokens": "3.1.0", "@gouvfr-lasuite/integration": "1.0.2", "@types/node": "22.10.7", diff --git a/src/cunningham-tokens-sass.scss b/src/cunningham-tokens-sass.scss index 1829490d..056bb607 100644 --- a/src/cunningham-tokens-sass.scss +++ b/src/cunningham-tokens-sass.scss @@ -706,6 +706,11 @@ $themes: ( 'header--weight': 500, 'body--background-color-hover': #F0F0F3 ), + 'forms-fileuploader': ( + 'border-style': solid, + 'border-radius': 4px, + 'border-width': 1px + ), 'forms-checkbox': ( 'font-size': 0.875rem ), @@ -1468,6 +1473,11 @@ $themes: ( 'header--weight': 500, 'body--background-color-hover': #F0F0F3 ), + 'forms-fileuploader': ( + 'border-style': solid, + 'border-radius': 4px, + 'border-width': 1px + ), 'forms-checkbox': ( 'font-size': 0.875rem ), @@ -2203,6 +2213,11 @@ $themes: ( 'header--weight': 500, 'body--background-color-hover': #F0F0F3 ), + 'forms-fileuploader': ( + 'border-style': solid, + 'border-radius': 4px, + 'border-width': 1px + ), 'forms-checkbox': ( 'font-size': 0.875rem ), @@ -2938,6 +2953,11 @@ $themes: ( 'header--weight': 500, 'body--background-color-hover': #F0F0F3 ), + 'forms-fileuploader': ( + 'border-style': solid, + 'border-radius': 4px, + 'border-width': 1px + ), 'forms-checkbox': ( 'font-size': 0.875rem ), @@ -3676,6 +3696,11 @@ $themes: ( 'header--weight': 500, 'body--background-color-hover': #F0F0F3 ), + 'forms-fileuploader': ( + 'border-style': solid, + 'border-radius': 4px, + 'border-width': 1px + ), 'forms-checkbox': ( 'font-size': 0.875rem ), @@ -4414,6 +4439,11 @@ $themes: ( 'header--weight': 500, 'body--background-color-hover': #F0F0F3 ), + 'forms-fileuploader': ( + 'border-style': solid, + 'border-radius': 4px, + 'border-width': 1px + ), 'forms-checkbox': ( 'font-size': 0.875rem ), diff --git a/src/cunningham-tokens.css b/src/cunningham-tokens.css index cbd635ad..346585d1 100644 --- a/src/cunningham-tokens.css +++ b/src/cunningham-tokens.css @@ -556,6 +556,9 @@ --c--components--datagrid--header--size: 12px; --c--components--datagrid--header--weight: 500; --c--components--datagrid--body--background-color-hover: var(--c--contextuals--background--semantic--neutral--tertiary); + --c--components--forms-fileuploader--border-style: solid; + --c--components--forms-fileuploader--border-radius: 4px; + --c--components--forms-fileuploader--border-width: 1px; --c--components--forms-checkbox--font-size: var(--c--globals--font--sizes--sm); --c--components--forms-input--border-radius: 4px; --c--components--forms-input--border-radius--hover: 4px; @@ -1144,6 +1147,9 @@ --c--components--datagrid--header--size: 12px; --c--components--datagrid--header--weight: 500; --c--components--datagrid--body--background-color-hover: var(--c--contextuals--background--semantic--neutral--tertiary); + --c--components--forms-fileuploader--border-style: solid; + --c--components--forms-fileuploader--border-radius: 4px; + --c--components--forms-fileuploader--border-width: 1px; --c--components--forms-checkbox--font-size: var(--c--globals--font--sizes--sm); --c--components--forms-input--border-radius: 4px; --c--components--forms-input--border-radius--hover: 4px; @@ -1707,6 +1713,9 @@ --c--components--datagrid--header--size: 12px; --c--components--datagrid--header--weight: 500; --c--components--datagrid--body--background-color-hover: var(--c--contextuals--background--semantic--neutral--tertiary); + --c--components--forms-fileuploader--border-style: solid; + --c--components--forms-fileuploader--border-radius: 4px; + --c--components--forms-fileuploader--border-width: 1px; --c--components--forms-checkbox--font-size: var(--c--globals--font--sizes--sm); --c--components--forms-input--border-radius: 4px; --c--components--forms-input--border-radius--hover: 4px; @@ -2270,6 +2279,9 @@ --c--components--datagrid--header--size: 12px; --c--components--datagrid--header--weight: 500; --c--components--datagrid--body--background-color-hover: var(--c--contextuals--background--semantic--neutral--tertiary); + --c--components--forms-fileuploader--border-style: solid; + --c--components--forms-fileuploader--border-radius: 4px; + --c--components--forms-fileuploader--border-width: 1px; --c--components--forms-checkbox--font-size: var(--c--globals--font--sizes--sm); --c--components--forms-input--border-radius: 4px; --c--components--forms-input--border-radius--hover: 4px; @@ -2836,6 +2848,9 @@ --c--components--datagrid--header--size: 12px; --c--components--datagrid--header--weight: 500; --c--components--datagrid--body--background-color-hover: var(--c--contextuals--background--semantic--neutral--tertiary); + --c--components--forms-fileuploader--border-style: solid; + --c--components--forms-fileuploader--border-radius: 4px; + --c--components--forms-fileuploader--border-width: 1px; --c--components--forms-checkbox--font-size: var(--c--globals--font--sizes--sm); --c--components--forms-input--border-radius: 4px; --c--components--forms-input--border-radius--hover: 4px; @@ -3402,6 +3417,9 @@ --c--components--datagrid--header--size: 12px; --c--components--datagrid--header--weight: 500; --c--components--datagrid--body--background-color-hover: var(--c--contextuals--background--semantic--neutral--tertiary); + --c--components--forms-fileuploader--border-style: solid; + --c--components--forms-fileuploader--border-radius: 4px; + --c--components--forms-fileuploader--border-width: 1px; --c--components--forms-checkbox--font-size: var(--c--globals--font--sizes--sm); --c--components--forms-input--border-radius: 4px; --c--components--forms-input--border-radius--hover: 4px; diff --git a/src/cunningham-tokens.ts b/src/cunningham-tokens.ts index 64fab001..5d6ac9fa 100644 --- a/src/cunningham-tokens.ts +++ b/src/cunningham-tokens.ts @@ -1 +1 @@ -export const tokens = {"themes":{"default":{"globals":{"colors":{"logo-1-light":"#4844AD","logo-2-light":"#4844AD","logo-1-dark":"#BEC5F0","logo-2-dark":"#BEC5F0","brand-050":"#EEF1FA","brand-100":"#DDE2F5","brand-150":"#CED3F1","brand-200":"#BEC5F0","brand-250":"#AFB5F1","brand-300":"#A0A5F6","brand-350":"#8F94FD","brand-400":"#8184FC","brand-450":"#7576EE","brand-500":"#6969DF","brand-550":"#5E5CD0","brand-600":"#534FC2","brand-650":"#4844AD","brand-700":"#3E3B98","brand-750":"#36347D","brand-800":"#2D2F5F","brand-850":"#262848","brand-900":"#1C1E32","brand-950":"#11131F","gray-000":"#FFFFFF","gray-025":"#F8F8F9","gray-050":"#F0F0F3","gray-100":"#E2E2EA","gray-150":"#D3D4E0","gray-200":"#C5C6D5","gray-250":"#B7B7CB","gray-300":"#A9A9BF","gray-350":"#9C9CB2","gray-400":"#8F8FA4","gray-450":"#828297","gray-500":"#75758A","gray-550":"#69697D","gray-600":"#5D5D70","gray-650":"#515164","gray-700":"#454558","gray-750":"#3A3A4C","gray-800":"#2F303D","gray-850":"#25252F","gray-900":"#1B1B23","gray-950":"#111114","gray-1000":"#000000","info-050":"#EAF2F9","info-100":"#D5E4F3","info-150":"#BFD7F0","info-200":"#A7CAEE","info-250":"#8DBDEF","info-300":"#6EB0F2","info-350":"#50A2F5","info-400":"#3593F4","info-450":"#1185ED","info-500":"#0077DE","info-550":"#0069CF","info-600":"#005BC0","info-650":"#0D4EAA","info-700":"#124394","info-750":"#163878","info-800":"#192F5A","info-850":"#192541","info-900":"#141B2D","info-950":"#0C111C","success-050":"#E8F1EA","success-100":"#CFE4D4","success-150":"#BAD9C1","success-200":"#A2CFAD","success-250":"#86C597","success-300":"#6CBA83","success-350":"#4FB070","success-400":"#40A363","success-450":"#309556","success-500":"#1E884A","success-550":"#027B3E","success-600":"#016D31","success-650":"#006024","success-700":"#005317","success-750":"#0D4511","success-800":"#11380E","success-850":"#132A11","success-900":"#101E0F","success-950":"#091209","warning-050":"#F8F0E9","warning-100":"#F1E0D3","warning-150":"#ECD0BC","warning-200":"#E8C0A4","warning-250":"#E8AE8A","warning-300":"#EB9970","warning-350":"#E98456","warning-400":"#E57036","warning-450":"#DA5E18","warning-500":"#CB5000","warning-550":"#BC4200","warning-600":"#AD3300","warning-650":"#9E2300","warning-700":"#882011","warning-750":"#731E16","warning-800":"#58201A","warning-850":"#401D18","warning-900":"#2E1714","warning-950":"#1D0F0D","error-050":"#F9EFEC","error-100":"#F4DFD9","error-150":"#F0CEC6","error-200":"#EEBCB2","error-250":"#EEA99D","error-300":"#EF9486","error-350":"#F37C6E","error-400":"#F65F53","error-450":"#F0463D","error-500":"#E82322","error-550":"#D7010E","error-600":"#C00100","error-650":"#AA0000","error-700":"#910C06","error-750":"#731E16","error-800":"#58201A","error-850":"#401D18","error-900":"#2E1714","error-950":"#1D0F0D","red-050":"#FAEFEE","red-100":"#F4DEDD","red-150":"#F1CDCB","red-200":"#EFBBBA","red-250":"#EEA8A8","red-300":"#F09394","red-350":"#F37B7E","red-400":"#EF6569","red-450":"#E94A55","red-500":"#DA3B49","red-550":"#CA2A3C","red-600":"#BB1330","red-650":"#A90021","red-700":"#910A13","red-750":"#731E16","red-800":"#58201A","red-850":"#411D18","red-900":"#2E1714","red-950":"#1D0F0D","orange-050":"#F8F0E9","orange-100":"#F1E0D3","orange-150":"#ECD0BD","orange-200":"#EABFA6","orange-250":"#EBAC90","orange-300":"#EC9772","orange-350":"#E5845A","orange-400":"#D6774D","orange-450":"#C86A40","orange-500":"#B95D33","orange-550":"#AB5025","orange-600":"#9D4315","orange-650":"#8F3600","orange-700":"#812900","orange-750":"#6C2511","orange-800":"#572017","orange-850":"#401D18","orange-900":"#2E1714","orange-950":"#1D0F0D","brown-050":"#F6F0E8","brown-100":"#F1E0D3","brown-150":"#EBD0BA","brown-200":"#E2C0A6","brown-250":"#D4B398","brown-300":"#C6A58B","brown-350":"#B8987E","brown-400":"#AA8B71","brown-450":"#9D7E65","brown-500":"#8F7158","brown-550":"#82654C","brown-600":"#765841","brown-650":"#694C35","brown-700":"#5D412A","brown-750":"#51361E","brown-800":"#452A13","brown-850":"#392008","brown-900":"#29180A","brown-950":"#1B0F08","yellow-050":"#F3F0E7","yellow-100":"#E9E2CF","yellow-150":"#E1D4B7","yellow-200":"#D9C599","yellow-250":"#D2B677","yellow-300":"#CAA756","yellow-350":"#C2972E","yellow-400":"#B98900","yellow-450":"#AB7B00","yellow-500":"#9D6E00","yellow-550":"#916100","yellow-600":"#855400","yellow-650":"#784700","yellow-700":"#6C3A00","yellow-750":"#5F2E00","yellow-800":"#512302","yellow-850":"#3E1D10","yellow-900":"#2D1711","yellow-950":"#1D0F0D","green-050":"#E6F1E9","green-100":"#CFE4D5","green-150":"#B8D8C1","green-200":"#A0CFAE","green-250":"#84C59A","green-300":"#65BA86","green-350":"#45B173","green-400":"#23A562","green-450":"#029755","green-500":"#008948","green-550":"#017B3B","green-600":"#006E2E","green-650":"#006022","green-700":"#005314","green-750":"#0D4510","green-800":"#11380E","green-850":"#132A11","green-900":"#101E0F","green-950":"#091209","blue-1-050":"#EBF1F9","blue-1-100":"#D6E4F4","blue-1-150":"#C1D7F0","blue-1-200":"#AACAEF","blue-1-250":"#8FBCEF","blue-1-300":"#7CAFEB","blue-1-350":"#68A1E4","blue-1-400":"#5B94D6","blue-1-450":"#4E86C7","blue-1-500":"#4279B9","blue-1-550":"#356CAC","blue-1-600":"#28609E","blue-1-650":"#1B5390","blue-1-700":"#0B4783","blue-1-750":"#0F3C6E","blue-1-800":"#133059","blue-1-850":"#152641","blue-1-900":"#121C2D","blue-1-950":"#0B111C","blue-2-050":"#E7F3F4","blue-2-100":"#CEE7E9","blue-2-150":"#B2DCE0","blue-2-200":"#91D1D7","blue-2-250":"#68C7D0","blue-2-300":"#43BBC5","blue-2-350":"#00AFBA","blue-2-400":"#01A0AA","blue-2-450":"#00929D","blue-2-500":"#00848F","blue-2-550":"#007682","blue-2-600":"#016874","blue-2-650":"#005B67","blue-2-700":"#004E5A","blue-2-750":"#00424E","blue-2-800":"#003642","blue-2-850":"#002A38","blue-2-900":"#061E28","blue-2-950":"#071219","purple-050":"#F7F0F6","purple-100":"#EEE0EE","purple-150":"#E7D1E7","purple-200":"#DBBFE4","purple-250":"#D3AEE2","purple-300":"#CB99E1","purple-350":"#C188D9","purple-400":"#B47BCB","purple-450":"#A66EBD","purple-500":"#9961AF","purple-550":"#8B55A1","purple-600":"#7E4894","purple-650":"#723C87","purple-700":"#633376","purple-750":"#552A65","purple-800":"#452551","purple-850":"#35213D","purple-900":"#261A2C","purple-950":"#17111C","pink-050":"#F8EFF4","pink-100":"#F0DFEA","pink-150":"#EACEDF","pink-200":"#E9BBD1","pink-250":"#E9A7C2","pink-300":"#E095B4","pink-350":"#D685A8","pink-400":"#C7799B","pink-450":"#B86C8D","pink-500":"#AA5F80","pink-550":"#9C5374","pink-600":"#8E4767","pink-650":"#813B5B","pink-700":"#732E4F","pink-750":"#632643","pink-800":"#521F38","pink-850":"#3E1C2B","pink-900":"#2D171F","pink-950":"#1C0E12","black-000":"#1B1B2300","black-050":"#1B1B230D","black-100":"#1B1B231A","black-150":"#1B1B2326","black-200":"#1B1B2333","black-250":"#1B1B2340","black-300":"#1B1B234D","black-350":"#1B1B2359","black-400":"#1B1B2366","black-450":"#1B1B2373","black-500":"#1B1B2380","black-550":"#1B1B238C","black-600":"#1B1B2399","black-650":"#1B1B23A6","black-700":"#1B1B23B2","black-750":"#1B1B23BF","black-800":"#1B1B23CC","black-850":"#1B1B23D9","black-900":"#1B1B23E5","black-950":"#111114F2","white-000":"#F8F8F900","white-050":"#F8F8F90D","white-100":"#F8F8F91A","white-150":"#F8F8F926","white-200":"#F8F8F933","white-250":"#F8F8F940","white-300":"#F8F8F94D","white-350":"#F8F8F959","white-400":"#F8F8F966","white-450":"#F8F8F973","white-500":"#F8F8F980","white-550":"#F8F8F98C","white-600":"#F8F8F999","white-650":"#F8F8F9A6","white-700":"#F8F8F9B2","white-750":"#F8F8F9BF","white-800":"#F8F8F9CC","white-850":"#F8F8F9D9","white-900":"#F8F8F9E5","white-950":"#F8F8F9F2","white-975":"#F8F8F9F9"},"transitions":{"ease-in":"cubic-bezier(0.32, 0, 0.67, 0)","ease-out":"cubic-bezier(0.33, 1, 0.68, 1)","ease-in-out":"cubic-bezier(0.65, 0, 0.35, 1)","duration":"250ms"},"font":{"sizes":{"xs":"0.75rem","sm":"0.875rem","md":"1rem","lg":"1.125rem","ml":"0.938rem","xl":"1.25rem","t":"0.6875rem","s":"0.75rem","h1":"2rem","h2":"1.75rem","h3":"1.5rem","h4":"1.375rem","h5":"1.25rem","h6":"1.125rem","xl-alt":"5rem","lg-alt":"4.5rem","md-alt":"4rem","sm-alt":"3.5rem","xs-alt":"3rem"},"weights":{"thin":100,"light":300,"regular":400,"medium":500,"bold":600,"extrabold":800,"black":800},"families":{"base":"Hanken Grotesk, Inter, Roboto Flex Variable, sans-serif","accent":"Hanken Grotesk, Inter, Roboto Flex Variable, sans-serif"}},"spacings":{"0":"0","l":"3rem","b":"1.625rem","m":"0.8125rem","s":"1rem","t":"0.5rem","st":"0.25rem","4xs":"0.125rem","3xs":"0.25rem","2xs":"0.375rem","xxxs":"0.25rem","xxs":"0.375rem","xs":"0.5rem","sm":"0.75rem","base":"1rem","md":"1.5rem","lg":"2rem","xl":"2.5rem","xxl":"3rem","xxxl":"3.5rem","2xl":"3rem","3xl":"3.5rem","4xl":"4rem","5xl":"4.5rem","6xl":"6rem","7xl":"7.5rem","none":"0","auto":"auto","bx":"2.2rem","full":"100%"},"breakpoints":{"xs":"480px","sm":"576px","md":"768px","lg":"992px","xl":"1200px","xxl":"1400px","xxs":"320px","mobile":"768px","tablet":"1024px"}},"contextuals":{"background":{"surface":{"primary":"#FFFFFF","secondary":"#FFFFFF","tertiary":"#F8F8F9"},"semantic":{"overlay":{"primary":"#1B1B230D","primary-hover":"#1B1B231A"},"contextual":{"primary":"#1B1B230D","primary-hover":"#1B1B231A"},"brand":{"primary":"#5E5CD0","primary-hover":"#4844AD","secondary":"#DDE2F5","secondary-hover":"#CED3F1","tertiary":"#EEF1FA","tertiary-hover":"#DDE2F5"},"neutral":{"primary":"#69697D","primary-hover":"#515164","secondary":"#E2E2EA","secondary-hover":"#D3D4E0","tertiary":"#F0F0F3","tertiary-hover":"#E2E2EA"},"info":{"primary":"#0069CF","primary-hover":"#0D4EAA","secondary":"#D5E4F3","secondary-hover":"#BFD7F0","tertiary":"#EAF2F9","tertiary-hover":"#D5E4F3"},"success":{"primary":"#027B3E","primary-hover":"#006024","secondary":"#CFE4D4","secondary-hover":"#BAD9C1","tertiary":"#E8F1EA","tertiary-hover":"#CFE4D4"},"warning":{"primary":"#BC4200","primary-hover":"#9E2300","secondary":"#F1E0D3","secondary-hover":"#ECD0BC","tertiary":"#F8F0E9","tertiary-hover":"#F1E0D3"},"error":{"primary":"#D7010E","primary-hover":"#AA0000","secondary":"#F4DFD9","secondary-hover":"#F0CEC6","tertiary":"#F9EFEC","tertiary-hover":"#F4DFD9"},"disabled":{"primary":"#E2E2EA","secondary":"#F0F0F3"}},"palette":{"brand":{"primary":"#6969DF","secondary":"#8F94FD","tertiary":"#CED3F1"},"red":{"primary":"#DA3B49","secondary":"#F37B7E","tertiary":"#F1CDCB"},"orange":{"primary":"#B95D33","secondary":"#E5845A","tertiary":"#ECD0BD"},"brown":{"primary":"#8F7158","secondary":"#B8987E","tertiary":"#EBD0BA"},"yellow":{"primary":"#9D6E00","secondary":"#C2972E","tertiary":"#E1D4B7"},"green":{"primary":"#008948","secondary":"#45B173","tertiary":"#B8D8C1"},"blue-1":{"primary":"#4279B9","secondary":"#68A1E4","tertiary":"#C1D7F0"},"blue-2":{"primary":"#00848F","secondary":"#00AFBA","tertiary":"#B2DCE0"},"purple":{"primary":"#9961AF","secondary":"#C188D9","tertiary":"#E7D1E7"},"pink":{"primary":"#AA5F80","secondary":"#D685A8","tertiary":"#EACEDF"},"gray":{"primary":"#75758A","secondary":"#9C9CB2","tertiary":"#D3D4E0"}}},"content":{"logo1":"#4844AD","logo2":"#4844AD","semantic":{"contextual":{"primary":"#F8F8F9F2"},"overlay":{"primary":"#F8F8F9F2"},"brand":{"primary":"#3E3B98","secondary":"#534FC2","tertiary":"#5E5CD0","on-brand":"#EEF1FA"},"neutral":{"primary":"#25252F","secondary":"#5D5D70","tertiary":"#69697D","on-neutral":"#F0F0F3"},"info":{"primary":"#124394","secondary":"#005BC0","tertiary":"#0069CF","on-info":"#EAF2F9"},"success":{"primary":"#005317","secondary":"#016D31","tertiary":"#027B3E","on-success":"#E8F1EA"},"warning":{"primary":"#882011","secondary":"#AD3300","tertiary":"#BC4200","on-warning":"#F8F0E9"},"error":{"primary":"#910C06","secondary":"#C00100","tertiary":"#D7010E","on-error":"#F9EFEC"},"disabled":{"primary":"#A9A9BF","secondary":"#F8F8F980"}},"palette":{"brand":{"primary":"#6969DF"},"red":{"primary":"#DA3B49"},"orange":{"primary":"#B95D33"},"brown":{"primary":"#8F7158"},"yellow":{"primary":"#9D6E00"},"green":{"primary":"#008948"},"blue-1":{"primary":"#4279B9"},"blue-2":{"primary":"#00848F"},"purple":{"primary":"#9961AF"},"pink":{"primary":"#AA5F80"},"gray":{"primary":"#75758A"}}},"border":{"surface":{"primary":"#E2E2EA"},"semantic":{"contextual":{"primary":"#F8F8F933"},"overlay":{"primary":"#F8F8F933"},"brand":{"primary":"#5E5CD0","secondary":"#A0A5F6","tertiary":"#CED3F1"},"neutral":{"primary":"#69697D","secondary":"#A9A9BF","tertiary":"#D3D4E0"},"info":{"primary":"#0069CF","secondary":"#6EB0F2","tertiary":"#BFD7F0"},"success":{"primary":"#027B3E","secondary":"#6CBA83","tertiary":"#BAD9C1"},"warning":{"primary":"#BC4200","secondary":"#EB9970","tertiary":"#ECD0BC"},"error":{"primary":"#D7010E","secondary":"#EF9486","tertiary":"#F0CEC6"},"disabled":{"primary":"#E2E2EA"}}}},"components":{"modal":{"width-small":"342px"},"tooltip":{"padding":"4px 8px"},"button":{"medium-height":"40px","medium-text-height":"40px","border-radius":"4px","border-radius--active":"4px","border-radius--focus":"4px"},"resize-handle":{"hover--color":"#E2E2EA"},"datagrid":{"header--color":"#25252F","header--size":"12px","header--weight":"500","body--background-color-hover":"#F0F0F3"},"forms-checkbox":{"font-size":"0.875rem"},"forms-input":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-select":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-textarea":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-datepicker":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"badge":{"font-size":"0.75rem","border-radius":"12px","padding-inline":"0.5rem","padding-block":"0.375rem","accent":{"background-color":"#DDE2F5","color":"#534FC2"},"neutral":{"background-color":"#E2E2EA","color":"#5D5D70"},"danger":{"background-color":"#F4DFD9","color":"#C00100"},"success":{"background-color":"#CFE4D4","color":"#016D31"},"warning":{"background-color":"#F1E0D3","color":"#AD3300"},"info":{"background-color":"#D5E4F3","color":"#005BC0"}}}},"dark":{"globals":{"colors":{"logo-1-light":"#4844AD","logo-2-light":"#4844AD","logo-1-dark":"#BEC5F0","logo-2-dark":"#BEC5F0","brand-050":"#EEF1FA","brand-100":"#DDE2F5","brand-150":"#CED3F1","brand-200":"#BEC5F0","brand-250":"#AFB5F1","brand-300":"#A0A5F6","brand-350":"#8F94FD","brand-400":"#8184FC","brand-450":"#7576EE","brand-500":"#6969DF","brand-550":"#5E5CD0","brand-600":"#534FC2","brand-650":"#4844AD","brand-700":"#3E3B98","brand-750":"#36347D","brand-800":"#2D2F5F","brand-850":"#262848","brand-900":"#1C1E32","brand-950":"#11131F","gray-000":"#FFFFFF","gray-025":"#F8F8F9","gray-050":"#F0F0F3","gray-100":"#E2E2EA","gray-150":"#D3D4E0","gray-200":"#C5C6D5","gray-250":"#B7B7CB","gray-300":"#A9A9BF","gray-350":"#9C9CB2","gray-400":"#8F8FA4","gray-450":"#828297","gray-500":"#75758A","gray-550":"#69697D","gray-600":"#5D5D70","gray-650":"#515164","gray-700":"#454558","gray-750":"#3A3A4C","gray-800":"#2F303D","gray-850":"#25252F","gray-900":"#1B1B23","gray-950":"#111114","gray-1000":"#000000","info-050":"#EAF2F9","info-100":"#D5E4F3","info-150":"#BFD7F0","info-200":"#A7CAEE","info-250":"#8DBDEF","info-300":"#6EB0F2","info-350":"#50A2F5","info-400":"#3593F4","info-450":"#1185ED","info-500":"#0077DE","info-550":"#0069CF","info-600":"#005BC0","info-650":"#0D4EAA","info-700":"#124394","info-750":"#163878","info-800":"#192F5A","info-850":"#192541","info-900":"#141B2D","info-950":"#0C111C","success-050":"#E8F1EA","success-100":"#CFE4D4","success-150":"#BAD9C1","success-200":"#A2CFAD","success-250":"#86C597","success-300":"#6CBA83","success-350":"#4FB070","success-400":"#40A363","success-450":"#309556","success-500":"#1E884A","success-550":"#027B3E","success-600":"#016D31","success-650":"#006024","success-700":"#005317","success-750":"#0D4511","success-800":"#11380E","success-850":"#132A11","success-900":"#101E0F","success-950":"#091209","warning-050":"#F8F0E9","warning-100":"#F1E0D3","warning-150":"#ECD0BC","warning-200":"#E8C0A4","warning-250":"#E8AE8A","warning-300":"#EB9970","warning-350":"#E98456","warning-400":"#E57036","warning-450":"#DA5E18","warning-500":"#CB5000","warning-550":"#BC4200","warning-600":"#AD3300","warning-650":"#9E2300","warning-700":"#882011","warning-750":"#731E16","warning-800":"#58201A","warning-850":"#401D18","warning-900":"#2E1714","warning-950":"#1D0F0D","error-050":"#F9EFEC","error-100":"#F4DFD9","error-150":"#F0CEC6","error-200":"#EEBCB2","error-250":"#EEA99D","error-300":"#EF9486","error-350":"#F37C6E","error-400":"#F65F53","error-450":"#F0463D","error-500":"#E82322","error-550":"#D7010E","error-600":"#C00100","error-650":"#AA0000","error-700":"#910C06","error-750":"#731E16","error-800":"#58201A","error-850":"#401D18","error-900":"#2E1714","error-950":"#1D0F0D","red-050":"#FAEFEE","red-100":"#F4DEDD","red-150":"#F1CDCB","red-200":"#EFBBBA","red-250":"#EEA8A8","red-300":"#F09394","red-350":"#F37B7E","red-400":"#EF6569","red-450":"#E94A55","red-500":"#DA3B49","red-550":"#CA2A3C","red-600":"#BB1330","red-650":"#A90021","red-700":"#910A13","red-750":"#731E16","red-800":"#58201A","red-850":"#411D18","red-900":"#2E1714","red-950":"#1D0F0D","orange-050":"#F8F0E9","orange-100":"#F1E0D3","orange-150":"#ECD0BD","orange-200":"#EABFA6","orange-250":"#EBAC90","orange-300":"#EC9772","orange-350":"#E5845A","orange-400":"#D6774D","orange-450":"#C86A40","orange-500":"#B95D33","orange-550":"#AB5025","orange-600":"#9D4315","orange-650":"#8F3600","orange-700":"#812900","orange-750":"#6C2511","orange-800":"#572017","orange-850":"#401D18","orange-900":"#2E1714","orange-950":"#1D0F0D","brown-050":"#F6F0E8","brown-100":"#F1E0D3","brown-150":"#EBD0BA","brown-200":"#E2C0A6","brown-250":"#D4B398","brown-300":"#C6A58B","brown-350":"#B8987E","brown-400":"#AA8B71","brown-450":"#9D7E65","brown-500":"#8F7158","brown-550":"#82654C","brown-600":"#765841","brown-650":"#694C35","brown-700":"#5D412A","brown-750":"#51361E","brown-800":"#452A13","brown-850":"#392008","brown-900":"#29180A","brown-950":"#1B0F08","yellow-050":"#F3F0E7","yellow-100":"#E9E2CF","yellow-150":"#E1D4B7","yellow-200":"#D9C599","yellow-250":"#D2B677","yellow-300":"#CAA756","yellow-350":"#C2972E","yellow-400":"#B98900","yellow-450":"#AB7B00","yellow-500":"#9D6E00","yellow-550":"#916100","yellow-600":"#855400","yellow-650":"#784700","yellow-700":"#6C3A00","yellow-750":"#5F2E00","yellow-800":"#512302","yellow-850":"#3E1D10","yellow-900":"#2D1711","yellow-950":"#1D0F0D","green-050":"#E6F1E9","green-100":"#CFE4D5","green-150":"#B8D8C1","green-200":"#A0CFAE","green-250":"#84C59A","green-300":"#65BA86","green-350":"#45B173","green-400":"#23A562","green-450":"#029755","green-500":"#008948","green-550":"#017B3B","green-600":"#006E2E","green-650":"#006022","green-700":"#005314","green-750":"#0D4510","green-800":"#11380E","green-850":"#132A11","green-900":"#101E0F","green-950":"#091209","blue-1-050":"#EBF1F9","blue-1-100":"#D6E4F4","blue-1-150":"#C1D7F0","blue-1-200":"#AACAEF","blue-1-250":"#8FBCEF","blue-1-300":"#7CAFEB","blue-1-350":"#68A1E4","blue-1-400":"#5B94D6","blue-1-450":"#4E86C7","blue-1-500":"#4279B9","blue-1-550":"#356CAC","blue-1-600":"#28609E","blue-1-650":"#1B5390","blue-1-700":"#0B4783","blue-1-750":"#0F3C6E","blue-1-800":"#133059","blue-1-850":"#152641","blue-1-900":"#121C2D","blue-1-950":"#0B111C","blue-2-050":"#E7F3F4","blue-2-100":"#CEE7E9","blue-2-150":"#B2DCE0","blue-2-200":"#91D1D7","blue-2-250":"#68C7D0","blue-2-300":"#43BBC5","blue-2-350":"#00AFBA","blue-2-400":"#01A0AA","blue-2-450":"#00929D","blue-2-500":"#00848F","blue-2-550":"#007682","blue-2-600":"#016874","blue-2-650":"#005B67","blue-2-700":"#004E5A","blue-2-750":"#00424E","blue-2-800":"#003642","blue-2-850":"#002A38","blue-2-900":"#061E28","blue-2-950":"#071219","purple-050":"#F7F0F6","purple-100":"#EEE0EE","purple-150":"#E7D1E7","purple-200":"#DBBFE4","purple-250":"#D3AEE2","purple-300":"#CB99E1","purple-350":"#C188D9","purple-400":"#B47BCB","purple-450":"#A66EBD","purple-500":"#9961AF","purple-550":"#8B55A1","purple-600":"#7E4894","purple-650":"#723C87","purple-700":"#633376","purple-750":"#552A65","purple-800":"#452551","purple-850":"#35213D","purple-900":"#261A2C","purple-950":"#17111C","pink-050":"#F8EFF4","pink-100":"#F0DFEA","pink-150":"#EACEDF","pink-200":"#E9BBD1","pink-250":"#E9A7C2","pink-300":"#E095B4","pink-350":"#D685A8","pink-400":"#C7799B","pink-450":"#B86C8D","pink-500":"#AA5F80","pink-550":"#9C5374","pink-600":"#8E4767","pink-650":"#813B5B","pink-700":"#732E4F","pink-750":"#632643","pink-800":"#521F38","pink-850":"#3E1C2B","pink-900":"#2D171F","pink-950":"#1C0E12","black-000":"#1B1B2300","black-050":"#1B1B230D","black-100":"#1B1B231A","black-150":"#1B1B2326","black-200":"#1B1B2333","black-250":"#1B1B2340","black-300":"#1B1B234D","black-350":"#1B1B2359","black-400":"#1B1B2366","black-450":"#1B1B2373","black-500":"#1B1B2380","black-550":"#1B1B238C","black-600":"#1B1B2399","black-650":"#1B1B23A6","black-700":"#1B1B23B2","black-750":"#1B1B23BF","black-800":"#1B1B23CC","black-850":"#1B1B23D9","black-900":"#1B1B23E5","black-950":"#111114F2","white-000":"#F8F8F900","white-050":"#F8F8F90D","white-100":"#F8F8F91A","white-150":"#F8F8F926","white-200":"#F8F8F933","white-250":"#F8F8F940","white-300":"#F8F8F94D","white-350":"#F8F8F959","white-400":"#F8F8F966","white-450":"#F8F8F973","white-500":"#F8F8F980","white-550":"#F8F8F98C","white-600":"#F8F8F999","white-650":"#F8F8F9A6","white-700":"#F8F8F9B2","white-750":"#F8F8F9BF","white-800":"#F8F8F9CC","white-850":"#F8F8F9D9","white-900":"#F8F8F9E5","white-950":"#F8F8F9F2","white-975":"#F8F8F9F9"},"transitions":{"ease-in":"cubic-bezier(0.32, 0, 0.67, 0)","ease-out":"cubic-bezier(0.33, 1, 0.68, 1)","ease-in-out":"cubic-bezier(0.65, 0, 0.35, 1)","duration":"250ms"},"font":{"sizes":{"xs":"0.75rem","sm":"0.875rem","md":"1rem","lg":"1.125rem","ml":"0.938rem","xl":"1.25rem","t":"0.6875rem","s":"0.75rem","h1":"2rem","h2":"1.75rem","h3":"1.5rem","h4":"1.375rem","h5":"1.25rem","h6":"1.125rem","xl-alt":"5rem","lg-alt":"4.5rem","md-alt":"4rem","sm-alt":"3.5rem","xs-alt":"3rem"},"weights":{"thin":100,"light":300,"regular":400,"medium":500,"bold":600,"extrabold":800,"black":800},"families":{"base":"Hanken Grotesk, Inter, Roboto Flex Variable, sans-serif","accent":"Hanken Grotesk, Inter, Roboto Flex Variable, sans-serif"}},"spacings":{"0":"0","l":"3rem","b":"1.625rem","m":"0.8125rem","s":"1rem","t":"0.5rem","st":"0.25rem","4xs":"0.125rem","3xs":"0.25rem","2xs":"0.375rem","xxxs":"0.25rem","xxs":"0.375rem","xs":"0.5rem","sm":"0.75rem","base":"1rem","md":"1.5rem","lg":"2rem","xl":"2.5rem","xxl":"3rem","xxxl":"3.5rem","2xl":"3rem","3xl":"3.5rem","4xl":"4rem","5xl":"4.5rem","6xl":"6rem","7xl":"7.5rem","none":"0","auto":"auto","bx":"2.2rem","full":"100%"},"breakpoints":{"xs":"480px","sm":"576px","md":"768px","lg":"992px","xl":"1200px","xxl":"1400px","xxs":"320px","mobile":"768px","tablet":"1024px"}},"contextuals":{"background":{"surface":{"primary":"#2F303D","secondary":"#25252F","tertiary":"#1B1B23"},"semantic":{"contextual":{"primary":"#F8F8F90D","primary-hover":"#F8F8F91A"},"overlay":{"primary":"#F8F8F90D","primary-hover":"#F8F8F91A"},"brand":{"primary":"#5E5CD0","primary-hover":"#4844AD","secondary":"#3E3B98","secondary-hover":"#36347D","tertiary":"#36347D","tertiary-hover":"#2D2F5F"},"neutral":{"primary":"#69697D","primary-hover":"#515164","secondary":"#454558","secondary-hover":"#3A3A4C","tertiary":"#3A3A4C","tertiary-hover":"#2F303D"},"info":{"primary":"#0069CF","primary-hover":"#0D4EAA","secondary":"#124394","secondary-hover":"#163878","tertiary":"#163878","tertiary-hover":"#192F5A"},"success":{"primary":"#027B3E","primary-hover":"#006024","secondary":"#005317","secondary-hover":"#0D4511","tertiary":"#0D4511","tertiary-hover":"#11380E"},"warning":{"primary":"#BC4200","primary-hover":"#9E2300","secondary":"#882011","secondary-hover":"#731E16","tertiary":"#731E16","tertiary-hover":"#58201A"},"error":{"primary":"#D7010E","primary-hover":"#AA0000","secondary":"#910C06","secondary-hover":"#731E16","tertiary":"#731E16","tertiary-hover":"#58201A"},"disabled":{"primary":"#3A3A4C","secondary":"#2F303D"}},"palette":{"brand":{"primary":"#8F94FD","secondary":"#7576EE","tertiary":"#5E5CD0"},"red":{"primary":"#F37B7E","secondary":"#E94A55","tertiary":"#CA2A3C"},"orange":{"primary":"#E5845A","secondary":"#C86A40","tertiary":"#AB5025"},"brown":{"primary":"#B8987E","secondary":"#9D7E65","tertiary":"#82654C"},"yellow":{"primary":"#C2972E","secondary":"#AB7B00","tertiary":"#916100"},"green":{"primary":"#45B173","secondary":"#029755","tertiary":"#017B3B"},"blue-1":{"primary":"#68A1E4","secondary":"#4E86C7","tertiary":"#356CAC"},"blue-2":{"primary":"#00AFBA","secondary":"#00929D","tertiary":"#007682"},"purple":{"primary":"#C188D9","secondary":"#A66EBD","tertiary":"#8B55A1"},"pink":{"primary":"#D685A8","secondary":"#B86C8D","tertiary":"#9C5374"},"gray":{"primary":"#9C9CB2","secondary":"#828297","tertiary":"#69697D"}}},"content":{"logo1":"#BEC5F0","logo2":"#BEC5F0","semantic":{"contextual":{"primary":"#1B1B23D9"},"overlay":{"primary":"#1B1B23D9"},"brand":{"primary":"#EEF1FA","secondary":"#DDE2F5","tertiary":"#AFB5F1","on-brand":"#EEF1FA"},"neutral":{"primary":"#F0F0F3","secondary":"#E2E2EA","tertiary":"#B7B7CB","on-neutral":"#F0F0F3"},"info":{"primary":"#EAF2F9","secondary":"#D5E4F3","tertiary":"#8DBDEF","on-info":"#EAF2F9"},"success":{"primary":"#E8F1EA","secondary":"#CFE4D4","tertiary":"#86C597","on-success":"#E8F1EA"},"warning":{"primary":"#F8F0E9","secondary":"#F1E0D3","tertiary":"#E8AE8A","on-warning":"#F8F0E9"},"error":{"primary":"#F9EFEC","secondary":"#F4DFD9","tertiary":"#EEA99D","on-error":"#F9EFEC"},"disabled":{"primary":"#5D5D70","secondary":"#1B1B234D"}},"palette":{"brand":{"primary":"#6969DF"},"red":{"primary":"#DA3B49"},"orange":{"primary":"#B95D33"},"brown":{"primary":"#8F7158"},"yellow":{"primary":"#9D6E00"},"green":{"primary":"#008948"},"blue-1":{"primary":"#4279B9"},"blue-2":{"primary":"#00848F"},"purple":{"primary":"#9961AF"},"pink":{"primary":"#AA5F80"},"gray":{"primary":"#75758A"}}},"border":{"surface":{"primary":"#3A3A4C"},"semantic":{"contextual":{"primary":"#1B1B2333"},"overlay":{"primary":"#1B1B2333"},"brand":{"primary":"#7576EE","secondary":"#534FC2","tertiary":"#3E3B98"},"neutral":{"primary":"#828297","secondary":"#5D5D70","tertiary":"#454558"},"info":{"primary":"#1185ED","secondary":"#005BC0","tertiary":"#124394"},"success":{"primary":"#309556","secondary":"#016D31","tertiary":"#005317"},"warning":{"primary":"#DA5E18","secondary":"#AD3300","tertiary":"#882011"},"error":{"primary":"#F0463D","secondary":"#C00100","tertiary":"#910C06"},"disabled":{"primary":"#2F303D"}}}},"components":{"modal":{"width-small":"342px"},"tooltip":{"padding":"4px 8px"},"button":{"medium-height":"40px","medium-text-height":"40px","border-radius":"4px","border-radius--active":"4px","border-radius--focus":"4px"},"resize-handle":{"hover--color":"#E2E2EA"},"datagrid":{"header--color":"#25252F","header--size":"12px","header--weight":"500","body--background-color-hover":"#F0F0F3"},"forms-checkbox":{"font-size":"0.875rem"},"forms-input":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-select":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-textarea":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-datepicker":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"badge":{"font-size":"0.75rem","border-radius":"12px","padding-inline":"0.5rem","padding-block":"0.375rem","accent":{"background-color":"#DDE2F5","color":"#534FC2"},"neutral":{"background-color":"#E2E2EA","color":"#5D5D70"},"danger":{"background-color":"#F4DFD9","color":"#C00100"},"success":{"background-color":"#CFE4D4","color":"#016D31"},"warning":{"background-color":"#F1E0D3","color":"#AD3300"},"info":{"background-color":"#D5E4F3","color":"#005BC0"}}}},"dsfr-light":{"globals":{"colors":{"logo-1-light":"#2845C1","logo-2-light":"#C83F49","logo-1-dark":"#95ABFF","logo-2-dark":"#E78087","brand-050":"#EDF0FF","brand-100":"#DAE2FF","brand-150":"#C8D3FF","brand-200":"#B5C4FF","brand-250":"#A2B6FF","brand-300":"#90A7FF","brand-350":"#7E98FF","brand-400":"#6C89FE","brand-450":"#5C7AF7","brand-500":"#4C6CEF","brand-550":"#3E5DE7","brand-600":"#304DDF","brand-650":"#2845C1","brand-700":"#223E9E","brand-750":"#1F367D","brand-800":"#1B2E5F","brand-850":"#172446","brand-900":"#121B30","brand-950":"#0C111A","gray-100":"#DFE2EA","gray-150":"#CFD5DE","gray-200":"#C1C7D3","gray-250":"#B2B9C7","gray-300":"#A4ABBC","gray-350":"#969EB0","gray-400":"#8891A4","gray-450":"#7B8498","gray-500":"#6D778C","gray-550":"#626A80","gray-600":"#555E74","gray-650":"#4A5267","gray-700":"#3F4759","gray-750":"#363B4C","gray-800":"#2B303D","gray-850":"#222631","gray-900":"#181B24","gray-950":"#0F1117","gray-1000":"#000000","info-050":"#E7F2FF","info-100":"#CFE5FF","info-150":"#B7D7FF","info-200":"#A0CAFE","info-250":"#8CBCF9","info-300":"#77AEF4","info-350":"#63A0EE","info-400":"#5092E7","info-450":"#4185DC","info-500":"#3677CC","info-550":"#2F6ABB","info-600":"#265EAA","info-650":"#28528F","info-700":"#274775","info-750":"#243C5E","info-800":"#20314A","info-850":"#1B2637","info-900":"#141C27","info-950":"#0D1118","success-050":"#DEF7E6","success-100":"#BAEECF","success-150":"#A5E2C0","success-200":"#95D4B3","success-250":"#85C6A7","success-300":"#74B99B","success-350":"#65AB8F","success-400":"#579E84","success-450":"#4B9079","success-500":"#40836F","success-550":"#367664","success-600":"#2B695A","success-650":"#2C5A50","success-700":"#2A4D45","success-750":"#26403C","success-800":"#213430","success-850":"#1B2826","success-900":"#151D1C","success-950":"#0D1212","warning-050":"#FFEEDF","warning-100":"#FFDCBE","warning-150":"#FFCA9C","warning-200":"#FFB778","warning-250":"#FDA54F","warning-300":"#F59425","warning-350":"#E78613","warning-400":"#D7790C","warning-450":"#C86C08","warning-500":"#B85F03","warning-550":"#A75400","warning-600":"#984800","warning-650":"#814112","warning-700":"#6C3A19","warning-750":"#58321C","warning-800":"#452A1A","warning-850":"#352117","warning-900":"#261813","warning-950":"#170F0C","error-050":"#FFEDEB","error-100":"#FFDAD7","error-150":"#FFC7C2","error-200":"#FFB3AD","error-250":"#FF9F99","error-300":"#FF8984","error-350":"#FF706E","error-400":"#FB5759","error-450":"#F63A45","error-500":"#E32C39","error-550":"#CF202D","error-600":"#BD0F23","error-650":"#9D2227","error-700":"#812727","error-750":"#672624","error-800":"#512220","error-850":"#3D1C1B","error-900":"#2A1614","error-950":"#190E0D","red-050":"#FFEDEB","red-100":"#FFDAD7","red-150":"#FFC7C2","red-200":"#FFB3AD","red-250":"#FF9F99","red-300":"#FF8984","red-350":"#FF706E","red-400":"#FB5759","red-450":"#F63A45","red-500":"#E32C39","red-550":"#CF202D","red-600":"#BD0F23","red-650":"#9D2227","red-700":"#812727","red-750":"#672624","red-800":"#512220","red-850":"#3D1C1B","red-900":"#410003","red-950":"#190E0D","orange-050":"#FCEDEB","orange-100":"#F8DCD7","orange-150":"#F1CCC5","orange-200":"#EABCB4","orange-250":"#E2ACA2","orange-300":"#DA9C92","orange-350":"#D28C81","orange-400":"#CA7C70","orange-450":"#BE6E62","orange-500":"#AE6257","orange-550":"#9E564D","orange-600":"#8F4B42","orange-650":"#79443D","orange-700":"#643C37","orange-750":"#513430","orange-800":"#412B28","orange-850":"#312220","orange-900":"#231918","orange-950":"#150F0F","brown-050":"#F9EFEA","brown-100":"#F3DFD3","brown-150":"#EACFC1","brown-200":"#E2BFAE","brown-250":"#D8B19C","brown-300":"#D0A189","brown-350":"#C3937B","brown-400":"#B5866D","brown-450":"#A77A62","brown-500":"#996D57","brown-550":"#8B614D","brown-600":"#7C5542","brown-650":"#6A4C3C","brown-700":"#594236","brown-750":"#49382F","brown-800":"#3B2E28","brown-850":"#2D2420","brown-900":"#201A18","brown-950":"#13100F","yellow-050":"#FDF1C5","yellow-100":"#FBE18E","yellow-150":"#F4D261","yellow-200":"#EAC244","yellow-250":"#DFB41B","yellow-300":"#D1A516","yellow-350":"#C49711","yellow-400":"#B78A0C","yellow-450":"#A87D07","yellow-500":"#9B6F02","yellow-550":"#8D6300","yellow-600":"#7F5600","yellow-650":"#6E4C11","yellow-700":"#5D4219","yellow-750":"#4D371B","yellow-800":"#3D2E1A","yellow-850":"#2F2417","yellow-900":"#221A12","yellow-950":"#14100C","green-050":"#E7F9B3","green-100":"#D5EC98","green-150":"#C5DE86","green-200":"#B5D174","green-250":"#A5C464","green-300":"#95B755","green-350":"#85AA45","green-400":"#769D39","green-450":"#688F30","green-500":"#5A8228","green-550":"#4D7621","green-600":"#416919","green-650":"#3A5B20","green-700":"#324E22","green-750":"#2C4122","green-800":"#24351D","green-850":"#1D2919","green-900":"#161E13","green-950":"#0E120C","blue-1-050":"#E7F2FF","blue-1-100":"#CFE5FF","blue-1-150":"#B7D7FF","blue-1-200":"#A0CAFE","blue-1-250":"#8CBCF9","blue-1-300":"#77AEF4","blue-1-350":"#63A0EE","blue-1-400":"#5092E7","blue-1-450":"#4185DC","blue-1-500":"#3677CC","blue-1-550":"#2F6ABB","blue-1-600":"#265EAA","blue-1-650":"#28528F","blue-1-700":"#274775","blue-1-750":"#243C5E","blue-1-800":"#20314A","blue-1-850":"#1B2637","blue-1-900":"#141C27","blue-1-950":"#0D1118","blue-2-050":"#E2F4FD","blue-2-100":"#C4E8F8","blue-2-150":"#AADCF2","blue-2-200":"#93CFEB","blue-2-250":"#7CC2E2","blue-2-300":"#6CB4D6","blue-2-350":"#5CA7C9","blue-2-400":"#5099BC","blue-2-450":"#458BAE","blue-2-500":"#3A7EA0","blue-2-550":"#327191","blue-2-600":"#286483","blue-2-650":"#2B5770","blue-2-700":"#294A5E","blue-2-750":"#263E4D","blue-2-800":"#22323D","blue-2-850":"#1C272E","blue-2-900":"#151D21","blue-2-950":"#0E1114","purple-050":"#F5EEFF","purple-100":"#ECDCFF","purple-150":"#E2CBFF","purple-200":"#D9B9FF","purple-250":"#D0A7FF","purple-300":"#C894FE","purple-350":"#BE83FA","purple-400":"#B570F5","purple-450":"#AB5EF0","purple-500":"#A04BE8","purple-550":"#933CDB","purple-600":"#8530C8","purple-650":"#7033A5","purple-700":"#5D3185","purple-750":"#4C2C6A","purple-800":"#3C2652","purple-850":"#2D203C","purple-900":"#21182A","purple-950":"#130F19","pink-050":"#FFEBF6","pink-100":"#FFD8ED","pink-150":"#FCC4E3","pink-200":"#F7B2D9","pink-250":"#F29FCE","pink-300":"#ED8CC3","pink-350":"#E779B8","pink-400":"#E264AD","pink-450":"#D2579E","pink-500":"#C24B8E","pink-550":"#B0417F","pink-600":"#9F3670","pink-650":"#873560","pink-700":"#6F3250","pink-750":"#5A2C43","pink-800":"#472635","pink-850":"#351F29","pink-900":"#26171D","pink-950":"#170F12","black-000":"#181B2400","black-050":"#181B240D","black-100":"#181B241A","black-150":"#181B2426","black-200":"#181B2433","black-250":"#181B2440","black-300":"#181B244D","black-350":"#181B2459","black-400":"#181B2466","black-450":"#181B2473","black-500":"#181B2480","black-550":"#181B248C","black-600":"#181B2499","black-650":"#181B24A6","black-700":"#181B24B2","black-750":"#181B24BF","black-800":"#181B24CC","black-850":"#181B24D9","black-900":"#181B24E5","black-950":"#0F1117F2","white-000":"#F6F8F900","white-050":"#F6F8F90D","white-100":"#F6F8F91A","white-150":"#F6F8F926","white-200":"#F6F8F933","white-250":"#F6F8F940","white-300":"#F6F8F94D","white-350":"#F6F8F959","white-400":"#F6F8F966","white-450":"#F6F8F973","white-500":"#F6F8F980","white-550":"#F6F8F98C","white-600":"#F6F8F999","white-650":"#F6F8F9A6","white-700":"#F6F8F9B2","white-750":"#F6F8F9BF","white-800":"#F6F8F9CC","white-850":"#F6F8F9D9","white-900":"#F6F8F9E5","white-950":"#F6F8F9F2","white-975":"#F6F8F9F9"},"font":{"sizes":{"xs":"0.75rem","sm":"0.875rem","md":"1rem","lg":"1.125rem","ml":"0.938rem","xl":"1.25rem","t":"0.6875rem","s":"0.75rem","h1":"2rem","h2":"1.75rem","h3":"1.5rem","h4":"1.375rem","h5":"1.25rem","h6":"1.125rem","xl-alt":"5rem","lg-alt":"4.5rem","md-alt":"4rem","sm-alt":"3.5rem","xs-alt":"3rem"},"weights":{"thin":100,"extrabold":800},"families":{"accent":"Marianne, Inter, Roboto Flex Variable, sans-serif","base":"Marianne, Inter, Roboto Flex Variable, sans-serif"}},"spacings":{"0":"0","none":"0","auto":"auto","bx":"2.2rem","full":"100%","4xs":"0.125rem","3xs":"0.25rem","2xs":"0.375rem","xs":"0.5rem","sm":"0.75rem","base":"1rem","md":"1.5rem","lg":"2rem","xl":"2.5rem","xxl":"3rem","2xl":"3rem","xxxl":"3.5rem","3xl":"3.5rem","4xl":"4rem","5xl":"4.5rem","6xl":"6rem","7xl":"7.5rem"},"breakpoints":{"xxs":"320px","xs":"480px","mobile":"768px","tablet":"1024px"}},"contextuals":{"background":{"surface":{"primary":"#FFFFFF","secondary":"#FFFFFF","tertiary":"#F8F8F9"},"semantic":{"overlay":{"primary":"#1B1B230D","primary-hover":"#1B1B231A"},"contextual":{"primary":"#1B1B230D","primary-hover":"#1B1B231A"},"brand":{"primary":"#5E5CD0","primary-hover":"#4844AD","secondary":"#DDE2F5","secondary-hover":"#CED3F1","tertiary":"#EEF1FA","tertiary-hover":"#DDE2F5"},"neutral":{"primary":"#69697D","primary-hover":"#515164","secondary":"#E2E2EA","secondary-hover":"#D3D4E0","tertiary":"#F0F0F3","tertiary-hover":"#E2E2EA"},"info":{"primary":"#0069CF","primary-hover":"#0D4EAA","secondary":"#D5E4F3","secondary-hover":"#BFD7F0","tertiary":"#EAF2F9","tertiary-hover":"#D5E4F3"},"success":{"primary":"#027B3E","primary-hover":"#006024","secondary":"#CFE4D4","secondary-hover":"#BAD9C1","tertiary":"#E8F1EA","tertiary-hover":"#CFE4D4"},"warning":{"primary":"#BC4200","primary-hover":"#9E2300","secondary":"#F1E0D3","secondary-hover":"#ECD0BC","tertiary":"#F8F0E9","tertiary-hover":"#F1E0D3"},"error":{"primary":"#D7010E","primary-hover":"#AA0000","secondary":"#F4DFD9","secondary-hover":"#F0CEC6","tertiary":"#F9EFEC","tertiary-hover":"#F4DFD9"},"disabled":{"primary":"#E2E2EA","secondary":"#F0F0F3"}},"palette":{"brand":{"primary":"#6969DF","secondary":"#8F94FD","tertiary":"#CED3F1"},"red":{"primary":"#DA3B49","secondary":"#F37B7E","tertiary":"#F1CDCB"},"orange":{"primary":"#B95D33","secondary":"#E5845A","tertiary":"#ECD0BD"},"brown":{"primary":"#8F7158","secondary":"#B8987E","tertiary":"#EBD0BA"},"yellow":{"primary":"#9D6E00","secondary":"#C2972E","tertiary":"#E1D4B7"},"green":{"primary":"#008948","secondary":"#45B173","tertiary":"#B8D8C1"},"blue-1":{"primary":"#4279B9","secondary":"#68A1E4","tertiary":"#C1D7F0"},"blue-2":{"primary":"#00848F","secondary":"#00AFBA","tertiary":"#B2DCE0"},"purple":{"primary":"#9961AF","secondary":"#C188D9","tertiary":"#E7D1E7"},"pink":{"primary":"#AA5F80","secondary":"#D685A8","tertiary":"#EACEDF"},"gray":{"primary":"#75758A","secondary":"#9C9CB2","tertiary":"#D3D4E0"}}},"content":{"logo1":"#4844AD","logo2":"#4844AD","semantic":{"contextual":{"primary":"#F8F8F9F2"},"overlay":{"primary":"#F8F8F9F2"},"brand":{"primary":"#3E3B98","secondary":"#534FC2","tertiary":"#5E5CD0","on-brand":"#EEF1FA"},"neutral":{"primary":"#25252F","secondary":"#5D5D70","tertiary":"#69697D","on-neutral":"#F0F0F3"},"info":{"primary":"#124394","secondary":"#005BC0","tertiary":"#0069CF","on-info":"#EAF2F9"},"success":{"primary":"#005317","secondary":"#016D31","tertiary":"#027B3E","on-success":"#E8F1EA"},"warning":{"primary":"#882011","secondary":"#AD3300","tertiary":"#BC4200","on-warning":"#F8F0E9"},"error":{"primary":"#910C06","secondary":"#C00100","tertiary":"#D7010E","on-error":"#F9EFEC"},"disabled":{"primary":"#A9A9BF","secondary":"#F8F8F980"}},"palette":{"brand":{"primary":"#6969DF"},"red":{"primary":"#DA3B49"},"orange":{"primary":"#B95D33"},"brown":{"primary":"#8F7158"},"yellow":{"primary":"#9D6E00"},"green":{"primary":"#008948"},"blue-1":{"primary":"#4279B9"},"blue-2":{"primary":"#00848F"},"purple":{"primary":"#9961AF"},"pink":{"primary":"#AA5F80"},"gray":{"primary":"#75758A"}}},"border":{"surface":{"primary":"#E2E2EA"},"semantic":{"contextual":{"primary":"#F8F8F933"},"overlay":{"primary":"#F8F8F933"},"brand":{"primary":"#5E5CD0","secondary":"#A0A5F6","tertiary":"#CED3F1"},"neutral":{"primary":"#69697D","secondary":"#A9A9BF","tertiary":"#D3D4E0"},"info":{"primary":"#0069CF","secondary":"#6EB0F2","tertiary":"#BFD7F0"},"success":{"primary":"#027B3E","secondary":"#6CBA83","tertiary":"#BAD9C1"},"warning":{"primary":"#BC4200","secondary":"#EB9970","tertiary":"#ECD0BC"},"error":{"primary":"#D7010E","secondary":"#EF9486","tertiary":"#F0CEC6"},"disabled":{"primary":"#E2E2EA"}}}},"components":{"modal":{"width-small":"342px"},"tooltip":{"padding":"4px 8px"},"button":{"medium-height":"40px","medium-text-height":"40px","border-radius":"4px","border-radius--active":"4px","border-radius--focus":"4px"},"resize-handle":{"hover--color":"#E2E2EA"},"datagrid":{"header--color":"#25252F","header--size":"12px","header--weight":"500","body--background-color-hover":"#F0F0F3"},"forms-checkbox":{"font-size":"0.875rem"},"forms-input":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-select":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-textarea":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-datepicker":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"badge":{"font-size":"0.75rem","border-radius":"12px","padding-inline":"0.5rem","padding-block":"0.375rem","accent":{"background-color":"#DDE2F5","color":"#534FC2"},"neutral":{"background-color":"#E2E2EA","color":"#5D5D70"},"danger":{"background-color":"#F4DFD9","color":"#C00100"},"success":{"background-color":"#CFE4D4","color":"#016D31"},"warning":{"background-color":"#F1E0D3","color":"#AD3300"},"info":{"background-color":"#D5E4F3","color":"#005BC0"}}}},"dsfr-dark":{"globals":{"colors":{"logo-1-light":"#2845C1","logo-2-light":"#C83F49","logo-1-dark":"#95ABFF","logo-2-dark":"#E78087","brand-050":"#EDF0FF","brand-100":"#DAE2FF","brand-150":"#C8D3FF","brand-200":"#B5C4FF","brand-250":"#A2B6FF","brand-300":"#90A7FF","brand-350":"#7E98FF","brand-400":"#6C89FE","brand-450":"#5C7AF7","brand-500":"#4C6CEF","brand-550":"#3E5DE7","brand-600":"#304DDF","brand-650":"#2845C1","brand-700":"#223E9E","brand-750":"#1F367D","brand-800":"#1B2E5F","brand-850":"#172446","brand-900":"#121B30","brand-950":"#0C111A","gray-100":"#DFE2EA","gray-150":"#CFD5DE","gray-200":"#C1C7D3","gray-250":"#B2B9C7","gray-300":"#A4ABBC","gray-350":"#969EB0","gray-400":"#8891A4","gray-450":"#7B8498","gray-500":"#6D778C","gray-550":"#626A80","gray-600":"#555E74","gray-650":"#4A5267","gray-700":"#3F4759","gray-750":"#363B4C","gray-800":"#2B303D","gray-850":"#222631","gray-900":"#181B24","gray-950":"#0F1117","gray-1000":"#000000","info-050":"#E7F2FF","info-100":"#CFE5FF","info-150":"#B7D7FF","info-200":"#A0CAFE","info-250":"#8CBCF9","info-300":"#77AEF4","info-350":"#63A0EE","info-400":"#5092E7","info-450":"#4185DC","info-500":"#3677CC","info-550":"#2F6ABB","info-600":"#265EAA","info-650":"#28528F","info-700":"#274775","info-750":"#243C5E","info-800":"#20314A","info-850":"#1B2637","info-900":"#141C27","info-950":"#0D1118","success-050":"#DEF7E6","success-100":"#BAEECF","success-150":"#A5E2C0","success-200":"#95D4B3","success-250":"#85C6A7","success-300":"#74B99B","success-350":"#65AB8F","success-400":"#579E84","success-450":"#4B9079","success-500":"#40836F","success-550":"#367664","success-600":"#2B695A","success-650":"#2C5A50","success-700":"#2A4D45","success-750":"#26403C","success-800":"#213430","success-850":"#1B2826","success-900":"#151D1C","success-950":"#0D1212","warning-050":"#FFEEDF","warning-100":"#FFDCBE","warning-150":"#FFCA9C","warning-200":"#FFB778","warning-250":"#FDA54F","warning-300":"#F59425","warning-350":"#E78613","warning-400":"#D7790C","warning-450":"#C86C08","warning-500":"#B85F03","warning-550":"#A75400","warning-600":"#984800","warning-650":"#814112","warning-700":"#6C3A19","warning-750":"#58321C","warning-800":"#452A1A","warning-850":"#352117","warning-900":"#261813","warning-950":"#170F0C","error-050":"#FFEDEB","error-100":"#FFDAD7","error-150":"#FFC7C2","error-200":"#FFB3AD","error-250":"#FF9F99","error-300":"#FF8984","error-350":"#FF706E","error-400":"#FB5759","error-450":"#F63A45","error-500":"#E32C39","error-550":"#CF202D","error-600":"#BD0F23","error-650":"#9D2227","error-700":"#812727","error-750":"#672624","error-800":"#512220","error-850":"#3D1C1B","error-900":"#2A1614","error-950":"#190E0D","red-050":"#FFEDEB","red-100":"#FFDAD7","red-150":"#FFC7C2","red-200":"#FFB3AD","red-250":"#FF9F99","red-300":"#FF8984","red-350":"#FF706E","red-400":"#FB5759","red-450":"#F63A45","red-500":"#E32C39","red-550":"#CF202D","red-600":"#BD0F23","red-650":"#9D2227","red-700":"#812727","red-750":"#672624","red-800":"#512220","red-850":"#3D1C1B","red-900":"#410003","red-950":"#190E0D","orange-050":"#FCEDEB","orange-100":"#F8DCD7","orange-150":"#F1CCC5","orange-200":"#EABCB4","orange-250":"#E2ACA2","orange-300":"#DA9C92","orange-350":"#D28C81","orange-400":"#CA7C70","orange-450":"#BE6E62","orange-500":"#AE6257","orange-550":"#9E564D","orange-600":"#8F4B42","orange-650":"#79443D","orange-700":"#643C37","orange-750":"#513430","orange-800":"#412B28","orange-850":"#312220","orange-900":"#231918","orange-950":"#150F0F","brown-050":"#F9EFEA","brown-100":"#F3DFD3","brown-150":"#EACFC1","brown-200":"#E2BFAE","brown-250":"#D8B19C","brown-300":"#D0A189","brown-350":"#C3937B","brown-400":"#B5866D","brown-450":"#A77A62","brown-500":"#996D57","brown-550":"#8B614D","brown-600":"#7C5542","brown-650":"#6A4C3C","brown-700":"#594236","brown-750":"#49382F","brown-800":"#3B2E28","brown-850":"#2D2420","brown-900":"#201A18","brown-950":"#13100F","yellow-050":"#FDF1C5","yellow-100":"#FBE18E","yellow-150":"#F4D261","yellow-200":"#EAC244","yellow-250":"#DFB41B","yellow-300":"#D1A516","yellow-350":"#C49711","yellow-400":"#B78A0C","yellow-450":"#A87D07","yellow-500":"#9B6F02","yellow-550":"#8D6300","yellow-600":"#7F5600","yellow-650":"#6E4C11","yellow-700":"#5D4219","yellow-750":"#4D371B","yellow-800":"#3D2E1A","yellow-850":"#2F2417","yellow-900":"#221A12","yellow-950":"#14100C","green-050":"#E7F9B3","green-100":"#D5EC98","green-150":"#C5DE86","green-200":"#B5D174","green-250":"#A5C464","green-300":"#95B755","green-350":"#85AA45","green-400":"#769D39","green-450":"#688F30","green-500":"#5A8228","green-550":"#4D7621","green-600":"#416919","green-650":"#3A5B20","green-700":"#324E22","green-750":"#2C4122","green-800":"#24351D","green-850":"#1D2919","green-900":"#161E13","green-950":"#0E120C","blue-1-050":"#E7F2FF","blue-1-100":"#CFE5FF","blue-1-150":"#B7D7FF","blue-1-200":"#A0CAFE","blue-1-250":"#8CBCF9","blue-1-300":"#77AEF4","blue-1-350":"#63A0EE","blue-1-400":"#5092E7","blue-1-450":"#4185DC","blue-1-500":"#3677CC","blue-1-550":"#2F6ABB","blue-1-600":"#265EAA","blue-1-650":"#28528F","blue-1-700":"#274775","blue-1-750":"#243C5E","blue-1-800":"#20314A","blue-1-850":"#1B2637","blue-1-900":"#141C27","blue-1-950":"#0D1118","blue-2-050":"#E2F4FD","blue-2-100":"#C4E8F8","blue-2-150":"#AADCF2","blue-2-200":"#93CFEB","blue-2-250":"#7CC2E2","blue-2-300":"#6CB4D6","blue-2-350":"#5CA7C9","blue-2-400":"#5099BC","blue-2-450":"#458BAE","blue-2-500":"#3A7EA0","blue-2-550":"#327191","blue-2-600":"#286483","blue-2-650":"#2B5770","blue-2-700":"#294A5E","blue-2-750":"#263E4D","blue-2-800":"#22323D","blue-2-850":"#1C272E","blue-2-900":"#151D21","blue-2-950":"#0E1114","purple-050":"#F5EEFF","purple-100":"#ECDCFF","purple-150":"#E2CBFF","purple-200":"#D9B9FF","purple-250":"#D0A7FF","purple-300":"#C894FE","purple-350":"#BE83FA","purple-400":"#B570F5","purple-450":"#AB5EF0","purple-500":"#A04BE8","purple-550":"#933CDB","purple-600":"#8530C8","purple-650":"#7033A5","purple-700":"#5D3185","purple-750":"#4C2C6A","purple-800":"#3C2652","purple-850":"#2D203C","purple-900":"#21182A","purple-950":"#130F19","pink-050":"#FFEBF6","pink-100":"#FFD8ED","pink-150":"#FCC4E3","pink-200":"#F7B2D9","pink-250":"#F29FCE","pink-300":"#ED8CC3","pink-350":"#E779B8","pink-400":"#E264AD","pink-450":"#D2579E","pink-500":"#C24B8E","pink-550":"#B0417F","pink-600":"#9F3670","pink-650":"#873560","pink-700":"#6F3250","pink-750":"#5A2C43","pink-800":"#472635","pink-850":"#351F29","pink-900":"#26171D","pink-950":"#170F12","black-000":"#181B2400","black-050":"#181B240D","black-100":"#181B241A","black-150":"#181B2426","black-200":"#181B2433","black-250":"#181B2440","black-300":"#181B244D","black-350":"#181B2459","black-400":"#181B2466","black-450":"#181B2473","black-500":"#181B2480","black-550":"#181B248C","black-600":"#181B2499","black-650":"#181B24A6","black-700":"#181B24B2","black-750":"#181B24BF","black-800":"#181B24CC","black-850":"#181B24D9","black-900":"#181B24E5","black-950":"#0F1117F2","white-000":"#F6F8F900","white-050":"#F6F8F90D","white-100":"#F6F8F91A","white-150":"#F6F8F926","white-200":"#F6F8F933","white-250":"#F6F8F940","white-300":"#F6F8F94D","white-350":"#F6F8F959","white-400":"#F6F8F966","white-450":"#F6F8F973","white-500":"#F6F8F980","white-550":"#F6F8F98C","white-600":"#F6F8F999","white-650":"#F6F8F9A6","white-700":"#F6F8F9B2","white-750":"#F6F8F9BF","white-800":"#F6F8F9CC","white-850":"#F6F8F9D9","white-900":"#F6F8F9E5","white-950":"#F6F8F9F2","white-975":"#F6F8F9F9"},"font":{"sizes":{"xs":"0.75rem","sm":"0.875rem","md":"1rem","lg":"1.125rem","ml":"0.938rem","xl":"1.25rem","t":"0.6875rem","s":"0.75rem","h1":"2rem","h2":"1.75rem","h3":"1.5rem","h4":"1.375rem","h5":"1.25rem","h6":"1.125rem","xl-alt":"5rem","lg-alt":"4.5rem","md-alt":"4rem","sm-alt":"3.5rem","xs-alt":"3rem"},"weights":{"thin":100,"extrabold":800},"families":{"accent":"Marianne, Inter, Roboto Flex Variable, sans-serif","base":"Marianne, Inter, Roboto Flex Variable, sans-serif"}},"spacings":{"0":"0","none":"0","auto":"auto","bx":"2.2rem","full":"100%","4xs":"0.125rem","3xs":"0.25rem","2xs":"0.375rem","xs":"0.5rem","sm":"0.75rem","base":"1rem","md":"1.5rem","lg":"2rem","xl":"2.5rem","xxl":"3rem","2xl":"3rem","xxxl":"3.5rem","3xl":"3.5rem","4xl":"4rem","5xl":"4.5rem","6xl":"6rem","7xl":"7.5rem"},"breakpoints":{"xxs":"320px","xs":"480px","mobile":"768px","tablet":"1024px"}},"contextuals":{"background":{"surface":{"primary":"#2F303D","secondary":"#25252F","tertiary":"#1B1B23"},"semantic":{"contextual":{"primary":"#F8F8F90D","primary-hover":"#F8F8F91A"},"overlay":{"primary":"#F8F8F90D","primary-hover":"#F8F8F91A"},"brand":{"primary":"#5E5CD0","primary-hover":"#4844AD","secondary":"#3E3B98","secondary-hover":"#36347D","tertiary":"#36347D","tertiary-hover":"#2D2F5F"},"neutral":{"primary":"#69697D","primary-hover":"#515164","secondary":"#454558","secondary-hover":"#3A3A4C","tertiary":"#3A3A4C","tertiary-hover":"#2F303D"},"info":{"primary":"#0069CF","primary-hover":"#0D4EAA","secondary":"#124394","secondary-hover":"#163878","tertiary":"#163878","tertiary-hover":"#192F5A"},"success":{"primary":"#027B3E","primary-hover":"#006024","secondary":"#005317","secondary-hover":"#0D4511","tertiary":"#0D4511","tertiary-hover":"#11380E"},"warning":{"primary":"#BC4200","primary-hover":"#9E2300","secondary":"#882011","secondary-hover":"#731E16","tertiary":"#731E16","tertiary-hover":"#58201A"},"error":{"primary":"#D7010E","primary-hover":"#AA0000","secondary":"#910C06","secondary-hover":"#731E16","tertiary":"#731E16","tertiary-hover":"#58201A"},"disabled":{"primary":"#3A3A4C","secondary":"#2F303D"}},"palette":{"brand":{"primary":"#8F94FD","secondary":"#7576EE","tertiary":"#5E5CD0"},"red":{"primary":"#F37B7E","secondary":"#E94A55","tertiary":"#CA2A3C"},"orange":{"primary":"#E5845A","secondary":"#C86A40","tertiary":"#AB5025"},"brown":{"primary":"#B8987E","secondary":"#9D7E65","tertiary":"#82654C"},"yellow":{"primary":"#C2972E","secondary":"#AB7B00","tertiary":"#916100"},"green":{"primary":"#45B173","secondary":"#029755","tertiary":"#017B3B"},"blue-1":{"primary":"#68A1E4","secondary":"#4E86C7","tertiary":"#356CAC"},"blue-2":{"primary":"#00AFBA","secondary":"#00929D","tertiary":"#007682"},"purple":{"primary":"#C188D9","secondary":"#A66EBD","tertiary":"#8B55A1"},"pink":{"primary":"#D685A8","secondary":"#B86C8D","tertiary":"#9C5374"},"gray":{"primary":"#9C9CB2","secondary":"#828297","tertiary":"#69697D"}}},"content":{"logo1":"#BEC5F0","logo2":"#BEC5F0","semantic":{"contextual":{"primary":"#1B1B23D9"},"overlay":{"primary":"#1B1B23D9"},"brand":{"primary":"#EEF1FA","secondary":"#DDE2F5","tertiary":"#AFB5F1","on-brand":"#EEF1FA"},"neutral":{"primary":"#F0F0F3","secondary":"#E2E2EA","tertiary":"#B7B7CB","on-neutral":"#F0F0F3"},"info":{"primary":"#EAF2F9","secondary":"#D5E4F3","tertiary":"#8DBDEF","on-info":"#EAF2F9"},"success":{"primary":"#E8F1EA","secondary":"#CFE4D4","tertiary":"#86C597","on-success":"#E8F1EA"},"warning":{"primary":"#F8F0E9","secondary":"#F1E0D3","tertiary":"#E8AE8A","on-warning":"#F8F0E9"},"error":{"primary":"#F9EFEC","secondary":"#F4DFD9","tertiary":"#EEA99D","on-error":"#F9EFEC"},"disabled":{"primary":"#5D5D70","secondary":"#1B1B234D"}},"palette":{"brand":{"primary":"#6969DF"},"red":{"primary":"#DA3B49"},"orange":{"primary":"#B95D33"},"brown":{"primary":"#8F7158"},"yellow":{"primary":"#9D6E00"},"green":{"primary":"#008948"},"blue-1":{"primary":"#4279B9"},"blue-2":{"primary":"#00848F"},"purple":{"primary":"#9961AF"},"pink":{"primary":"#AA5F80"},"gray":{"primary":"#75758A"}}},"border":{"surface":{"primary":"#3A3A4C"},"semantic":{"contextual":{"primary":"#1B1B2333"},"overlay":{"primary":"#1B1B2333"},"brand":{"primary":"#7576EE","secondary":"#534FC2","tertiary":"#3E3B98"},"neutral":{"primary":"#828297","secondary":"#5D5D70","tertiary":"#454558"},"info":{"primary":"#1185ED","secondary":"#005BC0","tertiary":"#124394"},"success":{"primary":"#309556","secondary":"#016D31","tertiary":"#005317"},"warning":{"primary":"#DA5E18","secondary":"#AD3300","tertiary":"#882011"},"error":{"primary":"#F0463D","secondary":"#C00100","tertiary":"#910C06"},"disabled":{"primary":"#2F303D"}}}},"components":{"modal":{"width-small":"342px"},"tooltip":{"padding":"4px 8px"},"button":{"medium-height":"40px","medium-text-height":"40px","border-radius":"4px","border-radius--active":"4px","border-radius--focus":"4px"},"resize-handle":{"hover--color":"#E2E2EA"},"datagrid":{"header--color":"#25252F","header--size":"12px","header--weight":"500","body--background-color-hover":"#F0F0F3"},"forms-checkbox":{"font-size":"0.875rem"},"forms-input":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-select":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-textarea":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-datepicker":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"badge":{"font-size":"0.75rem","border-radius":"12px","padding-inline":"0.5rem","padding-block":"0.375rem","accent":{"background-color":"#DDE2F5","color":"#534FC2"},"neutral":{"background-color":"#E2E2EA","color":"#5D5D70"},"danger":{"background-color":"#F4DFD9","color":"#C00100"},"success":{"background-color":"#CFE4D4","color":"#016D31"},"warning":{"background-color":"#F1E0D3","color":"#AD3300"},"info":{"background-color":"#D5E4F3","color":"#005BC0"}}}},"anct-light":{"globals":{"colors":{"logo-1-light":"#2845C1","logo-2-light":"#FBC63A","logo-1-dark":"#94A3E5","logo-2-dark":"#EFD183","brand-050":"#EEF0F9","brand-100":"#DDE2F5","brand-150":"#CBD4F1","brand-200":"#B8C6F0","brand-250":"#A5B7F2","brand-300":"#91A8F7","brand-350":"#7C98FE","brand-400":"#6A89FF","brand-450":"#5A7AFB","brand-500":"#4B6BF0","brand-550":"#3E5CE7","brand-600":"#3352D5","brand-650":"#2A47C0","brand-700":"#2340A3","brand-750":"#223A7F","brand-800":"#1F325F","brand-850":"#1B2845","brand-900":"#151E30","brand-950":"#0D121D","gray-000":"#FFFFFF","gray-025":"#F6F8FA","gray-050":"#ECF1F7","gray-100":"#DDE3F3","gray-150":"#CCD4EA","gray-200":"#BCC7E0","gray-250":"#AEB9D2","gray-300":"#A1ABC4","gray-350":"#949EB6","gray-400":"#8791A9","gray-450":"#7A849B","gray-500":"#6D778E","gray-550":"#616A81","gray-600":"#555E75","gray-650":"#495268","gray-700":"#3E475C","gray-750":"#333B50","gray-800":"#283044","gray-850":"#1E2539","gray-900":"#171B28","gray-950":"#0F1118","gray-1000":"#000000","info-050":"#E9F2F8","info-100":"#D2E5F1","info-150":"#C0D7F0","info-200":"#AEC8F0","info-250":"#9EB9F2","info-300":"#8DABEE","info-350":"#7B9DE9","info-400":"#6E8FDB","info-450":"#6282CD","info-500":"#5575BF","info-550":"#4968B1","info-600":"#3E5CA3","info-650":"#344F97","info-700":"#294389","info-750":"#243972","info-800":"#1D2F5B","info-850":"#1A2744","info-900":"#151D2F","info-950":"#0E131F","success-050":"#E7F1E9","success-100":"#CFE3D3","success-150":"#B9D8C0","success-200":"#A1CEAC","success-250":"#85C496","success-300":"#63BC7F","success-350":"#45B16B","success-400":"#1CA659","success-450":"#00984C","success-500":"#008A3F","success-550":"#007C32","success-600":"#006E24","success-650":"#016016","success-700":"#005305","success-750":"#0D450A","success-800":"#11380E","success-850":"#132A11","success-900":"#101E0F","success-950":"#091209","warning-050":"#F6F0E8","warning-100":"#EDE2D1","warning-150":"#E6D3B8","warning-200":"#E3C39F","warning-250":"#E3B082","warning-300":"#E19E5C","warning-350":"#D98E3F","warning-400":"#CF7D19","warning-450":"#C17000","warning-500":"#B36300","warning-550":"#A45600","warning-600":"#964900","warning-650":"#893C00","warning-700":"#7B2F00","warning-750":"#68270D","warning-800":"#562013","warning-850":"#411D18","warning-900":"#2E1714","warning-950":"#1D0F0D","error-050":"#F9EFEC","error-100":"#F4DFD9","error-150":"#F0CEC6","error-200":"#EEBCB2","error-250":"#EEA99D","error-300":"#EF9487","error-350":"#F37C6E","error-400":"#F65F53","error-450":"#EF443C","error-500":"#E0342E","error-550":"#D0201F","error-600":"#C0000C","error-650":"#AA0000","error-700":"#910C06","error-750":"#731E16","error-800":"#58201A","error-850":"#411D18","error-900":"#2E1714","error-950":"#1D0F0D","red-050":"#F9EFEC","red-100":"#F4DEDA","red-150":"#F0CDC9","red-200":"#EEBBB6","red-250":"#EEA8A2","red-300":"#F0938D","red-350":"#EC7E78","red-400":"#E46D67","red-450":"#D95B58","red-500":"#CA4E4B","red-550":"#BB403F","red-600":"#AC3233","red-650":"#9D2227","red-700":"#882023","red-750":"#721D1B","red-800":"#58201A","red-850":"#401D18","red-900":"#2E1714","red-950":"#1D0F0D","orange-050":"#F8F0E9","orange-100":"#F1E0D3","orange-150":"#ECD0BD","orange-200":"#E9C0A5","orange-250":"#E8AE8A","orange-300":"#EB9870","orange-350":"#EB845A","orange-400":"#E66E37","orange-450":"#DD5B16","orange-500":"#CE4D00","orange-550":"#BF3E00","orange-600":"#B02F00","orange-650":"#A11E00","orange-700":"#8A1E14","orange-750":"#731E16","orange-800":"#58201A","orange-850":"#401D18","orange-900":"#2E1714","orange-950":"#1D0F0D","brown-050":"#F5F0E8","brown-100":"#ECE2D1","brown-150":"#E9D1B9","brown-200":"#E3C19D","brown-250":"#DCB187","brown-300":"#D2A26F","brown-350":"#C49562","brown-400":"#B68855","brown-450":"#A97B48","brown-500":"#9B6E3B","brown-550":"#8E612F","brown-600":"#815521","brown-650":"#744913","brown-700":"#673D00","brown-750":"#5A3100","brown-800":"#4E2600","brown-850":"#3D1F0B","brown-900":"#2C170F","brown-950":"#1C0F0B","yellow-050":"#F3F0E7","yellow-100":"#E9E2CF","yellow-150":"#E0D4B7","yellow-200":"#DAC59A","yellow-250":"#D5B67A","yellow-300":"#D0A559","yellow-350":"#CC9331","yellow-400":"#C48400","yellow-450":"#B77600","yellow-500":"#AA6800","yellow-550":"#9D5A00","yellow-600":"#914D00","yellow-650":"#843F00","yellow-700":"#773200","yellow-750":"#6A2601","yellow-800":"#56210F","yellow-850":"#401D16","yellow-900":"#2E1714","yellow-950":"#1D0F0D","green-050":"#E3F1EF","green-100":"#CAE5E1","green-150":"#B0DBD4","green-200":"#91D1C7","green-250":"#6AC8BC","green-300":"#4DBCAF","green-350":"#3CAFA2","green-400":"#2AA194","green-450":"#109487","green-500":"#00867A","green-550":"#00786D","green-600":"#016A60","green-650":"#015D53","green-700":"#005047","green-750":"#00443B","green-800":"#00382F","green-850":"#002C25","green-900":"#041F1A","green-950":"#041310","blue-1-050":"#EAF2F9","blue-1-100":"#D4E4F3","blue-1-150":"#BFD7F0","blue-1-200":"#AAC9EF","blue-1-250":"#96BBF1","blue-1-300":"#82ACF6","blue-1-350":"#709BFE","blue-1-400":"#608BFF","blue-1-450":"#537BFB","blue-1-500":"#476DEC","blue-1-550":"#3C60DD","blue-1-600":"#3252CF","blue-1-650":"#2B48B9","blue-1-700":"#28409B","blue-1-750":"#24397E","blue-1-800":"#223260","blue-1-850":"#1F2A48","blue-1-900":"#191F32","blue-1-950":"#111320","blue-2-050":"#E5F2F3","blue-2-100":"#CDE6EC","blue-2-150":"#B7D9EA","blue-2-200":"#9BCDE7","blue-2-250":"#84C1E0","blue-2-300":"#6BB4D8","blue-2-350":"#5CA6CB","blue-2-400":"#4D99BC","blue-2-450":"#3E8CAE","blue-2-500":"#2F7FA2","blue-2-550":"#1F7295","blue-2-600":"#056688","blue-2-650":"#00597B","blue-2-700":"#004C6C","blue-2-750":"#003F5E","blue-2-800":"#003353","blue-2-850":"#0C273E","blue-2-900":"#0E1C2C","blue-2-950":"#0A121C","purple-050":"#EDF1FA","purple-100":"#DCE2F5","purple-150":"#CCD4F1","purple-200":"#BCC4F0","purple-250":"#ADB6F2","purple-300":"#9EA6F6","purple-350":"#8E95FD","purple-400":"#8083FF","purple-450":"#7173FF","purple-500":"#6665F1","purple-550":"#5B57E2","purple-600":"#5049D4","purple-650":"#4641BC","purple-700":"#3D39A2","purple-750":"#363680","purple-800":"#2E3162","purple-850":"#242848","purple-900":"#1C1E32","purple-950":"#121320","pink-050":"#F8EFF4","pink-100":"#F0DEE9","pink-150":"#EBCDDF","pink-200":"#E7BDD6","pink-250":"#E5A9CC","pink-300":"#E695C0","pink-350":"#EA7CAE","pink-400":"#E4659F","pink-450":"#DD4F93","pink-500":"#CD4085","pink-550":"#BE3279","pink-600":"#AE216D","pink-650":"#9B195D","pink-700":"#86164E","pink-750":"#6E1B3D","pink-800":"#551E31","pink-850":"#3F1C24","pink-900":"#2D161A","pink-950":"#1C0E10","black-000":"#191B2200","black-050":"#191B220D","black-100":"#191B221A","black-150":"#191B2226","black-200":"#191B2233","black-250":"#191B2240","black-300":"#191B224D","black-350":"#191B2259","black-400":"#191B2266","black-450":"#191B2273","black-500":"#191B2280","black-550":"#191B228C","black-600":"#191B2299","black-650":"#191B22A6","black-700":"#191B22B2","black-750":"#191B22BF","black-800":"#191B22CC","black-850":"#191B22D9","black-900":"#191B22E5","black-950":"#0F1115F2","white-000":"#F7F8F800","white-050":"#F7F8F80D","white-100":"#F7F8F81A","white-150":"#F7F8F826","white-200":"#F7F8F833","white-250":"#F7F8F840","white-300":"#F7F8F84D","white-350":"#F7F8F859","white-400":"#F7F8F866","white-450":"#F7F8F873","white-500":"#F7F8F880","white-550":"#F7F8F88C","white-600":"#F7F8F899","white-650":"#F7F8F8A6","white-700":"#F7F8F8B2","white-750":"#F7F8F8BF","white-800":"#F7F8F8CC","white-850":"#F7F8F8D9","white-900":"#F7F8F8E5","white-950":"#F7F8F8F2","white-975":"#F7F8F8F9"},"font":{"sizes":{"xs":"0.75rem","sm":"0.875rem","md":"1rem","lg":"1.125rem","ml":"0.938rem","xl":"1.25rem","t":"0.6875rem","s":"0.75rem","h1":"2rem","h2":"1.75rem","h3":"1.5rem","h4":"1.375rem","h5":"1.25rem","h6":"1.125rem","xl-alt":"5rem","lg-alt":"4.5rem","md-alt":"4rem","sm-alt":"3.5rem","xs-alt":"3rem"},"weights":{"thin":100,"extrabold":800},"families":{"accent":"Marianne, Inter, Roboto Flex Variable, sans-serif","base":"Marianne, Inter, Roboto Flex Variable, sans-serif"}},"spacings":{"0":"0","none":"0","auto":"auto","bx":"2.2rem","full":"100%","4xs":"0.125rem","3xs":"0.25rem","2xs":"0.375rem","xs":"0.5rem","sm":"0.75rem","base":"1rem","md":"1.5rem","lg":"2rem","xl":"2.5rem","xxl":"3rem","2xl":"3rem","xxxl":"3.5rem","3xl":"3.5rem","4xl":"4rem","5xl":"4.5rem","6xl":"6rem","7xl":"7.5rem"},"breakpoints":{"xxs":"320px","xs":"480px","mobile":"768px","tablet":"1024px"}},"contextuals":{"background":{"surface":{"primary":"#FFFFFF","secondary":"#FFFFFF","tertiary":"#F8F8F9"},"semantic":{"overlay":{"primary":"#1B1B230D","primary-hover":"#1B1B231A"},"contextual":{"primary":"#1B1B230D","primary-hover":"#1B1B231A"},"brand":{"primary":"#5E5CD0","primary-hover":"#4844AD","secondary":"#DDE2F5","secondary-hover":"#CED3F1","tertiary":"#EEF1FA","tertiary-hover":"#DDE2F5"},"neutral":{"primary":"#69697D","primary-hover":"#515164","secondary":"#E2E2EA","secondary-hover":"#D3D4E0","tertiary":"#F0F0F3","tertiary-hover":"#E2E2EA"},"info":{"primary":"#0069CF","primary-hover":"#0D4EAA","secondary":"#D5E4F3","secondary-hover":"#BFD7F0","tertiary":"#EAF2F9","tertiary-hover":"#D5E4F3"},"success":{"primary":"#027B3E","primary-hover":"#006024","secondary":"#CFE4D4","secondary-hover":"#BAD9C1","tertiary":"#E8F1EA","tertiary-hover":"#CFE4D4"},"warning":{"primary":"#BC4200","primary-hover":"#9E2300","secondary":"#F1E0D3","secondary-hover":"#ECD0BC","tertiary":"#F8F0E9","tertiary-hover":"#F1E0D3"},"error":{"primary":"#D7010E","primary-hover":"#AA0000","secondary":"#F4DFD9","secondary-hover":"#F0CEC6","tertiary":"#F9EFEC","tertiary-hover":"#F4DFD9"},"disabled":{"primary":"#E2E2EA","secondary":"#F0F0F3"}},"palette":{"brand":{"primary":"#6969DF","secondary":"#8F94FD","tertiary":"#CED3F1"},"red":{"primary":"#DA3B49","secondary":"#F37B7E","tertiary":"#F1CDCB"},"orange":{"primary":"#B95D33","secondary":"#E5845A","tertiary":"#ECD0BD"},"brown":{"primary":"#8F7158","secondary":"#B8987E","tertiary":"#EBD0BA"},"yellow":{"primary":"#9D6E00","secondary":"#C2972E","tertiary":"#E1D4B7"},"green":{"primary":"#008948","secondary":"#45B173","tertiary":"#B8D8C1"},"blue-1":{"primary":"#4279B9","secondary":"#68A1E4","tertiary":"#C1D7F0"},"blue-2":{"primary":"#00848F","secondary":"#00AFBA","tertiary":"#B2DCE0"},"purple":{"primary":"#9961AF","secondary":"#C188D9","tertiary":"#E7D1E7"},"pink":{"primary":"#AA5F80","secondary":"#D685A8","tertiary":"#EACEDF"},"gray":{"primary":"#75758A","secondary":"#9C9CB2","tertiary":"#D3D4E0"}}},"content":{"logo1":"#4844AD","logo2":"#4844AD","semantic":{"contextual":{"primary":"#F8F8F9F2"},"overlay":{"primary":"#F8F8F9F2"},"brand":{"primary":"#3E3B98","secondary":"#534FC2","tertiary":"#5E5CD0","on-brand":"#EEF1FA"},"neutral":{"primary":"#25252F","secondary":"#5D5D70","tertiary":"#69697D","on-neutral":"#F0F0F3"},"info":{"primary":"#124394","secondary":"#005BC0","tertiary":"#0069CF","on-info":"#EAF2F9"},"success":{"primary":"#005317","secondary":"#016D31","tertiary":"#027B3E","on-success":"#E8F1EA"},"warning":{"primary":"#882011","secondary":"#AD3300","tertiary":"#BC4200","on-warning":"#F8F0E9"},"error":{"primary":"#910C06","secondary":"#C00100","tertiary":"#D7010E","on-error":"#F9EFEC"},"disabled":{"primary":"#A9A9BF","secondary":"#F8F8F980"}},"palette":{"brand":{"primary":"#6969DF"},"red":{"primary":"#DA3B49"},"orange":{"primary":"#B95D33"},"brown":{"primary":"#8F7158"},"yellow":{"primary":"#9D6E00"},"green":{"primary":"#008948"},"blue-1":{"primary":"#4279B9"},"blue-2":{"primary":"#00848F"},"purple":{"primary":"#9961AF"},"pink":{"primary":"#AA5F80"},"gray":{"primary":"#75758A"}}},"border":{"surface":{"primary":"#E2E2EA"},"semantic":{"contextual":{"primary":"#F8F8F933"},"overlay":{"primary":"#F8F8F933"},"brand":{"primary":"#5E5CD0","secondary":"#A0A5F6","tertiary":"#CED3F1"},"neutral":{"primary":"#69697D","secondary":"#A9A9BF","tertiary":"#D3D4E0"},"info":{"primary":"#0069CF","secondary":"#6EB0F2","tertiary":"#BFD7F0"},"success":{"primary":"#027B3E","secondary":"#6CBA83","tertiary":"#BAD9C1"},"warning":{"primary":"#BC4200","secondary":"#EB9970","tertiary":"#ECD0BC"},"error":{"primary":"#D7010E","secondary":"#EF9486","tertiary":"#F0CEC6"},"disabled":{"primary":"#E2E2EA"}}}},"components":{"modal":{"width-small":"342px"},"tooltip":{"padding":"4px 8px"},"button":{"medium-height":"40px","medium-text-height":"40px","border-radius":"4px","border-radius--active":"4px","border-radius--focus":"4px"},"resize-handle":{"hover--color":"#E2E2EA"},"datagrid":{"header--color":"#25252F","header--size":"12px","header--weight":"500","body--background-color-hover":"#F0F0F3"},"forms-checkbox":{"font-size":"0.875rem"},"forms-input":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-select":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-textarea":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-datepicker":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"badge":{"font-size":"0.75rem","border-radius":"12px","padding-inline":"0.5rem","padding-block":"0.375rem","accent":{"background-color":"#DDE2F5","color":"#534FC2"},"neutral":{"background-color":"#E2E2EA","color":"#5D5D70"},"danger":{"background-color":"#F4DFD9","color":"#C00100"},"success":{"background-color":"#CFE4D4","color":"#016D31"},"warning":{"background-color":"#F1E0D3","color":"#AD3300"},"info":{"background-color":"#D5E4F3","color":"#005BC0"}}}},"anct-dark":{"globals":{"colors":{"logo-1-light":"#2845C1","logo-2-light":"#FBC63A","logo-1-dark":"#94A3E5","logo-2-dark":"#EFD183","brand-050":"#EEF0F9","brand-100":"#DDE2F5","brand-150":"#CBD4F1","brand-200":"#B8C6F0","brand-250":"#A5B7F2","brand-300":"#91A8F7","brand-350":"#7C98FE","brand-400":"#6A89FF","brand-450":"#5A7AFB","brand-500":"#4B6BF0","brand-550":"#3E5CE7","brand-600":"#3352D5","brand-650":"#2A47C0","brand-700":"#2340A3","brand-750":"#223A7F","brand-800":"#1F325F","brand-850":"#1B2845","brand-900":"#151E30","brand-950":"#0D121D","gray-000":"#FFFFFF","gray-025":"#F6F8FA","gray-050":"#ECF1F7","gray-100":"#DDE3F3","gray-150":"#CCD4EA","gray-200":"#BCC7E0","gray-250":"#AEB9D2","gray-300":"#A1ABC4","gray-350":"#949EB6","gray-400":"#8791A9","gray-450":"#7A849B","gray-500":"#6D778E","gray-550":"#616A81","gray-600":"#555E75","gray-650":"#495268","gray-700":"#3E475C","gray-750":"#333B50","gray-800":"#283044","gray-850":"#1E2539","gray-900":"#171B28","gray-950":"#0F1118","gray-1000":"#000000","info-050":"#E9F2F8","info-100":"#D2E5F1","info-150":"#C0D7F0","info-200":"#AEC8F0","info-250":"#9EB9F2","info-300":"#8DABEE","info-350":"#7B9DE9","info-400":"#6E8FDB","info-450":"#6282CD","info-500":"#5575BF","info-550":"#4968B1","info-600":"#3E5CA3","info-650":"#344F97","info-700":"#294389","info-750":"#243972","info-800":"#1D2F5B","info-850":"#1A2744","info-900":"#151D2F","info-950":"#0E131F","success-050":"#E7F1E9","success-100":"#CFE3D3","success-150":"#B9D8C0","success-200":"#A1CEAC","success-250":"#85C496","success-300":"#63BC7F","success-350":"#45B16B","success-400":"#1CA659","success-450":"#00984C","success-500":"#008A3F","success-550":"#007C32","success-600":"#006E24","success-650":"#016016","success-700":"#005305","success-750":"#0D450A","success-800":"#11380E","success-850":"#132A11","success-900":"#101E0F","success-950":"#091209","warning-050":"#F6F0E8","warning-100":"#EDE2D1","warning-150":"#E6D3B8","warning-200":"#E3C39F","warning-250":"#E3B082","warning-300":"#E19E5C","warning-350":"#D98E3F","warning-400":"#CF7D19","warning-450":"#C17000","warning-500":"#B36300","warning-550":"#A45600","warning-600":"#964900","warning-650":"#893C00","warning-700":"#7B2F00","warning-750":"#68270D","warning-800":"#562013","warning-850":"#411D18","warning-900":"#2E1714","warning-950":"#1D0F0D","error-050":"#F9EFEC","error-100":"#F4DFD9","error-150":"#F0CEC6","error-200":"#EEBCB2","error-250":"#EEA99D","error-300":"#EF9487","error-350":"#F37C6E","error-400":"#F65F53","error-450":"#EF443C","error-500":"#E0342E","error-550":"#D0201F","error-600":"#C0000C","error-650":"#AA0000","error-700":"#910C06","error-750":"#731E16","error-800":"#58201A","error-850":"#411D18","error-900":"#2E1714","error-950":"#1D0F0D","red-050":"#F9EFEC","red-100":"#F4DEDA","red-150":"#F0CDC9","red-200":"#EEBBB6","red-250":"#EEA8A2","red-300":"#F0938D","red-350":"#EC7E78","red-400":"#E46D67","red-450":"#D95B58","red-500":"#CA4E4B","red-550":"#BB403F","red-600":"#AC3233","red-650":"#9D2227","red-700":"#882023","red-750":"#721D1B","red-800":"#58201A","red-850":"#401D18","red-900":"#2E1714","red-950":"#1D0F0D","orange-050":"#F8F0E9","orange-100":"#F1E0D3","orange-150":"#ECD0BD","orange-200":"#E9C0A5","orange-250":"#E8AE8A","orange-300":"#EB9870","orange-350":"#EB845A","orange-400":"#E66E37","orange-450":"#DD5B16","orange-500":"#CE4D00","orange-550":"#BF3E00","orange-600":"#B02F00","orange-650":"#A11E00","orange-700":"#8A1E14","orange-750":"#731E16","orange-800":"#58201A","orange-850":"#401D18","orange-900":"#2E1714","orange-950":"#1D0F0D","brown-050":"#F5F0E8","brown-100":"#ECE2D1","brown-150":"#E9D1B9","brown-200":"#E3C19D","brown-250":"#DCB187","brown-300":"#D2A26F","brown-350":"#C49562","brown-400":"#B68855","brown-450":"#A97B48","brown-500":"#9B6E3B","brown-550":"#8E612F","brown-600":"#815521","brown-650":"#744913","brown-700":"#673D00","brown-750":"#5A3100","brown-800":"#4E2600","brown-850":"#3D1F0B","brown-900":"#2C170F","brown-950":"#1C0F0B","yellow-050":"#F3F0E7","yellow-100":"#E9E2CF","yellow-150":"#E0D4B7","yellow-200":"#DAC59A","yellow-250":"#D5B67A","yellow-300":"#D0A559","yellow-350":"#CC9331","yellow-400":"#C48400","yellow-450":"#B77600","yellow-500":"#AA6800","yellow-550":"#9D5A00","yellow-600":"#914D00","yellow-650":"#843F00","yellow-700":"#773200","yellow-750":"#6A2601","yellow-800":"#56210F","yellow-850":"#401D16","yellow-900":"#2E1714","yellow-950":"#1D0F0D","green-050":"#E3F1EF","green-100":"#CAE5E1","green-150":"#B0DBD4","green-200":"#91D1C7","green-250":"#6AC8BC","green-300":"#4DBCAF","green-350":"#3CAFA2","green-400":"#2AA194","green-450":"#109487","green-500":"#00867A","green-550":"#00786D","green-600":"#016A60","green-650":"#015D53","green-700":"#005047","green-750":"#00443B","green-800":"#00382F","green-850":"#002C25","green-900":"#041F1A","green-950":"#041310","blue-1-050":"#EAF2F9","blue-1-100":"#D4E4F3","blue-1-150":"#BFD7F0","blue-1-200":"#AAC9EF","blue-1-250":"#96BBF1","blue-1-300":"#82ACF6","blue-1-350":"#709BFE","blue-1-400":"#608BFF","blue-1-450":"#537BFB","blue-1-500":"#476DEC","blue-1-550":"#3C60DD","blue-1-600":"#3252CF","blue-1-650":"#2B48B9","blue-1-700":"#28409B","blue-1-750":"#24397E","blue-1-800":"#223260","blue-1-850":"#1F2A48","blue-1-900":"#191F32","blue-1-950":"#111320","blue-2-050":"#E5F2F3","blue-2-100":"#CDE6EC","blue-2-150":"#B7D9EA","blue-2-200":"#9BCDE7","blue-2-250":"#84C1E0","blue-2-300":"#6BB4D8","blue-2-350":"#5CA6CB","blue-2-400":"#4D99BC","blue-2-450":"#3E8CAE","blue-2-500":"#2F7FA2","blue-2-550":"#1F7295","blue-2-600":"#056688","blue-2-650":"#00597B","blue-2-700":"#004C6C","blue-2-750":"#003F5E","blue-2-800":"#003353","blue-2-850":"#0C273E","blue-2-900":"#0E1C2C","blue-2-950":"#0A121C","purple-050":"#EDF1FA","purple-100":"#DCE2F5","purple-150":"#CCD4F1","purple-200":"#BCC4F0","purple-250":"#ADB6F2","purple-300":"#9EA6F6","purple-350":"#8E95FD","purple-400":"#8083FF","purple-450":"#7173FF","purple-500":"#6665F1","purple-550":"#5B57E2","purple-600":"#5049D4","purple-650":"#4641BC","purple-700":"#3D39A2","purple-750":"#363680","purple-800":"#2E3162","purple-850":"#242848","purple-900":"#1C1E32","purple-950":"#121320","pink-050":"#F8EFF4","pink-100":"#F0DEE9","pink-150":"#EBCDDF","pink-200":"#E7BDD6","pink-250":"#E5A9CC","pink-300":"#E695C0","pink-350":"#EA7CAE","pink-400":"#E4659F","pink-450":"#DD4F93","pink-500":"#CD4085","pink-550":"#BE3279","pink-600":"#AE216D","pink-650":"#9B195D","pink-700":"#86164E","pink-750":"#6E1B3D","pink-800":"#551E31","pink-850":"#3F1C24","pink-900":"#2D161A","pink-950":"#1C0E10","black-000":"#191B2200","black-050":"#191B220D","black-100":"#191B221A","black-150":"#191B2226","black-200":"#191B2233","black-250":"#191B2240","black-300":"#191B224D","black-350":"#191B2259","black-400":"#191B2266","black-450":"#191B2273","black-500":"#191B2280","black-550":"#191B228C","black-600":"#191B2299","black-650":"#191B22A6","black-700":"#191B22B2","black-750":"#191B22BF","black-800":"#191B22CC","black-850":"#191B22D9","black-900":"#191B22E5","black-950":"#0F1115F2","white-000":"#F7F8F800","white-050":"#F7F8F80D","white-100":"#F7F8F81A","white-150":"#F7F8F826","white-200":"#F7F8F833","white-250":"#F7F8F840","white-300":"#F7F8F84D","white-350":"#F7F8F859","white-400":"#F7F8F866","white-450":"#F7F8F873","white-500":"#F7F8F880","white-550":"#F7F8F88C","white-600":"#F7F8F899","white-650":"#F7F8F8A6","white-700":"#F7F8F8B2","white-750":"#F7F8F8BF","white-800":"#F7F8F8CC","white-850":"#F7F8F8D9","white-900":"#F7F8F8E5","white-950":"#F7F8F8F2","white-975":"#F7F8F8F9"},"font":{"sizes":{"xs":"0.75rem","sm":"0.875rem","md":"1rem","lg":"1.125rem","ml":"0.938rem","xl":"1.25rem","t":"0.6875rem","s":"0.75rem","h1":"2rem","h2":"1.75rem","h3":"1.5rem","h4":"1.375rem","h5":"1.25rem","h6":"1.125rem","xl-alt":"5rem","lg-alt":"4.5rem","md-alt":"4rem","sm-alt":"3.5rem","xs-alt":"3rem"},"weights":{"thin":100,"extrabold":800},"families":{"accent":"Marianne, Inter, Roboto Flex Variable, sans-serif","base":"Marianne, Inter, Roboto Flex Variable, sans-serif"}},"spacings":{"0":"0","none":"0","auto":"auto","bx":"2.2rem","full":"100%","4xs":"0.125rem","3xs":"0.25rem","2xs":"0.375rem","xs":"0.5rem","sm":"0.75rem","base":"1rem","md":"1.5rem","lg":"2rem","xl":"2.5rem","xxl":"3rem","2xl":"3rem","xxxl":"3.5rem","3xl":"3.5rem","4xl":"4rem","5xl":"4.5rem","6xl":"6rem","7xl":"7.5rem"},"breakpoints":{"xxs":"320px","xs":"480px","mobile":"768px","tablet":"1024px"}},"contextuals":{"background":{"surface":{"primary":"#2F303D","secondary":"#25252F","tertiary":"#1B1B23"},"semantic":{"contextual":{"primary":"#F8F8F90D","primary-hover":"#F8F8F91A"},"overlay":{"primary":"#F8F8F90D","primary-hover":"#F8F8F91A"},"brand":{"primary":"#5E5CD0","primary-hover":"#4844AD","secondary":"#3E3B98","secondary-hover":"#36347D","tertiary":"#36347D","tertiary-hover":"#2D2F5F"},"neutral":{"primary":"#69697D","primary-hover":"#515164","secondary":"#454558","secondary-hover":"#3A3A4C","tertiary":"#3A3A4C","tertiary-hover":"#2F303D"},"info":{"primary":"#0069CF","primary-hover":"#0D4EAA","secondary":"#124394","secondary-hover":"#163878","tertiary":"#163878","tertiary-hover":"#192F5A"},"success":{"primary":"#027B3E","primary-hover":"#006024","secondary":"#005317","secondary-hover":"#0D4511","tertiary":"#0D4511","tertiary-hover":"#11380E"},"warning":{"primary":"#BC4200","primary-hover":"#9E2300","secondary":"#882011","secondary-hover":"#731E16","tertiary":"#731E16","tertiary-hover":"#58201A"},"error":{"primary":"#D7010E","primary-hover":"#AA0000","secondary":"#910C06","secondary-hover":"#731E16","tertiary":"#731E16","tertiary-hover":"#58201A"},"disabled":{"primary":"#3A3A4C","secondary":"#2F303D"}},"palette":{"brand":{"primary":"#8F94FD","secondary":"#7576EE","tertiary":"#5E5CD0"},"red":{"primary":"#F37B7E","secondary":"#E94A55","tertiary":"#CA2A3C"},"orange":{"primary":"#E5845A","secondary":"#C86A40","tertiary":"#AB5025"},"brown":{"primary":"#B8987E","secondary":"#9D7E65","tertiary":"#82654C"},"yellow":{"primary":"#C2972E","secondary":"#AB7B00","tertiary":"#916100"},"green":{"primary":"#45B173","secondary":"#029755","tertiary":"#017B3B"},"blue-1":{"primary":"#68A1E4","secondary":"#4E86C7","tertiary":"#356CAC"},"blue-2":{"primary":"#00AFBA","secondary":"#00929D","tertiary":"#007682"},"purple":{"primary":"#C188D9","secondary":"#A66EBD","tertiary":"#8B55A1"},"pink":{"primary":"#D685A8","secondary":"#B86C8D","tertiary":"#9C5374"},"gray":{"primary":"#9C9CB2","secondary":"#828297","tertiary":"#69697D"}}},"content":{"logo1":"#BEC5F0","logo2":"#BEC5F0","semantic":{"contextual":{"primary":"#1B1B23D9"},"overlay":{"primary":"#1B1B23D9"},"brand":{"primary":"#EEF1FA","secondary":"#DDE2F5","tertiary":"#AFB5F1","on-brand":"#EEF1FA"},"neutral":{"primary":"#F0F0F3","secondary":"#E2E2EA","tertiary":"#B7B7CB","on-neutral":"#F0F0F3"},"info":{"primary":"#EAF2F9","secondary":"#D5E4F3","tertiary":"#8DBDEF","on-info":"#EAF2F9"},"success":{"primary":"#E8F1EA","secondary":"#CFE4D4","tertiary":"#86C597","on-success":"#E8F1EA"},"warning":{"primary":"#F8F0E9","secondary":"#F1E0D3","tertiary":"#E8AE8A","on-warning":"#F8F0E9"},"error":{"primary":"#F9EFEC","secondary":"#F4DFD9","tertiary":"#EEA99D","on-error":"#F9EFEC"},"disabled":{"primary":"#5D5D70","secondary":"#1B1B234D"}},"palette":{"brand":{"primary":"#6969DF"},"red":{"primary":"#DA3B49"},"orange":{"primary":"#B95D33"},"brown":{"primary":"#8F7158"},"yellow":{"primary":"#9D6E00"},"green":{"primary":"#008948"},"blue-1":{"primary":"#4279B9"},"blue-2":{"primary":"#00848F"},"purple":{"primary":"#9961AF"},"pink":{"primary":"#AA5F80"},"gray":{"primary":"#75758A"}}},"border":{"surface":{"primary":"#3A3A4C"},"semantic":{"contextual":{"primary":"#1B1B2333"},"overlay":{"primary":"#1B1B2333"},"brand":{"primary":"#7576EE","secondary":"#534FC2","tertiary":"#3E3B98"},"neutral":{"primary":"#828297","secondary":"#5D5D70","tertiary":"#454558"},"info":{"primary":"#1185ED","secondary":"#005BC0","tertiary":"#124394"},"success":{"primary":"#309556","secondary":"#016D31","tertiary":"#005317"},"warning":{"primary":"#DA5E18","secondary":"#AD3300","tertiary":"#882011"},"error":{"primary":"#F0463D","secondary":"#C00100","tertiary":"#910C06"},"disabled":{"primary":"#2F303D"}}}},"components":{"modal":{"width-small":"342px"},"tooltip":{"padding":"4px 8px"},"button":{"medium-height":"40px","medium-text-height":"40px","border-radius":"4px","border-radius--active":"4px","border-radius--focus":"4px"},"resize-handle":{"hover--color":"#E2E2EA"},"datagrid":{"header--color":"#25252F","header--size":"12px","header--weight":"500","body--background-color-hover":"#F0F0F3"},"forms-checkbox":{"font-size":"0.875rem"},"forms-input":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-select":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-textarea":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-datepicker":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"badge":{"font-size":"0.75rem","border-radius":"12px","padding-inline":"0.5rem","padding-block":"0.375rem","accent":{"background-color":"#DDE2F5","color":"#534FC2"},"neutral":{"background-color":"#E2E2EA","color":"#5D5D70"},"danger":{"background-color":"#F4DFD9","color":"#C00100"},"success":{"background-color":"#CFE4D4","color":"#016D31"},"warning":{"background-color":"#F1E0D3","color":"#AD3300"},"info":{"background-color":"#D5E4F3","color":"#005BC0"}}}}}}; +export const tokens = {"themes":{"default":{"globals":{"colors":{"logo-1-light":"#4844AD","logo-2-light":"#4844AD","logo-1-dark":"#BEC5F0","logo-2-dark":"#BEC5F0","brand-050":"#EEF1FA","brand-100":"#DDE2F5","brand-150":"#CED3F1","brand-200":"#BEC5F0","brand-250":"#AFB5F1","brand-300":"#A0A5F6","brand-350":"#8F94FD","brand-400":"#8184FC","brand-450":"#7576EE","brand-500":"#6969DF","brand-550":"#5E5CD0","brand-600":"#534FC2","brand-650":"#4844AD","brand-700":"#3E3B98","brand-750":"#36347D","brand-800":"#2D2F5F","brand-850":"#262848","brand-900":"#1C1E32","brand-950":"#11131F","gray-000":"#FFFFFF","gray-025":"#F8F8F9","gray-050":"#F0F0F3","gray-100":"#E2E2EA","gray-150":"#D3D4E0","gray-200":"#C5C6D5","gray-250":"#B7B7CB","gray-300":"#A9A9BF","gray-350":"#9C9CB2","gray-400":"#8F8FA4","gray-450":"#828297","gray-500":"#75758A","gray-550":"#69697D","gray-600":"#5D5D70","gray-650":"#515164","gray-700":"#454558","gray-750":"#3A3A4C","gray-800":"#2F303D","gray-850":"#25252F","gray-900":"#1B1B23","gray-950":"#111114","gray-1000":"#000000","info-050":"#EAF2F9","info-100":"#D5E4F3","info-150":"#BFD7F0","info-200":"#A7CAEE","info-250":"#8DBDEF","info-300":"#6EB0F2","info-350":"#50A2F5","info-400":"#3593F4","info-450":"#1185ED","info-500":"#0077DE","info-550":"#0069CF","info-600":"#005BC0","info-650":"#0D4EAA","info-700":"#124394","info-750":"#163878","info-800":"#192F5A","info-850":"#192541","info-900":"#141B2D","info-950":"#0C111C","success-050":"#E8F1EA","success-100":"#CFE4D4","success-150":"#BAD9C1","success-200":"#A2CFAD","success-250":"#86C597","success-300":"#6CBA83","success-350":"#4FB070","success-400":"#40A363","success-450":"#309556","success-500":"#1E884A","success-550":"#027B3E","success-600":"#016D31","success-650":"#006024","success-700":"#005317","success-750":"#0D4511","success-800":"#11380E","success-850":"#132A11","success-900":"#101E0F","success-950":"#091209","warning-050":"#F8F0E9","warning-100":"#F1E0D3","warning-150":"#ECD0BC","warning-200":"#E8C0A4","warning-250":"#E8AE8A","warning-300":"#EB9970","warning-350":"#E98456","warning-400":"#E57036","warning-450":"#DA5E18","warning-500":"#CB5000","warning-550":"#BC4200","warning-600":"#AD3300","warning-650":"#9E2300","warning-700":"#882011","warning-750":"#731E16","warning-800":"#58201A","warning-850":"#401D18","warning-900":"#2E1714","warning-950":"#1D0F0D","error-050":"#F9EFEC","error-100":"#F4DFD9","error-150":"#F0CEC6","error-200":"#EEBCB2","error-250":"#EEA99D","error-300":"#EF9486","error-350":"#F37C6E","error-400":"#F65F53","error-450":"#F0463D","error-500":"#E82322","error-550":"#D7010E","error-600":"#C00100","error-650":"#AA0000","error-700":"#910C06","error-750":"#731E16","error-800":"#58201A","error-850":"#401D18","error-900":"#2E1714","error-950":"#1D0F0D","red-050":"#FAEFEE","red-100":"#F4DEDD","red-150":"#F1CDCB","red-200":"#EFBBBA","red-250":"#EEA8A8","red-300":"#F09394","red-350":"#F37B7E","red-400":"#EF6569","red-450":"#E94A55","red-500":"#DA3B49","red-550":"#CA2A3C","red-600":"#BB1330","red-650":"#A90021","red-700":"#910A13","red-750":"#731E16","red-800":"#58201A","red-850":"#411D18","red-900":"#2E1714","red-950":"#1D0F0D","orange-050":"#F8F0E9","orange-100":"#F1E0D3","orange-150":"#ECD0BD","orange-200":"#EABFA6","orange-250":"#EBAC90","orange-300":"#EC9772","orange-350":"#E5845A","orange-400":"#D6774D","orange-450":"#C86A40","orange-500":"#B95D33","orange-550":"#AB5025","orange-600":"#9D4315","orange-650":"#8F3600","orange-700":"#812900","orange-750":"#6C2511","orange-800":"#572017","orange-850":"#401D18","orange-900":"#2E1714","orange-950":"#1D0F0D","brown-050":"#F6F0E8","brown-100":"#F1E0D3","brown-150":"#EBD0BA","brown-200":"#E2C0A6","brown-250":"#D4B398","brown-300":"#C6A58B","brown-350":"#B8987E","brown-400":"#AA8B71","brown-450":"#9D7E65","brown-500":"#8F7158","brown-550":"#82654C","brown-600":"#765841","brown-650":"#694C35","brown-700":"#5D412A","brown-750":"#51361E","brown-800":"#452A13","brown-850":"#392008","brown-900":"#29180A","brown-950":"#1B0F08","yellow-050":"#F3F0E7","yellow-100":"#E9E2CF","yellow-150":"#E1D4B7","yellow-200":"#D9C599","yellow-250":"#D2B677","yellow-300":"#CAA756","yellow-350":"#C2972E","yellow-400":"#B98900","yellow-450":"#AB7B00","yellow-500":"#9D6E00","yellow-550":"#916100","yellow-600":"#855400","yellow-650":"#784700","yellow-700":"#6C3A00","yellow-750":"#5F2E00","yellow-800":"#512302","yellow-850":"#3E1D10","yellow-900":"#2D1711","yellow-950":"#1D0F0D","green-050":"#E6F1E9","green-100":"#CFE4D5","green-150":"#B8D8C1","green-200":"#A0CFAE","green-250":"#84C59A","green-300":"#65BA86","green-350":"#45B173","green-400":"#23A562","green-450":"#029755","green-500":"#008948","green-550":"#017B3B","green-600":"#006E2E","green-650":"#006022","green-700":"#005314","green-750":"#0D4510","green-800":"#11380E","green-850":"#132A11","green-900":"#101E0F","green-950":"#091209","blue-1-050":"#EBF1F9","blue-1-100":"#D6E4F4","blue-1-150":"#C1D7F0","blue-1-200":"#AACAEF","blue-1-250":"#8FBCEF","blue-1-300":"#7CAFEB","blue-1-350":"#68A1E4","blue-1-400":"#5B94D6","blue-1-450":"#4E86C7","blue-1-500":"#4279B9","blue-1-550":"#356CAC","blue-1-600":"#28609E","blue-1-650":"#1B5390","blue-1-700":"#0B4783","blue-1-750":"#0F3C6E","blue-1-800":"#133059","blue-1-850":"#152641","blue-1-900":"#121C2D","blue-1-950":"#0B111C","blue-2-050":"#E7F3F4","blue-2-100":"#CEE7E9","blue-2-150":"#B2DCE0","blue-2-200":"#91D1D7","blue-2-250":"#68C7D0","blue-2-300":"#43BBC5","blue-2-350":"#00AFBA","blue-2-400":"#01A0AA","blue-2-450":"#00929D","blue-2-500":"#00848F","blue-2-550":"#007682","blue-2-600":"#016874","blue-2-650":"#005B67","blue-2-700":"#004E5A","blue-2-750":"#00424E","blue-2-800":"#003642","blue-2-850":"#002A38","blue-2-900":"#061E28","blue-2-950":"#071219","purple-050":"#F7F0F6","purple-100":"#EEE0EE","purple-150":"#E7D1E7","purple-200":"#DBBFE4","purple-250":"#D3AEE2","purple-300":"#CB99E1","purple-350":"#C188D9","purple-400":"#B47BCB","purple-450":"#A66EBD","purple-500":"#9961AF","purple-550":"#8B55A1","purple-600":"#7E4894","purple-650":"#723C87","purple-700":"#633376","purple-750":"#552A65","purple-800":"#452551","purple-850":"#35213D","purple-900":"#261A2C","purple-950":"#17111C","pink-050":"#F8EFF4","pink-100":"#F0DFEA","pink-150":"#EACEDF","pink-200":"#E9BBD1","pink-250":"#E9A7C2","pink-300":"#E095B4","pink-350":"#D685A8","pink-400":"#C7799B","pink-450":"#B86C8D","pink-500":"#AA5F80","pink-550":"#9C5374","pink-600":"#8E4767","pink-650":"#813B5B","pink-700":"#732E4F","pink-750":"#632643","pink-800":"#521F38","pink-850":"#3E1C2B","pink-900":"#2D171F","pink-950":"#1C0E12","black-000":"#1B1B2300","black-050":"#1B1B230D","black-100":"#1B1B231A","black-150":"#1B1B2326","black-200":"#1B1B2333","black-250":"#1B1B2340","black-300":"#1B1B234D","black-350":"#1B1B2359","black-400":"#1B1B2366","black-450":"#1B1B2373","black-500":"#1B1B2380","black-550":"#1B1B238C","black-600":"#1B1B2399","black-650":"#1B1B23A6","black-700":"#1B1B23B2","black-750":"#1B1B23BF","black-800":"#1B1B23CC","black-850":"#1B1B23D9","black-900":"#1B1B23E5","black-950":"#111114F2","white-000":"#F8F8F900","white-050":"#F8F8F90D","white-100":"#F8F8F91A","white-150":"#F8F8F926","white-200":"#F8F8F933","white-250":"#F8F8F940","white-300":"#F8F8F94D","white-350":"#F8F8F959","white-400":"#F8F8F966","white-450":"#F8F8F973","white-500":"#F8F8F980","white-550":"#F8F8F98C","white-600":"#F8F8F999","white-650":"#F8F8F9A6","white-700":"#F8F8F9B2","white-750":"#F8F8F9BF","white-800":"#F8F8F9CC","white-850":"#F8F8F9D9","white-900":"#F8F8F9E5","white-950":"#F8F8F9F2","white-975":"#F8F8F9F9"},"transitions":{"ease-in":"cubic-bezier(0.32, 0, 0.67, 0)","ease-out":"cubic-bezier(0.33, 1, 0.68, 1)","ease-in-out":"cubic-bezier(0.65, 0, 0.35, 1)","duration":"250ms"},"font":{"sizes":{"xs":"0.75rem","sm":"0.875rem","md":"1rem","lg":"1.125rem","ml":"0.938rem","xl":"1.25rem","t":"0.6875rem","s":"0.75rem","h1":"2rem","h2":"1.75rem","h3":"1.5rem","h4":"1.375rem","h5":"1.25rem","h6":"1.125rem","xl-alt":"5rem","lg-alt":"4.5rem","md-alt":"4rem","sm-alt":"3.5rem","xs-alt":"3rem"},"weights":{"thin":100,"light":300,"regular":400,"medium":500,"bold":600,"extrabold":800,"black":800},"families":{"base":"Hanken Grotesk, Inter, Roboto Flex Variable, sans-serif","accent":"Hanken Grotesk, Inter, Roboto Flex Variable, sans-serif"}},"spacings":{"0":"0","l":"3rem","b":"1.625rem","m":"0.8125rem","s":"1rem","t":"0.5rem","st":"0.25rem","4xs":"0.125rem","3xs":"0.25rem","2xs":"0.375rem","xxxs":"0.25rem","xxs":"0.375rem","xs":"0.5rem","sm":"0.75rem","base":"1rem","md":"1.5rem","lg":"2rem","xl":"2.5rem","xxl":"3rem","xxxl":"3.5rem","2xl":"3rem","3xl":"3.5rem","4xl":"4rem","5xl":"4.5rem","6xl":"6rem","7xl":"7.5rem","none":"0","auto":"auto","bx":"2.2rem","full":"100%"},"breakpoints":{"xs":"480px","sm":"576px","md":"768px","lg":"992px","xl":"1200px","xxl":"1400px","xxs":"320px","mobile":"768px","tablet":"1024px"}},"contextuals":{"background":{"surface":{"primary":"#FFFFFF","secondary":"#FFFFFF","tertiary":"#F8F8F9"},"semantic":{"overlay":{"primary":"#1B1B230D","primary-hover":"#1B1B231A"},"contextual":{"primary":"#1B1B230D","primary-hover":"#1B1B231A"},"brand":{"primary":"#5E5CD0","primary-hover":"#4844AD","secondary":"#DDE2F5","secondary-hover":"#CED3F1","tertiary":"#EEF1FA","tertiary-hover":"#DDE2F5"},"neutral":{"primary":"#69697D","primary-hover":"#515164","secondary":"#E2E2EA","secondary-hover":"#D3D4E0","tertiary":"#F0F0F3","tertiary-hover":"#E2E2EA"},"info":{"primary":"#0069CF","primary-hover":"#0D4EAA","secondary":"#D5E4F3","secondary-hover":"#BFD7F0","tertiary":"#EAF2F9","tertiary-hover":"#D5E4F3"},"success":{"primary":"#027B3E","primary-hover":"#006024","secondary":"#CFE4D4","secondary-hover":"#BAD9C1","tertiary":"#E8F1EA","tertiary-hover":"#CFE4D4"},"warning":{"primary":"#BC4200","primary-hover":"#9E2300","secondary":"#F1E0D3","secondary-hover":"#ECD0BC","tertiary":"#F8F0E9","tertiary-hover":"#F1E0D3"},"error":{"primary":"#D7010E","primary-hover":"#AA0000","secondary":"#F4DFD9","secondary-hover":"#F0CEC6","tertiary":"#F9EFEC","tertiary-hover":"#F4DFD9"},"disabled":{"primary":"#E2E2EA","secondary":"#F0F0F3"}},"palette":{"brand":{"primary":"#6969DF","secondary":"#8F94FD","tertiary":"#CED3F1"},"red":{"primary":"#DA3B49","secondary":"#F37B7E","tertiary":"#F1CDCB"},"orange":{"primary":"#B95D33","secondary":"#E5845A","tertiary":"#ECD0BD"},"brown":{"primary":"#8F7158","secondary":"#B8987E","tertiary":"#EBD0BA"},"yellow":{"primary":"#9D6E00","secondary":"#C2972E","tertiary":"#E1D4B7"},"green":{"primary":"#008948","secondary":"#45B173","tertiary":"#B8D8C1"},"blue-1":{"primary":"#4279B9","secondary":"#68A1E4","tertiary":"#C1D7F0"},"blue-2":{"primary":"#00848F","secondary":"#00AFBA","tertiary":"#B2DCE0"},"purple":{"primary":"#9961AF","secondary":"#C188D9","tertiary":"#E7D1E7"},"pink":{"primary":"#AA5F80","secondary":"#D685A8","tertiary":"#EACEDF"},"gray":{"primary":"#75758A","secondary":"#9C9CB2","tertiary":"#D3D4E0"}}},"content":{"logo1":"#4844AD","logo2":"#4844AD","semantic":{"contextual":{"primary":"#F8F8F9F2"},"overlay":{"primary":"#F8F8F9F2"},"brand":{"primary":"#3E3B98","secondary":"#534FC2","tertiary":"#5E5CD0","on-brand":"#EEF1FA"},"neutral":{"primary":"#25252F","secondary":"#5D5D70","tertiary":"#69697D","on-neutral":"#F0F0F3"},"info":{"primary":"#124394","secondary":"#005BC0","tertiary":"#0069CF","on-info":"#EAF2F9"},"success":{"primary":"#005317","secondary":"#016D31","tertiary":"#027B3E","on-success":"#E8F1EA"},"warning":{"primary":"#882011","secondary":"#AD3300","tertiary":"#BC4200","on-warning":"#F8F0E9"},"error":{"primary":"#910C06","secondary":"#C00100","tertiary":"#D7010E","on-error":"#F9EFEC"},"disabled":{"primary":"#A9A9BF","secondary":"#F8F8F980"}},"palette":{"brand":{"primary":"#6969DF"},"red":{"primary":"#DA3B49"},"orange":{"primary":"#B95D33"},"brown":{"primary":"#8F7158"},"yellow":{"primary":"#9D6E00"},"green":{"primary":"#008948"},"blue-1":{"primary":"#4279B9"},"blue-2":{"primary":"#00848F"},"purple":{"primary":"#9961AF"},"pink":{"primary":"#AA5F80"},"gray":{"primary":"#75758A"}}},"border":{"surface":{"primary":"#E2E2EA"},"semantic":{"contextual":{"primary":"#F8F8F933"},"overlay":{"primary":"#F8F8F933"},"brand":{"primary":"#5E5CD0","secondary":"#A0A5F6","tertiary":"#CED3F1"},"neutral":{"primary":"#69697D","secondary":"#A9A9BF","tertiary":"#D3D4E0"},"info":{"primary":"#0069CF","secondary":"#6EB0F2","tertiary":"#BFD7F0"},"success":{"primary":"#027B3E","secondary":"#6CBA83","tertiary":"#BAD9C1"},"warning":{"primary":"#BC4200","secondary":"#EB9970","tertiary":"#ECD0BC"},"error":{"primary":"#D7010E","secondary":"#EF9486","tertiary":"#F0CEC6"},"disabled":{"primary":"#E2E2EA"}}}},"components":{"modal":{"width-small":"342px"},"tooltip":{"padding":"4px 8px"},"button":{"medium-height":"40px","medium-text-height":"40px","border-radius":"4px","border-radius--active":"4px","border-radius--focus":"4px"},"resize-handle":{"hover--color":"#E2E2EA"},"datagrid":{"header--color":"#25252F","header--size":"12px","header--weight":"500","body--background-color-hover":"#F0F0F3"},"forms-fileuploader":{"border-style":"solid","border-radius":"4px","border-width":"1px"},"forms-checkbox":{"font-size":"0.875rem"},"forms-input":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-select":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-textarea":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-datepicker":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"badge":{"font-size":"0.75rem","border-radius":"12px","padding-inline":"0.5rem","padding-block":"0.375rem","accent":{"background-color":"#DDE2F5","color":"#534FC2"},"neutral":{"background-color":"#E2E2EA","color":"#5D5D70"},"danger":{"background-color":"#F4DFD9","color":"#C00100"},"success":{"background-color":"#CFE4D4","color":"#016D31"},"warning":{"background-color":"#F1E0D3","color":"#AD3300"},"info":{"background-color":"#D5E4F3","color":"#005BC0"}}}},"dark":{"globals":{"colors":{"logo-1-light":"#4844AD","logo-2-light":"#4844AD","logo-1-dark":"#BEC5F0","logo-2-dark":"#BEC5F0","brand-050":"#EEF1FA","brand-100":"#DDE2F5","brand-150":"#CED3F1","brand-200":"#BEC5F0","brand-250":"#AFB5F1","brand-300":"#A0A5F6","brand-350":"#8F94FD","brand-400":"#8184FC","brand-450":"#7576EE","brand-500":"#6969DF","brand-550":"#5E5CD0","brand-600":"#534FC2","brand-650":"#4844AD","brand-700":"#3E3B98","brand-750":"#36347D","brand-800":"#2D2F5F","brand-850":"#262848","brand-900":"#1C1E32","brand-950":"#11131F","gray-000":"#FFFFFF","gray-025":"#F8F8F9","gray-050":"#F0F0F3","gray-100":"#E2E2EA","gray-150":"#D3D4E0","gray-200":"#C5C6D5","gray-250":"#B7B7CB","gray-300":"#A9A9BF","gray-350":"#9C9CB2","gray-400":"#8F8FA4","gray-450":"#828297","gray-500":"#75758A","gray-550":"#69697D","gray-600":"#5D5D70","gray-650":"#515164","gray-700":"#454558","gray-750":"#3A3A4C","gray-800":"#2F303D","gray-850":"#25252F","gray-900":"#1B1B23","gray-950":"#111114","gray-1000":"#000000","info-050":"#EAF2F9","info-100":"#D5E4F3","info-150":"#BFD7F0","info-200":"#A7CAEE","info-250":"#8DBDEF","info-300":"#6EB0F2","info-350":"#50A2F5","info-400":"#3593F4","info-450":"#1185ED","info-500":"#0077DE","info-550":"#0069CF","info-600":"#005BC0","info-650":"#0D4EAA","info-700":"#124394","info-750":"#163878","info-800":"#192F5A","info-850":"#192541","info-900":"#141B2D","info-950":"#0C111C","success-050":"#E8F1EA","success-100":"#CFE4D4","success-150":"#BAD9C1","success-200":"#A2CFAD","success-250":"#86C597","success-300":"#6CBA83","success-350":"#4FB070","success-400":"#40A363","success-450":"#309556","success-500":"#1E884A","success-550":"#027B3E","success-600":"#016D31","success-650":"#006024","success-700":"#005317","success-750":"#0D4511","success-800":"#11380E","success-850":"#132A11","success-900":"#101E0F","success-950":"#091209","warning-050":"#F8F0E9","warning-100":"#F1E0D3","warning-150":"#ECD0BC","warning-200":"#E8C0A4","warning-250":"#E8AE8A","warning-300":"#EB9970","warning-350":"#E98456","warning-400":"#E57036","warning-450":"#DA5E18","warning-500":"#CB5000","warning-550":"#BC4200","warning-600":"#AD3300","warning-650":"#9E2300","warning-700":"#882011","warning-750":"#731E16","warning-800":"#58201A","warning-850":"#401D18","warning-900":"#2E1714","warning-950":"#1D0F0D","error-050":"#F9EFEC","error-100":"#F4DFD9","error-150":"#F0CEC6","error-200":"#EEBCB2","error-250":"#EEA99D","error-300":"#EF9486","error-350":"#F37C6E","error-400":"#F65F53","error-450":"#F0463D","error-500":"#E82322","error-550":"#D7010E","error-600":"#C00100","error-650":"#AA0000","error-700":"#910C06","error-750":"#731E16","error-800":"#58201A","error-850":"#401D18","error-900":"#2E1714","error-950":"#1D0F0D","red-050":"#FAEFEE","red-100":"#F4DEDD","red-150":"#F1CDCB","red-200":"#EFBBBA","red-250":"#EEA8A8","red-300":"#F09394","red-350":"#F37B7E","red-400":"#EF6569","red-450":"#E94A55","red-500":"#DA3B49","red-550":"#CA2A3C","red-600":"#BB1330","red-650":"#A90021","red-700":"#910A13","red-750":"#731E16","red-800":"#58201A","red-850":"#411D18","red-900":"#2E1714","red-950":"#1D0F0D","orange-050":"#F8F0E9","orange-100":"#F1E0D3","orange-150":"#ECD0BD","orange-200":"#EABFA6","orange-250":"#EBAC90","orange-300":"#EC9772","orange-350":"#E5845A","orange-400":"#D6774D","orange-450":"#C86A40","orange-500":"#B95D33","orange-550":"#AB5025","orange-600":"#9D4315","orange-650":"#8F3600","orange-700":"#812900","orange-750":"#6C2511","orange-800":"#572017","orange-850":"#401D18","orange-900":"#2E1714","orange-950":"#1D0F0D","brown-050":"#F6F0E8","brown-100":"#F1E0D3","brown-150":"#EBD0BA","brown-200":"#E2C0A6","brown-250":"#D4B398","brown-300":"#C6A58B","brown-350":"#B8987E","brown-400":"#AA8B71","brown-450":"#9D7E65","brown-500":"#8F7158","brown-550":"#82654C","brown-600":"#765841","brown-650":"#694C35","brown-700":"#5D412A","brown-750":"#51361E","brown-800":"#452A13","brown-850":"#392008","brown-900":"#29180A","brown-950":"#1B0F08","yellow-050":"#F3F0E7","yellow-100":"#E9E2CF","yellow-150":"#E1D4B7","yellow-200":"#D9C599","yellow-250":"#D2B677","yellow-300":"#CAA756","yellow-350":"#C2972E","yellow-400":"#B98900","yellow-450":"#AB7B00","yellow-500":"#9D6E00","yellow-550":"#916100","yellow-600":"#855400","yellow-650":"#784700","yellow-700":"#6C3A00","yellow-750":"#5F2E00","yellow-800":"#512302","yellow-850":"#3E1D10","yellow-900":"#2D1711","yellow-950":"#1D0F0D","green-050":"#E6F1E9","green-100":"#CFE4D5","green-150":"#B8D8C1","green-200":"#A0CFAE","green-250":"#84C59A","green-300":"#65BA86","green-350":"#45B173","green-400":"#23A562","green-450":"#029755","green-500":"#008948","green-550":"#017B3B","green-600":"#006E2E","green-650":"#006022","green-700":"#005314","green-750":"#0D4510","green-800":"#11380E","green-850":"#132A11","green-900":"#101E0F","green-950":"#091209","blue-1-050":"#EBF1F9","blue-1-100":"#D6E4F4","blue-1-150":"#C1D7F0","blue-1-200":"#AACAEF","blue-1-250":"#8FBCEF","blue-1-300":"#7CAFEB","blue-1-350":"#68A1E4","blue-1-400":"#5B94D6","blue-1-450":"#4E86C7","blue-1-500":"#4279B9","blue-1-550":"#356CAC","blue-1-600":"#28609E","blue-1-650":"#1B5390","blue-1-700":"#0B4783","blue-1-750":"#0F3C6E","blue-1-800":"#133059","blue-1-850":"#152641","blue-1-900":"#121C2D","blue-1-950":"#0B111C","blue-2-050":"#E7F3F4","blue-2-100":"#CEE7E9","blue-2-150":"#B2DCE0","blue-2-200":"#91D1D7","blue-2-250":"#68C7D0","blue-2-300":"#43BBC5","blue-2-350":"#00AFBA","blue-2-400":"#01A0AA","blue-2-450":"#00929D","blue-2-500":"#00848F","blue-2-550":"#007682","blue-2-600":"#016874","blue-2-650":"#005B67","blue-2-700":"#004E5A","blue-2-750":"#00424E","blue-2-800":"#003642","blue-2-850":"#002A38","blue-2-900":"#061E28","blue-2-950":"#071219","purple-050":"#F7F0F6","purple-100":"#EEE0EE","purple-150":"#E7D1E7","purple-200":"#DBBFE4","purple-250":"#D3AEE2","purple-300":"#CB99E1","purple-350":"#C188D9","purple-400":"#B47BCB","purple-450":"#A66EBD","purple-500":"#9961AF","purple-550":"#8B55A1","purple-600":"#7E4894","purple-650":"#723C87","purple-700":"#633376","purple-750":"#552A65","purple-800":"#452551","purple-850":"#35213D","purple-900":"#261A2C","purple-950":"#17111C","pink-050":"#F8EFF4","pink-100":"#F0DFEA","pink-150":"#EACEDF","pink-200":"#E9BBD1","pink-250":"#E9A7C2","pink-300":"#E095B4","pink-350":"#D685A8","pink-400":"#C7799B","pink-450":"#B86C8D","pink-500":"#AA5F80","pink-550":"#9C5374","pink-600":"#8E4767","pink-650":"#813B5B","pink-700":"#732E4F","pink-750":"#632643","pink-800":"#521F38","pink-850":"#3E1C2B","pink-900":"#2D171F","pink-950":"#1C0E12","black-000":"#1B1B2300","black-050":"#1B1B230D","black-100":"#1B1B231A","black-150":"#1B1B2326","black-200":"#1B1B2333","black-250":"#1B1B2340","black-300":"#1B1B234D","black-350":"#1B1B2359","black-400":"#1B1B2366","black-450":"#1B1B2373","black-500":"#1B1B2380","black-550":"#1B1B238C","black-600":"#1B1B2399","black-650":"#1B1B23A6","black-700":"#1B1B23B2","black-750":"#1B1B23BF","black-800":"#1B1B23CC","black-850":"#1B1B23D9","black-900":"#1B1B23E5","black-950":"#111114F2","white-000":"#F8F8F900","white-050":"#F8F8F90D","white-100":"#F8F8F91A","white-150":"#F8F8F926","white-200":"#F8F8F933","white-250":"#F8F8F940","white-300":"#F8F8F94D","white-350":"#F8F8F959","white-400":"#F8F8F966","white-450":"#F8F8F973","white-500":"#F8F8F980","white-550":"#F8F8F98C","white-600":"#F8F8F999","white-650":"#F8F8F9A6","white-700":"#F8F8F9B2","white-750":"#F8F8F9BF","white-800":"#F8F8F9CC","white-850":"#F8F8F9D9","white-900":"#F8F8F9E5","white-950":"#F8F8F9F2","white-975":"#F8F8F9F9"},"transitions":{"ease-in":"cubic-bezier(0.32, 0, 0.67, 0)","ease-out":"cubic-bezier(0.33, 1, 0.68, 1)","ease-in-out":"cubic-bezier(0.65, 0, 0.35, 1)","duration":"250ms"},"font":{"sizes":{"xs":"0.75rem","sm":"0.875rem","md":"1rem","lg":"1.125rem","ml":"0.938rem","xl":"1.25rem","t":"0.6875rem","s":"0.75rem","h1":"2rem","h2":"1.75rem","h3":"1.5rem","h4":"1.375rem","h5":"1.25rem","h6":"1.125rem","xl-alt":"5rem","lg-alt":"4.5rem","md-alt":"4rem","sm-alt":"3.5rem","xs-alt":"3rem"},"weights":{"thin":100,"light":300,"regular":400,"medium":500,"bold":600,"extrabold":800,"black":800},"families":{"base":"Hanken Grotesk, Inter, Roboto Flex Variable, sans-serif","accent":"Hanken Grotesk, Inter, Roboto Flex Variable, sans-serif"}},"spacings":{"0":"0","l":"3rem","b":"1.625rem","m":"0.8125rem","s":"1rem","t":"0.5rem","st":"0.25rem","4xs":"0.125rem","3xs":"0.25rem","2xs":"0.375rem","xxxs":"0.25rem","xxs":"0.375rem","xs":"0.5rem","sm":"0.75rem","base":"1rem","md":"1.5rem","lg":"2rem","xl":"2.5rem","xxl":"3rem","xxxl":"3.5rem","2xl":"3rem","3xl":"3.5rem","4xl":"4rem","5xl":"4.5rem","6xl":"6rem","7xl":"7.5rem","none":"0","auto":"auto","bx":"2.2rem","full":"100%"},"breakpoints":{"xs":"480px","sm":"576px","md":"768px","lg":"992px","xl":"1200px","xxl":"1400px","xxs":"320px","mobile":"768px","tablet":"1024px"}},"contextuals":{"background":{"surface":{"primary":"#2F303D","secondary":"#25252F","tertiary":"#1B1B23"},"semantic":{"contextual":{"primary":"#F8F8F90D","primary-hover":"#F8F8F91A"},"overlay":{"primary":"#F8F8F90D","primary-hover":"#F8F8F91A"},"brand":{"primary":"#5E5CD0","primary-hover":"#4844AD","secondary":"#3E3B98","secondary-hover":"#36347D","tertiary":"#36347D","tertiary-hover":"#2D2F5F"},"neutral":{"primary":"#69697D","primary-hover":"#515164","secondary":"#454558","secondary-hover":"#3A3A4C","tertiary":"#3A3A4C","tertiary-hover":"#2F303D"},"info":{"primary":"#0069CF","primary-hover":"#0D4EAA","secondary":"#124394","secondary-hover":"#163878","tertiary":"#163878","tertiary-hover":"#192F5A"},"success":{"primary":"#027B3E","primary-hover":"#006024","secondary":"#005317","secondary-hover":"#0D4511","tertiary":"#0D4511","tertiary-hover":"#11380E"},"warning":{"primary":"#BC4200","primary-hover":"#9E2300","secondary":"#882011","secondary-hover":"#731E16","tertiary":"#731E16","tertiary-hover":"#58201A"},"error":{"primary":"#D7010E","primary-hover":"#AA0000","secondary":"#910C06","secondary-hover":"#731E16","tertiary":"#731E16","tertiary-hover":"#58201A"},"disabled":{"primary":"#3A3A4C","secondary":"#2F303D"}},"palette":{"brand":{"primary":"#8F94FD","secondary":"#7576EE","tertiary":"#5E5CD0"},"red":{"primary":"#F37B7E","secondary":"#E94A55","tertiary":"#CA2A3C"},"orange":{"primary":"#E5845A","secondary":"#C86A40","tertiary":"#AB5025"},"brown":{"primary":"#B8987E","secondary":"#9D7E65","tertiary":"#82654C"},"yellow":{"primary":"#C2972E","secondary":"#AB7B00","tertiary":"#916100"},"green":{"primary":"#45B173","secondary":"#029755","tertiary":"#017B3B"},"blue-1":{"primary":"#68A1E4","secondary":"#4E86C7","tertiary":"#356CAC"},"blue-2":{"primary":"#00AFBA","secondary":"#00929D","tertiary":"#007682"},"purple":{"primary":"#C188D9","secondary":"#A66EBD","tertiary":"#8B55A1"},"pink":{"primary":"#D685A8","secondary":"#B86C8D","tertiary":"#9C5374"},"gray":{"primary":"#9C9CB2","secondary":"#828297","tertiary":"#69697D"}}},"content":{"logo1":"#BEC5F0","logo2":"#BEC5F0","semantic":{"contextual":{"primary":"#1B1B23D9"},"overlay":{"primary":"#1B1B23D9"},"brand":{"primary":"#EEF1FA","secondary":"#DDE2F5","tertiary":"#AFB5F1","on-brand":"#EEF1FA"},"neutral":{"primary":"#F0F0F3","secondary":"#E2E2EA","tertiary":"#B7B7CB","on-neutral":"#F0F0F3"},"info":{"primary":"#EAF2F9","secondary":"#D5E4F3","tertiary":"#8DBDEF","on-info":"#EAF2F9"},"success":{"primary":"#E8F1EA","secondary":"#CFE4D4","tertiary":"#86C597","on-success":"#E8F1EA"},"warning":{"primary":"#F8F0E9","secondary":"#F1E0D3","tertiary":"#E8AE8A","on-warning":"#F8F0E9"},"error":{"primary":"#F9EFEC","secondary":"#F4DFD9","tertiary":"#EEA99D","on-error":"#F9EFEC"},"disabled":{"primary":"#5D5D70","secondary":"#1B1B234D"}},"palette":{"brand":{"primary":"#6969DF"},"red":{"primary":"#DA3B49"},"orange":{"primary":"#B95D33"},"brown":{"primary":"#8F7158"},"yellow":{"primary":"#9D6E00"},"green":{"primary":"#008948"},"blue-1":{"primary":"#4279B9"},"blue-2":{"primary":"#00848F"},"purple":{"primary":"#9961AF"},"pink":{"primary":"#AA5F80"},"gray":{"primary":"#75758A"}}},"border":{"surface":{"primary":"#3A3A4C"},"semantic":{"contextual":{"primary":"#1B1B2333"},"overlay":{"primary":"#1B1B2333"},"brand":{"primary":"#7576EE","secondary":"#534FC2","tertiary":"#3E3B98"},"neutral":{"primary":"#828297","secondary":"#5D5D70","tertiary":"#454558"},"info":{"primary":"#1185ED","secondary":"#005BC0","tertiary":"#124394"},"success":{"primary":"#309556","secondary":"#016D31","tertiary":"#005317"},"warning":{"primary":"#DA5E18","secondary":"#AD3300","tertiary":"#882011"},"error":{"primary":"#F0463D","secondary":"#C00100","tertiary":"#910C06"},"disabled":{"primary":"#2F303D"}}}},"components":{"modal":{"width-small":"342px"},"tooltip":{"padding":"4px 8px"},"button":{"medium-height":"40px","medium-text-height":"40px","border-radius":"4px","border-radius--active":"4px","border-radius--focus":"4px"},"resize-handle":{"hover--color":"#E2E2EA"},"datagrid":{"header--color":"#25252F","header--size":"12px","header--weight":"500","body--background-color-hover":"#F0F0F3"},"forms-fileuploader":{"border-style":"solid","border-radius":"4px","border-width":"1px"},"forms-checkbox":{"font-size":"0.875rem"},"forms-input":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-select":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-textarea":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-datepicker":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"badge":{"font-size":"0.75rem","border-radius":"12px","padding-inline":"0.5rem","padding-block":"0.375rem","accent":{"background-color":"#DDE2F5","color":"#534FC2"},"neutral":{"background-color":"#E2E2EA","color":"#5D5D70"},"danger":{"background-color":"#F4DFD9","color":"#C00100"},"success":{"background-color":"#CFE4D4","color":"#016D31"},"warning":{"background-color":"#F1E0D3","color":"#AD3300"},"info":{"background-color":"#D5E4F3","color":"#005BC0"}}}},"dsfr-light":{"globals":{"colors":{"logo-1-light":"#2845C1","logo-2-light":"#C83F49","logo-1-dark":"#95ABFF","logo-2-dark":"#E78087","brand-050":"#EDF0FF","brand-100":"#DAE2FF","brand-150":"#C8D3FF","brand-200":"#B5C4FF","brand-250":"#A2B6FF","brand-300":"#90A7FF","brand-350":"#7E98FF","brand-400":"#6C89FE","brand-450":"#5C7AF7","brand-500":"#4C6CEF","brand-550":"#3E5DE7","brand-600":"#304DDF","brand-650":"#2845C1","brand-700":"#223E9E","brand-750":"#1F367D","brand-800":"#1B2E5F","brand-850":"#172446","brand-900":"#121B30","brand-950":"#0C111A","gray-100":"#DFE2EA","gray-150":"#CFD5DE","gray-200":"#C1C7D3","gray-250":"#B2B9C7","gray-300":"#A4ABBC","gray-350":"#969EB0","gray-400":"#8891A4","gray-450":"#7B8498","gray-500":"#6D778C","gray-550":"#626A80","gray-600":"#555E74","gray-650":"#4A5267","gray-700":"#3F4759","gray-750":"#363B4C","gray-800":"#2B303D","gray-850":"#222631","gray-900":"#181B24","gray-950":"#0F1117","gray-1000":"#000000","info-050":"#E7F2FF","info-100":"#CFE5FF","info-150":"#B7D7FF","info-200":"#A0CAFE","info-250":"#8CBCF9","info-300":"#77AEF4","info-350":"#63A0EE","info-400":"#5092E7","info-450":"#4185DC","info-500":"#3677CC","info-550":"#2F6ABB","info-600":"#265EAA","info-650":"#28528F","info-700":"#274775","info-750":"#243C5E","info-800":"#20314A","info-850":"#1B2637","info-900":"#141C27","info-950":"#0D1118","success-050":"#DEF7E6","success-100":"#BAEECF","success-150":"#A5E2C0","success-200":"#95D4B3","success-250":"#85C6A7","success-300":"#74B99B","success-350":"#65AB8F","success-400":"#579E84","success-450":"#4B9079","success-500":"#40836F","success-550":"#367664","success-600":"#2B695A","success-650":"#2C5A50","success-700":"#2A4D45","success-750":"#26403C","success-800":"#213430","success-850":"#1B2826","success-900":"#151D1C","success-950":"#0D1212","warning-050":"#FFEEDF","warning-100":"#FFDCBE","warning-150":"#FFCA9C","warning-200":"#FFB778","warning-250":"#FDA54F","warning-300":"#F59425","warning-350":"#E78613","warning-400":"#D7790C","warning-450":"#C86C08","warning-500":"#B85F03","warning-550":"#A75400","warning-600":"#984800","warning-650":"#814112","warning-700":"#6C3A19","warning-750":"#58321C","warning-800":"#452A1A","warning-850":"#352117","warning-900":"#261813","warning-950":"#170F0C","error-050":"#FFEDEB","error-100":"#FFDAD7","error-150":"#FFC7C2","error-200":"#FFB3AD","error-250":"#FF9F99","error-300":"#FF8984","error-350":"#FF706E","error-400":"#FB5759","error-450":"#F63A45","error-500":"#E32C39","error-550":"#CF202D","error-600":"#BD0F23","error-650":"#9D2227","error-700":"#812727","error-750":"#672624","error-800":"#512220","error-850":"#3D1C1B","error-900":"#2A1614","error-950":"#190E0D","red-050":"#FFEDEB","red-100":"#FFDAD7","red-150":"#FFC7C2","red-200":"#FFB3AD","red-250":"#FF9F99","red-300":"#FF8984","red-350":"#FF706E","red-400":"#FB5759","red-450":"#F63A45","red-500":"#E32C39","red-550":"#CF202D","red-600":"#BD0F23","red-650":"#9D2227","red-700":"#812727","red-750":"#672624","red-800":"#512220","red-850":"#3D1C1B","red-900":"#410003","red-950":"#190E0D","orange-050":"#FCEDEB","orange-100":"#F8DCD7","orange-150":"#F1CCC5","orange-200":"#EABCB4","orange-250":"#E2ACA2","orange-300":"#DA9C92","orange-350":"#D28C81","orange-400":"#CA7C70","orange-450":"#BE6E62","orange-500":"#AE6257","orange-550":"#9E564D","orange-600":"#8F4B42","orange-650":"#79443D","orange-700":"#643C37","orange-750":"#513430","orange-800":"#412B28","orange-850":"#312220","orange-900":"#231918","orange-950":"#150F0F","brown-050":"#F9EFEA","brown-100":"#F3DFD3","brown-150":"#EACFC1","brown-200":"#E2BFAE","brown-250":"#D8B19C","brown-300":"#D0A189","brown-350":"#C3937B","brown-400":"#B5866D","brown-450":"#A77A62","brown-500":"#996D57","brown-550":"#8B614D","brown-600":"#7C5542","brown-650":"#6A4C3C","brown-700":"#594236","brown-750":"#49382F","brown-800":"#3B2E28","brown-850":"#2D2420","brown-900":"#201A18","brown-950":"#13100F","yellow-050":"#FDF1C5","yellow-100":"#FBE18E","yellow-150":"#F4D261","yellow-200":"#EAC244","yellow-250":"#DFB41B","yellow-300":"#D1A516","yellow-350":"#C49711","yellow-400":"#B78A0C","yellow-450":"#A87D07","yellow-500":"#9B6F02","yellow-550":"#8D6300","yellow-600":"#7F5600","yellow-650":"#6E4C11","yellow-700":"#5D4219","yellow-750":"#4D371B","yellow-800":"#3D2E1A","yellow-850":"#2F2417","yellow-900":"#221A12","yellow-950":"#14100C","green-050":"#E7F9B3","green-100":"#D5EC98","green-150":"#C5DE86","green-200":"#B5D174","green-250":"#A5C464","green-300":"#95B755","green-350":"#85AA45","green-400":"#769D39","green-450":"#688F30","green-500":"#5A8228","green-550":"#4D7621","green-600":"#416919","green-650":"#3A5B20","green-700":"#324E22","green-750":"#2C4122","green-800":"#24351D","green-850":"#1D2919","green-900":"#161E13","green-950":"#0E120C","blue-1-050":"#E7F2FF","blue-1-100":"#CFE5FF","blue-1-150":"#B7D7FF","blue-1-200":"#A0CAFE","blue-1-250":"#8CBCF9","blue-1-300":"#77AEF4","blue-1-350":"#63A0EE","blue-1-400":"#5092E7","blue-1-450":"#4185DC","blue-1-500":"#3677CC","blue-1-550":"#2F6ABB","blue-1-600":"#265EAA","blue-1-650":"#28528F","blue-1-700":"#274775","blue-1-750":"#243C5E","blue-1-800":"#20314A","blue-1-850":"#1B2637","blue-1-900":"#141C27","blue-1-950":"#0D1118","blue-2-050":"#E2F4FD","blue-2-100":"#C4E8F8","blue-2-150":"#AADCF2","blue-2-200":"#93CFEB","blue-2-250":"#7CC2E2","blue-2-300":"#6CB4D6","blue-2-350":"#5CA7C9","blue-2-400":"#5099BC","blue-2-450":"#458BAE","blue-2-500":"#3A7EA0","blue-2-550":"#327191","blue-2-600":"#286483","blue-2-650":"#2B5770","blue-2-700":"#294A5E","blue-2-750":"#263E4D","blue-2-800":"#22323D","blue-2-850":"#1C272E","blue-2-900":"#151D21","blue-2-950":"#0E1114","purple-050":"#F5EEFF","purple-100":"#ECDCFF","purple-150":"#E2CBFF","purple-200":"#D9B9FF","purple-250":"#D0A7FF","purple-300":"#C894FE","purple-350":"#BE83FA","purple-400":"#B570F5","purple-450":"#AB5EF0","purple-500":"#A04BE8","purple-550":"#933CDB","purple-600":"#8530C8","purple-650":"#7033A5","purple-700":"#5D3185","purple-750":"#4C2C6A","purple-800":"#3C2652","purple-850":"#2D203C","purple-900":"#21182A","purple-950":"#130F19","pink-050":"#FFEBF6","pink-100":"#FFD8ED","pink-150":"#FCC4E3","pink-200":"#F7B2D9","pink-250":"#F29FCE","pink-300":"#ED8CC3","pink-350":"#E779B8","pink-400":"#E264AD","pink-450":"#D2579E","pink-500":"#C24B8E","pink-550":"#B0417F","pink-600":"#9F3670","pink-650":"#873560","pink-700":"#6F3250","pink-750":"#5A2C43","pink-800":"#472635","pink-850":"#351F29","pink-900":"#26171D","pink-950":"#170F12","black-000":"#181B2400","black-050":"#181B240D","black-100":"#181B241A","black-150":"#181B2426","black-200":"#181B2433","black-250":"#181B2440","black-300":"#181B244D","black-350":"#181B2459","black-400":"#181B2466","black-450":"#181B2473","black-500":"#181B2480","black-550":"#181B248C","black-600":"#181B2499","black-650":"#181B24A6","black-700":"#181B24B2","black-750":"#181B24BF","black-800":"#181B24CC","black-850":"#181B24D9","black-900":"#181B24E5","black-950":"#0F1117F2","white-000":"#F6F8F900","white-050":"#F6F8F90D","white-100":"#F6F8F91A","white-150":"#F6F8F926","white-200":"#F6F8F933","white-250":"#F6F8F940","white-300":"#F6F8F94D","white-350":"#F6F8F959","white-400":"#F6F8F966","white-450":"#F6F8F973","white-500":"#F6F8F980","white-550":"#F6F8F98C","white-600":"#F6F8F999","white-650":"#F6F8F9A6","white-700":"#F6F8F9B2","white-750":"#F6F8F9BF","white-800":"#F6F8F9CC","white-850":"#F6F8F9D9","white-900":"#F6F8F9E5","white-950":"#F6F8F9F2","white-975":"#F6F8F9F9"},"font":{"sizes":{"xs":"0.75rem","sm":"0.875rem","md":"1rem","lg":"1.125rem","ml":"0.938rem","xl":"1.25rem","t":"0.6875rem","s":"0.75rem","h1":"2rem","h2":"1.75rem","h3":"1.5rem","h4":"1.375rem","h5":"1.25rem","h6":"1.125rem","xl-alt":"5rem","lg-alt":"4.5rem","md-alt":"4rem","sm-alt":"3.5rem","xs-alt":"3rem"},"weights":{"thin":100,"extrabold":800},"families":{"accent":"Marianne, Inter, Roboto Flex Variable, sans-serif","base":"Marianne, Inter, Roboto Flex Variable, sans-serif"}},"spacings":{"0":"0","none":"0","auto":"auto","bx":"2.2rem","full":"100%","4xs":"0.125rem","3xs":"0.25rem","2xs":"0.375rem","xs":"0.5rem","sm":"0.75rem","base":"1rem","md":"1.5rem","lg":"2rem","xl":"2.5rem","xxl":"3rem","2xl":"3rem","xxxl":"3.5rem","3xl":"3.5rem","4xl":"4rem","5xl":"4.5rem","6xl":"6rem","7xl":"7.5rem"},"breakpoints":{"xxs":"320px","xs":"480px","mobile":"768px","tablet":"1024px"}},"contextuals":{"background":{"surface":{"primary":"#FFFFFF","secondary":"#FFFFFF","tertiary":"#F8F8F9"},"semantic":{"overlay":{"primary":"#1B1B230D","primary-hover":"#1B1B231A"},"contextual":{"primary":"#1B1B230D","primary-hover":"#1B1B231A"},"brand":{"primary":"#5E5CD0","primary-hover":"#4844AD","secondary":"#DDE2F5","secondary-hover":"#CED3F1","tertiary":"#EEF1FA","tertiary-hover":"#DDE2F5"},"neutral":{"primary":"#69697D","primary-hover":"#515164","secondary":"#E2E2EA","secondary-hover":"#D3D4E0","tertiary":"#F0F0F3","tertiary-hover":"#E2E2EA"},"info":{"primary":"#0069CF","primary-hover":"#0D4EAA","secondary":"#D5E4F3","secondary-hover":"#BFD7F0","tertiary":"#EAF2F9","tertiary-hover":"#D5E4F3"},"success":{"primary":"#027B3E","primary-hover":"#006024","secondary":"#CFE4D4","secondary-hover":"#BAD9C1","tertiary":"#E8F1EA","tertiary-hover":"#CFE4D4"},"warning":{"primary":"#BC4200","primary-hover":"#9E2300","secondary":"#F1E0D3","secondary-hover":"#ECD0BC","tertiary":"#F8F0E9","tertiary-hover":"#F1E0D3"},"error":{"primary":"#D7010E","primary-hover":"#AA0000","secondary":"#F4DFD9","secondary-hover":"#F0CEC6","tertiary":"#F9EFEC","tertiary-hover":"#F4DFD9"},"disabled":{"primary":"#E2E2EA","secondary":"#F0F0F3"}},"palette":{"brand":{"primary":"#6969DF","secondary":"#8F94FD","tertiary":"#CED3F1"},"red":{"primary":"#DA3B49","secondary":"#F37B7E","tertiary":"#F1CDCB"},"orange":{"primary":"#B95D33","secondary":"#E5845A","tertiary":"#ECD0BD"},"brown":{"primary":"#8F7158","secondary":"#B8987E","tertiary":"#EBD0BA"},"yellow":{"primary":"#9D6E00","secondary":"#C2972E","tertiary":"#E1D4B7"},"green":{"primary":"#008948","secondary":"#45B173","tertiary":"#B8D8C1"},"blue-1":{"primary":"#4279B9","secondary":"#68A1E4","tertiary":"#C1D7F0"},"blue-2":{"primary":"#00848F","secondary":"#00AFBA","tertiary":"#B2DCE0"},"purple":{"primary":"#9961AF","secondary":"#C188D9","tertiary":"#E7D1E7"},"pink":{"primary":"#AA5F80","secondary":"#D685A8","tertiary":"#EACEDF"},"gray":{"primary":"#75758A","secondary":"#9C9CB2","tertiary":"#D3D4E0"}}},"content":{"logo1":"#4844AD","logo2":"#4844AD","semantic":{"contextual":{"primary":"#F8F8F9F2"},"overlay":{"primary":"#F8F8F9F2"},"brand":{"primary":"#3E3B98","secondary":"#534FC2","tertiary":"#5E5CD0","on-brand":"#EEF1FA"},"neutral":{"primary":"#25252F","secondary":"#5D5D70","tertiary":"#69697D","on-neutral":"#F0F0F3"},"info":{"primary":"#124394","secondary":"#005BC0","tertiary":"#0069CF","on-info":"#EAF2F9"},"success":{"primary":"#005317","secondary":"#016D31","tertiary":"#027B3E","on-success":"#E8F1EA"},"warning":{"primary":"#882011","secondary":"#AD3300","tertiary":"#BC4200","on-warning":"#F8F0E9"},"error":{"primary":"#910C06","secondary":"#C00100","tertiary":"#D7010E","on-error":"#F9EFEC"},"disabled":{"primary":"#A9A9BF","secondary":"#F8F8F980"}},"palette":{"brand":{"primary":"#6969DF"},"red":{"primary":"#DA3B49"},"orange":{"primary":"#B95D33"},"brown":{"primary":"#8F7158"},"yellow":{"primary":"#9D6E00"},"green":{"primary":"#008948"},"blue-1":{"primary":"#4279B9"},"blue-2":{"primary":"#00848F"},"purple":{"primary":"#9961AF"},"pink":{"primary":"#AA5F80"},"gray":{"primary":"#75758A"}}},"border":{"surface":{"primary":"#E2E2EA"},"semantic":{"contextual":{"primary":"#F8F8F933"},"overlay":{"primary":"#F8F8F933"},"brand":{"primary":"#5E5CD0","secondary":"#A0A5F6","tertiary":"#CED3F1"},"neutral":{"primary":"#69697D","secondary":"#A9A9BF","tertiary":"#D3D4E0"},"info":{"primary":"#0069CF","secondary":"#6EB0F2","tertiary":"#BFD7F0"},"success":{"primary":"#027B3E","secondary":"#6CBA83","tertiary":"#BAD9C1"},"warning":{"primary":"#BC4200","secondary":"#EB9970","tertiary":"#ECD0BC"},"error":{"primary":"#D7010E","secondary":"#EF9486","tertiary":"#F0CEC6"},"disabled":{"primary":"#E2E2EA"}}}},"components":{"modal":{"width-small":"342px"},"tooltip":{"padding":"4px 8px"},"button":{"medium-height":"40px","medium-text-height":"40px","border-radius":"4px","border-radius--active":"4px","border-radius--focus":"4px"},"resize-handle":{"hover--color":"#E2E2EA"},"datagrid":{"header--color":"#25252F","header--size":"12px","header--weight":"500","body--background-color-hover":"#F0F0F3"},"forms-fileuploader":{"border-style":"solid","border-radius":"4px","border-width":"1px"},"forms-checkbox":{"font-size":"0.875rem"},"forms-input":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-select":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-textarea":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-datepicker":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"badge":{"font-size":"0.75rem","border-radius":"12px","padding-inline":"0.5rem","padding-block":"0.375rem","accent":{"background-color":"#DDE2F5","color":"#534FC2"},"neutral":{"background-color":"#E2E2EA","color":"#5D5D70"},"danger":{"background-color":"#F4DFD9","color":"#C00100"},"success":{"background-color":"#CFE4D4","color":"#016D31"},"warning":{"background-color":"#F1E0D3","color":"#AD3300"},"info":{"background-color":"#D5E4F3","color":"#005BC0"}}}},"dsfr-dark":{"globals":{"colors":{"logo-1-light":"#2845C1","logo-2-light":"#C83F49","logo-1-dark":"#95ABFF","logo-2-dark":"#E78087","brand-050":"#EDF0FF","brand-100":"#DAE2FF","brand-150":"#C8D3FF","brand-200":"#B5C4FF","brand-250":"#A2B6FF","brand-300":"#90A7FF","brand-350":"#7E98FF","brand-400":"#6C89FE","brand-450":"#5C7AF7","brand-500":"#4C6CEF","brand-550":"#3E5DE7","brand-600":"#304DDF","brand-650":"#2845C1","brand-700":"#223E9E","brand-750":"#1F367D","brand-800":"#1B2E5F","brand-850":"#172446","brand-900":"#121B30","brand-950":"#0C111A","gray-100":"#DFE2EA","gray-150":"#CFD5DE","gray-200":"#C1C7D3","gray-250":"#B2B9C7","gray-300":"#A4ABBC","gray-350":"#969EB0","gray-400":"#8891A4","gray-450":"#7B8498","gray-500":"#6D778C","gray-550":"#626A80","gray-600":"#555E74","gray-650":"#4A5267","gray-700":"#3F4759","gray-750":"#363B4C","gray-800":"#2B303D","gray-850":"#222631","gray-900":"#181B24","gray-950":"#0F1117","gray-1000":"#000000","info-050":"#E7F2FF","info-100":"#CFE5FF","info-150":"#B7D7FF","info-200":"#A0CAFE","info-250":"#8CBCF9","info-300":"#77AEF4","info-350":"#63A0EE","info-400":"#5092E7","info-450":"#4185DC","info-500":"#3677CC","info-550":"#2F6ABB","info-600":"#265EAA","info-650":"#28528F","info-700":"#274775","info-750":"#243C5E","info-800":"#20314A","info-850":"#1B2637","info-900":"#141C27","info-950":"#0D1118","success-050":"#DEF7E6","success-100":"#BAEECF","success-150":"#A5E2C0","success-200":"#95D4B3","success-250":"#85C6A7","success-300":"#74B99B","success-350":"#65AB8F","success-400":"#579E84","success-450":"#4B9079","success-500":"#40836F","success-550":"#367664","success-600":"#2B695A","success-650":"#2C5A50","success-700":"#2A4D45","success-750":"#26403C","success-800":"#213430","success-850":"#1B2826","success-900":"#151D1C","success-950":"#0D1212","warning-050":"#FFEEDF","warning-100":"#FFDCBE","warning-150":"#FFCA9C","warning-200":"#FFB778","warning-250":"#FDA54F","warning-300":"#F59425","warning-350":"#E78613","warning-400":"#D7790C","warning-450":"#C86C08","warning-500":"#B85F03","warning-550":"#A75400","warning-600":"#984800","warning-650":"#814112","warning-700":"#6C3A19","warning-750":"#58321C","warning-800":"#452A1A","warning-850":"#352117","warning-900":"#261813","warning-950":"#170F0C","error-050":"#FFEDEB","error-100":"#FFDAD7","error-150":"#FFC7C2","error-200":"#FFB3AD","error-250":"#FF9F99","error-300":"#FF8984","error-350":"#FF706E","error-400":"#FB5759","error-450":"#F63A45","error-500":"#E32C39","error-550":"#CF202D","error-600":"#BD0F23","error-650":"#9D2227","error-700":"#812727","error-750":"#672624","error-800":"#512220","error-850":"#3D1C1B","error-900":"#2A1614","error-950":"#190E0D","red-050":"#FFEDEB","red-100":"#FFDAD7","red-150":"#FFC7C2","red-200":"#FFB3AD","red-250":"#FF9F99","red-300":"#FF8984","red-350":"#FF706E","red-400":"#FB5759","red-450":"#F63A45","red-500":"#E32C39","red-550":"#CF202D","red-600":"#BD0F23","red-650":"#9D2227","red-700":"#812727","red-750":"#672624","red-800":"#512220","red-850":"#3D1C1B","red-900":"#410003","red-950":"#190E0D","orange-050":"#FCEDEB","orange-100":"#F8DCD7","orange-150":"#F1CCC5","orange-200":"#EABCB4","orange-250":"#E2ACA2","orange-300":"#DA9C92","orange-350":"#D28C81","orange-400":"#CA7C70","orange-450":"#BE6E62","orange-500":"#AE6257","orange-550":"#9E564D","orange-600":"#8F4B42","orange-650":"#79443D","orange-700":"#643C37","orange-750":"#513430","orange-800":"#412B28","orange-850":"#312220","orange-900":"#231918","orange-950":"#150F0F","brown-050":"#F9EFEA","brown-100":"#F3DFD3","brown-150":"#EACFC1","brown-200":"#E2BFAE","brown-250":"#D8B19C","brown-300":"#D0A189","brown-350":"#C3937B","brown-400":"#B5866D","brown-450":"#A77A62","brown-500":"#996D57","brown-550":"#8B614D","brown-600":"#7C5542","brown-650":"#6A4C3C","brown-700":"#594236","brown-750":"#49382F","brown-800":"#3B2E28","brown-850":"#2D2420","brown-900":"#201A18","brown-950":"#13100F","yellow-050":"#FDF1C5","yellow-100":"#FBE18E","yellow-150":"#F4D261","yellow-200":"#EAC244","yellow-250":"#DFB41B","yellow-300":"#D1A516","yellow-350":"#C49711","yellow-400":"#B78A0C","yellow-450":"#A87D07","yellow-500":"#9B6F02","yellow-550":"#8D6300","yellow-600":"#7F5600","yellow-650":"#6E4C11","yellow-700":"#5D4219","yellow-750":"#4D371B","yellow-800":"#3D2E1A","yellow-850":"#2F2417","yellow-900":"#221A12","yellow-950":"#14100C","green-050":"#E7F9B3","green-100":"#D5EC98","green-150":"#C5DE86","green-200":"#B5D174","green-250":"#A5C464","green-300":"#95B755","green-350":"#85AA45","green-400":"#769D39","green-450":"#688F30","green-500":"#5A8228","green-550":"#4D7621","green-600":"#416919","green-650":"#3A5B20","green-700":"#324E22","green-750":"#2C4122","green-800":"#24351D","green-850":"#1D2919","green-900":"#161E13","green-950":"#0E120C","blue-1-050":"#E7F2FF","blue-1-100":"#CFE5FF","blue-1-150":"#B7D7FF","blue-1-200":"#A0CAFE","blue-1-250":"#8CBCF9","blue-1-300":"#77AEF4","blue-1-350":"#63A0EE","blue-1-400":"#5092E7","blue-1-450":"#4185DC","blue-1-500":"#3677CC","blue-1-550":"#2F6ABB","blue-1-600":"#265EAA","blue-1-650":"#28528F","blue-1-700":"#274775","blue-1-750":"#243C5E","blue-1-800":"#20314A","blue-1-850":"#1B2637","blue-1-900":"#141C27","blue-1-950":"#0D1118","blue-2-050":"#E2F4FD","blue-2-100":"#C4E8F8","blue-2-150":"#AADCF2","blue-2-200":"#93CFEB","blue-2-250":"#7CC2E2","blue-2-300":"#6CB4D6","blue-2-350":"#5CA7C9","blue-2-400":"#5099BC","blue-2-450":"#458BAE","blue-2-500":"#3A7EA0","blue-2-550":"#327191","blue-2-600":"#286483","blue-2-650":"#2B5770","blue-2-700":"#294A5E","blue-2-750":"#263E4D","blue-2-800":"#22323D","blue-2-850":"#1C272E","blue-2-900":"#151D21","blue-2-950":"#0E1114","purple-050":"#F5EEFF","purple-100":"#ECDCFF","purple-150":"#E2CBFF","purple-200":"#D9B9FF","purple-250":"#D0A7FF","purple-300":"#C894FE","purple-350":"#BE83FA","purple-400":"#B570F5","purple-450":"#AB5EF0","purple-500":"#A04BE8","purple-550":"#933CDB","purple-600":"#8530C8","purple-650":"#7033A5","purple-700":"#5D3185","purple-750":"#4C2C6A","purple-800":"#3C2652","purple-850":"#2D203C","purple-900":"#21182A","purple-950":"#130F19","pink-050":"#FFEBF6","pink-100":"#FFD8ED","pink-150":"#FCC4E3","pink-200":"#F7B2D9","pink-250":"#F29FCE","pink-300":"#ED8CC3","pink-350":"#E779B8","pink-400":"#E264AD","pink-450":"#D2579E","pink-500":"#C24B8E","pink-550":"#B0417F","pink-600":"#9F3670","pink-650":"#873560","pink-700":"#6F3250","pink-750":"#5A2C43","pink-800":"#472635","pink-850":"#351F29","pink-900":"#26171D","pink-950":"#170F12","black-000":"#181B2400","black-050":"#181B240D","black-100":"#181B241A","black-150":"#181B2426","black-200":"#181B2433","black-250":"#181B2440","black-300":"#181B244D","black-350":"#181B2459","black-400":"#181B2466","black-450":"#181B2473","black-500":"#181B2480","black-550":"#181B248C","black-600":"#181B2499","black-650":"#181B24A6","black-700":"#181B24B2","black-750":"#181B24BF","black-800":"#181B24CC","black-850":"#181B24D9","black-900":"#181B24E5","black-950":"#0F1117F2","white-000":"#F6F8F900","white-050":"#F6F8F90D","white-100":"#F6F8F91A","white-150":"#F6F8F926","white-200":"#F6F8F933","white-250":"#F6F8F940","white-300":"#F6F8F94D","white-350":"#F6F8F959","white-400":"#F6F8F966","white-450":"#F6F8F973","white-500":"#F6F8F980","white-550":"#F6F8F98C","white-600":"#F6F8F999","white-650":"#F6F8F9A6","white-700":"#F6F8F9B2","white-750":"#F6F8F9BF","white-800":"#F6F8F9CC","white-850":"#F6F8F9D9","white-900":"#F6F8F9E5","white-950":"#F6F8F9F2","white-975":"#F6F8F9F9"},"font":{"sizes":{"xs":"0.75rem","sm":"0.875rem","md":"1rem","lg":"1.125rem","ml":"0.938rem","xl":"1.25rem","t":"0.6875rem","s":"0.75rem","h1":"2rem","h2":"1.75rem","h3":"1.5rem","h4":"1.375rem","h5":"1.25rem","h6":"1.125rem","xl-alt":"5rem","lg-alt":"4.5rem","md-alt":"4rem","sm-alt":"3.5rem","xs-alt":"3rem"},"weights":{"thin":100,"extrabold":800},"families":{"accent":"Marianne, Inter, Roboto Flex Variable, sans-serif","base":"Marianne, Inter, Roboto Flex Variable, sans-serif"}},"spacings":{"0":"0","none":"0","auto":"auto","bx":"2.2rem","full":"100%","4xs":"0.125rem","3xs":"0.25rem","2xs":"0.375rem","xs":"0.5rem","sm":"0.75rem","base":"1rem","md":"1.5rem","lg":"2rem","xl":"2.5rem","xxl":"3rem","2xl":"3rem","xxxl":"3.5rem","3xl":"3.5rem","4xl":"4rem","5xl":"4.5rem","6xl":"6rem","7xl":"7.5rem"},"breakpoints":{"xxs":"320px","xs":"480px","mobile":"768px","tablet":"1024px"}},"contextuals":{"background":{"surface":{"primary":"#2F303D","secondary":"#25252F","tertiary":"#1B1B23"},"semantic":{"contextual":{"primary":"#F8F8F90D","primary-hover":"#F8F8F91A"},"overlay":{"primary":"#F8F8F90D","primary-hover":"#F8F8F91A"},"brand":{"primary":"#5E5CD0","primary-hover":"#4844AD","secondary":"#3E3B98","secondary-hover":"#36347D","tertiary":"#36347D","tertiary-hover":"#2D2F5F"},"neutral":{"primary":"#69697D","primary-hover":"#515164","secondary":"#454558","secondary-hover":"#3A3A4C","tertiary":"#3A3A4C","tertiary-hover":"#2F303D"},"info":{"primary":"#0069CF","primary-hover":"#0D4EAA","secondary":"#124394","secondary-hover":"#163878","tertiary":"#163878","tertiary-hover":"#192F5A"},"success":{"primary":"#027B3E","primary-hover":"#006024","secondary":"#005317","secondary-hover":"#0D4511","tertiary":"#0D4511","tertiary-hover":"#11380E"},"warning":{"primary":"#BC4200","primary-hover":"#9E2300","secondary":"#882011","secondary-hover":"#731E16","tertiary":"#731E16","tertiary-hover":"#58201A"},"error":{"primary":"#D7010E","primary-hover":"#AA0000","secondary":"#910C06","secondary-hover":"#731E16","tertiary":"#731E16","tertiary-hover":"#58201A"},"disabled":{"primary":"#3A3A4C","secondary":"#2F303D"}},"palette":{"brand":{"primary":"#8F94FD","secondary":"#7576EE","tertiary":"#5E5CD0"},"red":{"primary":"#F37B7E","secondary":"#E94A55","tertiary":"#CA2A3C"},"orange":{"primary":"#E5845A","secondary":"#C86A40","tertiary":"#AB5025"},"brown":{"primary":"#B8987E","secondary":"#9D7E65","tertiary":"#82654C"},"yellow":{"primary":"#C2972E","secondary":"#AB7B00","tertiary":"#916100"},"green":{"primary":"#45B173","secondary":"#029755","tertiary":"#017B3B"},"blue-1":{"primary":"#68A1E4","secondary":"#4E86C7","tertiary":"#356CAC"},"blue-2":{"primary":"#00AFBA","secondary":"#00929D","tertiary":"#007682"},"purple":{"primary":"#C188D9","secondary":"#A66EBD","tertiary":"#8B55A1"},"pink":{"primary":"#D685A8","secondary":"#B86C8D","tertiary":"#9C5374"},"gray":{"primary":"#9C9CB2","secondary":"#828297","tertiary":"#69697D"}}},"content":{"logo1":"#BEC5F0","logo2":"#BEC5F0","semantic":{"contextual":{"primary":"#1B1B23D9"},"overlay":{"primary":"#1B1B23D9"},"brand":{"primary":"#EEF1FA","secondary":"#DDE2F5","tertiary":"#AFB5F1","on-brand":"#EEF1FA"},"neutral":{"primary":"#F0F0F3","secondary":"#E2E2EA","tertiary":"#B7B7CB","on-neutral":"#F0F0F3"},"info":{"primary":"#EAF2F9","secondary":"#D5E4F3","tertiary":"#8DBDEF","on-info":"#EAF2F9"},"success":{"primary":"#E8F1EA","secondary":"#CFE4D4","tertiary":"#86C597","on-success":"#E8F1EA"},"warning":{"primary":"#F8F0E9","secondary":"#F1E0D3","tertiary":"#E8AE8A","on-warning":"#F8F0E9"},"error":{"primary":"#F9EFEC","secondary":"#F4DFD9","tertiary":"#EEA99D","on-error":"#F9EFEC"},"disabled":{"primary":"#5D5D70","secondary":"#1B1B234D"}},"palette":{"brand":{"primary":"#6969DF"},"red":{"primary":"#DA3B49"},"orange":{"primary":"#B95D33"},"brown":{"primary":"#8F7158"},"yellow":{"primary":"#9D6E00"},"green":{"primary":"#008948"},"blue-1":{"primary":"#4279B9"},"blue-2":{"primary":"#00848F"},"purple":{"primary":"#9961AF"},"pink":{"primary":"#AA5F80"},"gray":{"primary":"#75758A"}}},"border":{"surface":{"primary":"#3A3A4C"},"semantic":{"contextual":{"primary":"#1B1B2333"},"overlay":{"primary":"#1B1B2333"},"brand":{"primary":"#7576EE","secondary":"#534FC2","tertiary":"#3E3B98"},"neutral":{"primary":"#828297","secondary":"#5D5D70","tertiary":"#454558"},"info":{"primary":"#1185ED","secondary":"#005BC0","tertiary":"#124394"},"success":{"primary":"#309556","secondary":"#016D31","tertiary":"#005317"},"warning":{"primary":"#DA5E18","secondary":"#AD3300","tertiary":"#882011"},"error":{"primary":"#F0463D","secondary":"#C00100","tertiary":"#910C06"},"disabled":{"primary":"#2F303D"}}}},"components":{"modal":{"width-small":"342px"},"tooltip":{"padding":"4px 8px"},"button":{"medium-height":"40px","medium-text-height":"40px","border-radius":"4px","border-radius--active":"4px","border-radius--focus":"4px"},"resize-handle":{"hover--color":"#E2E2EA"},"datagrid":{"header--color":"#25252F","header--size":"12px","header--weight":"500","body--background-color-hover":"#F0F0F3"},"forms-fileuploader":{"border-style":"solid","border-radius":"4px","border-width":"1px"},"forms-checkbox":{"font-size":"0.875rem"},"forms-input":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-select":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-textarea":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-datepicker":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"badge":{"font-size":"0.75rem","border-radius":"12px","padding-inline":"0.5rem","padding-block":"0.375rem","accent":{"background-color":"#DDE2F5","color":"#534FC2"},"neutral":{"background-color":"#E2E2EA","color":"#5D5D70"},"danger":{"background-color":"#F4DFD9","color":"#C00100"},"success":{"background-color":"#CFE4D4","color":"#016D31"},"warning":{"background-color":"#F1E0D3","color":"#AD3300"},"info":{"background-color":"#D5E4F3","color":"#005BC0"}}}},"anct-light":{"globals":{"colors":{"logo-1-light":"#2845C1","logo-2-light":"#FBC63A","logo-1-dark":"#94A3E5","logo-2-dark":"#EFD183","brand-050":"#EEF0F9","brand-100":"#DDE2F5","brand-150":"#CBD4F1","brand-200":"#B8C6F0","brand-250":"#A5B7F2","brand-300":"#91A8F7","brand-350":"#7C98FE","brand-400":"#6A89FF","brand-450":"#5A7AFB","brand-500":"#4B6BF0","brand-550":"#3E5CE7","brand-600":"#3352D5","brand-650":"#2A47C0","brand-700":"#2340A3","brand-750":"#223A7F","brand-800":"#1F325F","brand-850":"#1B2845","brand-900":"#151E30","brand-950":"#0D121D","gray-000":"#FFFFFF","gray-025":"#F6F8FA","gray-050":"#ECF1F7","gray-100":"#DDE3F3","gray-150":"#CCD4EA","gray-200":"#BCC7E0","gray-250":"#AEB9D2","gray-300":"#A1ABC4","gray-350":"#949EB6","gray-400":"#8791A9","gray-450":"#7A849B","gray-500":"#6D778E","gray-550":"#616A81","gray-600":"#555E75","gray-650":"#495268","gray-700":"#3E475C","gray-750":"#333B50","gray-800":"#283044","gray-850":"#1E2539","gray-900":"#171B28","gray-950":"#0F1118","gray-1000":"#000000","info-050":"#E9F2F8","info-100":"#D2E5F1","info-150":"#C0D7F0","info-200":"#AEC8F0","info-250":"#9EB9F2","info-300":"#8DABEE","info-350":"#7B9DE9","info-400":"#6E8FDB","info-450":"#6282CD","info-500":"#5575BF","info-550":"#4968B1","info-600":"#3E5CA3","info-650":"#344F97","info-700":"#294389","info-750":"#243972","info-800":"#1D2F5B","info-850":"#1A2744","info-900":"#151D2F","info-950":"#0E131F","success-050":"#E7F1E9","success-100":"#CFE3D3","success-150":"#B9D8C0","success-200":"#A1CEAC","success-250":"#85C496","success-300":"#63BC7F","success-350":"#45B16B","success-400":"#1CA659","success-450":"#00984C","success-500":"#008A3F","success-550":"#007C32","success-600":"#006E24","success-650":"#016016","success-700":"#005305","success-750":"#0D450A","success-800":"#11380E","success-850":"#132A11","success-900":"#101E0F","success-950":"#091209","warning-050":"#F6F0E8","warning-100":"#EDE2D1","warning-150":"#E6D3B8","warning-200":"#E3C39F","warning-250":"#E3B082","warning-300":"#E19E5C","warning-350":"#D98E3F","warning-400":"#CF7D19","warning-450":"#C17000","warning-500":"#B36300","warning-550":"#A45600","warning-600":"#964900","warning-650":"#893C00","warning-700":"#7B2F00","warning-750":"#68270D","warning-800":"#562013","warning-850":"#411D18","warning-900":"#2E1714","warning-950":"#1D0F0D","error-050":"#F9EFEC","error-100":"#F4DFD9","error-150":"#F0CEC6","error-200":"#EEBCB2","error-250":"#EEA99D","error-300":"#EF9487","error-350":"#F37C6E","error-400":"#F65F53","error-450":"#EF443C","error-500":"#E0342E","error-550":"#D0201F","error-600":"#C0000C","error-650":"#AA0000","error-700":"#910C06","error-750":"#731E16","error-800":"#58201A","error-850":"#411D18","error-900":"#2E1714","error-950":"#1D0F0D","red-050":"#F9EFEC","red-100":"#F4DEDA","red-150":"#F0CDC9","red-200":"#EEBBB6","red-250":"#EEA8A2","red-300":"#F0938D","red-350":"#EC7E78","red-400":"#E46D67","red-450":"#D95B58","red-500":"#CA4E4B","red-550":"#BB403F","red-600":"#AC3233","red-650":"#9D2227","red-700":"#882023","red-750":"#721D1B","red-800":"#58201A","red-850":"#401D18","red-900":"#2E1714","red-950":"#1D0F0D","orange-050":"#F8F0E9","orange-100":"#F1E0D3","orange-150":"#ECD0BD","orange-200":"#E9C0A5","orange-250":"#E8AE8A","orange-300":"#EB9870","orange-350":"#EB845A","orange-400":"#E66E37","orange-450":"#DD5B16","orange-500":"#CE4D00","orange-550":"#BF3E00","orange-600":"#B02F00","orange-650":"#A11E00","orange-700":"#8A1E14","orange-750":"#731E16","orange-800":"#58201A","orange-850":"#401D18","orange-900":"#2E1714","orange-950":"#1D0F0D","brown-050":"#F5F0E8","brown-100":"#ECE2D1","brown-150":"#E9D1B9","brown-200":"#E3C19D","brown-250":"#DCB187","brown-300":"#D2A26F","brown-350":"#C49562","brown-400":"#B68855","brown-450":"#A97B48","brown-500":"#9B6E3B","brown-550":"#8E612F","brown-600":"#815521","brown-650":"#744913","brown-700":"#673D00","brown-750":"#5A3100","brown-800":"#4E2600","brown-850":"#3D1F0B","brown-900":"#2C170F","brown-950":"#1C0F0B","yellow-050":"#F3F0E7","yellow-100":"#E9E2CF","yellow-150":"#E0D4B7","yellow-200":"#DAC59A","yellow-250":"#D5B67A","yellow-300":"#D0A559","yellow-350":"#CC9331","yellow-400":"#C48400","yellow-450":"#B77600","yellow-500":"#AA6800","yellow-550":"#9D5A00","yellow-600":"#914D00","yellow-650":"#843F00","yellow-700":"#773200","yellow-750":"#6A2601","yellow-800":"#56210F","yellow-850":"#401D16","yellow-900":"#2E1714","yellow-950":"#1D0F0D","green-050":"#E3F1EF","green-100":"#CAE5E1","green-150":"#B0DBD4","green-200":"#91D1C7","green-250":"#6AC8BC","green-300":"#4DBCAF","green-350":"#3CAFA2","green-400":"#2AA194","green-450":"#109487","green-500":"#00867A","green-550":"#00786D","green-600":"#016A60","green-650":"#015D53","green-700":"#005047","green-750":"#00443B","green-800":"#00382F","green-850":"#002C25","green-900":"#041F1A","green-950":"#041310","blue-1-050":"#EAF2F9","blue-1-100":"#D4E4F3","blue-1-150":"#BFD7F0","blue-1-200":"#AAC9EF","blue-1-250":"#96BBF1","blue-1-300":"#82ACF6","blue-1-350":"#709BFE","blue-1-400":"#608BFF","blue-1-450":"#537BFB","blue-1-500":"#476DEC","blue-1-550":"#3C60DD","blue-1-600":"#3252CF","blue-1-650":"#2B48B9","blue-1-700":"#28409B","blue-1-750":"#24397E","blue-1-800":"#223260","blue-1-850":"#1F2A48","blue-1-900":"#191F32","blue-1-950":"#111320","blue-2-050":"#E5F2F3","blue-2-100":"#CDE6EC","blue-2-150":"#B7D9EA","blue-2-200":"#9BCDE7","blue-2-250":"#84C1E0","blue-2-300":"#6BB4D8","blue-2-350":"#5CA6CB","blue-2-400":"#4D99BC","blue-2-450":"#3E8CAE","blue-2-500":"#2F7FA2","blue-2-550":"#1F7295","blue-2-600":"#056688","blue-2-650":"#00597B","blue-2-700":"#004C6C","blue-2-750":"#003F5E","blue-2-800":"#003353","blue-2-850":"#0C273E","blue-2-900":"#0E1C2C","blue-2-950":"#0A121C","purple-050":"#EDF1FA","purple-100":"#DCE2F5","purple-150":"#CCD4F1","purple-200":"#BCC4F0","purple-250":"#ADB6F2","purple-300":"#9EA6F6","purple-350":"#8E95FD","purple-400":"#8083FF","purple-450":"#7173FF","purple-500":"#6665F1","purple-550":"#5B57E2","purple-600":"#5049D4","purple-650":"#4641BC","purple-700":"#3D39A2","purple-750":"#363680","purple-800":"#2E3162","purple-850":"#242848","purple-900":"#1C1E32","purple-950":"#121320","pink-050":"#F8EFF4","pink-100":"#F0DEE9","pink-150":"#EBCDDF","pink-200":"#E7BDD6","pink-250":"#E5A9CC","pink-300":"#E695C0","pink-350":"#EA7CAE","pink-400":"#E4659F","pink-450":"#DD4F93","pink-500":"#CD4085","pink-550":"#BE3279","pink-600":"#AE216D","pink-650":"#9B195D","pink-700":"#86164E","pink-750":"#6E1B3D","pink-800":"#551E31","pink-850":"#3F1C24","pink-900":"#2D161A","pink-950":"#1C0E10","black-000":"#191B2200","black-050":"#191B220D","black-100":"#191B221A","black-150":"#191B2226","black-200":"#191B2233","black-250":"#191B2240","black-300":"#191B224D","black-350":"#191B2259","black-400":"#191B2266","black-450":"#191B2273","black-500":"#191B2280","black-550":"#191B228C","black-600":"#191B2299","black-650":"#191B22A6","black-700":"#191B22B2","black-750":"#191B22BF","black-800":"#191B22CC","black-850":"#191B22D9","black-900":"#191B22E5","black-950":"#0F1115F2","white-000":"#F7F8F800","white-050":"#F7F8F80D","white-100":"#F7F8F81A","white-150":"#F7F8F826","white-200":"#F7F8F833","white-250":"#F7F8F840","white-300":"#F7F8F84D","white-350":"#F7F8F859","white-400":"#F7F8F866","white-450":"#F7F8F873","white-500":"#F7F8F880","white-550":"#F7F8F88C","white-600":"#F7F8F899","white-650":"#F7F8F8A6","white-700":"#F7F8F8B2","white-750":"#F7F8F8BF","white-800":"#F7F8F8CC","white-850":"#F7F8F8D9","white-900":"#F7F8F8E5","white-950":"#F7F8F8F2","white-975":"#F7F8F8F9"},"font":{"sizes":{"xs":"0.75rem","sm":"0.875rem","md":"1rem","lg":"1.125rem","ml":"0.938rem","xl":"1.25rem","t":"0.6875rem","s":"0.75rem","h1":"2rem","h2":"1.75rem","h3":"1.5rem","h4":"1.375rem","h5":"1.25rem","h6":"1.125rem","xl-alt":"5rem","lg-alt":"4.5rem","md-alt":"4rem","sm-alt":"3.5rem","xs-alt":"3rem"},"weights":{"thin":100,"extrabold":800},"families":{"accent":"Marianne, Inter, Roboto Flex Variable, sans-serif","base":"Marianne, Inter, Roboto Flex Variable, sans-serif"}},"spacings":{"0":"0","none":"0","auto":"auto","bx":"2.2rem","full":"100%","4xs":"0.125rem","3xs":"0.25rem","2xs":"0.375rem","xs":"0.5rem","sm":"0.75rem","base":"1rem","md":"1.5rem","lg":"2rem","xl":"2.5rem","xxl":"3rem","2xl":"3rem","xxxl":"3.5rem","3xl":"3.5rem","4xl":"4rem","5xl":"4.5rem","6xl":"6rem","7xl":"7.5rem"},"breakpoints":{"xxs":"320px","xs":"480px","mobile":"768px","tablet":"1024px"}},"contextuals":{"background":{"surface":{"primary":"#FFFFFF","secondary":"#FFFFFF","tertiary":"#F8F8F9"},"semantic":{"overlay":{"primary":"#1B1B230D","primary-hover":"#1B1B231A"},"contextual":{"primary":"#1B1B230D","primary-hover":"#1B1B231A"},"brand":{"primary":"#5E5CD0","primary-hover":"#4844AD","secondary":"#DDE2F5","secondary-hover":"#CED3F1","tertiary":"#EEF1FA","tertiary-hover":"#DDE2F5"},"neutral":{"primary":"#69697D","primary-hover":"#515164","secondary":"#E2E2EA","secondary-hover":"#D3D4E0","tertiary":"#F0F0F3","tertiary-hover":"#E2E2EA"},"info":{"primary":"#0069CF","primary-hover":"#0D4EAA","secondary":"#D5E4F3","secondary-hover":"#BFD7F0","tertiary":"#EAF2F9","tertiary-hover":"#D5E4F3"},"success":{"primary":"#027B3E","primary-hover":"#006024","secondary":"#CFE4D4","secondary-hover":"#BAD9C1","tertiary":"#E8F1EA","tertiary-hover":"#CFE4D4"},"warning":{"primary":"#BC4200","primary-hover":"#9E2300","secondary":"#F1E0D3","secondary-hover":"#ECD0BC","tertiary":"#F8F0E9","tertiary-hover":"#F1E0D3"},"error":{"primary":"#D7010E","primary-hover":"#AA0000","secondary":"#F4DFD9","secondary-hover":"#F0CEC6","tertiary":"#F9EFEC","tertiary-hover":"#F4DFD9"},"disabled":{"primary":"#E2E2EA","secondary":"#F0F0F3"}},"palette":{"brand":{"primary":"#6969DF","secondary":"#8F94FD","tertiary":"#CED3F1"},"red":{"primary":"#DA3B49","secondary":"#F37B7E","tertiary":"#F1CDCB"},"orange":{"primary":"#B95D33","secondary":"#E5845A","tertiary":"#ECD0BD"},"brown":{"primary":"#8F7158","secondary":"#B8987E","tertiary":"#EBD0BA"},"yellow":{"primary":"#9D6E00","secondary":"#C2972E","tertiary":"#E1D4B7"},"green":{"primary":"#008948","secondary":"#45B173","tertiary":"#B8D8C1"},"blue-1":{"primary":"#4279B9","secondary":"#68A1E4","tertiary":"#C1D7F0"},"blue-2":{"primary":"#00848F","secondary":"#00AFBA","tertiary":"#B2DCE0"},"purple":{"primary":"#9961AF","secondary":"#C188D9","tertiary":"#E7D1E7"},"pink":{"primary":"#AA5F80","secondary":"#D685A8","tertiary":"#EACEDF"},"gray":{"primary":"#75758A","secondary":"#9C9CB2","tertiary":"#D3D4E0"}}},"content":{"logo1":"#4844AD","logo2":"#4844AD","semantic":{"contextual":{"primary":"#F8F8F9F2"},"overlay":{"primary":"#F8F8F9F2"},"brand":{"primary":"#3E3B98","secondary":"#534FC2","tertiary":"#5E5CD0","on-brand":"#EEF1FA"},"neutral":{"primary":"#25252F","secondary":"#5D5D70","tertiary":"#69697D","on-neutral":"#F0F0F3"},"info":{"primary":"#124394","secondary":"#005BC0","tertiary":"#0069CF","on-info":"#EAF2F9"},"success":{"primary":"#005317","secondary":"#016D31","tertiary":"#027B3E","on-success":"#E8F1EA"},"warning":{"primary":"#882011","secondary":"#AD3300","tertiary":"#BC4200","on-warning":"#F8F0E9"},"error":{"primary":"#910C06","secondary":"#C00100","tertiary":"#D7010E","on-error":"#F9EFEC"},"disabled":{"primary":"#A9A9BF","secondary":"#F8F8F980"}},"palette":{"brand":{"primary":"#6969DF"},"red":{"primary":"#DA3B49"},"orange":{"primary":"#B95D33"},"brown":{"primary":"#8F7158"},"yellow":{"primary":"#9D6E00"},"green":{"primary":"#008948"},"blue-1":{"primary":"#4279B9"},"blue-2":{"primary":"#00848F"},"purple":{"primary":"#9961AF"},"pink":{"primary":"#AA5F80"},"gray":{"primary":"#75758A"}}},"border":{"surface":{"primary":"#E2E2EA"},"semantic":{"contextual":{"primary":"#F8F8F933"},"overlay":{"primary":"#F8F8F933"},"brand":{"primary":"#5E5CD0","secondary":"#A0A5F6","tertiary":"#CED3F1"},"neutral":{"primary":"#69697D","secondary":"#A9A9BF","tertiary":"#D3D4E0"},"info":{"primary":"#0069CF","secondary":"#6EB0F2","tertiary":"#BFD7F0"},"success":{"primary":"#027B3E","secondary":"#6CBA83","tertiary":"#BAD9C1"},"warning":{"primary":"#BC4200","secondary":"#EB9970","tertiary":"#ECD0BC"},"error":{"primary":"#D7010E","secondary":"#EF9486","tertiary":"#F0CEC6"},"disabled":{"primary":"#E2E2EA"}}}},"components":{"modal":{"width-small":"342px"},"tooltip":{"padding":"4px 8px"},"button":{"medium-height":"40px","medium-text-height":"40px","border-radius":"4px","border-radius--active":"4px","border-radius--focus":"4px"},"resize-handle":{"hover--color":"#E2E2EA"},"datagrid":{"header--color":"#25252F","header--size":"12px","header--weight":"500","body--background-color-hover":"#F0F0F3"},"forms-fileuploader":{"border-style":"solid","border-radius":"4px","border-width":"1px"},"forms-checkbox":{"font-size":"0.875rem"},"forms-input":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-select":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-textarea":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-datepicker":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"badge":{"font-size":"0.75rem","border-radius":"12px","padding-inline":"0.5rem","padding-block":"0.375rem","accent":{"background-color":"#DDE2F5","color":"#534FC2"},"neutral":{"background-color":"#E2E2EA","color":"#5D5D70"},"danger":{"background-color":"#F4DFD9","color":"#C00100"},"success":{"background-color":"#CFE4D4","color":"#016D31"},"warning":{"background-color":"#F1E0D3","color":"#AD3300"},"info":{"background-color":"#D5E4F3","color":"#005BC0"}}}},"anct-dark":{"globals":{"colors":{"logo-1-light":"#2845C1","logo-2-light":"#FBC63A","logo-1-dark":"#94A3E5","logo-2-dark":"#EFD183","brand-050":"#EEF0F9","brand-100":"#DDE2F5","brand-150":"#CBD4F1","brand-200":"#B8C6F0","brand-250":"#A5B7F2","brand-300":"#91A8F7","brand-350":"#7C98FE","brand-400":"#6A89FF","brand-450":"#5A7AFB","brand-500":"#4B6BF0","brand-550":"#3E5CE7","brand-600":"#3352D5","brand-650":"#2A47C0","brand-700":"#2340A3","brand-750":"#223A7F","brand-800":"#1F325F","brand-850":"#1B2845","brand-900":"#151E30","brand-950":"#0D121D","gray-000":"#FFFFFF","gray-025":"#F6F8FA","gray-050":"#ECF1F7","gray-100":"#DDE3F3","gray-150":"#CCD4EA","gray-200":"#BCC7E0","gray-250":"#AEB9D2","gray-300":"#A1ABC4","gray-350":"#949EB6","gray-400":"#8791A9","gray-450":"#7A849B","gray-500":"#6D778E","gray-550":"#616A81","gray-600":"#555E75","gray-650":"#495268","gray-700":"#3E475C","gray-750":"#333B50","gray-800":"#283044","gray-850":"#1E2539","gray-900":"#171B28","gray-950":"#0F1118","gray-1000":"#000000","info-050":"#E9F2F8","info-100":"#D2E5F1","info-150":"#C0D7F0","info-200":"#AEC8F0","info-250":"#9EB9F2","info-300":"#8DABEE","info-350":"#7B9DE9","info-400":"#6E8FDB","info-450":"#6282CD","info-500":"#5575BF","info-550":"#4968B1","info-600":"#3E5CA3","info-650":"#344F97","info-700":"#294389","info-750":"#243972","info-800":"#1D2F5B","info-850":"#1A2744","info-900":"#151D2F","info-950":"#0E131F","success-050":"#E7F1E9","success-100":"#CFE3D3","success-150":"#B9D8C0","success-200":"#A1CEAC","success-250":"#85C496","success-300":"#63BC7F","success-350":"#45B16B","success-400":"#1CA659","success-450":"#00984C","success-500":"#008A3F","success-550":"#007C32","success-600":"#006E24","success-650":"#016016","success-700":"#005305","success-750":"#0D450A","success-800":"#11380E","success-850":"#132A11","success-900":"#101E0F","success-950":"#091209","warning-050":"#F6F0E8","warning-100":"#EDE2D1","warning-150":"#E6D3B8","warning-200":"#E3C39F","warning-250":"#E3B082","warning-300":"#E19E5C","warning-350":"#D98E3F","warning-400":"#CF7D19","warning-450":"#C17000","warning-500":"#B36300","warning-550":"#A45600","warning-600":"#964900","warning-650":"#893C00","warning-700":"#7B2F00","warning-750":"#68270D","warning-800":"#562013","warning-850":"#411D18","warning-900":"#2E1714","warning-950":"#1D0F0D","error-050":"#F9EFEC","error-100":"#F4DFD9","error-150":"#F0CEC6","error-200":"#EEBCB2","error-250":"#EEA99D","error-300":"#EF9487","error-350":"#F37C6E","error-400":"#F65F53","error-450":"#EF443C","error-500":"#E0342E","error-550":"#D0201F","error-600":"#C0000C","error-650":"#AA0000","error-700":"#910C06","error-750":"#731E16","error-800":"#58201A","error-850":"#411D18","error-900":"#2E1714","error-950":"#1D0F0D","red-050":"#F9EFEC","red-100":"#F4DEDA","red-150":"#F0CDC9","red-200":"#EEBBB6","red-250":"#EEA8A2","red-300":"#F0938D","red-350":"#EC7E78","red-400":"#E46D67","red-450":"#D95B58","red-500":"#CA4E4B","red-550":"#BB403F","red-600":"#AC3233","red-650":"#9D2227","red-700":"#882023","red-750":"#721D1B","red-800":"#58201A","red-850":"#401D18","red-900":"#2E1714","red-950":"#1D0F0D","orange-050":"#F8F0E9","orange-100":"#F1E0D3","orange-150":"#ECD0BD","orange-200":"#E9C0A5","orange-250":"#E8AE8A","orange-300":"#EB9870","orange-350":"#EB845A","orange-400":"#E66E37","orange-450":"#DD5B16","orange-500":"#CE4D00","orange-550":"#BF3E00","orange-600":"#B02F00","orange-650":"#A11E00","orange-700":"#8A1E14","orange-750":"#731E16","orange-800":"#58201A","orange-850":"#401D18","orange-900":"#2E1714","orange-950":"#1D0F0D","brown-050":"#F5F0E8","brown-100":"#ECE2D1","brown-150":"#E9D1B9","brown-200":"#E3C19D","brown-250":"#DCB187","brown-300":"#D2A26F","brown-350":"#C49562","brown-400":"#B68855","brown-450":"#A97B48","brown-500":"#9B6E3B","brown-550":"#8E612F","brown-600":"#815521","brown-650":"#744913","brown-700":"#673D00","brown-750":"#5A3100","brown-800":"#4E2600","brown-850":"#3D1F0B","brown-900":"#2C170F","brown-950":"#1C0F0B","yellow-050":"#F3F0E7","yellow-100":"#E9E2CF","yellow-150":"#E0D4B7","yellow-200":"#DAC59A","yellow-250":"#D5B67A","yellow-300":"#D0A559","yellow-350":"#CC9331","yellow-400":"#C48400","yellow-450":"#B77600","yellow-500":"#AA6800","yellow-550":"#9D5A00","yellow-600":"#914D00","yellow-650":"#843F00","yellow-700":"#773200","yellow-750":"#6A2601","yellow-800":"#56210F","yellow-850":"#401D16","yellow-900":"#2E1714","yellow-950":"#1D0F0D","green-050":"#E3F1EF","green-100":"#CAE5E1","green-150":"#B0DBD4","green-200":"#91D1C7","green-250":"#6AC8BC","green-300":"#4DBCAF","green-350":"#3CAFA2","green-400":"#2AA194","green-450":"#109487","green-500":"#00867A","green-550":"#00786D","green-600":"#016A60","green-650":"#015D53","green-700":"#005047","green-750":"#00443B","green-800":"#00382F","green-850":"#002C25","green-900":"#041F1A","green-950":"#041310","blue-1-050":"#EAF2F9","blue-1-100":"#D4E4F3","blue-1-150":"#BFD7F0","blue-1-200":"#AAC9EF","blue-1-250":"#96BBF1","blue-1-300":"#82ACF6","blue-1-350":"#709BFE","blue-1-400":"#608BFF","blue-1-450":"#537BFB","blue-1-500":"#476DEC","blue-1-550":"#3C60DD","blue-1-600":"#3252CF","blue-1-650":"#2B48B9","blue-1-700":"#28409B","blue-1-750":"#24397E","blue-1-800":"#223260","blue-1-850":"#1F2A48","blue-1-900":"#191F32","blue-1-950":"#111320","blue-2-050":"#E5F2F3","blue-2-100":"#CDE6EC","blue-2-150":"#B7D9EA","blue-2-200":"#9BCDE7","blue-2-250":"#84C1E0","blue-2-300":"#6BB4D8","blue-2-350":"#5CA6CB","blue-2-400":"#4D99BC","blue-2-450":"#3E8CAE","blue-2-500":"#2F7FA2","blue-2-550":"#1F7295","blue-2-600":"#056688","blue-2-650":"#00597B","blue-2-700":"#004C6C","blue-2-750":"#003F5E","blue-2-800":"#003353","blue-2-850":"#0C273E","blue-2-900":"#0E1C2C","blue-2-950":"#0A121C","purple-050":"#EDF1FA","purple-100":"#DCE2F5","purple-150":"#CCD4F1","purple-200":"#BCC4F0","purple-250":"#ADB6F2","purple-300":"#9EA6F6","purple-350":"#8E95FD","purple-400":"#8083FF","purple-450":"#7173FF","purple-500":"#6665F1","purple-550":"#5B57E2","purple-600":"#5049D4","purple-650":"#4641BC","purple-700":"#3D39A2","purple-750":"#363680","purple-800":"#2E3162","purple-850":"#242848","purple-900":"#1C1E32","purple-950":"#121320","pink-050":"#F8EFF4","pink-100":"#F0DEE9","pink-150":"#EBCDDF","pink-200":"#E7BDD6","pink-250":"#E5A9CC","pink-300":"#E695C0","pink-350":"#EA7CAE","pink-400":"#E4659F","pink-450":"#DD4F93","pink-500":"#CD4085","pink-550":"#BE3279","pink-600":"#AE216D","pink-650":"#9B195D","pink-700":"#86164E","pink-750":"#6E1B3D","pink-800":"#551E31","pink-850":"#3F1C24","pink-900":"#2D161A","pink-950":"#1C0E10","black-000":"#191B2200","black-050":"#191B220D","black-100":"#191B221A","black-150":"#191B2226","black-200":"#191B2233","black-250":"#191B2240","black-300":"#191B224D","black-350":"#191B2259","black-400":"#191B2266","black-450":"#191B2273","black-500":"#191B2280","black-550":"#191B228C","black-600":"#191B2299","black-650":"#191B22A6","black-700":"#191B22B2","black-750":"#191B22BF","black-800":"#191B22CC","black-850":"#191B22D9","black-900":"#191B22E5","black-950":"#0F1115F2","white-000":"#F7F8F800","white-050":"#F7F8F80D","white-100":"#F7F8F81A","white-150":"#F7F8F826","white-200":"#F7F8F833","white-250":"#F7F8F840","white-300":"#F7F8F84D","white-350":"#F7F8F859","white-400":"#F7F8F866","white-450":"#F7F8F873","white-500":"#F7F8F880","white-550":"#F7F8F88C","white-600":"#F7F8F899","white-650":"#F7F8F8A6","white-700":"#F7F8F8B2","white-750":"#F7F8F8BF","white-800":"#F7F8F8CC","white-850":"#F7F8F8D9","white-900":"#F7F8F8E5","white-950":"#F7F8F8F2","white-975":"#F7F8F8F9"},"font":{"sizes":{"xs":"0.75rem","sm":"0.875rem","md":"1rem","lg":"1.125rem","ml":"0.938rem","xl":"1.25rem","t":"0.6875rem","s":"0.75rem","h1":"2rem","h2":"1.75rem","h3":"1.5rem","h4":"1.375rem","h5":"1.25rem","h6":"1.125rem","xl-alt":"5rem","lg-alt":"4.5rem","md-alt":"4rem","sm-alt":"3.5rem","xs-alt":"3rem"},"weights":{"thin":100,"extrabold":800},"families":{"accent":"Marianne, Inter, Roboto Flex Variable, sans-serif","base":"Marianne, Inter, Roboto Flex Variable, sans-serif"}},"spacings":{"0":"0","none":"0","auto":"auto","bx":"2.2rem","full":"100%","4xs":"0.125rem","3xs":"0.25rem","2xs":"0.375rem","xs":"0.5rem","sm":"0.75rem","base":"1rem","md":"1.5rem","lg":"2rem","xl":"2.5rem","xxl":"3rem","2xl":"3rem","xxxl":"3.5rem","3xl":"3.5rem","4xl":"4rem","5xl":"4.5rem","6xl":"6rem","7xl":"7.5rem"},"breakpoints":{"xxs":"320px","xs":"480px","mobile":"768px","tablet":"1024px"}},"contextuals":{"background":{"surface":{"primary":"#2F303D","secondary":"#25252F","tertiary":"#1B1B23"},"semantic":{"contextual":{"primary":"#F8F8F90D","primary-hover":"#F8F8F91A"},"overlay":{"primary":"#F8F8F90D","primary-hover":"#F8F8F91A"},"brand":{"primary":"#5E5CD0","primary-hover":"#4844AD","secondary":"#3E3B98","secondary-hover":"#36347D","tertiary":"#36347D","tertiary-hover":"#2D2F5F"},"neutral":{"primary":"#69697D","primary-hover":"#515164","secondary":"#454558","secondary-hover":"#3A3A4C","tertiary":"#3A3A4C","tertiary-hover":"#2F303D"},"info":{"primary":"#0069CF","primary-hover":"#0D4EAA","secondary":"#124394","secondary-hover":"#163878","tertiary":"#163878","tertiary-hover":"#192F5A"},"success":{"primary":"#027B3E","primary-hover":"#006024","secondary":"#005317","secondary-hover":"#0D4511","tertiary":"#0D4511","tertiary-hover":"#11380E"},"warning":{"primary":"#BC4200","primary-hover":"#9E2300","secondary":"#882011","secondary-hover":"#731E16","tertiary":"#731E16","tertiary-hover":"#58201A"},"error":{"primary":"#D7010E","primary-hover":"#AA0000","secondary":"#910C06","secondary-hover":"#731E16","tertiary":"#731E16","tertiary-hover":"#58201A"},"disabled":{"primary":"#3A3A4C","secondary":"#2F303D"}},"palette":{"brand":{"primary":"#8F94FD","secondary":"#7576EE","tertiary":"#5E5CD0"},"red":{"primary":"#F37B7E","secondary":"#E94A55","tertiary":"#CA2A3C"},"orange":{"primary":"#E5845A","secondary":"#C86A40","tertiary":"#AB5025"},"brown":{"primary":"#B8987E","secondary":"#9D7E65","tertiary":"#82654C"},"yellow":{"primary":"#C2972E","secondary":"#AB7B00","tertiary":"#916100"},"green":{"primary":"#45B173","secondary":"#029755","tertiary":"#017B3B"},"blue-1":{"primary":"#68A1E4","secondary":"#4E86C7","tertiary":"#356CAC"},"blue-2":{"primary":"#00AFBA","secondary":"#00929D","tertiary":"#007682"},"purple":{"primary":"#C188D9","secondary":"#A66EBD","tertiary":"#8B55A1"},"pink":{"primary":"#D685A8","secondary":"#B86C8D","tertiary":"#9C5374"},"gray":{"primary":"#9C9CB2","secondary":"#828297","tertiary":"#69697D"}}},"content":{"logo1":"#BEC5F0","logo2":"#BEC5F0","semantic":{"contextual":{"primary":"#1B1B23D9"},"overlay":{"primary":"#1B1B23D9"},"brand":{"primary":"#EEF1FA","secondary":"#DDE2F5","tertiary":"#AFB5F1","on-brand":"#EEF1FA"},"neutral":{"primary":"#F0F0F3","secondary":"#E2E2EA","tertiary":"#B7B7CB","on-neutral":"#F0F0F3"},"info":{"primary":"#EAF2F9","secondary":"#D5E4F3","tertiary":"#8DBDEF","on-info":"#EAF2F9"},"success":{"primary":"#E8F1EA","secondary":"#CFE4D4","tertiary":"#86C597","on-success":"#E8F1EA"},"warning":{"primary":"#F8F0E9","secondary":"#F1E0D3","tertiary":"#E8AE8A","on-warning":"#F8F0E9"},"error":{"primary":"#F9EFEC","secondary":"#F4DFD9","tertiary":"#EEA99D","on-error":"#F9EFEC"},"disabled":{"primary":"#5D5D70","secondary":"#1B1B234D"}},"palette":{"brand":{"primary":"#6969DF"},"red":{"primary":"#DA3B49"},"orange":{"primary":"#B95D33"},"brown":{"primary":"#8F7158"},"yellow":{"primary":"#9D6E00"},"green":{"primary":"#008948"},"blue-1":{"primary":"#4279B9"},"blue-2":{"primary":"#00848F"},"purple":{"primary":"#9961AF"},"pink":{"primary":"#AA5F80"},"gray":{"primary":"#75758A"}}},"border":{"surface":{"primary":"#3A3A4C"},"semantic":{"contextual":{"primary":"#1B1B2333"},"overlay":{"primary":"#1B1B2333"},"brand":{"primary":"#7576EE","secondary":"#534FC2","tertiary":"#3E3B98"},"neutral":{"primary":"#828297","secondary":"#5D5D70","tertiary":"#454558"},"info":{"primary":"#1185ED","secondary":"#005BC0","tertiary":"#124394"},"success":{"primary":"#309556","secondary":"#016D31","tertiary":"#005317"},"warning":{"primary":"#DA5E18","secondary":"#AD3300","tertiary":"#882011"},"error":{"primary":"#F0463D","secondary":"#C00100","tertiary":"#910C06"},"disabled":{"primary":"#2F303D"}}}},"components":{"modal":{"width-small":"342px"},"tooltip":{"padding":"4px 8px"},"button":{"medium-height":"40px","medium-text-height":"40px","border-radius":"4px","border-radius--active":"4px","border-radius--focus":"4px"},"resize-handle":{"hover--color":"#E2E2EA"},"datagrid":{"header--color":"#25252F","header--size":"12px","header--weight":"500","body--background-color-hover":"#F0F0F3"},"forms-fileuploader":{"border-style":"solid","border-radius":"4px","border-width":"1px"},"forms-checkbox":{"font-size":"0.875rem"},"forms-input":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-select":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-textarea":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"forms-datepicker":{"border-radius":"4px","border-radius--hover":"4px","border-radius--focus":"4px"},"badge":{"font-size":"0.75rem","border-radius":"12px","padding-inline":"0.5rem","padding-block":"0.375rem","accent":{"background-color":"#DDE2F5","color":"#534FC2"},"neutral":{"background-color":"#E2E2EA","color":"#5D5D70"},"danger":{"background-color":"#F4DFD9","color":"#C00100"},"success":{"background-color":"#CFE4D4","color":"#016D31"},"warning":{"background-color":"#F1E0D3","color":"#AD3300"},"info":{"background-color":"#D5E4F3","color":"#005BC0"}}}}}}; diff --git a/yarn.lock b/yarn.lock index d6c64330..3d3f8bb2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1292,10 +1292,10 @@ dependencies: tslib "^2.8.0" -"@gouvfr-lasuite/cunningham-react@4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/cunningham-react/-/cunningham-react-4.3.0.tgz#a92f70e5f32d2b9cae4971642544d306e7fadfed" - integrity sha512-jGUebugMdK4LnyctS3EPrd1hKdt7oN4o22oazARuerq9JP0Yb9cmB49NsyleoFaMerF7nTJoD4eJCy0Y/9AeoQ== +"@gouvfr-lasuite/cunningham-react@4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/cunningham-react/-/cunningham-react-4.4.0.tgz#0a04d84848487b1e06bb504635f6075454d6c2b4" + integrity sha512-nWv1nQvGLIfz1cE08D0cQ1zn05+eX7PWOeenv4yAyDSb4I6pgrNMD+7zxnBlyOj9LPyvk66QClhivaPns66Gnw== dependencies: "@fontsource-variable/roboto-flex" "5.2.5" "@fontsource/material-icons-outlined" "5.2.5" From 812ad4c4c15bcbd5f37567e2a4f59441598a3f79 Mon Sep 17 00:00:00 2001 From: Nathan Vasse Date: Thu, 23 Jul 2026 15:27:36 +0200 Subject: [PATCH 2/8] =?UTF-8?q?=E2=9C=A8(front)=20add=20Alert=20component?= =?UTF-8?q?=20with=20custom=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap cunningham's new Alert to swap the material icons for the kit's own icon set, so alerts render consistently with the other components. It is exported at the root for direct use by consumer apps. --- src/components/alert/Alert.mdx | 38 ++++++++++++++++ src/components/alert/Alert.tsx | 34 ++++++++++++++ src/components/alert/alert.stories.tsx | 63 ++++++++++++++++++++++++++ src/components/alert/index.scss | 38 ++++++++++++++++ src/index.ts | 1 + src/library.scss | 1 + 6 files changed, 175 insertions(+) create mode 100644 src/components/alert/Alert.mdx create mode 100644 src/components/alert/Alert.tsx create mode 100644 src/components/alert/alert.stories.tsx create mode 100644 src/components/alert/index.scss diff --git a/src/components/alert/Alert.mdx b/src/components/alert/Alert.mdx new file mode 100644 index 00000000..ade23e79 --- /dev/null +++ b/src/components/alert/Alert.mdx @@ -0,0 +1,38 @@ +import { Meta, Canvas } from "@storybook/blocks"; +import * as AlertStories from "./alert.stories"; + + + +# Alert + +A thin wrapper around Cunningham's [`Alert`](https://cunningham.numerique.gouv.fr/?path=/docs/components-alert--docs) + +## Props + +### `children` + +The message of the alert. Any React node works, but it is usually a short +sentence describing the information, success, warning or error. + + + +### `type` + +The visual variant, from Cunningham's `VariantType` enum: `info`, `success`, +`warning`, `error` or `neutral`. Defaults to `info`. + + + + + + +### `canClose` + +When `true`, a close button is rendered on the right side of the alert, +letting the user dismiss it. + +### `buttons` + +A React node rendered next to the alert message. + + diff --git a/src/components/alert/Alert.tsx b/src/components/alert/Alert.tsx new file mode 100644 index 00000000..5b017760 --- /dev/null +++ b/src/components/alert/Alert.tsx @@ -0,0 +1,34 @@ +import { + Alert as AlertCunningham, + AlertProps, + VariantType, +} from "@gouvfr-lasuite/cunningham-react"; +import { IconSize } from "../icon"; +import { CircleCheck, Error as ErrorIcon, Warning } from ":/icons"; + +export const Alert = (props: AlertProps) => { + const Icon = typeToIcon(props.type); + return ( + : undefined} + /> + ); +}; + +const typeToIcon = (type?: VariantType) => { + switch (type) { + case VariantType.ERROR: + return ErrorIcon; + case VariantType.WARNING: + return Warning; + case VariantType.SUCCESS: + return CircleCheck; + case VariantType.INFO: + return ErrorIcon; + case VariantType.NEUTRAL: + return undefined; + default: + return ErrorIcon; + } +}; diff --git a/src/components/alert/alert.stories.tsx b/src/components/alert/alert.stories.tsx new file mode 100644 index 00000000..ef2525f4 --- /dev/null +++ b/src/components/alert/alert.stories.tsx @@ -0,0 +1,63 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Button, VariantType } from "@gouvfr-lasuite/cunningham-react"; +import { Alert } from "./Alert"; + +const meta = { + title: "Components/Alert", + component: Alert, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Info: Story = { + args: { + children: "Alert component info", + }, +}; + +export const CustomButtons: Story = { + args: { + children: "Alert component info", + canClose: true, + buttons: ( +
+ + +
+ ), + }, +}; + +export const Success: Story = { + args: { + children: "Alert component Success", + type: VariantType.SUCCESS, + }, +}; + +export const Warning: Story = { + args: { + children: "Alert component Warning", + type: VariantType.WARNING, + }, +}; + +export const Error: Story = { + args: { + children: "Alert component Error", + type: VariantType.ERROR, + }, +}; + +export const Neutral: Story = { + args: { + children: "Alert component Neutral", + type: VariantType.NEUTRAL, + }, +}; diff --git a/src/components/alert/index.scss b/src/components/alert/index.scss new file mode 100644 index 00000000..2e7e3ea6 --- /dev/null +++ b/src/components/alert/index.scss @@ -0,0 +1,38 @@ +.c__alert { + border-left-width: 1px; + padding: 12px; + + &__content { + min-height: auto; + font-size: 12px; + font-weight: 400; + } + + &__additional { + font-size: 11px; + } + + &--success { + background-color: var( + --c--contextuals--background--semantic--success--tertiary + ); + } + + &--warning { + background-color: var( + --c--contextuals--background--semantic--warning--tertiary + ); + } + + &--error { + background-color: var( + --c--contextuals--background--semantic--error--tertiary + ); + } + + &--neutral { + background-color: var( + --c--contextuals--background--semantic--neutral--tertiary + ); + } +} diff --git a/src/index.ts b/src/index.ts index 0f721099..75ed9167 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import "./library.scss"; export * from "./components/Provider/Provider"; +export * from "./components/alert/Alert"; export * from "./locales/Locale"; export * from "./components/button"; export * from "./components/menu"; diff --git a/src/library.scss b/src/library.scss index e1476504..83e15853 100644 --- a/src/library.scss +++ b/src/library.scss @@ -1,5 +1,6 @@ @use "@gouvfr-lasuite/cunningham-react/style"; @use "cunningham-custom-style"; +@use "./components/alert"; @use "./components/button"; @use "./components/dropdown-menu"; @use "./components/footer"; From 8e74e1f0509e974028ac5248953857b749540602 Mon Sep 17 00:00:00 2001 From: Nathan Vasse Date: Thu, 23 Jul 2026 15:27:46 +0200 Subject: [PATCH 3/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F(front)=20move=20upload?= =?UTF-8?q?=20component=20under=20form/file-uploader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The uploader is a form control, so it now lives alongside the other form components and its stories under Components/Forms. The move also brings an MDX docs page. Root exports are unchanged for consumers. --- e2e/helpers/mount-upload.tsx | 4 +- .../form/file-uploader/FileUploader.mdx | 126 ++++++++++++++++++ .../file-uploader}/FileUploader.tsx | 0 .../file-uploader}/UploadFileItem.tsx | 0 .../file-uploader/file-uploader.stories.tsx} | 14 +- .../{upload => form/file-uploader}/index.scss | 0 .../{upload => form/file-uploader}/index.tsx | 0 .../{upload => form/file-uploader}/types.ts | 0 .../upload-file-item.stories.scss | 0 .../upload-file-item.stories.tsx | 2 +- .../file-uploader}/utils.test.ts | 0 .../{upload => form/file-uploader}/utils.ts | 0 src/components/form/index.tsx | 1 + src/index.ts | 1 - src/library.scss | 2 +- 15 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 src/components/form/file-uploader/FileUploader.mdx rename src/components/{upload => form/file-uploader}/FileUploader.tsx (100%) rename src/components/{upload => form/file-uploader}/UploadFileItem.tsx (100%) rename src/components/{upload/upload.stories.tsx => form/file-uploader/file-uploader.stories.tsx} (97%) rename src/components/{upload => form/file-uploader}/index.scss (100%) rename src/components/{upload => form/file-uploader}/index.tsx (100%) rename src/components/{upload => form/file-uploader}/types.ts (100%) rename src/components/{upload => form/file-uploader}/upload-file-item.stories.scss (100%) rename src/components/{upload => form/file-uploader}/upload-file-item.stories.tsx (98%) rename src/components/{upload => form/file-uploader}/utils.test.ts (100%) rename src/components/{upload => form/file-uploader}/utils.ts (100%) diff --git a/e2e/helpers/mount-upload.tsx b/e2e/helpers/mount-upload.tsx index 3f16a47c..ea48456d 100644 --- a/e2e/helpers/mount-upload.tsx +++ b/e2e/helpers/mount-upload.tsx @@ -1,8 +1,8 @@ import { useState } from "react"; import { Modal, ModalSize } from "@gouvfr-lasuite/cunningham-react"; import { CunninghamProvider } from "../../src/components/Provider/Provider"; -import { FileUploader } from "../../src/components/upload/FileUploader"; -import { UploadFile } from "../../src/components/upload/types"; +import { FileUploader } from "../../src/components/form/file-uploader/FileUploader"; +import { UploadFile } from "../../src/components/form/file-uploader/types"; const GB = 1000 * 1000 * 1000; diff --git a/src/components/form/file-uploader/FileUploader.mdx b/src/components/form/file-uploader/FileUploader.mdx new file mode 100644 index 00000000..e362145c --- /dev/null +++ b/src/components/form/file-uploader/FileUploader.mdx @@ -0,0 +1,126 @@ +import { Meta, Canvas, ArgTypes } from "@storybook/blocks"; +import * as FileUploaderStories from "./file-uploader.stories"; + + + +# FileUploader + +`FileUploader` is a drop zone where users can drag & drop files or browse the file system. It is fully controlled: you own the list of files and their upload states, and the component renders them. + +Labels are translated via Cunningham, so the component must live under a `CunninghamProvider`. The styles are bundled in `@gouvfr-lasuite/ui-kit/style`, so make sure it is imported once in your app. + +## Usage + +```tsx +import { FileUploader, UploadFile } from "@gouvfr-lasuite/ui-kit"; + +const [files, setFiles] = useState([]); + + + setFiles((prev) => prev.filter((f) => f.id !== file.id)) + } +/>; +``` + + + +## The controlled model + +The component never keeps files internally. When the user selects or drops files, `onAddFiles` is called with the **next** collection: the replacement file in single mode, or the current `files` plus the added ones in multiple mode. Each entry is an `UploadFile`: + +```ts +type UploadFile = { + /** Stable identifier. */ + id: string; + /** Native file selected by the user, including its binary contents. */ + originalFile: File; + /** "pending" | "uploading" | "done" (default) | "error" */ + status?: UploadFileStatus; + /** Upload progress percentage, from 0 to 100, used when status is "uploading". */ + progress?: number; + /** Short error message shown inline when status is "error". */ + error?: string; + /** Optional detailed error message shown when hovering or focusing the error icon. */ + errorDetails?: string; +}; +``` + +Start your upload from `onAddFiles`, then update each file's `status` and `progress` as the request advances — the uploader re-renders accordingly. + +## Single or multiple files + +By default only one file can be selected; the dropzone itself displays the file and its state (uploading / done / error), and picking a new file replaces it: + + + +Pass `multiple` to accept several files: the dropzone stays available and the files are listed below it, each row with its own progress, error, and remove/cancel controls: + + + + + +The component forwards `accept` to the native ``, so file types can be restricted: + +```tsx + +``` + +`maxSize` (in bytes) is displayed as a "Max …" hint in the dropzone. It is informative only: enforce the limit yourself and flag oversized files with `status: "error"`. + +## States + +A file with `status: "uploading"` shows a spinner (single mode) or a progress bar (multiple mode); `"error"` shows the `error` message — plus `errorDetails` on the error icon's tooltip in multiple mode: + + + + + +A typical flow cycles through those states: + + + +## Remove and cancel + +The remove control appears on done or errored files only when `onRemoveFile` is provided; the cancel control appears on uploading files only when `onCancelFile` is provided. Both are called with the targeted `UploadFile` — filtering it out of your state removes the row: + +```tsx + + setFiles((prev) => prev.filter((f) => f.id !== file.id)) + } + onCancelFile={(file) => { + abortUpload(file.id); + setFiles((prev) => prev.filter((f) => f.id !== file.id)); + }} +/> +``` + +## In a modal + +The uploader adapts to its container width, e.g. inside a `Modal`: + + + +## Custom labels + +Every text is translated via Cunningham (the `components.upload.*` keys) and can be overridden per instance through `labels`: + +```tsx + +``` + +## Props + + diff --git a/src/components/upload/FileUploader.tsx b/src/components/form/file-uploader/FileUploader.tsx similarity index 100% rename from src/components/upload/FileUploader.tsx rename to src/components/form/file-uploader/FileUploader.tsx diff --git a/src/components/upload/UploadFileItem.tsx b/src/components/form/file-uploader/UploadFileItem.tsx similarity index 100% rename from src/components/upload/UploadFileItem.tsx rename to src/components/form/file-uploader/UploadFileItem.tsx diff --git a/src/components/upload/upload.stories.tsx b/src/components/form/file-uploader/file-uploader.stories.tsx similarity index 97% rename from src/components/upload/upload.stories.tsx rename to src/components/form/file-uploader/file-uploader.stories.tsx index 158b5e65..ba9ef6f5 100644 --- a/src/components/upload/upload.stories.tsx +++ b/src/components/form/file-uploader/file-uploader.stories.tsx @@ -59,7 +59,11 @@ const createSimulatedUploadFiles = (): SimulatedUploadFile[] => [ }, { id: "simulated-error", - originalFile: createFileFixture("Flower wallpaper.jpg", 8 * MB, "image/jpeg"), + originalFile: createFileFixture( + "Flower wallpaper.jpg", + 8 * MB, + "image/jpeg", + ), status: "error", error: "The upload failed", errorDetails: "The file could not be uploaded. Please try again.", @@ -100,7 +104,7 @@ const SimulatedUploader = (props: FileUploaderProps) => { return { ...file, progress }; }), ); - }, 1000); + }, 100); return () => window.clearInterval(intervalId); }, []); @@ -137,7 +141,8 @@ const SimulatedUploader = (props: FileUploaderProps) => { ...file, status: "uploading" as const, progress: 0, - outcome: index % 2 === 0 ? ("done" as const) : ("error" as const), + outcome: + index % 2 === 0 ? ("done" as const) : ("error" as const), }; }); }); @@ -160,9 +165,8 @@ const SimulatedUploader = (props: FileUploaderProps) => { }; const meta: Meta = { - title: "Components/Upload", + title: "Components/Forms/FileUploader", component: FileUploader, - tags: ["autodocs"], parameters: { docs: { description: { diff --git a/src/components/upload/index.scss b/src/components/form/file-uploader/index.scss similarity index 100% rename from src/components/upload/index.scss rename to src/components/form/file-uploader/index.scss diff --git a/src/components/upload/index.tsx b/src/components/form/file-uploader/index.tsx similarity index 100% rename from src/components/upload/index.tsx rename to src/components/form/file-uploader/index.tsx diff --git a/src/components/upload/types.ts b/src/components/form/file-uploader/types.ts similarity index 100% rename from src/components/upload/types.ts rename to src/components/form/file-uploader/types.ts diff --git a/src/components/upload/upload-file-item.stories.scss b/src/components/form/file-uploader/upload-file-item.stories.scss similarity index 100% rename from src/components/upload/upload-file-item.stories.scss rename to src/components/form/file-uploader/upload-file-item.stories.scss diff --git a/src/components/upload/upload-file-item.stories.tsx b/src/components/form/file-uploader/upload-file-item.stories.tsx similarity index 98% rename from src/components/upload/upload-file-item.stories.tsx rename to src/components/form/file-uploader/upload-file-item.stories.tsx index 0f68ebfe..33f1235b 100644 --- a/src/components/upload/upload-file-item.stories.tsx +++ b/src/components/form/file-uploader/upload-file-item.stories.tsx @@ -91,7 +91,7 @@ const RowPreview = ({ ); const meta = { - title: "Components/Upload/Rows", + title: "Components/Forms/FileUploader/Rows", component: UploadFileItem, tags: ["autodocs"], parameters: { diff --git a/src/components/upload/utils.test.ts b/src/components/form/file-uploader/utils.test.ts similarity index 100% rename from src/components/upload/utils.test.ts rename to src/components/form/file-uploader/utils.test.ts diff --git a/src/components/upload/utils.ts b/src/components/form/file-uploader/utils.ts similarity index 100% rename from src/components/upload/utils.ts rename to src/components/form/file-uploader/utils.ts diff --git a/src/components/form/index.tsx b/src/components/form/index.tsx index c8a3baf3..cb8a8b99 100644 --- a/src/components/form/index.tsx +++ b/src/components/form/index.tsx @@ -1 +1,2 @@ export * from "./label"; +export * from "./file-uploader"; diff --git a/src/index.ts b/src/index.ts index 75ed9167..1c443ea5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,6 @@ export * from "./components/share"; export * from "./components/users"; export * from "./components/onboarding-modal"; export * from "./components/storage-gauge"; -export * from "./components/upload"; export * from "./components/release-note-modal"; export * from "./components/preview"; export * from "./hooks/useCustomTranslations"; diff --git a/src/library.scss b/src/library.scss index 83e15853..2e635255 100644 --- a/src/library.scss +++ b/src/library.scss @@ -1,6 +1,7 @@ @use "@gouvfr-lasuite/cunningham-react/style"; @use "cunningham-custom-style"; @use "./components/alert"; +@use "./components/form/file-uploader"; @use "./components/button"; @use "./components/dropdown-menu"; @use "./components/footer"; @@ -44,7 +45,6 @@ @use "./components/feedback-form"; @use "./components/onboarding-modal"; @use "./components/storage-gauge"; -@use "./components/upload"; @use "./components/preview/preview"; @use "react-pdf/dist/Page/AnnotationLayer.css"; @use "react-pdf/dist/Page/TextLayer.css"; From faf4e0b32bbf7d78c650ee6693bd9b6b572cb6a9 Mon Sep 17 00:00:00 2001 From: Nathan Vasse Date: Thu, 23 Jul 2026 15:27:52 +0200 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=92=84(front)=20refresh=20share=20mod?= =?UTF-8?q?al=20styling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Role and link dropdown triggers now use nano neutral buttons with the kit's chevron icons, and section titles are lighter, to match the new share modal design. User rows break long names and emails instead of overflowing. Stories say Reader instead of Viewer to follow the products' wording. --- .../share/access/AccessRoleDropdown.tsx | 14 +++++--- .../share/modal/items/ShareLinkSettings.tsx | 16 +++++---- .../share/modal/items/share-items.stories.tsx | 2 +- src/components/share/modal/share-modal.scss | 34 +++++++++++-------- src/components/users/rows/index.scss | 6 ++++ 5 files changed, 46 insertions(+), 26 deletions(-) diff --git a/src/components/share/access/AccessRoleDropdown.tsx b/src/components/share/access/AccessRoleDropdown.tsx index 98358df7..e9deec33 100644 --- a/src/components/share/access/AccessRoleDropdown.tsx +++ b/src/components/share/access/AccessRoleDropdown.tsx @@ -6,6 +6,8 @@ import { DropdownMenuOption, DropdownMenuProps, } from ":/components/dropdown-menu"; +import { ChevronDown, ChevronUp } from ":/icons"; +import { IconSize } from ":/components/icon"; type AccessRoleDropdownProps = { selectedRole: string; @@ -79,13 +81,15 @@ export const AccessRoleDropdown = ({ diff --git a/src/components/share/modal/items/share-items.stories.tsx b/src/components/share/modal/items/share-items.stories.tsx index d8479365..05bbdbe5 100644 --- a/src/components/share/modal/items/share-items.stories.tsx +++ b/src/components/share/modal/items/share-items.stories.tsx @@ -16,7 +16,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => { const roles = [ { label: "Admin", value: "admin" }, { label: "Editor", value: "editor" }, - { label: "Viewer", value: "viewer" }, + { label: "Reader", value: "reader" }, ]; export const InvitationItem = { diff --git a/src/components/share/modal/share-modal.scss b/src/components/share/modal/share-modal.scss index fbc3ce7d..4b14cedd 100644 --- a/src/components/share/modal/share-modal.scss +++ b/src/components/share/modal/share-modal.scss @@ -7,7 +7,7 @@ $md: map.get($themes, "default", "globals", "breakpoints", "md"); .c__share-modal__members { display: flex; flex-direction: column; - gap: var(--c--globals--spacings--xs); + gap: 4px; padding: var(--c--globals--spacings--xs) var(--c--globals--spacings--base); padding-top: 0; } @@ -25,11 +25,24 @@ $md: map.get($themes, "default", "globals", "breakpoints", "md"); } .c__share-modal__members-title, -.c__share-modal__invitations-title { - font-size: var(--c--globals--font--sizes--sm); - font-weight: 700; - margin-bottom: var(--c--globals--spacings--4xs); - color: var(--c--contextuals--content--semantic--neutral--primary); +.c__share-modal__invitations-title, +.c__share-modal__link-settings__title { + font-size: var(--c--globals--font--sizes--xs); + font-weight: 500; + color: var(--c--contextuals--content--semantic--neutral--tertiary); +} + +.c__share-modal__members-title { + display: flex; + align-items: center; + justify-content: space-between; +} + +.c__share-member-item { + .c__quick-search-item-template { + padding-left: 0; + padding-right: 0; + } } .c__share-modal__copy-link-footer { @@ -115,18 +128,11 @@ $md: map.get($themes, "default", "globals", "breakpoints", "md"); } .c__share-modal__link-settings { - padding: var(--c--globals--spacings--base); + padding: 12px 16px; gap: 8px; display: flex; flex-direction: column; - &__title { - font-size: var(--c--globals--font--sizes--sm); - font-weight: 700; - margin-bottom: var(--c--globals--spacings--4xs); - color: var(--c--contextuals--content--semantic--neutral--primary); - } - &__content { display: flex; gap: 8px; diff --git a/src/components/users/rows/index.scss b/src/components/users/rows/index.scss index b5d37d16..39c34227 100644 --- a/src/components/users/rows/index.scss +++ b/src/components/users/rows/index.scss @@ -3,10 +3,15 @@ align-items: center; gap: var(--c--globals--spacings--xs); + .c__avatar { + flex-shrink: 0; + } + &__name { font-size: 14px; font-weight: 500; line-height: 18px; + word-break: break-all; color: var(--c--contextuals--content--semantic--neutral--primary); } @@ -19,6 +24,7 @@ color: var(--c--contextuals--content--semantic--neutral--tertiary); font-size: 12px; line-height: 16px; + word-break: break-all; &.has-only-email { font-weight: 500; From b3cf57010adcbb53bbdef7ed1f9de54c16e9c2a0 Mon Sep 17 00:00:00 2001 From: Nathan Vasse Date: Thu, 23 Jul 2026 15:27:59 +0200 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9C=A8(front)=20add=20ShareImportModal?= =?UTF-8?q?=20for=20importing=20contacts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apps can now offer a CSV/XLSX contact import with a downloadable template. Parsing and validation (type, size, row count, columns) happen client side via read-excel-file so errors surface immediately in the uploader, before anything reaches the backend. The rows are handed to the app through onImport. Behaviour is covered by dedicated component tests. --- e2e/helpers/mount-share-import-modal.tsx | 58 ++++ e2e/share-import-modal/fixtures/contacts.xlsx | Bin 0 -> 2360 bytes .../share-import-modal.spec.tsx | 256 ++++++++++++++++++ package.json | 15 +- src/components/modal/index.scss | 6 + .../share/import-modal/ShareImportModal.tsx | 187 +++++++++++++ .../share/import-modal/assets/contacts.csv | 4 + .../share/import-modal/import-modal.scss | 5 + .../share-import-modal.stories.tsx | 154 +++++++++++ .../share/import-modal/utils.test.ts | 147 ++++++++++ src/components/share/import-modal/utils.ts | 127 +++++++++ src/components/share/index.ts | 1 + src/library.scss | 1 + src/locales/en-US.json | 18 +- src/locales/fr-FR.json | 17 ++ yarn.lock | 34 ++- 16 files changed, 1021 insertions(+), 9 deletions(-) create mode 100644 e2e/helpers/mount-share-import-modal.tsx create mode 100644 e2e/share-import-modal/fixtures/contacts.xlsx create mode 100644 e2e/share-import-modal/share-import-modal.spec.tsx create mode 100644 src/components/share/import-modal/ShareImportModal.tsx create mode 100644 src/components/share/import-modal/assets/contacts.csv create mode 100644 src/components/share/import-modal/import-modal.scss create mode 100644 src/components/share/import-modal/share-import-modal.stories.tsx create mode 100644 src/components/share/import-modal/utils.test.ts create mode 100644 src/components/share/import-modal/utils.ts diff --git a/e2e/helpers/mount-share-import-modal.tsx b/e2e/helpers/mount-share-import-modal.tsx new file mode 100644 index 00000000..73ed03b2 --- /dev/null +++ b/e2e/helpers/mount-share-import-modal.tsx @@ -0,0 +1,58 @@ +import { useEffect } from "react"; +import { CunninghamProvider } from "../../src/components/Provider/Provider"; +import { + ShareImportModal, + ShareImportRow, +} from "../../src/components/share/import-modal/ShareImportModal"; + +// Playwright CT bridges function props as one-way dispatchers, so tests +// describe scenarios with plain serializable data and this helper — which runs +// in the browser — builds the real callbacks locally. Callback invocations are +// recorded on `window.__shareImportCalls` so tests can assert them via +// `page.evaluate`. + +declare global { + interface Window { + __shareImportCalls: { name: string; rows?: ShareImportRow[] }[]; + } +} + +const useCallRecorder = () => { + useEffect(() => { + window.__shareImportCalls = []; + }, []); + return (name: string, rows?: ShareImportRow[]) => + window.__shareImportCalls.push({ name, rows }); +}; + +interface TestShareImportModalProps { + title?: string; + description?: string; + maxRows?: number; + /** Registers an onDownloadTemplate handler recorded as "download-template". */ + withDownloadTemplate?: boolean; +} + +export const TestShareImportModal = ({ + title, + description, + maxRows, + withDownloadTemplate, +}: TestShareImportModalProps) => { + const record = useCallRecorder(); + return ( + + record("close")} + title={title} + description={description} + maxRows={maxRows} + onDownloadTemplate={ + withDownloadTemplate ? () => record("download-template") : undefined + } + onImport={(rows) => record("import", rows)} + /> + + ); +}; diff --git a/e2e/share-import-modal/fixtures/contacts.xlsx b/e2e/share-import-modal/fixtures/contacts.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e892d9ba0a530919cd358f87d8829f358eda9ef9 GIT binary patch literal 2360 zcmWIWW@Zs#U|`^2kgop_bMcg?02dz97gTj=dN%cjC?_i?swY8%@X|I5#IF*$Pc z?nKMhP#GtKD92MfYQ)QnhBLurC2qJqulR zozMJ8+%nJgNCl%a$G0Op!+G|w z*|XoyrunjvMq4Xq^$*9^zX7G<(=MxjJ~?y#miC=fwtkH}E?Im*cD=_kA00~(>!&YD zMR@|#k4zH(A93B^uH9vW>XssZHveF$kFCeo_k?T{IH)R;y}md~bj_{%+_GjRBAc$( z?f!IZSj%tuZr^QNO{g{rI@$>TDIXrbTn$Um9wx*!1H}a+B zvhOxV;-Snc-wmc2-BnJrE?RKsNaxbLdr4d&S+n_OEI(orv*5wT?Ay^iPgt^UrT*|} z&6@jYO^_{X&$j1DyV*jk3Lee>wTc55i#nzeOvn+UpW^i2Ax_SA0 zi%<4B>zn`newOs9oHIwXoFm01?9&GQB@8*W=^vs_o-h9VMT^z7ckVX1g_DdY*j(E^ z=~2!@69$)qEK9uj4EyrE0<9Y^Z~3mQP+lcJr!S^$^&-QDcN(%qVYcE&yNhluNQrrG zI4R>VukwATYuy3Uzv#ZZwW(;rD!oAK6JK|4dnV)~6k4X&D0DJnx9Y}#>5KaWnapB3 zOzqDr+jIPkoVoeOvX%Dl{LLj?l!x5t!eA>E#2$u9?z|6G{3&%$HmY|(`T*w zKKoOP>Df6kD({qYTmR42URGzVu=(nJ{%gDCai=y;NPuB0ed@RVdcPGYtpf~ODP(2H z`4CnZaUC+?VR-O+-l8Y(HdP8tyc+9rBlH>b6usYx(b2PhZQj1ULO_&%o|DigRn1es zZ926p<KLTBFFxZ8+uk5{rvVoaQ{NE6`C&ig1!| zx~;hBU1r#vD0j88sMqPDNw2I+y!uxu$E4q6`@5Sle#HtY&V5-$0$(4!kiWC-?ShvH zUsKhD>)srg{Z;w9i{heF page.locator('input[type="file"]'); +const infoAlert = (page: Page) => page.locator(".c__alert--info"); +// Parse errors are displayed inline on the file, in the uploader dropzone. +const fileError = (page: Page) => + page.locator(".c__file-uploader__dropzone__error"); +const importButton = (page: Page) => + page.getByRole("button", { name: "Import", exact: true }); + +const uploadCsv = (page: Page, content: string, name = "contacts.csv") => + fileInput(page).setInputFiles({ + name, + mimeType: "text/csv", + buffer: Buffer.from(content), + }); + +const calls = (page: Page) => + page.evaluate(() => window.__shareImportCalls ?? []); + +test.describe("ShareImportModal", () => { + test("renders the default texts and actions", async ({ mount, page }) => { + await mount(); + + await expect(page.getByText("Import contacts")).toBeVisible(); + await expect( + page.getByText( + "Upload a CSV or XLS file with your contacts, or start from our template.", + ), + ).toBeVisible(); + await expect( + page.getByRole("button", { name: "Download template" }), + ).toBeVisible(); + await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible(); + await expect(importButton(page)).toBeDisabled(); + }); + + test("overrides the title and description when provided", async ({ + mount, + page, + }) => { + await mount( + , + ); + + await expect(page.getByText("Custom title")).toBeVisible(); + await expect(page.getByText("Custom description")).toBeVisible(); + }); + + test("parses a comma-separated CSV and imports its rows", async ({ + mount, + page, + }) => { + await mount(); + + await uploadCsv( + page, + "alice@example.com,admin\nbob@example.com,viewer", + ); + + await expect(infoAlert(page)).toContainText( + "2 rows ready to be imported.", + ); + await expect(importButton(page)).toBeEnabled(); + + await importButton(page).click(); + + expect(await calls(page)).toEqual([ + { + name: "import", + rows: [ + { email: "alice@example.com", role: "admin" }, + { email: "bob@example.com", role: "viewer" }, + ], + }, + ]); + }); + + test("hides the parse alerts once the import is requested", async ({ + mount, + page, + }) => { + await mount(); + + await uploadCsv(page, "alice@example.com,admin"); + await expect(infoAlert(page)).toBeVisible(); + + await importButton(page).click(); + + await expect(infoAlert(page)).toHaveCount(0); + await expect(fileError(page)).toHaveCount(0); + + // Selecting a new file resets the import attempt and its alerts. + await uploadCsv(page, "bob@example.com,viewer"); + await expect(infoAlert(page)).toContainText("1 row ready to be imported."); + }); + + test("parses a semicolon-separated CSV", async ({ mount, page }) => { + await mount(); + + await uploadCsv(page, "alice@example.com;admin"); + + await expect(infoAlert(page)).toContainText("1 row ready to be imported."); + }); + + test("parses an XLSX file", async ({ mount, page }) => { + await mount(); + + await fileInput(page).setInputFiles( + fileURLToPath(new URL("./fixtures/contacts.xlsx", import.meta.url)), + ); + + await expect(infoAlert(page)).toContainText( + "2 rows ready to be imported.", + ); + }); + + test("rejects a file with an invalid extension", async ({ mount, page }) => { + await mount(); + + await fileInput(page).setInputFiles({ + name: "contacts.txt", + mimeType: "text/plain", + buffer: Buffer.from("alice@example.com,admin"), + }); + + await expect(fileError(page)).toContainText( + "Only CSV or XLSX files are allowed.", + ); + await expect(importButton(page)).toBeDisabled(); + }); + + test("rejects a file with a malformed row and reports its number", async ({ + mount, + page, + }) => { + await mount(); + + await uploadCsv(page, "alice@example.com,admin\nbob@example.com"); + + await expect(fileError(page)).toContainText( + "Row 2 is invalid: two columns are expected (email, role).", + ); + await expect(importButton(page)).toBeDisabled(); + }); + + test("rejects an empty file", async ({ mount, page }) => { + await mount(); + + await uploadCsv(page, "\n\n"); + + await expect(fileError(page)).toContainText( + "The file contains no rows.", + ); + await expect(importButton(page)).toBeDisabled(); + }); + + test("rejects a file larger than 200 KB", async ({ mount, page }) => { + await mount(); + + await uploadCsv(page, "a".repeat(200 * 1024 + 1)); + + await expect(fileError(page)).toContainText( + "The file exceeds the maximum size of 200 KB.", + ); + await expect(importButton(page)).toBeDisabled(); + }); + + test("rejects a file with more rows than the default maximum of 100", async ({ + mount, + page, + }) => { + await mount(); + + const rows = Array.from( + { length: 101 }, + (_, index) => `user.${index}@example.com,admin`, + ); + await uploadCsv(page, rows.join("\n")); + + await expect(fileError(page)).toContainText( + "The file exceeds the maximum of 100 rows.", + ); + await expect(importButton(page)).toBeDisabled(); + }); + + test("honors a custom maxRows limit", async ({ mount, page }) => { + await mount(); + + await uploadCsv( + page, + "alice@example.com,admin\nbob@example.com,viewer", + ); + + await expect(fileError(page)).toContainText( + "The file exceeds the maximum of 1 rows.", + ); + await expect(importButton(page)).toBeDisabled(); + }); + + test("clears the alerts when the file is deleted", async ({ + mount, + page, + }) => { + await mount(); + + await uploadCsv(page, "alice@example.com,admin"); + await expect(infoAlert(page)).toBeVisible(); + + // The dropzone is itself a button whose name contains "Delete", so + // scope the locator to the inner control. + await page + .getByTestId("file-uploader-dropzone") + .getByRole("button", { name: "Delete" }) + .click(); + + await expect(infoAlert(page)).toHaveCount(0); + await expect(fileError(page)).toHaveCount(0); + await expect(importButton(page)).toBeDisabled(); + }); + + test("invokes onClose when cancel is pressed", async ({ mount, page }) => { + await mount(); + + await page.getByRole("button", { name: "Cancel" }).click(); + + expect(await calls(page)).toEqual([{ name: "close" }]); + }); + + test("invokes onDownloadTemplate when provided", async ({ mount, page }) => { + await mount(); + + await page.getByRole("button", { name: "Download template" }).click(); + + expect(await calls(page)).toEqual([{ name: "download-template" }]); + }); + + test("downloads the bundled template by default", async ({ + mount, + page, + }) => { + await mount(); + + const downloadPromise = page.waitForEvent("download"); + await page.getByRole("button", { name: "Download template" }).click(); + const download = await downloadPromise; + + expect(download.suggestedFilename()).toBe("contacts.csv"); + }); +}); diff --git a/package.json b/package.json index e58294fa..89866369 100644 --- a/package.json +++ b/package.json @@ -59,15 +59,16 @@ "cmdk": "1.0.4", "react-arborist": "3.4.3", "react-aria-components": "1.16.0", + "react-pdf": "10.1.0", "react-resizable-panels": "2.1.7", "react-stately": "3.37.0", "react-virtualized": "9.22.6", - "react-pdf": "10.1.0" + "read-excel-file": "9.3.1" }, "peerDependencies": { + "@tanstack/react-query": "^5", "react": "^19.1.2", - "react-dom": "^19.1.2", - "@tanstack/react-query": "^5" + "react-dom": "^19.1.2" }, "devDependencies": { "@babel/core": "7.26.0", @@ -76,6 +77,8 @@ "@babel/preset-typescript": "7.26.0", "@chromatic-com/storybook": "3.2.4", "@eslint/js": "9.17.0", + "@playwright/experimental-ct-react": "1.52.0", + "@playwright/test": "1.52.0", "@storybook/addon-a11y": "8.5.2", "@storybook/addon-essentials": "8.5.2", "@storybook/addon-interactions": "8.5.2", @@ -104,6 +107,7 @@ "glob": "11.0.0", "globals": "15.14.0", "jsdom": "25.0.1", + "pdf-lib": "1.17.1", "react": "19.1.2", "react-dom": "19.1.2", "react-hook-form": "7.54.2", @@ -122,10 +126,7 @@ "vite-tsconfig-paths": "5.1.4", "vitest": "2.1.8", "vitest-fetch-mock": "0.4.3", - "yup": "1.6.1", - "@playwright/experimental-ct-react": "1.52.0", - "@playwright/test": "1.52.0", - "pdf-lib": "1.17.1" + "yup": "1.6.1" }, "eslintConfig": { "extends": [ diff --git a/src/components/modal/index.scss b/src/components/modal/index.scss index 13030cfe..de292995 100644 --- a/src/components/modal/index.scss +++ b/src/components/modal/index.scss @@ -9,6 +9,12 @@ margin-bottom: var(--c--globals--spacings--2xs); } +.c__modal__content__text--light { + color: var(--c--contextuals--content--semantic--neutral--secondary); + font-size: 14px; + font-weight: 400; +} + .c__modal__close .c__button--tertiary-text:hover, .c__modal__close .c__button--tertiary-text:focus-visible { box-shadow: none; diff --git a/src/components/share/import-modal/ShareImportModal.tsx b/src/components/share/import-modal/ShareImportModal.tsx new file mode 100644 index 00000000..a07a03cd --- /dev/null +++ b/src/components/share/import-modal/ShareImportModal.tsx @@ -0,0 +1,187 @@ +import { FileUploader, UploadFile } from ":/components/form/file-uploader"; +import { Spinner } from ":/components/loader/Spinner"; +import { Alert } from ":/components/alert/Alert"; +import { + Button, + Modal, + ModalProps, + ModalSize, + useCunningham, + VariantType, +} from "@gouvfr-lasuite/cunningham-react"; +import { PropsWithChildren, useRef, useState } from "react"; +import { + ACCEPTED_FILE_EXTENSIONS, + parseShareImportFile, + ShareImportParseError, + ShareImportRow, +} from "./utils"; +import contactsTemplateUrl from "./assets/contacts.csv?url"; + +const downloadTemplate = () => { + const link = document.createElement("a"); + link.href = contactsTemplateUrl; + link.download = "contacts.csv"; + link.click(); +}; + +export type { ShareImportRow } from "./utils"; + +type ShareImportModalProps = Pick & + PropsWithChildren & { + title?: string; + description?: string; + maxRows?: number; + onDownloadTemplate?: () => void; + onImport?: (rows: ShareImportRow[]) => void; + isImporting?: boolean; + onFileChange?: (file?: File) => void; + }; + +export const ShareImportModal = (props: ShareImportModalProps) => { + const { t } = useCunningham(); + const [file, setFile] = useState(); + const [rows, setRows] = useState(); + const [parseError, setParseError] = useState(); + const [isInvalidFile, setIsInvalidFile] = useState(false); + const [isParsing, setIsParsing] = useState(false); + // Once the import has been requested, the parse alerts are stale: the + // remaining feedback (spinner, consumer error) comes from the import itself. + const [importRequested, setImportRequested] = useState(false); + // Guards against a slow parse of a previous file overwriting the + // state of the file selected after it. + const parseRequestId = useRef(0); + + const resetFile = () => { + parseRequestId.current++; + props.onFileChange?.(undefined); + setFile(undefined); + setRows(undefined); + setParseError(undefined); + setIsInvalidFile(false); + setIsParsing(false); + setImportRequested(false); + }; + + const parseErrorMessage = parseError + ? t( + `components.share.import.errors.${parseError.type}`, + parseError.type === "invalid_row" + ? { row: parseError.row } + : parseError.type === "too_many_rows" + ? { max: parseError.max } + : undefined, + ) + : undefined; + const fileErrorMessage = isInvalidFile + ? t("components.share.import.invalid_file_type") + : parseErrorMessage; + + // The uploader is controlled: the file status reflects the parse lifecycle. + const uploaderFiles: UploadFile[] = file + ? [ + { + ...file, + status: isParsing ? "uploading" : fileErrorMessage ? "error" : "done", + error: fileErrorMessage, + }, + ] + : []; + + return ( + + {t("components.share.import.download_template")} + + } + rightActions={ + <> + + + + } + > +
+
+ {props.description ?? t("components.share.import.description")} +
+ { + const selected = files[0]; + props.onFileChange?.(selected?.originalFile); + const requestId = ++parseRequestId.current; + setFile(selected); + setRows(undefined); + setParseError(undefined); + setIsParsing(false); + setImportRequested(false); + const isAccepted = + !!selected && + ACCEPTED_FILE_EXTENSIONS.some((extension) => + selected.originalFile.name.toLowerCase().endsWith(extension), + ); + setIsInvalidFile(!!selected && !isAccepted); + if (!selected || !isAccepted) { + return; + } + setIsParsing(true); + const result = await parseShareImportFile(selected.originalFile, { + maxRows: props.maxRows, + }); + if (requestId !== parseRequestId.current) { + return; + } + setIsParsing(false); + setRows(result.rows); + setParseError(result.error); + }} + onRemoveFile={resetFile} + onCancelFile={resetFile} + /> + {rows && !importRequested && ( + + {t( + rows.length === 1 + ? "components.share.import.parsed_rows_singular" + : "components.share.import.parsed_rows_plural", + { count: rows.length }, + )} + + )} + {props.children} +
+
+ ); +}; diff --git a/src/components/share/import-modal/assets/contacts.csv b/src/components/share/import-modal/assets/contacts.csv new file mode 100644 index 00000000..7d1732be --- /dev/null +++ b/src/components/share/import-modal/assets/contacts.csv @@ -0,0 +1,4 @@ +amandine.salambo@lasuite.fr;reader +desirae.dokidis@lasuite.fr;editor +alfredo.levin@lasuite.fr;admin +charlie.saris@lasuite.fr;owner \ No newline at end of file diff --git a/src/components/share/import-modal/import-modal.scss b/src/components/share/import-modal/import-modal.scss new file mode 100644 index 00000000..b0cc20bf --- /dev/null +++ b/src/components/share/import-modal/import-modal.scss @@ -0,0 +1,5 @@ +.c__share__import__modal { + display: flex; + flex-direction: column; + gap: 16px; +} \ No newline at end of file diff --git a/src/components/share/import-modal/share-import-modal.stories.tsx b/src/components/share/import-modal/share-import-modal.stories.tsx new file mode 100644 index 00000000..a832ee9d --- /dev/null +++ b/src/components/share/import-modal/share-import-modal.stories.tsx @@ -0,0 +1,154 @@ +import { useState } from "react"; +import type { Meta } from "@storybook/react"; +import { + Description, + Title, + Subtitle, + Primary, + ArgTypes, +} from "@storybook/blocks"; +import { Button } from "@gouvfr-lasuite/cunningham-react"; +import { ShareImportModal } from "./ShareImportModal"; + +/** + * The `ShareImportModal` component displays a modal to import contacts from a CSV or XLSX file. + * The file is parsed by the component itself: `onImport` receives the parsed rows, not the file. + * + * ## Installation + * + * ```tsx + * import { ShareImportModal } from "@gouvfr-lasuite/ui-kit"; + * ``` + * + * ## Basic Usage + * + * ```tsx + * setIsOpen(false)} + * onImport={(rows) => inviteContacts(rows)} + * /> + * ``` + * + * ## Expected File Format + * + * The file must be a `.csv` or `.xlsx` file of **two columns without a header row**: + * the first column is the email, the second one is the role. + * + * ```csv + * amandine.salambo@lasuite.fr;reader + * desirae.dokidis@lasuite.fr;editor + * ``` + * + * - CSV delimiters `;` and `,` are both supported (auto-detected). + * - The maximum file size is 200 KB. + * - At most 100 rows can be imported by default (configurable with `maxRows`). + * - A template file is provided: the "Download template" button downloads it + * (override this behavior with `onDownloadTemplate`). + * + * ## Data Types + * + * ### ShareImportRow + * | Property | Type | Description | + * |----------|------|-------------| + * | `email` | `string` | First column of the file | + * | `role` | `string` | Second column of the file | + * + * ## Error Handling + * + * Parsing errors are displayed in an `Alert` below the file uploader and keep + * the Import button disabled: + * + * | Error | Cause | + * |-------|-------| + * | File too large | The file exceeds 200 KB | + * | Unreadable | The file could not be read or is not a valid CSV/XLSX | + * | Empty | The file contains no rows | + * | Invalid row | A row doesn't have exactly two non-empty columns (the row number is reported) | + * | Too many rows | The file has more rows than `maxRows` (100 by default) | + * + * On success, an info `Alert` tells how many rows are ready to be imported. + */ +const meta: Meta = { + title: "Components/Share/ImportModal", + component: ShareImportModal, + tags: ["autodocs"], + parameters: { + docs: { + story: { + inline: false, + iframeHeight: 700, + }, + page: () => ( + <> + + <Subtitle /> + <Description /> + <Primary /> + <ArgTypes /> + </> + ), + }, + }, + argTypes: { + isOpen: { + description: "Controls the modal open state", + control: "boolean", + }, + onClose: { + description: "Callback called on close", + control: false, + }, + title: { + description: "Overrides the default modal title", + control: "text", + }, + description: { + description: "Overrides the default modal description", + control: "text", + }, + maxRows: { + description: "Maximum number of rows that can be imported", + control: "number", + table: { + defaultValue: { summary: "100" }, + }, + }, + onDownloadTemplate: { + description: + "Overrides the default behavior of the download template button (downloading the bundled CSV template)", + control: false, + }, + onImport: { + description: + "Callback called with the parsed rows when the import button is pressed", + control: false, + }, + }, +}; + +export default meta; + +const ShareImportModalExample = () => { + const [isOpen, setIsOpen] = useState(true); + return ( + <> + <Button onClick={() => setIsOpen(true)}>Open modal</Button> + <ShareImportModal + isOpen={isOpen} + onClose={() => setIsOpen(false)} + onImport={(rows) => { + window.alert( + `Imported ${rows.length} rows:\n\n${rows + .map((row) => `${row.email};${row.role}`) + .join("\n")}`, + ); + }} + /> + </> + ); +}; + +export const Default = { + render: () => <ShareImportModalExample />, +}; diff --git a/src/components/share/import-modal/utils.test.ts b/src/components/share/import-modal/utils.test.ts new file mode 100644 index 00000000..810ab214 --- /dev/null +++ b/src/components/share/import-modal/utils.test.ts @@ -0,0 +1,147 @@ +import { describe, it, expect } from "vitest"; +import { + parseShareImportFile, + DEFAULT_MAX_IMPORT_ROWS, + MAX_IMPORT_FILE_SIZE, +} from "./utils"; + +const csvFile = (content: string, name = "contacts.csv") => + new File([content], name); + +describe("parseShareImportFile", () => { + it("parses a comma-separated CSV", async () => { + const result = await parseShareImportFile( + csvFile("alice@example.com,admin\nbob@example.com,viewer"), + ); + expect(result.rows).toEqual([ + { email: "alice@example.com", role: "admin" }, + { email: "bob@example.com", role: "viewer" }, + ]); + }); + + it("parses a semicolon-separated CSV", async () => { + const result = await parseShareImportFile( + csvFile("alice@example.com;admin\nbob@example.com;viewer"), + ); + expect(result.rows).toEqual([ + { email: "alice@example.com", role: "admin" }, + { email: "bob@example.com", role: "viewer" }, + ]); + }); + + it("handles CRLF line endings and trailing blank lines", async () => { + const result = await parseShareImportFile( + csvFile("alice@example.com,admin\r\nbob@example.com,viewer\r\n\r\n"), + ); + expect(result.rows).toHaveLength(2); + }); + + it("strips a leading BOM", async () => { + const result = await parseShareImportFile( + csvFile("alice@example.com,admin"), + ); + expect(result.rows).toEqual([ + { email: "alice@example.com", role: "admin" }, + ]); + }); + + it("handles quoted fields containing the delimiter and escaped quotes", async () => { + const result = await parseShareImportFile( + csvFile('"alice@example.com","role, with ""comma"""'), + ); + expect(result.rows).toEqual([ + { email: "alice@example.com", role: 'role, with "comma"' }, + ]); + }); + + it("trims whitespace around fields", async () => { + const result = await parseShareImportFile( + csvFile(" alice@example.com , admin "), + ); + expect(result.rows).toEqual([ + { email: "alice@example.com", role: "admin" }, + ]); + }); + + it("fails on a row with a single column", async () => { + const result = await parseShareImportFile( + csvFile("alice@example.com,admin\nbob@example.com"), + ); + expect(result.error).toEqual({ type: "invalid_row", row: 2 }); + }); + + it("fails on a row with three columns", async () => { + const result = await parseShareImportFile( + csvFile("alice@example.com,admin,extra"), + ); + expect(result.error).toEqual({ type: "invalid_row", row: 1 }); + }); + + it("fails on a row with an empty email or role", async () => { + expect( + (await parseShareImportFile(csvFile(",admin"))).error, + ).toEqual({ type: "invalid_row", row: 1 }); + expect( + (await parseShareImportFile(csvFile("alice@example.com,"))).error, + ).toEqual({ type: "invalid_row", row: 1 }); + }); + + it("reports the 1-based row number of the original file, counting blank lines", async () => { + const result = await parseShareImportFile( + csvFile("alice@example.com,admin\n\nbob@example.com"), + ); + expect(result.error).toEqual({ type: "invalid_row", row: 3 }); + }); + + it("fails on an empty file", async () => { + const result = await parseShareImportFile(csvFile("\n\n")); + expect(result.error).toEqual({ type: "empty" }); + }); + + it("fails on a file larger than 10 MB", async () => { + const result = await parseShareImportFile( + csvFile("a".repeat(MAX_IMPORT_FILE_SIZE + 1)), + ); + expect(result.error).toEqual({ type: "file_too_large" }); + }); + + it("fails on a file with more rows than the default maximum", async () => { + const content = Array.from( + { length: DEFAULT_MAX_IMPORT_ROWS + 1 }, + (_, index) => `user.${index}@example.com,admin`, + ).join("\n"); + const result = await parseShareImportFile(csvFile(content)); + expect(result.error).toEqual({ + type: "too_many_rows", + max: DEFAULT_MAX_IMPORT_ROWS, + }); + }); + + it("accepts a file with exactly the maximum number of rows", async () => { + const content = Array.from( + { length: DEFAULT_MAX_IMPORT_ROWS }, + (_, index) => `user.${index}@example.com,admin`, + ).join("\n"); + const result = await parseShareImportFile(csvFile(content)); + expect(result.rows).toHaveLength(DEFAULT_MAX_IMPORT_ROWS); + }); + + it("honors a custom maxRows option", async () => { + const content = "alice@example.com,admin\nbob@example.com,viewer"; + const result = await parseShareImportFile(csvFile(content), { + maxRows: 1, + }); + expect(result.error).toEqual({ type: "too_many_rows", max: 1 }); + const withinLimit = await parseShareImportFile(csvFile(content), { + maxRows: 2, + }); + expect(withinLimit.rows).toHaveLength(2); + }); + + it("fails on an unreadable xlsx file", async () => { + const result = await parseShareImportFile( + new File(["not an xlsx"], "contacts.xlsx"), + ); + expect(result.error).toEqual({ type: "unreadable" }); + }); +}); diff --git a/src/components/share/import-modal/utils.ts b/src/components/share/import-modal/utils.ts new file mode 100644 index 00000000..75575655 --- /dev/null +++ b/src/components/share/import-modal/utils.ts @@ -0,0 +1,127 @@ +import { readSheet } from "read-excel-file/universal"; + +export const ACCEPTED_FILE_EXTENSIONS = [".csv", ".xlsx"]; +export const MAX_IMPORT_FILE_SIZE = 200 * 1024; // 200 KB +export const DEFAULT_MAX_IMPORT_ROWS = 100; + +export type ShareImportRow = { + email: string; + role: string; +}; + +export type ShareImportParseError = + | { type: "file_too_large" } + | { type: "unreadable" } + | { type: "empty" } + | { type: "invalid_row"; row: number } + | { type: "too_many_rows"; max: number }; + +export type ShareImportParseResult = + | { rows: ShareImportRow[]; error?: never } + | { rows?: never; error: ShareImportParseError }; + +/** + * Parses a single CSV line. Quoted fields containing the delimiter are + * supported, `""` inside quotes unescapes to `"`. Fields spanning several + * lines are not supported (emails and roles never contain newlines). + */ +const parseCsvLine = (line: string, delimiter: string): string[] => { + const fields: string[] = []; + let current = ""; + let inQuotes = false; + for (let i = 0; i < line.length; i++) { + const char = line[i]; + if (inQuotes) { + if (char === '"') { + if (line[i + 1] === '"') { + current += '"'; + i++; + } else { + inQuotes = false; + } + } else { + current += char; + } + } else if (char === '"') { + inQuotes = true; + } else if (char === delimiter) { + fields.push(current); + current = ""; + } else { + current += char; + } + } + fields.push(current); + return fields; +}; + +/** + * Validates raw rows: each non-blank row must contain exactly two non-empty + * fields (email, role). Blank rows are skipped but still counted so that + * `invalid_row` reports the 1-based row number of the original file. + */ +const toRows = (rawRows: string[][]): ShareImportParseResult => { + const rows: ShareImportRow[] = []; + for (let index = 0; index < rawRows.length; index++) { + const fields = rawRows[index].map((field) => field.trim()); + while (fields.length > 0 && fields[fields.length - 1] === "") { + fields.pop(); + } + if (fields.length === 0) { + continue; + } + if (fields.length !== 2 || fields[0] === "" || fields[1] === "") { + return { error: { type: "invalid_row", row: index + 1 } }; + } + rows.push({ email: fields[0], role: fields[1] }); + } + if (rows.length === 0) { + return { error: { type: "empty" } }; + } + return { rows }; +}; + +const parseCsv = async (file: File): Promise<ShareImportParseResult> => { + let text: string; + try { + text = await file.text(); + } catch { + return { error: { type: "unreadable" } }; + } + if (text.charCodeAt(0) === 0xfeff) { + text = text.slice(1); + } + const lines = text.split(/\r\n|\r|\n/); + // French Excel/Calc exports CSV with ";" as delimiter. + const firstLine = lines.find((line) => line.trim() !== ""); + const delimiter = firstLine?.includes(";") ? ";" : ","; + return toRows(lines.map((line) => parseCsvLine(line, delimiter))); +}; + +const parseXlsx = async (file: File): Promise<ShareImportParseResult> => { + try { + const cells = await readSheet(file); + return toRows( + cells.map((row) => row.map((cell) => (cell === null ? "" : String(cell)))), + ); + } catch { + return { error: { type: "unreadable" } }; + } +}; + +export const parseShareImportFile = async ( + file: File, + options?: { maxRows?: number }, +): Promise<ShareImportParseResult> => { + if (file.size > MAX_IMPORT_FILE_SIZE) { + return { error: { type: "file_too_large" } }; + } + const result = file.name.toLowerCase().endsWith(".xlsx") + ? await parseXlsx(file) + : await parseCsv(file); + const maxRows = options?.maxRows ?? DEFAULT_MAX_IMPORT_ROWS; + if (result.rows && result.rows.length > maxRows) { + return { error: { type: "too_many_rows", max: maxRows } }; + } + return result; +}; diff --git a/src/components/share/index.ts b/src/components/share/index.ts index ea8b6c25..50d64088 100644 --- a/src/components/share/index.ts +++ b/src/components/share/index.ts @@ -1,5 +1,6 @@ export * from "./users-invitation/InvitationUserSelectorList"; export * from "./access/AccessRoleDropdown"; +export * from "./import-modal/ShareImportModal"; export * from "./modal"; export * from "./types"; export * from "./utils"; \ No newline at end of file diff --git a/src/library.scss b/src/library.scss index 2e635255..3926d926 100644 --- a/src/library.scss +++ b/src/library.scss @@ -33,6 +33,7 @@ @use "./components/tooltip"; @use "./components/share/users-invitation"; @use "./components/share/access"; +@use "./components/share/import-modal/import-modal"; @use "./components/users/avatar"; @use "./components/users/rows"; @use "./components/users/menu"; diff --git a/src/locales/en-US.json b/src/locales/en-US.json index b638b951..77764a9e 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -13,7 +13,23 @@ "shareButton": "Share", "modalTitle": "Share folder", "modalAriaLabel": "Share modal", - + "import": { + "title": "Import contacts", + "description": "Upload a CSV or XLS file with your contacts, or start from our template.", + "download_template": "Download template", + "cancel": "Cancel", + "import": "Import", + "invalid_file_type": "Only CSV or XLSX files are allowed.", + "parsed_rows_singular": "{count} row ready to be imported.", + "parsed_rows_plural": "{count} rows ready to be imported.", + "errors": { + "file_too_large": "The file exceeds the maximum size of 200 KB.", + "unreadable": "The file could not be read. Please check that it is a valid CSV or XLSX file.", + "empty": "The file contains no rows.", + "invalid_row": "Row {row} is invalid: two columns are expected (email, role).", + "too_many_rows": "The file exceeds the maximum of {max} rows." + } + }, "access": { "delete": "Remove access" }, diff --git a/src/locales/fr-FR.json b/src/locales/fr-FR.json index 4d51fdd9..ebbb5356 100644 --- a/src/locales/fr-FR.json +++ b/src/locales/fr-FR.json @@ -13,6 +13,23 @@ "modalTitle": "Partage", "modalAriaLabel": "Modale de partage", "shareButton": "Partager", + "import": { + "title": "Importer des contacts", + "description": "Téléversez un fichier CSV ou XLS avec vos contacts, ou partez de notre modèle.", + "download_template": "Télécharger le modèle", + "cancel": "Annuler", + "import": "Importer", + "invalid_file_type": "Seuls les fichiers CSV ou XLSX sont acceptés.", + "parsed_rows_singular": "{count} ligne prête à être importée.", + "parsed_rows_plural": "{count} lignes prêtes à être importées.", + "errors": { + "file_too_large": "Le fichier dépasse la taille maximale de 200 Ko.", + "unreadable": "Le fichier n'a pas pu être lu. Vérifiez qu'il s'agit d'un fichier CSV ou XLSX valide.", + "empty": "Le fichier ne contient aucune ligne.", + "invalid_row": "La ligne {row} est invalide : deux colonnes sont attendues (email, rôle).", + "too_many_rows": "Le fichier dépasse le maximum de {max} lignes." + } + }, "access": { "delete": "Retirer l'accès" }, diff --git a/yarn.lock b/yarn.lock index 3d3f8bb2..6e815c47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6012,6 +6012,11 @@ fflate@^0.8.2: resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== +fflate@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.3.tgz#bc27d8eb30343d4d512abb03480202ce65d825fc" + integrity sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA== + figlet@1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.8.1.tgz#e8e8a07e8c16be24c31086d7d5de8a9b9cf7f0fd" @@ -6252,7 +6257,7 @@ gopd@^1.0.1, gopd@^1.2.0: resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -7343,6 +7348,11 @@ node-addon-api@^7.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + node-releases@^2.0.21: version "2.0.21" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.21.tgz#f59b018bc0048044be2d4c4c04e4c8b18160894c" @@ -8066,6 +8076,15 @@ react@19.1.2: dependencies: loose-envify "^1.1.0" +read-excel-file@9.3.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/read-excel-file/-/read-excel-file-9.3.1.tgz#8bbd360bb297106e432e5798f74c1bc66dcbefe0" + integrity sha512-yzC1vJ/yl3PGJfCDrOI6/rBagF0bRm/CK1NTNXYdomB+13mDB9SFyoRibsDXxDAFrwCANfuRqzuUWDljkkSEuQ== + dependencies: + fflate "^0.8.3" + saxen "^11.0.2" + unzipper-esm "^0.13.2" + readdirp@^4.0.1: version "4.1.2" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" @@ -8300,6 +8319,11 @@ sax@^1.5.0: resolved "https://registry.yarnpkg.com/sax/-/sax-1.6.0.tgz#da59637629307b97e7c4cb28e080a7bc38560d5b" integrity sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA== +saxen@^11.0.2: + version "11.1.0" + resolved "https://registry.yarnpkg.com/saxen/-/saxen-11.1.0.tgz#caa873c9bcf92c4662320aca61b0b2d3595306f2" + integrity sha512-GOxBOAmiWVAytOHBuMlgFMZ4MAk+2Ny5QJpzM9I4IozfPLXq3FsDdIHNviTQGZGIx7G2mBkA+uySiJlYmSU1Gg== + saxes@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" @@ -8875,6 +8899,14 @@ unplugin@^1.3.1: acorn "^8.14.0" webpack-virtual-modules "^0.6.2" +unzipper-esm@^0.13.2: + version "0.13.2" + resolved "https://registry.yarnpkg.com/unzipper-esm/-/unzipper-esm-0.13.2.tgz#b803a9ba11d74ea41728fe4d2936cba4ec7b3961" + integrity sha512-lt8GtgDYV8YcAFZNQuLyR2QvHI8C/TstpgsdjUn9ZxiWLJgn+e5uW6DsO3e/HUJVuWD57ZLLFMZ9xk26tePuHQ== + dependencies: + graceful-fs "^4.2.2" + node-int64 "^0.4.0" + update-browserslist-db@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" From 7a3c42f6ab0b5ddf576a84a7f635c8f9b8adde8b Mon Sep 17 00:00:00 2001 From: Nathan Vasse <nathan.vasse@gmail.com> Date: Thu, 23 Jul 2026 15:28:04 +0200 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9C=A8(front)=20hook=20contact=20import?= =?UTF-8?q?=20into=20ShareModal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With allowFileImport set, an Import contacts menu in the members title opens ShareImportModal, so apps only have to wire onImportContacts. Returning false keeps the modal open for the app to surface a server error through importModalChildren. The stories showcase the flow and its failure path, and the component docs move to a proper MDX page. --- .../share-modal-import.spec.tsx | 60 +++ src/components/share/modal/ShareModal.tsx | 418 +++++++++++------- .../share/modal/stories/ShareModal.mdx | 243 ++++++++++ .../share/modal/stories/ShareModalExample.tsx | 82 +++- .../modal/stories/share-modal.stories.tsx | 195 ++++---- 5 files changed, 712 insertions(+), 286 deletions(-) create mode 100644 e2e/share-import-modal/share-modal-import.spec.tsx create mode 100644 src/components/share/modal/stories/ShareModal.mdx diff --git a/e2e/share-import-modal/share-modal-import.spec.tsx b/e2e/share-import-modal/share-modal-import.spec.tsx new file mode 100644 index 00000000..af703464 --- /dev/null +++ b/e2e/share-import-modal/share-modal-import.spec.tsx @@ -0,0 +1,60 @@ +import { test, expect } from "@playwright/experimental-ct-react"; +import { CunninghamProvider } from "../../src/components/Provider/Provider"; +import { ShareModalExample } from "../../src/components/share/modal/stories/ShareModalExample"; + +test.describe("ShareModal import menu", () => { + test("opens the import modal from the ... menu and hides it when not allowed", async ({ + mount, + page, + }) => { + await mount( + <CunninghamProvider currentLocale="en-US"> + <ShareModalExample allowFileImport={true} /> + </CunninghamProvider>, + ); + + const kebab = page.getByRole("button", { name: "Import contacts" }); + await expect(kebab).toBeVisible(); + await kebab.click(); + + await page.getByRole("menuitem", { name: "Import contacts" }).click(); + + await expect( + page.getByText( + "Upload a CSV or XLS file with your contacts, or start from our template.", + ), + ).toBeVisible(); + }); + + test("shows no ... button without allowFileImport or without canUpdate", async ({ + mount, + page, + }) => { + await mount( + <CunninghamProvider currentLocale="en-US"> + <ShareModalExample /> + </CunninghamProvider>, + ); + + await expect(page.getByTestId("members-list")).toBeVisible(); + await expect( + page.getByRole("button", { name: "Import contacts" }), + ).toHaveCount(0); + }); + + test("shows no ... button when canUpdate is false", async ({ + mount, + page, + }) => { + await mount( + <CunninghamProvider currentLocale="en-US"> + <ShareModalExample allowFileImport={true} canUpdate={false} /> + </CunninghamProvider>, + ); + + await expect(page.getByTestId("members-list")).toBeVisible(); + await expect( + page.getByRole("button", { name: "Import contacts" }), + ).toHaveCount(0); + }); +}); diff --git a/src/components/share/modal/ShareModal.tsx b/src/components/share/modal/ShareModal.tsx index 2ab417e4..348295c6 100644 --- a/src/components/share/modal/ShareModal.tsx +++ b/src/components/share/modal/ShareModal.tsx @@ -1,8 +1,14 @@ import { + DropdownMenu, DropdownMenuOption, DropdownMenuProps, + useDropdownMenu, } from ":/components/dropdown-menu"; import { ShareSearchField } from "./ShareSearchField"; +import { + ShareImportModal, + ShareImportRow, +} from ":/components/share/import-modal/ShareImportModal"; import { useState, useRef, @@ -33,6 +39,8 @@ import { ShareInvitationItem } from "./items/ShareInvitationItem"; import { useResponsive } from ":/hooks/useResponsive"; import { ShareLinkSettings } from "./items/ShareLinkSettings"; import { CustomTranslations } from ":/hooks/useCustomTranslations"; +import { More } from ":/icons"; +import { IconSize } from ":/components/icon"; enum ViewMode { CANNOT_VIEW = "cannot_view", @@ -45,15 +53,15 @@ type ShareModalInvitationProps<UserType, InvitationType> = { invitations?: InvitationData<UserType, InvitationType>[]; onUpdateInvitation?: ( invitation: InvitationData<UserType, InvitationType>, - role: string + role: string, ) => void; onDeleteInvitation?: ( - invitation: InvitationData<UserType, InvitationType> + invitation: InvitationData<UserType, InvitationType>, ) => void; hasNextInvitations?: boolean; onLoadNextInvitations?: () => void; invitationRoleTopMessage?: ( - invitation: InvitationData<UserType, InvitationType> + invitation: InvitationData<UserType, InvitationType>, ) => string; }; @@ -66,10 +74,10 @@ type ShareModalAccessProps<UserType, AccessType> = { onDeleteAccess?: (access: AccessData<UserType, AccessType>) => void; onUpdateAccess?: ( access: AccessData<UserType, AccessType>, - role: string + role: string, ) => void; accessRoleTopMessage?: ( - access: AccessData<UserType, AccessType> + access: AccessData<UserType, AccessType>, ) => string | ReactNode | undefined; }; @@ -110,11 +118,16 @@ export type ShareModalProps<UserType, InvitationType, AccessType> = { onClose: () => void; invitationRoles?: DropdownMenuOption[]; getAccessRoles?: ( - access: AccessData<UserType, AccessType> + access: AccessData<UserType, AccessType>, ) => DropdownMenuOption[]; outsideSearchContent?: ReactNode; hideInvitations?: boolean; hideMembers?: boolean; + allowFileImport?: boolean; + maxImportRows?: number; + onImportContacts?: (rows: ShareImportRow[]) => Promise<boolean> | boolean; + onImportFileChange?: (file?: File) => void; + importModalChildren?: ReactNode; customTranslations?: CustomTranslations; } & ShareModalInvitationProps<UserType, InvitationType> & ShareModalAccessProps<UserType, AccessType> & @@ -133,6 +146,7 @@ export const ShareModal = <UserType, InvitationType, AccessType>({ hasNextInvitations = false, hideInvitations = false, hideMembers = false, + allowFileImport = false, cannotViewChildren, customTranslations, ...props @@ -166,8 +180,12 @@ export const ShareModal = <UserType, InvitationType, AccessType>({ UserData<UserType>[] >([]); const [selectedInvitationRole, setSelectedInvitationRole] = useState<string>( - props.invitationRoles?.[0]?.value ?? "" + props.invitationRoles?.[0]?.value ?? "", ); + const importMenu = useDropdownMenu(); + const [isImportModalOpen, setIsImportModalOpen] = useState(false); + const [isImporting, setIsImporting] = useState(false); + const showFileImport = allowFileImport && canUpdate; /** * The height of the modal content @@ -209,7 +227,7 @@ export const ShareModal = <UserType, InvitationType, AccessType>({ setSearchQuery(""); props.onSearchUsers!(""); }, - [props] + [props], ); const onRemoveUser = (user: UserData<UserType>) => { @@ -218,7 +236,7 @@ export const ShareModal = <UserType, InvitationType, AccessType>({ const usersData: QuickSearchData<UserData<UserType>> = useMemo(() => { const searchMemberResult = searchUsersResult?.filter( - (user) => !pendingInvitationUsers.includes(user) + (user) => !pendingInvitationUsers.includes(user), ); let emptyString: string | undefined = searchQuery !== "" @@ -227,7 +245,7 @@ export const ShareModal = <UserType, InvitationType, AccessType>({ const isValidEmail = (email: string) => { return !!email.match( - /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z\-0-9]{2,}))$/ + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z\-0-9]{2,}))$/, ); }; @@ -307,178 +325,238 @@ export const ShareModal = <UserType, InvitationType, AccessType>({ const viewMode = getViewMode(); return ( - <Modal - title={props.modalTitle ?? t("components.share.modalTitle")} - isOpen={props.isOpen} - onClose={props.onClose} - aria-label={t("components.share.modalAriaLabel")} - closeOnClickOutside - size={isMobile ? ModalSize.FULL : ModalSize.LARGE} - > - <div className="c__share-modal no-padding"> - {viewMode === ViewMode.CANNOT_VIEW && ( - <div - className="c__share-modal__cannot-view" - style={{ - height: listHeight, - }} - > - <div className="c__share-modal__cannot-view__content"> - <p> - {props.cannotViewMessage ?? - t("components.share.cannot_view.message")} - </p> - </div> - {cannotViewChildren} - </div> - )} - - {viewMode === ViewMode.SEARCH && ( - <QuickSearch - onFilter={onInputChange} - inputValue={inputValue} - showInput={canUpdate} - loading={props.loading} - placeholder={t("components.share.user.placeholder")} - inputContent={ - <div ref={selectedUsersRef}> - <ShareSearchField - selectedUsers={pendingInvitationUsers} - onRemoveUser={onRemoveUser} - inputValue={inputValue} - onInputChange={onInputChange} - placeholder={t("components.share.user.placeholder")} - loading={props.loading} - roles={props.invitationRoles!} - selectedRole={selectedInvitationRole} - onSelectRole={setSelectedInvitationRole} - onShare={() => { - props.onInviteUser!( - pendingInvitationUsers, - selectedInvitationRole, - ); - setPendingInvitationUsers([]); - }} - /> - </div> - } - > + <> + <Modal + title={props.modalTitle ?? t("components.share.modalTitle")} + isOpen={props.isOpen} + onClose={props.onClose} + aria-label={t("components.share.modalAriaLabel")} + closeOnClickOutside + size={isMobile ? ModalSize.FULL : ModalSize.LARGE} + > + <div className="c__share-modal no-padding"> + {viewMode === ViewMode.CANNOT_VIEW && ( <div + className="c__share-modal__cannot-view" style={{ height: listHeight, - overflowY: "auto", }} > - {showSearchUsers && ( - <div - className="c__share-modal__search-users" - data-testid="search-users-list" - > - <QuickSearchGroup - group={usersData} - onSelect={(user) => { - onSelect(user); + <div className="c__share-modal__cannot-view__content"> + <p> + {props.cannotViewMessage ?? + t("components.share.cannot_view.message")} + </p> + </div> + {cannotViewChildren} + </div> + )} + + {viewMode === ViewMode.SEARCH && ( + <QuickSearch + onFilter={onInputChange} + inputValue={inputValue} + showInput={canUpdate} + loading={props.loading} + placeholder={t("components.share.user.placeholder")} + inputContent={ + <div ref={selectedUsersRef}> + <ShareSearchField + selectedUsers={pendingInvitationUsers} + onRemoveUser={onRemoveUser} + inputValue={inputValue} + onInputChange={onInputChange} + placeholder={t("components.share.user.placeholder")} + loading={props.loading} + roles={props.invitationRoles!} + selectedRole={selectedInvitationRole} + onSelectRole={setSelectedInvitationRole} + onShare={() => { + props.onInviteUser!( + pendingInvitationUsers, + selectedInvitationRole, + ); + setPendingInvitationUsers([]); }} - renderElement={(user) => <SearchUserItem user={user} />} /> </div> - )} - - {!showSearchUsers && children} - - {/* Invitations list */} - {showInvitations && ( - <div - className="c__share-modal__invitations" - data-testid="invitations-list" - > - <span className="c__share-modal__invitations-title"> - {t("components.share.invitations.title")} - </span> - {invitations.map((invitation) => ( - <ShareInvitationItem - key={invitation.id} - invitation={invitation} - roles={props.invitationRoles!} - updateRole={props.onUpdateInvitation} - deleteInvitation={props.onDeleteInvitation} - canUpdate={canUpdate} - roleTopMessage={props.invitationRoleTopMessage?.( - invitation - )} + } + > + <div + style={{ + height: listHeight, + overflowY: "auto", + }} + > + {showSearchUsers && ( + <div + className="c__share-modal__search-users" + data-testid="search-users-list" + > + <QuickSearchGroup + group={usersData} + onSelect={(user) => { + onSelect(user); + }} + renderElement={(user) => <SearchUserItem user={user} />} /> - ))} - <ShowMoreButton - show={hasNextInvitations} - onShowMore={props.onLoadNextInvitations} - /> - </div> - )} - - {/* Members list */} - {showMembers && ( - <div - className="c__share-modal__members" - data-testid="members-list" - > - <span className="c__share-modal__members-title"> - {t( - members.length > 1 - ? "components.share.members.title_plural" - : "components.share.members.title_singular", - { - count: members.length, - } - )} - </span> - {members.map((member) => ( - <ShareMemberItem - key={member.id} - accessData={member} - accessRoleKey={props.accessRoleKey ?? "role"} - canUpdate={canUpdate} - roleTopMessage={props.accessRoleTopMessage?.(member)} - roles={ - props.getAccessRoles?.(member) ?? props.invitationRoles! - } - updateRole={props.onUpdateAccess} - deleteAccess={props.onDeleteAccess} + </div> + )} + + {!showSearchUsers && children} + + {/* Invitations list */} + {showInvitations && ( + <div + className="c__share-modal__invitations" + data-testid="invitations-list" + > + <span className="c__share-modal__invitations-title"> + {t("components.share.invitations.title")} + </span> + {invitations.map((invitation) => ( + <ShareInvitationItem + key={invitation.id} + invitation={invitation} + roles={props.invitationRoles!} + updateRole={props.onUpdateInvitation} + deleteInvitation={props.onDeleteInvitation} + canUpdate={canUpdate} + roleTopMessage={props.invitationRoleTopMessage?.( + invitation, + )} + /> + ))} + <ShowMoreButton + show={hasNextInvitations} + onShowMore={props.onLoadNextInvitations} /> - ))} - <ShowMoreButton - show={hasNextMembers} - onShowMore={props.onLoadNextMembers} - /> - </div> - )} - </div> - </QuickSearch> - )} - - <div ref={handleRef}> - {!showSearchUsers && ( - <div className="c__share-modal__footer"> - {props.linkSettings && ( - <ShareLinkSettings - linkReachChoices={props.linkReachChoices} - canUpdate={canUpdate} - onUpdateLinkReach={props.onUpdateLinkReach!} - linkReach={props.linkReach} - linkRoleChoices={props.linkRoleChoices} - linkRole={props.linkRole} - onUpdateLinkRole={props.onUpdateLinkRole!} - showLinkRole={props.showLinkRole} - customTranslations={customTranslations} - topLinkReachMessage={props.topLinkReachMessage} - topLinkRoleMessage={props.topLinkRoleMessage} - /> - )} - {outsideSearchContent} - </div> + </div> + )} + + {/* Members list */} + {showMembers && ( + <div + className="c__share-modal__members" + data-testid="members-list" + > + <div className="c__share-modal__members-title"> + <span> + {t( + members.length > 1 + ? "components.share.members.title_plural" + : "components.share.members.title_singular", + { + count: members.length, + }, + )} + </span> + {showFileImport && ( + <DropdownMenu + options={[ + { + label: t("components.share.import.title"), + icon: ( + <span className="material-icons"> + upload_file + </span> + ), + callback: () => setIsImportModalOpen(true), + }, + ]} + isOpen={importMenu.isOpen} + onOpenChange={importMenu.setIsOpen} + > + <Button + variant="tertiary" + color="neutral" + size="small" + icon={<More size={IconSize.SMALL} />} + onClick={() => + importMenu.setIsOpen(!importMenu.isOpen) + } + aria-label={t("components.share.import.title")} + /> + </DropdownMenu> + )} + </div> + {members.map((member) => ( + <ShareMemberItem + key={member.id} + accessData={member} + accessRoleKey={props.accessRoleKey ?? "role"} + canUpdate={canUpdate} + roleTopMessage={props.accessRoleTopMessage?.(member)} + roles={ + props.getAccessRoles?.(member) ?? + props.invitationRoles! + } + updateRole={props.onUpdateAccess} + deleteAccess={props.onDeleteAccess} + /> + ))} + <ShowMoreButton + show={hasNextMembers} + onShowMore={props.onLoadNextMembers} + /> + </div> + )} + </div> + </QuickSearch> )} + + <div ref={handleRef}> + {!showSearchUsers && ( + <div className="c__share-modal__footer"> + {props.linkSettings && ( + <ShareLinkSettings + linkReachChoices={props.linkReachChoices} + canUpdate={canUpdate} + onUpdateLinkReach={props.onUpdateLinkReach!} + linkReach={props.linkReach} + linkRoleChoices={props.linkRoleChoices} + linkRole={props.linkRole} + onUpdateLinkRole={props.onUpdateLinkRole!} + showLinkRole={props.showLinkRole} + customTranslations={customTranslations} + topLinkReachMessage={props.topLinkReachMessage} + topLinkRoleMessage={props.topLinkRoleMessage} + /> + )} + {outsideSearchContent} + </div> + )} + </div> </div> - </div> - </Modal> + </Modal> + {showFileImport && ( + <ShareImportModal + isOpen={isImportModalOpen} + onClose={() => setIsImportModalOpen(false)} + maxRows={props.maxImportRows} + isImporting={isImporting} + onFileChange={props.onImportFileChange} + onImport={async (rows) => { + if (!props.onImportContacts) { + setIsImportModalOpen(false); + return; + } + setIsImporting(true); + try { + if (await props.onImportContacts(rows)) { + setIsImportModalOpen(false); + } + } catch { + // A rejected import behaves like `false`: the modal stays open. + } finally { + setIsImporting(false); + } + }} + > + {props.importModalChildren} + </ShareImportModal> + )} + </> ); }; diff --git a/src/components/share/modal/stories/ShareModal.mdx b/src/components/share/modal/stories/ShareModal.mdx new file mode 100644 index 00000000..26530543 --- /dev/null +++ b/src/components/share/modal/stories/ShareModal.mdx @@ -0,0 +1,243 @@ +import { Meta, Canvas, ArgTypes, Markdown } from "@storybook/blocks"; +import * as ShareModalStories from "./share-modal.stories"; + +<Meta title="Components/Share/Docs" /> + +# ShareModal + +**ShareModal** is the sharing dialog of la Suite: it lets a user search people +and invite them, review and change who already has access, manage pending +invitations, configure the share link, and mass-import contacts from a file. +Every capability is optional and driven by props — you enable the sections you +need and provide callbacks, the modal owns all the UI in between. + +Here it is with everything enabled — search for a user (try typing an email), +change a role, open the "..." menu on the members title: + +<Canvas of={ShareModalStories.Default} /> + +## Installation + +```tsx +import { ShareModal } from "@gouvfr-lasuite/ui-kit"; +``` + +Labels are translated via Cunningham (`useCunningham`), so the modal must live +under a `CunninghamProvider`. + +## Anatomy + +From top to bottom the modal shows: + +1. **A search input** (`onSearchUsers` / `searchUsersResult`) to find users to + invite. Hidden when `canUpdate` is `false`. +2. **Pending selection** — users picked from the results stack up with a role + dropdown (`invitationRoles`) and a share button that fires `onInviteUser`. +3. **Invitations** (`invitations`) — pending invitations, each with a role + dropdown and a delete action. +4. **Members** (`accesses`) — people who already have access, with role + management and the optional "Import contacts" menu. +5. **Footer** — link settings (`linkSettings`) and/or your own + `outsideSearchContent` (e.g. a copy-link button). + +## Providing the data + +The component is generic: your own user/invitation/access types flow through as +long as they contain the minimal shape below. + +### UserData + +<Markdown> + {` +| Property | Type | Description | +|----------|------|-------------| +| \`id\` | \`string\` | Unique identifier | +| \`full_name\` | \`string\` | Full name | +| \`email\` | \`string\` | Email address | +`} +</Markdown> + +### AccessData + +<Markdown> + {` +| Property | Type | Description | +|----------|------|-------------| +| \`id\` | \`string\` | Access identifier | +| \`role\` | \`string\` | Assigned role (key configurable via \`accessRoleKey\`) | +| \`user\` | \`UserData\` | Related user | +| \`is_explicit\` | \`boolean?\` | If \`false\`, inherited access (no deletion) | +| \`can_delete\` | \`boolean?\` | If \`false\`, prevents deletion | +`} +</Markdown> + +### InvitationData + +<Markdown> + {` +| Property | Type | Description | +|----------|------|-------------| +| \`id\` | \`string\` | Invitation identifier | +| \`role\` | \`string\` | Proposed role | +| \`email\` | \`string\` | Invitee email | +| \`user\` | \`UserData\` | User data | +`} +</Markdown> + +## Searching and inviting + +Type in the search input and the modal calls `onSearchUsers` (debounced by +300 ms); render results by feeding them back through `searchUsersResult`. +Selecting a result adds it to the pending list; the share button then calls +`onInviteUser(users, role)` with the chosen role from `invitationRoles`. + +If the query looks like an email address that matches no result, the modal +offers to invite that address directly — useful for people who don't have an +account yet. + +```tsx +<ShareModal + isOpen={isOpen} + onClose={() => setOpen(false)} + onSearchUsers={(query) => setSearchQuery(query)} + searchUsersResult={searchResults} + invitationRoles={[ + { label: "Admin", value: "admin" }, + { label: "Reader", value: "reader" }, + ]} + onInviteUser={(users, role) => invite(users, role)} +/> +``` + +## Managing members and invitations + +Role changes and removals go through `onUpdateAccess` / `onDeleteAccess` and +`onUpdateInvitation` / `onDeleteInvitation`. Per-member role choices can be +computed with `getAccessRoles(access)` (falls back to `invitationRoles`). +Paginate long lists with `hasNextMembers` / `onLoadNextMembers` (and the +invitation equivalents). + +Whether a member's access can be deleted is data-driven: + +<Markdown> + {` +| \`is_explicit\` | \`can_delete\` | Deletion allowed | +|---------------|--------------|------------------| +| \`false\` | - | No (implicit access) | +| \`true\`/\`undefined\` | \`false\` | No | +| \`true\`/\`undefined\` | \`true\`/\`undefined\` | Yes | +`} +</Markdown> + +For example, to prevent deletion of the last admin: + +```tsx +const accesses = rawAccesses.map((access, _, all) => ({ + ...access, + can_delete: all.length > 1 || access.user.id !== currentUserId, +})); +``` + +## Importing contacts from a file + +With `allowFileImport={true}` (and `canUpdate`), a "..." menu appears on the +members title with an "Import contacts" option. It opens the +[ShareImportModal](?path=/docs/components-share-importmodal--docs), which +parses a two-column CSV/XLSX file (email, role) and hands you the result: + +```tsx +const [importModalChildren, setImportModalChildren] = useState<ReactNode>(); + +<ShareModal + allowFileImport={true} + onImportContacts={async (rows) => { + // rows: { email, role }[] + try { + await inviteContacts(rows); + return true; // closes the import modal + } catch { + setImportModalChildren( + <Alert type={VariantType.ERROR}>Import failed, please try again.</Alert>, + ); + return false; // keeps it open + } + }} + importModalChildren={importModalChildren} + onImportFileChange={() => setImportModalChildren(undefined)} +/>; +``` + +`onImportContacts` is asynchronous: while its promise is pending the import +modal cannot be closed (no ESC, no close button, cancel disabled) and the +import button is disabled with a spinner. Resolve `true` to close the modal; +resolving `false` (or throwing) keeps it open so you can surface the error — +`importModalChildren` renders below the modal content, which is the natural +spot for an error `Alert`. Use `onImportFileChange` to clear it when another +file is selected, so a stale error does not stick to the new attempt. + +At most 100 rows can be imported by default; use `maxImportRows` to change the +limit (it is forwarded to the import modal as `maxRows`). + +Try it below: download the template from the import modal (or bring any +two-column CSV), import it, and watch the lists refresh — this example mocks a +backend that processes the file, then randomly registers each row +as a pending invitation or as a member: + +<Canvas of={ShareModalStories.ImportContacts} /> + +And here the mocked backend always rejects the file — the modal stays open and +the error is displayed through `importModalChildren`: + +<Canvas of={ShareModalStories.ImportContactsFailure} /> + +## Read-only and restricted views + +Two props control what the viewer may do: + +- `canUpdate={false}` — read-only: no search input, no role dropdowns, no + deletions, no import menu. +- `canView={false}` — the viewer may not even see who has access: the lists are + replaced with a message (`cannotViewMessage`, `cannotViewChildren`). Note + that `canUpdate` must also be `false` in that case, otherwise the component + throws. + +<Canvas of={ShareModalStories.DefaultReadOnly} /> + +<Canvas of={ShareModalStories.DefaultCannotView} /> + +## Link settings + +Enable the footer with `linkSettings={true}`: a link-reach dropdown +(`linkReachChoices`, `onUpdateLinkReach`) and optionally a link-role dropdown +(`showLinkRole`, `linkRoleChoices`, `onUpdateLinkRole`). Use `hideInvitations` +and `hideMembers` to reduce the modal to the link settings alone: + +<Canvas of={ShareModalStories.LinkSettingsOnly} /> + +Without the role selector, only the reach can be changed: + +<Canvas of={ShareModalStories.LinkSettingsOnlyWithoutRole} /> + +Read-only variants of both are available in the sidebar +(`LinkSettingsOnlyReadOnly`, `LinkSettingsOnlyWithoutRoleReadOnly`), and +`WithoutLinkSettings` shows the opposite: people management with no footer. + +## Customizing texts + +Built-in texts can be overridden per-key with `customTranslations` — here the +description of the "public" reach choice: + +<Canvas of={ShareModalStories.LinkSettingsCustomTexts} /> + +## Requirements and validation + +The component throws at render time when a configuration cannot work: + +- `invitationRoles` and `onSearchUsers` are required unless both + `hideInvitations` and `hideMembers` are set. +- `onInviteUser` is required unless `hideInvitations` is set. +- `canView={false}` requires `canUpdate={false}`. + +## Props + +<ArgTypes of={ShareModalStories} /> diff --git a/src/components/share/modal/stories/ShareModalExample.tsx b/src/components/share/modal/stories/ShareModalExample.tsx index 159836c4..9bed908b 100644 --- a/src/components/share/modal/stories/ShareModalExample.tsx +++ b/src/components/share/modal/stories/ShareModalExample.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { ReactNode, useEffect, useState } from "react"; import { ShareModal } from "../ShareModal"; import { @@ -8,6 +8,9 @@ import { } from ":/components/share/types.ts"; import { ShareModalCopyLinkFooter } from "../../utils/ShareModalCopyLinkFooter"; import { DropdownMenuOption } from ":/components/dropdown-menu"; +import { ShareImportRow } from "../../import-modal/ShareImportModal"; +import { Alert } from ":/components/alert/Alert"; +import { VariantType } from "@gouvfr-lasuite/cunningham-react"; type UserType = UserData<{ short_name?: string; @@ -28,14 +31,20 @@ type AccessType = AccessData< } >; -export const ShareModalExample = (props: { +export const ShareModalExample = ({ + importFails, + ...props +}: { linkSettings?: boolean; canUpdate?: boolean; canView?: boolean; + allowFileImport?: boolean; + importFails?: boolean; }) => { const [userQuery, setUserQuery] = useState(""); const [users, setUsers] = useState<UserType[]>([]); const [loading, setLoading] = useState(false); + const [invitations, setInvitations] = useState<InvitationType[]>(() => { const ids = [1]; return ids.map((id) => ({ @@ -56,7 +65,7 @@ export const ShareModalExample = (props: { id: id.toString(), name: "John Doe " + id, email: "john.doe@example.com " + id, - role: id === 1 ? "viewer" : "admin", + role: id === 1 ? "reader" : "admin", // Example: the first member cannot be deleted (e.g., last admin or current user) can_delete: id !== 1, user: { @@ -68,9 +77,10 @@ export const ShareModalExample = (props: { }); const invitationRoles = [ + { label: "Owner", value: "owner" }, { label: "Admin", value: "admin" }, { label: "Editor", value: "editor" }, - { label: "Viewer", value: "viewer" }, + { label: "Reader", value: "reader" }, ]; const getAccessRoles = (access: AccessType): DropdownMenuOption[] => { @@ -83,8 +93,8 @@ export const ShareModalExample = (props: { isDisabled: isAdmin, }, { - label: "Viewer", - value: "viewer", + label: "Reader", + value: "reader", isDisabled: isAdmin, }, ]; @@ -140,6 +150,62 @@ export const ShareModalExample = (props: { setInvitations(invitations.filter((invit) => invit.id !== invitation.id)); }; + const [importModalChildren, setImportModalChildren] = useState<ReactNode>(); + + /** + * Mock backend for the contacts import: after 1s of "processing", each + * imported row is randomly registered as a pending invitation or as a + * member, and the lists refresh with the emails and roles from the file. + * When `importFails` is set, the "server" rejects the file instead: the + * promise resolves false so the import modal stays open and the error is + * surfaced through `importModalChildren`. + */ + const onImportContacts = (rows: ShareImportRow[]): Promise<boolean> => { + return new Promise((resolve) => { + setTimeout(() => { + if (importFails) { + setImportModalChildren( + <Alert type={VariantType.ERROR}> + The server could not process the file. Please try again. + </Alert>, + ); + resolve(false); + return; + } + const newInvitations: InvitationType[] = []; + const newMembers: AccessType[] = []; + rows.forEach((row, index) => { + const id = `import-${Date.now()}-${index}`; + const user = { + id, + full_name: row.email.split("@")[0], + email: row.email, + }; + if (Math.random() < 0.5) { + newInvitations.push({ + id, + surname: "", + role: row.role, + email: row.email, + user, + }); + } else { + newMembers.push({ + id, + name: user.full_name, + role: row.role, + can_delete: true, + user, + }); + } + }); + setInvitations((prev) => [...prev, ...newInvitations]); + setMembers((prev) => [...prev, ...newMembers]); + resolve(true); + }, 1000); + }); + }; + return ( <ShareModal isOpen={true} @@ -167,6 +233,10 @@ export const ShareModalExample = (props: { }} canUpdate={props.canUpdate ?? true} canView={props.canView ?? true} + allowFileImport={props.allowFileImport} + onImportContacts={onImportContacts} + importModalChildren={importModalChildren} + onImportFileChange={() => setImportModalChildren(undefined)} hasNextInvitations={true} onLoadNextInvitations={() => { console.log("LOAD NEXT INVITATIONS"); diff --git a/src/components/share/modal/stories/share-modal.stories.tsx b/src/components/share/modal/stories/share-modal.stories.tsx index 36e2ef71..c2bfd3da 100644 --- a/src/components/share/modal/stories/share-modal.stories.tsx +++ b/src/components/share/modal/stories/share-modal.stories.tsx @@ -1,123 +1,19 @@ import type { Meta } from "@storybook/react"; -import { Description, Title, Subtitle, ArgTypes } from "@storybook/blocks"; import { useState } from "react"; import { ShareModalExample } from "./ShareModalExample"; import { ShareModal } from "../ShareModal"; import { UserData } from ":/components/share/types.ts"; -/** - * The `ShareModal` component displays a sharing modal with access management, invitations, and link settings. - * - * ## Installation - * - * ```tsx - * import { ShareModal } from "@gouvfr-lasuite/ui-kit"; - * ``` - * - * ## Basic Usage - * - * ```tsx - * <ShareModal - * isOpen={true} - * onClose={() => setOpen(false)} - * accesses={members} - * invitations={invitations} - * invitationRoles={[ - * { label: "Admin", value: "admin" }, - * { label: "Viewer", value: "viewer" }, - * ]} - * onSearchUsers={setSearchQuery} - * searchUsersResult={searchResults} - * onInviteUser={(users, role) => invite(users, role)} - * onUpdateAccess={(access, role) => updateAccess(access, role)} - * onDeleteAccess={(access) => deleteAccess(access)} - * /> - * ``` - * - * ## Data Types - * - * ### UserData - * | Property | Type | Description | - * |----------|------|-------------| - * | `id` | `string` | Unique identifier | - * | `full_name` | `string` | Full name | - * | `email` | `string` | Email address | - * - * ### AccessData - * | Property | Type | Description | - * |----------|------|-------------| - * | `id` | `string` | Access identifier | - * | `role` | `string` | Assigned role | - * | `user` | `UserData` | Related user | - * | `is_explicit` | `boolean?` | If `false`, inherited access (no deletion) | - * | `can_delete` | `boolean?` | If `false`, prevents deletion | - * - * ### InvitationData - * | Property | Type | Description | - * |----------|------|-------------| - * | `id` | `string` | Invitation identifier | - * | `role` | `string` | Proposed role | - * | `email` | `string` | Invitee email | - * | `user` | `UserData` | User data | - * - * ## Access Deletion Control - * - * | `is_explicit` | `can_delete` | Deletion allowed | - * |---------------|--------------|------------------| - * | `false` | - | No (implicit access) | - * | `true`/`undefined` | `false` | No | - * | `true`/`undefined` | `true`/`undefined` | Yes | - * - * ### Example: Prevent deletion of the last admin - * - * ```tsx - * const accesses = rawAccesses.map((access, _, all) => ({ - * ...access, - * can_delete: all.length > 1 || access.user.id !== currentUserId, - * })); - * ``` - * - * ## Link Settings - * - * Enable with `linkSettings={true}`: - * - * ```tsx - * <ShareModal - * linkSettings={true} - * linkReachChoices={[ - * { value: "public", subText: "Everyone" }, - * { value: "restricted", subText: "Members only" }, - * ]} - * linkRole="reader" - * showLinkRole={true} - * linkRoleChoices={[ - * { value: "reader" }, - * { value: "editor" }, - * ]} - * onUpdateLinkReach={(value) => updateReach(value)} - * onUpdateLinkRole={(value) => updateRole(value)} - * /> - * ``` - * - * ## Read-only Mode - * - * Use `canUpdate={false}` to disable modifications. - * Use `canView={false}` to hide the members list. - */ +// The docs page is the attached MDX guide: ./ShareModal.mdx const meta: Meta<typeof ShareModal> = { title: "Components/Share/Modal", component: ShareModal, - tags: ["autodocs"], parameters: { docs: { - page: () => ( - <> - <Title /> - <Subtitle /> - <Description /> - <ArgTypes /> - </> - ), + story: { + inline: false, + iframeHeight: 700, + }, }, }, argTypes: { @@ -201,6 +97,21 @@ const meta: Meta<typeof ShareModal> = { description: "Callback when link role changes", control: false, }, + allowFileImport: { + description: + "Shows an 'Import contacts' menu on the members title (requires canUpdate)", + control: "boolean", + }, + onImportContacts: { + description: + "Async callback called with the rows parsed by the import contacts modal. Resolve `true` to close the modal, `false` (or throw) to keep it open. While pending the modal is locked and the import button shows a spinner", + control: false, + }, + importModalChildren: { + description: + "Extra content rendered inside the import contacts modal (e.g. an error Alert)", + control: false, + }, hideInvitations: { description: "Hides the invitations section", control: "boolean", @@ -218,8 +129,14 @@ const meta: Meta<typeof ShareModal> = { export default meta; +/** + * The full modal: user search with invitation flow, pending invitations, + * members list with role management, link settings, and the "Import contacts" + * menu on the members title (`allowFileImport={true}`) that opens the + * `ShareImportModal`. + */ export const Default = { - render: () => <ShareModalExample linkSettings={true} />, + render: () => <ShareModalExample linkSettings={true} allowFileImport={true} />, }; const SELECTABLE_USERS: UserData<unknown>[] = [ @@ -257,24 +174,68 @@ export const UnifiedSearchField = { render: () => <SelectableShareModal />, }; +/** + * Read-only mode (`canUpdate={false}`): the search input, role dropdowns, + * deletion actions and the import contacts menu are all hidden — the modal + * only displays the current accesses and link settings. + */ export const DefaultReadOnly = { render: () => <ShareModalExample linkSettings={true} canUpdate={false} />, }; +/** + * A working `onImportContacts` flow: open the "..." menu on the members title, + * import a two-column CSV/XLSX file, and a mock backend "processes" it for + * 1 s before refreshing the lists — each row of the file is randomly + * registered as a pending invitation or as a member, keeping the real emails + * and roles from the file. While the import is pending the modal is locked + * and the import button shows a spinner; it closes once the promise + * resolves `true`. + */ +export const ImportContacts = { + render: () => <ShareModalExample allowFileImport={true} />, +}; + +/** + * Failing import: the mock server resolves `false` after a delay, so the + * import modal stays open (non-closable while pending, import button showing + * a spinner) and the consumer surfaces the error as an Alert through + * `importModalChildren`. + */ +export const ImportContactsFailure = { + render: () => <ShareModalExample allowFileImport={true} importFails={true} />, +}; + +/** + * Invitations and members without the link settings footer + * (`linkSettings` omitted). + */ export const WithoutLinkSettings = { render: () => <ShareModalExample />, }; +/** + * `canView={false}`: the members list is replaced by an explanatory message + * (customizable via `cannotViewMessage` / `cannotViewChildren`). + */ export const DefaultCannotView = { render: () => <ShareModalExample canView={false} canUpdate={false} />, }; +/** + * Same as CannotView but keeping the link settings footer visible. + */ export const DefaultCannotViewWithLinkSettings = { render: () => ( <ShareModalExample canView={false} canUpdate={false} linkSettings={true} /> ), }; +/** + * Link-settings-only mode (`hideInvitations` + `hideMembers`): the modal is + * reduced to the link reach/role dropdowns. A disabled reach choice shows the + * `subText`/`isDisabled` option flags. + */ export const LinkSettingsOnly = { render: () => ( <ShareModal @@ -313,6 +274,9 @@ export const LinkSettingsOnly = { ), }; +/** + * Link-settings-only in read-only mode: both dropdowns are rendered disabled. + */ export const LinkSettingsOnlyReadOnly = { render: () => ( <ShareModal @@ -344,6 +308,10 @@ export const LinkSettingsOnlyReadOnly = { ), }; +/** + * Link settings without the role selector (`showLinkRole` omitted): only the + * link reach can be changed. + */ export const LinkSettingsOnlyWithoutRole = { render: () => ( <ShareModal @@ -364,6 +332,9 @@ export const LinkSettingsOnlyWithoutRole = { ), }; +/** + * Reach-only link settings in read-only mode. + */ export const LinkSettingsOnlyWithoutRoleReadOnly = { render: () => ( <ShareModal @@ -385,6 +356,10 @@ export const LinkSettingsOnlyWithoutRoleReadOnly = { ), }; +/** + * Overriding built-in texts with the `customTranslations` prop — here the + * description of the "public" link reach choice. + */ export const LinkSettingsCustomTexts = { render: () => ( <ShareModal From 9304a78eddc1eeb3ee51f576498bb26853ecfd18 Mon Sep 17 00:00:00 2001 From: Nathan Vasse <nathan.vasse@gmail.com> Date: Thu, 23 Jul 2026 15:28:09 +0200 Subject: [PATCH 7/8] =?UTF-8?q?=E2=9C=85(front)=20split=20share=20modal=20?= =?UTF-8?q?e2e=20suite=20by=20concern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The single spec had grown too large to navigate as the modal gained features. Tests are now grouped per area (basics, link settings, invitations, members, search, import) on top of a shared mount helper that records callback calls so scenarios stay serializable across the Playwright CT bridge. --- e2e/helpers/mount-share-modal.tsx | 324 ++++++++++++++++++ e2e/helpers/mount-share.tsx | 47 --- e2e/share-modal/share-modal-basics.spec.tsx | 119 +++++++ .../share-modal-import-integration.spec.tsx | 136 ++++++++ .../share-modal-invitations.spec.tsx | 94 +++++ .../share-modal-link-settings.spec.tsx | 177 ++++++++++ e2e/share-modal/share-modal-members.spec.tsx | 184 ++++++++++ .../share-modal-search-field.spec.tsx | 127 +++++++ .../share-modal-search-invite.spec.tsx | 203 +++++++++++ e2e/share/share-modal.spec.tsx | 163 --------- 10 files changed, 1364 insertions(+), 210 deletions(-) create mode 100644 e2e/helpers/mount-share-modal.tsx delete mode 100644 e2e/helpers/mount-share.tsx create mode 100644 e2e/share-modal/share-modal-basics.spec.tsx create mode 100644 e2e/share-modal/share-modal-import-integration.spec.tsx create mode 100644 e2e/share-modal/share-modal-invitations.spec.tsx create mode 100644 e2e/share-modal/share-modal-link-settings.spec.tsx create mode 100644 e2e/share-modal/share-modal-members.spec.tsx create mode 100644 e2e/share-modal/share-modal-search-field.spec.tsx create mode 100644 e2e/share-modal/share-modal-search-invite.spec.tsx delete mode 100644 e2e/share/share-modal.spec.tsx diff --git a/e2e/helpers/mount-share-modal.tsx b/e2e/helpers/mount-share-modal.tsx new file mode 100644 index 00000000..7ac28ea1 --- /dev/null +++ b/e2e/helpers/mount-share-modal.tsx @@ -0,0 +1,324 @@ +import { useEffect, useState } from "react"; +import { CunninghamProvider } from "../../src/components/Provider/Provider"; +import { ShareModal } from "../../src/components/share/modal/ShareModal"; +import { ShareModalCopyLinkFooter } from "../../src/components/share/utils/ShareModalCopyLinkFooter"; +import { + AccessData, + InvitationData, + UserData, +} from "../../src/components/share/types"; +import { DropdownMenuOption } from "../../src/components/dropdown-menu"; +import { CustomTranslations } from "../../src/hooks/useCustomTranslations"; + +// Playwright CT bridges function props as one-way dispatchers, so tests +// describe scenarios with plain serializable data and this helper — which runs +// in the browser — builds the real callbacks locally. Callback invocations are +// recorded on `window.__shareModalCalls` so tests can assert them via +// `page.evaluate`. + +export type ShareModalCall = { name: string } & Record<string, unknown>; + +declare global { + interface Window { + __shareModalCalls: ShareModalCall[]; + __resolveImport?: (success: boolean) => void; + } +} + +const useCallRecorder = () => { + useEffect(() => { + window.__shareModalCalls = []; + }, []); + return (call: ShareModalCall) => { + (window.__shareModalCalls ??= []).push(call); + }; +}; + +type TestUser = UserData<object>; +type TestInvitation = InvitationData<object, object>; +type TestAccess = AccessData<object, object>; + +// Search results must keep stable object references: ShareModal excludes +// pending users from the results with an identity-based `includes`. +const directory: TestUser[] = [ + { id: "u1", full_name: "Alice Martin", email: "alice@example.com" }, + { id: "u2", full_name: "Bob Martin", email: "bob@example.com" }, + { id: "u3", full_name: "Alice Bernard", email: "alice.bernard@example.com" }, + { id: "u4", full_name: "Amandine Salambo", email: "amandine@example.com" }, + { id: "u5", full_name: "Jakob Philips", email: "jakob@example.com" }, + { id: "u6", full_name: "Kaylynn George", email: "kaylynn@example.com" }, + { id: "u7", full_name: "Charlotte Dubois", email: "charlotte@example.com" }, + { id: "u8", full_name: "Sophie Moreau", email: "sophie@example.com" }, + { + id: "u9", + full_name: "Christopher Martin", + email: "christopher@example.com", + }, +]; + +// Mirrors the ShareModalExample story seed: the first member is a reader that +// cannot be deleted (e.g. current user), the others are admins. +const makeMembers = (count: number): TestAccess[] => + Array.from({ length: count }, (_, index) => { + const id = (index + 1).toString(); + return { + id, + role: index === 0 ? "reader" : "admin", + can_delete: index !== 0, + user: { + id, + full_name: `John Doe ${id}`, + email: `john.doe.${id}@example.com`, + }, + }; + }); + +const makeInvitations = (count: number): TestInvitation[] => + Array.from({ length: count }, (_, index) => { + const id = (index + 1).toString(); + return { + id, + role: "admin", + email: `invited.${id}@example.com`, + user: { + id: `invited-${id}`, + full_name: "", + email: `invited.${id}@example.com`, + }, + }; + }); + +const invitationRoles: DropdownMenuOption[] = [ + { label: "Admin", value: "admin" }, + { label: "Editor", value: "editor" }, + { label: "Reader", value: "reader" }, +]; + +const getAccessRoles = (access: TestAccess): DropdownMenuOption[] => [ + { label: "Admin", value: "admin" }, + { label: "Editor", value: "editor", isDisabled: access.role === "admin" }, + { label: "Reader", value: "reader", isDisabled: access.role === "admin" }, +]; + +export const ADMIN_TOP_MESSAGE = + "You cannot change the role of an administrator"; + +interface TestShareModalProps { + canUpdate?: boolean; + canView?: boolean; + linkSettings?: boolean; + showLinkRole?: boolean; + /** false renders empty linkRoleChoices to hide the role dropdown. */ + withLinkRoleChoices?: boolean; + allowFileImport?: boolean; + maxImportRows?: number; + /** Value onImportContacts resolves with (defaults to true: close). */ + importResult?: boolean; + /** Import stays pending until the test calls window.__resolveImport(value). */ + holdImport?: boolean; + withImportModalChildren?: boolean; + hideMembers?: boolean; + hideInvitations?: boolean; + membersCount?: number; + invitationsCount?: number; + hasNextMembers?: boolean; + hasNextInvitations?: boolean; + /** Searches never resolve: loading stays true forever. */ + holdSearch?: boolean; + /** The user directory returns no result for any query. */ + emptySearchResults?: boolean; + withGetAccessRoles?: boolean; + withAccessRoleTopMessage?: boolean; + withTopLinkMessages?: boolean; + withFooter?: boolean; + withCannotViewChildren?: boolean; + modalTitle?: string; + cannotViewMessage?: string; + customTranslations?: Record<string, string>; +} + +export const TestShareModal = ({ + canUpdate = true, + canView = true, + linkSettings = false, + showLinkRole = true, + withLinkRoleChoices = true, + allowFileImport = false, + maxImportRows, + importResult = true, + holdImport = false, + withImportModalChildren = false, + hideMembers = false, + hideInvitations = false, + membersCount = 4, + invitationsCount = 1, + hasNextMembers = false, + hasNextInvitations = false, + holdSearch = false, + emptySearchResults = false, + withGetAccessRoles = true, + withAccessRoleTopMessage = false, + withTopLinkMessages = false, + withFooter = true, + withCannotViewChildren = false, + modalTitle, + cannotViewMessage, + customTranslations, +}: TestShareModalProps) => { + const record = useCallRecorder(); + const [members, setMembers] = useState<TestAccess[]>(() => + makeMembers(membersCount), + ); + const [invitations, setInvitations] = useState<TestInvitation[]>(() => + makeInvitations(invitationsCount), + ); + const [searchUsersResult, setSearchUsersResult] = useState<TestUser[]>([]); + const [loading, setLoading] = useState(false); + + const onSearchUsers = (query: string) => { + record({ name: "search", query }); + if (query === "") { + setSearchUsersResult([]); + setLoading(false); + return; + } + if (holdSearch) { + setLoading(true); + return; + } + if (emptySearchResults) { + setSearchUsersResult([]); + return; + } + const lowered = query.toLowerCase(); + setSearchUsersResult( + directory.filter( + (user) => + user.full_name.toLowerCase().includes(lowered) || + user.email.toLowerCase().includes(lowered), + ), + ); + }; + + const onInviteUser = (users: TestUser[], role: string) => { + record({ name: "invite", emails: users.map((user) => user.email), role }); + setMembers((prev) => [ + ...prev, + ...users.map((user) => ({ + id: `invited-${user.id}`, + role, + can_delete: true, + user, + })), + ]); + }; + + return ( + <CunninghamProvider currentLocale="en-US"> + <ShareModal + isOpen={true} + onClose={() => record({ name: "close" })} + modalTitle={modalTitle} + canUpdate={canUpdate} + canView={canView} + cannotViewMessage={cannotViewMessage} + cannotViewChildren={ + withCannotViewChildren ? ( + <p data-testid="cannot-view-children">Request access</p> + ) : undefined + } + hideMembers={hideMembers} + hideInvitations={hideInvitations} + accesses={members} + invitations={invitations} + invitationRoles={invitationRoles} + getAccessRoles={withGetAccessRoles ? getAccessRoles : undefined} + accessRoleTopMessage={ + withAccessRoleTopMessage + ? (access) => + access.role === "admin" ? ADMIN_TOP_MESSAGE : undefined + : undefined + } + onSearchUsers={onSearchUsers} + searchUsersResult={searchUsersResult} + loading={loading} + onInviteUser={onInviteUser} + onUpdateAccess={(access, role) => { + record({ name: "update-access", id: access.id, role }); + setMembers((prev) => + prev.map((member) => + member.id === access.id ? { ...member, role } : member, + ), + ); + }} + onDeleteAccess={(access) => { + record({ name: "delete-access", id: access.id }); + setMembers((prev) => + prev.filter((member) => member.id !== access.id), + ); + }} + onUpdateInvitation={(invitation, role) => { + record({ name: "update-invitation", id: invitation.id, role }); + setInvitations((prev) => + prev.map((item) => + item.id === invitation.id ? { ...item, role } : item, + ), + ); + }} + onDeleteInvitation={(invitation) => { + record({ name: "delete-invitation", id: invitation.id }); + setInvitations((prev) => + prev.filter((item) => item.id !== invitation.id), + ); + }} + hasNextMembers={hasNextMembers} + onLoadNextMembers={() => record({ name: "load-next-members" })} + hasNextInvitations={hasNextInvitations} + onLoadNextInvitations={() => record({ name: "load-next-invitations" })} + allowFileImport={allowFileImport} + maxImportRows={maxImportRows} + onImportContacts={(rows) => { + record({ name: "import-contacts", rows }); + if (holdImport) { + return new Promise<boolean>((resolve) => { + window.__resolveImport = resolve; + }); + } + return importResult; + }} + importModalChildren={ + withImportModalChildren ? ( + <p data-testid="import-modal-children">Import failed</p> + ) : undefined + } + linkSettings={linkSettings} + linkReach="public" + linkReachChoices={[{ value: "public" }, { value: "restricted" }]} + onUpdateLinkReach={(value) => + record({ name: "update-link-reach", value }) + } + linkRole="reader" + showLinkRole={showLinkRole} + linkRoleChoices={ + withLinkRoleChoices ? [{ value: "reader" }, { value: "editor" }] : [] + } + onUpdateLinkRole={(value) => record({ name: "update-link-role", value })} + topLinkReachMessage={ + withTopLinkMessages ? <span>Top link reach message</span> : undefined + } + topLinkRoleMessage={ + withTopLinkMessages ? <span>Top link role message</span> : undefined + } + customTranslations={customTranslations as CustomTranslations} + outsideSearchContent={ + withFooter ? ( + <ShareModalCopyLinkFooter + onCopyLink={() => record({ name: "copy-link" })} + onOk={() => record({ name: "ok" })} + /> + ) : undefined + } + /> + </CunninghamProvider> + ); +}; diff --git a/e2e/helpers/mount-share.tsx b/e2e/helpers/mount-share.tsx deleted file mode 100644 index 9f65a56a..00000000 --- a/e2e/helpers/mount-share.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { useState } from "react"; -import { CunninghamProvider } from "../../src/components/Provider/Provider"; -import { ShareModal } from "../../src/components/share/modal/ShareModal"; -import { UserData } from "../../src/components/share/types"; - -type SimpleUser = UserData<unknown>; - -const USERS: SimpleUser[] = [ - { id: "u1", full_name: "Amandine Salambo", email: "amandine@example.com" }, - { id: "u2", full_name: "Jakob Philips", email: "jakob@example.com" }, - { id: "u3", full_name: "Kaylynn George", email: "kaylynn@example.com" }, - { id: "u4", full_name: "Beatrice Laurent", email: "beatrice@example.com" }, - { id: "u5", full_name: "Mohamed Benali", email: "mohamed@example.com" }, - { id: "u6", full_name: "Charlotte Dubois", email: "charlotte@example.com" }, - { id: "u7", full_name: "Alejandro Romero", email: "alejandro@example.com" }, - { id: "u8", full_name: "Sophie Moreau", email: "sophie@example.com" }, - { - id: "u9", - full_name: "Christopher Martin", - email: "christopher@example.com", - }, -]; - -/** - * Minimal stateful ShareModal for Playwright CT. Search always resolves to the - * same users so selection is deterministic. - */ -export const TestShareModal = () => { - const [, setSearch] = useState(""); - return ( - <CunninghamProvider currentLocale="en-US"> - <ShareModal - isOpen - onClose={() => undefined} - invitationRoles={[ - { label: "Admin", value: "admin" }, - { label: "Editor", value: "editor" }, - ]} - onSearchUsers={setSearch} - onInviteUser={() => undefined} - searchUsersResult={USERS} - accesses={[]} - invitations={[]} - /> - </CunninghamProvider> - ); -}; diff --git a/e2e/share-modal/share-modal-basics.spec.tsx b/e2e/share-modal/share-modal-basics.spec.tsx new file mode 100644 index 00000000..09872322 --- /dev/null +++ b/e2e/share-modal/share-modal-basics.spec.tsx @@ -0,0 +1,119 @@ +import { test, expect } from "@playwright/experimental-ct-react"; +import type { Page } from "@playwright/test"; +import { TestShareModal } from "../helpers/mount-share-modal"; + +const calls = (page: Page) => + page.evaluate(() => window.__shareModalCalls ?? []); +const searchInput = (page: Page) => page.getByRole("combobox"); + +test.describe("ShareModal shell and view modes", () => { + test("renders the default title, lists, search and footer", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await expect(page.getByText("Share folder")).toBeVisible(); + await expect(page.getByTestId("members-list")).toBeVisible(); + await expect(page.getByTestId("share-member-item")).toHaveCount(4); + await expect(page.getByText("Pending invitations")).toBeVisible(); + await expect(searchInput(page)).toBeVisible(); + await expect(page.getByRole("button", { name: "Copy link" })).toBeVisible(); + await expect(page.getByRole("button", { name: "OK" })).toBeVisible(); + }); + + test("uses a custom modal title", async ({ mount, page }) => { + await mount(<TestShareModal modalTitle="Partager" />); + + await expect(page.getByText("Partager")).toBeVisible(); + await expect(page.getByText("Share folder")).toHaveCount(0); + }); + + test("invokes onClose when pressing Escape", async ({ mount, page }) => { + await mount(<TestShareModal />); + + await expect(page.getByText("Share folder")).toBeVisible(); + await page.keyboard.press("Escape"); + + expect(await calls(page)).toContainEqual({ name: "close" }); + }); + + test("cannot-view mode shows the message and children and hides the search", async ({ + mount, + page, + }) => { + await mount( + <TestShareModal + canView={false} + canUpdate={false} + withCannotViewChildren + />, + ); + + await expect( + page.getByText( + "You can view this item but you need additional access to view its members or modify the settings.", + ), + ).toBeVisible(); + await expect(page.getByTestId("cannot-view-children")).toBeVisible(); + await expect(searchInput(page)).toHaveCount(0); + await expect(page.getByTestId("members-list")).toHaveCount(0); + }); + + test("cannot-view mode uses a custom message", async ({ mount, page }) => { + await mount( + <TestShareModal + canView={false} + canUpdate={false} + cannotViewMessage="Custom denied" + />, + ); + + await expect(page.getByText("Custom denied")).toBeVisible(); + }); + + test("empty mode renders neither search nor lists but keeps the footer", async ({ + mount, + page, + }) => { + await mount(<TestShareModal hideMembers hideInvitations />); + + await expect(page.getByText("Share folder")).toBeVisible(); + await expect(searchInput(page)).toHaveCount(0); + await expect(page.getByTestId("members-list")).toHaveCount(0); + await expect(page.getByTestId("invitations-list")).toHaveCount(0); + await expect(page.getByRole("button", { name: "OK" })).toBeVisible(); + }); + + test("read-only mode hides the search input and renders static roles", async ({ + mount, + page, + }) => { + await mount(<TestShareModal canUpdate={false} linkSettings />); + + await expect(page.getByTestId("members-list")).toBeVisible(); + await expect(searchInput(page)).toHaveCount(0); + await expect( + page + .getByTestId("members-list") + .locator(".c__access-role-dropdown__role-label-can-not-update"), + ).toHaveCount(4); + await expect(page.getByTestId("access-role-dropdown-button")).toHaveCount( + 0, + ); + await expect( + page.getByTestId("share-link-reach-dropdown-button"), + ).toHaveCount(0); + }); + + test("hiding members alone keeps the search and invitations", async ({ + mount, + page, + }) => { + await mount(<TestShareModal hideMembers />); + + await expect(searchInput(page)).toBeVisible(); + await expect(page.getByTestId("invitations-list")).toBeVisible(); + await expect(page.getByTestId("members-list")).toHaveCount(0); + }); +}); diff --git a/e2e/share-modal/share-modal-import-integration.spec.tsx b/e2e/share-modal/share-modal-import-integration.spec.tsx new file mode 100644 index 00000000..523de8c1 --- /dev/null +++ b/e2e/share-modal/share-modal-import-integration.spec.tsx @@ -0,0 +1,136 @@ +import { test, expect } from "@playwright/experimental-ct-react"; +import type { Page } from "@playwright/test"; +import { TestShareModal } from "../helpers/mount-share-modal"; + +const calls = (page: Page) => + page.evaluate(() => window.__shareModalCalls ?? []); + +const importModalDescription = (page: Page) => + page.getByText( + "Upload a CSV or XLS file with your contacts, or start from our template.", + ); + +const openImportModalAndUpload = async (page: Page) => { + await page.getByRole("button", { name: "Import contacts" }).click(); + await page.getByRole("menuitem", { name: "Import contacts" }).click(); + + await expect(importModalDescription(page)).toBeVisible(); + + await page.locator('input[type="file"]').setInputFiles({ + name: "contacts.csv", + mimeType: "text/csv", + buffer: Buffer.from("alice@example.com,admin"), + }); + await expect(page.locator(".c__alert--info")).toContainText( + "1 row ready to be imported.", + ); +}; + +// The kebab-visibility cases (allowFileImport on/off, canUpdate false) are +// covered by e2e/share-import-modal/share-modal-import.spec.tsx, and the +// parsing internals by share-import-modal.spec.tsx. This spec only covers the +// seam those files cannot assert: the rows reaching onImportContacts and the +// close/lock behavior driven by its resolved value. +test.describe("ShareModal import contacts seam", () => { + test("imported rows flow to onImportContacts and the import modal closes when it resolves true", async ({ + mount, + page, + }) => { + await mount(<TestShareModal allowFileImport />); + + await openImportModalAndUpload(page); + + await page.getByRole("button", { name: "Import", exact: true }).click(); + + expect(await calls(page)).toContainEqual({ + name: "import-contacts", + rows: [{ email: "alice@example.com", role: "admin" }], + }); + await expect(importModalDescription(page)).toHaveCount(0); + }); + + test("keeps the import modal open when onImportContacts resolves false", async ({ + mount, + page, + }) => { + await mount(<TestShareModal allowFileImport importResult={false} />); + + await openImportModalAndUpload(page); + + await page.getByRole("button", { name: "Import", exact: true }).click(); + + expect(await calls(page)).toContainEqual({ + name: "import-contacts", + rows: [{ email: "alice@example.com", role: "admin" }], + }); + await expect(importModalDescription(page)).toBeVisible(); + }); + + test("locks the import modal while onImportContacts is pending", async ({ + mount, + page, + }) => { + await mount(<TestShareModal allowFileImport holdImport />); + + await openImportModalAndUpload(page); + + const importModal = page.locator(".c__modal--medium"); + const importButton = page.getByRole("button", { + name: "Import", + exact: true, + }); + await importButton.click(); + + await expect(importButton).toBeDisabled(); + await expect(importButton.locator(".c__spinner")).toBeVisible(); + await expect( + page.getByRole("button", { name: "Cancel", exact: true }), + ).toBeDisabled(); + // preventClose removes the import modal's own close button (the parent + // ShareModal keeps its X, hence the scoping to the medium modal). + await expect( + importModal.getByRole("button", { name: "close" }), + ).toHaveCount(0); + + await page.keyboard.press("Escape"); + await expect(importModalDescription(page)).toBeVisible(); + + await page.evaluate(() => window.__resolveImport?.(true)); + await expect(importModalDescription(page)).toHaveCount(0); + }); + + test("forwards importModalChildren into the import modal", async ({ + mount, + page, + }) => { + await mount(<TestShareModal allowFileImport withImportModalChildren />); + + await page.getByRole("button", { name: "Import contacts" }).click(); + await page.getByRole("menuitem", { name: "Import contacts" }).click(); + + await expect(page.getByTestId("import-modal-children")).toBeVisible(); + }); + + test("maxImportRows is forwarded to the import modal", async ({ + mount, + page, + }) => { + await mount(<TestShareModal allowFileImport maxImportRows={1} />); + + await page.getByRole("button", { name: "Import contacts" }).click(); + await page.getByRole("menuitem", { name: "Import contacts" }).click(); + + await page.locator('input[type="file"]').setInputFiles({ + name: "contacts.csv", + mimeType: "text/csv", + buffer: Buffer.from("alice@example.com,admin\nbob@example.com,viewer"), + }); + + await expect( + page.locator(".c__file-uploader__dropzone__error"), + ).toContainText("The file exceeds the maximum of 1 rows."); + await expect( + page.getByRole("button", { name: "Import", exact: true }), + ).toBeDisabled(); + }); +}); diff --git a/e2e/share-modal/share-modal-invitations.spec.tsx b/e2e/share-modal/share-modal-invitations.spec.tsx new file mode 100644 index 00000000..0305aec0 --- /dev/null +++ b/e2e/share-modal/share-modal-invitations.spec.tsx @@ -0,0 +1,94 @@ +import { test, expect } from "@playwright/experimental-ct-react"; +import type { Page } from "@playwright/test"; +import { TestShareModal } from "../helpers/mount-share-modal"; + +const calls = (page: Page) => + page.evaluate(() => window.__shareModalCalls ?? []); +const invitationsList = (page: Page) => page.getByTestId("invitations-list"); +const invitationRoleButton = (page: Page) => + invitationsList(page).getByTestId("access-role-dropdown-button"); + +test.describe("ShareModal invitations list", () => { + test("renders the invitation rows under the pending title", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await expect(invitationsList(page)).toBeVisible(); + await expect(page.getByText("Pending invitations")).toBeVisible(); + await expect(page.getByTestId("share-invitation-item")).toHaveCount(1); + await expect(page.getByText("invited.1@example.com")).toBeVisible(); + await expect(invitationRoleButton(page)).toContainText("Admin"); + }); + + test("hides the section when there is no invitation", async ({ + mount, + page, + }) => { + await mount(<TestShareModal invitationsCount={0} />); + + await expect(page.getByTestId("members-list")).toBeVisible(); + await expect(invitationsList(page)).toHaveCount(0); + }); + + test("changing a role invokes onUpdateInvitation and updates the row", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await invitationRoleButton(page).click(); + await page.getByRole("menuitem", { name: "Reader" }).click(); + + expect(await calls(page)).toContainEqual({ + name: "update-invitation", + id: "1", + role: "reader", + }); + await expect(invitationRoleButton(page)).toContainText("Reader"); + }); + + test("deleting the last invitation removes the section", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await invitationRoleButton(page).click(); + await page.getByRole("menuitem", { name: "Remove access" }).click(); + + expect(await calls(page)).toContainEqual({ + name: "delete-invitation", + id: "1", + }); + await expect(invitationsList(page)).toHaveCount(0); + await expect(page.getByTestId("members-list")).toBeVisible(); + }); + + test("shows more invitations on demand", async ({ mount, page }) => { + await mount(<TestShareModal hasNextInvitations />); + + await invitationsList(page) + .getByRole("button", { name: "Show more" }) + .click(); + + expect(await calls(page)).toContainEqual({ + name: "load-next-invitations", + }); + }); + + test("read-only mode renders a static role label", async ({ + mount, + page, + }) => { + await mount(<TestShareModal canUpdate={false} />); + + await expect( + invitationsList(page).locator( + ".c__access-role-dropdown__role-label-can-not-update", + ), + ).toHaveText("Admin"); + await expect(invitationRoleButton(page)).toHaveCount(0); + }); +}); diff --git a/e2e/share-modal/share-modal-link-settings.spec.tsx b/e2e/share-modal/share-modal-link-settings.spec.tsx new file mode 100644 index 00000000..0530027c --- /dev/null +++ b/e2e/share-modal/share-modal-link-settings.spec.tsx @@ -0,0 +1,177 @@ +import { test, expect } from "@playwright/experimental-ct-react"; +import type { Page } from "@playwright/test"; +import { TestShareModal } from "../helpers/mount-share-modal"; + +const calls = (page: Page) => + page.evaluate(() => window.__shareModalCalls ?? []); +const reachButton = (page: Page) => + page.getByTestId("share-link-reach-dropdown-button"); +const roleButton = (page: Page) => + page.getByTestId("share-link-role-dropdown-button"); +// Both a desktop and a mobile description are in the DOM; CSS hides one. +const reachDescription = (page: Page) => + page.locator(".c__share-modal__link-settings__content__description.desktop"); + +test.describe("ShareModal link settings and footer", () => { + test("renders the reach and role with their initial values", async ({ + mount, + page, + }) => { + await mount(<TestShareModal linkSettings />); + + await expect(page.getByTestId("share-link-settings")).toBeVisible(); + await expect(page.getByText("Link settings")).toBeVisible(); + await expect(reachButton(page)).toContainText("Public"); + await expect(reachDescription(page)).toHaveText( + "Anyone with the link can access the document", + ); + await expect(roleButton(page)).toContainText("Reader"); + }); + + test("changing the reach invokes onUpdateLinkReach and updates the description", async ({ + mount, + page, + }) => { + await mount(<TestShareModal linkSettings />); + + await reachButton(page).click(); + await page.getByRole("menuitem", { name: "Private" }).click(); + + expect(await calls(page)).toContainEqual({ + name: "update-link-reach", + value: "restricted", + }); + await expect(reachButton(page)).toContainText("Private"); + await expect(reachDescription(page)).toHaveText( + "Only users of the space can access the document", + ); + }); + + test("changing the role invokes onUpdateLinkRole", async ({ + mount, + page, + }) => { + await mount(<TestShareModal linkSettings />); + + await roleButton(page).click(); + await page.getByRole("menuitem", { name: "Editor" }).click(); + + expect(await calls(page)).toContainEqual({ + name: "update-link-role", + value: "editor", + }); + await expect(roleButton(page)).toContainText("Editor"); + }); + + test("hides the role dropdown when showLinkRole is false", async ({ + mount, + page, + }) => { + await mount(<TestShareModal linkSettings showLinkRole={false} />); + + await expect(reachButton(page)).toBeVisible(); + await expect(roleButton(page)).toHaveCount(0); + }); + + test("hides the role dropdown when linkRoleChoices is empty", async ({ + mount, + page, + }) => { + await mount(<TestShareModal linkSettings withLinkRoleChoices={false} />); + + await expect(reachButton(page)).toBeVisible(); + await expect(roleButton(page)).toHaveCount(0); + }); + + test("shows the top messages inside the open dropdowns", async ({ + mount, + page, + }) => { + await mount(<TestShareModal linkSettings withTopLinkMessages />); + + await reachButton(page).click(); + await expect(page.getByText("Top link reach message")).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(page.getByText("Top link reach message")).toHaveCount(0); + + await roleButton(page).click(); + await expect(page.getByText("Top link role message")).toBeVisible(); + }); + + test("read-only mode renders disabled values without dropdowns", async ({ + mount, + page, + }) => { + await mount(<TestShareModal linkSettings canUpdate={false} />); + + await expect( + page.locator( + ".c__share-modal__link-settings__content__select__value.disabled", + ), + ).toHaveText("Public"); + await expect( + page.locator( + ".c__share-modal__link-settings__content__select-role__value.disabled", + ), + ).toHaveText("Reader"); + await expect(reachButton(page)).toHaveCount(0); + await expect(roleButton(page)).toHaveCount(0); + }); + + test("renders no link settings section by default", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await expect(page.getByTestId("members-list")).toBeVisible(); + await expect(page.getByTestId("share-link-settings")).toHaveCount(0); + await expect(page.getByRole("button", { name: "OK" })).toBeVisible(); + }); + + test("applies customTranslations overrides", async ({ mount, page }) => { + await mount( + <TestShareModal + linkSettings + customTranslations={{ + "components.share.linkSettings.title": "Custom link title", + }} + />, + ); + + await expect(page.getByText("Custom link title")).toBeVisible(); + await expect(page.getByText("Link settings")).toHaveCount(0); + }); + + test("the footer buttons invoke onCopyLink and onOk", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await page.getByRole("button", { name: "Copy link" }).click(); + await page.getByRole("button", { name: "OK" }).click(); + + expect(await calls(page)).toEqual([ + { name: "copy-link" }, + { name: "ok" }, + ]); + }); + + test("hides the footer while searching and restores it after", async ({ + mount, + page, + }) => { + await mount(<TestShareModal linkSettings />); + + await page.getByRole("combobox").pressSequentially("alice"); + await expect(page.getByTestId("search-users-list")).toBeVisible(); + await expect(page.getByRole("button", { name: "OK" })).toHaveCount(0); + await expect(page.getByTestId("share-link-settings")).toHaveCount(0); + + await page.getByRole("combobox").fill(""); + + await expect(page.getByRole("button", { name: "OK" })).toBeVisible(); + await expect(page.getByTestId("share-link-settings")).toBeVisible(); + }); +}); diff --git a/e2e/share-modal/share-modal-members.spec.tsx b/e2e/share-modal/share-modal-members.spec.tsx new file mode 100644 index 00000000..845af087 --- /dev/null +++ b/e2e/share-modal/share-modal-members.spec.tsx @@ -0,0 +1,184 @@ +import { test, expect } from "@playwright/experimental-ct-react"; +import type { Page } from "@playwright/test"; +import { TestShareModal } from "../helpers/mount-share-modal"; + +// Kept in sync with ADMIN_TOP_MESSAGE in the mount helper: Playwright CT +// specs can only import components from files containing JSX. +const ADMIN_TOP_MESSAGE = "You cannot change the role of an administrator"; + +const calls = (page: Page) => + page.evaluate(() => window.__shareModalCalls ?? []); +const memberItems = (page: Page) => page.getByTestId("share-member-item"); +const memberRoleButton = (page: Page, index: number) => + memberItems(page).nth(index).getByTestId("access-role-dropdown-button"); + +test.describe("ShareModal members list", () => { + test("renders the member rows with names, emails and current roles", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await expect(memberItems(page)).toHaveCount(4); + await expect(page.getByText("Shared between 4 people")).toBeVisible(); + await expect(page.getByText("John Doe 1")).toBeVisible(); + await expect(page.getByText("john.doe.1@example.com")).toBeVisible(); + await expect(memberRoleButton(page, 0)).toContainText("Reader"); + await expect(memberRoleButton(page, 1)).toContainText("Admin"); + }); + + test("uses the singular title with a single member", async ({ + mount, + page, + }) => { + await mount(<TestShareModal membersCount={1} />); + + await expect(page.getByText("Shared between 1 person")).toBeVisible(); + }); + + test("changing a role invokes onUpdateAccess and updates the row", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await memberRoleButton(page, 0).click(); + await page.getByRole("menuitem", { name: "Editor" }).click(); + + expect(await calls(page)).toContainEqual({ + name: "update-access", + id: "1", + role: "editor", + }); + await expect(memberRoleButton(page, 0)).toContainText("Editor"); + }); + + test("getAccessRoles disables options per member", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + // Member 2 is an admin: only the Admin role stays enabled. + await memberRoleButton(page, 1).click(); + + await expect( + page.getByRole("menuitem", { name: "Editor" }), + ).toHaveAttribute("aria-disabled", "true"); + await expect( + page.getByRole("menuitem", { name: "Reader" }), + ).toHaveAttribute("aria-disabled", "true"); + await expect( + page.getByRole("menuitem", { name: "Admin" }), + ).not.toHaveAttribute("aria-disabled", "true"); + + // Playwright refuses to click aria-disabled elements; dispatch the event + // directly to prove the option really is inert. + await page.getByRole("menuitem", { name: "Editor" }).dispatchEvent("click"); + + expect( + (await calls(page)).filter((call) => call.name === "update-access"), + ).toEqual([]); + await expect(memberRoleButton(page, 1)).toContainText("Admin"); + }); + + test("shows the accessRoleTopMessage only for admins", async ({ + mount, + page, + }) => { + await mount(<TestShareModal withAccessRoleTopMessage />); + + await memberRoleButton(page, 1).click(); + await expect(page.getByText(ADMIN_TOP_MESSAGE)).toBeVisible(); + await page.keyboard.press("Escape"); + + await memberRoleButton(page, 0).click(); + await expect(page.getByRole("menuitem", { name: "Admin" })).toBeVisible(); + await expect(page.getByText(ADMIN_TOP_MESSAGE)).toHaveCount(0); + }); + + test("deleting a member invokes onDeleteAccess and removes the row", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await memberRoleButton(page, 1).click(); + await page.getByRole("menuitem", { name: "Remove access" }).click(); + + expect(await calls(page)).toContainEqual({ + name: "delete-access", + id: "2", + }); + await expect(memberItems(page)).toHaveCount(3); + await expect(page.getByText("Shared between 3 people")).toBeVisible(); + }); + + test("disables the deletion when can_delete is false", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + // Member 1 is seeded with can_delete: false. + await memberRoleButton(page, 0).click(); + + await expect( + page.getByRole("menuitem", { name: "Remove access" }), + ).toHaveAttribute("aria-disabled", "true"); + + // Playwright refuses to click aria-disabled elements; dispatch the event + // directly to prove the option really is inert. + await page + .getByRole("menuitem", { name: "Remove access" }) + .dispatchEvent("click"); + + expect( + (await calls(page)).filter((call) => call.name === "delete-access"), + ).toEqual([]); + await expect(memberItems(page)).toHaveCount(4); + }); + + test("shows more members on demand", async ({ mount, page }) => { + await mount(<TestShareModal hasNextMembers />); + + await page + .getByTestId("members-list") + .getByRole("button", { name: "Show more" }) + .click(); + + expect(await calls(page)).toContainEqual({ name: "load-next-members" }); + }); + + test("hides the show-more button without a next page", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await expect(page.getByTestId("members-list")).toBeVisible(); + await expect( + page + .getByTestId("members-list") + .getByRole("button", { name: "Show more" }), + ).toHaveCount(0); + }); + + test("read-only mode renders static role labels", async ({ + mount, + page, + }) => { + await mount(<TestShareModal canUpdate={false} />); + + const staticLabels = page + .getByTestId("members-list") + .locator(".c__access-role-dropdown__role-label-can-not-update"); + await expect(staticLabels).toHaveCount(4); + await expect(staticLabels.first()).toHaveText("Reader"); + await expect( + page + .getByTestId("members-list") + .getByTestId("access-role-dropdown-button"), + ).toHaveCount(0); + }); +}); diff --git a/e2e/share-modal/share-modal-search-field.spec.tsx b/e2e/share-modal/share-modal-search-field.spec.tsx new file mode 100644 index 00000000..4b942ce3 --- /dev/null +++ b/e2e/share-modal/share-modal-search-field.spec.tsx @@ -0,0 +1,127 @@ +import { test, expect } from "@playwright/experimental-ct-react"; +import type { Page } from "@playwright/test"; +import { TestShareModal } from "../helpers/mount-share-modal"; + +const field = (page: Page) => page.getByTestId("share-search-field"); +const input = (page: Page) => page.getByRole("combobox"); + +const selectUser = async (page: Page, query: string, name: string) => { + await input(page).fill(query); + await page.getByTestId("search-users-list").getByText(name).click(); +}; + +test.describe("ShareModal unified search field", () => { + test("selected users appear as chips inside the search field (no gray box)", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await expect(field(page)).toBeVisible(); + + const searchIcon = field(page).locator( + ".c__share-modal__search-field__icon", + ); + await expect(searchIcon.locator("svg")).toBeVisible(); + await expect(searchIcon.locator(".material-icons")).toHaveCount(0); + await expect(searchIcon).toHaveCSS("color", "rgb(93, 93, 112)"); + + // The legacy separate selected-users frame must be gone. + await expect(page.locator(".c__share-modal__selected-users")).toHaveCount( + 0, + ); + + await selectUser(page, "ama", "Amandine Salambo"); + + // The selected user is now a chip living inside the search field… + const chip = field(page).getByTestId("selected-user-item"); + await expect(chip).toBeVisible(); + await expect(chip).toContainText("Amandine Salambo"); + + // …along with the role selector and the invite button. + await expect(page.getByTestId("share-invite-button")).toBeVisible(); + await expect(field(page).getByRole("button", { name: "Admin" })).toBeVisible(); + }); + + test("Backspace selects the last chip before removing it when the input is empty", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await selectUser(page, "ama", "Amandine Salambo"); + await selectUser(page, "ja", "Jakob Philips"); + + const chips = field(page).getByTestId("selected-user-item"); + const amandineChip = chips.filter({ hasText: "Amandine Salambo" }); + const jakobChip = chips.filter({ hasText: "Jakob Philips" }); + + await expect(input(page)).toHaveValue(""); + await expect(chips).toHaveCount(2); + + await input(page).press("Backspace"); + + await expect(chips).toHaveCount(2); + await expect(jakobChip).toHaveAttribute("data-selected", "true"); + await expect(amandineChip).not.toHaveAttribute("data-selected", "true"); + + await input(page).press("Backspace"); + + await expect(jakobChip).toHaveCount(0); + await expect(amandineChip).toBeVisible(); + + await input(page).press("Backspace"); + await expect(amandineChip).toHaveAttribute("data-selected", "true"); + + await input(page).press("Backspace"); + await expect(field(page).getByTestId("selected-user-item")).toHaveCount(0); + await expect(page.getByTestId("share-invite-button")).toHaveCount(0); + }); + + test("the first row stays fixed while the input follows the current row", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + const icon = field(page).locator(".c__share-modal__search-field__icon"); + + await selectUser(page, "ama", "Amandine Salambo"); + + const firstChip = field(page) + .getByTestId("selected-user-item") + .filter({ hasText: "Amandine Salambo" }); + const initialChipBox = await firstChip.boundingBox(); + const initialIconBox = await icon.boundingBox(); + const initialInputBox = await input(page).boundingBox(); + + for (const [query, name] of [ + ["ja", "Jakob Philips"], + ["ka", "Kaylynn George"], + ["cha", "Charlotte Dubois"], + ["so", "Sophie Moreau"], + ["chr", "Christopher Martin"], + ["alice", "Alice Martin"], + ["bob", "Bob Martin"], + ["bern", "Alice Bernard"], + ]) { + await selectUser(page, query, name); + } + + const chips = field(page).getByTestId("selected-user-item"); + const chipRows = await chips.evaluateAll((elements) => + Array.from( + new Set(elements.map((element) => element.getBoundingClientRect().top)), + ), + ); + const wrappedChipBox = await firstChip.boundingBox(); + const wrappedIconBox = await icon.boundingBox(); + const wrappedInputBox = await input(page).boundingBox(); + + expect(chipRows.length).toBeGreaterThan(1); + expect(wrappedIconBox?.y).toBe(initialIconBox?.y); + expect(wrappedChipBox?.y).toBe(initialChipBox?.y); + expect(initialInputBox?.y).toBe(initialChipBox?.y); + expect(wrappedInputBox?.y).toBe(Math.max(...chipRows)); + }); +}); diff --git a/e2e/share-modal/share-modal-search-invite.spec.tsx b/e2e/share-modal/share-modal-search-invite.spec.tsx new file mode 100644 index 00000000..419946f5 --- /dev/null +++ b/e2e/share-modal/share-modal-search-invite.spec.tsx @@ -0,0 +1,203 @@ +import { test, expect } from "@playwright/experimental-ct-react"; +import type { Page } from "@playwright/test"; +import { TestShareModal } from "../helpers/mount-share-modal"; + +const searchInput = (page: Page) => page.getByRole("combobox"); +const searchCalls = (page: Page) => + page.evaluate(() => + (window.__shareModalCalls ?? []).filter((call) => call.name === "search"), + ); +const calls = (page: Page) => + page.evaluate(() => window.__shareModalCalls ?? []); + +const selectUser = async (page: Page, query: string, name: string) => { + await searchInput(page).pressSequentially(query); + await page.getByTestId("search-users-list").getByText(name).click(); +}; + +test.describe("ShareModal user search and invite", () => { + test("searching shows results and hides the lists and footer", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await searchInput(page).pressSequentially("alice"); + + await expect(page.getByTestId("search-users-list")).toBeVisible(); + await expect(page.getByTestId("search-user-item")).toHaveCount(2); + await expect(page.getByText("Alice Martin")).toBeVisible(); + await expect(page.getByText("Alice Bernard")).toBeVisible(); + await expect(page.getByTestId("members-list")).toHaveCount(0); + await expect(page.getByTestId("invitations-list")).toHaveCount(0); + await expect(page.getByRole("button", { name: "OK" })).toHaveCount(0); + }); + + test("debounces onSearchUsers to a single trailing call", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await searchInput(page).pressSequentially("ali"); + await expect(page.getByTestId("search-users-list")).toBeVisible(); + + expect(await searchCalls(page)).toEqual([{ name: "search", query: "ali" }]); + }); + + test("clearing the input resets immediately and restores the lists", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await searchInput(page).pressSequentially("alice"); + await expect(page.getByTestId("search-users-list")).toBeVisible(); + + await searchInput(page).fill(""); + + await expect(page.getByTestId("search-users-list")).toHaveCount(0); + await expect(page.getByTestId("members-list")).toBeVisible(); + expect(await searchCalls(page)).toEqual([ + { name: "search", query: "alice" }, + { name: "search", query: "" }, + ]); + }); + + test("shows the loading state while a search is pending", async ({ + mount, + page, + }) => { + await mount(<TestShareModal holdSearch />); + + await searchInput(page).pressSequentially("alice"); + + await expect(page.locator(".c__spinner")).toBeVisible(); + await expect(page.getByTestId("members-list")).toHaveCount(0); + }); + + test("shows no result for an unmatched non-email query", async ({ + mount, + page, + }) => { + await mount(<TestShareModal emptySearchResults />); + + await searchInput(page).pressSequentially("zzz"); + + await expect(page.getByText("No result")).toBeVisible(); + await expect(page.getByTestId("search-user-item")).toHaveCount(0); + }); + + test("offers an invite row for a valid unknown email", async ({ + mount, + page, + }) => { + await mount(<TestShareModal emptySearchResults />); + + await searchInput(page).pressSequentially("new.person@example.com"); + + await expect(page.getByTestId("search-user-item")).toHaveCount(1); + await expect(page.getByText("new.person@example.com")).toBeVisible(); + await expect(page.getByText("No result")).toHaveCount(0); + }); + + test("selecting a result adds a pending chip and clears the search", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await selectUser(page, "bob", "Bob Martin"); + + await expect(page.getByTestId("share-invite-button")).toBeVisible(); + await expect(page.getByTestId("selected-user-item")).toHaveCount(1); + await expect(page.getByTestId("selected-user-item")).toContainText( + "Bob Martin", + ); + await expect(searchInput(page)).toHaveValue(""); + // Pending users keep the search view active: lists and footer stay hidden. + await expect(page.getByTestId("members-list")).toHaveCount(0); + await expect(page.getByRole("button", { name: "OK" })).toHaveCount(0); + }); + + test("removes a pending chip", async ({ mount, page }) => { + await mount(<TestShareModal />); + + await selectUser(page, "bob", "Bob Martin"); + await expect(page.getByTestId("selected-user-item")).toHaveCount(1); + + await page.getByTestId("selected-user-item").getByRole("button").click(); + + await expect(page.getByTestId("selected-user-item")).toHaveCount(0); + // Removing the last chip also hides the invite action. + await expect(page.getByTestId("share-invite-button")).toHaveCount(0); + await expect(page.getByTestId("members-list")).toBeVisible(); + }); + + test("pending role defaults to the first role and can be changed", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await selectUser(page, "bob", "Bob Martin"); + + const roleButton = page + .getByTestId("share-search-field") + .getByTestId("access-role-dropdown-button"); + await expect(roleButton).toContainText("Admin"); + + await roleButton.click(); + await page.getByRole("menuitem", { name: "Editor" }).click(); + + await expect(roleButton).toContainText("Editor"); + }); + + test("share invokes onInviteUser with the users and role, then clears the chips", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await selectUser(page, "alice", "Alice Martin"); + await selectUser(page, "bob", "Bob Martin"); + await expect(page.getByTestId("selected-user-item")).toHaveCount(2); + + const roleButton = page + .getByTestId("share-search-field") + .getByTestId("access-role-dropdown-button"); + await roleButton.click(); + await page.getByRole("menuitem", { name: "Reader" }).click(); + + await page.getByRole("button", { name: "Share" }).click(); + + expect(await calls(page)).toContainEqual({ + name: "invite", + emails: ["alice@example.com", "bob@example.com"], + role: "reader", + }); + await expect(page.getByTestId("selected-user-item")).toHaveCount(0); + // The helper registers invited users as members. + await expect(page.getByTestId("share-member-item")).toHaveCount(6); + await expect(page.getByRole("button", { name: "OK" })).toBeVisible(); + }); + + test("excludes already selected users from the search results", async ({ + mount, + page, + }) => { + await mount(<TestShareModal />); + + await selectUser(page, "alice", "Alice Martin"); + + await searchInput(page).pressSequentially("alice"); + + await expect(page.getByTestId("search-user-item")).toHaveCount(1); + await expect( + page.getByTestId("search-users-list").getByText("Alice Bernard"), + ).toBeVisible(); + await expect( + page.getByTestId("search-users-list").getByText("Alice Martin"), + ).toHaveCount(0); + }); +}); diff --git a/e2e/share/share-modal.spec.tsx b/e2e/share/share-modal.spec.tsx deleted file mode 100644 index 4b46cc86..00000000 --- a/e2e/share/share-modal.spec.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { test, expect } from "@playwright/experimental-ct-react"; -import { TestShareModal } from "../helpers/mount-share"; - -test.describe("ShareModal — unified search field", () => { - test("selected users appear as chips inside the search field (no gray box)", async ({ - mount, - page, - }) => { - await mount(<TestShareModal />); - - const field = page.getByTestId("share-search-field"); - await expect(field).toBeVisible(); - - const searchIcon = field.locator(".c__share-modal__search-field__icon"); - await expect(searchIcon.locator("svg")).toBeVisible(); - await expect(searchIcon.locator(".material-icons")).toHaveCount(0); - await expect(searchIcon).toHaveCSS("color", "rgb(93, 93, 112)"); - - // The legacy separate selected-users frame must be gone. - await expect(page.locator(".c__share-modal__selected-users")).toHaveCount(0); - - // Type to trigger the (debounced) search results. - const input = field.locator(".c__share-modal__search-field__input"); - await input.fill("am"); - - const results = page.getByTestId("search-users-list"); - await expect(results).toBeVisible(); - await results.getByText("Amandine Salambo").click(); - - // The selected user is now a chip living inside the search field… - const chip = field.getByTestId("selected-user-item"); - await expect(chip).toBeVisible(); - await expect(chip).toContainText("Amandine Salambo"); - - // …along with the role selector and the invite button. - await expect(page.getByTestId("share-invite-button")).toBeVisible(); - await expect( - field.getByRole("button", { name: "Admin" }), - ).toBeVisible(); - }); - - test("a chip can be removed, hiding the invite action", async ({ - mount, - page, - }) => { - await mount(<TestShareModal />); - - const field = page.getByTestId("share-search-field"); - const input = field.locator(".c__share-modal__search-field__input"); - await input.fill("am"); - await page - .getByTestId("search-users-list") - .getByText("Amandine Salambo") - .click(); - - const chip = field.getByTestId("selected-user-item"); - await expect(chip).toBeVisible(); - - await chip.getByRole("button").click(); - - await expect(field.getByTestId("selected-user-item")).toHaveCount(0); - await expect(page.getByTestId("share-invite-button")).toHaveCount(0); - }); - - test("Backspace selects the last chip before removing it when the input is empty", async ({ - mount, - page, - }) => { - await mount(<TestShareModal />); - - const field = page.getByTestId("share-search-field"); - const input = field.locator(".c__share-modal__search-field__input"); - - await input.fill("am"); - await page - .getByTestId("search-users-list") - .getByText("Amandine Salambo") - .click(); - - await input.fill("ja"); - await page - .getByTestId("search-users-list") - .getByText("Jakob Philips") - .click(); - - const chips = field.getByTestId("selected-user-item"); - const amandineChip = chips.filter({ hasText: "Amandine Salambo" }); - const jakobChip = chips.filter({ hasText: "Jakob Philips" }); - - await expect(input).toHaveValue(""); - await expect(chips).toHaveCount(2); - - await input.press("Backspace"); - - await expect(chips).toHaveCount(2); - await expect(jakobChip).toHaveAttribute("data-selected", "true"); - await expect(amandineChip).not.toHaveAttribute("data-selected", "true"); - - await input.press("Backspace"); - - await expect(jakobChip).toHaveCount(0); - await expect(amandineChip).toBeVisible(); - - await input.press("Backspace"); - await expect(amandineChip).toHaveAttribute("data-selected", "true"); - - await input.press("Backspace"); - await expect(field.getByTestId("selected-user-item")).toHaveCount(0); - await expect(page.getByTestId("share-invite-button")).toHaveCount(0); - }); - - test("the first row stays fixed while the input follows the current row", async ({ - mount, - page, - }) => { - await mount(<TestShareModal />); - - const field = page.getByTestId("share-search-field"); - const input = field.locator(".c__share-modal__search-field__input"); - const icon = field.locator(".c__share-modal__search-field__icon"); - const results = page.getByTestId("search-users-list"); - - await input.fill("am"); - await results.getByText("Amandine Salambo").click(); - - const firstChip = field - .getByTestId("selected-user-item") - .filter({ hasText: "Amandine Salambo" }); - const initialChipBox = await firstChip.boundingBox(); - const initialIconBox = await icon.boundingBox(); - const initialInputBox = await input.boundingBox(); - - for (const [query, name] of [ - ["ja", "Jakob Philips"], - ["ka", "Kaylynn George"], - ["be", "Beatrice Laurent"], - ["mo", "Mohamed Benali"], - ["ch", "Charlotte Dubois"], - ["al", "Alejandro Romero"], - ["so", "Sophie Moreau"], - ["cr", "Christopher Martin"], - ]) { - await input.fill(query); - await results.getByText(name).click(); - } - - const chips = field.getByTestId("selected-user-item"); - const chipRows = await chips.evaluateAll((elements) => - Array.from( - new Set(elements.map((element) => element.getBoundingClientRect().top)), - ), - ); - const wrappedChipBox = await firstChip.boundingBox(); - const wrappedIconBox = await icon.boundingBox(); - const wrappedInputBox = await input.boundingBox(); - - expect(chipRows.length).toBeGreaterThan(1); - expect(wrappedIconBox?.y).toBe(initialIconBox?.y); - expect(wrappedChipBox?.y).toBe(initialChipBox?.y); - expect(initialInputBox?.y).toBe(initialChipBox?.y); - expect(wrappedInputBox?.y).toBe(Math.max(...chipRows)); - }); -}); From 1cb96e3c15754df38319054701ff1950df254a3c Mon Sep 17 00:00:00 2001 From: Nathan Vasse <nathan.vasse@gmail.com> Date: Thu, 23 Jul 2026 16:59:30 +0200 Subject: [PATCH 8/8] =?UTF-8?q?=E2=9C=85(front)=20unpin=20the=20input=20ro?= =?UTF-8?q?w=20assertion=20in=20search=20field=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec asserted the input's y equals the last chip row exactly. On CI's Linux font rendering the chips are wider, the last row is full and the input wraps to its own row below, so the equality only held on chromium. Assert the input never stays pinned above the chip rows instead, which is the behaviour the test is really about. --- e2e/share-modal/share-modal-search-field.spec.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/e2e/share-modal/share-modal-search-field.spec.tsx b/e2e/share-modal/share-modal-search-field.spec.tsx index 4b942ce3..6ee04fd3 100644 --- a/e2e/share-modal/share-modal-search-field.spec.tsx +++ b/e2e/share-modal/share-modal-search-field.spec.tsx @@ -122,6 +122,9 @@ test.describe("ShareModal unified search field", () => { expect(wrappedIconBox?.y).toBe(initialIconBox?.y); expect(wrappedChipBox?.y).toBe(initialChipBox?.y); expect(initialInputBox?.y).toBe(initialChipBox?.y); - expect(wrappedInputBox?.y).toBe(Math.max(...chipRows)); + // The input follows the flow of the chips: it sits on the last chip row, + // or wraps just below it when that row is full — where exactly depends on + // the browser's font metrics, so only assert it never stays pinned above. + expect(wrappedInputBox!.y).toBeGreaterThanOrEqual(Math.max(...chipRows)); }); });