From 167153ca32e8e24ce968a8a592030dd3b288c3f0 Mon Sep 17 00:00:00 2001 From: Alexander Polyankin <alexander.polyankin@metabase.com> Date: Fri, 25 Aug 2023 23:10:46 +0300 Subject: [PATCH] Mantine TextInput and NumberInput (#33381) --- .../SearchFilterModal/filters/TypeFilter.tsx | 25 +- .../buttons/Button/Button.stories.mdx | 2 +- .../buttons/Button/Button.styled.tsx | 2 +- .../feedback/Loader/Loader.stories.mdx | 2 +- .../ui/components/feedback/Loader/Loader.tsx | 4 +- .../inputs/Checkbox/Checkbox.stories.mdx | 183 +++++--- .../inputs/Checkbox/Checkbox.styled.tsx | 123 ++--- .../components/inputs/Input/Input.styled.tsx | 139 ++++++ .../ui/components/inputs/Input/index.ts | 1 + .../NumberInput/NumberInput.stories.mdx | 435 ++++++++++++++++++ .../inputs/NumberInput/NumberInput.styled.tsx | 45 ++ .../ui/components/inputs/NumberInput/index.ts | 2 + .../components/inputs/Radio/Radio.stories.mdx | 149 +++--- .../components/inputs/Radio/Radio.styled.tsx | 89 +++- .../inputs/TextInput/TextInput.stories.mdx | 318 +++++++++++++ .../inputs/TextInput/TextInput.styled.tsx | 15 + .../ui/components/inputs/TextInput/index.ts | 2 + .../metabase/ui/components/inputs/index.ts | 5 +- .../components/overlays/Menu/Menu.stories.mdx | 2 +- .../components/overlays/Menu/Menu.styled.tsx | 4 +- .../typography/Anchor/Anchor.stories.mdx | 114 ++--- .../typography/Anchor/Anchor.styled.tsx | 5 - .../typography/Text/Text.stories.mdx | 150 ++++-- .../typography/Text/Text.styled.tsx | 13 + .../typography/Title/Title.stories.mdx | 27 +- frontend/src/metabase/ui/theme.ts | 10 +- 26 files changed, 1497 insertions(+), 369 deletions(-) create mode 100644 frontend/src/metabase/ui/components/inputs/Input/Input.styled.tsx create mode 100644 frontend/src/metabase/ui/components/inputs/Input/index.ts create mode 100644 frontend/src/metabase/ui/components/inputs/NumberInput/NumberInput.stories.mdx create mode 100644 frontend/src/metabase/ui/components/inputs/NumberInput/NumberInput.styled.tsx create mode 100644 frontend/src/metabase/ui/components/inputs/NumberInput/index.ts create mode 100644 frontend/src/metabase/ui/components/inputs/TextInput/TextInput.stories.mdx create mode 100644 frontend/src/metabase/ui/components/inputs/TextInput/TextInput.styled.tsx create mode 100644 frontend/src/metabase/ui/components/inputs/TextInput/index.ts diff --git a/frontend/src/metabase/search/components/SearchFilterModal/filters/TypeFilter.tsx b/frontend/src/metabase/search/components/SearchFilterModal/filters/TypeFilter.tsx index 41b581f8160..c83e5e2e28f 100644 --- a/frontend/src/metabase/search/components/SearchFilterModal/filters/TypeFilter.tsx +++ b/frontend/src/metabase/search/components/SearchFilterModal/filters/TypeFilter.tsx @@ -30,21 +30,16 @@ export const TypeFilter: SearchFilterComponent<"type"> = ({ <LoadingSpinner /> ) : ( <SearchFilterView data-testid={dataTestId} title={t`Type`}> - <Checkbox.Group - value={value} - onChange={onChange} - data-testid="type-filter-checkbox-group" - inputContainer={children => ( - <TypeCheckboxGroupWrapper>{children}</TypeCheckboxGroupWrapper> - )} - > - {typeFilters.map(model => ( - <Checkbox - key={model} - value={model} - label={getTranslatedEntityName(model)} - /> - ))} + <Checkbox.Group value={value} onChange={onChange}> + <TypeCheckboxGroupWrapper data-testid="type-filter-checkbox-group"> + {typeFilters.map(model => ( + <Checkbox + key={model} + value={model} + label={getTranslatedEntityName(model)} + /> + ))} + </TypeCheckboxGroupWrapper> </Checkbox.Group> </SearchFilterView> ); diff --git a/frontend/src/metabase/ui/components/buttons/Button/Button.stories.mdx b/frontend/src/metabase/ui/components/buttons/Button/Button.stories.mdx index 424af06cf9d..9c979b12f80 100644 --- a/frontend/src/metabase/ui/components/buttons/Button/Button.stories.mdx +++ b/frontend/src/metabase/ui/components/buttons/Button/Button.stories.mdx @@ -69,7 +69,7 @@ Not to use: ## Docs -- [Figma File](https://www.figma.com/file/Ey1rOyIxRHpmRvE9XrGyop/Buttons-%2F-Button?type=design&node-id=1-96&mode=design&t=JaO1GZU7AnpAmx4o-0) +- [Figma File](https://www.figma.com/file/Ey1rOyIxRHpmRvE9XrGyop/Buttons-%2F-Button?type=design&node-id=1-96&mode=design&t=yaNljw178EFJeU7k-0) - [Mantine Button Docs](https://mantine.dev/core/button/) ## Caveats diff --git a/frontend/src/metabase/ui/components/buttons/Button/Button.styled.tsx b/frontend/src/metabase/ui/components/buttons/Button/Button.styled.tsx index dfbf13a3cea..9e432658e2b 100644 --- a/frontend/src/metabase/ui/components/buttons/Button/Button.styled.tsx +++ b/frontend/src/metabase/ui/components/buttons/Button/Button.styled.tsx @@ -19,7 +19,7 @@ export const getButtonOverrides = (): MantineThemeOverride["components"] => ({ height: "auto", padding: compact ? `${rem(3)} ${rem(7)}` : `${rem(11)} ${rem(15)}`, fontSize: theme.fontSizes.md, - lineHeight: "1rem", + lineHeight: theme.lineHeight, [`&:has(.${getStylesRef("label")}:empty)`]: { padding: compact ? `${rem(3)} ${rem(3)}` : `${rem(11)} ${rem(11)}`, [`.${getStylesRef("leftIcon")}`]: { diff --git a/frontend/src/metabase/ui/components/feedback/Loader/Loader.stories.mdx b/frontend/src/metabase/ui/components/feedback/Loader/Loader.stories.mdx index f55b7fbf190..bdf68114ec4 100644 --- a/frontend/src/metabase/ui/components/feedback/Loader/Loader.stories.mdx +++ b/frontend/src/metabase/ui/components/feedback/Loader/Loader.stories.mdx @@ -27,7 +27,7 @@ Our themed wrapper around [Mantine Loader](https://mantine.dev/core/loader/). ## Docs -- [Figma File](https://www.figma.com/file/NUXRUa9Ot3HvgC1WwIA4UH/Loader?node-id=1%3A96) +- [Figma File](https://www.figma.com/file/NUXRUa9Ot3HvgC1WwIA4UH/Loader?type=design&node-id=1-96&mode=design&t=yaNljw178EFJeU7k-0) - [Mantine Loader Docs](https://mantine.dev/core/loader/) ## Caveats diff --git a/frontend/src/metabase/ui/components/feedback/Loader/Loader.tsx b/frontend/src/metabase/ui/components/feedback/Loader/Loader.tsx index 4fc9c2b1b26..97eb97af3e4 100644 --- a/frontend/src/metabase/ui/components/feedback/Loader/Loader.tsx +++ b/frontend/src/metabase/ui/components/feedback/Loader/Loader.tsx @@ -1,4 +1,4 @@ -import { Loader as MantineLoader } from "@mantine/core"; +import { Loader as MantineLoader, getSize } from "@mantine/core"; import type { LoaderProps } from "@mantine/core"; const SIZES: Record<string, string> = { @@ -10,5 +10,5 @@ const SIZES: Record<string, string> = { }; export const Loader = ({ size = "md", ...props }: LoaderProps) => ( - <MantineLoader {...props} size={SIZES[size] ?? size} /> + <MantineLoader {...props} size={getSize({ size, sizes: SIZES })} /> ); diff --git a/frontend/src/metabase/ui/components/inputs/Checkbox/Checkbox.stories.mdx b/frontend/src/metabase/ui/components/inputs/Checkbox/Checkbox.stories.mdx index 5f3003f87e1..acfdbe118f6 100644 --- a/frontend/src/metabase/ui/components/inputs/Checkbox/Checkbox.stories.mdx +++ b/frontend/src/metabase/ui/components/inputs/Checkbox/Checkbox.stories.mdx @@ -1,25 +1,37 @@ import { Canvas, Story, Meta } from "@storybook/addon-docs"; -import { Checkbox } from "./"; - - -<Meta title="Inputs/Checkbox & Checkbox Group" component={Checkbox} - argTypes={ - { - size: { - options: ["xs", "sm", "md", "lg", "xl"], - control: { type: 'inline-radio' } - }, - } - } +import { Checkbox, Stack } from "metabase/ui"; + +export const args = { + label: "Label", + description: "", + disabled: false, + labelPosition: "right", +}; + +export const argTypes = { + label: { + control: { type: "text" }, + }, + description: { + control: { type: "text" }, + }, + disabled: { + control: { type: "boolean" }, + }, + labelPosition: { + options: ["left", "right"], + control: { type: "inline-radio" }, + }, +}; + +<Meta + title="Inputs/Checkbox" + component={Checkbox} + args={args} + argTypes={argTypes} /> -export const Template = args => <Checkbox.Group defaultValue={["yes"]} {...args}> - <Checkbox label="Yes" value="yes" /> - <Checkbox label="No" value="no" /> - <Checkbox label="I'm not sure" indeterminate /> -</Checkbox.Group> - -# Checkbox & Checkbox.Group +# Checkbox Our themed wrapper around [Mantine Checkbox](https://mantine.dev/core/checkbox/). @@ -29,87 +41,110 @@ Checkbox buttons allow users to select a single option from a list of mutually e ## Docs -- [Figma File](https://www.figma.com/file/sF1qSHk6yVqO1rFgmH0nzT/Input-%2F-Checkbox?type=design&node-id=1-96&t=4851xqAoQVenSrTX-11) +- [Figma File](https://www.figma.com/file/sF1qSHk6yVqO1rFgmH0nzT/Input-%2F-Checkbox?type=design&node-id=1-96&mode=design&t=yaNljw178EFJeU7k-0) - [Mantine Checkbox Docs](https://mantine.dev/core/checkbox/) -## Caveats - -- Please don't use the `error` prop on Checkbox components (We'll standardize on this as other inputs get converted). - ## Usage guidelines - **Use this component if there are more than 5 options**. If there are fewer options, feel free to check out Checkbox or Select. - For option ordering, try to use your best judgement on a sensible order. For example, Yes should come before No. Alphabetical ordering is usually a good fallback if there's no inherent order in your set of choices. - In almost all circumstances you'll want to use `<Checkbox.Group>` to provide a set of options and help with defaultValues and state management between them. -## General +## Examples + +export const Template = args => ( + <Stack> + <Checkbox {...args} label="Default checkbox" /> + <Checkbox {...args} label="Indeterminate checkbox" indeterminate /> + <Checkbox + {...args} + label="Indeterminate checked checkbox" + checked + indeterminate + /> + <Checkbox {...args} label="Checked checkbox" checked /> + <Checkbox {...args} label="Disabled checkbox" disabled /> + <Checkbox + {...args} + label="Disabled intermediate checkbox" + disabled + intermediate + /> + <Checkbox + {...args} + label="Disabled indeterminate checked checkbox" + disabled + checked + indeterminate + /> + <Checkbox {...args} label="Disabled checked checkbox" disabled checked /> + </Stack> +); + +export const Default = args => <Checkbox {...args} />; + +<Canvas> + <Story name="Default">{Default}</Story> +</Canvas> + +### Checkbox.Group + +export const CheckboxGroup = args => ( + <Checkbox.Group + defaultValue={["react"]} + label="An array of good frameworks" + description="But which one to use?" + > + <Stack mt="md"> + <Checkbox value="react" label="React" /> + <Checkbox value="svelte" label="Svelte" /> + <Checkbox value="ng" label="Angular" /> + <Checkbox value="vue" label="Vue" /> + </Stack> + </Checkbox.Group> +); <Canvas> - <Story name="Default"> - {Template.bind({})} - </Story> + <Story name="Checkbox group">{CheckboxGroup}</Story> </Canvas> -## Disabled +### Label + +export const Label = args => <Template {...args} />; + +<Canvas> + <Story name="Label">{Label}</Story> +</Canvas> + +#### Left label position + +export const LabelLeft = args => <Template {...args} labelPosition="left" />; <Canvas> - <Story name="Disabled"> - <Checkbox.Group value={["yes"]}> - <Checkbox label="Yes" value="yes" disabled /> - <Checkbox label="No" value="no" disabled /> - <Checkbox label="I'm not sure" indeterminate disabled /> - </Checkbox.Group> - </Story> + <Story name="Label, left position">{LabelLeft}</Story> </Canvas> -## Description +### Description -If needed you can add a description value to a Checkbox button to give more context about the choice. +export const Description = args => ( + <Template {...args} description="Description" /> +); <Canvas> - <Story name="Descriptions"> - <Checkbox.Group defaultValue={["chocolate"]}> - <Checkbox - label="Chocolate" - value="chocolate" - description="One of our most popular flavors" - /> - <Checkbox - label="Vanilla" - value="vanilla" - description="A classic for all seasons" - /> - <Checkbox - label="Strawberry, no description needed!" - value="strawberry" - /> - <Checkbox - label="Swirl" - value="swirl" - description="Why not have it both ways?" - /> - </Checkbox.Group> - </Story> + <Story name="Description">{Description}</Story> </Canvas> -## Using the Checkbox group props +export const DescriptionLeft = args => ( + <Template {...args} description="Description" labelPosition="left" /> +); -You can use `label` and `description` on the Checkbox.Group component to provide a label and description for the set of choices. +#### Left label position <Canvas> - <Story name="Checkbox Group props"> - <Checkbox.Group - label="A set of options" - description="You could so something like this if there was a deprecated setting that a user needs to update." - defaultValue={["one"]} - > - <Checkbox label="Old bad setting" value="one" disabled /> - <Checkbox label="A better newer setting" value="two" /> - </Checkbox.Group> - </Story> + <Story name="Description, left position">{DescriptionLeft}</Story> </Canvas> ## Related components +- Radio - Select -- Checkbox diff --git a/frontend/src/metabase/ui/components/inputs/Checkbox/Checkbox.styled.tsx b/frontend/src/metabase/ui/components/inputs/Checkbox/Checkbox.styled.tsx index 904ec2799f6..5228c0fb68c 100644 --- a/frontend/src/metabase/ui/components/inputs/Checkbox/Checkbox.styled.tsx +++ b/frontend/src/metabase/ui/components/inputs/Checkbox/Checkbox.styled.tsx @@ -1,77 +1,80 @@ -import type { MantineTheme, MantineThemeOverride } from "@mantine/core"; +import { getStylesRef, getSize, rem } from "@mantine/core"; +import type { + CheckboxStylesParams, + MantineTheme, + MantineThemeOverride, +} from "@mantine/core"; import { CheckboxIcon } from "./CheckboxIcon"; +const SIZES = { + md: rem(20), +}; + export const getCheckboxOverrides = (): MantineThemeOverride["components"] => ({ Checkbox: { defaultProps: { icon: CheckboxIcon, size: "md", }, - styles: (theme: MantineTheme, params) => { - return { - root: { - marginBottom: theme.spacing.md, - }, - label: { - fontWeight: 700, - color: theme.colors.text[2], - [`padding${params.labelPosition === "left" ? "Right" : "Left"}`]: - theme.spacing.sm, - }, - input: { - borderRadius: theme.radius.xs, - - "&:focus": { - outline: `2px solid ${theme.colors.brand[1]}`, + styles: ( + theme: MantineTheme, + { labelPosition }: CheckboxStylesParams, + { size = "md" }, + ) => ({ + root: { + [`&:has(.${getStylesRef("input")}:disabled)`]: { + [`.${getStylesRef("label")}`]: { + color: theme.colors.text[0], }, - "&:disabled": { - background: theme.colors.border[0], - border: 0, - "& + svg > *": { - fill: theme.colors.text[0], - }, + [`.${getStylesRef("description")}`]: { + color: theme.colors.text[0], + }, + [`.${getStylesRef("icon")}`]: { + color: theme.colors.text[0], }, - cursor: "pointer", - ...(params.indeterminate && { - background: theme.colors.brand[1], - border: `1px solid ${theme.colors.brand[1]}`, - }), - transform: `scale(0.75)`, - }, - icon: { - ...(params.indeterminate && { - "& > *": { - fill: theme.white, - }, - }), }, - }; - }, - }, - CheckboxGroup: { - defaultProps: { - size: "md", - }, - styles: (theme: MantineTheme) => { - /* Note: we need the ':has' selector to target the space just - * above the first checkbox since we don't seem to have selector - * or a way to use params to detect whether group label/description - * exists. This is a bit of a hack, but it works. */ + }, + inner: { + width: getSize({ size, sizes: SIZES }), + height: getSize({ size, sizes: SIZES }), + }, + input: { + ref: getStylesRef("input"), + width: getSize({ size, sizes: SIZES }), + height: getSize({ size, sizes: SIZES }), + cursor: "pointer", + borderRadius: theme.radius.xs, - return { - label: { - fontWeight: 700, - color: theme.colors.text[2], - "&:has(+ .mantine-Checkbox-root)": { - marginBottom: theme.spacing.md, + "&:checked": { + borderColor: theme.colors.brand[1], + backgroundColor: theme.colors.brand[1], + [`.${getStylesRef("icon")}`]: { + color: theme.white, }, }, - description: { - "&:has(+ .mantine-Checkbox-root)": { - marginBottom: theme.spacing.md, - }, + "&:disabled": { + borderColor: theme.colors.border[0], + backgroundColor: theme.colors.border[0], }, - }; - }, + }, + label: { + ref: getStylesRef("label"), + color: theme.colors.text[2], + fontSize: theme.fontSizes.md, + fontWeight: "bold", + lineHeight: "1rem", + }, + description: { + ref: getStylesRef("description"), + color: theme.colors.text[2], + fontSize: theme.fontSizes.sm, + lineHeight: "1rem", + marginTop: theme.spacing.xs, + }, + icon: { + ref: getStylesRef("icon"), + color: theme.colors.text[0], + }, + }), }, }); diff --git a/frontend/src/metabase/ui/components/inputs/Input/Input.styled.tsx b/frontend/src/metabase/ui/components/inputs/Input/Input.styled.tsx new file mode 100644 index 00000000000..ddad72e294e --- /dev/null +++ b/frontend/src/metabase/ui/components/inputs/Input/Input.styled.tsx @@ -0,0 +1,139 @@ +import { getSize, rem } from "@mantine/core"; +import type { InputStylesParams, MantineThemeOverride } from "@mantine/core"; + +const SIZES = { + xs: rem(28), + md: rem(40), +}; + +const PADDING = 12; +const DEFAULT_ICON_WIDTH = 40; +const UNSTYLED_ICON_WIDTH = 28; +const BORDER_WIDTH = 1; + +export const getInputOverrides = (): MantineThemeOverride["components"] => ({ + Input: { + defaultProps: { + size: "md", + }, + styles: (theme, { multiline }: InputStylesParams, { size = "md" }) => ({ + input: { + color: theme.colors.text[2], + borderRadius: theme.radius.xs, + height: multiline ? "auto" : getSize({ size, sizes: SIZES }), + minHeight: getSize({ size, sizes: SIZES }), + "&::placeholder": { + color: theme.colors.text[0], + }, + "&:disabled": { + backgroundColor: theme.colors.bg[0], + }, + "&[data-invalid]": { + color: theme.colors.error[0], + borderColor: theme.colors.error[0], + "&::placeholder": { + color: theme.colors.error[0], + }, + }, + }, + icon: { + color: theme.colors.text[2], + }, + rightSection: { + color: theme.colors.text[0], + }, + }), + sizes: { + xs: theme => ({ + input: { + fontSize: theme.fontSizes.sm, + lineHeight: theme.lineHeight, + }, + }), + md: theme => ({ + input: { + fontSize: theme.fontSizes.md, + lineHeight: rem(24), + }, + }), + }, + variants: { + default: ( + theme, + { withRightSection, rightSectionWidth }: InputStylesParams, + ) => ({ + input: { + paddingLeft: rem(PADDING - BORDER_WIDTH), + paddingRight: withRightSection + ? typeof rightSectionWidth === "number" + ? rem(rightSectionWidth - BORDER_WIDTH) + : `calc(${rightSectionWidth} - ${BORDER_WIDTH}px)` + : rem(PADDING - BORDER_WIDTH), + borderColor: theme.colors.border[0], + "&:focus": { + borderColor: theme.colors.brand[1], + }, + "&:read-only:not(:disabled)": { + borderColor: theme.colors.text[0], + }, + "&[data-with-icon]": { + paddingLeft: rem(DEFAULT_ICON_WIDTH - BORDER_WIDTH), + }, + }, + icon: { + width: rem(DEFAULT_ICON_WIDTH), + }, + rightSection: { + width: rightSectionWidth ?? rem(DEFAULT_ICON_WIDTH), + }, + }), + unstyled: ( + theme, + { withRightSection, rightSectionWidth }: InputStylesParams, + ) => ({ + input: { + paddingLeft: 0, + paddingRight: withRightSection ? rightSectionWidth : 0, + "&[data-with-icon]": { + paddingLeft: rem(UNSTYLED_ICON_WIDTH), + }, + }, + icon: { + width: rem(UNSTYLED_ICON_WIDTH), + justifyContent: "left", + }, + rightSection: { + width: rightSectionWidth ?? rem(UNSTYLED_ICON_WIDTH), + justifyContent: "right", + }, + }), + }, + }, + InputWrapper: { + defaultProps: { + size: "md", + inputWrapperOrder: ["label", "description", "error", "input"], + }, + styles: theme => ({ + label: { + color: theme.colors.text[2], + fontSize: theme.fontSizes.sm, + fontWeight: "bold", + lineHeight: "1rem", + }, + description: { + color: theme.colors.text[2], + fontSize: theme.fontSizes.xs, + lineHeight: "1rem", + }, + error: { + color: theme.colors.error[0], + fontSize: theme.fontSizes.xs, + lineHeight: "1rem", + }, + required: { + color: theme.colors.error[0], + }, + }), + }, +}); diff --git a/frontend/src/metabase/ui/components/inputs/Input/index.ts b/frontend/src/metabase/ui/components/inputs/Input/index.ts new file mode 100644 index 00000000000..c831b4e759d --- /dev/null +++ b/frontend/src/metabase/ui/components/inputs/Input/index.ts @@ -0,0 +1 @@ +export { getInputOverrides } from "./Input.styled"; diff --git a/frontend/src/metabase/ui/components/inputs/NumberInput/NumberInput.stories.mdx b/frontend/src/metabase/ui/components/inputs/NumberInput/NumberInput.stories.mdx new file mode 100644 index 00000000000..9564e9a1c6b --- /dev/null +++ b/frontend/src/metabase/ui/components/inputs/NumberInput/NumberInput.stories.mdx @@ -0,0 +1,435 @@ +import { Canvas, Story, Meta } from "@storybook/addon-docs"; +import { Icon } from "metabase/core/components/Icon"; +import { Stack } from "metabase/ui"; +import { NumberInput } from "./"; + +export const args = { + variant: "default", + size: "md", + label: "Label", + description: "", + error: "", + placeholder: "Placeholder", + disabled: false, + withAsterisk: false, + min: undefined, + max: undefined, + step: undefined, + hideControls: true, + precision: undefined, + decimalSeparator: undefined, +}; + +export const sampleArgs = { + value: 1234, + label: "Goal value", + description: "Constant line added as a marker to the chart", + placeholder: "0", + error: "required", + min: 0, + max: 100, +}; + +export const argTypes = { + variant: { + options: ["default", "unstyled"], + control: { type: "inline-radio" }, + }, + size: { + options: ["xs", "md"], + control: { type: "inline-radio" }, + }, + label: { + control: { type: "number" }, + }, + description: { + control: { type: "number" }, + }, + placeholder: { + control: { type: "number" }, + }, + error: { + control: { type: "number" }, + }, + disabled: { + control: { type: "boolean" }, + }, + withAsterisk: { + control: { type: "boolean" }, + }, + min: { + control: { type: "number" }, + }, + max: { + control: { type: "number" }, + }, + step: { + control: { type: "number" }, + }, + hideControls: { + control: { type: "boolean" }, + }, + precision: { + control: { type: "number" }, + }, + decimalSeparator: { + control: { type: "text" }, + }, +}; + +<Meta + title="Inputs/NumberInput" + component={NumberInput} + args={args} + argTypes={argTypes} +/> + +# NumberInput + +Our themed wrapper around [Mantine NumberInput](https://mantine.dev/core/number-input/). + +## Docs + +- [Figma File](https://www.figma.com/file/YWyb541aUHtYXJVPzyYSbg/Input-%2F-Numbers?type=design&node-id=1-96&mode=design&t=1bDfUrJc5alZmVpx-0) +- [Mantine NumberInput Docs](https://mantine.dev/core/number-input/) + +## Examples + +export const Default = args => <NumberInput {...args} />; + +export const Template = args => ( + <Stack> + <NumberInput {...args} variant="default" /> + <NumberInput {...args} variant="unstyled" /> + </Stack> +); + +export const Empty = args => ( + <Template + {...args} + label={sampleArgs.label} + placeholder={sampleArgs.placeholder} + /> +); + +export const Filled = args => ( + <Template + {...args} + defaultValue={sampleArgs.value} + label={sampleArgs.label} + placeholder={sampleArgs.placeholder} + /> +); + +export const Asterisk = args => ( + <Template + {...args} + label={sampleArgs.label} + placeholder={sampleArgs.placeholder} + withAsterisk + /> +); + +export const Description = args => ( + <Template + {...args} + label={sampleArgs.label} + description={sampleArgs.description} + placeholder={sampleArgs.placeholder} + /> +); + +export const Disabled = args => ( + <Template + {...args} + label={sampleArgs.label} + description={sampleArgs.description} + placeholder={sampleArgs.placeholder} + disabled + withAsterisk + /> +); + +export const Error = args => ( + <Template + {...args} + label={sampleArgs.label} + description={sampleArgs.description} + placeholder={sampleArgs.placeholder} + error={sampleArgs.error} + withAsterisk + /> +); + +export const Icons = args => ( + <Template + {...args} + label={sampleArgs.label} + description={sampleArgs.description} + placeholder={sampleArgs.placeholder} + icon={<Icon name="int" />} + withAsterisk + /> +); + +export const ReadOnly = args => ( + <Template + {...args} + defaultValue={sampleArgs.value} + label={sampleArgs.label} + description={sampleArgs.description} + placeholder={sampleArgs.placeholder} + icon={<Icon name="int" />} + readOnly + /> +); + +export const MinMax = args => ( + <Template + {...args} + label={sampleArgs.label} + description={sampleArgs.description} + placeholder={`${sampleArgs.min} to ${sampleArgs.max}`} + icon={<Icon name="int" />} + min={sampleArgs.min} + max={sampleArgs.max} + /> +); + +export const Controls = args => ( + <Template + {...args} + label={sampleArgs.label} + description={sampleArgs.description} + placeholder={`${sampleArgs.min} to ${sampleArgs.max}`} + icon={<Icon name="int" />} + min={sampleArgs.min} + max={sampleArgs.max} + hideControls={false} + /> +); + +export const Precision = args => ( + <Template + {...args} + defaultValue={sampleArgs.value} + label={sampleArgs.label} + description={sampleArgs.description} + icon={<Icon name="int" />} + precision={2} + decimalSeparator="." + /> +); + +export const ParserFormatter = args => ( + <Template + {...args} + defaultValue={sampleArgs.value} + label={sampleArgs.label} + description={sampleArgs.description} + placeholder={sampleArgs.placeholder} + icon={<Icon name="int" />} + parser={value => value.replace(/^\$/, "")} + formatter={value => (value != null ? `$${value}` : "$")} + /> +); + +<Canvas> + <Story name="Default">{Default}</Story> +</Canvas> + +### Size - md + +export const EmptyMd = args => <Empty {...args} size="md" />; + +<Canvas> + <Story name="Empty, md">{EmptyMd}</Story> +</Canvas> + +#### Filled + +export const FilledMd = args => <Filled {...args} size="md" />; + +<Canvas> + <Story name="Filled, md">{FilledMd}</Story> +</Canvas> + +#### Asterisk + +export const AsteriskMd = args => <Asterisk {...args} size="md" />; + +<Canvas> + <Story name="Asterisk, md">{AsteriskMd}</Story> +</Canvas> + +#### Description + +export const DescriptionMd = args => <Description {...args} size="md" />; + +<Canvas> + <Story name="Description, md">{DescriptionMd}</Story> +</Canvas> + +#### Disabled + +export const DisabledMd = args => <Disabled {...args} size="md" />; + +<Canvas> + <Story name="Disabled, md">{Disabled}</Story> +</Canvas> + +#### Error + +export const ErrorMd = args => <Error {...args} size="md" />; + +<Canvas> + <Story name="Error, md">{ErrorMd}</Story> +</Canvas> + +#### Icon + +export const IconMd = args => <Icons {...args} size="md" />; + +<Canvas> + <Story name="Icon, md">{IconMd}</Story> +</Canvas> + +#### Read only + +export const ReadOnlyMd = args => <ReadOnly {...args} size="md" />; + +<Canvas> + <Story name="Read only, md">{ReadOnlyMd}</Story> +</Canvas> + +#### Min / max + +export const MinMaxMd = args => <MinMax {...args} size="md" />; + +<Canvas> + <Story name="Min / max, md">{MinMaxMd}</Story> +</Canvas> + +#### Controls + +export const ControlsMd = args => <Controls {...args} size="md" />; + +<Canvas> + <Story name="Controls, md">{ControlsMd}</Story> +</Canvas> + +#### Precision and decimal separator + +export const PrecisionMd = args => <Precision {...args} size="md" />; + +<Canvas> + <Story name="Precision and decimal separator, md">{PrecisionMd}</Story> +</Canvas> + +#### Parser and formatter + +export const ParserFormatterMd = args => ( + <ParserFormatter {...args} size="md" /> +); + +<Canvas> + <Story name="Parser and formatter, md">{ParserFormatterMd}</Story> +</Canvas> + +### Size - xs + +export const EmptyXs = args => <Empty {...args} size="xs" />; + +<Canvas> + <Story name="Empty, xs">{EmptyXs}</Story> +</Canvas> + +#### Filled + +export const FilledXs = args => <Filled {...args} size="xs" />; + +<Canvas> + <Story name="Filled, xs">{FilledXs}</Story> +</Canvas> + +#### Asterisk + +export const AsteriskXs = args => <Asterisk {...args} size="xs" />; + +<Canvas> + <Story name="Asterisk, xs">{AsteriskXs}</Story> +</Canvas> + +#### Description + +export const DescriptionXs = args => <Description {...args} size="xs" />; + +<Canvas> + <Story name="Description, xs">{DescriptionXs}</Story> +</Canvas> + +#### Disabled + +export const DisabledXs = args => <Disabled {...args} size="xs" />; + +<Canvas> + <Story name="Disabled, xs">{Disabled}</Story> +</Canvas> + +#### Error + +export const ErrorXs = args => <Error {...args} size="xs" />; + +<Canvas> + <Story name="Error, xs">{ErrorXs}</Story> +</Canvas> + +#### Icon + +export const IconXs = args => <Icons {...args} size="xs" />; + +<Canvas> + <Story name="Icon, xs">{IconXs}</Story> +</Canvas> + +#### Read only + +export const ReadOnlyXs = args => <ReadOnly {...args} size="xs" />; + +<Canvas> + <Story name="Read only, xs">{ReadOnlyXs}</Story> +</Canvas> + +#### Min / max + +export const MinMaxXs = args => <MinMax {...args} size="xs" />; + +<Canvas> + <Story name="Min / max, xs">{MinMaxXs}</Story> +</Canvas> + +#### Controls + +export const ControlsXs = args => <Controls {...args} size="xs" />; + +<Canvas> + <Story name="Controls, xs">{ControlsXs}</Story> +</Canvas> + +#### Precision and decimal separator + +export const PrecisionXs = args => <Precision {...args} size="xs" />; + +<Canvas> + <Story name="Precision and decimal separator, xs">{PrecisionXs}</Story> +</Canvas> + +#### Parser and formatter + +export const ParserFormatterXs = args => ( + <ParserFormatter {...args} size="xs" /> +); + +<Canvas> + <Story name="Parser and formatter, xs">{ParserFormatterXs}</Story> +</Canvas> diff --git a/frontend/src/metabase/ui/components/inputs/NumberInput/NumberInput.styled.tsx b/frontend/src/metabase/ui/components/inputs/NumberInput/NumberInput.styled.tsx new file mode 100644 index 00000000000..15fea19a657 --- /dev/null +++ b/frontend/src/metabase/ui/components/inputs/NumberInput/NumberInput.styled.tsx @@ -0,0 +1,45 @@ +import { getSize, rem } from "@mantine/core"; +import type { + ContextStylesParams, + MantineThemeOverride, + NumberInputStylesParams, +} from "@mantine/core"; + +const CONTROL_SIZES = { + xs: rem(16), + md: rem(20), +}; + +export const getNumberInputOverrides = + (): MantineThemeOverride["components"] => ({ + NumberInput: { + defaultProps: { + size: "md", + hideControls: true, + }, + styles: ( + theme, + params: NumberInputStylesParams, + { size = "md" }: ContextStylesParams, + ) => ({ + wrapper: { + marginTop: theme.spacing.xs, + }, + control: { + color: theme.colors.text[2], + width: getSize({ size, sizes: CONTROL_SIZES }), + borderColor: theme.colors.border[0], + "&:disabled": { + color: theme.colors.border[0], + backgroundColor: theme.colors.bg[0], + }, + }, + rightSection: { + width: "auto", + margin: 0, + borderTopRightRadius: theme.radius.xs, + borderBottomRightRadius: theme.radius.xs, + }, + }), + }, + }); diff --git a/frontend/src/metabase/ui/components/inputs/NumberInput/index.ts b/frontend/src/metabase/ui/components/inputs/NumberInput/index.ts new file mode 100644 index 00000000000..660dcae3cf2 --- /dev/null +++ b/frontend/src/metabase/ui/components/inputs/NumberInput/index.ts @@ -0,0 +1,2 @@ +export { NumberInput } from "@mantine/core"; +export { getNumberInputOverrides } from "./NumberInput.styled"; diff --git a/frontend/src/metabase/ui/components/inputs/Radio/Radio.stories.mdx b/frontend/src/metabase/ui/components/inputs/Radio/Radio.stories.mdx index 52ad90452c8..c264c744f27 100644 --- a/frontend/src/metabase/ui/components/inputs/Radio/Radio.stories.mdx +++ b/frontend/src/metabase/ui/components/inputs/Radio/Radio.stories.mdx @@ -1,9 +1,32 @@ import { Canvas, Story, Meta } from "@storybook/addon-docs"; -import { Radio } from "./"; - -<Meta title="Inputs/Radio & Radio Group" component={Radio} /> - -# Radio & Radio.Group +import { Radio, Stack } from "metabase/ui"; + +export const args = { + label: "Label", + description: "", + disabled: false, + labelPosition: "right", +}; + +export const argTypes = { + label: { + control: { type: "text" }, + }, + description: { + control: { type: "text" }, + }, + disabled: { + control: { type: "boolean" }, + }, + labelPosition: { + options: ["left", "right"], + control: { type: "inline-radio" }, + }, +}; + +<Meta title="Inputs/Radio" component={Radio} args={args} argTypes={argTypes} /> + +# Radio Our themed wrapper around [Mantine Radio](https://mantine.dev/core/radio/). @@ -13,86 +36,90 @@ Radio buttons allow users to select a single option from a list of mutually excl ## Docs -- [Figma File](https://www.figma.com/file/7LCGPhkbJdrhdIaeiU1O9c/Input-%2F-Radio?type=design&node-id=1%3A96&t=S6gieWhmvLP15ARp-1) +- [Figma File](https://www.figma.com/file/7LCGPhkbJdrhdIaeiU1O9c/Input-%2F-Radio?type=design&node-id=1-96&mode=design&t=yaNljw178EFJeU7k-0) - [Mantine Radio Docs](https://mantine.dev/core/radio/) -## Caveats - -- As of right now, don't use the size prop. -- Please don't use the `error` prop on Radio components (We'll standardize on this as other inputs get converted). - ## Usage guidelines -- **Limit usage to 5 options max**. Radio buttons should be used when there are 2-3 options to choose from. If there are more than 5 options, consider using a [Select](../Select) or [Checkbox](../Checkbox) instead. +- **Use this component if there are more than 5 options**. If there are fewer options, feel free to check out Radio or Select. - For option ordering, try to use your best judgement on a sensible order. For example, Yes should come before No. Alphabetical ordering is usually a good fallback if there's no inherent order in your set of choices. -- In almost all circumstances you'll want to use `<Raio.Group>` to provide a set of options and help with defaultValues and state management between them. +- In almost all circumstances you'll want to use `<Radio.Group>` to provide a set of options and help with defaultValues and state management between them. + +## Examples + +export const Template = args => ( + <Stack> + <Radio {...args} label="Default radio" /> + <Radio {...args} label="Checked radio" checked /> + <Radio {...args} label="Disabled radio" disabled /> + <Radio {...args} label="Disabled checked radio" disabled checked /> + </Stack> +); + +export const Default = args => <Radio {...args} />; + +<Canvas> + <Story name="Default">{Default}</Story> +</Canvas> -## General +### Radio.Group + +export const RadioGroup = args => ( + <Radio.Group + defaultValue={"react"} + label="An array of good frameworks" + description="But which one to use?" + > + <Stack mt="md"> + <Radio value="react" label="React" /> + <Radio value="svelte" label="Svelte" /> + <Radio value="ng" label="Angular" /> + <Radio value="vue" label="Vue" /> + </Stack> + </Radio.Group> +); <Canvas> - <Story name="Default"> - <Radio.Group defaultValue="yes"> - <Radio label="Yes" value="yes" /> - <Radio label="No" value="no" /> - </Radio.Group> - </Story> + <Story name="Radio group">{RadioGroup}</Story> </Canvas> -## Disabled +### Label + +export const Label = args => <Template {...args} />; <Canvas> - <Story name="Disabled"> - <Radio.Group value="yes"> - <Radio label="Yes" value="yes" disabled /> - <Radio label="No" value="no" disabled /> - </Radio.Group> - </Story> + <Story name="Label">{Label}</Story> </Canvas> -## Description +#### Left label position -If needed you can add a description value to a radio button to give more context about the choice. +export const LabelLeft = args => <Template {...args} labelPosition="left" />; <Canvas> - <Story name="Descriptions"> - <Radio.Group defaultValue="chocolate"> - <Radio - label="Chocolate" - value="chocolate" - description="One of our most popular flavors" - /> - <Radio - label="Vanilla" - value="vanilla" - description="A classic for all seasons" - /> - <Radio - label="Swirl" - value="swirl" - description="Why not have it both ways?" - /> - </Radio.Group> - </Story> + <Story name="Label, left position">{LabelLeft}</Story> </Canvas> -## Using the Radio group props +### Description -You can use `label` and `description` on the Radio.Group component to provide a label and description for the set of choices. +export const Description = args => ( + <Template {...args} description="Description" /> +); <Canvas> - <Story name="Radio Group props"> - <Radio.Group - label="A set of options" - description="You could so something like this if there was a deprecated setting that a user needs to update." - defaultValue="one" - > - <Radio label="Old bad setting" value="one" disabled /> - <Radio label="A better newer setting" value="two" /> - </Radio.Group> - </Story> + <Story name="Description">{Description}</Story> +</Canvas> + +export const DescriptionLeft = args => ( + <Template {...args} description="Description" labelPosition="left" /> +); + +#### Left label position + +<Canvas> + <Story name="Description, left position">{DescriptionLeft}</Story> </Canvas> ## Related components -- Select - Checkbox +- Select diff --git a/frontend/src/metabase/ui/components/inputs/Radio/Radio.styled.tsx b/frontend/src/metabase/ui/components/inputs/Radio/Radio.styled.tsx index 42f4f305a09..16b91dd32ac 100644 --- a/frontend/src/metabase/ui/components/inputs/Radio/Radio.styled.tsx +++ b/frontend/src/metabase/ui/components/inputs/Radio/Radio.styled.tsx @@ -1,30 +1,77 @@ -import type { MantineThemeOverride } from "@mantine/core"; +import { getStylesRef, getSize, rem } from "@mantine/core"; +import type { + RadioStylesParams, + MantineTheme, + MantineThemeOverride, +} from "@mantine/core"; + +const SIZES = { + md: rem(20), +}; export const getRadioOverrides = (): MantineThemeOverride["components"] => ({ Radio: { - styles: theme => { - return { - root: { - marginBottom: theme.spacing.md, + defaultProps: { + size: "md", + }, + styles: ( + theme: MantineTheme, + { labelPosition }: RadioStylesParams, + { size = "md" }, + ) => ({ + root: { + [`&:has(.${getStylesRef("input")}:disabled)`]: { + [`.${getStylesRef("label")}`]: { + color: theme.colors.text[0], + }, + [`.${getStylesRef("description")}`]: { + color: theme.colors.text[0], + }, + [`.${getStylesRef("icon")}`]: { + color: theme.white, + }, }, - label: { - color: theme.colors.text[2], - fontWeight: 700, + }, + inner: { + width: getSize({ size, sizes: SIZES }), + height: getSize({ size, sizes: SIZES }), + }, + radio: { + ref: getStylesRef("input"), + width: getSize({ size, sizes: SIZES }), + height: getSize({ size, sizes: SIZES }), + cursor: "pointer", + borderColor: theme.colors.text[0], + + "&:checked": { + borderColor: theme.colors.brand[1], + backgroundColor: theme.colors.brand[1], }, - }; - }, - }, - RadioGroup: { - styles: theme => { - return { - label: { - fontWeight: 700, - color: theme.colors.text[2], + "&:disabled": { + opacity: 0.3, }, - description: { - marginBottom: theme.spacing.md, + "&:disabled:not(:checked)": { + borderColor: theme.colors.text[0], + backgroundColor: theme.colors.bg[1], }, - }; - }, + }, + label: { + ref: getStylesRef("label"), + color: theme.colors.text[2], + fontSize: theme.fontSizes.md, + fontWeight: "bold", + lineHeight: theme.lineHeight, + }, + description: { + ref: getStylesRef("description"), + color: theme.colors.text[2], + fontSize: theme.fontSizes.sm, + lineHeight: theme.lineHeight, + marginTop: theme.spacing.xs, + }, + icon: { + ref: getStylesRef("icon"), + }, + }), }, }); diff --git a/frontend/src/metabase/ui/components/inputs/TextInput/TextInput.stories.mdx b/frontend/src/metabase/ui/components/inputs/TextInput/TextInput.stories.mdx new file mode 100644 index 00000000000..01a2fa97731 --- /dev/null +++ b/frontend/src/metabase/ui/components/inputs/TextInput/TextInput.stories.mdx @@ -0,0 +1,318 @@ +import { Canvas, Story, Meta } from "@storybook/addon-docs"; +import { Icon } from "metabase/core/components/Icon"; +import { Stack } from "metabase/ui"; +import { TextInput } from "./"; + +export const args = { + variant: "default", + size: "md", + label: "Label", + description: "", + error: "", + placeholder: "Placeholder", + disabled: false, + withAsterisk: false, +}; + +export const sampleArgs = { + value: "Metabase", + label: "Company or team name", + description: "Name used for this instance", + placeholder: "Department of awesome", + error: "required", +}; + +export const argTypes = { + variant: { + options: ["default", "unstyled"], + control: { type: "inline-radio" }, + }, + size: { + options: ["xs", "md"], + control: { type: "inline-radio" }, + }, + label: { + control: { type: "text" }, + }, + description: { + control: { type: "text" }, + }, + placeholder: { + control: { type: "text" }, + }, + error: { + control: { type: "text" }, + }, + disabled: { + control: { type: "boolean" }, + }, + withAsterisk: { + control: { type: "boolean" }, + }, +}; + +<Meta + title="Inputs/TextInput" + component={TextInput} + args={args} + argTypes={argTypes} +/> + +# TextInput + +Our themed wrapper around [Mantine TextInput](https://mantine.dev/core/text-input/). + +## Docs + +- [Figma File](https://www.figma.com/file/oIZhYS5OoRA7twd4KqN4Eu/Input-%2F-Text?type=design&node-id=1-96&mode=design&t=yaNljw178EFJeU7k-0) +- [Mantine TextInput Docs](https://mantine.dev/core/text-input/) + +## Examples + +export const Default = args => <TextInput {...args} />; + +export const Template = args => ( + <Stack> + <TextInput {...args} variant="default" /> + <TextInput {...args} variant="unstyled" /> + </Stack> +); + +export const Empty = args => ( + <Template + {...args} + label={sampleArgs.label} + placeholder={sampleArgs.placeholder} + /> +); + +export const Filled = args => ( + <Template + {...args} + defaultValue={sampleArgs.value} + label={sampleArgs.label} + placeholder={sampleArgs.placeholder} + /> +); + +export const Asterisk = args => ( + <Template + {...args} + label={sampleArgs.label} + placeholder={sampleArgs.placeholder} + withAsterisk + /> +); + +export const Description = args => ( + <Template + {...args} + label={sampleArgs.label} + description={sampleArgs.description} + placeholder={sampleArgs.placeholder} + /> +); + +export const Disabled = args => ( + <Template + {...args} + label={sampleArgs.label} + description={sampleArgs.description} + placeholder={sampleArgs.placeholder} + disabled + withAsterisk + /> +); + +export const Error = args => ( + <Template + {...args} + label={sampleArgs.label} + description={sampleArgs.description} + placeholder={sampleArgs.placeholder} + error={sampleArgs.error} + withAsterisk + /> +); + +export const Icons = args => ( + <Template + {...args} + label={sampleArgs.label} + description={sampleArgs.description} + placeholder={sampleArgs.placeholder} + icon={<Icon name="dashboard" />} + withAsterisk + /> +); + +export const RightSection = args => ( + <Template + {...args} + label={sampleArgs.label} + description={sampleArgs.description} + placeholder={sampleArgs.placeholder} + rightSection={<Icon name="chevrondown" />} + withAsterisk + /> +); + +export const ReadOnly = args => ( + <Template + {...args} + defaultValue={sampleArgs.value} + label={sampleArgs.label} + description={sampleArgs.description} + placeholder={sampleArgs.placeholder} + rightSection={<Icon name="chevrondown" />} + readOnly + /> +); + +<Canvas> + <Story name="Default">{Default}</Story> +</Canvas> + +### Size - md + +export const EmptyMd = args => <Empty {...args} size="md" />; + +<Canvas> + <Story name="Empty, md">{EmptyMd}</Story> +</Canvas> + +#### Filled + +export const FilledMd = args => <Filled {...args} size="md" />; + +<Canvas> + <Story name="Filled, md">{FilledMd}</Story> +</Canvas> + +#### Asterisk + +export const AsteriskMd = args => <Asterisk {...args} size="md" />; + +<Canvas> + <Story name="Asterisk, md">{AsteriskMd}</Story> +</Canvas> + +#### Description + +export const DescriptionMd = args => <Description {...args} size="md" />; + +<Canvas> + <Story name="Description, md">{DescriptionMd}</Story> +</Canvas> + +#### Disabled + +export const DisabledMd = args => <Disabled {...args} size="md" />; + +<Canvas> + <Story name="Disabled, md">{Disabled}</Story> +</Canvas> + +#### Error + +export const ErrorMd = args => <Error {...args} size="md" />; + +<Canvas> + <Story name="Error, md">{ErrorMd}</Story> +</Canvas> + +#### Icon + +export const IconMd = args => <Icons {...args} size="md" />; + +<Canvas> + <Story name="Icon, md">{IconMd}</Story> +</Canvas> + +#### Right section + +export const RightSectionMd = args => <RightSection {...args} size="md" />; + +<Canvas> + <Story name="Right section, md">{RightSectionMd}</Story> +</Canvas> + +#### Read only + +export const ReadOnlyMd = args => <ReadOnly {...args} size="md" />; + +<Canvas> + <Story name="Read only, md">{ReadOnlyMd}</Story> +</Canvas> + +### Size - xs + +export const EmptyXs = args => <Empty {...args} size="xs" />; + +<Canvas> + <Story name="Empty, xs">{EmptyXs}</Story> +</Canvas> + +#### Filled + +export const FilledXs = args => <Filled {...args} size="xs" />; + +<Canvas> + <Story name="Filled, xs">{FilledXs}</Story> +</Canvas> + +#### Asterisk + +export const AsteriskXs = args => <Asterisk {...args} size="xs" />; + +<Canvas> + <Story name="Asterisk, xs">{AsteriskXs}</Story> +</Canvas> + +#### Description + +export const DescriptionXs = args => <Description {...args} size="xs" />; + +<Canvas> + <Story name="Description, xs">{DescriptionXs}</Story> +</Canvas> + +#### Disabled + +export const DisabledXs = args => <Disabled {...args} size="xs" />; + +<Canvas> + <Story name="Disabled, xs">{Disabled}</Story> +</Canvas> + +#### Error + +export const ErrorXs = args => <Error {...args} size="xs" />; + +<Canvas> + <Story name="Error, xs">{ErrorXs}</Story> +</Canvas> + +#### Icon + +export const IconXs = args => <Icons {...args} size="xs" />; + +<Canvas> + <Story name="Icon, xs">{IconXs}</Story> +</Canvas> + +#### Right section + +export const RightSectionXs = args => <RightSection {...args} size="xs" />; + +<Canvas> + <Story name="Right section, xs">{RightSectionXs}</Story> +</Canvas> + +#### Read only + +export const ReadOnlyXs = args => <ReadOnly {...args} size="xs" />; + +<Canvas> + <Story name="Read only, xs">{ReadOnlyXs}</Story> +</Canvas> diff --git a/frontend/src/metabase/ui/components/inputs/TextInput/TextInput.styled.tsx b/frontend/src/metabase/ui/components/inputs/TextInput/TextInput.styled.tsx new file mode 100644 index 00000000000..8fe82569021 --- /dev/null +++ b/frontend/src/metabase/ui/components/inputs/TextInput/TextInput.styled.tsx @@ -0,0 +1,15 @@ +import type { MantineThemeOverride } from "@mantine/core"; + +export const getTextInputOverrides = + (): MantineThemeOverride["components"] => ({ + TextInput: { + defaultProps: { + size: "md", + }, + styles: theme => ({ + wrapper: { + marginTop: theme.spacing.xs, + }, + }), + }, + }); diff --git a/frontend/src/metabase/ui/components/inputs/TextInput/index.ts b/frontend/src/metabase/ui/components/inputs/TextInput/index.ts new file mode 100644 index 00000000000..2d0e1cd158f --- /dev/null +++ b/frontend/src/metabase/ui/components/inputs/TextInput/index.ts @@ -0,0 +1,2 @@ +export { TextInput } from "@mantine/core"; +export { getTextInputOverrides } from "./TextInput.styled"; diff --git a/frontend/src/metabase/ui/components/inputs/index.ts b/frontend/src/metabase/ui/components/inputs/index.ts index 6546c7acfc0..c15c8f30500 100644 --- a/frontend/src/metabase/ui/components/inputs/index.ts +++ b/frontend/src/metabase/ui/components/inputs/index.ts @@ -1,2 +1,5 @@ -export * from "./Radio"; export * from "./Checkbox"; +export * from "./Input"; +export * from "./NumberInput"; +export * from "./Radio"; +export * from "./TextInput"; diff --git a/frontend/src/metabase/ui/components/overlays/Menu/Menu.stories.mdx b/frontend/src/metabase/ui/components/overlays/Menu/Menu.stories.mdx index e1bdc69400d..a475f110ade 100644 --- a/frontend/src/metabase/ui/components/overlays/Menu/Menu.stories.mdx +++ b/frontend/src/metabase/ui/components/overlays/Menu/Menu.stories.mdx @@ -81,7 +81,7 @@ Not to use: ## Docs -- [Figma File](https://www.figma.com/file/MZhwfwmOaa8HeCBBUCeq7R/Menu?type=design&node-id=1-96&mode=design&t=Q0nq1hUkXN7VFjRt-0) +- [Figma File](https://www.figma.com/file/MZhwfwmOaa8HeCBBUCeq7R/Menu?type=design&node-id=1-96&mode=design&t=vj3dPYMbYVYVuKBy-0) - [Mantine Menu Docs](https://mantine.dev/core/menu/) ## Caveats diff --git a/frontend/src/metabase/ui/components/overlays/Menu/Menu.styled.tsx b/frontend/src/metabase/ui/components/overlays/Menu/Menu.styled.tsx index 5feee98e792..cf8c695ec5b 100644 --- a/frontend/src/metabase/ui/components/overlays/Menu/Menu.styled.tsx +++ b/frontend/src/metabase/ui/components/overlays/Menu/Menu.styled.tsx @@ -17,7 +17,7 @@ export const getMenuOverrides = (): MantineThemeOverride["components"] => ({ color: theme.colors.text[2], fontSize: theme.fontSizes.md, fontWeight: 700, - lineHeight: "1rem", + lineHeight: theme.lineHeight, padding: theme.spacing.md, "&:hover, &:focus": { @@ -41,7 +41,7 @@ export const getMenuOverrides = (): MantineThemeOverride["components"] => ({ color: theme.colors.text[0], fontSize: theme.fontSizes.md, fontWeight: 700, - lineHeight: "1rem", + lineHeight: theme.lineHeight, padding: `0.375rem ${theme.spacing.md}`, }, divider: { diff --git a/frontend/src/metabase/ui/components/typography/Anchor/Anchor.stories.mdx b/frontend/src/metabase/ui/components/typography/Anchor/Anchor.stories.mdx index 8795ef3eafe..c1abd7265ea 100644 --- a/frontend/src/metabase/ui/components/typography/Anchor/Anchor.stories.mdx +++ b/frontend/src/metabase/ui/components/typography/Anchor/Anchor.stories.mdx @@ -1,10 +1,38 @@ +import { Fragment } from "react"; import { Canvas, Story, Meta } from "@storybook/addon-docs"; -import { Anchor } from "./"; -import { Text } from "../Text" - -<Meta title="Typography/Anchor" component={Anchor} /> - -export const Template = (args) => <Anchor href="https://www.youtube.com/@metabasedata" {...args} />; +import { Anchor, Grid, Text } from "metabase/ui"; + +export const args = { + size: "md", + align: "unset", + truncate: false, +}; + +export const sampleArgs = { + text: "Weniger", + href: "https://example.test", +}; + +export const argTypes = { + size: { + options: ["xs", "sm", "md", "lg"], + control: { type: "inline-radio" }, + }, + align: { + options: ["left", "center", "right"], + control: { type: "inline-radio" }, + }, + truncate: { + control: { type: "boolean" }, + }, +}; + +<Meta + title="Typography/Anchor" + component={Anchor} + args={args} + argTypes={argTypes} +/> # Anchor @@ -16,64 +44,42 @@ The Anchor component allows users to display links with themed styles, and repla ## Docs -- [Figma File](https://www.figma.com/file/h6aMN8H67eu2w8wmDngfnM/Foundation-%2F-text?type=design&node-id=5-70&mode=design&t=9Sh3xAM7HsIZIN1z-11) +- [Figma File](https://www.figma.com/file/8nuIBDQGSGKLfAPsebbASA/Navigation-%2F-Anchor?type=design&node-id=1-96&mode=design&t=2eUYOsqZZeMc4OGT-0) - [Mantine Anchor Docs](https://mantine.dev/core/anchor/) -## Caveats - -N/A - -## Usage guidelines +## Examples -N/A - -## General +export const Default = args => ( + <Anchor href={sampleArgs.href}>{sampleArgs.text}</Anchor> +); <Canvas> - <Story name="Default" args={{ - children: "You should subscribe to our Youtube channel!", - italic: false, - strikethrough: false, - weight: "normal", - lineClamp: -1, - align: "unset", - }} - argTypes={ - { - children: { - control: { - type: "text" - } - }, - size: { - options: ["xs", "sm", "md", "lg", "xl"], - control: { type: 'inline-radio' } - }, - weight: { - options: ["normal", "bold"], - control: { type: 'inline-radio' } - }, - align: { - options: ["unset", "left", "right", "center"], - control: { type: 'select' } - } - } - } - > - {Template.bind({})} - </Story> + <Story name="Default">{Default}</Story> </Canvas> +### Sizes + +export const Sizes = args => ( + <Grid align="center" maw="18rem"> + {argTypes.size.options.map(size => ( + <Fragment key={size}> + <Grid.Col span={2}> + <Text weight="bold">{size}</Text> + </Grid.Col> + <Grid.Col span={10}> + <Anchor {...args} size={size} href={sampleArgs.href}> + {sampleArgs.text} + </Anchor> + </Grid.Col> + </Fragment> + ))} + </Grid> +); + <Canvas> - <Story name="Using in conjunction with Text" - > - <Text span>We've noticed that you've been looking for the latest in BI content. </Text> - <Anchor href="https://www.youtube.com/@metabasedata">You should subscribe to our Youtube channel!</Anchor> - <Text span> You won't regret it!</Text> - </Story> + <Story name="Sizes">{Sizes}</Story> </Canvas> - ## Related components - Anchor diff --git a/frontend/src/metabase/ui/components/typography/Anchor/Anchor.styled.tsx b/frontend/src/metabase/ui/components/typography/Anchor/Anchor.styled.tsx index 7a2551211e9..1f6d4af736e 100644 --- a/frontend/src/metabase/ui/components/typography/Anchor/Anchor.styled.tsx +++ b/frontend/src/metabase/ui/components/typography/Anchor/Anchor.styled.tsx @@ -5,12 +5,7 @@ export const getAnchorOverrides = (): MantineThemeOverride["components"] => ({ styles: theme => { return { root: { - fontFamily: "inherit", color: theme.colors.brand[1], - "&:focus": { - outline: `2px solid ${theme.colors.brand[0]}`, - outlineOffset: "2px", - }, "&:active": { color: theme.colors.text[2], textDecoration: "underline", diff --git a/frontend/src/metabase/ui/components/typography/Text/Text.stories.mdx b/frontend/src/metabase/ui/components/typography/Text/Text.stories.mdx index 177d83a0944..3629ba08c0c 100644 --- a/frontend/src/metabase/ui/components/typography/Text/Text.stories.mdx +++ b/frontend/src/metabase/ui/components/typography/Text/Text.stories.mdx @@ -1,70 +1,126 @@ +import { Fragment } from "react"; import { Canvas, Story, Meta } from "@storybook/addon-docs"; -import { Text } from "./"; - - -<Meta title="Typography/Text" component={Text} - args={{ - children: "Having small touches of colour makes it more colourful than having the whole thing in colour", - underline: false, - italic: false, - strikethrough: false, - weight: "normal", - lineClamp: -1, - align: "unset", - }} - argTypes={ - { - children: { - control: { - type: "text" - } - }, - size: { - options: ["xs", "sm", "md", "lg", "xl"], - control: { type: 'inline-radio' } - }, - weight: { - options: ["normal", "bold"], - control: { type: 'inline-radio' } - }, - align: { - options: ["unset", "left", "right", "center"], - control: { type: 'select' } - } - } - } +import { Box, Grid, Text } from "metabase/ui"; + +export const args = { + size: "md", + align: "unset", + weight: "normal", + italic: false, + underline: false, + strikethrough: false, + truncate: false, + lineClamp: undefined, +}; + +export const sampleArgs = { + shortText: "Weniger", + longText: + "Having small touches of colour makes it more colourful than having the whole thing in colour", +}; + +export const argTypes = { + size: { + options: ["xs", "sm", "md", "lg"], + control: { type: "inline-radio" }, + }, + align: { + options: ["left", "center", "right"], + control: { type: "inline-radio" }, + }, + weight: { + options: ["normal", "bold"], + control: { type: "inline-radio" }, + }, + italic: { + control: { type: "boolean" }, + }, + underline: { + control: { type: "boolean" }, + }, + underline: { + control: { type: "boolean" }, + }, + strikethrough: { + control: { type: "boolean" }, + }, + truncate: { + control: { type: "boolean" }, + }, + lineClamp: { + control: { type: "number" }, + }, +}; + +<Meta + title="Typography/Text" + component={Text} + args={args} + argTypes={argTypes} /> -export const Template = (args) => <Text {...args} />; - # Text Our themed wrapper around [Mantine Text](https://mantine.dev/core/text/). ## When to use Text -The Text component allows users to display text with themed styles, and replaces the usage of `<div>text</div>` or `<span>text</span>`. This component also handles sizing, line clamps, text decoration, and font weight. For links, use the `Anchor` component, and for code, use the `Code` component. +The Text component allows users to display text with themed styles, and replaces the usage of `<div>text</div>` or `<span>text</span>`. This component also handles sizing, line clamps, text decoration, and font weight. For links, use the `Anchor` component, and for code, use the `Code` component. ## Docs -- [Figma File](https://www.figma.com/file/h6aMN8H67eu2w8wmDngfnM/Foundation-%2F-text?type=design&node-id=5-70&mode=design&t=9Sh3xAM7HsIZIN1z-11) +- [Figma File](https://www.figma.com/file/h6aMN8H67eu2w8wmDngfnM/Typography-%2F-Text?type=design&node-id=1-96&mode=design&t=2eUYOsqZZeMc4OGT-0) - [Mantine Text Docs](https://mantine.dev/core/text/) -## Caveats +## Examples + +export const Template = args => ( + <Grid align="center" maw="18rem"> + {argTypes.size.options.map(size => ( + <Fragment key={size}> + <Grid.Col span={2}> + <Text weight="bold">{size}</Text> + </Grid.Col> + <Grid.Col span={10}> + <Text {...args} size={size} /> + </Grid.Col> + </Fragment> + ))} + </Grid> +); + +export const Default = args => <Text {...args}>{sampleArgs.shortText}</Text>; + +<Canvas> + <Story name="Default">{Default}</Story> +</Canvas> + +### Sizes + +export const Sizes = args => ( + <Template {...args}>{sampleArgs.shortText}</Template> +); -N/A +<Canvas> + <Story name="Sizes">{Sizes}</Story> +</Canvas> + +### Multiline -## Usage guidelines +export const Multiline = args => ( + <Template {...args}>{sampleArgs.longText}</Template> +); + +<Canvas> + <Story name="Multiline">{Multiline}</Story> +</Canvas> -N/A +### Truncated -## General +export const Truncated = args => <Multiline {...args} lineClamp={2} />; <Canvas> - <Story name="Default" - > - {Template.bind({})} - </Story> + <Story name="Truncated">{Truncated}</Story> </Canvas> ## Related components diff --git a/frontend/src/metabase/ui/components/typography/Text/Text.styled.tsx b/frontend/src/metabase/ui/components/typography/Text/Text.styled.tsx index fba3ea960c9..b289d804115 100644 --- a/frontend/src/metabase/ui/components/typography/Text/Text.styled.tsx +++ b/frontend/src/metabase/ui/components/typography/Text/Text.styled.tsx @@ -4,6 +4,19 @@ export const getTextOverrides = (): MantineThemeOverride["components"] => ({ Text: { defaultProps: { color: "text.2", + size: "md", + }, + sizes: { + md: () => ({ + root: { + lineHeight: "1.5rem", + }, + }), + lg: () => ({ + root: { + lineHeight: "1.5rem", + }, + }), }, }, }); diff --git a/frontend/src/metabase/ui/components/typography/Title/Title.stories.mdx b/frontend/src/metabase/ui/components/typography/Title/Title.stories.mdx index df04479bdf6..08f269e207d 100644 --- a/frontend/src/metabase/ui/components/typography/Title/Title.stories.mdx +++ b/frontend/src/metabase/ui/components/typography/Title/Title.stories.mdx @@ -1,6 +1,5 @@ import { Canvas, Story, Meta } from "@storybook/addon-docs"; -import { Box, Stack } from "metabase/ui"; -import { Title } from "./"; +import { Box, Stack, Title } from "metabase/ui"; export const args = { align: "left", @@ -43,7 +42,7 @@ TBD ## Docs -- [Figma File](https://www.figma.com/file/SEQS7bshKQ4y4V5FwvdROv/Typography-%2F-Title?type=design&node-id=5-70&mode=design&t=EmLhPoPqbYgYV1aP-0) +- [Figma File](https://www.figma.com/file/SEQS7bshKQ4y4V5FwvdROv/Typography-%2F-Title?type=design&node-id=1-96&mode=design&t=2eUYOsqZZeMc4OGT-0) - [Mantine Title Docs](https://mantine.dev/core/title/) ## Caveats @@ -104,28 +103,12 @@ export const Default = args => <Title {...args}>Header</Title>; <Story name="Default">{Default}</Story> </Canvas> -### Left alignment +### Sizes -export const LeftAlignment = args => <Template {...args} />; +export const Sizes = args => <Template {...args} />; <Canvas> - <Story name="Left alignment">{LeftAlignment}</Story> -</Canvas> - -### Center alignment - -export const CenterAlignment = args => <Template {...args} align="center" />; - -<Canvas> - <Story name="Center alignment">{CenterAlignment}</Story> -</Canvas> - -### Right alignment - -export const RightAlignment = args => <Template {...args} align="right" />; - -<Canvas> - <Story name="Right alignment">{RightAlignment}</Story> + <Story name="Sizes">{Sizes}</Story> </Canvas> ### Underlined diff --git a/frontend/src/metabase/ui/theme.ts b/frontend/src/metabase/ui/theme.ts index f2a4dfc7f3b..32619999b75 100644 --- a/frontend/src/metabase/ui/theme.ts +++ b/frontend/src/metabase/ui/theme.ts @@ -6,8 +6,11 @@ import { getAnchorOverrides, getButtonOverrides, getCheckboxOverrides, + getInputOverrides, getMenuOverrides, + getNumberInputOverrides, getRadioOverrides, + getTextInputOverrides, getTextOverrides, getTitleOverrides, } from "./components"; @@ -36,9 +39,10 @@ export const getThemeOverrides = (): MantineThemeOverride => ({ color("bg-medium"), color("bg-dark"), ]), + error: getThemeColors([color("error")]), }, primaryColor: "brand", - primaryShade: 2, + primaryShade: 1, shadows: { md: "0px 4px 20px 0px rgba(0, 0, 0, 0.05)", }, @@ -62,6 +66,7 @@ export const getThemeOverrides = (): MantineThemeOverride => ({ lg: rem(17), xl: rem(21), }, + lineHeight: "1rem", headings: { sizes: { h1: { @@ -95,8 +100,11 @@ export const getThemeOverrides = (): MantineThemeOverride => ({ ...getAnchorOverrides(), ...getButtonOverrides(), ...getCheckboxOverrides(), + ...getInputOverrides(), ...getMenuOverrides(), + ...getNumberInputOverrides(), ...getRadioOverrides(), + ...getTextInputOverrides(), ...getTextOverrides(), ...getTitleOverrides(), }, -- GitLab