diff --git a/changelog.txt b/changelog.txt index 89e06159..969e1ff4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,6 @@ +Version 1.6.0 - xth x, 2025 +- New - Introduced a versatile Text component that supports multiple HTML elements and customizable styles for enhanced typography flexibility. + Version 1.5.1 - 28th March, 2025 - Improvement - Introduced the `areaChartWrapper` prop in the AreaChart component to allow for better customization options. - Improvement - Removed the group selector from the Tabs component to simplify style overrides. diff --git a/src/components/index.ts b/src/components/index.ts index 43ff2878..9a302428 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -41,3 +41,4 @@ export { default as AreaChart } from './area-chart'; export { default as Dropzone } from './dropzone'; export { default as Table } from './table'; export { default as FilePreview } from './file-preview'; +export { default as Text } from './text'; diff --git a/src/components/text/index.ts b/src/components/text/index.ts new file mode 100644 index 00000000..2178acad --- /dev/null +++ b/src/components/text/index.ts @@ -0,0 +1 @@ +export { default } from './text'; diff --git a/src/components/text/styles.ts b/src/components/text/styles.ts new file mode 100644 index 00000000..3a4e0b37 --- /dev/null +++ b/src/components/text/styles.ts @@ -0,0 +1,63 @@ +/** + * The class names for the font weight. + */ +export const fontWeightClassNames = { + 400: 'font-normal', + 500: 'font-medium', + 600: 'font-semibold', + 700: 'font-bold', +}; + +/** + * The class names for the font size. + */ +export const fontSizeClassNames = { + 36: 'text-4xl', + 30: 'text-3xl', + 24: 'text-2xl', + 20: 'text-xl', + 18: 'text-lg', + 16: 'text-base', + 14: 'text-sm', + 12: 'text-xs', +}; + +/** + * Line height class names. + */ +export const lineHeightClassNames = { + 44: 'leading-11', + 38: 'leading-9.5', + 32: 'leading-8', + 30: 'leading-7.5', + 28: 'leading-7', + 24: 'leading-6', + 20: 'leading-5', + 16: 'leading-4', +}; + +/** + * Letter spacing class names. + */ +export const letterSpacingClassNames = { + 2: 'tracking-2', // 2px +}; + +/** + * Font color class names. + */ +export const fontColorClassNames = { + brand600: 'text-brand-primary-600', + link: 'text-link-primary', + primary: 'text-text-primary', + secondary: 'text-text-secondary', + tertiary: 'text-text-tertiary', + disabled: 'text-text-disabled', + help: 'text-field-helper', + label: 'text-field-label', + info: 'text-support-info', + success: 'text-support-success', + warning: 'text-support-warning', + error: 'text-support-error', + inverse: 'text-text-on-color', +}; diff --git a/src/components/text/text.stories.tsx b/src/components/text/text.stories.tsx new file mode 100644 index 00000000..32138d79 --- /dev/null +++ b/src/components/text/text.stories.tsx @@ -0,0 +1,102 @@ +import { Meta, StoryFn } from '@storybook/react'; +import Text from './text'; +import { + fontWeightClassNames, + fontSizeClassNames, + lineHeightClassNames, + letterSpacingClassNames, + fontColorClassNames, +} from './styles'; + +export default { + title: 'Atoms/Text', + component: Text, + tags: [ 'autodocs' ], + argTypes: { + weight: { + control: 'select', + options: Object.keys( fontWeightClassNames ).map( Number ), + description: 'The font weight of the text', + }, + size: { + control: 'select', + options: Object.keys( fontSizeClassNames ).map( Number ), + description: 'The font size of the text', + }, + lineHeight: { + control: 'select', + options: Object.keys( lineHeightClassNames ).map( Number ), + description: 'The line height of the text', + }, + letterSpacing: { + control: 'select', + options: Object.keys( letterSpacingClassNames ).map( Number ), + description: 'The letter spacing of the text', + }, + color: { + control: 'select', + options: Object.keys( fontColorClassNames ), + description: 'The font color of the text', + }, + as: { + control: 'select', + options: [ + 'p', + 'label', + 'span', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'div', + ], + description: 'The element to render the text as. Any HTML element or React component can be added here.', + }, + children: { + control: 'text', + description: 'The content of the text', + }, + className: { + control: 'text', + description: 'Additional class names to apply', + }, + }, +} satisfies Meta; + +// Create our story type +type Story = StoryFn; + +// Main template for all stories +const Template: Story = ( args ) => ; + +// Default story +export const Default: Story = Template.bind( {} ); +Default.args = { + children: 'The quick brown fox jumps over the lazy dog', + as: 'p', + color: 'primary', +}; + +// Heading example +export const Heading: Story = Template.bind( {} ); +Heading.args = { + children: 'This is a heading', + as: 'h1', + size: 36, + weight: 600, + color: 'primary', +}; + +// Paragraph example +export const Paragraph: Story = Template.bind( {} ); +Paragraph.args = { + children: + 'This is a paragraph with some longer text to demonstrate how the component handles multi-line content. The Text component is designed to be flexible and work with various HTML elements.', + as: 'p', + size: 16, + weight: 400, + lineHeight: 24, + color: 'help', +}; diff --git a/src/components/text/text.tsx b/src/components/text/text.tsx new file mode 100644 index 00000000..b1190f86 --- /dev/null +++ b/src/components/text/text.tsx @@ -0,0 +1,132 @@ +import { cn } from '@/utilities/functions'; +import { + fontColorClassNames, + fontSizeClassNames, + fontWeightClassNames, + letterSpacingClassNames, + lineHeightClassNames, +} from './styles'; +import { + forwardRef, + type ElementType, + type ReactNode, + type ComponentType, + type PropsWithoutRef, +} from 'react'; + +// Polymorphic component type utilities +export type PropsOf< + C extends keyof JSX.IntrinsicElements | ComponentType, +> = JSX.LibraryManagedAttributes>; + +type AsProp = { + /** + * The element to render the text as. + * + * @default 'p' + */ + as?: C; +}; + +export type ExtendableProps< + ExtendedProps = object, + OverrideProps = object, +> = OverrideProps & Omit; + +export type InheritableElementProps< + C extends ElementType, + Props = object, +> = ExtendableProps, Props>; + +export type PolymorphicComponentProps< + C extends ElementType, + Props = object, +> = InheritableElementProps>; + +export type PolymorphicRef = + React.ComponentPropsWithRef['ref']; + +export type PolymorphicComponentPropsWithRef< + C extends ElementType, + Props = object, +> = PolymorphicComponentProps & { ref?: PolymorphicRef }; + +// Base props for the Text component +export interface TextBaseProps { + /** + * The content of the text. + */ + children: ReactNode; + /** + * The font weight of the text. + */ + weight?: keyof typeof fontWeightClassNames; + /** + * The font size of the text. + */ + size?: keyof typeof fontSizeClassNames; + /** + * The line height of the text. + */ + lineHeight?: keyof typeof lineHeightClassNames; + /** + * The letter spacing of the text. + */ + letterSpacing?: keyof typeof letterSpacingClassNames; + /** + * The font color of the text. + */ + color?: keyof typeof fontColorClassNames; + /** + * Additional class names to apply + */ + className?: string; +} + +export type TextProps = + PolymorphicComponentPropsWithRef; + +// Type definition for Text component with proper forwarded ref +export type TextComponent = ( + props: TextProps +) => JSX.Element; + +// Create component with properly typed forwardRef +const Text = forwardRef( function Text( + { + as, + children, + weight, + size, + lineHeight, + letterSpacing, + color = 'primary', + className, + ...rest + }: PropsWithoutRef>, + ref: PolymorphicRef +) { + const Component = as || 'p'; + + return ( + + { children } + + ); +} ) as TextComponent; + +export { Text }; + +export default Text; diff --git a/src/theme/default-config.js b/src/theme/default-config.js index 77003f82..4092f6d4 100644 --- a/src/theme/default-config.js +++ b/src/theme/default-config.js @@ -204,6 +204,14 @@ const defaultTheme = { fontSize: { tiny: '0.625rem', }, + lineHeight: { + 11: '2.75rem', // 44px + 9.5: '2.375rem', // 38px + 7.5: '1.875rem', // 30px + }, + letterSpacing: { + 2: '0.125em', // 2px + }, size: { 3.25: '0.8125rem', // 13px },