diff --git a/package.json b/package.json index 7691e8622..4796fab9d 100755 --- a/package.json +++ b/package.json @@ -128,6 +128,7 @@ "./Notice": "./dist/Notice.js", "./Header": "./dist/Header/index.js", "./DarkModeSwitch": "./dist/DarkModeSwitch.js", + "./Badge": "./dist/Badge.js", "./Alert": "./dist/Alert.js", "./Accordion": "./dist/Accordion.js" } diff --git a/src/Badge.tsx b/src/Badge.tsx new file mode 100644 index 000000000..28357b934 --- /dev/null +++ b/src/Badge.tsx @@ -0,0 +1,54 @@ +import React, { memo, forwardRef, ReactNode } from "react"; +import { symToStr } from "tsafe/symToStr"; +import { assert } from "tsafe/assert"; +import type { Equals } from "tsafe"; +import { fr } from "./lib"; +import { cx } from "./lib/tools/cx"; + +// We make users import dsfr.css, so we don't need to import the scoped CSS +// but in the future if we have a complete component coverage it +// we could stop requiring users to import the hole CSS and only import on a +// per component basis. +import "./dsfr/component/badge/badge.css"; + +export type BadgeProps = { + className?: string; + severity?: BadgeProps.Severity; + isSmall?: boolean; + noIcon?: boolean; + label: NonNullable; +}; + +export namespace BadgeProps { + export type Severity = "info" | "success" | "error" | "warning" | "new"; +} + +/** @see */ +export const Badge = memo( + forwardRef(props => { + const { className, severity, label, isSmall, noIcon, ...rest } = props; + + assert>(); + + return ( +

+ {label} +

+ ); + }) +); + +Badge.displayName = symToStr({ Badge }); + +export default Badge; diff --git a/stories/Badge.stories.tsx b/stories/Badge.stories.tsx new file mode 100644 index 000000000..9575bfee6 --- /dev/null +++ b/stories/Badge.stories.tsx @@ -0,0 +1,109 @@ +import { Badge } from "../dist/Badge"; +import type { BadgeProps } from "../dist/Badge"; +import { sectionName } from "./sectionName"; +import { getStoryFactory } from "./getStory"; +import { assert } from "tsafe/assert"; +import type { Equals } from "tsafe"; + +const { meta, getStory } = getStoryFactory({ + sectionName, + wrappedComponent: { Badge }, + description: ` +- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/badge) +- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Badge.tsx)`, + argTypes: { + severity: { + options: (() => { + const severities = ["success", "warning", "info", "error", "new"] as const; + + assert>(); + + return [null, ...severities]; + })(), + control: { type: "select", labels: { null: "no severity" } } + }, + noIcon: { + type: { name: "boolean" }, + description: "Remove badge icon when true" + }, + isSmall: { + type: { name: "boolean" }, + description: "Set small badge size (`sm`) when true" + }, + label: { + type: { name: "string", required: true }, + description: "Label to display on tne badge" + } + }, + disabledProps: ["lang"] +}); + +export default meta; + +export const Default = getStory({ + severity: "success", + label: "Label badge" +}); + +export const BadgeWithoutSeverity = getStory( + { + label: "Label" + }, + { + description: "Medium info `Badge` with icon" + } +); + +export const InfoBadge = getStory( + { + severity: "info", + label: "Label info" + }, + { + description: "Medium info `Badge` with icon" + } +); + +export const WarningBadge = getStory( + { + severity: "warning", + noIcon: false, + label: 'Label "warning"' + }, + { + description: "Medium warning `Badge` with icon" + } +); + +export const SuccessBadge = getStory( + { + severity: "success", + noIcon: true, + label: "Label success" + }, + { + description: "Medium success `Badge` without icon" + } +); + +export const ErrorBadge = getStory( + { + severity: "error", + noIcon: true, + label: "Label error" + }, + { + description: "Medium error `Badge` without icon" + } +); + +export const NewBadge = getStory( + { + severity: "new", + isSmall: true, + label: "Label new" + }, + { + description: "Small new `Badge` with icon" + } +);