diff --git a/src/plugins/components/index.ts b/src/plugins/components/index.ts index 3254ac3..af462f3 100644 --- a/src/plugins/components/index.ts +++ b/src/plugins/components/index.ts @@ -24,6 +24,7 @@ import inputFileRegular from './input-file-regular' import inputFile from './input-file' import input from './input' import inputNumber from './input-number' +import kbd from './kbd' import label from './label' import link from './link' import list from './list' @@ -80,6 +81,7 @@ const components = [ inputFile, input, inputNumber, + kbd, label, link, list, diff --git a/src/plugins/components/kbd/index.ts b/src/plugins/components/kbd/index.ts new file mode 100644 index 0000000..ab189cd --- /dev/null +++ b/src/plugins/components/kbd/index.ts @@ -0,0 +1,159 @@ +import plugin from 'tailwindcss/plugin' +import { defu } from 'defu' +import { type PluginOption, defaultPluginOptions } from '../../options' +import { type KbdPluginConfig, defaultConfig, key } from './kbd.config' + +export default plugin.withOptions( + function (options: PluginOption) { + let { prefix } = defu(options, defaultPluginOptions) + + if (prefix) { + prefix = `${prefix}-` + } + + return function ({ addComponents, theme }) { + const config = theme(`shurikenUi.${key}`) satisfies KbdPluginConfig + + addComponents({ + [`.${prefix}kbd`]: { + [`@apply inline-flex items-center justify-center`]: {}, + //Font + [`@apply font-${config.font.family} leading-none text-${config.font.color.light} dark:text-${config.font.color.dark}`]: + {}, + //Icon:outer + [`.${prefix}kbd-icon-outer`]: { + [`@apply inline-flex items-center justify-center`]: {}, + }, + //Icon:inner + [`.${prefix}kbd-icon`]: { + [`@apply shrink-0`]: {}, + }, + //Rounded:sm + [`&.${prefix}kbd-rounded-sm`]: { + [`@apply ${config.rounded.sm}`]: {}, + }, + //Rounded:md + [`&.${prefix}kbd-rounded-md`]: { + [`@apply ${config.rounded.md}`]: {}, + }, + //Rounded:lg + [`&.${prefix}kbd-rounded-lg`]: { + [`@apply ${config.rounded.lg}`]: {}, + }, + //Rounded:full + [`&.${prefix}kbd-rounded-full`]: { + [`@apply ${config.rounded.full}`]: {}, + }, + //Size:xs + [`&.${prefix}kbd-xs`]: { + [`@apply font-medium`]: {}, + //Size + [`@apply min-h-[${config.size.xs.size}] min-w-[${config.size.xs.size}]`]: + {}, + //Padding + [`@apply px-${config.size.xs.padding.x} py-${config.size.xs.padding.y}`]: + {}, + //Font + [`@apply leading-${config.size.xs.font.lead} text-${config.size.xs.font.size}`]: + {}, + //Icon:outer + [`.${prefix}kbd-icon-outer`]: { + [`@apply w-${config.size.xs.icon.outer.size} h-${config.size.xs.icon.outer.size}`]: + {}, + }, + //Icon:inner + [`.${prefix}kbd-icon-inner`]: { + [`@apply w-${config.size.xs.icon.inner.size} h-${config.size.xs.icon.inner.size}`]: + {}, + }, + }, + //Size:sm + [`&.${prefix}kbd-sm`]: { + //Size + [`@apply min-h-[${config.size.sm.size}] min-w-[${config.size.sm.size}]`]: + {}, + //Padding + [`@apply px-${config.size.sm.padding.x} py-${config.size.sm.padding.y}`]: + {}, + //Font + [`@apply leading-${config.size.sm.font.lead} text-${config.size.sm.font.size}`]: + {}, + //Icon:outer + [`.${prefix}kbd-icon-outer`]: { + [`@apply w-${config.size.sm.icon.outer.size} h-${config.size.sm.icon.outer.size}`]: + {}, + }, + //Icon:inner + [`.${prefix}kbd-icon-inner`]: { + [`@apply w-${config.size.sm.icon.inner.size} h-${config.size.sm.icon.inner.size}`]: + {}, + }, + }, + //Size:md + [`&.${prefix}kbd-md`]: { + //Size + [`@apply min-h-[${config.size.md.size}] min-w-[${config.size.md.size}]`]: + {}, + //Padding + [`@apply px-${config.size.md.padding.x} py-${config.size.md.padding.y}`]: + {}, + //Font + [`@apply leading-${config.size.md.font.lead} text-${config.size.md.font.size}`]: + {}, + //Icon:outer + [`.${prefix}kbd-icon-outer`]: { + [`@apply w-${config.size.md.icon.outer.size} h-${config.size.md.icon.outer.size}`]: + {}, + }, + //Icon:inner + [`.${prefix}kbd-icon-inner`]: { + [`@apply w-${config.size.md.icon.inner.size} h-${config.size.md.icon.inner.size}`]: + {}, + }, + }, + //Size:lg + [`&.${prefix}kbd-lg`]: { + //Size + [`@apply min-h-[${config.size.lg.size}] min-w-[${config.size.lg.size}]`]: + {}, + //Padding + [`@apply px-${config.size.lg.padding.x} py-${config.size.lg.padding.y}`]: + {}, + //Font + [`@apply leading-${config.size.lg.font.lead} text-${config.size.lg.font.size}`]: + {}, + //Icon:outer + [`.${prefix}kbd-icon-outer`]: { + [`@apply w-${config.size.lg.icon.outer.size} h-${config.size.lg.icon.outer.size}`]: + {}, + }, + //Icon:inner + [`.${prefix}kbd-icon-inner`]: { + [`@apply w-${config.size.lg.icon.inner.size} h-${config.size.lg.icon.inner.size}`]: + {}, + }, + }, + //Color:default + [`&.${prefix}kbd-default`]: { + [`@apply bg-white dark:bg-muted-800 border border-b-2 border-muted-500/20 dark:border-muted-300/20`]: + {}, + }, + //Color:muted + [`&.${prefix}kbd-muted`]: { + [`@apply bg-muted-50 dark:bg-muted-800 border border-b-2 border-muted-600/20 dark:border-muted-300/20`]: + {}, + }, + }, + }) + } + }, + function () { + return { + theme: { + shurikenUi: { + [key]: defaultConfig, + }, + }, + } + }, +) diff --git a/src/plugins/components/kbd/kbd.component.ts b/src/plugins/components/kbd/kbd.component.ts new file mode 100644 index 0000000..f3ffc51 --- /dev/null +++ b/src/plugins/components/kbd/kbd.component.ts @@ -0,0 +1,34 @@ +import { html } from 'lit' +import { spread } from '@open-wc/lit-helpers' + +import type { KbdAttrs } from './kbd.types' +import * as variants from './kbd.variants' + +/** + * Primary UI component for user interaction + */ +export const Kbd = ({ + rounded = 'md', + size = 'md', + color = 'default', + classes, + children, + ...attrs +}: KbdAttrs) => { + return html` + + ${children} + + ` +} diff --git a/src/plugins/components/kbd/kbd.config.ts b/src/plugins/components/kbd/kbd.config.ts new file mode 100644 index 0000000..37369cb --- /dev/null +++ b/src/plugins/components/kbd/kbd.config.ts @@ -0,0 +1,122 @@ +export const key = 'kbd' as const + +export const defaultConfig = { + font: { + family: 'mono', + color: { + light: 'muted-700', + dark: 'muted-200', + }, + }, + rounded: { + sm: 'rounded-sm', + md: 'rounded-md', + lg: 'rounded-lg', + full: 'rounded-full', + }, + size: { + xs: { + font: { + size: 'xs', + lead: '4', + }, + padding: { + x: '1', + y: '0.5', + }, + size: '1.2em', + icon: { + outer: { + size: '4', + }, + inner: { + size: '3.5', + }, + }, + }, + sm: { + font: { + size: 'sm', + lead: '5', + }, + padding: { + x: '1', + y: '0.5', + }, + size: '1.6em', + icon: { + outer: { + size: '5', + }, + inner: { + size: '3.5', + }, + }, + }, + md: { + font: { + size: 'base', + lead: '6', + }, + padding: { + x: '2', + y: '1', + }, + size: '2.2em', + icon: { + outer: { + size: '5', + }, + inner: { + size: '4', + }, + }, + }, + lg: { + font: { + size: 'lg', + lead: '7', + }, + padding: { + x: '4', + y: '1', + }, + size: '2.5em', + icon: { + outer: { + size: '6', + }, + inner: { + size: '5', + }, + }, + }, + }, + color: { + default: { + background: { + light: 'white', + dark: 'muted-800', + }, + border: { + light: 'muted-500/20', + dark: 'muted-300/20', + }, + }, + muted: { + background: { + light: 'muted-50', + dark: 'muted-800', + }, + border: { + light: 'muted-600/20', + dark: 'muted-300/20', + }, + }, + }, +} + +export type KbdConfig = typeof defaultConfig +export interface KbdPluginConfig { + [key]: KbdConfig +} diff --git a/src/plugins/components/kbd/kbd.doc.mdx b/src/plugins/components/kbd/kbd.doc.mdx new file mode 100644 index 0000000..dfe0bef --- /dev/null +++ b/src/plugins/components/kbd/kbd.doc.mdx @@ -0,0 +1,93 @@ +import { Meta, Primary, Controls, Story } from '@storybook/blocks' +import * as KbdStories from './kbd.stories' +import { defaultConfig } from './kbd.config' + + + +# Kbd +Sometimes you need to display a keyboard key (such as when displaying keyboard shortcuts) in your UI. The Kbd component is used to indicate input that is typically entered via the keyboard. + + + +## Props + + + +## Variants + +
+ +### Color: default + +Kbd elements can have different sizes and colors. Below is an eample of available sizes and with the default color. + +
+ + + + +
+ +
+ +### Color: muted + +Kbd elements can have different sizes and colors. Below is an eample of available sizes and with the muted color. + +
+ + + + +
+ +
+ +### Slot: icon + +Kbd elements can have an icon inside instead of text. Below is an eample of available icon sizes. + +
+ + + + +
+ +
+
+ +## Customization + +### Default config + +
+
+ + View configuration options + + + + + +
+
+        {JSON.stringify(defaultConfig, null, 2)}
+      
+
+ +
+
diff --git a/src/plugins/components/kbd/kbd.stories.ts b/src/plugins/components/kbd/kbd.stories.ts new file mode 100644 index 0000000..4ec4740 --- /dev/null +++ b/src/plugins/components/kbd/kbd.stories.ts @@ -0,0 +1,248 @@ +import type { Meta, StoryObj } from '@storybook/web-components' +import { html } from 'lit' + +import type { KbdAttrs } from './kbd.types' +import { Kbd } from './kbd.component' + +// More on how to set up stories at: https://storybook.js.org/docs/web-components/writing-stories/introduction +const meta = { + title: 'Shuriken UI/Base/Kbd', + // tags: ['autodocs'], + render: (args) => Kbd(args), + argTypes: { + size: { + control: { type: 'select' }, + options: ['xs', 'sm', 'md', 'lg'], + defaultValue: 'md', + }, + color: { + control: { type: 'select' }, + options: ['default', 'muted'], + defaultValue: 'default', + }, + rounded: { + control: { type: 'select' }, + options: ['none', 'sm', 'md', 'lg', 'full'], + defaultValue: 'none', + }, + }, +} 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 + children: html` + Esc + `, + }, +} +// #endregion + +// #region Default +export const DefaultXs: Story = { + name: 'Default: xs', + args: { + size: 'xs', + children: html` + Esc + `, + }, +} + +export const DefaultSm: Story = { + name: 'Default: sm', + args: { + size: 'sm', + children: html` + Esc + `, + }, +} + +export const DefaultMd: Story = { + name: 'Default: md', + args: { + size: 'md', + children: html` + Esc + `, + }, +} + +export const DefaultLg: Story = { + name: 'Default: lg', + args: { + size: 'lg', + children: html` + Esc + `, + }, +} +// #endregion + +// #region Muted +export const MutedXs: Story = { + name: 'Muted: xs', + args: { + size: 'xs', + color: 'muted', + children: html` + Esc + `, + }, +} + +export const MutedSm: Story = { + name: 'Muted: sm', + args: { + size: 'sm', + color: 'muted', + children: html` + Esc + `, + }, +} + +export const MutedMd: Story = { + name: 'Muted: md', + args: { + size: 'md', + color: 'muted', + children: html` + Esc + `, + }, +} + +export const MutedLg: Story = { + name: 'Muted: lg', + args: { + size: 'lg', + color: 'muted', + children: html` + Esc + `, + }, +} +// #endregion + +// #region Icon +export const IconXs: Story = { + name: 'Icon: xs', + args: { + size: 'xs', + children: html` + + + + + + + + + `, + }, +} + +export const IconSm: Story = { + name: 'Icon: sm', + args: { + size: 'sm', + children: html` + + + + + + + + + `, + }, +} + +export const IconMd: Story = { + name: 'Icon: md', + args: { + size: 'md', + children: html` + + + + + + + + + `, + }, +} + +export const IconLg: Story = { + name: 'Icon: lg', + args: { + size: 'lg', + children: html` + + + + + + + + + `, + }, +} +// #endregion diff --git a/src/plugins/components/kbd/kbd.test.ts b/src/plugins/components/kbd/kbd.test.ts new file mode 100644 index 0000000..126810f --- /dev/null +++ b/src/plugins/components/kbd/kbd.test.ts @@ -0,0 +1,35 @@ +import { axe } from 'vitest-axe' +import { expect, test, describe } from 'vitest' +import { render, html } from 'lit' + +import { Kbd } from './kbd.component' + +describe('Kbd', () => { + test('Should render slot', () => { + const card = Kbd({ + children: html` + Hello world + `, + }) + + render(Kbd, document.body) + + expect(document.body.querySelector('.nui-kbd')?.outerHTML)?.toContain( + 'Hello world', + ) + }) + + test('Should have no axe violations', async () => { + const kbd = Kbd({ + children: html` + Hello world + `, + }) + + render(kbd, document.body) + + expect( + await axe(document.body.querySelector('.nui-kbd')!.outerHTML), + )?.toHaveNoViolations() + }) +}) diff --git a/src/plugins/components/kbd/kbd.types.ts b/src/plugins/components/kbd/kbd.types.ts new file mode 100644 index 0000000..43023bc --- /dev/null +++ b/src/plugins/components/kbd/kbd.types.ts @@ -0,0 +1,19 @@ +import type { PropertyVariant } from '~/types/utils' + +export interface KbdProps extends Record { + rounded?: 'none' | 'sm' | 'md' | 'lg' | 'full' + size?: 'xs' | 'sm' | 'md' | 'lg' + color?: 'default' | 'muted' + classes?: { + wrapper?: string | string[] + } +} + +export interface KbdEvents {} + +export interface KbdSlots { + children: any +} + +export type KbdAttrs = KbdProps & KbdEvents & KbdSlots +export type KbdVariant = PropertyVariant diff --git a/src/plugins/components/kbd/kbd.variants.ts b/src/plugins/components/kbd/kbd.variants.ts new file mode 100644 index 0000000..a258597 --- /dev/null +++ b/src/plugins/components/kbd/kbd.variants.ts @@ -0,0 +1,21 @@ +import type { KbdVariant } from './kbd.types' + +export const rounded = { + none: '', + sm: 'nui-kbd-rounded-sm', + md: 'nui-kbd-rounded-md', + lg: 'nui-kbd-rounded-lg', + full: 'nui-kbd-rounded-full', +} as const satisfies KbdVariant<'rounded'> + +export const size = { + xs: 'nui-kbd-xs', + sm: 'nui-kbd-sm', + md: 'nui-kbd-md', + lg: 'nui-kbd-lg', +} as const satisfies KbdVariant<'size'> + +export const color = { + default: 'nui-kbd-default', + muted: 'nui-kbd-muted', +} as const satisfies KbdVariant<'color'> diff --git a/tailwind.config.ts b/tailwind.config.ts index 079c8e4..18fdb06 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -8,8 +8,10 @@ export default withShurikenUI({ './src/**/*.doc.mdx', ], theme: { - fontFamily: { - sans: ['Inter', 'sans-serif'], + extend: { + fontFamily: { + sans: ['Inter', 'sans-serif'], + }, }, }, })