diff --git a/services/web/frontend/js/shared/components/ds/ds-form-control.tsx b/services/web/frontend/js/shared/components/ds/ds-form-control.tsx new file mode 100644 index 0000000000..d41bb3fdbc --- /dev/null +++ b/services/web/frontend/js/shared/components/ds/ds-form-control.tsx @@ -0,0 +1,22 @@ +import { forwardRef } from 'react' +import { Form, FormControlProps } from 'react-bootstrap' +import classnames from 'classnames' + +interface ButtonProps extends FormControlProps { + size?: 'lg' +} + +const DSFormControl = forwardRef( + ({ className, ...props }, ref) => { + return ( + + ) + } +) +DSFormControl.displayName = 'DSFormControl' + +export default DSFormControl diff --git a/services/web/frontend/js/shared/components/ds/ds-form-feedback.tsx b/services/web/frontend/js/shared/components/ds/ds-form-feedback.tsx new file mode 100644 index 0000000000..e3955754e9 --- /dev/null +++ b/services/web/frontend/js/shared/components/ds/ds-form-feedback.tsx @@ -0,0 +1,20 @@ +import { Form } from 'react-bootstrap' +import FormText from '@/shared/components/form/form-text' +import { ComponentProps } from 'react' + +export type FormFeedbackProps = Pick< + ComponentProps, + 'type' | 'className' | 'children' +> + +function DSFormFeedback(props: FormFeedbackProps) { + return ( + + + {props.children} + + + ) +} + +export default DSFormFeedback diff --git a/services/web/frontend/js/shared/components/ds/ds-form-group.tsx b/services/web/frontend/js/shared/components/ds/ds-form-group.tsx new file mode 100644 index 0000000000..78d857da57 --- /dev/null +++ b/services/web/frontend/js/shared/components/ds/ds-form-group.tsx @@ -0,0 +1,18 @@ +import { forwardRef } from 'react' +import { Form, FormGroupProps } from 'react-bootstrap' +import classnames from 'classnames' + +const DSFormGroup = forwardRef( + ({ className, ...props }, ref) => { + return ( + + ) + } +) +DSFormGroup.displayName = 'DSFormGroup' + +export default DSFormGroup diff --git a/services/web/frontend/js/shared/components/ds/ds-form-label.tsx b/services/web/frontend/js/shared/components/ds/ds-form-label.tsx new file mode 100644 index 0000000000..98a5e0d6be --- /dev/null +++ b/services/web/frontend/js/shared/components/ds/ds-form-label.tsx @@ -0,0 +1,14 @@ +import { ComponentProps } from 'react' +import { Form } from 'react-bootstrap' +import classnames from 'classnames' + +function DSFormLabel({ + className, + ...props +}: ComponentProps) { + return ( + + ) +} + +export default DSFormLabel diff --git a/services/web/frontend/js/shared/components/ds/ds-form-text.tsx b/services/web/frontend/js/shared/components/ds/ds-form-text.tsx new file mode 100644 index 0000000000..7b0014f1bf --- /dev/null +++ b/services/web/frontend/js/shared/components/ds/ds-form-text.tsx @@ -0,0 +1,48 @@ +import { Form, FormTextProps as BS5FormTextProps } from 'react-bootstrap' +import classnames from 'classnames' +import { MergeAndOverride } from '@ol-types/utils' +import { CheckCircle, WarningCircle } from '@phosphor-icons/react' + +type TextType = 'success' | 'error' + +export type FormTextProps = MergeAndOverride< + BS5FormTextProps, + { type?: TextType } +> + +const typeClasses = { + error: 'text-danger', + success: 'text-success', +} as const + +export const getFormTextClass = (type?: TextType) => + type && type in typeClasses + ? typeClasses[type as keyof typeof typeClasses] + : undefined + +function FormTextIcon({ type }: { type?: TextType }) { + switch (type) { + case 'success': + return + case 'error': + return + default: + return null + } +} + +function DSFormText({ type, children, className, ...rest }: FormTextProps) { + return ( + + + + {children} + + + ) +} + +export default DSFormText diff --git a/services/web/frontend/stories/shared/ds-button.stories.tsx b/services/web/frontend/stories/shared/ds-button.stories.tsx index 359e483cb9..fd0b916612 100644 --- a/services/web/frontend/stories/shared/ds-button.stories.tsx +++ b/services/web/frontend/stories/shared/ds-button.stories.tsx @@ -9,7 +9,7 @@ export const Button = (args: Args) => { } const meta: Meta = { - title: 'Shared / Components / DS Button', + title: 'Shared / DS Components', component: DSButton, args: { children: 'Button', diff --git a/services/web/frontend/stories/shared/ds-form-control.stories.tsx b/services/web/frontend/stories/shared/ds-form-control.stories.tsx new file mode 100644 index 0000000000..ee42914dd2 --- /dev/null +++ b/services/web/frontend/stories/shared/ds-form-control.stories.tsx @@ -0,0 +1,58 @@ +import { Meta } from '@storybook/react' +import { figmaDesignUrl } from '../../../.storybook/utils/figma-design-url' +import DSFormControl from '@/shared/components/ds/ds-form-control' +import DSFormText from '@/shared/components/ds/ds-form-text' +import DSFormGroup from '@/shared/components/ds/ds-form-group' +import DSFormLabel from '@/shared/components/ds/ds-form-label' +import { ComponentProps } from 'react' + +type Args = ComponentProps & { + textType: ComponentProps['type'] +} + +export const FormControl = ({ textType, value, ...args }: Args) => { + return ( + + Form input label + + Form input feedback + + ) +} + +const meta: Meta = { + title: 'Shared / DS Components', + component: FormControl, + argTypes: { + disabled: { control: 'boolean' }, + isInvalid: { control: 'boolean' }, + placeholder: { control: 'text' }, + readOnly: { control: 'boolean' }, + value: { control: 'text' }, + size: { control: 'radio', options: ['lg', undefined] }, + textType: { control: 'radio', options: ['error', 'success', undefined] }, + }, + parameters: { + controls: { + include: [ + 'disabled', + 'isInvalid', + 'placeholder', + 'readOnly', + 'size', + 'value', + 'textType', + ], + }, + ...figmaDesignUrl( + 'https://www.figma.com/design/aJQlecvqCS9Ry8b6JA1lQN/DS---Components?node-id=6318-428&t=pcx9KKzhlzpRmA4S-0' + ), + }, +} + +export default meta diff --git a/services/web/frontend/stylesheets/ds/components/all.scss b/services/web/frontend/stylesheets/ds/components/all.scss index 2d43224818..65516dff8d 100644 --- a/services/web/frontend/stylesheets/ds/components/all.scss +++ b/services/web/frontend/stylesheets/ds/components/all.scss @@ -1 +1,2 @@ @import 'button'; +@import 'form-control'; diff --git a/services/web/frontend/stylesheets/ds/components/button.scss b/services/web/frontend/stylesheets/ds/components/button.scss index 915537ffa8..4b6d857e6a 100644 --- a/services/web/frontend/stylesheets/ds/components/button.scss +++ b/services/web/frontend/stylesheets/ds/components/button.scss @@ -1,11 +1,11 @@ +@import '../mixins'; + .btn.btn-ds { --bs-btn-font-family: var(--ds-font-family-sans); --bs-btn-border-radius: var(--ds-border-radius-200); &:focus-visible { - outline: 2px solid var(--ds-color-yellow-500); - outline-offset: 2px; - box-shadow: none; + @include ds-focus-outline; } // Default size diff --git a/services/web/frontend/stylesheets/ds/components/form-control.scss b/services/web/frontend/stylesheets/ds/components/form-control.scss new file mode 100644 index 0000000000..88cc0eaffd --- /dev/null +++ b/services/web/frontend/stylesheets/ds/components/form-control.scss @@ -0,0 +1,85 @@ +@use 'sass:math'; +@import '../mixins'; + +.form-control-ds, +.form-group-ds, +.form-text-ds, +.form-label-ds { + --bs-body-font-family: var(--ds-font-family-sans), sans-serif; + --bs-success-rgb: 25, 117, 76; // #19754c + --bs-danger-rgb: 195, 9, 43; // #c3092b + --content-placeholder: var(--ds-color-text-disabled); + + // Without this, it inherits the body's --bs-body-font-family which isn't the DS font + font-family: var(--ds-font-family-sans), sans-serif; +} + +input.form-control.form-control-ds { + border-radius: var(--ds-border-radius-200); + border-color: var(--ds-color-neutral-300); + color: var(--ds-color-neutral-950); + padding: calc(var(--ds-spacing-250) - 1px) calc(var(--ds-spacing-300) - 1px); + + &.form-control-lg { + padding: calc(var(--ds-spacing-350) - 1px) calc(var(--ds-spacing-300) - 1px); + } + + &:read-only { + background: var(--ds-color-neutral-200); + color: var(--ds-color-text-secondary); + } + + &:disabled { + color: var(--ds-color-text-disabled); + background: var(--ds-color-neutral-100); + } + + &:not(:disabled, :focus-visible) { + &:hover, + &.hover { + border-color: var(--ds-color-neutral-400); + } + } + + &:focus-visible, + &:focus { + @include ds-focus-outline; + + border-color: var(--ds-color-neutral-950); + } + + &.is-invalid, + .was-validated &:invalid { + border-color: var(--ds-color-red-500); + } +} + +.form-label.form-label-ds { + @include ds-body-md-semibold; + + color: var(--ds-color-text-primary); + + &:has(+ .form-control:read-only) { + color: var(--ds-color-text-secondary); + } + + &:has(+ .form-control:disabled) { + color: var(--ds-color-text-disabled); + } +} + +.form-text.form-text-ds { + color: var(--ds-color-text-secondary); + + .form-text-inner-ds { + display: flex; + gap: var(--ds-spacing-200); + margin-top: var(--ds-spacing-150); + + @include ds-body-sm-regular; + } + + .ciam-form-text-icon { + font-size: math.div(20em, 14); + } +} diff --git a/services/web/frontend/stylesheets/ds/mixins.scss b/services/web/frontend/stylesheets/ds/mixins.scss index b50207c20f..376f40d816 100644 --- a/services/web/frontend/stylesheets/ds/mixins.scss +++ b/services/web/frontend/stylesheets/ds/mixins.scss @@ -107,3 +107,10 @@ font-weight: var(--ds-font-weight-semibold); line-height: var(--ds-font-line-height-1200); } + +// Focus outline +@mixin ds-focus-outline() { + box-shadow: none; + outline: 2px solid var(--ds-color-yellow-500); + outline-offset: 2px; +}