diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index fe070cf..dde81f2 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -6,16 +6,16 @@ background-color: transparent !important; } - h2 { + h2:not(.nui-heading) { color: #4c1d95 !important; } - h3 { + h3:not(.nui-heading) { font-weight: 500 !important; font-size: 1.15rem !important; } - h3 + p { + h3:not(.nui-heading) + p { margin-top: 0.25rem !important; color: #64748b !important; } diff --git a/src/plugins/components/heading/heading.component.ts b/src/plugins/components/heading/heading.component.ts new file mode 100644 index 0000000..ba9aa88 --- /dev/null +++ b/src/plugins/components/heading/heading.component.ts @@ -0,0 +1,29 @@ +import { html, unsafeStatic } from 'lit/static-html.js' +import { spread } from '@open-wc/lit-helpers' + +import type { HeadingAttrs } from './heading.types' +import * as variants from './heading.variants' + +/** + * Primary UI component for user interaction + */ +export const Heading = ({ + size = 'xl', + weight = 'semibold', + lead = 'normal', + as = 'p', + children, + ...attrs +}: HeadingAttrs) => { + return html` + <${unsafeStatic(as)} class=${[ + 'nui-heading', + size && variants.size[size], + weight && variants.weight[weight], + lead && variants.lead[lead], + ] + .filter(Boolean) + .join(' ')} + ${spread(attrs)}>${children} + ` +} diff --git a/src/plugins/components/heading/heading.doc.mdx b/src/plugins/components/heading/heading.doc.mdx new file mode 100644 index 0000000..817006a --- /dev/null +++ b/src/plugins/components/heading/heading.doc.mdx @@ -0,0 +1,75 @@ +import { Meta, Primary, Controls, Story } from '@storybook/blocks' +import * as HeadingStories from './heading.stories' +import { defaultHeadingConfig } from '.' + + + +# Heading + +Headings are an important part of any website or application and are often referred as part of the "Typography". They help creating structure and consistency accross the page and your content. + + + +## Props + + + +## Variants + +
+ +### Sizes + +Use the Heading component to display a title or an important heading. You can use various props to customize the size, weight, the line-height and the Html tag used to render the heading. + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +## Customization + +### Default config + +
+
+ + View configuration options + + + + + +
+
+        {JSON.stringify(defaultHeadingConfig, null, 2)}
+      
+
+ +
+
diff --git a/src/plugins/components/heading/heading.stories.ts b/src/plugins/components/heading/heading.stories.ts new file mode 100644 index 0000000..5638d67 --- /dev/null +++ b/src/plugins/components/heading/heading.stories.ts @@ -0,0 +1,155 @@ +import type { Meta, StoryObj } from '@storybook/web-components' +import { html } from 'lit' + +import type { HeadingAttrs } from './heading.types' +import { Heading } from './heading.component' + +// More on how to set up stories at: https://storybook.js.org/docs/web-components/writing-stories/introduction +const meta = { + title: 'Shuriken UI/Typography/Heading', + // tags: ['autodocs'], + render: (args) => Heading(args), + argTypes: { + as: { + control: { type: 'select' }, + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'p'], + defaultValue: 'p', + }, + size: { + control: { type: 'select' }, + options: [ + 'xs', + 'sm', + 'md', + 'lg', + 'xl', + '2xl', + '3xl', + '4xl', + '5xl', + '6xl', + '7xl', + '8xl', + '9xl', + ], + defaultValue: 'xl', + }, + weight: { + control: { type: 'select' }, + options: ['light', 'normal', 'medium', 'semibold', 'bold', 'extrabold'], + defaultValue: 'normal', + }, + lead: { + control: { type: 'select' }, + options: ['none', 'tight', 'snug', 'normal', 'relaxed', 'loose'], + defaultValue: 'normal', + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +// first export is the Primary story + +// #region Main +export const Main: Story = { + name: 'Main example', + args: { + // set default values used for UI preview + as: 'h2', + size: 'xl', + weight: 'semibold', + lead: 'none', + children: html` + Hello world + `, + }, +} +// #endregion + +// #region Variants +export const SizeSm: Story = { + name: 'Size: sm', + args: { + // set default values used for UI preview + as: 'h2', + size: 'sm', + weight: 'medium', + lead: 'none', + children: html` + Iam a sm heading + `, + }, +} + +export const SizeMd: Story = { + name: 'Size: md', + args: { + // set default values used for UI preview + as: 'h2', + size: 'md', + weight: 'medium', + lead: 'none', + children: html` + Iam a md heading + `, + }, +} + +export const SizeLg: Story = { + name: 'Size: lg', + args: { + // set default values used for UI preview + as: 'h2', + size: 'lg', + weight: 'medium', + lead: 'none', + children: html` + Iam a lg heading + `, + }, +} + +export const SizeXl: Story = { + name: 'Size: xl', + args: { + // set default values used for UI preview + as: 'h2', + size: 'xl', + weight: 'medium', + lead: 'none', + children: html` + Iam a xl heading + `, + }, +} + +export const Size2Xl: Story = { + name: 'Size: 2xl', + args: { + // set default values used for UI preview + as: 'h2', + size: '2xl', + weight: 'medium', + lead: 'none', + children: html` + Iam a 2xl heading + `, + }, +} + +export const Size3Xl: Story = { + name: 'Size: 3xl', + args: { + // set default values used for UI preview + as: 'h2', + size: '3xl', + weight: 'medium', + lead: 'none', + children: html` + Iam a 3xl heading + `, + }, +} +// #endregion diff --git a/src/plugins/components/heading/heading.test.ts b/src/plugins/components/heading/heading.test.ts new file mode 100644 index 0000000..644141b --- /dev/null +++ b/src/plugins/components/heading/heading.test.ts @@ -0,0 +1,35 @@ +import { axe } from 'vitest-axe' +import { expect, test, describe } from 'vitest' +import { render, html } from 'lit' + +import { Heading } from './heading.component' + +describe('Heading', () => { + test('Should render slot', () => { + const heading = Heading({ + children: html` + Hello world + `, + }) + + render(heading, document.body) + + expect(document.body.querySelector('.nui-heading')?.outerHTML)?.toContain( + 'Hello world', + ) + }) + + test('Should have no axe violations', async () => { + const heading = Heading({ + children: html` + Hello world + `, + }) + + render(heading, document.body) + + expect( + await axe(document.body.querySelector('.nui-heading')!.outerHTML), + )?.toHaveNoViolations() + }) +}) diff --git a/src/plugins/components/heading/heading.types.ts b/src/plugins/components/heading/heading.types.ts new file mode 100644 index 0000000..8decb18 --- /dev/null +++ b/src/plugins/components/heading/heading.types.ts @@ -0,0 +1,30 @@ +import type { PropertyVariant } from '~/types/utils' + +export interface HeadingProps extends Record { + as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p' + size?: + | 'xs' + | 'sm' + | 'md' + | 'lg' + | 'xl' + | '2xl' + | '3xl' + | '4xl' + | '5xl' + | '6xl' + | '7xl' + | '8xl' + | '9xl' + weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | 'extrabold' + lead?: 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose' +} + +export interface HeadingEvents {} + +export interface HeadingSlots { + children: any +} + +export type HeadingAttrs = HeadingProps & HeadingEvents & HeadingSlots +export type HeadingVariant = PropertyVariant diff --git a/src/plugins/components/heading/heading.variants.ts b/src/plugins/components/heading/heading.variants.ts new file mode 100644 index 0000000..a0ad0eb --- /dev/null +++ b/src/plugins/components/heading/heading.variants.ts @@ -0,0 +1,35 @@ +import type { HeadingVariant } from './heading.types' + +export const size = { + xs: 'nui-heading-xs', + sm: 'nui-heading-sm', + md: 'nui-heading-md', + lg: 'nui-heading-lg', + xl: 'nui-heading-xl', + '2xl': 'nui-heading-2xl', + '3xl': 'nui-heading-3xl', + '4xl': 'nui-heading-4xl', + '5xl': 'nui-heading-5xl', + '6xl': 'nui-heading-6xl', + '7xl': 'nui-heading-7xl', + '8xl': 'nui-heading-8xl', + '9xl': 'nui-heading-9xl', +} as const satisfies HeadingVariant<'size'> + +export const weight = { + light: 'nui-weight-light', + normal: 'nui-weight-normal', + medium: 'nui-weight-medium', + semibold: 'nui-weight-semibold', + bold: 'nui-weight-bold', + extrabold: 'nui-weight-extrabold', +} as const satisfies HeadingVariant<'weight'> + +export const lead = { + none: 'nui-lead-none', + tight: 'nui-lead-tight', + snug: 'nui-lead-snug', + normal: 'nui-lead-normal', + relaxed: 'nui-lead-relaxed', + loose: 'nui-lead-loose', +} as const satisfies HeadingVariant<'lead'> diff --git a/src/plugins/components/heading.ts b/src/plugins/components/heading/index.ts similarity index 96% rename from src/plugins/components/heading.ts rename to src/plugins/components/heading/index.ts index da11b60..e4b1881 100644 --- a/src/plugins/components/heading.ts +++ b/src/plugins/components/heading/index.ts @@ -1,8 +1,8 @@ import plugin from 'tailwindcss/plugin' import { defu } from 'defu' -import { type PluginOption, defaultPluginOptions } from '../options' +import { type PluginOption, defaultPluginOptions } from '../../options' -const defaultHeadingConfig = { +export const defaultHeadingConfig = { textXS: 'xs', textSM: 'sm', textMD: 'base', @@ -44,7 +44,7 @@ export default plugin.withOptions( addComponents({ [`.${prefix}heading`]: { - [`@apply font-heading`]: {}, + [`@apply font-sans`]: {}, [`&.${prefix}heading-xs`]: { [`@apply text-${config.textXS}`]: {},