Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const preview: Preview = {
'日付ピッカー',
'フォームコントロールラベル',
'ボタン',
'見出し',
'ユーティリティリンク',
'ラジオボタン',
'ランゲージセレクター',
Expand Down
101 changes: 101 additions & 0 deletions src/components/Heading/Heading.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Heading>;

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<HeadingPlaygroundProps> = {
render: ({ level, size, hasChip, hasIcon, hasRule, rule, hasShoulder, shoulderText, text }) => {
return (
<Heading size={size} hasChip={hasChip} rule={hasRule ? rule : undefined}>
{hasShoulder && <HeadingShoulder>{shoulderText}</HeadingShoulder>}
<HeadingTitle level={level}>
{hasIcon && (
<svg
width='24'
height='24'
viewBox='0 0 24 24'
fill='currentColor'
aria-hidden='true'
className='inline-block mr-[0.4em] w-[1.25em] h-[1.25em] align-[-0.25em]'
>
<path d='M4.6 20.5c-.5-.1-1-.6-1.1-1l16-16c.5.1.9.6 1 1l-16 16Zm-1.1-6.4v-2L12 3.4h2.1L3.5 14.1Zm0-7.4V5.3c0-1 .8-1.8 1.8-1.8h1.4L3.5 6.7Zm13.8 13.8 3.2-3.2v1.4c0 1-.8 1.8-1.8 1.8h-1.4Zm-7.4 0L20.5 9.9v2L12 20.6H9.9Z' />
</svg>
)}

{text}
</HeadingTitle>
</Heading>
);
},
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' },
},
},
};
105 changes: 105 additions & 0 deletions src/components/Heading/Heading.tsx
Original file line number Diff line number Diff line change
@@ -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<HeadingSize, string> = {
'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<RuleSize, string> = {
'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 (
<p
className={`
font-bold text-[length:var(--shoulder-size)] leading-[var(--shoulder-line-height)] tracking-[var(--shoulder-letter-spacing)]
${className ?? ''}
`}
{...rest}
>
{children}
</p>
);
};

export type HeadingTitleProps = ComponentProps<'h2'> & {
level: HeadingLevel;
};

export const HeadingTitle = forwardRef<HTMLHeadingElement, HeadingTitleProps>((props, ref) => {
const { level: Component, className, children, ...rest } = props;
return (
<Component ref={ref} className={`${className ?? ''}`} {...rest}>
{children}
</Component>
);
});

export type HeadingProps = HTMLAttributes<HTMLElement> & {
size: HeadingSize;
hasChip?: boolean;
rule?: RuleSize;
};

export const Heading = forwardRef<HTMLElement, HeadingProps>((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 (
<Component
ref={ref as RefObject<HTMLDivElement>}
className={`
text-solid-gray-800
${sizeClasses[size]}
${hasChip ? chipClasses : ''}
${hasChip && hasShoulder ? chipShoulderClasses : ''}
${rule ? `border-solid border-blue-900 ${ruleClasses[rule]}` : ''}
${className ?? ''}
`}
{...rest}
>
{children}
</Component>
);
});
1 change: 1 addition & 0 deletions src/components/Heading/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Heading, HeadingShoulder, HeadingTitle } from './Heading';
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export {
HamburgerMenuButton,
HamburgerWithLabelIcon,
} from './HamburgerMenuButton';
export { Heading, HeadingShoulder, HeadingTitle } from './Heading';
export { Input } from './Input';
export { Label } from './Label';
export {
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export {
EmergencyBannerButton,
EmergencyBannerHeading,
ErrorText,
Heading,
HeadingShoulder,
HeadingTitle,
Input,
Label,
Legend,
Expand Down