├── README.md ├── src ├── components │ ├── form │ │ ├── input │ │ │ ├── index.scss │ │ │ └── input.stories.tsx │ │ ├── radio │ │ │ ├── index.scss │ │ │ └── radio.stories.tsx │ │ ├── select │ │ │ ├── index.scss │ │ │ └── select.stories.tsx │ │ ├── switch │ │ │ ├── index.scss │ │ │ └── switch.stories.tsx │ │ ├── checkbox │ │ │ ├── index.scss │ │ │ └── checkbox.stories.tsx │ │ ├── textarea │ │ │ ├── index.scss │ │ │ └── textarea.stories.tsx │ │ ├── index.tsx │ │ ├── label │ │ │ ├── index.tsx │ │ │ ├── label.tsx │ │ │ ├── WithLabel.tsx │ │ │ ├── label.stories.tsx │ │ │ ├── index.scss │ │ │ └── with-label.stories.tsx │ │ └── form.stories.tsx │ ├── badge │ │ ├── index.tsx │ │ ├── types.ts │ │ ├── badge.tsx │ │ ├── badge.stories.tsx │ │ └── index.scss │ ├── loader │ │ ├── index.tsx │ │ ├── Spinner.tsx │ │ ├── index.scss │ │ └── spinner.stories.tsx │ ├── layout │ │ ├── utils.ts │ │ ├── index.tsx │ │ ├── right-panel │ │ │ ├── RightPanel.tsx │ │ │ └── index.scss │ │ ├── left-panel │ │ │ ├── LeftPanel.tsx │ │ │ └── index.scss │ │ ├── header │ │ │ ├── index.scss │ │ │ └── Header.tsx │ │ └── index.scss │ ├── button │ │ ├── index.tsx │ │ ├── index.scss │ │ └── ProConnectButton.tsx │ ├── language │ │ ├── index.tsx │ │ ├── index.scss │ │ ├── language-picker.stories.tsx │ │ └── language-picker.tsx │ ├── share │ │ ├── access │ │ │ ├── index.tsx │ │ │ ├── index.scss │ │ │ ├── access-role-dropdown.stories.tsx │ │ │ └── AccessRoleDropdown.tsx │ │ ├── utils │ │ │ ├── index.tsx │ │ │ └── ShareModalCopyLinkFooter.tsx │ │ ├── modal │ │ │ ├── index.tsx │ │ │ └── items │ │ │ │ ├── index.tsx │ │ │ │ ├── index.scss │ │ │ │ ├── SearchUserItem.tsx │ │ │ │ ├── share-items.stories.tsx │ │ │ │ ├── ShareInvitationItem.tsx │ │ │ │ └── ShareMemberItem.tsx │ │ ├── index.ts │ │ ├── types.ts │ │ ├── users-invitation │ │ │ ├── index.scss │ │ │ └── InvitationUserSelectorList.tsx │ │ └── assets │ │ │ ├── public.svg │ │ │ └── lock_person.svg │ ├── tree-view │ │ ├── providers │ │ │ ├── index.tsx │ │ │ └── TreeContext.tsx │ │ ├── index.tsx │ │ ├── TreeViewSeparator.tsx │ │ ├── utils.tsx │ │ ├── stories │ │ │ ├── tree-view-example.scss │ │ │ └── logo-example.svg │ │ └── types.ts │ ├── tabs │ │ ├── index.tsx │ │ ├── types.ts │ │ ├── Tabs.tsx │ │ ├── tabs.stories.tsx │ │ └── index.scss │ ├── users │ │ ├── avatar │ │ │ ├── index.tsx │ │ │ ├── UserAvatar.tsx │ │ │ ├── utils.ts │ │ │ ├── utils.test.ts │ │ │ ├── avatar.stories.tsx │ │ │ └── index.scss │ │ ├── index.ts │ │ ├── rows │ │ │ ├── user-row.stories.tsx │ │ │ ├── index.scss │ │ │ └── UserRow.tsx │ │ └── menu │ │ │ └── index.stories.tsx │ ├── icon │ │ ├── index.tsx │ │ ├── types.ts │ │ ├── index.scss │ │ ├── utils.ts │ │ └── Icon.tsx │ ├── la-gaufre │ │ ├── index.scss │ │ ├── LaGaufre.stories.tsx │ │ ├── LaGaufre.tsx │ │ └── LaGaufreV2.stories.tsx │ ├── separator │ │ ├── index.tsx │ │ ├── VerticalSeparator.tsx │ │ ├── HorizontalSeparator.tsx │ │ ├── AbstractSeparator.tsx │ │ └── index.scss │ ├── tooltip │ │ └── index.scss │ ├── dropdown-menu │ │ ├── index.tsx │ │ ├── useDropdownMenu.tsx │ │ ├── types.ts │ │ ├── index.scss │ │ └── DropdownMenu.tsx │ ├── quick-search │ │ ├── index.tsx │ │ ├── QuickSearchItem.tsx │ │ ├── types.tsx │ │ ├── QuickSearchItemTemplate.tsx │ │ ├── index.scss │ │ ├── QuickSearch.tsx │ │ ├── QuickSearchInput.tsx │ │ ├── QuickSearchGroup.tsx │ │ ├── quick-search.stories.tsx │ │ └── quick-search-global-style.scss │ ├── footer │ │ ├── assets │ │ │ └── external-link.svg │ │ ├── Footer.stories.tsx │ │ ├── Footer.tsx │ │ └── index.scss │ ├── Provider │ │ └── Provider.tsx │ ├── dnd │ │ ├── Draggable.tsx │ │ └── Droppable.tsx │ ├── hero │ │ ├── Hero.stories.tsx │ │ ├── Hero.tsx │ │ └── index.scss │ ├── datagrid │ │ ├── index.scss │ │ ├── datagrid.stories.tsx │ │ └── resources │ │ │ └── databaseCars.json │ ├── modal │ │ ├── index.scss │ │ └── modal.stories.tsx │ └── filter │ │ ├── index.scss │ │ ├── Filter.tsx │ │ └── Filter.stories.tsx ├── styles │ ├── _variables.scss │ └── fonts.scss ├── index.scss ├── assets │ └── fonts │ │ └── Marianne │ │ ├── Marianne-Bold.woff │ │ ├── Marianne-Bold.woff2 │ │ ├── Marianne-Light.woff │ │ ├── Marianne-Thin.woff │ │ ├── Marianne-Thin.woff2 │ │ ├── Marianne-Light.woff2 │ │ ├── Marianne-Medium.woff │ │ ├── Marianne-Medium.woff2 │ │ ├── Marianne-Regular.woff │ │ ├── Marianne-Regular.woff2 │ │ ├── Marianne-ExtraBold.woff │ │ ├── Marianne-ExtraBold.woff2 │ │ ├── Marianne-Bold_Italic.woff │ │ ├── Marianne-Bold_Italic.woff2 │ │ ├── Marianne-Light_Italic.woff │ │ ├── Marianne-Light_Italic.woff2 │ │ ├── Marianne-Medium_Italic.woff │ │ ├── Marianne-Thin_Italic.woff │ │ ├── Marianne-Thin_Italic.woff2 │ │ ├── Marianne-Medium_Italic.woff2 │ │ ├── Marianne-Regular_Italic.woff │ │ ├── Marianne-Regular_Italic.woff2 │ │ ├── Marianne-ExtraBold_Italic.woff │ │ ├── Marianne-ExtraBold_Italic.woff2 │ │ └── Marianne-font.css ├── style-stories.scss ├── utils │ ├── children.tsx │ ├── get-ui-kit-themes-from-globals.ts │ └── objects.ts ├── locales │ ├── Locale.tsx │ ├── en-US.json │ └── fr-FR.json ├── hooks │ ├── useCustomTranslations.ts │ ├── useControllableState.ts │ ├── useResponsive.tsx │ └── useArrowRoving.ts ├── cunningham-custom-style.scss ├── types │ ├── translations.type-check.ts │ └── translations.ts ├── index.ts └── library.scss ├── common.d.ts ├── public ├── storybook │ ├── hero-image.png │ ├── logo-fichiers.svg │ ├── logo-uikit-dark.svg │ └── logo-uikit-default.svg └── vite.svg ├── .changeset └── config.json ├── .storybook ├── main.ts ├── preview-head.html ├── preview.tsx ├── manager.tsx └── theme.ts ├── CONTRIBUTING.md ├── eslint.config.js ├── .gitignore ├── tsconfig.json ├── LICENSE ├── vite.config.ts └── .github └── workflows ├── front-dependencies-installation.yml └── main.yml /README.md: -------------------------------------------------------------------------------- 1 | # design-system -------------------------------------------------------------------------------- /src/components/form/input/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/form/radio/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/form/select/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/form/switch/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/form/checkbox/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/form/textarea/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $header-height: 52px; 2 | -------------------------------------------------------------------------------- /src/components/badge/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./badge"; -------------------------------------------------------------------------------- /src/components/form/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./label"; 2 | -------------------------------------------------------------------------------- /src/components/loader/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./Spinner"; 2 | -------------------------------------------------------------------------------- /src/components/form/label/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./WithLabel"; 2 | -------------------------------------------------------------------------------- /src/components/layout/utils.ts: -------------------------------------------------------------------------------- 1 | export const headerHeight = 52; 2 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | @use "./library"; 2 | @use "cunningham-tokens"; 3 | -------------------------------------------------------------------------------- /src/components/button/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./ProConnectButton"; 2 | -------------------------------------------------------------------------------- /src/components/language/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./language-picker"; 2 | -------------------------------------------------------------------------------- /src/components/share/access/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./AccessRoleDropdown"; 2 | -------------------------------------------------------------------------------- /src/components/tree-view/providers/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./TreeContext"; 2 | -------------------------------------------------------------------------------- /src/components/share/utils/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./ShareModalCopyLinkFooter"; 2 | -------------------------------------------------------------------------------- /src/components/tabs/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./Tabs"; 2 | export * from "./types"; 3 | -------------------------------------------------------------------------------- /src/components/share/modal/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./ShareModal"; 2 | export * from "./items"; 3 | -------------------------------------------------------------------------------- /src/components/users/avatar/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./UserAvatar"; 2 | export * from "./utils"; 3 | -------------------------------------------------------------------------------- /common.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: string; 3 | export default content; 4 | } -------------------------------------------------------------------------------- /src/components/icon/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./Icon"; 2 | export * from "./types"; 3 | export * from "./utils"; 4 | -------------------------------------------------------------------------------- /src/components/la-gaufre/index.scss: -------------------------------------------------------------------------------- 1 | button.lasuite-gaufre-btn { 2 | box-shadow: inset 0 0 0 0 !important; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/separator/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./HorizontalSeparator"; 2 | export * from "./VerticalSeparator"; 3 | -------------------------------------------------------------------------------- /public/storybook/hero-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/public/storybook/hero-image.png -------------------------------------------------------------------------------- /src/components/tooltip/index.scss: -------------------------------------------------------------------------------- 1 | .c__tooltip__content { 2 | font-weight: 500; 3 | white-space: break-spaces; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/users/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./avatar"; 2 | export * from "./rows/UserRow"; 3 | export * from "./menu"; 4 | -------------------------------------------------------------------------------- /src/components/language/index.scss: -------------------------------------------------------------------------------- 1 | .c__language-picker { 2 | display: flex; 3 | align-items: center; 4 | gap: 0.5rem; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/dropdown-menu/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./DropdownMenu"; 2 | export * from "./types"; 3 | export * from "./useDropdownMenu"; 4 | -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Bold.woff -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Bold.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Light.woff -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Thin.woff -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Thin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Light.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Medium.woff -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Medium.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Regular.woff -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Regular.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-ExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-ExtraBold.woff -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-ExtraBold.woff2 -------------------------------------------------------------------------------- /src/components/tabs/types.ts: -------------------------------------------------------------------------------- 1 | export type TabData = { 2 | id: string; 3 | label: string; 4 | icon?: string; 5 | content: React.ReactNode; 6 | }; 7 | -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Bold_Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Bold_Italic.woff -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Bold_Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Bold_Italic.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Light_Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Light_Italic.woff -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Light_Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Light_Italic.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Medium_Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Medium_Italic.woff -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Thin_Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Thin_Italic.woff -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Thin_Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Thin_Italic.woff2 -------------------------------------------------------------------------------- /src/components/share/modal/items/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./ShareInvitationItem"; 2 | export * from "./ShareMemberItem"; 3 | export * from "./SearchUserItem"; 4 | -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Medium_Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Medium_Italic.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Regular_Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Regular_Italic.woff -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-Regular_Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-Regular_Italic.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-ExtraBold_Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-ExtraBold_Italic.woff -------------------------------------------------------------------------------- /src/assets/fonts/Marianne/Marianne-ExtraBold_Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suitenumerique/ui-kit/HEAD/src/assets/fonts/Marianne/Marianne-ExtraBold_Italic.woff2 -------------------------------------------------------------------------------- /src/components/badge/types.ts: -------------------------------------------------------------------------------- 1 | export type BadgeType = 2 | | "accent" 3 | | "neutral" 4 | | "danger" 5 | | "success" 6 | | "warning" 7 | | "info"; -------------------------------------------------------------------------------- /src/styles/fonts.scss: -------------------------------------------------------------------------------- 1 | @use "../assets/fonts/Marianne/Marianne-font.css"; 2 | @use "@gouvfr-lasuite/cunningham-react/sass/icons"; 3 | @use "@fontsource/material-icons"; 4 | -------------------------------------------------------------------------------- /src/components/layout/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./header/Header"; 2 | export * from "./left-panel/LeftPanel"; 3 | export * from "./MainLayout"; 4 | export * from "./utils"; 5 | -------------------------------------------------------------------------------- /src/components/dropdown-menu/useDropdownMenu.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export const useDropdownMenu = () => { 4 | const [isOpen, setIsOpen] = useState(false); 5 | return { isOpen, setIsOpen }; 6 | }; 7 | -------------------------------------------------------------------------------- /src/components/share/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./users-invitation/InvitationUserSelectorList"; 2 | export * from "./access/AccessRoleDropdown"; 3 | export * from "./modal"; 4 | export * from "./types"; 5 | export * from "./utils"; -------------------------------------------------------------------------------- /src/components/loader/Spinner.tsx: -------------------------------------------------------------------------------- 1 | type SpinnerProps = { 2 | size?: "sm" | "md" | "lg" | "xl"; 3 | }; 4 | 5 | export const Spinner = ({ size = "sm" }: SpinnerProps) => { 6 | return
; 7 | }; 8 | -------------------------------------------------------------------------------- /src/style-stories.scss: -------------------------------------------------------------------------------- 1 | @use "./components/tree-view/stories/tree-view-example"; 2 | 3 | .left-panel-story { 4 | height: 100dvh; 5 | width: 300px; 6 | 7 | @media screen and (max-width: 1024px) { 8 | width: 100dvw; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/quick-search/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./QuickSearch"; 2 | export * from "./QuickSearchGroup"; 3 | export * from "./QuickSearchItem"; 4 | export * from "./QuickSearchInput"; 5 | export * from "./QuickSearchItemTemplate"; 6 | export * from "./types"; 7 | -------------------------------------------------------------------------------- /src/components/footer/assets/external-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/icon/types.ts: -------------------------------------------------------------------------------- 1 | export enum IconSize { 2 | X_SMALL = "xsmall", 3 | SMALL = "small", 4 | MEDIUM = "medium", 5 | LARGE = "large", 6 | X_LARGE = "xlarge", 7 | } 8 | 9 | export enum IconType { 10 | OUTLINED = "outlined", 11 | FILLED = "filled", 12 | }; -------------------------------------------------------------------------------- /src/utils/children.tsx: -------------------------------------------------------------------------------- 1 | import { Children, ReactNode } from "react"; 2 | 3 | export const hasChildrens = (element: ReactNode): boolean => { 4 | let hasChildren = false; 5 | Children.forEach(element, (child: ReactNode) => { 6 | hasChildren = hasChildren || !!child; 7 | }); 8 | return hasChildren; 9 | }; 10 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /src/components/separator/VerticalSeparator.tsx: -------------------------------------------------------------------------------- 1 | import { _AbstractSeparator, AbstractSeparatorProps } from "./AbstractSeparator"; 2 | 3 | type Props = Omit; 4 | 5 | export const VerticalSeparator = (props: Props) => { 6 | return ( 7 | <_AbstractSeparator direction="vertical" {...props} /> 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/Provider/Provider.tsx: -------------------------------------------------------------------------------- 1 | import { locales } from ":/locales/Locale"; 2 | import { CunninghamProvider as OriginalProvider } from "@gouvfr-lasuite/cunningham-react"; 3 | 4 | export const CunninghamProvider = ( 5 | props: Parameters[0] 6 | ) => { 7 | return ; 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/separator/HorizontalSeparator.tsx: -------------------------------------------------------------------------------- 1 | import { _AbstractSeparator, AbstractSeparatorProps } from "./AbstractSeparator"; 2 | 3 | type Props = Omit; 4 | 5 | export const HorizontalSeparator = (props: Props) => { 6 | return ( 7 | <_AbstractSeparator direction="horizontal" {...props} /> 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/dropdown-menu/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export type DropdownMenuOption = { 4 | label: string; 5 | icon?: ReactNode; 6 | callback?: () => void | Promise; 7 | isDisabled?: boolean; 8 | showSeparator?: boolean; 9 | isHidden?: boolean; 10 | isChecked?: boolean; 11 | testId?: string; 12 | value?: string; 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/tree-view/index.tsx: -------------------------------------------------------------------------------- 1 | import { TreeApi } from "react-arborist"; 2 | 3 | export * from "./TreeView"; 4 | export * from "./TreeViewSeparator"; 5 | export * from "./TreeViewItem"; 6 | export * from "./types"; 7 | export * from "./utils"; 8 | export * from "./useTree"; 9 | export * from "./providers"; 10 | export { TreeApi }; 11 | export type { NodeRendererProps } from "react-arborist"; 12 | -------------------------------------------------------------------------------- /src/locales/Locale.tsx: -------------------------------------------------------------------------------- 1 | import {enUS as originalEnUS, frFR as originalFrFR} from "@gouvfr-lasuite/cunningham-react"; 2 | import { default as enUS } from "./en-US.json"; 3 | import { default as frFR } from "./fr-FR.json"; 4 | import { deepMerge } from "../utils/objects"; 5 | 6 | export const locales = { 7 | "en-US": deepMerge(originalEnUS, enUS), 8 | "fr-FR": deepMerge(originalFrFR, frFR), 9 | } 10 | -------------------------------------------------------------------------------- /src/components/tree-view/TreeViewSeparator.tsx: -------------------------------------------------------------------------------- 1 | import { CursorProps } from "react-arborist"; 2 | 3 | export const TreeViewSeparator = ({ top, left }: CursorProps) => { 4 | return ( 5 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/share/modal/items/index.scss: -------------------------------------------------------------------------------- 1 | .c__share-member-item { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | border-radius: 10px; 6 | 7 | &__right { 8 | display: flex; 9 | align-items: center; 10 | } 11 | } 12 | 13 | .c__search-user-item-right { 14 | display: flex; 15 | align-items: center; 16 | color: var(--c--contextuals--content--semantic--brand--tertiary); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/quick-search/QuickSearchItem.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from "cmdk"; 2 | import { PropsWithChildren } from "react"; 3 | 4 | type Props = { 5 | onSelect?: (value: string) => void; 6 | id?: string; 7 | }; 8 | export const QuickSearchItem = ({ 9 | children, 10 | onSelect, 11 | id, 12 | }: PropsWithChildren) => { 13 | return ( 14 | 15 | {children} 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/la-gaufre/LaGaufre.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { LaGaufre } from "./LaGaufre"; 4 | 5 | const meta = { 6 | title: "Components/LaGaufre", 7 | component: LaGaufre, 8 | tags: ["autodocs"], 9 | parameters: { 10 | layout: "fullscreen", 11 | }, 12 | } satisfies Meta; 13 | 14 | export default meta; 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = {}; 18 | -------------------------------------------------------------------------------- /src/components/la-gaufre/LaGaufre.tsx: -------------------------------------------------------------------------------- 1 | import { Gaufre } from "@gouvfr-lasuite/integration"; 2 | import "@gouvfr-lasuite/integration/dist/css/gaufre.css"; 3 | 4 | export const LaGaufre = () => { 5 | return ( 6 | <> 7 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/form/label/label.tsx: -------------------------------------------------------------------------------- 1 | import { LabelHTMLAttributes } from "react"; 2 | 3 | export const Label = ({ 4 | children, 5 | text, 6 | ...props 7 | }: LabelHTMLAttributes & { text?: string }) => { 8 | return ( 9 |
10 | 11 | {text && ( 12 | {text} 13 | )} 14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/icon/index.scss: -------------------------------------------------------------------------------- 1 | .material-icons.icon--filled { 2 | font-family: "Material Icons", sans-serif; 3 | } 4 | 5 | // Icon sizes 6 | .material-icons.icon--xsmall { 7 | font-size: 11px; 8 | } 9 | 10 | .material-icons.icon--small { 11 | font-size: 16px; 12 | } 13 | 14 | .material-icons.icon--medium { 15 | font-size: 24px; 16 | } 17 | 18 | .material-icons.icon--large { 19 | font-size: 32px; 20 | } 21 | 22 | .material-icons.icon--xlarge { 23 | font-size: 48px; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/share/types.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export type InvitationData = T & { 4 | id: string; 5 | role: string; 6 | email: string; 7 | user: UserData; 8 | }; 9 | 10 | 11 | export type AccessData = T & { 12 | id: string; 13 | role: string; 14 | user: UserData; 15 | }; 16 | 17 | 18 | export type UserData = T & { 19 | id: string; 20 | full_name: string; 21 | email: string; 22 | }; 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/share/access/index.scss: -------------------------------------------------------------------------------- 1 | .c__access-role-dropdown { 2 | display: flex; 3 | align-items: center; 4 | gap: var(--c--globals--spacings--4xs); 5 | user-select: none; 6 | font-weight: 500; 7 | 8 | &__role-label { 9 | color: var(--c--contextuals--content--semantic--brand--tertiary); 10 | } 11 | 12 | &__icon { 13 | color: var(--c--contextuals--content--semantic--brand--tertiary); 14 | } 15 | 16 | &__role-label-can-not-update { 17 | color: var(--c--contextuals--content--semantic--neutral--secondary); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/button/index.scss: -------------------------------------------------------------------------------- 1 | .pro-connect-button { 2 | background-position: 50% 50%; 3 | background-repeat: no-repeat; 4 | width: 214px; 5 | height: 56px !important; 6 | border: "none"; 7 | cursor: pointer; 8 | 9 | background-color: var(--c--contextuals--background--semantic--brand--primary); 10 | 11 | &:hover { 12 | background-color: var(--c--contextuals--background--semantic--brand--primary-hover); 13 | } 14 | 15 | &:disabled { 16 | background-color: var( 17 | --c--contextuals--background--semantic--disabled--secondary 18 | ) !important; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | 3 | const config: StorybookConfig = { 4 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 5 | addons: [ 6 | "@storybook/addon-onboarding", 7 | "@storybook/addon-essentials", 8 | "@chromatic-com/storybook", 9 | "@storybook/addon-interactions", 10 | "@storybook/addon-a11y", 11 | ], 12 | 13 | framework: { 14 | name: "@storybook/react-vite", 15 | options: {}, 16 | }, 17 | 18 | staticDirs: ["../src/assets/fonts/Marianne"], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Release 4 | 5 | 1. Run `yarn build` 6 | 7 | 2. Update CHANGELOG.md according to the Major / Minor / Patch semver convention. 8 | 9 | 3. Based on semver upgrade the package version in `package.json`. 10 | 11 | 4. Commit the changes and create a PR named "🔖(release) version package". 12 | 13 | 5. Ask for approval, once the PR is approved, merge it. 14 | 15 | 6. Once merged, run `npx @changesets/cli publish`. It will publish the new version of the package to NPM and create a git tag. 16 | 17 | 7. Run `git push origin ` 18 | 19 | 8. Tell everyone 🎉 ! 20 | -------------------------------------------------------------------------------- /src/components/form/label/WithLabel.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { PropsWithChildren } from "react"; 3 | import { Label } from "./label"; 4 | 5 | export type WithLabelProps = { 6 | label: string; 7 | text?: string; 8 | labelSide?: "left" | "right"; 9 | }; 10 | 11 | export const WithLabel = ({ 12 | label, 13 | text, 14 | labelSide = "right", 15 | children, 16 | }: PropsWithChildren) => { 17 | return ( 18 |
19 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/quick-search/types.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export type QuickSearchAction = { 4 | onSelect?: () => void; // for the keyboard selection with = { 9 | groupName: string; // The name of the group 10 | elements: T[]; // The elements to display 11 | emptyString?: string; // If no elements, show this string 12 | startActions?: QuickSearchAction[]; // Before all elements 13 | endActions?: QuickSearchAction[]; // After all elements 14 | showWhenEmpty?: boolean; // If no elements, show this group 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/dnd/Draggable.tsx: -------------------------------------------------------------------------------- 1 | import { Data, useDraggable } from "@dnd-kit/core"; 2 | 3 | type DraggableProps = { 4 | id: string; 5 | data?: Data; 6 | children: React.ReactNode; 7 | }; 8 | 9 | export const Draggable = (props: DraggableProps) => { 10 | const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ 11 | id: props.id, 12 | data: props.data, 13 | }); 14 | 15 | const style = { 16 | opacity: isDragging ? 0.5 : 1, 17 | }; 18 | 19 | return ( 20 |
21 | {props.children} 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/layout/right-panel/RightPanel.tsx: -------------------------------------------------------------------------------- 1 | import { useResponsive } from ":/hooks/useResponsive"; 2 | import clsx from "clsx"; 3 | import { PropsWithChildren } from "react"; 4 | 5 | export type RightPanelProps = { 6 | isOpen?: boolean; 7 | }; 8 | export const RightPanel = ({ 9 | children, 10 | isOpen, 11 | }: PropsWithChildren) => { 12 | const { isDesktop } = useResponsive(); 13 | 14 | if (!isDesktop) { 15 | return ( 16 |
17 | {children} 18 |
19 | ); 20 | } 21 | 22 | return ( 23 |
{children}
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/button/ProConnectButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@gouvfr-lasuite/cunningham-react"; 2 | import logo from ":/assets/proconnect-content.svg"; 3 | import logoDisabled from ":/assets/proconnect-content-disabled.svg"; 4 | 5 | export type ProConnectButtonProps = { 6 | disabled?: boolean; 7 | onClick?: () => void; 8 | }; 9 | export const ProConnectButton = ({ 10 | disabled, 11 | onClick, 12 | }: ProConnectButtonProps) => { 13 | return ( 14 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/utils/get-ui-kit-themes-from-globals.ts: -------------------------------------------------------------------------------- 1 | import { getThemesFromGlobals } from "@gouvfr-lasuite/cunningham-tokens"; 2 | import { deepMerge } from "./objects"; 3 | import { commonTokenOverrides, commonGlobals } from "../../cunningham"; 4 | 5 | 6 | 7 | // Utils functions to create a theme from a set of globals with UIKit overrides applied by default 8 | export const getUIKitThemesFromGlobals = (globals: Parameters[0], options: Parameters[1] = {}) => { 9 | return getThemesFromGlobals( 10 | deepMerge(commonGlobals, globals!), { 11 | ...options, 12 | overrides: deepMerge(commonTokenOverrides, options.overrides ?? {}), 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | }, 23 | }, 24 | ) 25 | -------------------------------------------------------------------------------- /src/components/icon/utils.ts: -------------------------------------------------------------------------------- 1 | import { IconSize } from "./types"; 2 | 3 | export const iconSizeMap: Record = { 4 | [IconSize.X_SMALL]: 11, 5 | [IconSize.SMALL]: 16, 6 | [IconSize.MEDIUM]: 24, 7 | [IconSize.LARGE]: 32, 8 | [IconSize.X_LARGE]: 40, 9 | }; 10 | 11 | export const containerSizeMap: Record = { 12 | [IconSize.X_SMALL]: 16, 13 | [IconSize.SMALL]: 24, 14 | [IconSize.MEDIUM]: 32, 15 | [IconSize.LARGE]: 40, 16 | [IconSize.X_LARGE]: 48, 17 | }; 18 | 19 | export const getContainerSize = (iconSize: IconSize): number => { 20 | return containerSizeMap[iconSize] ?? 24; 21 | }; 22 | 23 | 24 | export const getIconSize = (iconSize: IconSize): number => { 25 | return iconSizeMap[iconSize] ?? 24; 26 | }; -------------------------------------------------------------------------------- /src/components/badge/badge.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { HTMLAttributes, PropsWithChildren } from "react"; 3 | import { BadgeType } from "./types"; 4 | 5 | type BadgeProps = HTMLAttributes & { 6 | uppercased?: boolean; 7 | type?: BadgeType; 8 | }; 9 | 10 | export const Badge = ({ 11 | children, 12 | uppercased = false, 13 | type = "accent", 14 | className, 15 | ...props 16 | }: PropsWithChildren) => { 17 | return ( 18 |
25 | {children} 26 |
27 | ); 28 | }; 29 | 30 | export default Badge; 31 | -------------------------------------------------------------------------------- /src/components/users/rows/user-row.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { UserRow } from "./UserRow"; 3 | 4 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 5 | const meta = { 6 | title: "Components/users/Row", 7 | component: UserRow, 8 | tags: ["autodocs"], 9 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const Default: Story = { 16 | args: { 17 | fullName: "Gustave Eiffel", 18 | email: "gustave.eiffel@example.com", 19 | showEmail: true, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /src/components/form/textarea/textarea.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { TextArea } from "@gouvfr-lasuite/cunningham-react"; 4 | 5 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 6 | const meta = { 7 | title: "Components/Forms/TextArea", 8 | component: TextArea, 9 | tags: ["autodocs"], 10 | 11 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs 12 | } satisfies Meta; 13 | 14 | export default meta; 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = { 18 | args: { 19 | defaultValue: "Hello world", 20 | label: "Describe your job", 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/users/rows/index.scss: -------------------------------------------------------------------------------- 1 | .c__user-row { 2 | display: flex; 3 | align-items: center; 4 | gap: var(--c--globals--spacings--xs); 5 | 6 | &__name { 7 | font-size: 14px; 8 | font-weight: 500; 9 | line-height: 18px; 10 | color: var(--c--contextuals--content--semantic--neutral--primary); 11 | } 12 | 13 | &__info { 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | &__email { 19 | color: var(--c--contextuals--content--semantic--neutral--tertiary); 20 | font-size: 12px; 21 | line-height: 16px; 22 | 23 | &.has-only-email { 24 | font-weight: 500; 25 | font-size: var(--c--globals--font--sizes--sm); 26 | color: var(--c--contextuals--content--semantic--neutral--primary); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/dnd/Droppable.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | import { Data, useDroppable } from "@dnd-kit/core"; 3 | import { useEffect } from "react"; 4 | 5 | type DroppableProps = { 6 | id: string; 7 | onOver?: (isOver: boolean, data?: Data) => void; 8 | data?: Data; 9 | children: React.ReactNode; 10 | }; 11 | 12 | export const Droppable = (props: DroppableProps) => { 13 | const { isOver, setNodeRef } = useDroppable({ 14 | id: props.id, 15 | data: props.data, 16 | }); 17 | 18 | useEffect(() => { 19 | props.onOver?.(isOver, props.data); 20 | }, [isOver, props.data, props.onOver]); 21 | 22 | return ( 23 |
24 | {props.children} 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Distribution / packaging 2 | .Python 3 | build/ 4 | develop-eggs/ 5 | dist/ 6 | downloads/ 7 | eggs/ 8 | .eggs/ 9 | lib/ 10 | lib64/ 11 | parts/ 12 | sdist/ 13 | var/ 14 | wheels/ 15 | pip-wheel-metadata/ 16 | share/python-wheels/ 17 | *.egg-info/ 18 | .installed.cfg 19 | *.egg 20 | MANIFEST 21 | .DS_Store 22 | .next/ 23 | 24 | # Environments 25 | .venv 26 | env/ 27 | venv/ 28 | ENV/ 29 | env.bak/ 30 | venv.bak/ 31 | env.d/development/* 32 | !env.d/development/*.dist 33 | env.d/terraform 34 | 35 | # npm 36 | node_modules 37 | 38 | 39 | # Logs 40 | *.log 41 | 42 | 43 | 44 | # Test & lint 45 | .coverage 46 | .pylint.d 47 | .pytest_cache 48 | db.sqlite3 49 | .mypy_cache 50 | 51 | # Site media 52 | /data/ 53 | 54 | # IDEs 55 | .idea/ 56 | .vscode/ 57 | *.iml 58 | .devcontainer 59 | node_modules -------------------------------------------------------------------------------- /src/components/form/label/label.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { WithLabel } from "./WithLabel"; 4 | import { Label } from "./label"; 5 | 6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 7 | const meta = { 8 | title: "Components/Forms/Label", 9 | component: Label, 10 | tags: ["autodocs"], 11 | 12 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | export const Default: Story = { 19 | args: { 20 | children: "Label", 21 | text: "Description liée à ce label sur plusieurs lignes", 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/loader/index.scss: -------------------------------------------------------------------------------- 1 | .c__spinner { 2 | border-radius: 50%; 3 | background: radial-gradient(farthest-side, #cecece 94%, #0000) top/3.8px 3.8px 4 | no-repeat, 5 | conic-gradient(#0000 30%, #cecece); 6 | -webkit-mask: radial-gradient( 7 | farthest-side, 8 | #0000 calc(100% - 3.8px), 9 | #000 0 10 | ); 11 | animation: spinner-c7wet2 1s infinite linear; 12 | &.sm { 13 | width: 16px; 14 | height: 16px; 15 | } 16 | 17 | &.md { 18 | width: 24px; 19 | height: 24px; 20 | } 21 | 22 | &.lg { 23 | width: 32px; 24 | height: 32px; 25 | } 26 | 27 | &.xl { 28 | width: 40px; 29 | height: 40px; 30 | } 31 | } 32 | 33 | .spinner { 34 | } 35 | 36 | @keyframes spinner-c7wet2 { 37 | 100% { 38 | transform: rotate(1turn); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/objects.ts: -------------------------------------------------------------------------------- 1 | function isObject(item: unknown) { 2 | return item && typeof item === "object" && !Array.isArray(item); 3 | } 4 | 5 | 6 | export function deepMerge< 7 | T extends Record, 8 | S extends Record[] 9 | >(target: T, ...sources: S): T & S[number] { 10 | if (!sources.length) return target; 11 | const source = sources.shift(); 12 | 13 | if (isObject(target) && isObject(source)) { 14 | for (const key in source) { 15 | if (isObject(source[key])) { 16 | if (!target[key]) Object.assign(target, { [key]: {} }); 17 | deepMerge(target[key] as T, source[key] as S[number]); 18 | } else { 19 | Object.assign(target, { [key]: source[key] }); 20 | } 21 | } 22 | } 23 | return deepMerge(target, ...sources); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/language/language-picker.stories.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { Meta, StoryObj } from "@storybook/react"; 3 | import { LanguagePicker } from "./language-picker"; 4 | 5 | const meta: Meta = { 6 | title: "Components/LanguagePicker", 7 | component: LanguagePicker, 8 | tags: ["autodocs"], 9 | }; 10 | 11 | export default meta; 12 | type Story = StoryObj; 13 | 14 | const languages = [ 15 | { label: "Français", value: "fr-FR" }, 16 | { label: "English", value: "en-US" }, 17 | { label: "German", value: "de-DE" }, 18 | ]; 19 | 20 | export const Default: Story = { 21 | args: { 22 | languages, 23 | onChange: (value) => { 24 | alert(`Language changed to ${languages.find((lang) => lang.value === value)?.label}`); 25 | }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/layout/left-panel/LeftPanel.tsx: -------------------------------------------------------------------------------- 1 | import { useResponsive } from ":/hooks/useResponsive"; 2 | import clsx from "clsx"; 3 | import { PropsWithChildren } from "react"; 4 | 5 | export type LeftPanelProps = { 6 | isOpen?: boolean; 7 | hasHeader?: boolean; 8 | }; 9 | export const LeftPanel = ({ 10 | children, 11 | isOpen = false, 12 | hasHeader = true, 13 | }: PropsWithChildren) => { 14 | const { isDesktop } = useResponsive(); 15 | 16 | if (!isDesktop) { 17 | return ( 18 |
23 | {children} 24 |
25 | ); 26 | } 27 | 28 | return ( 29 |
30 | {children} 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/share/utils/ShareModalCopyLinkFooter.tsx: -------------------------------------------------------------------------------- 1 | import { Button, useCunningham } from "@gouvfr-lasuite/cunningham-react"; 2 | 3 | export type ShareModalCopyLinkFooterProps = { 4 | onCopyLink: () => void; 5 | onOk: () => void; 6 | }; 7 | 8 | export const ShareModalCopyLinkFooter = ({ 9 | onCopyLink, 10 | onOk, 11 | }: ShareModalCopyLinkFooterProps) => { 12 | const { t } = useCunningham(); 13 | return ( 14 |
15 | 22 | 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/tree-view/utils.tsx: -------------------------------------------------------------------------------- 1 | import { TreeViewDataType, TreeViewNodeTypeEnum } from "./types"; 2 | 3 | export const isSeparator = (node?: TreeViewDataType): boolean => { 4 | if (!node) { 5 | return false; 6 | } 7 | return node.nodeType === TreeViewNodeTypeEnum.SEPARATOR; 8 | }; 9 | 10 | export const isTitle = (node?: TreeViewDataType): boolean => { 11 | if (!node) { 12 | return false; 13 | } 14 | return node.nodeType === TreeViewNodeTypeEnum.TITLE; 15 | }; 16 | 17 | export const isViewMore = (node?: TreeViewDataType): boolean => { 18 | if (!node) { 19 | return false; 20 | } 21 | return node.nodeType === TreeViewNodeTypeEnum.VIEW_MORE; 22 | }; 23 | 24 | export const isNode = (node?: TreeViewDataType): boolean => { 25 | return !isSeparator(node) && !isTitle(node) && !isViewMore(node); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/hero/Hero.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Hero, HomeGutter } from "./Hero"; 4 | 5 | const meta = { 6 | title: "Components/Hero", 7 | component: Hero, 8 | tags: ["autodocs"], 9 | parameters: { 10 | layout: "fullscreen", 11 | }, 12 | render: (args) => ( 13 | 14 | 15 | 16 | ), 17 | } satisfies Meta; 18 | 19 | export default meta; 20 | type Story = StoryObj; 21 | 22 | export const Default: Story = { 23 | args: { 24 | title: "Stockage et partage faciles.", 25 | subtitle: 26 | "Stockez et partagez vos fichiers simplement dans un espace cloud collaboratif synchronisé.", 27 | logo: DocLogo, 28 | banner: "/storybook/hero-image.png", 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/datagrid/index.scss: -------------------------------------------------------------------------------- 1 | .c__datagrid__table__container { 2 | overflow: auto; 3 | } 4 | 5 | .c__datagrid__table__container > table th { 6 | height: 24px; 7 | } 8 | 9 | .c__datagrid__table__container > table thead tr { 10 | border: none; 11 | } 12 | 13 | .c__datagrid__table__container > table td { 14 | height: 40px; 15 | } 16 | 17 | .c__datagrid__table__container > table th .c__datagrid__header { 18 | color: var(--c--components--datagrid--header--color); 19 | font-weight: var(--c--components--datagrid--header--weight); 20 | font-size: var(--c--components--datagrid--header--size); 21 | text-transform: none; 22 | } 23 | 24 | .c__datagrid__table__container > table tbody tr { 25 | border: none; 26 | } 27 | 28 | .c__datagrid__table__container > table tbody tr:hover { 29 | background-color: var( 30 | --c--components--datagrid--body--background-color-hover 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/la-gaufre/LaGaufreV2.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { LaGaufre } from "./LaGaufre"; 4 | import { LaGaufreV2 } from "./LaGaufreV2"; 5 | 6 | const meta = { 7 | title: "Components/LaGaufreV2", 8 | component: LaGaufreV2, 9 | tags: ["autodocs"], 10 | parameters: { 11 | layout: "fullscreen", 12 | }, 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | export const Default: Story = { 19 | args: { 20 | widgetPath: "https://static.suite.anct.gouv.fr/widgets/lagaufre.js", 21 | apiUrl: "https://lasuite.numerique.gouv.fr/api/services", 22 | }, 23 | render: (args) => { 24 | return ( 25 |
28 | 29 |
30 | ); 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/layout/right-panel/index.scss: -------------------------------------------------------------------------------- 1 | .c__right-panel { 2 | height: calc(100dvh - 52px); 3 | width: 0; 4 | border-left: 1px solid var(--c--contextuals--border--surface--primary); 5 | transition: width 0.15s ease-in-out; 6 | overflow-y: auto; 7 | transition: width 0.2s cubic-bezier(0.4, 0, 0.2, 1); 8 | background-color: var(--c--contextuals--background--surface--secondary); 9 | &.open { 10 | width: 300px; 11 | } 12 | } 13 | 14 | .c__right-panel__mobile { 15 | z-index: 998; 16 | width: 100dvw; 17 | height: calc(100dvh - 52px); 18 | overflow-y: auto; 19 | position: fixed; 20 | background-color: var(--c--contextuals--background--surface--secondary); 21 | transition: transform 0.15s ease-in-out; 22 | transform: translateX(100dvw); 23 | 24 | // Animation for both opening and closing 25 | transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); 26 | 27 | &.open { 28 | transform: translateX(0); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/loader/spinner.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Spinner } from "./Spinner"; 4 | 5 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 6 | const meta = { 7 | title: "Components/Spinner", 8 | component: Spinner, 9 | tags: ["autodocs"], 10 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Small: Story = { 17 | args: { 18 | size: "sm", 19 | }, 20 | }; 21 | export const Medium: Story = { 22 | args: { 23 | size: "md", 24 | }, 25 | }; 26 | 27 | export const Large: Story = { 28 | args: { 29 | size: "lg", 30 | }, 31 | }; 32 | 33 | export const XLarge: Story = { 34 | args: { 35 | size: "xl", 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/share/modal/items/SearchUserItem.tsx: -------------------------------------------------------------------------------- 1 | import { UserRow } from ":/components/users/rows/UserRow"; 2 | import { useCunningham } from "@gouvfr-lasuite/cunningham-react"; 3 | import { UserData } from ":/components/share/types.ts"; 4 | import { QuickSearchItemTemplate } from ":/components/quick-search"; 5 | 6 | type SearchUserItemProps = { 7 | user: UserData; 8 | }; 9 | 10 | export const SearchUserItem = ({ 11 | user, 12 | }: SearchUserItemProps) => { 13 | const { t } = useCunningham(); 14 | 15 | return ( 16 | } 18 | alwaysShowRight={false} 19 | right={ 20 |
21 | {t("components.share.item.add")} 22 | add 23 |
24 | } 25 | /> 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/users/avatar/UserAvatar.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { getUserInitials, getUserColor, AVATAR_COLORS } from "./utils"; 3 | 4 | export type AvatarProps = { 5 | fullName: string; 6 | size?: "xsmall" | "small" | "medium" | "large"; 7 | forceColor?: string; 8 | backgroundColor?: (typeof AVATAR_COLORS)[number]; 9 | }; 10 | 11 | export const UserAvatar = ({ 12 | fullName, 13 | size = "medium", 14 | forceColor, 15 | backgroundColor: color, 16 | }: AvatarProps) => { 17 | const initials = getUserInitials(fullName); 18 | const backgroundColor = color ? color : getUserColor(fullName); 19 | return ( 20 |
26 |
{initials}
27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/form/label/index.scss: -------------------------------------------------------------------------------- 1 | .c__with-label { 2 | display: flex; 3 | width: fit-content; 4 | align-items: center; 5 | gap: var(--c--globals--spacings--xs); 6 | padding: var(--c--globals--spacings--3xs); 7 | 8 | &.left { 9 | flex-direction: row-reverse; 10 | } 11 | 12 | &:hover { 13 | cursor: pointer; 14 | background-color: var( 15 | --c--contextuals--background--semantic--neutral--tertiary-hover 16 | ); 17 | } 18 | } 19 | 20 | .suite__label__container { 21 | display: flex; 22 | flex-direction: column; 23 | 24 | label { 25 | font-size: var(--c--globals--font--sizes--sm); 26 | font-weight: var(--c--globals--font--weights--medium); 27 | color: var(--c--contextuals--content--semantic--neutral--primary); 28 | } 29 | 30 | &__description { 31 | font-weight: 400; 32 | font-size: var(--c--globals--font--sizes--s); 33 | color: var(--c--contextuals--content--semantic--neutral--secondary); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/users/avatar/utils.ts: -------------------------------------------------------------------------------- 1 | export const AVATAR_COLORS = [ 2 | "gray", 3 | "brand", 4 | "red", 5 | "orange", 6 | "brown", 7 | "green", 8 | "blue-1", 9 | "blue-2", 10 | "pink", 11 | "yellow", 12 | "purple", 13 | ]; 14 | 15 | 16 | /** 17 | * Split the name into parts and return the first two initials 18 | */ 19 | export const getUserInitials = (name: string) => { 20 | // If there are more than 2 words, take only the first two ones 21 | return name 22 | .split(/[\s-_]+/) 23 | .slice(0, 2) 24 | .map((n) => n[0]) 25 | .join("") 26 | .toUpperCase(); 27 | }; 28 | 29 | /** 30 | * Get a consistent avatar's color according to a hash of the name 31 | */ 32 | export const getUserColor = (name: string) => { 33 | let sum = 0; 34 | for (let i = 0; i < name.length; i++) { 35 | sum += name.charCodeAt(i); 36 | } 37 | 38 | return AVATAR_COLORS[sum % AVATAR_COLORS.length]; 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/users/rows/UserRow.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { UserAvatar } from "../avatar/UserAvatar"; 3 | 4 | type UserProps = { 5 | fullName?: string; 6 | email?: string; 7 | showEmail?: boolean; 8 | }; 9 | 10 | export const UserRow = ({ 11 | fullName: full_name, 12 | email, 13 | showEmail = true, 14 | }: UserProps) => { 15 | const name = full_name && full_name !== "" ? full_name : email ?? ""; 16 | return ( 17 |
18 | 19 |
20 | {full_name} 21 | {showEmail && email && ( 22 | 27 | {email} 28 | 29 | )} 30 |
31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/hero/Hero.tsx: -------------------------------------------------------------------------------- 1 | import { ProConnectButton } from "../button/ProConnectButton"; 2 | 3 | export const Hero = ({ 4 | logo, 5 | mainButton, 6 | banner, 7 | title, 8 | subtitle, 9 | }: { 10 | logo: React.ReactNode; 11 | mainButton?: React.ReactNode; 12 | banner: string; 13 | title: string; 14 | subtitle: string; 15 | }) => { 16 | return ( 17 |
18 |
19 |
20 | {logo} 21 |

{title}

22 | 23 | {subtitle} 24 | 25 | {mainButton ? mainButton : } 26 |
27 | 28 |
29 |
30 | ); 31 | }; 32 | 33 | export const HomeGutter = ({ children }: { children: React.ReactNode }) => { 34 | return
{children}
; 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/separator/AbstractSeparator.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { CSSProperties } from "react"; 3 | 4 | export type AbstractSeparatorProps = { 5 | withPadding?: boolean; 6 | direction: "horizontal" | "vertical"; 7 | width?: "thin" | "double"; 8 | size?: string; 9 | }; 10 | 11 | /** 12 | * An abstract component which display a vertical or horizontal separator. 13 | * It should not be used directly, but rather extended by the concrete implementations. 14 | * See `HorizontalSeparator` and `VerticalSeparator` for concrete implementations. 15 | */ 16 | export const _AbstractSeparator = ({ withPadding = true, direction, width = "thin", size }: AbstractSeparatorProps) => { 17 | return
; 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/quick-search/QuickSearchItemTemplate.tsx: -------------------------------------------------------------------------------- 1 | import { useResponsive } from ":/hooks/useResponsive"; 2 | import clsx from "clsx"; 3 | import { ReactNode } from "react"; 4 | 5 | export type QuickSearchItemTemplateProps = { 6 | alwaysShowRight?: boolean; 7 | left: ReactNode; 8 | right?: ReactNode; 9 | }; 10 | 11 | export const QuickSearchItemTemplate = ({ 12 | alwaysShowRight = false, 13 | left, 14 | right, 15 | }: QuickSearchItemTemplateProps) => { 16 | const { isDesktop } = useResponsive(); 17 | return ( 18 |
19 |
{left}
20 | 21 | {isDesktop && right && ( 22 |
28 | {right} 29 |
30 | )} 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/separator/index.scss: -------------------------------------------------------------------------------- 1 | .c__separator { 2 | --border-width: 1px; 3 | --padding: var(--c--globals--spacings--sm, 0.75rem); 4 | background-color: var(--c--contextuals--border--surface--primary); 5 | } 6 | 7 | .c__separator--double { 8 | --border-width: 2px; 9 | } 10 | 11 | .c__separator--horizontal { 12 | height: var(--border-width); 13 | width: var(--size, 100%); 14 | 15 | &.with-padding { 16 | margin-block: var(--padding); 17 | } 18 | } 19 | 20 | .c__separator--vertical { 21 | width: var(--border-width); 22 | /* Let flexbox compute cross-size; ensures full container height in row flex */ 23 | align-self: stretch; 24 | /* Also work inline by default */ 25 | display: inline-block; 26 | height: var(--size, auto); 27 | min-height: 1.5em; 28 | vertical-align: middle; 29 | 30 | &.with-padding { 31 | margin-inline: var(--padding); 32 | } 33 | } 34 | 35 | /* When --size is defined, don't stretch */ 36 | .c__separator--vertical[style*="--size"] { 37 | align-self: auto; 38 | } 39 | -------------------------------------------------------------------------------- /public/storybook/logo-fichiers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/hooks/useCustomTranslations.ts: -------------------------------------------------------------------------------- 1 | import { useCunningham } from "@gouvfr-lasuite/cunningham-react"; 2 | import type { TranslationKey } from "../types/translations"; 3 | 4 | /** 5 | * Type for custom translations - maps translation keys to their string values 6 | */ 7 | export type CustomTranslations = Partial>; 8 | 9 | /** 10 | * Hook for using custom translations with type safety. 11 | * 12 | * This hook provides a translation function that first checks custom translations, 13 | * then falls back to the default Cunningham translations. 14 | * 15 | * Note: This is not a bullet proof solution, it doesn't handle variables replacement 16 | * as Cunningham does for now. 17 | * 18 | * @param customTranslations - Optional object mapping translation keys to custom values 19 | * @returns Object containing the translation function 20 | */ 21 | export const useCustomTranslations = ( 22 | customTranslations?: CustomTranslations 23 | ) => { 24 | const { t } = useCunningham(); 25 | return { 26 | t: (key: TranslationKey) => customTranslations?.[key] ?? t(key), 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/form/select/select.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Select } from "@gouvfr-lasuite/cunningham-react"; 4 | 5 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 6 | const meta = { 7 | title: "Components/Forms/Select", 8 | component: Select, 9 | tags: ["autodocs"], 10 | 11 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs 12 | } satisfies Meta; 13 | 14 | export default meta; 15 | type Story = StoryObj; 16 | 17 | const CITIES = [ 18 | "Paris", 19 | "Marseille", 20 | "Lyon", 21 | "Toulouse", 22 | "Nice", 23 | "Nantes", 24 | "Strasbourg", 25 | "Montpellier", 26 | "Bordeaux", 27 | "Lille", 28 | ]; 29 | const OPTIONS = CITIES.map((city) => ({ 30 | label: city, 31 | value: city.toLowerCase(), 32 | })); 33 | 34 | export const Uncontrolled: Story = { 35 | args: { 36 | label: "Select a city", 37 | options: OPTIONS, 38 | defaultValue: OPTIONS[4].value, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noErrorTruncation": false, // Turn on to see the full error message 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "lib": [ 7 | "dom", 8 | "dom.iterable", 9 | "es6", 10 | "scripthost", 11 | "es2015", 12 | "es2016", 13 | "es2017", 14 | "es2021.string", 15 | "esnext.intl" 16 | ], 17 | "moduleResolution": "bundler", 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true, 20 | "strict": true, 21 | "jsx": "react-jsx", 22 | "module": "esnext", 23 | "sourceMap": true, 24 | "target": "es6", 25 | /* Bundler mode */ 26 | "allowImportingTsExtensions": true, 27 | "isolatedModules": true, 28 | "noEmit": true, 29 | "paths": { 30 | ":/*": ["./src/*"] 31 | }, 32 | "typeRoots": ["./dist/index.d.ts"], 33 | 34 | "noUnusedLocals": true, 35 | "noUnusedParameters": true, 36 | "noFallthroughCasesInSwitch": true 37 | }, 38 | "include": ["src", "cunningham.ts", "common.d.ts"], 39 | "exclude": ["node_modules", "dist"], 40 | } 41 | -------------------------------------------------------------------------------- /src/components/layout/left-panel/index.scss: -------------------------------------------------------------------------------- 1 | .c__left-panel { 2 | display: flex; 3 | overflow-y: auto; 4 | 5 | &.has-header { 6 | height: calc(100dvh - 52px); 7 | } 8 | 9 | &:not(.has-header) { 10 | height: 100dvh; 11 | } 12 | 13 | flex-direction: column; 14 | gap: 1rem; 15 | width: 100%; 16 | background-color: var(--c--contextuals--background--surface--secondary); 17 | border-right: 1px solid var(--c--contextuals--border--surface--primary); 18 | } 19 | 20 | .c__left-panel__mobile { 21 | z-index: 999; 22 | width: 100dvw; 23 | height: calc(100dvh - 52px); 24 | overflow-y: auto; 25 | border-right: 1px solid var(--c--contextuals--border--surface--primary); 26 | position: fixed; 27 | background-color: var(--c--contextuals--background--surface--secondary); 28 | transition: transform 0.15s ease-in-out; 29 | transform: translateX(-100dvw); 30 | 31 | // Animation for both opening and closing 32 | transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); 33 | 34 | &.open { 35 | transform: translateX(0); 36 | border-top: 1px solid var(--c--contextuals--border--surface--primary); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/cunningham-custom-style.scss: -------------------------------------------------------------------------------- 1 | .c__input, 2 | .c__field, 3 | .c__select, 4 | .c__datagrid { 5 | font-family: var(--c--globals--font--families--base); 6 | } 7 | 8 | /** 9 | * Date picker 10 | */ 11 | .c__popover.c__popover--borderless { 12 | z-index: 3; 13 | } 14 | 15 | .c__date-picker__wrapper { 16 | transition: all var(--c--globals--transitions--duration) 17 | var(--c--globals--transitions--ease-out); 18 | } 19 | 20 | .c__date-picker:not(.c__date-picker--disabled):hover .c__date-picker__wrapper { 21 | box-shadow: var(--c--contextuals--border--semantic--brand--secondary) 0 0 0 22 | 2px; 23 | } 24 | 25 | .c__date-picker.c__date-picker--invalid:not(.c__date-picker--disabled):hover 26 | .c__date-picker__wrapper { 27 | box-shadow: var(--c--contextuals--border--semantic--error--secondary) 0 0 0 28 | 2px; 29 | } 30 | 31 | .c__date-picker__wrapper button[aria-label="Clear date"], 32 | .c__date-picker.c__date-picker--invalid .c__date-picker__wrapper * { 33 | color: var(--c--contextuals--border--semantic--error--secondary); 34 | } 35 | 36 | /** 37 | * Toast 38 | */ 39 | .c__toast__container { 40 | z-index: 10000; 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 La Suite numérique 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/modal/index.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Modal 3 | */ 4 | 5 | .c__modal__title { 6 | text-align: left; 7 | padding: 0; 8 | font-size: 1.125rem; 9 | margin-bottom: var(--c--globals--spacings--2xs); 10 | } 11 | 12 | .c__modal__close .c__button--tertiary-text:hover, 13 | .c__modal__close .c__button--tertiary-text:focus-visible { 14 | box-shadow: none; 15 | } 16 | 17 | .c__modal__close button { 18 | padding: 0; 19 | font-size: 88px; 20 | width: 28px !important; 21 | height: 28px; 22 | } 23 | 24 | .c__modal__close button .material-icons { 25 | padding: 0; 26 | font-size: 24px; 27 | color: var(--c--contextuals--content--semantic--neutral--secondary); 28 | } 29 | 30 | .c__modal__close .c__button { 31 | padding: 0 !important; 32 | } 33 | 34 | .c__modal--full .c__modal__content { 35 | overflow-y: auto; 36 | } 37 | 38 | @media screen and (width <= 420px) { 39 | .c__modal__scroller { 40 | padding: 0.7rem; 41 | } 42 | 43 | .c__modal__title h2 { 44 | font-size: 1rem; 45 | } 46 | } 47 | 48 | @media (width <= 576px) { 49 | .c__modal__footer--sided { 50 | gap: 0.5rem; 51 | flex-direction: column-reverse; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/layout/header/index.scss: -------------------------------------------------------------------------------- 1 | @use "../../../styles/_variables"; 2 | 3 | .c__header { 4 | display: flex; 5 | background-color: var(--c--contextuals--background--surface--secondary); 6 | border-bottom: 1px solid var(--c--contextuals--border--surface--primary); 7 | justify-content: space-between; 8 | align-items: center; 9 | height: variables.$header-height; 10 | min-height: variables.$header-height; 11 | max-height: variables.$header-height; 12 | padding: 0 1.125rem; 13 | 14 | @media screen and (max-width: 1024px) { 15 | padding: 0 1rem; 16 | } 17 | 18 | &__toggle-menu { 19 | display: none; 20 | 21 | @media screen and (max-width: 1024px) { 22 | display: block; 23 | } 24 | 25 | &__icon { 26 | color: var(--c--contextuals--content--semantic--brand--tertiary); 27 | } 28 | } 29 | 30 | &__left, 31 | &__right { 32 | display: flex; 33 | align-items: center; 34 | } 35 | 36 | &__right { 37 | display: flex; 38 | align-items: center; 39 | 40 | &__language-picker { 41 | @media screen and (max-width: 1024px) { 42 | display: none; 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/quick-search/index.scss: -------------------------------------------------------------------------------- 1 | .quick-search-input-container { 2 | display: flex; 3 | align-items: center; 4 | gap: var(--c--globals--spacings--2xs); 5 | padding: var(--c--globals--spacings--xs) var(--c--globals--spacings--base); 6 | margin-top: var(--c--globals--spacings--xs); 7 | } 8 | 9 | .c__quick-search-item-template { 10 | display: flex; 11 | padding: var(--c--globals--spacings--3xs, 0.25rem) 12 | var(--c--globals--spacings--2xs, 0.375rem); 13 | justify-content: space-between; 14 | width: 100%; 15 | align-items: center; 16 | 17 | &__left { 18 | display: flex; 19 | align-items: center; 20 | gap: var(--c--globals--spacings--2xs); 21 | width: 100%; 22 | } 23 | 24 | &__right { 25 | display: flex; 26 | align-items: center; 27 | justify-content: flex-end; 28 | } 29 | 30 | &__right:not(.always-show-right) { 31 | opacity: 0; 32 | } 33 | 34 | &__right.always-show-right { 35 | opacity: 1 !important; 36 | } 37 | } 38 | 39 | .quick-search-group__empty-string { 40 | color: var(--c--contextuals--content--semantic--neutral--tertiary); 41 | margin-left: var(--c--globals--spacings--base); 42 | } 43 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { resolve } from "path"; 4 | import dts from "vite-plugin-dts"; 5 | import tsconfigPaths from "vite-tsconfig-paths"; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | build: { 10 | lib: { 11 | entry: { 12 | index: "./src/index.ts", 13 | }, 14 | name: "@lasuite/ui-kit", 15 | formats: ["es", "cjs"], 16 | cssFileName: "style", 17 | }, 18 | rollupOptions: { 19 | external: ["react", "react-dom", "@gouvfr-lasuite/cunningham-react"], 20 | output: { 21 | globals: { 22 | react: "React", 23 | "react-dom": "ReactDOM", 24 | }, 25 | }, 26 | }, 27 | sourcemap: true, 28 | emptyOutDir: true, 29 | }, 30 | plugins: [tsconfigPaths(), dts({ rollupTypes: true }), react()], 31 | resolve: { 32 | alias: [ 33 | { 34 | find: ":", 35 | replacement: resolve(__dirname, "./src"), 36 | }, 37 | { 38 | find: "src", 39 | replacement: resolve(__dirname, "./src"), 40 | }, 41 | ], 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /src/components/badge/badge.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Badge } from './badge'; 3 | 4 | const meta: Meta = { 5 | title: 'Components/Badge', 6 | component: Badge, 7 | tags: ['autodocs'], 8 | }; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Default: Story = { 14 | args: { 15 | children: 'Badge', 16 | }, 17 | }; 18 | 19 | export const All: Story = { 20 | render: () => ( 21 |
22 | Accent 23 | Neutral 24 | Info 25 | Success 26 | Warning 27 | Danger 28 |
29 | ) 30 | } 31 | 32 | export const WithNumber: Story = { 33 | args: { 34 | children: '42', 35 | }, 36 | }; 37 | 38 | export const Uppercased: Story = { 39 | args: { 40 | children: 'new', 41 | uppercased: true, 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/tabs/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs, TabList, Tab, TabPanel } from "react-aria-components"; 2 | import { TabData } from "./types"; 3 | import clsx from "clsx"; 4 | 5 | export type TabsProps = { 6 | tabs: TabData[]; 7 | defaultSelectedTab?: string; 8 | fullWidth?: boolean; 9 | }; 10 | 11 | export const CustomTabs = ({ 12 | tabs, 13 | defaultSelectedTab, 14 | fullWidth = false, 15 | }: TabsProps) => { 16 | if (tabs.length === 0) { 17 | return null; 18 | } 19 | 20 | return ( 21 |
26 | 27 | 28 | {tabs.map((tab) => ( 29 | 30 | {tab.icon && {tab.icon}} 31 | {tab.label} 32 | 33 | ))} 34 | 35 | {tabs.map((tab) => ( 36 | 37 | {tab.content} 38 | 39 | ))} 40 | 41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/form/radio/radio.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Radio } from "@gouvfr-lasuite/cunningham-react"; 4 | 5 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 6 | const meta = { 7 | title: "Components/Forms/Radio", 8 | component: Radio, 9 | tags: ["autodocs"], 10 | 11 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs 12 | } satisfies Meta; 13 | 14 | export default meta; 15 | type Story = StoryObj; 16 | 17 | export const Checked: Story = { 18 | args: { 19 | checked: true, 20 | }, 21 | }; 22 | 23 | export const Unchecked: Story = { 24 | args: { 25 | checked: false, 26 | }, 27 | }; 28 | 29 | export const Disabled: Story = { 30 | args: { 31 | disabled: true, 32 | }, 33 | }; 34 | 35 | export const DisabledChecked: Story = { 36 | args: { 37 | disabled: true, 38 | checked: true, 39 | }, 40 | }; 41 | 42 | export const WithLabel: Story = { 43 | args: { 44 | checked: true, 45 | label: "Label", 46 | text: "Description liée à ce label sur plusieurs lignes", 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/layout/index.scss: -------------------------------------------------------------------------------- 1 | @use "./../../styles/variables"; 2 | 3 | .c__main-layout { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100dvh; 7 | 8 | // Disable transitions during window resize to prevent mobile panels from being visible 9 | &.resizing { 10 | .c__left-panel__mobile, 11 | .c__right-panel__mobile, 12 | .c__right-panel { 13 | transition: none !important; 14 | } 15 | } 16 | } 17 | 18 | .c__main-layout__header { 19 | position: fixed; 20 | top: 0; 21 | left: 0; 22 | right: 0; 23 | } 24 | 25 | .c__main-layout__content { 26 | width: 100%; 27 | height: 100%; 28 | display: flex; 29 | margin-top: calc(variables.$header-height); 30 | 31 | &__left-panel { 32 | height: calc(100dvh - variables.$header-height); 33 | width: 300px; 34 | border-right: 1px solid var(--c--contextuals--border--surface--primary); 35 | @media screen and (max-width: 1024px) { 36 | width: 100dvw; 37 | } 38 | } 39 | &__center { 40 | display: flex; 41 | 42 | &__children { 43 | height: calc(100dvh - variables.$header-height); 44 | width: 100%; 45 | background: var(--c--contextuals--background--surface--primary); 46 | flex: 1; 47 | overflow-y: auto; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/front-dependencies-installation.yml: -------------------------------------------------------------------------------- 1 | name: Install frontend installation reusable workflow 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | node_version: 7 | required: false 8 | default: "20.x" 9 | type: string 10 | 11 | jobs: 12 | front-dependencies-installation: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Restore the frontend cache 18 | uses: actions/cache@v4 19 | id: front-node_modules 20 | with: 21 | path: "node_modules" 22 | key: front-node_modules-${{ hashFiles('yarn.lock') }} 23 | - name: Setup Node.js 24 | if: steps.front-node_modules.outputs.cache-hit != 'true' 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ inputs.node_version }} 28 | - name: Install dependencies 29 | if: steps.front-node_modules.outputs.cache-hit != 'true' 30 | run: yarn install --frozen-lockfile 31 | - name: Cache install frontend 32 | if: steps.front-node_modules.outputs.cache-hit != 'true' 33 | uses: actions/cache@v4 34 | with: 35 | path: "node_modules" 36 | key: front-node_modules-${{ hashFiles('yarn.lock') }} 37 | -------------------------------------------------------------------------------- /src/types/translations.type-check.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { locales } from ":/locales/Locale"; 3 | import { ExtractTranslationKeys } from "./translations"; 4 | 5 | // Translation type checking 6 | 7 | /** 8 | * Want to understand what the hell is that ? Read this: 9 | * https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650 10 | */ 11 | type AssertIsStrictEqual = 12 | (() => T extends X ? 1 : 2) extends 13 | (() => T extends Y ? 1 : 2) ? 14 | true : { error: "Types are not equal"; expected: X; actual: Y }; 15 | 16 | 17 | /** 18 | * Locale files should have the same keys 19 | * 20 | * IN CASE OF TS ERROR, DO NOT REMOVE OR BYPASS THIS TEST 21 | * INSTEAD FIX TRANSLATION ISSUE. 22 | * 23 | * A TS test to ensure that the translations are consistent 24 | * between the different locales. In case of a mismatch, the 25 | * `assertTranslationKeysMatch` will raise an TS error so it was not 26 | * possible to build the package. 27 | */ 28 | type EnKeys = ExtractTranslationKeys; 29 | type FrKeys = ExtractTranslationKeys; 30 | 31 | // @ts-expect-error : TS6133 - assertTranslationKeysMatch is not used 32 | function assertTranslationKeysMatch(): AssertIsStrictEqual { 33 | return true; 34 | } 35 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "./library.scss"; 2 | 3 | export * from "./components/Provider/Provider"; 4 | export * from "./locales/Locale"; 5 | export * from "./components/button"; 6 | export * from "./components/dropdown-menu"; 7 | export * from "./components/footer/Footer"; 8 | export * from "./components/filter/Filter"; 9 | export * from "./components/hero/Hero"; 10 | export * from "./components/la-gaufre/LaGaufre"; 11 | export * from "./components/la-gaufre/LaGaufreV2"; 12 | export * from "./components/language"; 13 | export * from "./components/layout"; 14 | export * from "./components/loader"; 15 | export * from "./components/form"; 16 | export * from "./components/quick-search"; 17 | export * from "./components/separator"; 18 | export * from "./components/tabs"; 19 | export * from "./components/tree-view"; 20 | export * from "./components/form/label/label"; 21 | export * from "./components/badge"; 22 | export * from "./components/icon"; 23 | export * from "./hooks/useResponsive"; 24 | export * from "./hooks/useArrowRoving"; 25 | export * from "./components/share"; 26 | export * from "./components/users"; 27 | export * from "./hooks/useCustomTranslations"; 28 | export * from "./utils/get-ui-kit-themes-from-globals"; 29 | export { anctGlobals, dsfrGlobals, whiteLabelGlobals } from "../cunningham"; 30 | export { default as cunninghamConfig } from "../cunningham"; 31 | -------------------------------------------------------------------------------- /src/components/icon/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import { IconSize, IconType } from "./types"; 4 | 5 | export type IconProps = { 6 | /** 7 | * The name of the Material Icon to display 8 | */ 9 | name: string; 10 | 11 | /** 12 | * The type of the icon 13 | */ 14 | type?: IconType; 15 | 16 | /** 17 | * The size of the icon 18 | */ 19 | size?: IconSize | number; 20 | /** 21 | * Custom CSS class name 22 | */ 23 | className?: string; 24 | /** 25 | * Custom color for the icon 26 | */ 27 | color?: string; 28 | 29 | /** 30 | * Additional props to pass to the span element 31 | */ 32 | [key: string]: unknown; 33 | }; 34 | 35 | export const Icon: React.FC = ({ 36 | name, 37 | size, 38 | className, 39 | color, 40 | 41 | type = IconType.FILLED, 42 | ...props 43 | }) => { 44 | const iconClasses = clsx( 45 | "material-icons", 46 | { 47 | [`icon--${size}`]: size !== undefined, 48 | [`icon--${type}`]: type !== IconType.OUTLINED, 49 | }, 50 | className 51 | ); 52 | 53 | const style = { 54 | color: color, 55 | fontSize: typeof size === "number" ? `${size}px` : undefined, 56 | }; 57 | 58 | return ( 59 | 60 | {name} 61 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /src/components/form/checkbox/checkbox.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Checkbox } from "@gouvfr-lasuite/cunningham-react"; 4 | 5 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 6 | const meta = { 7 | title: "Components/Forms/Checkbox", 8 | component: Checkbox, 9 | tags: ["autodocs"], 10 | 11 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs 12 | } satisfies Meta; 13 | 14 | export default meta; 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = {}; 18 | export const Disabled: Story = { 19 | args: { 20 | disabled: true, 21 | }, 22 | }; 23 | 24 | export const DisabledChecked: Story = { 25 | args: { 26 | disabled: true, 27 | checked: true, 28 | }, 29 | }; 30 | 31 | export const Indeterminate: Story = { 32 | args: { 33 | indeterminate: true, 34 | checked: true, 35 | }, 36 | }; 37 | 38 | export const IndeterminateDisable: Story = { 39 | args: { 40 | indeterminate: true, 41 | checked: true, 42 | disabled: true, 43 | }, 44 | }; 45 | 46 | export const WithLabel: Story = { 47 | args: { 48 | checked: true, 49 | label: "Label", 50 | text: "Description liée à ce label sur plusieurs lignes", 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /src/hooks/useControllableState.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | // TODO: This hook is from Cunningham, it should be exporter from it. 4 | 5 | /** 6 | * This hook is used to create a state that can be controlled by the parent. If not the state is handled internally. 7 | * 8 | * @param defaultValue if not controlled by the parent, this is the default value 9 | * @param propsValue if controlled by the parent, this is the value 10 | * @param propsCallback if controlled by the parent, this is the callback to call when the value changes 11 | */ 12 | export const useControllableState = ( 13 | defaultValue: T, 14 | propsValue?: T, 15 | propsCallback?: (value: T) => void 16 | ): [T, (value: T) => void] => { 17 | const [state, setState] = React.useState( 18 | typeof propsValue === "undefined" ? defaultValue : propsValue 19 | ); 20 | 21 | // Bottom-Up. 22 | const onChange = (value: T) => { 23 | if (propsCallback) { 24 | propsCallback(value); 25 | } else { 26 | setState(value); 27 | } 28 | }; 29 | 30 | // Top-Down. 31 | useEffect(() => { 32 | if (!propsCallback) { 33 | return; 34 | } 35 | if (typeof propsValue !== "undefined" && propsValue !== state) { 36 | setState(propsValue); 37 | } 38 | // eslint-disable-next-line react-hooks/exhaustive-deps 39 | }, [propsValue]); 40 | 41 | return [state, onChange]; 42 | }; 43 | -------------------------------------------------------------------------------- /src/library.scss: -------------------------------------------------------------------------------- 1 | @use "@gouvfr-lasuite/cunningham-react/style"; 2 | @use "cunningham-custom-style"; 3 | @use "./components/button"; 4 | @use "./components/dropdown-menu"; 5 | @use "./components/footer"; 6 | @use "./components/filter"; 7 | @use "./components/hero"; 8 | @use "./components/loader"; 9 | @use "./components/la-gaufre"; 10 | @use "./components/layout/header"; 11 | @use "./components/layout/left-panel"; 12 | @use "./components/layout/right-panel"; 13 | @use "./components/language"; 14 | @use "./components/layout"; 15 | @use "./components/quick-search"; 16 | @use "./components/quick-search/quick-search-global-style"; 17 | @use "./components/separator"; 18 | @use "./components/form/label"; 19 | @use "./components/tabs"; 20 | @use "./components/form/switch"; 21 | @use "./components/form/checkbox"; 22 | @use "./components/form/radio"; 23 | @use "./components/form/input"; 24 | @use "./components/form/select"; 25 | @use "./components/form/textarea"; 26 | @use "./components/datagrid"; 27 | @use "./components/tree-view"; 28 | @use "./components/tooltip"; 29 | @use "./components/share/users-invitation"; 30 | @use "./components/share/access"; 31 | @use "./components/users/avatar"; 32 | @use "./components/users/rows"; 33 | @use "./components/users/menu"; 34 | @use "./components/share/modal/items"; 35 | @use "./components/share/modal/share-modal"; 36 | @use "./components/modal"; 37 | @use "./components/badge"; 38 | @use "./components/icon"; 39 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/quick-search/QuickSearch.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from "cmdk"; 2 | import { ReactNode, useRef } from "react"; 3 | import { QuickSearchInput } from "./QuickSearchInput"; 4 | import { hasChildrens } from ":/utils/children"; 5 | 6 | export type QuickSearchProps = { 7 | onFilter?: (str: string) => void; 8 | inputValue?: string; 9 | inputContent?: ReactNode; 10 | showInput?: boolean; 11 | loading?: boolean; 12 | label?: string; 13 | placeholder?: string; 14 | children?: ReactNode; 15 | }; 16 | 17 | export const QuickSearch = ({ 18 | onFilter, 19 | inputContent, 20 | inputValue, 21 | loading, 22 | showInput = true, 23 | label, 24 | placeholder, 25 | children, 26 | }: QuickSearchProps) => { 27 | const ref = useRef(null); 28 | 29 | return ( 30 | <> 31 |
32 | 33 | {showInput && ( 34 | 41 | {inputContent} 42 | 43 | )} 44 | 45 |
{children}
46 |
47 |
48 |
49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/form/switch/switch.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Switch } from "@gouvfr-lasuite/cunningham-react"; 4 | 5 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 6 | const meta = { 7 | title: "Components/Forms/Switch", 8 | component: Switch, 9 | tags: ["autodocs"], 10 | 11 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs 12 | } satisfies Meta; 13 | 14 | export default meta; 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = {}; 18 | export const Disabled: Story = { 19 | args: { 20 | disabled: true, 21 | }, 22 | }; 23 | 24 | export const DisabledChecked: Story = { 25 | args: { 26 | disabled: true, 27 | checked: true, 28 | }, 29 | }; 30 | 31 | export const WithLabel: Story = { 32 | render: () => ( 33 |
34 | 39 |
40 | ), 41 | }; 42 | 43 | export const WithLabelRight: Story = { 44 | render: () => ( 45 |
46 | 51 |
52 | ), 53 | }; 54 | -------------------------------------------------------------------------------- /src/components/share/users-invitation/index.scss: -------------------------------------------------------------------------------- 1 | .c__add-share-user-list { 2 | display: flex; 3 | align-items: center; 4 | padding: var(--c--globals--spacings--sm); 5 | background-color: var(--c--contextuals--background--surface--tertiary); 6 | border-radius: var(--c--globals--spacings--3xs); 7 | border: 1px solid var(--c--contextuals--border--surface--primary); 8 | gap: var(--c--globals--spacings--3xs); 9 | 10 | &__items { 11 | display: flex; 12 | 13 | flex-wrap: wrap; 14 | gap: var(--c--globals--spacings--3xs); 15 | flex: 1; 16 | } 17 | 18 | &__right-actions { 19 | display: flex; 20 | align-items: center; 21 | gap: var(--c--globals--spacings--xs); 22 | } 23 | } 24 | 25 | .c__add-share-user-item { 26 | display: flex; 27 | align-items: center; 28 | justify-content: center; 29 | gap: var(--c--globals--spacings--3xs); 30 | border-radius: var(--c--globals--spacings--3xs); 31 | height: fit-content; 32 | padding: var(--c--globals--spacings--4xs) var(--c--globals--spacings--4xs) 33 | var(--c--globals--spacings--4xs) var(--c--globals--spacings--xs); 34 | background-color: var( 35 | --c--contextuals--background--semantic--neutral--secondary 36 | ); 37 | color: var(--c--contextuals--content--semantic--neutral--primary); 38 | font-size: var(--c--globals--font--sizes--xs); 39 | 40 | .close-icon { 41 | color: var(--c--contextuals--content--semantic--neutral--tertiary); 42 | font-size: var(--c--globals--font--sizes--sm); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import { CunninghamProvider } from "../src/components/Provider/Provider"; 2 | import "./../src/index.scss"; 3 | import "./../src/styles/fonts.scss"; 4 | import "./../src/style-stories.scss"; 5 | import type { Preview } from "@storybook/react"; 6 | import { DocsContainer } from "@storybook/blocks"; 7 | import React from "react"; 8 | import { BACKGROUND_COLOR_TO_THEME, getThemeFromGlobals, Themes, themes } from "./theme"; 9 | 10 | const DocsWithTheme = (props) => { 11 | const theme = getThemeFromGlobals(props.context.store.userGlobals.globals); 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | const preview: Preview = { 20 | decorators: [ 21 | (Story, context) => ( 22 | 23 |
24 | 25 |
26 |
27 | ), 28 | ], 29 | parameters: { 30 | backgrounds: { 31 | default: null, 32 | values: Object.entries(BACKGROUND_COLOR_TO_THEME).map(([key, value]) => ({ 33 | name: Themes[value][1], 34 | value: key, 35 | })), 36 | }, 37 | controls: { 38 | matchers: { 39 | color: /(background|color)$/i, 40 | date: /Date$/i, 41 | }, 42 | }, 43 | docs: { 44 | container: DocsWithTheme, 45 | }, 46 | }, 47 | }; 48 | 49 | export default preview; 50 | -------------------------------------------------------------------------------- /.storybook/manager.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | import { addons, types, useStorybookApi } from '@storybook/manager-api'; 3 | import { getThemeFromGlobals, themes } from './theme'; 4 | import { useEffect } from 'react'; 5 | import { useGlobals } from '@storybook/manager-api'; 6 | 7 | addons.setConfig({ theme: themes.default }); 8 | 9 | /** 10 | * This add-on is just here to apply the theme to the Storybook manager ( the top-most frame 11 | * containing sidebar, toolbar, etc ) when the theme is switched. 12 | * 13 | * The reason why we needed to add this add-on is that add-ons are the only place from where you can 14 | * dynamically change the current theme of the manager. 15 | */ 16 | addons.register('theme-synchronizer', () => { 17 | addons.add('theme-synchronizer/main', { 18 | title: 'Theme synchronizer', 19 | //👇 Sets the type of UI element in Storybook 20 | type: types.TOOL, 21 | //👇 Shows the Toolbar UI element if either the Canvas or Docs tab is active 22 | match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)), 23 | render: () => { 24 | const api = useStorybookApi(); 25 | const [globals] = useGlobals(); 26 | const theme = getThemeFromGlobals(globals); 27 | useEffect(() => { 28 | api.setOptions({ 29 | theme: themes[theme] 30 | }) 31 | }, [theme, api]); 32 | return null; 33 | }, 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/components/share/assets/public.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/layout/header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { DropdownMenuOption } from ":/components/dropdown-menu/types"; 2 | import { LanguagePicker } from ":/components/language/language-picker"; 3 | 4 | import { Button, useCunningham } from "@gouvfr-lasuite/cunningham-react"; 5 | 6 | export type HeaderProps = { 7 | leftIcon?: React.ReactNode; 8 | rightIcon?: React.ReactNode; 9 | languages?: DropdownMenuOption[]; 10 | onTogglePanel?: () => void; 11 | isPanelOpen?: boolean; 12 | }; 13 | 14 | export const Header = ({ 15 | leftIcon, 16 | rightIcon, 17 | languages, 18 | onTogglePanel, 19 | isPanelOpen, 20 | }: HeaderProps) => { 21 | const { t } = useCunningham(); 22 | return ( 23 |
24 |
25 |
37 |
{leftIcon}
38 |
39 | {languages && ( 40 |
41 | 42 |
43 | )} 44 | 45 | {rightIcon} 46 |
47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/tabs/tabs.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { CustomTabs } from "./Tabs"; 4 | 5 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 6 | const meta = { 7 | title: "Components/Tabs [WIP]", 8 | component: CustomTabs, 9 | tags: ["autodocs"], 10 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = { 17 | args: { 18 | defaultSelectedTab: "Tpr", 19 | tabs: [ 20 | { id: "FoR", label: "Infos", content: "Infos", icon: "info" }, 21 | { id: "Emp", label: "Activités", content: "Activités", icon: "list" }, 22 | { 23 | id: "Tpr", 24 | label: "Notifications", 25 | content: "Notifications", 26 | icon: "notifications", 27 | }, 28 | ], 29 | }, 30 | }; 31 | 32 | export const FullWidth: Story = { 33 | parameters: { 34 | layout: "fullscreen", 35 | }, 36 | args: { 37 | fullWidth: true, 38 | defaultSelectedTab: "Tpr", 39 | tabs: [ 40 | { id: "FoR", label: "Infos", content: "Infos", icon: "info" }, 41 | { id: "Emp", label: "Activités", content: "Activités", icon: "list" }, 42 | { 43 | id: "Tpr", 44 | label: "Notifications", 45 | content: "Notifications", 46 | icon: "notifications", 47 | }, 48 | ], 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/badge/index.scss: -------------------------------------------------------------------------------- 1 | .c__badge { 2 | display: inline-flex; 3 | text-align: center; 4 | line-height: 1em; 5 | font-size: var(--c--components--badge--font-size); 6 | padding-inline: var(--c--components--badge--padding-inline); 7 | padding-block: var(--c--components--badge--padding-block); 8 | border-radius: var(--c--components--badge--border-radius); 9 | font-weight: 600; 10 | font-variant-numeric: tabular-nums; 11 | } 12 | 13 | .c__badge--uppercased { 14 | text-transform: uppercase; 15 | letter-spacing: 0.05em; 16 | } 17 | 18 | .c__badge--accent { 19 | background-color: var(--c--components--badge--accent--background-color); 20 | color: var(--c--components--badge--accent--color); 21 | } 22 | 23 | .c__badge--neutral { 24 | background-color: var(--c--components--badge--neutral--background-color); 25 | color: var(--c--components--badge--neutral--color); 26 | } 27 | 28 | .c__badge--danger { 29 | background-color: var(--c--components--badge--danger--background-color); 30 | color: var(--c--components--badge--danger--color); 31 | } 32 | 33 | .c__badge--success { 34 | background-color: var(--c--components--badge--success--background-color); 35 | color: var(--c--components--badge--success--color); 36 | } 37 | 38 | .c__badge--warning { 39 | background-color: var(--c--components--badge--warning--background-color); 40 | color: var(--c--components--badge--warning--color); 41 | } 42 | 43 | .c__badge--info { 44 | background-color: var(--c--components--badge--info--background-color); 45 | color: var(--c--components--badge--info--color); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/users/avatar/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { getUserInitials, getUserColor, AVATAR_COLORS } from './utils'; 3 | 4 | describe('Avatar Utils', () => { 5 | describe('getUserInitials', () => { 6 | it('should return initials from a single name', () => { 7 | expect(getUserInitials('John')).toBe('J'); 8 | }); 9 | 10 | it('should return initials from two names', () => { 11 | expect(getUserInitials('John Doe')).toBe('JD'); 12 | }); 13 | 14 | it('should return initials from a composed name', () => { 15 | expect(getUserInitials('John-Doe')).toBe('JD'); 16 | }); 17 | 18 | it('should return initials from a composed name with underscores', () => { 19 | expect(getUserInitials('john_doe@example.local')).toBe('JD'); 20 | }); 21 | 22 | 23 | it('should return only first two initials for names with more than two words', () => { 24 | expect(getUserInitials('John James Doe')).toBe('JJ'); 25 | expect(getUserInitials('John James Michael Doe')).toBe('JJ'); 26 | }); 27 | 28 | it('should handle empty string', () => { 29 | expect(getUserInitials('')).toBe(''); 30 | }); 31 | }); 32 | 33 | describe('getUserColor', () => { 34 | it('should return a color from the predefined list', () => { 35 | const color = getUserColor('John'); 36 | expect(AVATAR_COLORS).toContain(color); 37 | }); 38 | 39 | it('should handle empty string', () => { 40 | const color = getUserColor(''); 41 | expect(AVATAR_COLORS).toContain(color); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/components/share/access/access-role-dropdown.stories.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | import type { Meta, StoryObj } from "@storybook/react"; 3 | import { AccessRoleDropdown } from "./AccessRoleDropdown"; 4 | import { useState } from "react"; 5 | 6 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 7 | const meta = { 8 | title: "Components/Share/AccessRoleDropdown", 9 | component: AccessRoleDropdown, 10 | tags: ["autodocs"], 11 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs 12 | } satisfies Meta; 13 | 14 | export default meta; 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = { 18 | args: { 19 | selectedRole: "admin", 20 | roles: [ 21 | { label: "Admin", value: "admin" }, 22 | { label: "Editor", value: "editor" }, 23 | { label: "Viewer", value: "viewer" }, 24 | ], 25 | onSelect: () => {}, 26 | canUpdate: true, 27 | isOpen: false, 28 | onOpenChange: () => {}, 29 | }, 30 | parameters: {}, 31 | render: (args) => { 32 | const [selectedRole, setSelectedRole] = useState(args.selectedRole); 33 | const [isOpen, setIsOpen] = useState(args.isOpen); 34 | 35 | return ( 36 | 44 | ); 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/footer/Footer.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Footer } from "./Footer"; 4 | 5 | const meta = { 6 | title: "Components/Footer", 7 | component: Footer, 8 | tags: ["autodocs"], 9 | parameters: { 10 | layout: "fullscreen", 11 | }, 12 | } satisfies Meta; 13 | 14 | export default meta; 15 | type Story = StoryObj; 16 | 17 | export const WithoutProps: Story = {}; 18 | 19 | export const WithProps: Story = { 20 | args: { 21 | externalLinks: [ 22 | { 23 | label: "legifrance.gouv.fr", 24 | href: "https://legifrance.gouv.fr/", 25 | }, 26 | { 27 | label: "info.gouv.fr", 28 | href: "https://info.gouv.fr/", 29 | }, 30 | { 31 | label: "service-public.fr", 32 | href: "https://service-public.fr/", 33 | }, 34 | { 35 | label: "data.gouv.fr", 36 | href: "https://data.gouv.fr/", 37 | }, 38 | ], 39 | legalLinks: [ 40 | { 41 | label: "Legal Mentions", 42 | href: "/legal-notice", 43 | }, 44 | { 45 | label: "Personal Data and cookies", 46 | href: "/personal-data-cookies", 47 | }, 48 | { 49 | label: "Accessibility: non-compliant", 50 | href: "/accessibility", 51 | }, 52 | ], 53 | license: { 54 | label: "Unless otherwise stated, all content on this site is under", 55 | link: { 56 | label: "licence etalab-2.0", 57 | href: "https://github.com/etalab/licence-ouverte/blob/master/LO.md", 58 | }, 59 | }, 60 | }, 61 | }; -------------------------------------------------------------------------------- /src/components/quick-search/QuickSearchInput.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from "cmdk"; 2 | import { ReactNode } from "react"; 3 | import { Spinner } from ":/components/loader/Spinner"; 4 | import { useCunningham } from "@gouvfr-lasuite/cunningham-react"; 5 | import { HorizontalSeparator } from ":/components/separator/HorizontalSeparator"; 6 | 7 | type Props = { 8 | loading?: boolean; 9 | inputValue?: string; 10 | onFilter?: (str: string) => void; 11 | placeholder?: string; 12 | children?: ReactNode; 13 | withSeparator?: boolean; 14 | }; 15 | export const QuickSearchInput = ({ 16 | loading, 17 | inputValue, 18 | onFilter, 19 | placeholder, 20 | children, 21 | withSeparator: separator = true, 22 | }: Props) => { 23 | const { t } = useCunningham(); 24 | 25 | if (children) { 26 | return ( 27 | <> 28 | {children} 29 | {separator && } 30 | 31 | ); 32 | } 33 | 34 | return ( 35 | <> 36 |
37 | {!loading && search} 38 | {loading && ( 39 |
40 | 41 |
42 | )} 43 | { 47 | e.stopPropagation(); 48 | }} 49 | role="combobox" 50 | value={inputValue} 51 | placeholder={placeholder} 52 | onValueChange={onFilter} 53 | /> 54 |
55 | {separator && } 56 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/tree-view/stories/tree-view-example.scss: -------------------------------------------------------------------------------- 1 | .tree-view-item { 2 | width: 100%; 3 | display: flex; 4 | align-items: center; 5 | 6 | .container { 7 | display: flex; 8 | justify-content: space-between; 9 | align-items: center; 10 | width: 100%; 11 | } 12 | 13 | .icon { 14 | width: 2rem; 15 | height: 2rem; 16 | background-color: var( 17 | --c--contextuals--background--semantic--neutral--tertiary 18 | ); 19 | border-radius: 50%; 20 | } 21 | 22 | .name { 23 | overflow: hidden; 24 | text-overflow: ellipsis; 25 | white-space: initial; 26 | display: -webkit-box; 27 | line-clamp: 1; 28 | width: 100%; 29 | -webkit-line-clamp: 1; 30 | -webkit-box-orient: vertical; 31 | } 32 | } 33 | 34 | .c__tree-view--node { 35 | .actions { 36 | &:not(.show-actions) { 37 | opacity: 0; 38 | } 39 | 40 | &.show-actions { 41 | opacity: 1; 42 | } 43 | } 44 | &:hover { 45 | .actions { 46 | opacity: 1; 47 | } 48 | } 49 | } 50 | 51 | .c__tree-view--row:focus-within .c__tree-view--node .actions { 52 | opacity: 1; 53 | } 54 | 55 | .right-panel { 56 | display: flex; 57 | flex-direction: column; 58 | } 59 | 60 | .drag-overlay-item { 61 | display: flex; 62 | align-items: center; 63 | opacity: 0.5; 64 | background-color: red; 65 | max-width: 300px; 66 | height: 30px; 67 | } 68 | 69 | .folder { 70 | display: flex; 71 | user-select: none; 72 | align-items: center; 73 | opacity: 1; 74 | background-color: transparent; 75 | padding: 10px; 76 | 77 | &.isSelected { 78 | background-color: red; 79 | } 80 | 81 | &.isOver { 82 | background-color: blue; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/components/share/assets/lock_person.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/hooks/useResponsive.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import config from "../../cunningham"; 3 | 4 | type ResponsiveStates = { 5 | isMobile: boolean; 6 | isTablet: boolean; 7 | isDesktop: boolean; 8 | }; 9 | 10 | type Breakpoints = { 11 | mobile: number; 12 | tablet: number; 13 | }; 14 | 15 | const breakpoints = { 16 | mobile: parseInt( 17 | config.themes.default.globals.breakpoints.mobile.replace("px", "") 18 | ), 19 | tablet: parseInt( 20 | config.themes.default.globals.breakpoints.tablet.replace("px", "") 21 | ), 22 | }; 23 | 24 | const getResponsiveStates = ( 25 | width: number, 26 | breakpoints: Breakpoints 27 | ): ResponsiveStates => { 28 | return { 29 | isMobile: width <= breakpoints.mobile, 30 | isTablet: width <= breakpoints.tablet, 31 | isDesktop: width > breakpoints.tablet, 32 | }; 33 | }; 34 | 35 | export const useResponsive = () => { 36 | const [responsiveStates, setResponsiveStates] = useState( 37 | getResponsiveStates(window.innerWidth, breakpoints) 38 | ); 39 | 40 | // Memoize breakpoints to avoid recalculation on every render 41 | useEffect(() => { 42 | const handleResize = () => { 43 | const newResponsiveState = getResponsiveStates( 44 | window.innerWidth, 45 | breakpoints 46 | ); 47 | const isSame = 48 | JSON.stringify(newResponsiveState) === JSON.stringify(responsiveStates); 49 | if (!isSame) { 50 | setResponsiveStates(newResponsiveState); 51 | } 52 | }; 53 | 54 | window.addEventListener("resize", handleResize); 55 | 56 | // Cleanup on unmount 57 | return () => { 58 | window.removeEventListener("resize", handleResize); 59 | }; 60 | }, [responsiveStates]); 61 | 62 | return responsiveStates; 63 | }; 64 | -------------------------------------------------------------------------------- /src/components/tree-view/types.ts: -------------------------------------------------------------------------------- 1 | export type BaseTreeViewData = { 2 | id: string; 3 | childrenCount?: number; 4 | hasLoadedChildren?: boolean; 5 | children?: BaseTreeViewData[] ; 6 | pagination?: { 7 | currentPage: number; 8 | totalCount?: number; 9 | hasMore: boolean; 10 | }; 11 | canDrop?: boolean; 12 | } & ( 13 | | ({ nodeType: TreeViewNodeTypeEnum.SIMPLE_NODE , label: string } ) 14 | | ({ nodeType: TreeViewNodeTypeEnum.TITLE; headerTitle: string } ) 15 | | ({ nodeType: TreeViewNodeTypeEnum.SEPARATOR } ) 16 | | ({ nodeType: TreeViewNodeTypeEnum.VIEW_MORE; label?: string } ) 17 | | ({ nodeType?: Exclude } & T) 18 | ); 19 | 20 | 21 | 22 | export type TreeViewDataType = BaseTreeViewData; 23 | 24 | export type TreeDataItem = { 25 | /** A unique key for the tree node. */ 26 | key: string; 27 | /** The key of the parent node. */ 28 | parentKey?: string | null; 29 | /** The value object for the tree node. */ 30 | value: TreeViewDataType; 31 | /** Children of the tree node. */ 32 | children: TreeDataItem[] | null; 33 | }; 34 | 35 | export enum TreeViewNodeTypeEnum { 36 | NODE = 'node', 37 | SEPARATOR = 'separator', 38 | TITLE = 'title', 39 | SIMPLE_NODE = 'simpleNode', 40 | VIEW_MORE = 'viewMore', 41 | } 42 | 43 | export enum TreeViewMoveModeEnum { 44 | FIRST_CHILD = 'first-child', 45 | LAST_CHILD = 'last-child', 46 | LEFT = 'left', 47 | RIGHT = 'right', 48 | } 49 | 50 | export type TreeViewMoveResult = { 51 | targetModeId: string; 52 | mode: TreeViewMoveModeEnum; 53 | oldParentId?: string; 54 | index: number; 55 | newParentId: string | null; 56 | sourceId: string; 57 | }; 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/components/users/avatar/avatar.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { UserAvatar } from "./UserAvatar"; 3 | import { AVATAR_COLORS } from "./utils"; 4 | 5 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 6 | const meta = { 7 | title: "Components/users/Avatar", 8 | component: UserAvatar, 9 | tags: ["autodocs"], 10 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const XSmall: Story = { 17 | args: { 18 | fullName: "Gustave Eiffel", 19 | size: "xsmall", 20 | }, 21 | }; 22 | 23 | export const AllColors: Story = { 24 | args: { 25 | fullName: "John Doe", 26 | size: "medium", 27 | }, 28 | render: () => { 29 | return ( 30 |
31 | {AVATAR_COLORS.map((color) => ( 32 |
33 | 38 | {color} 39 |
40 | ))} 41 |
42 | ); 43 | }, 44 | }; 45 | 46 | export const Small: Story = { 47 | args: { 48 | fullName: "Jules Verne", 49 | size: "small", 50 | }, 51 | }; 52 | export const Medium: Story = { 53 | args: { 54 | fullName: "John Doe", 55 | size: "medium", 56 | }, 57 | }; 58 | 59 | export const Large: Story = { 60 | args: { 61 | fullName: "Albert Einstein", 62 | size: "large", 63 | }, 64 | }; 65 | 66 | export const ComplexName: Story = { 67 | args: { 68 | fullName: "Jean-Philippe De La Rozière", 69 | size: "large", 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /src/components/share/access/AccessRoleDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { DropdownMenu, DropdownMenuOption } from ":/components/dropdown-menu"; 2 | 3 | type AccessRoleDropdownProps = { 4 | selectedRole: string; 5 | roles: DropdownMenuOption[]; 6 | onSelect: (role: string) => void; 7 | canUpdate?: boolean; 8 | isOpen?: boolean; 9 | onOpenChange?: (isOpen: boolean) => void; 10 | roleTopMessage?: string; 11 | }; 12 | 13 | export const AccessRoleDropdown = ({ 14 | roles, 15 | onSelect, 16 | canUpdate = true, 17 | selectedRole, 18 | isOpen, 19 | onOpenChange, 20 | roleTopMessage, 21 | }: AccessRoleDropdownProps) => { 22 | const currentRoleString = roles.find((role) => role.value === selectedRole); 23 | 24 | if (!canUpdate) { 25 | return ( 26 | 27 | {currentRoleString?.label} 28 | 29 | ); 30 | } 31 | return ( 32 | { 35 | const isAccessRoleDropdown = element.closest( 36 | ".c__access-role-dropdown" 37 | ); 38 | // If the element is not a child of the access role dropdown, close the dropdown 39 | if (isAccessRoleDropdown) { 40 | return false; 41 | } 42 | return true; 43 | }} 44 | onOpenChange={onOpenChange} 45 | options={roles} 46 | selectedValues={[selectedRole]} 47 | onSelectValue={onSelect} 48 | topMessage={roleTopMessage} 49 | > 50 |
{ 54 | onOpenChange?.(!isOpen); 55 | }} 56 | > 57 | 58 | {currentRoleString?.label} 59 | 60 | 61 | {isOpen ? "arrow_drop_up" : "arrow_drop_down"} 62 | 63 |
64 |
65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /src/components/share/modal/items/share-items.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ShareInvitationItem } from "./ShareInvitationItem"; 2 | import { ShareMemberItem } from "./ShareMemberItem"; 3 | import { SearchUserItem } from "./SearchUserItem"; 4 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 5 | const meta = { 6 | title: "Components/Share/Items", 7 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs 8 | }; 9 | 10 | export default meta; 11 | 12 | const Wrapper = ({ children }: { children: React.ReactNode }) => { 13 | return
{children}
; 14 | }; 15 | 16 | const roles = [ 17 | { label: "Admin", value: "admin" }, 18 | { label: "Editor", value: "editor" }, 19 | { label: "Viewer", value: "viewer" }, 20 | ]; 21 | 22 | export const InvitationItem = { 23 | render: () => ( 24 | 25 | 38 | 39 | ), 40 | }; 41 | 42 | export const MemberItem = { 43 | render: () => ( 44 | 45 | 58 | 59 | ), 60 | }; 61 | 62 | export const SearchResultUserItem = { 63 | render: () => ( 64 | 65 | 72 | 73 | ), 74 | }; 75 | -------------------------------------------------------------------------------- /src/components/tabs/index.scss: -------------------------------------------------------------------------------- 1 | .c__tabs { 2 | display: flex; 3 | 4 | &__full-width { 5 | .react-aria-Tabs { 6 | width: 100%; 7 | } 8 | 9 | .react-aria-Tab { 10 | flex: 1; 11 | display: flex; 12 | justify-content: center; 13 | } 14 | } 15 | 16 | .react-aria-Tabs { 17 | display: flex; 18 | 19 | &[data-orientation="horizontal"] { 20 | flex-direction: column; 21 | } 22 | } 23 | 24 | .react-aria-TabList { 25 | display: flex; 26 | width: 100%; 27 | } 28 | 29 | .react-aria-Tab { 30 | padding: 10px; 31 | cursor: pointer; 32 | outline: none; 33 | position: relative; 34 | color: var(--c--contextuals--content--semantic--neutral--tertiary); 35 | transition: color 200ms; 36 | --border-color: transparent; 37 | forced-color-adjust: none; 38 | display: flex; 39 | align-items: center; 40 | gap: 8px; 41 | border-bottom: 1px solid var(--c--contextuals--border--surface--primary); 42 | 43 | &[data-hovered] { 44 | background-color: var( 45 | --c--contextuals--background--semantic--neutral--tertiary-hover 46 | ); 47 | color: var(--text-color-hover); 48 | } 49 | 50 | &[data-selected] { 51 | font-weight: 500; 52 | color: var(--c--contextuals--content--semantic--brand--primary); 53 | border-bottom: 2px solid 54 | var(--c--contextuals--content--semantic--brand--primary); 55 | } 56 | 57 | &[data-disabled] { 58 | color: var(--text-color-disabled); 59 | &[data-selected] { 60 | --border-color: var(--text-color-disabled); 61 | } 62 | } 63 | 64 | &[data-focus-visible]:after { 65 | content: ""; 66 | position: absolute; 67 | inset: 4px; 68 | border-radius: 4px; 69 | border: 2px solid var(--focus-ring-color); 70 | } 71 | } 72 | 73 | .react-aria-TabPanel { 74 | margin-top: 4px; 75 | padding: 10px; 76 | border-radius: 4px; 77 | outline: none; 78 | 79 | &[data-focus-visible] { 80 | outline: 2px solid var(--focus-ring-color); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/components/form/form.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox, Radio, RadioGroup, Switch } from "@gouvfr-lasuite/cunningham-react"; 2 | 3 | import { Button, Select, TextArea } from "@gouvfr-lasuite/cunningham-react"; 4 | 5 | import { Input } from "@gouvfr-lasuite/cunningham-react"; 6 | import { Meta } from "@storybook/react"; 7 | import { Label } from "./label/label"; 8 | 9 | export default { 10 | title: "Components/Forms/Examples", 11 | } as Meta; 12 | 13 | const CITIES = [ 14 | "Paris", 15 | "Marseille", 16 | "Lyon", 17 | "Toulouse", 18 | "Nice", 19 | "Nantes", 20 | "Strasbourg", 21 | "Montpellier", 22 | "Bordeaux", 23 | "Lille", 24 | ]; 25 | const OPTIONS = CITIES.map((city) => ({ 26 | label: city, 27 | value: city.toLowerCase(), 28 | })); 29 | 30 | export const Example = () => { 31 | return ( 32 |
40 | 41 |