├── 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 |
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 |
20 | {children}
21 |
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:
,
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 |
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 |
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 |
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 |
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 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/src/components/form/label/with-label.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryFn, StoryObj } from "@storybook/react";
2 |
3 | import { WithLabel } from "./WithLabel";
4 | import { DropdownMenu } from ":/components/dropdown-menu/DropdownMenu";
5 | import { Button } from "@gouvfr-lasuite/cunningham-react";
6 | import { useState } from "react";
7 |
8 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
9 | const meta = {
10 | title: "Components/Forms/WithLabel",
11 | component: WithLabel,
12 | tags: ["autodocs"],
13 |
14 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
15 | } satisfies Meta;
16 |
17 | export default meta;
18 | type Story = StoryObj;
19 |
20 | const Template: StoryFn = (args) => {
21 | const [open, setOpen] = useState(false);
22 | return (
23 |
24 |
33 |
46 |
47 |
48 | );
49 | };
50 |
51 | export const Default: Story = {
52 | render: Template,
53 | args: {
54 | label: "Label",
55 | labelSide: "left",
56 | text: "Description liée à ce label sur plusieurs lignes",
57 | },
58 | };
59 |
60 | export const Right: Story = {
61 | render: Template,
62 | args: {
63 | label: "Label",
64 | labelSide: "right",
65 | text: "Description liée à ce label sur plusieurs lignes",
66 | },
67 | };
68 |
--------------------------------------------------------------------------------
/src/components/filter/index.scss:
--------------------------------------------------------------------------------
1 | .c__filter {
2 | &__button {
3 | background-color: var(
4 | --c--contextuals--background--semantic--neutral--tertiary
5 | );
6 | height: 32px;
7 | display: flex;
8 | align-items: center;
9 | cursor: pointer;
10 | border-radius: 4px;
11 | border: 1px solid var(--c--contextuals--border--semantic--neutral--tertiary);
12 | font-size: 14px;
13 | font-weight: 400;
14 | font-family: var(--c--globals--font--families--base);
15 | gap: 6px;
16 | padding: 0 6px 0 8px;
17 | color: var(--c--contextuals--content--semantic--neutral--tertiary);
18 |
19 | &__icon {
20 | font-size: 1.25rem;
21 | transition: all var(--c--globals--transitions--duration)
22 | var(--c--globals--transitions--ease-out);
23 | &.opened {
24 | transform: rotate(180deg);
25 | }
26 | }
27 |
28 | &:not(.c__filter__button--active) {
29 | &:hover,
30 | &:focus,
31 | &:focus-within {
32 | background-color: var(
33 | --c--contextuals--background--semantic--neutral--tertiary-hover
34 | );
35 | }
36 | }
37 |
38 | &--active {
39 | background-color: var(
40 | --c--contextuals--background--semantic--brand--secondary
41 | );
42 | border-color: var(--c--contextuals--border--semantic--brand--secondary);
43 | font-weight: 500;
44 | color: var(--c--contextuals--content--semantic--brand--primary);
45 |
46 | &:hover,
47 | &:focus,
48 | &:focus-within {
49 | background-color: var(
50 | --c--contextuals--background--semantic--brand--secondary-hover
51 | );
52 | }
53 | }
54 | }
55 |
56 | &__item {
57 | width: 100%;
58 | display: flex;
59 | align-items: center;
60 | gap: 6px;
61 | justify-content: space-between;
62 | }
63 |
64 | &__label {
65 | display: flex;
66 | align-items: center;
67 | }
68 |
69 | &__popover {
70 | background-color: var(--c--contextuals--background--surface--primary);
71 | border: 1px solid var(--c--contextuals--border--surface--primary);
72 | border-radius: 4px;
73 | padding: 8px;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/tree-view/providers/TreeContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useRef, useState } from "react";
2 | import { TreeApi } from "react-arborist";
3 | import { TreeDataItem, TreeViewDataType } from "../types";
4 | import { PaginatedChildrenResult, useTree } from "../useTree";
5 |
6 | export type TreeContextType = {
7 | treeApiRef: React.RefObject> | null>;
8 | treeData: ReturnType>;
9 | root: T | null;
10 | initialTargetId: string | null;
11 | setInitialTargetId: (id: string) => void;
12 | setRoot: (root: T) => void;
13 | };
14 |
15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
16 | export const TreeContext = createContext | null>(null);
17 |
18 | export type TreeProviderProps = {
19 | children: React.ReactNode;
20 | initialTreeData?: TreeViewDataType[];
21 | initialNodeId?: string;
22 | onRefresh?: (id: string) => Promise>>;
23 | onLoadChildren?: (
24 | id: string,
25 | page: number
26 | ) => Promise>;
27 | };
28 | export const TreeProvider = ({
29 | children,
30 | onRefresh: refreshCallback,
31 | initialNodeId,
32 | onLoadChildren,
33 | initialTreeData,
34 | }: TreeProviderProps) => {
35 | const treeApiRef = useRef>>(null);
36 | const [root, setRoot] = useState(null);
37 | const [initialTargetId, setInitialTargetId] = useState(
38 | initialNodeId ?? null
39 | );
40 |
41 | const treeData = useTree(
42 | initialTreeData ?? [],
43 | refreshCallback,
44 | onLoadChildren
45 | );
46 |
47 | return (
48 |
58 | {children}
59 |
60 | );
61 | };
62 |
63 | export const useTreeContext = () => {
64 | const Context = useContext(TreeContext);
65 | if (!Context) {
66 | throw new Error("TreeContext not found");
67 | }
68 | return Context as TreeContextType | null;
69 | };
70 |
--------------------------------------------------------------------------------
/src/components/quick-search/QuickSearchGroup.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { Command } from "cmdk";
3 |
4 | import { QuickSearchData } from "./types";
5 | import { QuickSearchItem } from "./QuickSearchItem";
6 |
7 | export type QuickSearchGroupProps = {
8 | group: QuickSearchData;
9 | renderElement?: (element: T) => ReactNode;
10 | onSelect?: (element: T) => void;
11 | };
12 |
13 | export const QuickSearchGroup = ({
14 | group,
15 | onSelect,
16 | renderElement,
17 | }: QuickSearchGroupProps) => {
18 | if (!group.showWhenEmpty && group.elements.length === 0) {
19 | return null;
20 | }
21 | return (
22 |
23 |
28 | {group.startActions?.map((action, index) => {
29 | return (
30 |
34 | {action.content}
35 |
36 | );
37 | })}
38 | {group.elements.map((groupElement, index) => {
39 | return (
40 | {
44 | onSelect?.(groupElement);
45 | }}
46 | >
47 | {renderElement?.(groupElement)}
48 |
49 | );
50 | })}
51 | {group.endActions?.map((action, index) => {
52 | return (
53 |
57 | {action.content}
58 |
59 | );
60 | })}
61 | {group.emptyString && group.elements.length === 0 && (
62 |
63 | {group.emptyString}
64 |
65 | )}
66 |
67 |
68 | );
69 | };
70 |
--------------------------------------------------------------------------------
/src/components/hero/index.scss:
--------------------------------------------------------------------------------
1 | @use "sass:map";
2 | @use "@gouvfr-lasuite/cunningham-react/style" as *;
3 | @use "../../cunningham-tokens-sass" as *;
4 |
5 | $md: map.get($themes, "default", "globals", "breakpoints", "md");
6 | $sm: map.get($themes, "default", "globals", "breakpoints", "sm");
7 |
8 | .c__home-gutter {
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 | max-width: 1120px;
13 | padding: 0 3rem;
14 | width: 100%;
15 | margin: auto;
16 | box-sizing: border-box;
17 |
18 | @media (max-width: $sm) {
19 | padding: 1rem;
20 | }
21 | }
22 |
23 | .c__hero {
24 | display: flex;
25 | max-width: 78rem;
26 | width: 100%;
27 | justify-content: space-around;
28 | align-items: center;
29 | height: 100vh;
30 | position: relative;
31 |
32 | * {
33 | box-sizing: border-box;
34 | }
35 |
36 | &__display {
37 | display: flex;
38 | width: 100%;
39 | justify-content: center;
40 | align-items: center;
41 | position: relative;
42 | flex-direction: row;
43 | gap: 1rem;
44 | overflow: auto;
45 |
46 | @media (max-width: $md) {
47 | flex-direction: column;
48 | }
49 |
50 | &__captions {
51 | display: flex;
52 | justify-content: center;
53 | align-items: center;
54 | gap: 0.75rem;
55 | flex-direction: column;
56 | flex-grow: 1;
57 | max-width: 400px;
58 |
59 | @media (max-width: $md) {
60 | max-width: 100%;
61 | }
62 |
63 | > h2 {
64 | font-size: 2.3rem;
65 | font-weight: bold;
66 | color: var(--c--contextuals--content--semantic--neutral--primary);
67 | text-align: center;
68 | margin: 0;
69 | line-height: 45px;
70 | }
71 |
72 | &__subtitle {
73 | font-size: 1.125rem;
74 | color: var(--c--contextuals--content--semantic--neutral--secondary);
75 | text-align: center;
76 | margin-bottom: 1rem;
77 | }
78 | }
79 |
80 | > img {
81 | width: auto;
82 | max-width: 100%;
83 | object-fit: contain;
84 | overflow: auto;
85 | max-height: 100%;
86 | width: 50%;
87 | flex-grow: 1;
88 |
89 | @media (max-width: $md) {
90 | display: none;
91 | }
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/components/datagrid/datagrid.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta } from "@storybook/react";
2 | import databaseCars from "./resources/databaseCars.json";
3 |
4 | import { Button, DataGrid, SimpleDataGrid } from "@gouvfr-lasuite/cunningham-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/Datagrid",
9 | component: DataGrid,
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 |
17 | export const Empty = () => {
18 | return (
19 |
29 | );
30 | };
31 |
32 | export const EmptyCustomWithButton = () => {
33 | return (
34 | add}>
46 | Create object
47 |
48 | }
49 | />
50 | );
51 | };
52 |
53 | export const ClientSideWithPagination = () => {
54 | return (
55 | <>
56 |
84 | >
85 | );
86 | };
87 |
--------------------------------------------------------------------------------
/src/components/datagrid/resources/databaseCars.json:
--------------------------------------------------------------------------------
1 | [{"id":"42e59801-eaf0-4cec-a9bb-8222788cd779","carName":"Hackett - Dickinson","year":2023,"price":5003},{"id":"92874aea-0984-473e-bb06-2e48f920099e","carName":"Brakus Group","year":2024,"price":5002},{"id":"5a29eb9c-2f43-498a-ad72-a2d9c933d8b5","carName":"Schamberger Inc","year":2023,"price":5005},{"id":"1c075a61-809b-4c32-bdf4-8679fe60957a","carName":"Schinner, Kertzmann and Mitchell","year":2023,"price":5003},{"id":"24091aba-94f2-4e0e-8ee1-5d83f80ad6a0","carName":"Ratke, Morissette and Beatty","year":2023,"price":5001},{"id":"f56b9138-8cc3-4175-86b4-4db860e3839f","carName":"Price LLC","year":2024,"price":5000},{"id":"d437fb53-a10b-46f9-8975-73568aa10635","carName":"Balistreri, Von and Wintheiser","year":2024,"price":5000},{"id":"fe4e25ef-2cd1-4ddd-9de9-b9cd4d51d3f4","carName":"Heathcote, Herzog and Casper","year":2023,"price":5005},{"id":"e41a2d28-6681-4bb2-988a-c4aac01a6d6b","carName":"Bins, Brown and Hirthe","year":2023,"price":5002},{"id":"f7acb144-40a5-4584-b5f2-9018c55bf98e","carName":"Welch Group","year":2023,"price":5000},{"id":"0d977b79-c762-478e-bed8-43d6faf1f7e7","carName":"Stroman and Sons","year":2024,"price":5000},{"id":"066aeb90-ff34-4f24-8d23-2df7dd4749ce","carName":"Padberg - Raynor","year":2023,"price":5002},{"id":"b242524e-1cc8-4d72-be79-430e881682f5","carName":"Emmerich, Breitenberg and Hand","year":2023,"price":5001},{"id":"88ea6672-ef61-468e-a0a0-cbe78ab4c9ca","carName":"Ratke, Abernathy and Macejkovic","year":2023,"price":5004},{"id":"5acfbdd8-0d1a-47a2-9a27-8cd998ce6f06","carName":"Ernser - Bartoletti","year":2023,"price":5005},{"id":"743eb166-853b-4501-ad26-3f5debcaaa98","carName":"Glover LLC","year":2024,"price":5000},{"id":"86508544-d335-4274-82e8-7b116a522651","carName":"Barton, Hodkiewicz and Harris","year":2023,"price":5002},{"id":"69c7b16a-124e-4c21-a906-efdacf7d305e","carName":"Dare and Sons","year":2024,"price":5005},{"id":"f1942cae-eb45-4540-9580-1276e84e3fa0","carName":"Ryan Inc","year":2023,"price":5005},{"id":"889763ec-881a-4bc6-af18-0d31ff46c000","carName":"Ziemann Inc","year":2023,"price":5005},{"id":"f9d65110-5be4-48e7-b80e-36d217b98a65","carName":"Borer, Miller and Watsica","year":2023,"price":5002},{"id":"81981644-07e7-45f4-af0b-ccd0503462be","carName":"Runolfsdottir Group","year":2023,"price":5000},{"id":"a27df970-ea88-4a24-91b4-a162ca3c7c43","carName":"White Inc","year":2023,"price":5000}]
2 |
--------------------------------------------------------------------------------
/src/components/footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { useCunningham } from "@gouvfr-lasuite/cunningham-react";
2 | import IconLink from "./assets/external-link.svg";
3 | import LogoGouv from ":/assets/logo-gouv.svg";
4 |
5 | type Link = {
6 | label: string;
7 | href: string;
8 | };
9 |
10 | export type FooterProps = {
11 | externalLinks?: readonly Link[];
12 | legalLinks?: readonly Link[];
13 | license?: {
14 | label: string;
15 | link: Link;
16 | };
17 | logo?: {
18 | src: string;
19 | width?: string;
20 | height?: string;
21 | alt: string;
22 | };
23 | };
24 |
25 | export const Footer = ({
26 | externalLinks,
27 | legalLinks,
28 | license,
29 | logo,
30 | }: FooterProps) => {
31 | const { t } = useCunningham();
32 | return (
33 |
76 | );
77 | };
78 |
--------------------------------------------------------------------------------
/src/components/dropdown-menu/index.scss:
--------------------------------------------------------------------------------
1 | .c__dropdown-menu-trigger {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | }
6 | .c__dropdown-menu {
7 | background-color: var(--c--contextuals--background--surface--primary);
8 | z-index: 1000;
9 | max-width: 320px;
10 | max-height: inherit;
11 | box-sizing: border-box;
12 | overflow: auto;
13 | min-width: 150px;
14 | box-sizing: border-box;
15 | outline: none;
16 | border-radius: var(--c--globals--spacings--3xs, 4px);
17 | border: 1px solid var(--c--contextuals--border--surface--primary);
18 | box-shadow: 0px 0px 6px 0px rgba(0, 0, 145, 0.1);
19 |
20 | .react-aria-Separator {
21 | height: 1px;
22 | background: var(--c--contextuals--border--semantic--neutral--tertiary);
23 | }
24 | }
25 |
26 | .c__dropdown-menu-item-top-message {
27 | display: flex;
28 | align-items: center;
29 |
30 | padding: var(--c--globals--spacings--sm, 0.5rem);
31 |
32 | font-weight: 500;
33 | color: var(--c--contextuals--content--semantic--neutral--primary);
34 | font-size: var(--c--globals--font--sizes--xs, 0.75rem);
35 | }
36 |
37 | .c__dropdown-menu-item {
38 | display: flex;
39 | gap: var(--c--globals--spacings--base, 16px);
40 | padding: 8px 16px;
41 | border-bottom: 1px solid transparent;
42 | padding-right: 24px;
43 | outline: none;
44 | cursor: pointer;
45 | line-height: 19px;
46 | font-size: var(--c--globals--font--sizes--sm, 0.875rem);
47 | font-weight: 500;
48 | color: var(--c--contextuals--content--semantic--neutral--primary);
49 | align-items: center;
50 | forced-color-adjust: none;
51 |
52 | &__label {
53 | flex: 1;
54 | }
55 |
56 | &[data-focused],
57 | &:hover {
58 | background: var(
59 | --c--contextuals--background--semantic--contextual--primary
60 | );
61 | }
62 |
63 | &[data-disabled] {
64 | color: var(--c--contextuals--content--semantic--disabled--primary);
65 | cursor: not-allowed;
66 |
67 | &:hover {
68 | background: transparent;
69 | }
70 | }
71 |
72 | .material-icons {
73 | display: flex;
74 | align-items: center;
75 | justify-content: center;
76 | width: 24px;
77 | height: 24px;
78 | font-size: 20px;
79 | }
80 |
81 | .checked {
82 | margin-left: var(--c--globals--spacings--md, 1.5rem);
83 | }
84 | }
85 |
86 | .dropdown-menu-separator {
87 | background-color: var(--c--contextuals--background--surface--primary);
88 | height: 1px;
89 | width: 100%;
90 | }
91 |
--------------------------------------------------------------------------------
/src/components/footer/index.scss:
--------------------------------------------------------------------------------
1 | .c__footer {
2 | position: relative;
3 |
4 | &__stripe {
5 | position: absolute;
6 | height: 2px;
7 | width: 100%;
8 | background: var(--c--contextuals--background--semantic--brand--primary);
9 | top: 0;
10 | }
11 |
12 | &__logo {
13 | color: transparent;
14 | width: 220px;
15 | height: auto;
16 | }
17 |
18 | &__content {
19 | display: flex;
20 | flex-direction: column;
21 | padding: 3rem 1.625rem 1rem;
22 |
23 | &__top {
24 | display: flex;
25 | flex-direction: row;
26 | gap: 1.5rem;
27 | align-items: center;
28 | justify-content: space-between;
29 | flex-wrap: wrap;
30 |
31 | &__logo {
32 | display: flex;
33 | align-items: center;
34 | gap: 6rem;
35 | flex-direction: row;
36 |
37 | .c__logo-gouv {
38 | font-size: 1.3rem;
39 | }
40 | }
41 |
42 | &__links {
43 | display: flex;
44 | flex-flow: wrap;
45 | gap: 0.5rem 1.5rem;
46 |
47 | a {
48 | color: var(--c--contextuals--content--semantic--neutral--primary);
49 | text-decoration: none;
50 | display: flex;
51 | gap: 0.2rem;
52 | transition: box-shadow 0.3s ease 0s;
53 | font-weight: bold;
54 | }
55 | }
56 | }
57 |
58 | &__middle {
59 | display: flex;
60 | flex-flow: wrap;
61 | margin-top: 1.625rem;
62 | padding-top: 0.5rem;
63 | border-top: 1px solid
64 | var(--c--contextuals--border--semantic--neutral--tertiary);
65 | gap: 0.5rem 1rem;
66 |
67 | a {
68 | text-decoration: none;
69 | font-size: 0.8125rem;
70 | color: var(--c--contextuals--content--semantic--neutral--secondary);
71 | display: flex;
72 | padding-right: 1rem;
73 | box-shadow: inset -1px 0px 0px 0px var(--c--contextuals--border--semantic--neutral--tertiary);
74 | }
75 | }
76 |
77 | &__mention {
78 | font-size: 0.8125rem;
79 | color: var(--c--contextuals--content--semantic--neutral--secondary);
80 | display: inline;
81 | margin-top: 1.625rem;
82 |
83 | a {
84 | color: var(--c--contextuals--content--semantic--neutral--secondary);
85 | text-decoration: none;
86 | display: inline-flex;
87 | box-shadow: 0px 1px 0 0
88 | var(--c--contextuals--border--semantic--neutral--tertiary);
89 | }
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/types/translations.ts:
--------------------------------------------------------------------------------
1 | import { locales } from ":/locales/Locale";
2 |
3 | /**
4 | * Utility types for extracting all nested keys from translation objects
5 | * and converting them to dot-notation string literals.
6 | */
7 |
8 | /**
9 | * Recursively extracts all nested keys from an object type and converts them
10 | * to dot-notation string literals.
11 | *
12 | * @example
13 | * ```typescript
14 | * type Example = {
15 | * components: {
16 | * create: string;
17 | * alert: {
18 | * close_aria_label: string;
19 | * expand_aria_label: string;
20 | * }
21 | * }
22 | * }
23 | *
24 | * type Keys = TranslationKeys;
25 | * // Result: "components.create" | "components.alert.close_aria_label" | "components.alert.expand_aria_label"
26 | * ```
27 | */
28 | export type TranslationKeys = T extends Record
29 | ? {
30 | [K in keyof T]: T[K] extends Record
31 | ? `${K & string}.${TranslationKeys}`
32 | : K
33 | }[keyof T]
34 | : never;
35 |
36 | /**
37 | * Helper type to define the structure of a translation object.
38 | * This avoids circular reference by using a more specific definition.
39 | */
40 | type TranslationValue = string | { [key: string]: TranslationValue };
41 |
42 | /**
43 | * Type constraint for objects that can have translation keys extracted.
44 | */
45 | export type TranslationObject = Record;
46 |
47 | /**
48 | * Utility type that combines TranslationKeys with proper constraint checking.
49 | * Use this as the main export for generating translation key types.
50 | */
51 | export type ExtractTranslationKeys = TranslationKeys;
52 |
53 |
54 | /**
55 | * All available translation keys extracted from the English translation file.
56 | * This type represents all possible keys that can be used with the translation system.
57 | *
58 | * Keys are in dot-notation format, e.g.:
59 | * - "components.share.copyLink"
60 | * - "components.share.access.delete"
61 | * - "components.footer.links.legal"
62 | */
63 | export type TranslationKey = ExtractTranslationKeys;
64 |
65 | /**
66 | * Type representing the structure of a translation object.
67 | * This can be used to ensure consistency between different locale files.
68 | */
69 | export type TranslationStructure = typeof locales["en-US"] & typeof locales["fr-FR"];
70 |
71 | /**
72 | * Union type of all supported locales
73 | */
74 | export type SupportedLocale = keyof typeof locales;
75 |
--------------------------------------------------------------------------------
/src/assets/fonts/Marianne/Marianne-font.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: Marianne;
3 | src: url("./Marianne-Thin.woff2") format("woff2"),
4 | url("./Marianne-Thin.woff") format("woff");
5 | font-weight: 100;
6 | }
7 |
8 | @font-face {
9 | font-family: Marianne;
10 | src: url("./Marianne-Thin_Italic.woff2") format("woff2"),
11 | url("./Marianne-Thin_Italic.woff") format("woff");
12 | font-weight: 100;
13 | font-style: italic;
14 | }
15 |
16 | @font-face {
17 | font-family: Marianne;
18 | src: url("./Marianne-Light.woff2") format("woff2"),
19 | url("./Marianne-Light.woff") format("woff");
20 | font-weight: 300;
21 | }
22 |
23 | @font-face {
24 | font-family: Marianne;
25 | src: url("./Marianne-Light_Italic.woff2") format("woff2"),
26 | url("./Marianne-Light_Italic.woff") format("woff");
27 | font-weight: 300;
28 | font-style: italic;
29 | }
30 |
31 | @font-face {
32 | font-family: Marianne;
33 | src: url("./Marianne-Regular.woff2") format("woff2"),
34 | url("./Marianne-Regular.woff") format("woff");
35 | font-weight: 400;
36 | }
37 |
38 | @font-face {
39 | font-family: Marianne;
40 | src: url("./Marianne-Regular_Italic.woff2") format("woff2"),
41 | url("./Marianne-Regular_Italic.woff") format("woff");
42 | font-weight: 400;
43 | font-style: italic;
44 | }
45 |
46 | @font-face {
47 | font-family: Marianne;
48 | src: url("./Marianne-Medium.woff2") format("woff2"),
49 | url("./Marianne-Medium.woff") format("woff");
50 | font-weight: 500;
51 | }
52 |
53 | @font-face {
54 | font-family: Marianne;
55 | src: url("./Marianne-Medium_Italic.woff2") format("woff2"),
56 | url("./Marianne-Medium_Italic.woff") format("woff");
57 | font-weight: 500;
58 | font-style: italic;
59 | }
60 |
61 | @font-face {
62 | font-family: Marianne;
63 | src: url("./Marianne-Bold.woff2") format("woff2"),
64 | url("./Marianne-Bold.woff") format("woff");
65 | font-weight: 700;
66 | }
67 |
68 | @font-face {
69 | font-family: Marianne;
70 | src: url("./Marianne-Bold_Italic.woff2") format("woff2"),
71 | url("./Marianne-Bold_Italic.woff") format("woff");
72 | font-weight: 700;
73 | font-style: italic;
74 | }
75 |
76 | @font-face {
77 | font-family: Marianne;
78 | src: url("./Marianne-ExtraBold.woff2") format("woff2"),
79 | url("./Marianne-ExtraBold.woff") format("woff");
80 | font-weight: 800;
81 | }
82 |
83 | @font-face {
84 | font-family: Marianne;
85 | src: url("./Marianne-ExtraBold_Italic.woff2") format("woff2"),
86 | url("./Marianne-ExtraBold_Italic.woff") format("woff");
87 | font-weight: 800;
88 | font-style: italic;
89 | }
90 |
--------------------------------------------------------------------------------
/src/components/share/users-invitation/InvitationUserSelectorList.tsx:
--------------------------------------------------------------------------------
1 | import { Button, useCunningham } from "@gouvfr-lasuite/cunningham-react";
2 | import { ReactNode, useState } from "react";
3 | import { DropdownMenuOption } from "../../dropdown-menu";
4 | import { AccessRoleDropdown } from "../access/AccessRoleDropdown";
5 | import { UserData } from ":/components/share/types.ts";
6 |
7 | export type AddShareUserListProps = {
8 | users: UserData[];
9 | onRemoveUser: (user: UserData) => void;
10 | rightActions?: ReactNode;
11 | onShare: () => void;
12 | roles: DropdownMenuOption[];
13 | selectedRole: string;
14 | shareButtonLabel?: string;
15 | onSelectRole: (role: string) => void;
16 | };
17 |
18 | export const InvitationUserSelectorList = ({
19 | users,
20 | onRemoveUser,
21 | rightActions,
22 | onShare,
23 | shareButtonLabel,
24 | roles,
25 | selectedRole,
26 | onSelectRole,
27 | }: AddShareUserListProps) => {
28 | const { t } = useCunningham();
29 | const [isOpen, setIsOpen] = useState(false);
30 | return (
31 |
32 |
33 | {users.map((user) => (
34 |
39 | ))}
40 |
41 |
42 | {rightActions}
43 |
50 |
53 |
54 |
55 | );
56 | };
57 |
58 | export type ShareSelectedUserItemProps = {
59 | user: UserData;
60 | onRemoveUser: (user: UserData) => void;
61 | };
62 |
63 | export const InvitationUserSelectorItem = ({
64 | user,
65 | onRemoveUser,
66 | }: ShareSelectedUserItemProps) => {
67 | return (
68 |
69 | {user.full_name || user.email}
70 |
78 | );
79 | };
80 |
--------------------------------------------------------------------------------
/src/locales/en-US.json:
--------------------------------------------------------------------------------
1 | {
2 | "components": {
3 | "share": {
4 | "copyLink": "Copy link",
5 | "ok": "Ok",
6 | "shareButton": "Share",
7 | "modalTitle": "Share folder",
8 | "access": {
9 | "delete": "Remove access"
10 | },
11 | "cannot_view": {
12 | "message": "You can view this item but you need additional access to view its members or modify the settings."
13 | },
14 | "invitations": {
15 | "title": "Pending invitations"
16 | },
17 | "members": {
18 | "title_plural": "Shared between {count} people",
19 | "title_singular": "Shared between {count} person",
20 | "load_more": "Show more "
21 | },
22 | "item": {
23 | "add": "Add"
24 | },
25 | "search": {
26 | "placeholder": "Search user",
27 | "group_name": "Search user result"
28 | },
29 | "user": {
30 | "no_result": "No result",
31 | "placeholder": "Search user"
32 | },
33 | "linkSettings": {
34 | "title": "Link settings",
35 | "reach": {
36 | "choices": {
37 | "public": {
38 | "title": "Public",
39 | "description": "Anyone with the link can access the document"
40 | },
41 | "restricted": {
42 | "title": "Private",
43 | "description": "Only users of the space can access the document"
44 | }
45 | }
46 | },
47 | "role": {
48 | "choices": {
49 | "reader": {
50 | "title": "Reader"
51 | },
52 | "editor": {
53 | "title": "Editor"
54 | }
55 | }
56 | }
57 | }
58 | },
59 | "laGaufre": {
60 | "label": "Digital LaSuite services",
61 | "closeLabel": "Close the menu",
62 | "viewMoreLabel": "View more",
63 | "viewLessLabel": "View less",
64 | "loadingText": "Loading…",
65 | "newWindowLabelSuffix": " (new window)",
66 | "headerLabel": "About"
67 | },
68 | "userMenu": {
69 | "term_of_service": "Terms of service",
70 | "manage_account": "Manage account",
71 | "open": "Open user menu",
72 | "close": "Close user menu",
73 | "accountSettings": "Account settings",
74 | "logout": "Logout",
75 | "dialogTitle": "User menu"
76 | },
77 | "treeView": {
78 | "viewMore": "Load more elements"
79 | },
80 | "footer": {
81 | "logo": {
82 | "alt": "Logo of the French government"
83 | }
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/public/storybook/logo-uikit-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/storybook/logo-uikit-default.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/form/input/input.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from "@storybook/react";
2 |
3 | import { Input } 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/Input",
8 | component: Input,
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: "Your name",
21 | },
22 | };
23 |
24 | export const Error = {
25 | args: {
26 | defaultValue: "Hello world",
27 | label: "Your name",
28 | state: "error",
29 | icon: person,
30 | text: "This is an optional error message",
31 | },
32 | };
33 |
34 | export const ErrorItems = {
35 | args: {
36 | defaultValue: "Hello world",
37 | label: "Your name",
38 | state: "error",
39 | icon: person,
40 | text: "This is an optional error message",
41 | textItems: [
42 | "Text too long",
43 | "Wrong choice",
44 | "Must contain at least 9 characters, uppercase and digits",
45 | ],
46 | },
47 | };
48 |
49 | export const DisabledEmpty = {
50 | args: {
51 | label: "Your name",
52 | icon: person,
53 | disabled: true,
54 | },
55 | };
56 |
57 | export const DisabledFilled = {
58 | args: {
59 | label: "Your name",
60 | defaultValue: "John Doe",
61 | icon: person,
62 | disabled: true,
63 | },
64 | };
65 |
66 | export const Empty = {
67 | args: {
68 | label: "Your email",
69 | },
70 | };
71 |
72 | export const Icon = {
73 | args: {
74 | label: "Account balance",
75 | icon: attach_money,
76 | defaultValue: "1000",
77 | },
78 | };
79 |
80 | export const IconRight = {
81 | args: {
82 | label: "Account balance",
83 | rightIcon: attach_money,
84 | defaultValue: "1000",
85 | },
86 | };
87 |
88 | export const FullWidth = {
89 | args: {
90 | defaultValue: "Hello world",
91 | label: "Your name",
92 | fullWidth: true,
93 | text: "This is a text, you can display anything you want here like warnings, informations or errors.",
94 | rightText: "0/300",
95 | },
96 | };
97 |
--------------------------------------------------------------------------------
/src/components/users/menu/index.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, StoryObj } from "@storybook/react";
2 | import { LanguagePicker, LanguagesOption } from ":/components/language";
3 | import { UserMenu } from ".";
4 |
5 | const meta: Meta = {
6 | title: "Components/users/Menu",
7 | component: UserMenu,
8 | tags: ["autodocs"],
9 | decorators: [
10 | (Story) => (
11 |
18 |
19 |
20 | ),
21 | ],
22 | };
23 |
24 | export default meta;
25 | type Story = StoryObj;
26 |
27 | const languages: LanguagesOption[] = [
28 | { label: "Français", value: "fr-FR", shortLabel: "FR" },
29 | { label: "English", value: "en-US", shortLabel: "EN" },
30 | { label: "Deutsch", value: "de-DE", shortLabel: "DE" },
31 | ];
32 |
33 | const termOfServiceUrl =
34 | "https://docs.numerique.gouv.fr/docs/8e298e03-c95f-44c7-be4a-ffb618af1854/";
35 |
36 | export const Default: Story = {
37 | args: {
38 | user: {
39 | full_name: "John Doe",
40 | email: "john.doe@example.com",
41 | },
42 | settingsCTA: () => {
43 | alert("Go to account settings");
44 | },
45 | actions: ,
46 | termOfServiceUrl,
47 | logout: () => {
48 | alert("You have been logged out!");
49 | },
50 | },
51 | };
52 |
53 | export const WithOnlyLogout: Story = {
54 | args: {
55 | user: {
56 | full_name: "J Doe",
57 | email: "john.doe@example.com",
58 | },
59 | logout: () => {
60 | alert("You have been logged out!");
61 | },
62 | },
63 | };
64 | export const WithOnlyFooterAction: Story = {
65 | args: {
66 | user: {
67 | full_name: "J Doe",
68 | email: "john.doe@example.com",
69 | },
70 | actions: (
71 |
72 | ),
73 | },
74 | };
75 | export const WithOnlyTermOfService: Story = {
76 | args: {
77 | user: {
78 | full_name: "J Doe",
79 | email: "john.doe@example.com",
80 | },
81 | termOfServiceUrl,
82 | },
83 | };
84 |
85 | export const Minimal: Story = {
86 | args: {
87 | user: {
88 | full_name: "Jean Martin",
89 | email: "jean.martin@example.com",
90 | },
91 | },
92 | };
93 |
94 | export const WithNoFullName: Story = {
95 | args: {
96 | user: {
97 | email: "jane.doe@example.com",
98 | },
99 | },
100 | };
101 |
--------------------------------------------------------------------------------
/src/components/users/avatar/index.scss:
--------------------------------------------------------------------------------
1 | .c__avatar {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | border-radius: 50%;
6 | // Use raw token value as fallback for browsers that don't support color-mix
7 | color: var(--c--contextuals--background--surface--tertiary);
8 | color: color-mix(in srgb, var(--c--contextuals--background--surface--tertiary) 95%, transparent);
9 | border: 1px solid var(--c--contextuals--background--surface--tertiary);
10 | border: 1px solid color-mix(in srgb, var(--c--contextuals--background--surface--tertiary) 80%, transparent);
11 |
12 | &__initials {
13 | display: flex;
14 | align-items: center;
15 | justify-content: center;
16 | text-align: center;
17 | font-style: normal;
18 | font-weight: 700;
19 | font-family: var(--c--globals--font--families--base);
20 | text-transform: uppercase;
21 | }
22 |
23 | &.xsmall {
24 | width: 16px;
25 | height: 16px;
26 | font-size: 7px;
27 | letter-spacing: -0.25px;
28 | }
29 |
30 | &.small {
31 | width: 24px;
32 | height: 24px;
33 | font-size: 10px;
34 | letter-spacing: -0.35px;
35 | }
36 |
37 | &.medium {
38 | width: 32px;
39 | height: 32px;
40 | font-size: 11px;
41 | letter-spacing: -0.38px;
42 | }
43 |
44 | &.large {
45 | width: 64px;
46 | height: 64px;
47 | font-size: 27px;
48 | letter-spacing: -0.54px;
49 | }
50 |
51 | &.brand {
52 | background-color: var(--c--contextuals--background--palette--brand--primary);
53 | }
54 |
55 | &.gray {
56 | background-color: var(--c--contextuals--background--palette--gray--primary);
57 | }
58 |
59 | &.purple {
60 | background-color: var(--c--contextuals--background--palette--purple--primary);
61 | }
62 |
63 | &.blue-1 {
64 | background-color: var(--c--contextuals--background--palette--blue-1--primary);
65 | }
66 |
67 | &.blue-2 {
68 | background-color: var(--c--contextuals--background--palette--blue-2--primary);
69 | }
70 |
71 | &.green {
72 | background-color: var(--c--contextuals--background--palette--green--primary);
73 | }
74 |
75 | &.yellow {
76 | background-color: var(--c--contextuals--background--palette--yellow--primary);
77 | }
78 |
79 | &.orange {
80 | background-color: var(--c--contextuals--background--palette--orange--primary);
81 | }
82 |
83 | &.red {
84 | background-color: var(--c--contextuals--background--palette--red--primary);
85 | }
86 |
87 | &.brown {
88 | background-color: var(--c--contextuals--background--palette--brown--primary);
89 | }
90 |
91 | &.pink {
92 | background-color: var(--c--contextuals--background--palette--pink--primary);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/components/filter/Filter.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment, useContext } from "react";
2 | import {
3 | Button,
4 | Label,
5 | ListBox,
6 | ListBoxItem,
7 | Popover,
8 | Select,
9 | SelectProps,
10 | SelectStateContext,
11 | Separator,
12 | } from "react-aria-components";
13 |
14 | import { Option } from "@gouvfr-lasuite/cunningham-react";
15 | import clsx from "clsx";
16 |
17 | export type FilterOption = Option & {
18 | showSeparator?: boolean;
19 | isChecked?: boolean;
20 | };
21 |
22 | export type FilterProps = {
23 | label: string;
24 | options: FilterOption[];
25 | } & SelectProps;
26 |
27 | export const Filter = (props: FilterProps) => {
28 | return (
29 |
32 | );
33 | };
34 |
35 | const FilterInner = (props: FilterProps) => {
36 | const state = useContext(SelectStateContext);
37 |
38 | const selectedOption = state?.selectedItem
39 | ? props.options.find((option) => option.value === state.selectedItem?.key)
40 | : null;
41 |
42 | return (
43 | <>
44 |
69 |
70 |
71 | {props.options.map((option) => (
72 |
73 |
78 |
79 | {option.render ? option.render() : option.label}
80 | {(props.selectedKey === option.value || option.isChecked) && (
81 | check
82 | )}
83 |
84 |
85 | {option.showSeparator && }
86 |
87 | ))}
88 |
89 |
90 | >
91 | );
92 | };
93 |
--------------------------------------------------------------------------------
/src/locales/fr-FR.json:
--------------------------------------------------------------------------------
1 | {
2 | "components": {
3 | "share": {
4 | "copyLink": "Copier le lien",
5 | "ok": "Ok",
6 | "modalTitle": "Partage",
7 | "shareButton": "Partager",
8 | "access": {
9 | "delete": "Retirer l'accès"
10 | },
11 | "cannot_view": {
12 | "message": "Vous pouvez voir cet élément mais vous avez besoin d'un accès supplémentaire pour voir ses membres ou modifier les paramètres."
13 | },
14 | "invitations": {
15 | "title": "Invitations en attente"
16 | },
17 | "members": {
18 | "title_plural": "Partagé entre {count} personnes",
19 | "title_singular": "Partagé entre {count} personne",
20 | "load_more": "Afficher plus"
21 | },
22 | "item": {
23 | "add": "Ajouter"
24 | },
25 | "search": {
26 | "placeholder": "Rechercher un utilisateur",
27 | "group_name": "Résultat de la recherche d'utilisateur"
28 | },
29 | "user": {
30 | "no_result": "Aucun résultat",
31 | "placeholder": "Rechercher un utilisateur"
32 | },
33 | "linkSettings": {
34 | "title": "Paramètres du lien",
35 | "reach": {
36 | "choices": {
37 | "public": {
38 | "title": "Public",
39 | "description": "N'importe qui avec le lien peut accéder au document"
40 | },
41 | "restricted": {
42 | "title": "Privé",
43 | "description": "Seulement les utilisateurs de l'espace peuvent y avoir accès"
44 | }
45 | }
46 | },
47 | "role": {
48 | "choices": {
49 | "reader": {
50 | "title": "Lecture seule"
51 | },
52 | "editor": {
53 | "title": "Édition"
54 | }
55 | }
56 | }
57 | }
58 | },
59 | "laGaufre": {
60 | "label": "Services de la Suite numérique",
61 | "closeLabel": "Fermer le menu",
62 | "loadingText": "Chargement…",
63 | "viewMoreLabel": "Voir plus",
64 | "viewLessLabel": "Voir moins",
65 | "newWindowLabelSuffix": " (nouvelle fenêtre)",
66 | "headerLabel": "À propos"
67 | },
68 | "userMenu": {
69 | "term_of_service": "Conditions d'utilisation",
70 | "open": "Ouvrir le menu utilisateur",
71 | "close": "Fermer le menu utilisateur",
72 | "accountSettings": "Paramètres du compte",
73 | "logout": "Déconnexion",
74 | "dialogTitle": "Menu utilisateur",
75 | "manage_account": "Gérer le compte"
76 | },
77 | "treeView": {
78 | "viewMore": "Charger plus d'éléments"
79 | },
80 | "footer": {
81 | "logo": {
82 | "alt": "Logo du gouvernement français"
83 | }
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Frontend CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - "*"
10 |
11 | jobs:
12 | install-front:
13 | uses: ./.github/workflows/front-dependencies-installation.yml
14 | with:
15 | node_version: "20.x"
16 |
17 | lint-git:
18 | runs-on: ubuntu-latest
19 | if: github.event_name == 'pull_request' # Makes sense only for pull requests
20 | steps:
21 | - name: Checkout repository
22 | uses: actions/checkout@v4
23 | with:
24 | fetch-depth: 0
25 | - name: show
26 | run: git log
27 | - name: Enforce absence of print statements in code
28 | run: |
29 | ! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/main.yml' | grep "print("
30 | - name: Check absence of fixup commits
31 | run: |
32 | ! git log | grep 'fixup!'
33 | - name: Install gitlint
34 | run: pip install --user requests gitlint
35 | - name: Lint commit messages added to main
36 | run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
37 |
38 | lint:
39 | runs-on: ubuntu-latest
40 | needs: install-front
41 | steps:
42 | - name: Checkout repository
43 | uses: actions/checkout@v4
44 | - name: Setup Node.js
45 | uses: actions/setup-node@v4
46 | with:
47 | node-version: "20.x"
48 | - name: Restore the frontend cache
49 | uses: actions/cache@v4
50 | with:
51 | path: "node_modules"
52 | key: front-node_modules-${{ hashFiles('yarn.lock') }}
53 | fail-on-cache-miss: true
54 | - name: Check linting
55 | run: yarn lint
56 |
57 | test:
58 | runs-on: ubuntu-latest
59 | needs: install-front
60 | steps:
61 | - name: Checkout repository
62 | uses: actions/checkout@v4
63 | - name: Setup Node.js
64 | uses: actions/setup-node@v4
65 | with:
66 | node-version: "20.x"
67 | - name: Restore the frontend cache
68 | uses: actions/cache@v4
69 | with:
70 | path: "node_modules"
71 | key: front-node_modules-${{ hashFiles('yarn.lock') }}
72 | fail-on-cache-miss: true
73 | - name: Run tests
74 | run: yarn test
75 |
76 | build:
77 | runs-on: ubuntu-latest
78 | needs: install-front
79 | steps:
80 | - name: Checkout repository
81 | uses: actions/checkout@v4
82 | - name: Setup Node.js
83 | uses: actions/setup-node@v4
84 | with:
85 | node-version: "20.x"
86 | - name: Restore the frontend cache
87 | uses: actions/cache@v4
88 | with:
89 | path: "node_modules"
90 | key: front-node_modules-${{ hashFiles('yarn.lock') }}
91 | fail-on-cache-miss: true
92 | - name: Build
93 | run: yarn build
94 |
--------------------------------------------------------------------------------
/src/components/share/modal/items/ShareInvitationItem.tsx:
--------------------------------------------------------------------------------
1 | import { QuickSearchItemTemplate } from ":/components/quick-search";
2 | import { InvitationData } from "../../types";
3 | import { UserRow } from ":/components/users/rows/UserRow";
4 | import { Button, useCunningham } from "@gouvfr-lasuite/cunningham-react";
5 | import {
6 | DropdownMenu,
7 | DropdownMenuOption,
8 | useDropdownMenu,
9 | } from ":/components/dropdown-menu";
10 | import { AccessRoleDropdown } from "../../access/AccessRoleDropdown";
11 |
12 | export type ShareInvitationItemProps = {
13 | invitation: InvitationData;
14 | roles: DropdownMenuOption[];
15 | updateRole?: (
16 | invitation: InvitationData,
17 | role: string
18 | ) => void;
19 | deleteInvitation?: (
20 | invitation: InvitationData
21 | ) => void;
22 | canUpdate?: boolean;
23 | roleTopMessage?: string;
24 | showMoreActionsButton?: boolean;
25 | };
26 |
27 | export const ShareInvitationItem = ({
28 | invitation,
29 | roles,
30 | updateRole,
31 | deleteInvitation,
32 | canUpdate = true,
33 | showMoreActionsButton = true,
34 | roleTopMessage,
35 | }: ShareInvitationItemProps) => {
36 | const { t } = useCunningham();
37 | const roleDropdown = useDropdownMenu();
38 | const menuOptions = useDropdownMenu();
39 | const options: DropdownMenuOption[] = [
40 | {
41 | label: t("components.share.access.delete"),
42 | callback: () => deleteInvitation?.(invitation),
43 | isDisabled: !canUpdate,
44 | icon: back_hand,
45 | },
46 | ];
47 |
48 | const handleOpenMenu = () => {
49 | const isOpen = menuOptions.isOpen;
50 | menuOptions.setIsOpen(!isOpen);
51 | };
52 |
53 | return (
54 |
55 |
}
57 | alwaysShowRight={true}
58 | right={
59 |
60 |
updateRole?.(invitation, role)}
64 | isOpen={roleDropdown.isOpen}
65 | canUpdate={canUpdate}
66 | onOpenChange={roleDropdown.setIsOpen}
67 | roleTopMessage={roleTopMessage}
68 | />
69 | {showMoreActionsButton && canUpdate && (
70 |
75 |
83 | )}
84 |
85 | }
86 | />
87 |
88 | );
89 | };
90 |
--------------------------------------------------------------------------------
/src/components/tree-view/stories/logo-example.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/components/share/modal/items/ShareMemberItem.tsx:
--------------------------------------------------------------------------------
1 | import { QuickSearchItemTemplate } from ":/components/quick-search";
2 | import { AccessData } from "../../types";
3 | import { UserRow } from ":/components/users/rows/UserRow";
4 | import { Button, useCunningham } from "@gouvfr-lasuite/cunningham-react";
5 | import {
6 | DropdownMenu,
7 | DropdownMenuOption,
8 | useDropdownMenu,
9 | } from ":/components/dropdown-menu";
10 | import { AccessRoleDropdown } from "../../access/AccessRoleDropdown";
11 | import { useMemo } from "react";
12 |
13 | export type ShareMemberItemProps = {
14 | accessData: AccessData;
15 | roles: DropdownMenuOption[];
16 | updateRole?: (access: AccessData, role: string) => void;
17 | deleteAccess?: (access: AccessData) => void;
18 | canUpdate?: boolean;
19 | roleTopMessage?: string;
20 | };
21 |
22 | export const ShareMemberItem = ({
23 | accessData,
24 | roles,
25 | updateRole,
26 | deleteAccess,
27 | canUpdate = true,
28 | roleTopMessage,
29 | }: ShareMemberItemProps) => {
30 | const { t } = useCunningham();
31 | const roleDropdown = useDropdownMenu();
32 |
33 | const menuOptions = useDropdownMenu();
34 | const options: DropdownMenuOption[] = useMemo(() => {
35 | const options: DropdownMenuOption[] = [];
36 | if (deleteAccess) {
37 | options.push({
38 | label: t("components.share.access.delete"),
39 | icon: back_hand,
40 | isDisabled: !canUpdate,
41 | callback: () => deleteAccess(accessData),
42 | });
43 | }
44 | return options;
45 | }, [deleteAccess, accessData, canUpdate, t]);
46 |
47 | const handleOpenMenu = () => {
48 | const isOpen = menuOptions.isOpen;
49 | menuOptions.setIsOpen(!isOpen);
50 | };
51 |
52 | return (
53 |
54 |
61 | }
62 | alwaysShowRight={true}
63 | right={
64 |
65 |
updateRole?.(accessData, role)}
69 | isOpen={roleDropdown.isOpen}
70 | onOpenChange={roleDropdown.setIsOpen}
71 | canUpdate={canUpdate}
72 | roleTopMessage={roleTopMessage}
73 | />
74 | {options.length > 0 && canUpdate && (
75 |
80 |
88 | )}
89 |
90 | }
91 | />
92 |
93 | );
94 | };
95 |
--------------------------------------------------------------------------------
/src/hooks/useArrowRoving.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 | /**
4 | * useArrowRoving
5 | *
6 | * Hook to manage keyboard navigation with ArrowLeft / ArrowRight inside a toolbar of actions.
7 | * - ArrowLeft / ArrowRight → move focus between buttons in the same container (roving focus).
8 | * - Enter → still activate the focused button normally, except right after roving
9 | * where the hook suppresses the "phantom click" (to avoid triggering actions by mistake).
10 | *
11 | * Usage:
12 | * const actionsRef = useRef(null);
13 | * useArrowRoving(actionsRef.current);
14 | *
15 | * return (
16 | *
17 | *
18 | *
19 | *
20 | * );
21 | */
22 |
23 | export function useArrowRoving(container: HTMLElement | null): void {
24 | useEffect(() => {
25 | if (!container) return;
26 |
27 | let blockNextActivate = false;
28 |
29 | const getButtons = (): HTMLButtonElement[] =>
30 | Array.from(container.querySelectorAll("button")).filter(
31 | (btn): btn is HTMLButtonElement =>
32 | !btn.disabled && btn.getAttribute("aria-disabled") !== "true"
33 | );
34 |
35 | const onKeyDown = (e: KeyboardEvent): void => {
36 | const isArrowKey = e.key === "ArrowLeft" || e.key === "ArrowRight";
37 | const isActivationKey = e.key === " ";
38 | const isHTMLElementTarget = e.target instanceof HTMLElement;
39 | if (!isHTMLElementTarget) return;
40 |
41 | if (isActivationKey) {
42 | const btn = e.target.closest("button");
43 | const isValidButton =
44 | btn instanceof HTMLButtonElement && container.contains(btn);
45 | if (isValidButton) {
46 | btn.click();
47 | }
48 | return;
49 | }
50 |
51 | if (!isArrowKey) return;
52 |
53 | const btn = e.target.closest("button");
54 | const isValidButton =
55 | btn instanceof HTMLButtonElement && container.contains(btn);
56 | if (!isValidButton) return;
57 |
58 | const buttons = getButtons();
59 | const buttonIndex = buttons.indexOf(btn as HTMLButtonElement);
60 | if (buttonIndex < 0) return;
61 |
62 | e.preventDefault();
63 | e.stopPropagation();
64 |
65 | const nextButtonIndex =
66 | e.key === "ArrowRight"
67 | ? (buttonIndex + 1) % buttons.length
68 | : (buttonIndex - 1 + buttons.length) % buttons.length;
69 |
70 | buttons[nextButtonIndex].focus();
71 | blockNextActivate = true;
72 | };
73 |
74 | // Prevents accidental click when pressing Enter/Space right after roving
75 | const onKeyUp = (e: KeyboardEvent): void => {
76 | if (!blockNextActivate) return;
77 | if (e.key === " " || e.key === "Enter") {
78 | e.preventDefault();
79 | e.stopPropagation();
80 | }
81 | blockNextActivate = false;
82 | };
83 |
84 | // Intercept events before they bubble
85 | const options: AddEventListenerOptions = { capture: true };
86 | container.addEventListener("keydown", onKeyDown, options);
87 | container.addEventListener("keyup", onKeyUp, options);
88 |
89 | return () => {
90 | container.removeEventListener("keydown", onKeyDown, options);
91 | container.removeEventListener("keyup", onKeyUp, options);
92 | };
93 | }, [container]);
94 | }
95 |
--------------------------------------------------------------------------------
/.storybook/theme.ts:
--------------------------------------------------------------------------------
1 | import { create } from "@storybook/theming";
2 | import { tokens } from "../src/cunningham-tokens";
3 |
4 | type DesignTokens = typeof tokens.themes.default;
5 |
6 | const buildTheme = (
7 | { globals, contextuals }: DesignTokens,
8 | type: "default" | "dark" = "default"
9 | ) => {
10 | return {
11 | brandUrl: "https://github.com/suitenumerique/cunningham",
12 | brandImage: `storybook/logo-uikit-${type}.svg`,
13 | brandTitle: "La Suite UI Kit",
14 | brandTarget: "_self",
15 |
16 | //
17 | colorPrimary: contextuals.content.semantic.brand.primary, // content.brand.primary
18 | colorSecondary: contextuals.content.semantic.brand.primary, // content.brand.secondary
19 |
20 | fontBase: globals.font.families.base,
21 |
22 | // UI
23 | appBg: contextuals.background.surface.secondary, // background.surface.tertiary
24 | appContentBg: contextuals.background.surface.tertiary, // background.surface.primary
25 | appBorderColor: contextuals.border.surface.primary, // border.surface.primary
26 | appBorderRadius: 4,
27 |
28 | // Text colors
29 | textColor: contextuals.content.semantic.neutral.primary, // content.neutral.primary
30 | textInverseColor: contextuals.content.semantic.neutral.secondary, // content.neutral.secondary
31 | textMutedColor: contextuals.content.semantic.neutral.tertiary,
32 |
33 | // Toolbar default and active colors
34 | barTextColor: contextuals.content.semantic.neutral.tertiary, // content.neutral.tertiary
35 | barSelectedColor: contextuals.content.semantic.neutral.primary, // content.neutral.primary
36 | barSelectedTextColor: contextuals.content.semantic.neutral.primary, // content.neutral.primary
37 | barBg: contextuals.background.surface.primary, // background.surface.primary
38 |
39 | // Form colors
40 | inputBg: contextuals.background.surface.primary, // background.surface.primary
41 | inputBorder: contextuals.border.semantic.neutral.secondary, // border.neutral.secondary
42 | inputTextColor: contextuals.content.semantic.neutral.primary, // content.neutral.primary
43 | inputBorderRadius: 2,
44 |
45 | // Code preview colors
46 | codeBg: contextuals.background.surface.secondary, // background.surface.secondary
47 | codeColor: contextuals.content.semantic.neutral.primary, // content.neutral.primary
48 | };
49 | };
50 |
51 | export const themes = {
52 | default: create({
53 | base: "light",
54 | ...buildTheme(tokens.themes.default),
55 | }),
56 | dark: create({
57 | base: "dark",
58 | ...buildTheme(tokens.themes.dark as DesignTokens, "dark"),
59 | }),
60 | };
61 |
62 | export const Themes = {
63 | 'dsfr-light': ['dsfr-light', 'DSFR light'],
64 | 'dsfr-dark': ['dsfr-dark', 'DSFR dark'],
65 | 'white-label-light': ['default', 'White label light'],
66 | 'white-label-dark': ['dark', 'White label dark'],
67 | 'anct-light': ['anct-light', 'ANCT light'],
68 | 'anct-dark': ['anct-dark', 'ANCT dark'],
69 | }
70 |
71 | export const BACKGROUND_COLOR_TO_THEME = {
72 | "#FEFFFF": 'dsfr-light',
73 | "#2B303D": 'dsfr-dark',
74 | "#FFFEFF": 'anct-light',
75 | "#283044": 'anct-dark',
76 | "#FFFFFF": 'white-label-light',
77 | "#2F2F40": 'white-label-dark',
78 | };
79 |
80 | export const getThemeFromGlobals = (globals): string => {
81 | const themeKey = BACKGROUND_COLOR_TO_THEME[globals.backgrounds?.value] ?? 'dsfr-light';
82 | return Themes[themeKey][0];
83 | };
84 |
--------------------------------------------------------------------------------
/src/components/dropdown-menu/DropdownMenu.tsx:
--------------------------------------------------------------------------------
1 | import { Menu, MenuItem, Popover, Separator } from "react-aria-components";
2 | import { DropdownMenuOption } from "./types";
3 | import { Fragment, PropsWithChildren, useId, useRef } from "react";
4 |
5 | export type DropdownMenuProps = {
6 | options: DropdownMenuOption[];
7 | onOpenChange?: (isOpen: boolean) => void;
8 | selectedValues?: string[];
9 | onSelectValue?: (value: string) => void;
10 | isOpen?: boolean;
11 | topMessage?: string;
12 | shouldCloseOnInteractOutside?: (element: Element) => boolean;
13 | };
14 |
15 | export const DropdownMenu = ({
16 | options,
17 | isOpen = false,
18 | onOpenChange,
19 | children,
20 | selectedValues = [],
21 | onSelectValue,
22 | topMessage,
23 | shouldCloseOnInteractOutside,
24 | }: PropsWithChildren) => {
25 | const id = useId();
26 | const onOpenChangeHandler = (isOpen: boolean) => {
27 | onOpenChange?.(isOpen);
28 | };
29 |
30 | const triggerRef = useRef(null);
31 | return (
32 | <>
33 | {
38 | e.stopPropagation();
39 | e.preventDefault();
40 | }}
41 | >
42 | {children}
43 |
44 |
45 |
54 |
97 |
98 | >
99 | );
100 | };
101 |
--------------------------------------------------------------------------------
/src/components/quick-search/quick-search.stories.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-hooks/rules-of-hooks */
2 | import type { Meta, StoryObj } from "@storybook/react";
3 |
4 | import { QuickSearch } from "./QuickSearch";
5 | import { QuickSearchGroup } from "./QuickSearchGroup";
6 | import { Button, Modal, ModalSize } from "@gouvfr-lasuite/cunningham-react";
7 | import { useState } from "react";
8 | import { QuickSearchData } from "./types";
9 | import { QuickSearchItemTemplate } from "./QuickSearchItemTemplate";
10 |
11 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
12 | const meta = {
13 | title: "Components/QuickSearch",
14 | component: QuickSearch,
15 |
16 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
17 | } satisfies Meta;
18 |
19 | type Data = {
20 | id: string;
21 | name: string;
22 | description: string;
23 | };
24 |
25 | export default meta;
26 | type Story = StoryObj;
27 | const initialGroup: QuickSearchData = {
28 | groupName: "Group 1",
29 | showWhenEmpty: false,
30 | elements: [
31 | {
32 | id: "1",
33 | name: "Amélie Goujon",
34 | description: "Amélie Goujon is a software engineer at Openfun",
35 | },
36 | {
37 | id: "2",
38 | name: "John Doe",
39 | description: "John Doe is a software engineer at Openfun",
40 | },
41 | {
42 | id: "3",
43 | name: "Jane Doe",
44 | description: "Jane Doe is a software engineer at Openfun",
45 | },
46 | ],
47 | };
48 | export const Small: Story = {
49 | render: () => {
50 | const [open, setOpen] = useState(false);
51 | const [selected, setSelected] = useState(null);
52 |
53 | const [group, setGroup] = useState(initialGroup);
54 | const onFilter = (filter: string) => {
55 | if (filter.length === 0) {
56 | setGroup(initialGroup);
57 | } else {
58 | setGroup({
59 | ...group,
60 | elements: group.elements.filter((element) =>
61 | element.name.includes(filter)
62 | ),
63 | });
64 | }
65 | };
66 | return (
67 |
68 |
69 | {selected && (
70 |
71 | Selected: {selected.name}
72 |
73 | )}
74 |
75 |
setOpen(false)}
78 | size={ModalSize.LARGE}
79 | >
80 |
81 |
82 | {
85 | setSelected(element);
86 | setOpen(false);
87 | }}
88 | renderElement={(element) => (
89 | {element.name}}
91 | right={add}
92 | />
93 | )}
94 | />
95 |
96 |
97 |
98 |
99 | );
100 | },
101 | };
102 |
--------------------------------------------------------------------------------
/src/components/quick-search/quick-search-global-style.scss:
--------------------------------------------------------------------------------
1 | .quick-search-container {
2 | [cmdk-root] {
3 | width: 100%;
4 |
5 | border-radius: 12px;
6 | overflow: hidden;
7 | transition: transform 100ms ease;
8 | outline: none;
9 | }
10 |
11 | [cmdk-input] {
12 | border: none;
13 | width: 100%;
14 | font-size: 17px;
15 | background: var(--c--contextuals--background--surface--primary);
16 | outline: none;
17 | color: var(--c--contextuals--content--semantic--neutral--primary);
18 | border-radius: 0;
19 |
20 | &::placeholder {
21 | color: var(--c--contextuals--content--semantic--neutral--tertiary);
22 | }
23 | }
24 |
25 | [cmdk-item] {
26 | content-visibility: auto;
27 | cursor: pointer;
28 | border-radius: var(--c--globals--spacings--xs);
29 | font-size: 14px;
30 | display: flex;
31 | align-items: center;
32 | gap: 8px;
33 | user-select: none;
34 | will-change: background, color;
35 | transition: all 150ms ease;
36 | transition-property: none;
37 |
38 | .show-right-on-focus {
39 | opacity: 0;
40 | }
41 |
42 | &:hover,
43 | &[data-selected="true"] {
44 | background: var(
45 | --c--contextuals--background--semantic--neutral--tertiary
46 | );
47 | .show-right-on-focus {
48 | opacity: 1;
49 | }
50 | }
51 |
52 | &[data-disabled="true"] {
53 | background: var(
54 | var(--c--contextuals--background--semantic--disabled--primary)
55 | );
56 | cursor: not-allowed;
57 | }
58 |
59 | & + [cmdk-item] {
60 | margin-top: 4px;
61 | }
62 | }
63 |
64 | [cmdk-list] {
65 | flex: 1;
66 | overflow-y: auto;
67 | overscroll-behavior: contain;
68 | }
69 |
70 | [cmdk-vercel-shortcuts] {
71 | display: flex;
72 | margin-left: auto;
73 | gap: 8px;
74 |
75 | kbd {
76 | font-size: 12px;
77 | min-width: 20px;
78 | padding: 4px;
79 | height: 20px;
80 | border-radius: 4px;
81 | color: white;
82 | background: var(
83 | --c--contextuals--background--semantic--neutral--tertiary
84 | );
85 | display: inline-flex;
86 | align-items: center;
87 | justify-content: center;
88 | text-transform: uppercase;
89 | }
90 | }
91 |
92 | [cmdk-separator] {
93 | height: 1px;
94 | width: 100%;
95 | background: var(--c--contextuals--background--semantic--neutral--tertiary);
96 | margin: 4px 0;
97 | }
98 |
99 | *:not([hidden]) + [cmdk-group] {
100 | margin-top: 8px;
101 | }
102 |
103 | [cmdk-group-heading] {
104 | user-select: none;
105 | font-size: var(--c--globals--font--sizes--sm);
106 | color: var(--c--contextuals--content--semantic--neutral--primary);
107 | font-weight: bold;
108 |
109 | display: flex;
110 | align-items: center;
111 | margin-bottom: var(--c--globals--spacings--xs);
112 | }
113 | }
114 |
115 | .c__modal__scroller:has(.quick-search-container),
116 | .c__modal__scroller:has(.no-padding),
117 | .c__modal__scroller:has(.noPadding) {
118 | padding: 0 !important;
119 |
120 | .c__modal__close .c__button {
121 | right: 5px;
122 | top: 5px;
123 | padding: 1.5rem 1rem;
124 | }
125 |
126 | .c__modal__title {
127 | font-size: var(--c--globals--font--sizes--xs);
128 | padding: var(--c--globals--spacings--sm) var(--c--globals--spacings--base);
129 | margin-bottom: 0;
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/components/language/language-picker.tsx:
--------------------------------------------------------------------------------
1 | import { Button, ButtonProps } from "@gouvfr-lasuite/cunningham-react";
2 | import { useEffect, useMemo, useState } from "react";
3 | import {
4 | useDropdownMenu,
5 | DropdownMenu,
6 | DropdownMenuOption,
7 | } from ":/components/dropdown-menu";
8 | import { Icon, IconSize } from ":/components/icon";
9 |
10 | export type LanguagesOption = DropdownMenuOption & {
11 | shortLabel?: string;
12 | };
13 |
14 | export type LanguagePickerProps = {
15 | languages: LanguagesOption[];
16 | onChange?: (language: string) => void;
17 | color?: ButtonProps["color"];
18 | variant?: ButtonProps["variant"];
19 | size?: ButtonProps["size"];
20 | fullWidth?: ButtonProps["fullWidth"];
21 | compact?: boolean;
22 | };
23 |
24 | /**
25 | * A DropdownMenu specific to languages.
26 | *
27 | * **Props:**
28 | * - `languages`: The languages options (use `isChecked` attribute to set the default selected language).
29 | * - `onChange`: A callback called when a language is selected.
30 | * - `size`: The size of the CTA.
31 | * - `color`: The color of the CTA.
32 | */
33 | export const LanguagePicker = ({
34 | languages,
35 | onChange,
36 | size,
37 | color = "brand",
38 | variant = "tertiary",
39 | fullWidth,
40 | compact: lightMode = false,
41 | }: LanguagePickerProps) => {
42 | const { isOpen, setIsOpen } = useDropdownMenu();
43 | const getInitialLanguage = () => {
44 | const selectedLanguage = languages.find((lang) => lang.isChecked);
45 | return selectedLanguage?.value ?? languages[0].value!;
46 | };
47 | const [selectedLanguage, setSelectedLanguage] =
48 | useState(getInitialLanguage);
49 |
50 | const handleLanguageChange = (value: string) => {
51 | setSelectedLanguage(value);
52 | onChange?.(value);
53 | };
54 |
55 | const iconSize = useMemo((): IconSize | undefined => {
56 | if (!size) return undefined;
57 | switch (size) {
58 | case "nano":
59 | return IconSize.X_SMALL;
60 | case "small":
61 | return IconSize.SMALL;
62 | case "medium":
63 | return IconSize.MEDIUM;
64 | }
65 | }, [size]);
66 |
67 | /**
68 | * Effect to update the selected language when the `languages` prop changes.
69 | */
70 | useEffect(() => {
71 | setSelectedLanguage(getInitialLanguage);
72 | // eslint-disable-next-line react-hooks/exhaustive-deps
73 | }, [languages]);
74 |
75 | const selectedLanguageOption = useMemo(() => {
76 | const lang = languages.find((lang) => lang.value === selectedLanguage);
77 | return lang?.shortLabel ?? lang?.label;
78 | }, [languages, selectedLanguage]);
79 |
80 | return (
81 |
88 |
103 |
104 | );
105 | };
106 |
--------------------------------------------------------------------------------
/src/components/filter/Filter.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from "@storybook/react";
2 |
3 | import { Filter, FilterOption } from "./Filter";
4 | import { useState } from "react";
5 | import { Key } from "react-aria-components";
6 | import { Button } from "@gouvfr-lasuite/cunningham-react";
7 |
8 | const meta: Meta = {
9 | title: "Components/Filter",
10 | component: Filter,
11 | tags: ["autodocs"],
12 | parameters: {
13 | layout: "fullscreen",
14 | },
15 | decorators: [
16 | (Story) => (
17 |
18 | {/* 👇 Decorators in Storybook also accept a function. Replace with Story() to enable it */}
19 |
20 |
21 | ),
22 | ],
23 | };
24 |
25 | export default meta;
26 | type Story = StoryObj;
27 |
28 | const OPTIONS: FilterOption[] = [
29 | {
30 | label: "All",
31 | value: "all",
32 | },
33 | {
34 | label: "File",
35 | value: "file",
36 | },
37 | {
38 | label: "Folder",
39 | value: "folder",
40 | },
41 | ];
42 |
43 | const OPTIONS_CUSTOM: FilterOption[] = [
44 | {
45 | label: "File",
46 | render: () => (
47 |
48 | file_present File
49 |
50 | ),
51 | value: "file",
52 | },
53 | {
54 | label: "Folder",
55 | value: "folder",
56 | showSeparator: true,
57 |
58 | render: () => (
59 |
60 | folder Folder
61 |
62 | ),
63 | },
64 | {
65 | label: "Reset",
66 | render: () => (
67 |
68 | all_inclusive All
69 |
70 | ),
71 | value: "all",
72 | },
73 | ];
74 |
75 | export const Uncontrolled: Story = {
76 | args: {
77 | label: "Type",
78 | options: OPTIONS,
79 | },
80 | };
81 |
82 | export const UncontrolledWithDefault: Story = {
83 | args: {
84 | label: "Type",
85 | defaultSelectedKey: "folder",
86 | options: OPTIONS,
87 | },
88 | };
89 |
90 | export const Controlled: Story = {
91 | args: {
92 | label: "Type",
93 | options: OPTIONS_CUSTOM,
94 | },
95 | render: (args) => {
96 | // eslint-disable-next-line react-hooks/rules-of-hooks
97 | const [selected, setSelected] = useState("folder");
98 | return (
99 |
100 |
{
104 | if (key === "all") {
105 | setSelected(null);
106 | } else {
107 | setSelected(key);
108 | }
109 | }}
110 | />
111 |
112 |
115 |
127 |
128 |
129 | );
130 | },
131 | };
132 |
--------------------------------------------------------------------------------
/src/components/modal/modal.stories.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Input,
4 | Modal,
5 | ModalSize,
6 | useModal,
7 | } from "@gouvfr-lasuite/cunningham-react";
8 | import type { Meta, StoryObj } from "@storybook/react";
9 | import { useEffect } from "react";
10 |
11 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
12 | const meta = {
13 | title: "Components/Modal",
14 | component: Modal,
15 | args: {
16 | children: "Description",
17 | title: "Title",
18 | isOpen: false,
19 | onClose: () => {},
20 | },
21 | decorators: [
22 | (Story, context) => {
23 | const modal = useModal();
24 |
25 | useEffect(() => {
26 | modal.open();
27 | // eslint-disable-next-line react-hooks/exhaustive-deps
28 | }, []);
29 |
30 | return (
31 | <>
32 |
33 |
34 | >
35 | );
36 | },
37 | ],
38 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
39 | } satisfies Meta;
40 |
41 | export default meta;
42 | type Story = StoryObj;
43 |
44 | export const ExampleCreateFolder: Story = {
45 | args: {
46 | size: ModalSize.SMALL,
47 | children: (
48 |
55 | ),
56 | rightActions: (
57 | <>
58 |
59 |
60 | >
61 | ),
62 | },
63 | };
64 |
65 | export const Small: Story = {
66 | args: {
67 | size: ModalSize.SMALL,
68 | },
69 | };
70 | export const Medium: Story = {
71 | args: {
72 | size: ModalSize.MEDIUM,
73 | },
74 | };
75 |
76 | export const Large: Story = {
77 | args: {
78 | size: ModalSize.LARGE,
79 | },
80 | };
81 | export const ExtraLarge: Story = {
82 | args: {
83 | size: ModalSize.EXTRA_LARGE,
84 | },
85 | };
86 | export const Full: Story = {
87 | args: {
88 | size: ModalSize.FULL,
89 | },
90 | };
91 |
92 | export const HideCloseButton: Story = {
93 | args: {
94 | size: ModalSize.MEDIUM,
95 | hideCloseButton: true,
96 | },
97 | };
98 | export const CloseOnClickOutside: Story = {
99 | args: {
100 | size: ModalSize.MEDIUM,
101 | hideCloseButton: true,
102 | closeOnClickOutside: true,
103 | },
104 | };
105 | export const DontCloseOnEsc: Story = {
106 | args: {
107 | size: ModalSize.MEDIUM,
108 | closeOnEsc: false,
109 | },
110 | };
111 | export const PreventClose: Story = {
112 | args: {
113 | size: ModalSize.MEDIUM,
114 | preventClose: true,
115 | },
116 | };
117 |
118 | export const ExampleApplication: Story = {
119 | args: {
120 | size: ModalSize.LARGE,
121 | title: "Application successful",
122 | titleIcon: done,
123 | children: (
124 | <>
125 | Thank you for submitting your application! Your information has been
126 | received successfully.
127 |
128 | You will receive a confirmation email shortly with the details of your
129 | submission. If there are any further steps required our team will be in
130 | touch.
131 | >
132 | ),
133 | rightActions: ,
134 | },
135 | };
136 |
--------------------------------------------------------------------------------