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/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/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; + +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; +type TestInvitation = InvitationData; +type TestAccess = AccessData; + +// 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; +} + +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(() => + makeMembers(membersCount), + ); + const [invitations, setInvitations] = useState(() => + makeInvitations(invitationsCount), + ); + const [searchUsersResult, setSearchUsersResult] = useState([]); + 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 ( + + record({ name: "close" })} + modalTitle={modalTitle} + canUpdate={canUpdate} + canView={canView} + cannotViewMessage={cannotViewMessage} + cannotViewChildren={ + withCannotViewChildren ? ( +

Request access

+ ) : 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((resolve) => { + window.__resolveImport = resolve; + }); + } + return importResult; + }} + importModalChildren={ + withImportModalChildren ? ( +

Import failed

+ ) : 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 ? Top link reach message : undefined + } + topLinkRoleMessage={ + withTopLinkMessages ? Top link role message : undefined + } + customTranslations={customTranslations as CustomTranslations} + outsideSearchContent={ + withFooter ? ( + record({ name: "copy-link" })} + onOk={() => record({ name: "ok" })} + /> + ) : undefined + } + /> +
+ ); +}; 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; - -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 ( - - undefined} - invitationRoles={[ - { label: "Admin", value: "admin" }, - { label: "Editor", value: "editor" }, - ]} - onSearchUsers={setSearch} - onInviteUser={() => undefined} - searchUsersResult={USERS} - accesses={[]} - invitations={[]} - /> - - ); -}; 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/e2e/share-import-modal/fixtures/contacts.xlsx b/e2e/share-import-modal/fixtures/contacts.xlsx new file mode 100644 index 00000000..e892d9ba Binary files /dev/null and b/e2e/share-import-modal/fixtures/contacts.xlsx differ diff --git a/e2e/share-import-modal/share-import-modal.spec.tsx b/e2e/share-import-modal/share-import-modal.spec.tsx new file mode 100644 index 00000000..152d62fa --- /dev/null +++ b/e2e/share-import-modal/share-import-modal.spec.tsx @@ -0,0 +1,256 @@ +import { test, expect } from "@playwright/experimental-ct-react"; +import type { Page } from "@playwright/test"; +import { fileURLToPath } from "url"; +import { TestShareImportModal } from "../helpers/mount-share-import-modal"; + +const fileInput = (page: Page) => 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/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( + + + , + ); + + 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( + + + , + ); + + 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( + + + , + ); + + await expect(page.getByTestId("members-list")).toBeVisible(); + await expect( + page.getByRole("button", { name: "Import contacts" }), + ).toHaveCount(0); + }); +}); 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(); + + 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(); + + 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(); + + 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( + , + ); + + 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( + , + ); + + await expect(page.getByText("Custom denied")).toBeVisible(); + }); + + test("empty mode renders neither search nor lists but keeps the footer", async ({ + mount, + page, + }) => { + await mount(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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( + , + ); + + 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(); + + 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(); + + 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(); + + 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(); + + await expect(page.getByText("Shared between 1 person")).toBeVisible(); + }); + + test("changing a role invokes onUpdateAccess and updates the row", async ({ + mount, + page, + }) => { + await mount(); + + 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(); + + // 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(); + + 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(); + + 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(); + + // 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(); + + 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(); + + 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(); + + 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..6ee04fd3 --- /dev/null +++ b/e2e/share-modal/share-modal-search-field.spec.tsx @@ -0,0 +1,130 @@ +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(); + + 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(); + + 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(); + + 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); + // 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)); + }); +}); 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); - - 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(); - - 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(); - - 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(); - - 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)); - }); -}); diff --git a/package.json b/package.json index b9612f4b..89866369 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", @@ -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/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/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/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/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 = ({ + } + 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/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/items/ShareLinkSettings.tsx b/src/components/share/modal/items/ShareLinkSettings.tsx index 1c64054f..45a350ce 100644 --- a/src/components/share/modal/items/ShareLinkSettings.tsx +++ b/src/components/share/modal/items/ShareLinkSettings.tsx @@ -16,6 +16,8 @@ import { PublicIcon, RestrictedIcon, } from "./share-modal-icons"; +import { IconSize } from ":/components/icon"; +import { ChevronDown, ChevronUp } from ":/icons"; const getIcon = (value: string | undefined) => { switch (value) { @@ -137,6 +139,7 @@ export const ShareLinkSettings = ({ > <Button variant="tertiary" + size="nano" icon={getIcon(selectedLinkReach)} iconPosition="left" data-testid="share-link-reach-dropdown-button" @@ -182,13 +185,14 @@ export const ShareLinkSettings = ({ > <Button variant="tertiary" + color="neutral" data-testid="share-link-role-dropdown-button" icon={ - <span className="material-icons"> - {linkRoleDropdown.isOpen - ? "arrow_drop_up" - : "arrow_drop_down"} - </span> + linkRoleDropdown.isOpen ? ( + <ChevronUp size={IconSize.SMALL} /> + ) : ( + <ChevronDown size={IconSize.SMALL} /> + ) } iconPosition="right" onClick={() => { @@ -196,7 +200,7 @@ export const ShareLinkSettings = ({ linkRoleDropdown.setIsOpen(!linkRoleDropdown.isOpen); } }} - size="small" + size="nano" > {selectedLinkRoleChoice?.label} </Button> 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/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 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; 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/src/index.ts b/src/index.ts index 0f721099..1c443ea5 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"; @@ -32,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 e1476504..3926d926 100644 --- a/src/library.scss +++ b/src/library.scss @@ -1,5 +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"; @@ -31,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"; @@ -43,7 +46,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"; 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 d6c64330..6e815c47 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" @@ -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"