diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 68b2df9..81085f3 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -44,6 +44,7 @@ const preview: Preview = { '日付ピッカー', 'フォームコントロールラベル', 'ボタン', + '見出し', 'ユーティリティリンク', 'ラジオボタン', 'ランゲージセレクター', diff --git a/src/components/Heading/Heading.stories.tsx b/src/components/Heading/Heading.stories.tsx new file mode 100644 index 0000000..5018bcb --- /dev/null +++ b/src/components/Heading/Heading.stories.tsx @@ -0,0 +1,101 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { + Heading, + type HeadingLevel, + HeadingShoulder, + type HeadingSize, + HeadingTitle, + type RuleSize, +} from './Heading'; + +const meta = { + id: 'Component/DADS v2/Heading', + title: 'Component/見出し', + component: Heading, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; + +type HeadingPlaygroundProps = { + level: HeadingLevel; + size: HeadingSize; + hasChip: boolean; + hasRule: boolean; + rule: RuleSize; + hasShoulder: boolean; + shoulderText?: string; + text: string; + hasIcon: boolean; +}; + +export const Playground: StoryObj = { + render: ({ level, size, hasChip, hasIcon, hasRule, rule, hasShoulder, shoulderText, text }) => { + return ( + + {hasShoulder && {shoulderText}} + + {hasIcon && ( + + )} + + {text} + + + ); + }, + args: { + level: 'h2', + size: '36', + hasChip: false, + hasIcon: false, + hasRule: false, + rule: '6', + hasShoulder: false, + shoulderText: 'ショルダーテキスト', + text: '見出しテキスト', + }, + argTypes: { + size: { + control: { type: 'inline-radio' }, + options: ['64', '57', '45', '36', '32', '28', '24', '20', '18', '16'], + }, + level: { + control: { type: 'inline-radio' }, + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], + }, + hasChip: { + control: { type: 'boolean' }, + }, + hasIcon: { + control: { type: 'boolean' }, + }, + hasRule: { + control: { type: 'boolean' }, + }, + rule: { + control: { type: 'inline-radio' }, + options: ['8', '6', '4', '2'], + if: { arg: 'hasRule', eq: true }, + }, + hasShoulder: { + control: { type: 'boolean' }, + }, + shoulderText: { + control: { type: 'text' }, + if: { arg: 'hasShoulder', eq: true }, + }, + text: { + control: { type: 'text' }, + }, + }, +}; diff --git a/src/components/Heading/Heading.tsx b/src/components/Heading/Heading.tsx new file mode 100644 index 0000000..4cd67e0 --- /dev/null +++ b/src/components/Heading/Heading.tsx @@ -0,0 +1,105 @@ +import { + Children, + type ComponentProps, + forwardRef, + type HTMLAttributes, + isValidElement, + type RefObject, +} from 'react'; + +export type HeadingSize = '64' | '57' | '45' | '36' | '32' | '28' | '24' | '20' | '18' | '16'; +export type RuleSize = '8' | '6' | '4' | '2'; +export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; + +const sizeClasses: Record = { + '64': 'text-dsp-64B-140 [--shoulder-size:calc(28/16*1rem)] [--shoulder-line-height:1.5] [--shoulder-letter-spacing:0.01em]', + '57': 'text-dsp-57B-140 [--shoulder-size:calc(24/16*1rem)] [--shoulder-line-height:1.5] [--shoulder-letter-spacing:0.02em]', + '45': 'text-std-45B-140 [--shoulder-size:calc(22/16*1rem)] [--shoulder-line-height:1.5] [--shoulder-letter-spacing:0.02em]', + '36': 'text-std-36B-140 [--shoulder-size:calc(20/16*1rem)] [--shoulder-line-height:1.5] [--shoulder-letter-spacing:0.02em]', + '32': 'text-std-32B-150 [--shoulder-size:calc(18/16*1rem)] [--shoulder-line-height:1.6] [--shoulder-letter-spacing:0.02em]', + '28': 'text-std-28B-150 [--shoulder-size:calc(16/16*1rem)] [--shoulder-line-height:1.7] [--shoulder-letter-spacing:0.01em]', + '24': 'text-std-24B-150 [--shoulder-size:calc(16/16*1rem)] [--shoulder-line-height:1.7] [--shoulder-letter-spacing:0.02em]', + '20': 'text-std-20B-150 [--shoulder-size:calc(16/16*1rem)] [--shoulder-line-height:1.7] [--shoulder-letter-spacing:0.02em]', + '18': 'text-std-18B-160 [--shoulder-size:calc(16/16*1rem)] [--shoulder-line-height:1.7] [--shoulder-letter-spacing:0.02em]', + '16': 'text-std-16B-170 [--shoulder-size:calc(16/16*1rem)] [--shoulder-line-height:1.7] [--shoulder-letter-spacing:0.02em]', +}; + +const ruleClasses: Record = { + '8': 'border-b-[calc(8/16*1rem)] pb-8', + '6': 'border-b-[calc(6/16*1rem)] pb-6', + '4': 'border-b-[calc(4/16*1rem)] pb-4', + '2': 'border-b-[calc(2/16*1rem)] pb-2', +}; + +const chipClasses = ` + relative pl-[calc(1em/3+0.5em)] + before:content-[''] before:absolute before:left-0 before:w-[calc(1em/3)] before:bg-blue-900 before:top-[0.2em] before:bottom-[0.1em] + supports-[top:1lh]:before:top-[calc(0.5lh-0.45em)] supports-[top:1lh]:before:bottom-[calc(0.5lh-0.55em)] + forced-colors:before:bg-[CanvasText] +`; + +const chipShoulderClasses = + 'before:!top-[calc((var(--shoulder-size)*(var(--shoulder-line-height)-1))/2)]'; + +export type HeadingShoulderProps = ComponentProps<'p'>; + +export const HeadingShoulder = (props: HeadingShoulderProps) => { + const { className, children, ...rest } = props; + return ( +

+ {children} +

+ ); +}; + +export type HeadingTitleProps = ComponentProps<'h2'> & { + level: HeadingLevel; +}; + +export const HeadingTitle = forwardRef((props, ref) => { + const { level: Component, className, children, ...rest } = props; + return ( + + {children} + + ); +}); + +export type HeadingProps = HTMLAttributes & { + size: HeadingSize; + hasChip?: boolean; + rule?: RuleSize; +}; + +export const Heading = forwardRef((props, ref) => { + const { size, hasChip, rule, className, children, ...rest } = props; + + const hasShoulder = Children.toArray(children).some( + (child) => isValidElement(child) && child.type === HeadingShoulder, + ); + + const Component = hasShoulder ? 'hgroup' : 'div'; + + return ( + } + className={` + text-solid-gray-800 + ${sizeClasses[size]} + ${hasChip ? chipClasses : ''} + ${hasChip && hasShoulder ? chipShoulderClasses : ''} + ${rule ? `border-solid border-blue-900 ${ruleClasses[rule]}` : ''} + ${className ?? ''} + `} + {...rest} + > + {children} + + ); +}); diff --git a/src/components/Heading/index.ts b/src/components/Heading/index.ts new file mode 100644 index 0000000..0514daa --- /dev/null +++ b/src/components/Heading/index.ts @@ -0,0 +1 @@ +export { Heading, HeadingShoulder, HeadingTitle } from './Heading'; diff --git a/src/components/index.ts b/src/components/index.ts index fc68c34..b6331b9 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -27,6 +27,7 @@ export { HamburgerMenuButton, HamburgerWithLabelIcon, } from './HamburgerMenuButton'; +export { Heading, HeadingShoulder, HeadingTitle } from './Heading'; export { Input } from './Input'; export { Label } from './Label'; export { diff --git a/src/index.ts b/src/index.ts index fa740a4..767af0a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,9 @@ export { EmergencyBannerButton, EmergencyBannerHeading, ErrorText, + Heading, + HeadingShoulder, + HeadingTitle, Input, Label, Legend,