diff --git a/.drone.yml b/.drone.yml index 722477f..96c6611 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4,34 +4,34 @@ type: docker name: web-ui-ci trigger: - branch: - - main - - develop - event: - - push - - pull_request + branch: + - main + - develop + event: + - push + - pull_request steps: - - name: install - image: node:22 - commands: - - corepack enable - - corepack prepare yarn@1.22.22 --activate - - yarn install --frozen-lockfile + - name: install + image: node:22 + commands: + - corepack enable + - corepack prepare yarn@1.22.22 --activate + - yarn install --frozen-lockfile - - name: lint - image: node:22 - commands: - - corepack enable - - corepack prepare yarn@1.22.22 --activate - - yarn lint + - name: lint + image: node:22 + commands: + - corepack enable + - corepack prepare yarn@1.22.22 --activate + - yarn lint - - name: build - image: node:22 - commands: - - corepack enable - - corepack prepare yarn@1.22.22 --activate - - yarn build + - name: build + image: node:22 + commands: + - corepack enable + - corepack prepare yarn@1.22.22 --activate + - yarn build --- kind: pipeline @@ -39,22 +39,22 @@ type: docker name: web-ui-publish trigger: - branch: - - main - event: - - promote - target: - - production + branch: + - main + event: + - promote + target: + - production steps: - - name: publish-npm - image: node:22 - environment: - NEXUS_NPM_TOKEN: - from_secret: nexus_npm_token - commands: - - corepack enable - - corepack prepare yarn@1.22.22 --activate - - yarn install --frozen-lockfile - - npm config set //nexus.beatrice.wtf/repository/npm-hosted/:_authToken "$NEXUS_NPM_TOKEN" - - yarn publish:nexus + - name: publish-npm + image: node:22 + environment: + NEXUS_NPM_TOKEN: + from_secret: nexus_npm_token + commands: + - corepack enable + - corepack prepare yarn@1.22.22 --activate + - yarn install --frozen-lockfile + - npm config set //nexus.beatrice.wtf/repository/npm-hosted/:_authToken "$NEXUS_NPM_TOKEN" + - yarn publish:nexus diff --git a/.prettierrc b/.prettierrc index 27411b6..5a93724 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,7 @@ { - "$schema": "https://json.schemastore.org/prettierrc", - "singleQuote": true, - "semi": true, - "printWidth": 100, - "tabWidth": 2 + "$schema": "https://json.schemastore.org/prettierrc", + "singleQuote": true, + "semi": true, + "printWidth": 100, + "tabWidth": 4 } diff --git a/.storybook/main.ts b/.storybook/main.ts index 8d12fed..37bff79 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,15 +1,15 @@ import type { StorybookConfig } from '@storybook/react-vite'; const config: StorybookConfig = { - stories: ['../src/**/*.stories.@(ts|tsx|mdx)'], - addons: ['@storybook/addon-docs', '@storybook/addon-a11y', '@storybook/addon-themes'], - framework: { - name: '@storybook/react-vite', - options: {}, - }, - docs: { - autodocs: 'tag', - }, + stories: ['../src/**/*.stories.@(ts|tsx|mdx)'], + addons: ['@storybook/addon-docs', '@storybook/addon-a11y', '@storybook/addon-themes'], + framework: { + name: '@storybook/react-vite', + options: {}, + }, + docs: { + autodocs: 'tag', + }, }; export default config; diff --git a/.storybook/preview.css b/.storybook/preview.css index f682171..34d163f 100644 --- a/.storybook/preview.css +++ b/.storybook/preview.css @@ -3,6 +3,6 @@ @tailwind utilities; body { - background-color: var(--bg-page); - color: var(--text-primary); + background-color: var(--bg-page); + color: var(--text-primary); } diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index db20517..afe30d0 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -6,22 +6,22 @@ import '../src/styles/base.css'; import '../src/styles/components.css'; const preview: Preview = { - decorators: [ - (Story) => ( - - - - ), - ], - parameters: { - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/i, - }, + decorators: [ + (Story) => ( + + + + ), + ], + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + layout: 'centered', }, - layout: 'centered', - }, }; export default preview; diff --git a/eslint.config.mjs b/eslint.config.mjs index 56d743f..e428a80 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,44 +5,44 @@ import globals from 'globals'; import tseslint from 'typescript-eslint'; export default tseslint.config( - { - ignores: [ - 'dist', - 'coverage', - 'storybook-static', - 'node_modules', - '*.config.cjs', - 'vite.config.js', - 'vite.config.d.ts', - 'tailwind-preset.cjs', - ], - }, - js.configs.recommended, - ...tseslint.configs.recommended, - { - files: ['**/*.{ts,tsx}'], - languageOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node, - }, + { + ignores: [ + 'dist', + 'coverage', + 'storybook-static', + 'node_modules', + '*.config.cjs', + 'vite.config.js', + 'vite.config.d.ts', + 'tailwind-preset.cjs', + ], }, - rules: { - 'no-empty': ['error', { allowEmptyCatch: true }], + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.node, + }, + }, + rules: { + 'no-empty': ['error', { allowEmptyCatch: true }], + }, }, - }, - { - files: ['src/**/*.{ts,tsx}'], - ignores: ['src/**/*.stories.ts', 'src/**/*.stories.tsx'], - plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, + { + files: ['src/**/*.{ts,tsx}'], + ignores: ['src/**/*.stories.ts', 'src/**/*.stories.tsx'], + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + }, }, - rules: { - ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], - }, - }, ); diff --git a/package.json b/package.json index a765904..7872d1c 100644 --- a/package.json +++ b/package.json @@ -1,86 +1,86 @@ { - "name": "@panic/web-ui", - "version": "0.1.7", - "license": "AGPL-3.0-only", - "description": "Core components for panic.haus web applications", - "type": "module", - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "name": "@panic/web-ui", + "version": "0.1.7", + "license": "AGPL-3.0-only", + "description": "Core components for panic.haus web applications", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./components/MDXEditorField": { + "types": "./dist/components/MDXEditorField.d.ts", + "import": "./dist/components/MDXEditorField.js" + }, + "./styles/base.css": "./dist/styles/base.css", + "./styles/components.css": "./dist/styles/components.css", + "./styles/utilities.css": "./dist/styles/utilities.css", + "./tailwind-preset": "./dist/tailwind-preset.cjs" }, - "./components/MDXEditorField": { - "types": "./dist/components/MDXEditorField.d.ts", - "import": "./dist/components/MDXEditorField.js" + "files": [ + "dist" + ], + "scripts": { + "clean": "rm -rf dist", + "build": "yarn clean && vite build && tsc -p tsconfig.build.json && mkdir -p dist/styles && cp src/styles/base.css dist/styles/base.css && tailwindcss -c tailwind.build.config.cjs -i src/styles/components.css -o dist/styles/components.css --minify && tailwindcss -c tailwind.build.config.cjs -i src/styles/utilities.css -o dist/styles/utilities.css --minify && cp tailwind-preset.cjs dist/tailwind-preset.cjs", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier . --write", + "format:check": "prettier . --check", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", + "prepublishOnly": "yarn build", + "publish:nexus": "npm publish --registry ${NEXUS_NPM_REGISTRY:-https://nexus.beatrice.wtf/repository/npm-hosted/}" }, - "./styles/base.css": "./dist/styles/base.css", - "./styles/components.css": "./dist/styles/components.css", - "./styles/utilities.css": "./dist/styles/utilities.css", - "./tailwind-preset": "./dist/tailwind-preset.cjs" - }, - "files": [ - "dist" - ], - "scripts": { - "clean": "rm -rf dist", - "build": "yarn clean && vite build && tsc -p tsconfig.build.json && mkdir -p dist/styles && cp src/styles/base.css dist/styles/base.css && tailwindcss -c tailwind.build.config.cjs -i src/styles/components.css -o dist/styles/components.css --minify && tailwindcss -c tailwind.build.config.cjs -i src/styles/utilities.css -o dist/styles/utilities.css --minify && cp tailwind-preset.cjs dist/tailwind-preset.cjs", - "lint": "eslint .", - "lint:fix": "eslint . --fix", - "format": "prettier . --write", - "format:check": "prettier . --check", - "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build", - "prepublishOnly": "yarn build", - "publish:nexus": "npm publish --registry ${NEXUS_NPM_REGISTRY:-https://nexus.beatrice.wtf/repository/npm-hosted/}" - }, - "publishConfig": { - "registry": "https://nexus.beatrice.wtf/repository/npm-hosted/", - "access": "restricted" - }, - "peerDependencies": { - "@heroicons/react": "^2.2.0", - "@mdxeditor/editor": "^3.52.4", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": "^7.0.0" - }, - "peerDependenciesMeta": { - "@mdxeditor/editor": { - "optional": true + "publishConfig": { + "registry": "https://nexus.beatrice.wtf/repository/npm-hosted/", + "access": "restricted" + }, + "peerDependencies": { + "@heroicons/react": "^2.2.0", + "@mdxeditor/editor": "^3.52.4", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.0.0" + }, + "peerDependenciesMeta": { + "@mdxeditor/editor": { + "optional": true + } + }, + "devDependencies": { + "@codemirror/language": "^6.11.3", + "@eslint/js": "^9", + "@heroicons/react": "^2.2.0", + "@lezer/highlight": "^1.2.1", + "@mdxeditor/editor": "^3.52.4", + "@storybook/addon-a11y": "^10.2.10", + "@storybook/addon-docs": "^10.2.10", + "@storybook/addon-themes": "^10.2.10", + "@storybook/react": "^10.2.10", + "@storybook/react-vite": "^10.2.10", + "@testing-library/dom": "^10.4.1", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^5.0.0", + "eslint": "^9", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.1", + "globals": "^17.3.0", + "postcss": "^8.4.49", + "prettier": "^3.8.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.0.0", + "storybook": "^10.2.10", + "tailwindcss": "^3.4.16", + "typescript": "^5.6.2", + "typescript-eslint": "^8.56.0", + "vite": "^7.0.0", + "yjs": "^13.6.24" } - }, - "devDependencies": { - "@codemirror/language": "^6.11.3", - "@eslint/js": "^9", - "@heroicons/react": "^2.2.0", - "@lezer/highlight": "^1.2.1", - "@mdxeditor/editor": "^3.52.4", - "@storybook/addon-a11y": "^10.2.10", - "@storybook/addon-docs": "^10.2.10", - "@storybook/addon-themes": "^10.2.10", - "@storybook/react": "^10.2.10", - "@storybook/react-vite": "^10.2.10", - "@testing-library/dom": "^10.4.1", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "@vitejs/plugin-react": "^5.0.0", - "eslint": "^9", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.5.1", - "globals": "^17.3.0", - "postcss": "^8.4.49", - "prettier": "^3.8.1", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": "^7.0.0", - "storybook": "^10.2.10", - "tailwindcss": "^3.4.16", - "typescript": "^5.6.2", - "typescript-eslint": "^8.56.0", - "vite": "^7.0.0", - "yjs": "^13.6.24" - } } diff --git a/postcss.config.cjs b/postcss.config.cjs index e6e6836..ac51a71 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -1,7 +1,7 @@ module.exports = { - plugins: { - tailwindcss: { - config: './tailwind.storybook.config.cjs', + plugins: { + tailwindcss: { + config: './tailwind.storybook.config.cjs', + }, }, - }, }; diff --git a/renovate.json b/renovate.json index 7190a60..9775346 100644 --- a/renovate.json +++ b/renovate.json @@ -1,3 +1,3 @@ { - "$schema": "https://docs.renovatebot.com/renovate-schema.json" + "$schema": "https://docs.renovatebot.com/renovate-schema.json" } diff --git a/src/components/Button.stories.tsx b/src/components/Button.stories.tsx index 112bac6..ae0119c 100644 --- a/src/components/Button.stories.tsx +++ b/src/components/Button.stories.tsx @@ -3,85 +3,85 @@ import { PlusIcon } from '@heroicons/react/24/solid'; import { Button } from './Button'; const meta = { - title: 'Components/Button', - component: Button, - tags: ['autodocs'], - parameters: { - docs: { - description: { - component: - 'Polymorphic button component: renders a native ` - ); + const handleLinkClick: MouseEventHandler = (event) => { + if (disabled) { + event.preventDefault(); + return; + } + onClick?.(event); + }; + + if (to) { + return ( + + {content} + + ); + } + + return ( + + ); } diff --git a/src/components/Chip.stories.tsx b/src/components/Chip.stories.tsx index 22d8e11..744b6ad 100644 --- a/src/components/Chip.stories.tsx +++ b/src/components/Chip.stories.tsx @@ -2,50 +2,51 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Chip } from './Chip'; const meta = { - title: 'Components/Chip', - component: Chip, - tags: ['autodocs'], - parameters: { - docs: { - description: { - component: - 'Compact badge/chip component with solid or outlined style. `tone` accepts a Tailwind color token (for example `cyan-700`) and resolves it to the correct border/background/text color at runtime.', - }, + title: 'Components/Chip', + component: Chip, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: + 'Compact badge/chip component with solid or outlined style. `tone` accepts a Tailwind color token (for example `cyan-700`) and resolves it to the correct border/background/text color at runtime.', + }, + }, }, - }, - argTypes: { - variant: { - description: 'Chip visual style.', - options: ['solid', 'outlined'], - control: 'inline-radio', - table: { type: { summary: "'solid' | 'outlined'" } }, + argTypes: { + variant: { + description: 'Chip visual style.', + options: ['solid', 'outlined'], + control: 'inline-radio', + table: { type: { summary: "'solid' | 'outlined'" } }, + }, + tone: { + description: + 'Tailwind color token (format: `-`, for example `cyan-700`, `indigo-600`, `rose-500`).', + control: 'text', + table: { type: { summary: 'string' } }, + }, + as: { + description: + "Root tag or component to render (for example `'span'`, `'a'`, `'button'`).", + control: false, + table: { type: { summary: 'ElementType' } }, + }, + className: { + description: 'Extra CSS classes for the root element.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + children: { + description: 'Text or React node rendered inside the chip.', + control: 'text', + table: { type: { summary: 'ReactNode' } }, + }, }, - tone: { - description: - 'Tailwind color token (format: `-`, for example `cyan-700`, `indigo-600`, `rose-500`).', - control: 'text', - table: { type: { summary: 'string' } }, + args: { + children: 'Published', + variant: 'solid', }, - as: { - description: "Root tag or component to render (for example `'span'`, `'a'`, `'button'`).", - control: false, - table: { type: { summary: 'ElementType' } }, - }, - className: { - description: 'Extra CSS classes for the root element.', - control: 'text', - table: { type: { summary: 'string' } }, - }, - children: { - description: 'Text or React node rendered inside the chip.', - control: 'text', - table: { type: { summary: 'ReactNode' } }, - }, - }, - args: { - children: 'Published', - variant: 'solid', - }, } satisfies Meta; export default meta; @@ -54,44 +55,44 @@ type Story = StoryObj; export const SolidDefault: Story = {}; export const OutlinedIndigo: Story = { - args: { - variant: 'outlined', - tone: 'indigo-700', - children: 'Draft', - }, + args: { + variant: 'outlined', + tone: 'indigo-700', + children: 'Draft', + }, }; export const OutlinedCyan: Story = { - args: { - variant: 'outlined', - tone: 'cyan-700', - children: 'Archived', - }, + args: { + variant: 'outlined', + tone: 'cyan-700', + children: 'Archived', + }, }; export const ToneMatrix: Story = { - render: () => ( -
- Default - - Indigo - - - Cyan - - - Rose - - Default - - Indigo - - - Cyan - - - Rose - -
- ), + render: () => ( +
+ Default + + Indigo + + + Cyan + + + Rose + + Default + + Indigo + + + Cyan + + + Rose + +
+ ), }; diff --git a/src/components/Chip.tsx b/src/components/Chip.tsx index 7df932a..27ae62b 100644 --- a/src/components/Chip.tsx +++ b/src/components/Chip.tsx @@ -4,67 +4,67 @@ import type { CSSProperties, ElementType, ReactNode } from 'react'; type ChipVariant = 'solid' | 'outlined'; type ChipProps = { - variant?: ChipVariant; - // Tailwind color token, e.g. "cyan-700", "indigo-500", "rose-600". - tone?: string; - as?: T; - className?: string; - children: ReactNode; + variant?: ChipVariant; + // Tailwind color token, e.g. "cyan-700", "indigo-500", "rose-600". + tone?: string; + as?: T; + className?: string; + children: ReactNode; }; const variantClassMap: Record = { - solid: 'chip-solid', - outlined: 'chip-outlined', + solid: 'chip-solid', + outlined: 'chip-outlined', }; type TailwindPalette = Record; function resolveTailwindToneColor(tone: string | undefined): string | null { - const normalizedTone = tone?.trim().toLowerCase(); - if (normalizedTone == null || normalizedTone === '') { - return null; - } + const normalizedTone = tone?.trim().toLowerCase(); + if (normalizedTone == null || normalizedTone === '') { + return null; + } - const colorSource = tailwindColors as unknown as Record; - const lastDashIndex = normalizedTone.lastIndexOf('-'); + const colorSource = tailwindColors as unknown as Record; + const lastDashIndex = normalizedTone.lastIndexOf('-'); - if (lastDashIndex === -1) { - const direct = colorSource[normalizedTone]; - return typeof direct === 'string' ? direct : null; - } + if (lastDashIndex === -1) { + const direct = colorSource[normalizedTone]; + return typeof direct === 'string' ? direct : null; + } - const colorName = normalizedTone.slice(0, lastDashIndex); - const shade = normalizedTone.slice(lastDashIndex + 1); - const palette = colorSource[colorName]; + const colorName = normalizedTone.slice(0, lastDashIndex); + const shade = normalizedTone.slice(lastDashIndex + 1); + const palette = colorSource[colorName]; - if (palette == null || typeof palette !== 'object') { - return null; - } + if (palette == null || typeof palette !== 'object') { + return null; + } - const shadeColor = (palette as TailwindPalette)[shade]; - return typeof shadeColor === 'string' ? shadeColor : null; + const shadeColor = (palette as TailwindPalette)[shade]; + return typeof shadeColor === 'string' ? shadeColor : null; } export function Chip({ - variant = 'solid', - tone, - as, - className = '', - children, + variant = 'solid', + tone, + as, + className = '', + children, }: Readonly>) { - const Component = as ?? ('span' as ElementType); - const toneColor = resolveTailwindToneColor(tone); - const toneStyle: CSSProperties | undefined = - toneColor == null - ? undefined - : variant === 'solid' - ? { borderColor: toneColor, backgroundColor: toneColor, color: '#ffffff' } - : { borderColor: toneColor, color: toneColor }; - const classes = `chip-root ${variantClassMap[variant]} ${className}`.trim(); + const Component = as ?? ('span' as ElementType); + const toneColor = resolveTailwindToneColor(tone); + const toneStyle: CSSProperties | undefined = + toneColor == null + ? undefined + : variant === 'solid' + ? { borderColor: toneColor, backgroundColor: toneColor, color: '#ffffff' } + : { borderColor: toneColor, color: toneColor }; + const classes = `chip-root ${variantClassMap[variant]} ${className}`.trim(); - return ( - - {children} - - ); + return ( + + {children} + + ); } diff --git a/src/components/Dropdown.stories.tsx b/src/components/Dropdown.stories.tsx index f911a0b..945c497 100644 --- a/src/components/Dropdown.stories.tsx +++ b/src/components/Dropdown.stories.tsx @@ -3,152 +3,152 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Dropdown } from './Dropdown'; const choices = [ - { id: 'draft', label: 'Draft' }, - { id: 'review', label: 'In review' }, - { id: 'published', label: 'Published' }, + { id: 'draft', label: 'Draft' }, + { id: 'review', label: 'In review' }, + { id: 'published', label: 'Published' }, ]; const meta = { - title: 'Components/Dropdown', - component: Dropdown, - tags: ['autodocs'], - parameters: { - docs: { - description: { - component: - 'Styled select component with label, error state, stacked/inline layout, and multiple sizes.', - }, + title: 'Components/Dropdown', + component: Dropdown, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: + 'Styled select component with label, error state, stacked/inline layout, and multiple sizes.', + }, + }, }, - }, - argTypes: { - label: { - description: 'Label text shown above (stacked) or on the left (inline).', - control: 'text', - table: { type: { summary: 'string' } }, + argTypes: { + label: { + description: 'Label text shown above (stacked) or on the left (inline).', + control: 'text', + table: { type: { summary: 'string' } }, + }, + value: { + description: 'Current selected value (must match one `choices[].id`).', + control: 'text', + table: { type: { summary: 'string' } }, + }, + choices: { + description: 'Options list in `{ id: string; label: string }` format.', + control: 'object', + table: { type: { summary: 'Array<{ id: string; label: string }>' } }, + }, + size: { + description: 'Control size.', + options: ['sm', 'md', 'lg', 'full'], + control: 'inline-radio', + table: { type: { summary: "'sm' | 'md' | 'lg' | 'full'" } }, + }, + layout: { + description: 'Label/control layout mode.', + options: ['stacked', 'inline'], + control: 'inline-radio', + table: { type: { summary: "'stacked' | 'inline'" } }, + }, + disabled: { + description: 'Disables the field.', + control: 'boolean', + table: { type: { summary: 'boolean' } }, + }, + required: { + description: 'Sets the native HTML `required` attribute.', + control: 'boolean', + table: { type: { summary: 'boolean' } }, + }, + error: { + description: 'Error message shown below the field.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + className: { + description: 'Extra CSS classes for the wrapper.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + selectClassName: { + description: 'Extra CSS classes for the `` element.', - control: 'text', - table: { type: { summary: 'string' } }, - }, - onChange: { - description: 'Callback fired with the newly selected value.', - action: 'changed', - table: { type: { summary: '(value: string) => void' } }, - }, - }, - args: { - label: 'Status', - value: 'draft', - choices, - size: 'md', - layout: 'stacked', - }, } satisfies Meta; export default meta; type Story = StoryObj; export const Stacked: Story = { - render: (args) => { - const [value, setValue] = useState(args.value); - return ( - { - setValue(next); - args.onChange?.(next); - }} - /> - ); - }, + render: (args) => { + const [value, setValue] = useState(args.value); + return ( + { + setValue(next); + args.onChange?.(next); + }} + /> + ); + }, }; export const Inline: Story = { - args: { - layout: 'inline', - size: 'sm', - }, - render: (args) => { - const [value, setValue] = useState(args.value); - return ( - { - setValue(next); - args.onChange?.(next); - }} - /> - ); - }, + args: { + layout: 'inline', + size: 'sm', + }, + render: (args) => { + const [value, setValue] = useState(args.value); + return ( + { + setValue(next); + args.onChange?.(next); + }} + /> + ); + }, }; export const Disabled: Story = { - args: { - disabled: true, - }, + args: { + disabled: true, + }, }; export const WithError: Story = { - args: { - error: 'Please choose a valid status', - }, + args: { + error: 'Please choose a valid status', + }, }; export const SizeMatrix: Story = { - render: (args) => { - const [value, setValue] = useState(args.value); - return ( -
- - - - -
- ); - }, + render: (args) => { + const [value, setValue] = useState(args.value); + return ( +
+ + + + +
+ ); + }, }; diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx index 0095f81..bd910f0 100644 --- a/src/components/Dropdown.tsx +++ b/src/components/Dropdown.tsx @@ -5,91 +5,91 @@ import type { ComponentSize } from './types'; type DropdownLayout = 'stacked' | 'inline'; type DropdownChoice = { - label: string; - id: string; + label: string; + id: string; }; type DropdownProps = { - label?: string; - value: string; - choices: DropdownChoice[]; - size?: ComponentSize; - layout?: DropdownLayout; - disabled?: boolean; - required?: boolean; - onChange?: (value: string) => void; - error?: string; - className?: string; - selectClassName?: string; + label?: string; + value: string; + choices: DropdownChoice[]; + size?: ComponentSize; + layout?: DropdownLayout; + disabled?: boolean; + required?: boolean; + onChange?: (value: string) => void; + error?: string; + className?: string; + selectClassName?: string; }; export function Dropdown({ - label, - value, - choices, - size = 'md', - layout = 'stacked', - disabled = false, - required = false, - onChange, - error, - className = '', - selectClassName = '', + label, + value, + choices, + size = 'md', + layout = 'stacked', + disabled = false, + required = false, + onChange, + error, + className = '', + selectClassName = '', }: Readonly) { - const containerSizeClass = { - sm: 'max-w-xs', - md: 'max-w-sm', - lg: 'max-w-md', - full: 'max-w-none', - }[size]; + const containerSizeClass = { + sm: 'max-w-xs', + md: 'max-w-sm', + lg: 'max-w-md', + full: 'max-w-none', + }[size]; - const selectSizeClass = { - sm: 'h-8 !text-xs', - md: 'h-10 text-sm', - lg: 'h-12 text-sm', - full: 'h-10 text-sm', - }[size]; + const selectSizeClass = { + sm: 'h-8 !text-xs', + md: 'h-10 text-sm', + lg: 'h-12 text-sm', + full: 'h-10 text-sm', + }[size]; - const handleChange: ChangeEventHandler = (event) => { - onChange?.(event.target.value); - }; + const handleChange: ChangeEventHandler = (event) => { + onChange?.(event.target.value); + }; - const wrapperClass = - layout === 'inline' ? 'inline-flex w-auto items-center gap-2' : 'block w-full gap-1'; + const wrapperClass = + layout === 'inline' ? 'inline-flex w-auto items-center gap-2' : 'block w-full gap-1'; - const selectWrapperClass = layout === 'inline' ? 'relative' : 'relative mt-1'; - const labelClass = layout === 'inline' ? 'text-xs ui-body-secondary' : ''; + const selectWrapperClass = layout === 'inline' ? 'relative' : 'relative mt-1'; + const labelClass = layout === 'inline' ? 'text-xs ui-body-secondary' : ''; - return ( - - ); + {label ? {label} : null} +
+ + + +
+ {error ? ( + + {error} + + ) : null} + + ); } diff --git a/src/components/Form.stories.tsx b/src/components/Form.stories.tsx index 5c0fe60..efa234e 100644 --- a/src/components/Form.stories.tsx +++ b/src/components/Form.stories.tsx @@ -6,94 +6,94 @@ import { Form } from './Form'; import { InputField } from './InputField'; const meta = { - title: 'Components/Form', - component: Form, - tags: ['autodocs'], - parameters: { - docs: { - description: { - component: - 'Surface container with a title bar and a responsive content grid, intended for CMS forms.', - }, + title: 'Components/Form', + component: Form, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: + 'Surface container with a title bar and a responsive content grid, intended for CMS forms.', + }, + }, }, - }, - argTypes: { - title: { - description: 'Form title displayed in the header bar.', - control: 'text', - table: { type: { summary: 'string' } }, + argTypes: { + title: { + description: 'Form title displayed in the header bar.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + titleBarRight: { + description: 'Optional node rendered on the right side of the title bar.', + control: false, + table: { type: { summary: 'ReactNode' } }, + }, + children: { + description: 'Form content rendered inside the responsive grid.', + control: false, + table: { type: { summary: 'ReactNode' } }, + }, + className: { + description: 'Extra CSS classes for the root container.', + control: 'text', + table: { type: { summary: 'string' } }, + }, }, - titleBarRight: { - description: 'Optional node rendered on the right side of the title bar.', - control: false, - table: { type: { summary: 'ReactNode' } }, + args: { + title: 'Post details', }, - children: { - description: 'Form content rendered inside the responsive grid.', - control: false, - table: { type: { summary: 'ReactNode' } }, - }, - className: { - description: 'Extra CSS classes for the root container.', - control: 'text', - table: { type: { summary: 'string' } }, - }, - }, - args: { - title: 'Post details', - }, } satisfies Meta; export default meta; type Story = StoryObj; export const Basic: Story = { - args: { - children: ( - <> - - - - - ), - }, + args: { + children: ( + <> + + + + + ), + }, }; export const WithActions: Story = { - render: (args) => { - const [title, setTitle] = useState('Storybook powered CMS'); - const [status, setStatus] = useState('draft'); + render: (args) => { + const [title, setTitle] = useState('Storybook powered CMS'); + const [status, setStatus] = useState('draft'); - return ( -
}> -
- setTitle(event.target.value)} - size="full" - /> -
- - - - ); - }, + return ( +
}> +
+ setTitle(event.target.value)} + size="full" + /> +
+ + + + ); + }, }; diff --git a/src/components/Form.tsx b/src/components/Form.tsx index 9d72de2..5b95f67 100644 --- a/src/components/Form.tsx +++ b/src/components/Form.tsx @@ -2,24 +2,24 @@ import type { ReactNode } from 'react'; import { Label } from './Label'; type FormProps = { - title: string; - titleBarRight?: ReactNode; - children: ReactNode; - className?: string; + title: string; + titleBarRight?: ReactNode; + children: ReactNode; + className?: string; }; export function Form({ title, titleBarRight, children, className = '' }: Readonly) { - return ( -
-
- - {titleBarRight ?
{titleBarRight}
: null} -
+ return ( +
+
+ + {titleBarRight ?
{titleBarRight}
: null} +
-
{children}
-
- ); +
{children}
+
+ ); } diff --git a/src/components/InputField.stories.tsx b/src/components/InputField.stories.tsx index 05477c2..aac0183 100644 --- a/src/components/InputField.stories.tsx +++ b/src/components/InputField.stories.tsx @@ -4,234 +4,234 @@ import { MagnifyingGlassIcon } from '@heroicons/react/24/solid'; import { InputField } from './InputField'; const meta = { - title: 'Components/InputField', - component: InputField, - tags: ['autodocs'], - parameters: { - docs: { - description: { - component: - 'Text input field with optional label, validation state, size/layout variants, and password visibility toggle.', - }, + title: 'Components/InputField', + component: InputField, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: + 'Text input field with optional label, validation state, size/layout variants, and password visibility toggle.', + }, + }, }, - }, - argTypes: { - label: { - description: 'Label text shown above (stacked) or on the left (inline).', - control: 'text', - table: { type: { summary: 'string' } }, + argTypes: { + label: { + description: 'Label text shown above (stacked) or on the left (inline).', + control: 'text', + table: { type: { summary: 'string' } }, + }, + placeholder: { + description: 'Input placeholder text.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + type: { + description: 'Native input type.', + options: ['text', 'password', 'email'], + control: 'inline-radio', + table: { type: { summary: "'text' | 'password' | 'email'" } }, + }, + size: { + description: 'Input size.', + options: ['sm', 'md', 'lg', 'full'], + control: 'inline-radio', + table: { type: { summary: "'sm' | 'md' | 'lg' | 'full'" } }, + }, + layout: { + description: 'Label/input layout mode.', + options: ['stacked', 'inline'], + control: 'inline-radio', + table: { type: { summary: "'stacked' | 'inline'" } }, + }, + value: { + description: 'Controlled input value.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + name: { + description: 'Native input `name` attribute.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + disabled: { + description: 'Disables the input.', + control: 'boolean', + table: { type: { summary: 'boolean' } }, + }, + required: { + description: 'Sets the native HTML `required` attribute.', + control: 'boolean', + table: { type: { summary: 'boolean' } }, + }, + error: { + description: 'Validation message shown below the field.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + rightIcon: { + description: + 'Optional trailing icon node (ignored for password type because toggle icon is used).', + control: false, + table: { type: { summary: 'ReactNode' } }, + }, + className: { + description: 'Extra CSS classes for the outer wrapper.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + inputClassName: { + description: 'Extra CSS classes for the `` element.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + onChange: { + description: 'Change handler callback.', + action: 'changed', + table: { type: { summary: 'ChangeEventHandler' } }, + }, + onBlur: { + description: 'Blur handler callback.', + control: false, + table: { type: { summary: 'FocusEventHandler' } }, + }, + inputRef: { + description: 'Ref forwarded to the native `` element.', + control: false, + table: { type: { summary: 'Ref' } }, + }, }, - placeholder: { - description: 'Input placeholder text.', - control: 'text', - table: { type: { summary: 'string' } }, + args: { + label: 'Email', + type: 'email', + placeholder: 'name@example.com', + value: '', + size: 'md', + layout: 'stacked', }, - type: { - description: 'Native input type.', - options: ['text', 'password', 'email'], - control: 'inline-radio', - table: { type: { summary: "'text' | 'password' | 'email'" } }, - }, - size: { - description: 'Input size.', - options: ['sm', 'md', 'lg', 'full'], - control: 'inline-radio', - table: { type: { summary: "'sm' | 'md' | 'lg' | 'full'" } }, - }, - layout: { - description: 'Label/input layout mode.', - options: ['stacked', 'inline'], - control: 'inline-radio', - table: { type: { summary: "'stacked' | 'inline'" } }, - }, - value: { - description: 'Controlled input value.', - control: 'text', - table: { type: { summary: 'string' } }, - }, - name: { - description: 'Native input `name` attribute.', - control: 'text', - table: { type: { summary: 'string' } }, - }, - disabled: { - description: 'Disables the input.', - control: 'boolean', - table: { type: { summary: 'boolean' } }, - }, - required: { - description: 'Sets the native HTML `required` attribute.', - control: 'boolean', - table: { type: { summary: 'boolean' } }, - }, - error: { - description: 'Validation message shown below the field.', - control: 'text', - table: { type: { summary: 'string' } }, - }, - rightIcon: { - description: - 'Optional trailing icon node (ignored for password type because toggle icon is used).', - control: false, - table: { type: { summary: 'ReactNode' } }, - }, - className: { - description: 'Extra CSS classes for the outer wrapper.', - control: 'text', - table: { type: { summary: 'string' } }, - }, - inputClassName: { - description: 'Extra CSS classes for the `` element.', - control: 'text', - table: { type: { summary: 'string' } }, - }, - onChange: { - description: 'Change handler callback.', - action: 'changed', - table: { type: { summary: 'ChangeEventHandler' } }, - }, - onBlur: { - description: 'Blur handler callback.', - control: false, - table: { type: { summary: 'FocusEventHandler' } }, - }, - inputRef: { - description: 'Ref forwarded to the native `` element.', - control: false, - table: { type: { summary: 'Ref' } }, - }, - }, - args: { - label: 'Email', - type: 'email', - placeholder: 'name@example.com', - value: '', - size: 'md', - layout: 'stacked', - }, } satisfies Meta; export default meta; type Story = StoryObj; export const Text: Story = { - args: { - type: 'text', - label: 'Title', - placeholder: 'Write a title', - }, - render: (args) => { - const [value, setValue] = useState('Storybook integration'); - return ( - { - setValue(event.target.value); - args.onChange?.(event); - }} - /> - ); - }, + args: { + type: 'text', + label: 'Title', + placeholder: 'Write a title', + }, + render: (args) => { + const [value, setValue] = useState('Storybook integration'); + return ( + { + setValue(event.target.value); + args.onChange?.(event); + }} + /> + ); + }, }; export const PasswordWithToggle: Story = { - args: { - type: 'password', - label: 'Password', - placeholder: 'Type a strong password', - }, - render: (args) => { - const [value, setValue] = useState('pa55word'); - return ( - { - setValue(event.target.value); - args.onChange?.(event); - }} - /> - ); - }, + args: { + type: 'password', + label: 'Password', + placeholder: 'Type a strong password', + }, + render: (args) => { + const [value, setValue] = useState('pa55word'); + return ( + { + setValue(event.target.value); + args.onChange?.(event); + }} + /> + ); + }, }; export const InlineWithIcon: Story = { - args: { - type: 'text', - label: 'Search', - layout: 'inline', - size: 'sm', - rightIcon: , - }, - render: (args) => { - const [value, setValue] = useState('posts'); - return ( - { - setValue(event.target.value); - args.onChange?.(event); - }} - /> - ); - }, + args: { + type: 'text', + label: 'Search', + layout: 'inline', + size: 'sm', + rightIcon: , + }, + render: (args) => { + const [value, setValue] = useState('posts'); + return ( + { + setValue(event.target.value); + args.onChange?.(event); + }} + /> + ); + }, }; export const Error: Story = { - args: { - type: 'email', - label: 'Email', - value: 'invalid.mail', - error: 'Enter a valid email address', - }, + args: { + type: 'email', + label: 'Email', + value: 'invalid.mail', + error: 'Enter a valid email address', + }, }; export const Disabled: Story = { - args: { - type: 'text', - label: 'Read only field', - value: 'Locked content', - disabled: true, - }, + args: { + type: 'text', + label: 'Read only field', + value: 'Locked content', + disabled: true, + }, }; export const SizeMatrix: Story = { - args: { - type: 'text', - label: 'Name', - placeholder: 'Enter value', - }, - render: (args) => { - const [value, setValue] = useState('Beatrice'); - return ( -
- setValue(event.target.value)} - /> - setValue(event.target.value)} - /> - setValue(event.target.value)} - /> - setValue(event.target.value)} - /> -
- ); - }, + args: { + type: 'text', + label: 'Name', + placeholder: 'Enter value', + }, + render: (args) => { + const [value, setValue] = useState('Beatrice'); + return ( +
+ setValue(event.target.value)} + /> + setValue(event.target.value)} + /> + setValue(event.target.value)} + /> + setValue(event.target.value)} + /> +
+ ); + }, }; diff --git a/src/components/InputField.tsx b/src/components/InputField.tsx index bbdfb8c..332755d 100644 --- a/src/components/InputField.tsx +++ b/src/components/InputField.tsx @@ -8,104 +8,104 @@ type InputKind = 'text' | 'password' | 'email'; type Layout = 'stacked' | 'inline'; type InputFieldProps = { - label?: string; - placeholder?: string; - type: InputKind; - size?: ComponentSize; - layout?: Layout; - value: string; - name?: string; - onChange?: ChangeEventHandler; - onBlur?: FocusEventHandler; - inputRef?: Ref; - disabled?: boolean; - required?: boolean; - error?: string; - rightIcon?: ReactNode; - className?: string; - inputClassName?: string; + label?: string; + placeholder?: string; + type: InputKind; + size?: ComponentSize; + layout?: Layout; + value: string; + name?: string; + onChange?: ChangeEventHandler; + onBlur?: FocusEventHandler; + inputRef?: Ref; + disabled?: boolean; + required?: boolean; + error?: string; + rightIcon?: ReactNode; + className?: string; + inputClassName?: string; }; export function InputField({ - label, - placeholder = '', - type, - size = 'md', - layout = 'stacked', - value, - name, - onChange, - onBlur, - inputRef, - disabled = false, - required = false, - error, - rightIcon, - className = '', - inputClassName = '', + label, + placeholder = '', + type, + size = 'md', + layout = 'stacked', + value, + name, + onChange, + onBlur, + inputRef, + disabled = false, + required = false, + error, + rightIcon, + className = '', + inputClassName = '', }: Readonly) { - const [showPassword, setShowPassword] = useState(false); - const containerSizeClass = { - sm: 'max-w-xs', - md: 'max-w-sm', - lg: 'max-w-md', - full: 'max-w-none', - }[size]; + const [showPassword, setShowPassword] = useState(false); + const containerSizeClass = { + sm: 'max-w-xs', + md: 'max-w-sm', + lg: 'max-w-md', + full: 'max-w-none', + }[size]; - const inputSizeClass = { - sm: 'h-8 !text-xs', - md: 'h-10 text-sm', - lg: 'h-12 text-sm', - full: 'h-10 text-sm', - }[size]; + const inputSizeClass = { + sm: 'h-8 !text-xs', + md: 'h-10 text-sm', + lg: 'h-12 text-sm', + full: 'h-10 text-sm', + }[size]; - const wrapperClass = - layout === 'inline' ? 'inline-flex w-auto items-center gap-2' : 'block w-full gap-1'; - const labelClass = layout === 'inline' ? 'text-xs ui-body-secondary' : ''; - const isPasswordType = type === 'password'; - const resolvedType: InputKind = isPasswordType && showPassword ? 'text' : type; - const hasTrailingIcon = isPasswordType || Boolean(rightIcon); - const inputWrapperClass = layout === 'inline' ? 'relative' : 'relative mt-1'; + const wrapperClass = + layout === 'inline' ? 'inline-flex w-auto items-center gap-2' : 'block w-full gap-1'; + const labelClass = layout === 'inline' ? 'text-xs ui-body-secondary' : ''; + const isPasswordType = type === 'password'; + const resolvedType: InputKind = isPasswordType && showPassword ? 'text' : type; + const hasTrailingIcon = isPasswordType || Boolean(rightIcon); + const inputWrapperClass = layout === 'inline' ? 'relative' : 'relative mt-1'; - return ( - - ); + return ( + + ); } diff --git a/src/components/Label.stories.tsx b/src/components/Label.stories.tsx index 2c195d1..bf35627 100644 --- a/src/components/Label.stories.tsx +++ b/src/components/Label.stories.tsx @@ -2,48 +2,50 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Label } from './Label'; const meta = { - title: 'Components/Label', - component: Label, - tags: ['autodocs'], - parameters: { - docs: { - description: { - component: - 'Typography helper component for headings, body text, caption, error text, and inline code styles.', - }, - }, - }, - argTypes: { - variant: { - description: 'Typography style preset.', - options: ['h1', 'h2', 'h3', 'h4', 'body', 'body2', 'caption', 'error', 'code'], - control: 'select', - table: { - type: { - summary: "'h1' | 'h2' | 'h3' | 'h4' | 'body' | 'body2' | 'caption' | 'error' | 'code'", + title: 'Components/Label', + component: Label, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: + 'Typography helper component for headings, body text, caption, error text, and inline code styles.', + }, }, - }, }, - as: { - description: "Override rendered HTML tag or component (for example `'p'`, `'span'`, `'h2'`).", - control: false, - table: { type: { summary: 'ElementType' } }, + argTypes: { + variant: { + description: 'Typography style preset.', + options: ['h1', 'h2', 'h3', 'h4', 'body', 'body2', 'caption', 'error', 'code'], + control: 'select', + table: { + type: { + summary: + "'h1' | 'h2' | 'h3' | 'h4' | 'body' | 'body2' | 'caption' | 'error' | 'code'", + }, + }, + }, + as: { + description: + "Override rendered HTML tag or component (for example `'p'`, `'span'`, `'h2'`).", + control: false, + table: { type: { summary: 'ElementType' } }, + }, + className: { + description: 'Extra CSS classes.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + children: { + description: 'Label content.', + control: 'text', + table: { type: { summary: 'ReactNode' } }, + }, }, - className: { - description: 'Extra CSS classes.', - control: 'text', - table: { type: { summary: 'string' } }, + args: { + variant: 'body', + children: 'Label text', }, - children: { - description: 'Label content.', - control: 'text', - table: { type: { summary: 'ReactNode' } }, - }, - }, - args: { - variant: 'body', - children: 'Label text', - }, } satisfies Meta; export default meta; @@ -52,31 +54,31 @@ type Story = StoryObj; export const Body: Story = {}; export const Error: Story = { - args: { - variant: 'error', - children: 'This field is required', - }, + args: { + variant: 'error', + children: 'This field is required', + }, }; export const Code: Story = { - args: { - variant: 'code', - children: 'const isPublished = true;', - }, + args: { + variant: 'code', + children: 'const isPublished = true;', + }, }; export const VariantScale: Story = { - render: () => ( -
- - - - - - - - - -
- ), + render: () => ( +
+ + + + + + + + + +
+ ), }; diff --git a/src/components/Label.tsx b/src/components/Label.tsx index 9b9b074..2243323 100644 --- a/src/components/Label.tsx +++ b/src/components/Label.tsx @@ -3,44 +3,44 @@ import type { ElementType, ReactNode } from 'react'; type LabelVariant = 'h1' | 'h2' | 'h3' | 'h4' | 'body' | 'body2' | 'caption' | 'error' | 'code'; type LabelProps = { - variant?: LabelVariant; - as?: T; - className?: string; - children: ReactNode; + variant?: LabelVariant; + as?: T; + className?: string; + children: ReactNode; }; const variantClassMap: Record = { - h1: 'ui-title text-3xl font-bold', - h2: 'ui-title text-2xl font-semibold', - h3: 'ui-title text-xl font-semibold', - h4: 'ui-title text-base font-semibold', - body: 'ui-body-primary text-sm', - body2: 'ui-body-secondary text-sm', - caption: 'ui-kicker text-xs font-semibold uppercase tracking-[0.12em]', - error: 'ui-error text-sm', - code: 'ui-code text-sm font-mono', + h1: 'ui-title text-3xl font-bold', + h2: 'ui-title text-2xl font-semibold', + h3: 'ui-title text-xl font-semibold', + h4: 'ui-title text-base font-semibold', + body: 'ui-body-primary text-sm', + body2: 'ui-body-secondary text-sm', + caption: 'ui-kicker text-xs font-semibold uppercase tracking-[0.12em]', + error: 'ui-error text-sm', + code: 'ui-code text-sm font-mono', }; const variantTagMap: Record = { - h1: 'h1', - h2: 'h2', - h3: 'h3', - h4: 'h3', - body: 'p', - body2: 'p', - caption: 'p', - error: 'p', - code: 'code', + h1: 'h1', + h2: 'h2', + h3: 'h3', + h4: 'h3', + body: 'p', + body2: 'p', + caption: 'p', + error: 'p', + code: 'code', }; export function Label({ - variant = 'body', - as, - className = '', - children, + variant = 'body', + as, + className = '', + children, }: Readonly>) { - const Component = as ?? variantTagMap[variant]; - const classes = `${variantClassMap[variant]} ${className}`.trim(); + const Component = as ?? variantTagMap[variant]; + const classes = `${variantClassMap[variant]} ${className}`.trim(); - return {children}; + return {children}; } diff --git a/src/components/MDXEditorField.stories.tsx b/src/components/MDXEditorField.stories.tsx index 3f08346..a8f05e2 100644 --- a/src/components/MDXEditorField.stories.tsx +++ b/src/components/MDXEditorField.stories.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { - headingsPlugin, - listsPlugin, - markdownShortcutPlugin, - quotePlugin, + headingsPlugin, + listsPlugin, + markdownShortcutPlugin, + quotePlugin, } from '@mdxeditor/editor'; import { MDXEditorField } from './MDXEditorField'; @@ -19,138 +19,145 @@ This is a paragraph with **bold** and _italic_ text. `; const meta = { - title: 'Components/MDXEditorField', - component: MDXEditorField, - tags: ['autodocs'], - parameters: { - docs: { - description: { - component: - 'MDX editor wrapper with label, editable/read-only/disabled modes, theme class support, and error rendering.', - }, + title: 'Components/MDXEditorField', + component: MDXEditorField, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: + 'MDX editor wrapper with label, editable/read-only/disabled modes, theme class support, and error rendering.', + }, + }, }, - }, - argTypes: { - label: { - description: 'Field label shown above the editor.', - control: 'text', - table: { type: { summary: 'string' } }, + argTypes: { + label: { + description: 'Field label shown above the editor.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + markdown: { + description: 'Controlled markdown content value.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + readOnly: { + description: 'Enables read-only mode.', + control: 'boolean', + table: { type: { summary: 'boolean' } }, + }, + disabled: { + description: 'Disables editing and applies disabled visuals.', + control: 'boolean', + table: { type: { summary: 'boolean' } }, + }, + themeClassName: { + description: + 'Theme class applied to MDXEditor (for example `light-theme` or `dark-theme`).', + control: 'text', + table: { type: { summary: 'string' } }, + }, + plugins: { + description: 'MDXEditor plugins array.', + control: false, + table: { type: { summary: 'MDXEditorProps["plugins"]' } }, + }, + contentEditableClassName: { + description: 'CSS class used on the content editable area.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + className: { + description: 'Extra CSS classes for the outer wrapper.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + editorWrapperClassName: { + description: 'Extra CSS classes for the editor shell element.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + editorWrapperStyle: { + description: 'Inline style object for the editor shell.', + control: 'object', + table: { type: { summary: 'CSSProperties' } }, + }, + editorClassName: { + description: 'Extra CSS classes for the MDXEditor instance.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + error: { + description: 'Error message shown below the editor.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + onChange: { + description: 'Callback fired when markdown changes in editable mode.', + action: 'changed', + table: { type: { summary: '(markdown: string) => void' } }, + }, + editorRef: { + description: 'Ref to MDXEditor methods.', + control: false, + table: { type: { summary: 'Ref' } }, + }, }, - markdown: { - description: 'Controlled markdown content value.', - control: 'text', - table: { type: { summary: 'string' } }, + args: { + label: 'Content', + markdown: sampleMarkdown, + plugins: basePlugins, + themeClassName: '', }, - readOnly: { - description: 'Enables read-only mode.', - control: 'boolean', - table: { type: { summary: 'boolean' } }, - }, - disabled: { - description: 'Disables editing and applies disabled visuals.', - control: 'boolean', - table: { type: { summary: 'boolean' } }, - }, - themeClassName: { - description: 'Theme class applied to MDXEditor (for example `light-theme` or `dark-theme`).', - control: 'text', - table: { type: { summary: 'string' } }, - }, - plugins: { - description: 'MDXEditor plugins array.', - control: false, - table: { type: { summary: 'MDXEditorProps["plugins"]' } }, - }, - contentEditableClassName: { - description: 'CSS class used on the content editable area.', - control: 'text', - table: { type: { summary: 'string' } }, - }, - className: { - description: 'Extra CSS classes for the outer wrapper.', - control: 'text', - table: { type: { summary: 'string' } }, - }, - editorWrapperClassName: { - description: 'Extra CSS classes for the editor shell element.', - control: 'text', - table: { type: { summary: 'string' } }, - }, - editorWrapperStyle: { - description: 'Inline style object for the editor shell.', - control: 'object', - table: { type: { summary: 'CSSProperties' } }, - }, - editorClassName: { - description: 'Extra CSS classes for the MDXEditor instance.', - control: 'text', - table: { type: { summary: 'string' } }, - }, - error: { - description: 'Error message shown below the editor.', - control: 'text', - table: { type: { summary: 'string' } }, - }, - onChange: { - description: 'Callback fired when markdown changes in editable mode.', - action: 'changed', - table: { type: { summary: '(markdown: string) => void' } }, - }, - editorRef: { - description: 'Ref to MDXEditor methods.', - control: false, - table: { type: { summary: 'Ref' } }, - }, - }, - args: { - label: 'Content', - markdown: sampleMarkdown, - plugins: basePlugins, - themeClassName: '', - }, } satisfies Meta; export default meta; type Story = StoryObj; export const Editable: Story = { - render: (args) => { - const [markdown, setMarkdown] = useState(args.markdown); - return ( -
- { - setMarkdown(next); - args.onChange?.(next); - }} - editorWrapperClassName="mt-2 overflow-hidden rounded-xl border" - /> -
- ); - }, + render: (args) => { + const [markdown, setMarkdown] = useState(args.markdown); + return ( +
+ { + setMarkdown(next); + args.onChange?.(next); + }} + editorWrapperClassName="mt-2 overflow-hidden rounded-xl border" + /> +
+ ); + }, }; export const ReadOnly: Story = { - args: { - readOnly: true, - }, - render: (args) => ( -
- -
- ), + args: { + readOnly: true, + }, + render: (args) => ( +
+ +
+ ), }; export const DisabledWithError: Story = { - args: { - disabled: true, - error: 'Editor is currently disabled', - }, - render: (args) => ( -
- -
- ), + args: { + disabled: true, + error: 'Editor is currently disabled', + }, + render: (args) => ( +
+ +
+ ), }; diff --git a/src/components/MDXEditorField.tsx b/src/components/MDXEditorField.tsx index 2f83e68..185624b 100644 --- a/src/components/MDXEditorField.tsx +++ b/src/components/MDXEditorField.tsx @@ -3,75 +3,75 @@ import type { CSSProperties, Ref } from 'react'; import { Label } from './Label'; type MDXEditorFieldProps = { - label?: string; - markdown: string; - readOnly?: boolean; - disabled?: boolean; - onChange?: (markdown: string) => void; - editorRef?: Ref; - themeClassName: string; - plugins: MDXEditorProps['plugins']; - contentEditableClassName?: string; - className?: string; - editorWrapperClassName?: string; - editorWrapperStyle?: CSSProperties; - editorClassName?: string; - error?: string; + label?: string; + markdown: string; + readOnly?: boolean; + disabled?: boolean; + onChange?: (markdown: string) => void; + editorRef?: Ref; + themeClassName: string; + plugins: MDXEditorProps['plugins']; + contentEditableClassName?: string; + className?: string; + editorWrapperClassName?: string; + editorWrapperStyle?: CSSProperties; + editorClassName?: string; + error?: string; }; export function MDXEditorField({ - label, - markdown, - readOnly = false, - disabled = false, - onChange, - editorRef, - themeClassName, - plugins, - contentEditableClassName = 'mdx-content', - className = '', - editorWrapperClassName = 'post-mdx-editor mt-2 overflow-hidden rounded-xl border', - editorWrapperStyle, - editorClassName = '', - error, + label, + markdown, + readOnly = false, + disabled = false, + onChange, + editorRef, + themeClassName, + plugins, + contentEditableClassName = 'mdx-content', + className = '', + editorWrapperClassName = 'post-mdx-editor mt-2 overflow-hidden rounded-xl border', + editorWrapperStyle, + editorClassName = '', + error, }: Readonly) { - const resolvedEditorClassName = `${themeClassName} ${editorClassName}`.trim(); - const editorModeKey = disabled || readOnly ? 'read-only' : 'editable'; - const resolvedEditorWrapperClassName = - `${editorWrapperClassName} ${disabled ? 'post-mdx-editor--disabled' : 'post-mdx-editor--enabled'}`.trim(); - const resolvedEditorWrapperStyle: CSSProperties = { - backgroundColor: disabled ? 'var(--field-disabled-bg)' : 'var(--field-bg)', - borderColor: disabled ? 'var(--field-disabled-border)' : 'var(--field-border)', - ...editorWrapperStyle, - }; + const resolvedEditorClassName = `${themeClassName} ${editorClassName}`.trim(); + const editorModeKey = disabled || readOnly ? 'read-only' : 'editable'; + const resolvedEditorWrapperClassName = + `${editorWrapperClassName} ${disabled ? 'post-mdx-editor--disabled' : 'post-mdx-editor--enabled'}`.trim(); + const resolvedEditorWrapperStyle: CSSProperties = { + backgroundColor: disabled ? 'var(--field-disabled-bg)' : 'var(--field-bg)', + borderColor: disabled ? 'var(--field-disabled-border)' : 'var(--field-border)', + ...editorWrapperStyle, + }; - return ( -
- {label ? ( - - ) : null} -
- -
- {error ? ( - - ) : null} -
- ); + return ( +
+ {label ? ( + + ) : null} +
+ +
+ {error ? ( + + ) : null} +
+ ); } diff --git a/src/components/SidebarNavItem.stories.tsx b/src/components/SidebarNavItem.stories.tsx index b323a65..b66b54b 100644 --- a/src/components/SidebarNavItem.stories.tsx +++ b/src/components/SidebarNavItem.stories.tsx @@ -3,80 +3,80 @@ import { HomeIcon, UserCircleIcon, UsersIcon } from '@heroicons/react/24/outline import { SidebarNavItem } from './SidebarNavItem'; const meta = { - title: 'Components/SidebarNavItem', - component: SidebarNavItem, - tags: ['autodocs'], - parameters: { - layout: 'padded', - docs: { - description: { - component: - 'Sidebar navigation link with active state styling and collapsed/expanded rendering mode.', - }, + title: 'Components/SidebarNavItem', + component: SidebarNavItem, + tags: ['autodocs'], + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'Sidebar navigation link with active state styling and collapsed/expanded rendering mode.', + }, + }, }, - }, - argTypes: { - to: { - description: 'Destination route path.', - control: 'text', - table: { type: { summary: 'string' } }, + argTypes: { + to: { + description: 'Destination route path.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + label: { + description: 'Navigation item label.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + icon: { + description: 'Icon component rendered before the label.', + control: false, + table: { type: { summary: 'ComponentType>' } }, + }, + collapsed: { + description: 'Collapsed state. When true, desktop view shows icon-only rail style.', + control: 'boolean', + table: { type: { summary: 'boolean' } }, + }, + onClick: { + description: 'Optional click callback (for example to close mobile drawer).', + action: 'clicked', + table: { type: { summary: '() => void' } }, + }, }, - label: { - description: 'Navigation item label.', - control: 'text', - table: { type: { summary: 'string' } }, + args: { + to: '/', + label: 'Dashboard', + icon: HomeIcon, + collapsed: false, }, - icon: { - description: 'Icon component rendered before the label.', - control: false, - table: { type: { summary: 'ComponentType>' } }, - }, - collapsed: { - description: 'Collapsed state. When true, desktop view shows icon-only rail style.', - control: 'boolean', - table: { type: { summary: 'boolean' } }, - }, - onClick: { - description: 'Optional click callback (for example to close mobile drawer).', - action: 'clicked', - table: { type: { summary: '() => void' } }, - }, - }, - args: { - to: '/', - label: 'Dashboard', - icon: HomeIcon, - collapsed: false, - }, } satisfies Meta; export default meta; type Story = StoryObj; export const Expanded: Story = { - render: (args) => ( - - ), + render: (args) => ( + + ), }; export const Collapsed: Story = { - args: { - collapsed: true, - }, - render: (args) => ( - - ), + args: { + collapsed: true, + }, + render: (args) => ( + + ), }; diff --git a/src/components/SidebarNavItem.tsx b/src/components/SidebarNavItem.tsx index f64fa9b..6867466 100644 --- a/src/components/SidebarNavItem.tsx +++ b/src/components/SidebarNavItem.tsx @@ -4,40 +4,40 @@ import { NavLink } from 'react-router-dom'; type IconType = ComponentType>; type SidebarNavItemProps = { - to: string; - label: string; - icon: IconType; - collapsed: boolean; - onClick?: () => void; + to: string; + label: string; + icon: IconType; + collapsed: boolean; + onClick?: () => void; }; export function SidebarNavItem({ - to, - label, - icon: Icon, - collapsed, - onClick, + to, + label, + icon: Icon, + collapsed, + onClick, }: Readonly) { - const layoutClass = collapsed - ? 'mx-auto w-8 justify-center px-0' - : 'px-2 lg:w-full lg:justify-start'; + const layoutClass = collapsed + ? 'mx-auto w-8 justify-center px-0' + : 'px-2 lg:w-full lg:justify-start'; - return ( - - `inline-flex h-8 items-center rounded-lg text-sm font-medium transition ${layoutClass} ${ - isActive ? 'bg-accent-500 text-white' : 'ui-body-secondary hover:bg-zinc-500/15' - }` - } - > - - {!collapsed ? ( - {label} - ) : ( - {label} - )} - - ); + return ( + + `inline-flex h-8 items-center rounded-lg text-sm font-medium transition ${layoutClass} ${ + isActive ? 'bg-accent-500 text-white' : 'ui-body-secondary hover:bg-zinc-500/15' + }` + } + > + + {!collapsed ? ( + {label} + ) : ( + {label} + )} + + ); } diff --git a/src/components/Table.stories.tsx b/src/components/Table.stories.tsx index ad20ef6..dcdcbf7 100644 --- a/src/components/Table.stories.tsx +++ b/src/components/Table.stories.tsx @@ -5,162 +5,163 @@ import { Chip } from './Chip'; import { Table, type TableHeader } from './Table'; type UserRow = { - id: string; - name: string; - role: 'ADMIN' | 'EDITOR' | 'AUTHOR'; - status: 'Active' | 'Pending'; - posts: number; + id: string; + name: string; + role: 'ADMIN' | 'EDITOR' | 'AUTHOR'; + status: 'Active' | 'Pending'; + posts: number; }; const rows: UserRow[] = [ - { id: '1', name: 'Beatrice Rosa', role: 'ADMIN', status: 'Active', posts: 48 }, - { id: '2', name: 'Luca Valli', role: 'EDITOR', status: 'Active', posts: 26 }, - { id: '3', name: 'Marta Bellini', role: 'AUTHOR', status: 'Pending', posts: 4 }, - { id: '4', name: 'Giulia Fontana', role: 'AUTHOR', status: 'Active', posts: 12 }, - { id: '5', name: 'Andrea Pini', role: 'EDITOR', status: 'Pending', posts: 9 }, - { id: '6', name: 'Sofia Denti', role: 'AUTHOR', status: 'Active', posts: 7 }, - { id: '7', name: 'Marco Serra', role: 'AUTHOR', status: 'Active', posts: 18 }, - { id: '8', name: 'Elena Neri', role: 'EDITOR', status: 'Active', posts: 31 }, + { id: '1', name: 'Beatrice Rosa', role: 'ADMIN', status: 'Active', posts: 48 }, + { id: '2', name: 'Luca Valli', role: 'EDITOR', status: 'Active', posts: 26 }, + { id: '3', name: 'Marta Bellini', role: 'AUTHOR', status: 'Pending', posts: 4 }, + { id: '4', name: 'Giulia Fontana', role: 'AUTHOR', status: 'Active', posts: 12 }, + { id: '5', name: 'Andrea Pini', role: 'EDITOR', status: 'Pending', posts: 9 }, + { id: '6', name: 'Sofia Denti', role: 'AUTHOR', status: 'Active', posts: 7 }, + { id: '7', name: 'Marco Serra', role: 'AUTHOR', status: 'Active', posts: 18 }, + { id: '8', name: 'Elena Neri', role: 'EDITOR', status: 'Active', posts: 31 }, ]; const headers: TableHeader[] = [ - { - id: 'name', - label: 'Name', - value: (row) => row.name, - sortable: true, - sortField: 'name', - cellClassName: 'table-cell-primary', - }, - { - id: 'role', - label: 'Role', - value: (row) => row.role, - sortable: true, - sortField: 'role', - }, - { - id: 'status', - label: 'Status', - value: (row) => ( - - {row.status} - - ), - }, - { - id: 'posts', - label: 'Posts', - value: (row) => row.posts, - sortable: true, - sortField: 'posts', - }, + { + id: 'name', + label: 'Name', + value: (row) => row.name, + sortable: true, + sortField: 'name', + cellClassName: 'table-cell-primary', + }, + { + id: 'role', + label: 'Role', + value: (row) => row.role, + sortable: true, + sortField: 'role', + }, + { + id: 'status', + label: 'Status', + value: (row) => ( + + {row.status} + + ), + }, + { + id: 'posts', + label: 'Posts', + value: (row) => row.posts, + sortable: true, + sortField: 'posts', + }, ]; type UsersTableProps = { - data: UserRow[]; - isLoading?: boolean; - emptyMessage?: string; - sorting?: SortState | null; - onSortChange?: (field: string) => void; - pagination?: { - page: number; - pageSize: number; - total: number; - totalPages: number; - onPageChange: (page: number) => void; - onPageSizeChange?: (pageSize: number) => void; - }; + data: UserRow[]; + isLoading?: boolean; + emptyMessage?: string; + sorting?: SortState | null; + onSortChange?: (field: string) => void; + pagination?: { + page: number; + pageSize: number; + total: number; + totalPages: number; + onPageChange: (page: number) => void; + onPageSizeChange?: (pageSize: number) => void; + }; }; function UsersTable(props: Readonly) { - return ( - - headers={headers} - data={props.data} - rowKey={(row) => row.id} - isLoading={props.isLoading} - emptyMessage={props.emptyMessage} - sorting={props.sorting} - onSortChange={props.onSortChange} - pagination={props.pagination} - /> - ); + return ( + + headers={headers} + data={props.data} + rowKey={(row) => row.id} + isLoading={props.isLoading} + emptyMessage={props.emptyMessage} + sorting={props.sorting} + onSortChange={props.onSortChange} + pagination={props.pagination} + /> + ); } function sortRows(data: UserRow[], sorting: SortState | null): UserRow[] { - if (!sorting) { - return data; - } + if (!sorting) { + return data; + } - const sorted = [...data]; - sorted.sort((a, b) => { - const left = a[sorting.field as keyof UserRow]; - const right = b[sorting.field as keyof UserRow]; - if (left === right) { - return 0; - } - if (typeof left === 'number' && typeof right === 'number') { - return sorting.direction === 'asc' ? left - right : right - left; - } - return sorting.direction === 'asc' - ? String(left).localeCompare(String(right)) - : String(right).localeCompare(String(left)); - }); - return sorted; + const sorted = [...data]; + sorted.sort((a, b) => { + const left = a[sorting.field as keyof UserRow]; + const right = b[sorting.field as keyof UserRow]; + if (left === right) { + return 0; + } + if (typeof left === 'number' && typeof right === 'number') { + return sorting.direction === 'asc' ? left - right : right - left; + } + return sorting.direction === 'asc' + ? String(left).localeCompare(String(right)) + : String(right).localeCompare(String(left)); + }); + return sorted; } const meta = { - title: 'Components/Table', - component: UsersTable, - tags: ['autodocs'], - parameters: { - docs: { - description: { - component: - 'Generic data table with loading/empty states, optional sorting controls, and optional pagination footer.', - }, - }, - }, - argTypes: { - data: { - description: 'Rows rendered in the table body.', - control: 'object', - table: { type: { summary: 'UserRow[]' } }, - }, - isLoading: { - description: 'When true, shows the loading indicator row.', - control: 'boolean', - table: { type: { summary: 'boolean' } }, - }, - emptyMessage: { - description: 'Message shown when `data` is empty and `isLoading` is false.', - control: 'text', - table: { type: { summary: 'string' } }, - }, - sorting: { - description: 'Current sort state object. Use `null` for no active sorting.', - control: 'object', - table: { type: { summary: "{ field: string; direction: 'asc' | 'desc' } | null" } }, - }, - onSortChange: { - description: 'Callback fired when a sortable header is clicked.', - action: 'sort changed', - table: { type: { summary: '(field: string) => void' } }, - }, - pagination: { - description: 'Pagination config object. When omitted, pagination footer is hidden.', - control: 'object', - table: { - type: { - summary: '{ page; pageSize; total; totalPages; onPageChange; onPageSizeChange? }', + title: 'Components/Table', + component: UsersTable, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: + 'Generic data table with loading/empty states, optional sorting controls, and optional pagination footer.', + }, }, - }, }, - }, - args: { - data: rows, - }, + argTypes: { + data: { + description: 'Rows rendered in the table body.', + control: 'object', + table: { type: { summary: 'UserRow[]' } }, + }, + isLoading: { + description: 'When true, shows the loading indicator row.', + control: 'boolean', + table: { type: { summary: 'boolean' } }, + }, + emptyMessage: { + description: 'Message shown when `data` is empty and `isLoading` is false.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + sorting: { + description: 'Current sort state object. Use `null` for no active sorting.', + control: 'object', + table: { type: { summary: "{ field: string; direction: 'asc' | 'desc' } | null" } }, + }, + onSortChange: { + description: 'Callback fired when a sortable header is clicked.', + action: 'sort changed', + table: { type: { summary: '(field: string) => void' } }, + }, + pagination: { + description: 'Pagination config object. When omitted, pagination footer is hidden.', + control: 'object', + table: { + type: { + summary: + '{ page; pageSize; total; totalPages; onPageChange; onPageSizeChange? }', + }, + }, + }, + }, + args: { + data: rows, + }, } satisfies Meta; export default meta; @@ -169,61 +170,61 @@ type Story = StoryObj; export const WithRows: Story = {}; export const Loading: Story = { - args: { - isLoading: true, - }, + args: { + isLoading: true, + }, }; export const Empty: Story = { - args: { - data: [], - emptyMessage: 'No users found', - }, + args: { + data: [], + emptyMessage: 'No users found', + }, }; export const InteractiveSortingAndPagination: Story = { - render: () => { - const [sorting, setSorting] = useState({ - field: 'name', - direction: 'asc', - }); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(5); + render: () => { + const [sorting, setSorting] = useState({ + field: 'name', + direction: 'asc', + }); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(5); - const sorted = useMemo(() => sortRows(rows, sorting), [sorting]); - const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize)); - const safePage = Math.min(page, totalPages); - const start = (safePage - 1) * pageSize; - const pagedRows = sorted.slice(start, start + pageSize); + const sorted = useMemo(() => sortRows(rows, sorting), [sorting]); + const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize)); + const safePage = Math.min(page, totalPages); + const start = (safePage - 1) * pageSize; + const pagedRows = sorted.slice(start, start + pageSize); - return ( - { - setPage(1); - setSorting((prev) => { - if (!prev || prev.field !== field) { - return { field, direction: 'asc' }; - } - if (prev.direction === 'asc') { - return { field, direction: 'desc' }; - } - return null; - }); - }} - pagination={{ - page: safePage, - pageSize, - total: sorted.length, - totalPages, - onPageChange: setPage, - onPageSizeChange: (next) => { - setPage(1); - setPageSize(next); - }, - }} - /> - ); - }, + return ( + { + setPage(1); + setSorting((prev) => { + if (!prev || prev.field !== field) { + return { field, direction: 'asc' }; + } + if (prev.direction === 'asc') { + return { field, direction: 'desc' }; + } + return null; + }); + }} + pagination={{ + page: safePage, + pageSize, + total: sorted.length, + totalPages, + onPageChange: setPage, + onPageSizeChange: (next) => { + setPage(1); + setPageSize(next); + }, + }} + /> + ); + }, }; diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 179d0b4..6244998 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -1,10 +1,10 @@ import type { ReactNode } from 'react'; import { - ArrowPathIcon, - ChevronDownIcon, - ChevronLeftIcon, - ChevronRightIcon, - ChevronUpIcon, + ArrowPathIcon, + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, + ChevronUpIcon, } from '@heroicons/react/24/solid'; import { ArrowsUpDownIcon } from '@heroicons/react/24/outline'; import { Button } from './Button'; @@ -15,183 +15,192 @@ import type { SortState } from '../types/sort'; type HeaderValue = ReactNode | ((row: T) => ReactNode); export type TableHeader = { - label: string; - id: string; - value: HeaderValue; - sortable?: boolean; - sortField?: string; - headerClassName?: string; - cellClassName?: string; + label: string; + id: string; + value: HeaderValue; + sortable?: boolean; + sortField?: string; + headerClassName?: string; + cellClassName?: string; }; type TableProps = { - headers: TableHeader[]; - data: T[]; - rowKey: (row: T, index: number) => string; - isLoading?: boolean; - emptyMessage?: string; - className?: string; - sorting?: SortState | null; - onSortChange?: (field: string) => void; - pagination?: { - page: number; - pageSize: number; - total: number; - totalPages: number; - onPageChange: (page: number) => void; - onPageSizeChange?: (pageSize: number) => void; - }; + headers: TableHeader[]; + data: T[]; + rowKey: (row: T, index: number) => string; + isLoading?: boolean; + emptyMessage?: string; + className?: string; + sorting?: SortState | null; + onSortChange?: (field: string) => void; + pagination?: { + page: number; + pageSize: number; + total: number; + totalPages: number; + onPageChange: (page: number) => void; + onPageSizeChange?: (pageSize: number) => void; + }; }; export function Table({ - headers, - data, - rowKey, - isLoading = false, - emptyMessage = 'No data to show.', - className = '', - sorting = null, - onSortChange, - pagination, + headers, + data, + rowKey, + isLoading = false, + emptyMessage = 'No data to show.', + className = '', + sorting = null, + onSortChange, + pagination, }: Readonly>) { - const canGoPrev = pagination != null && pagination.page > 1; - const canGoNext = pagination != null && pagination.page < pagination.totalPages; + const canGoPrev = pagination != null && pagination.page > 1; + const canGoNext = pagination != null && pagination.page < pagination.totalPages; - return ( -
-
- - - - {headers.map((header) => { - const canSort = - header.sortable === true && - typeof onSortChange === 'function' && - typeof header.sortField === 'string' && - header.sortField.length > 0; - const isActiveSort = canSort && sorting?.field === header.sortField; - const sortDirection = isActiveSort ? sorting?.direction : null; + return ( +
+
+
+ + + {headers.map((header) => { + const canSort = + header.sortable === true && + typeof onSortChange === 'function' && + typeof header.sortField === 'string' && + header.sortField.length > 0; + const isActiveSort = canSort && sorting?.field === header.sortField; + const sortDirection = isActiveSort ? sorting?.direction : null; - return ( - - ); - })} - - - - {isLoading ? ( - - - + return ( + + ); + })} + + + + {isLoading ? ( + + + + ) : null} + {!isLoading && data.length === 0 ? ( + + + + ) : null} + {!isLoading && + data.map((row, index) => ( + + {headers.map((header) => { + const content = + typeof header.value === 'function' + ? (header.value as (item: T) => ReactNode)(row) + : header.value; + return ( + + ); + })} + + ))} + +
- {canSort ? ( - - ) : ( - header.label - )} -
- -
+ {canSort ? ( + + ) : ( + header.label + )} +
+ +
+ +
+ {content} +
+
+ {pagination ? ( +
+ +
+ {pagination.onPageSizeChange ? ( + ({ + id: String(size), + label: String(size), + }))} + size="sm" + layout="inline" + className="max-w-none" + selectClassName="rounded-lg px-2" + disabled={isLoading} + onChange={(value) => pagination.onPageSizeChange?.(Number(value))} + /> + ) : null} +
+
) : null} - {!isLoading && data.length === 0 ? ( - - - - - - ) : null} - {!isLoading && - data.map((row, index) => ( - - {headers.map((header) => { - const content = - typeof header.value === 'function' - ? (header.value as (item: T) => ReactNode)(row) - : header.value; - return ( - - {content} - - ); - })} - - ))} - - -
- {pagination ? ( -
- -
- {pagination.onPageSizeChange ? ( - ({ - id: String(size), - label: String(size), - }))} - size="sm" - layout="inline" - className="max-w-none" - selectClassName="rounded-lg px-2" - disabled={isLoading} - onChange={(value) => pagination.onPageSizeChange?.(Number(value))} - /> - ) : null} -
- ) : null} - - ); + ); } diff --git a/src/styles/base.css b/src/styles/base.css index 892c08f..cfb057d 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -1,93 +1,93 @@ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); :root { - --bg-page: #16121a; - --surface-bg: rgba(24, 24, 27, 0.45); - --surface-bg-strong: rgba(24, 24, 27, 0.62); - --surface-border: rgba(82, 82, 91, 0.6); - --surface-divider: rgba(63, 63, 70, 0.85); - --text-primary: #d5cfdf; - --text-secondary: #bcb7c8; - --text-muted: #a1a1aa; - --text-soft: #8f8b9c; - --field-bg: rgba(24, 24, 27, 0.6); - --field-border: #3f3f46; - --field-disabled-bg: rgba(24, 24, 27, 0.5); - --field-disabled-border: #3f3f46; - --field-disabled-text: #bbb6c3; - --field-disabled-placeholder: #71717a; - --ghost-bg: rgba(24, 24, 27, 0.5); - --ghost-border: #3f3f46; - --ghost-hover: rgba(39, 39, 42, 0.7); - --ghost-disabled-bg: rgba(24, 24, 27, 0.3); - --ghost-disabled-border: #27272a; - --ghost-disabled-text: #71717a; - --primary-disabled-bg: #3f3f46; - --primary-disabled-text: #a1a1aa; - --table-head-bg: rgba(24, 24, 27, 0.8); - --table-row-divider: #27272a; - --auth-chrome-bg: rgba(24, 24, 27, 0.7); - --auth-glass-blur: 22px; - --auth-sidebar-mobile-width: min(86vw, 320px); - --auth-right-sidebar-mobile-width: min(86vw, 340px); - --error-border: rgba(252, 165, 165, 0.3); - --error-bg: rgba(239, 68, 68, 0.1); - --error-text: #fecaca; - --mdx-link: #7d6f98; - --mdx-link-hover: #9587ad; - --mdx-inline-code-bg: #27272a; - --mdx-inline-code-border: #3f3f46; - --mdx-codeblock-bg: #18181b; - --mdx-codeblock-border: #3f3f46; - --mdx-codeblock-text: #e4e4e7; - --mdx-codeblock-gutter: #a1a1aa; - --mdx-codeblock-active: #27272a; - --mdx-codeblock-selection: rgba(125, 111, 152, 0.35); - --mdx-codeblock-bracket: rgba(125, 111, 152, 0.45); - --shadow-glow: 0 0 0 1px rgba(63, 63, 70, 0.65), 0 18px 44px rgba(0, 0, 0, 0.45); + --bg-page: #16121a; + --surface-bg: rgba(24, 24, 27, 0.45); + --surface-bg-strong: rgba(24, 24, 27, 0.62); + --surface-border: rgba(82, 82, 91, 0.6); + --surface-divider: rgba(63, 63, 70, 0.85); + --text-primary: #d5cfdf; + --text-secondary: #bcb7c8; + --text-muted: #a1a1aa; + --text-soft: #8f8b9c; + --field-bg: rgba(24, 24, 27, 0.6); + --field-border: #3f3f46; + --field-disabled-bg: rgba(24, 24, 27, 0.5); + --field-disabled-border: #3f3f46; + --field-disabled-text: #bbb6c3; + --field-disabled-placeholder: #71717a; + --ghost-bg: rgba(24, 24, 27, 0.5); + --ghost-border: #3f3f46; + --ghost-hover: rgba(39, 39, 42, 0.7); + --ghost-disabled-bg: rgba(24, 24, 27, 0.3); + --ghost-disabled-border: #27272a; + --ghost-disabled-text: #71717a; + --primary-disabled-bg: #3f3f46; + --primary-disabled-text: #a1a1aa; + --table-head-bg: rgba(24, 24, 27, 0.8); + --table-row-divider: #27272a; + --auth-chrome-bg: rgba(24, 24, 27, 0.7); + --auth-glass-blur: 22px; + --auth-sidebar-mobile-width: min(86vw, 320px); + --auth-right-sidebar-mobile-width: min(86vw, 340px); + --error-border: rgba(252, 165, 165, 0.3); + --error-bg: rgba(239, 68, 68, 0.1); + --error-text: #fecaca; + --mdx-link: #7d6f98; + --mdx-link-hover: #9587ad; + --mdx-inline-code-bg: #27272a; + --mdx-inline-code-border: #3f3f46; + --mdx-codeblock-bg: #18181b; + --mdx-codeblock-border: #3f3f46; + --mdx-codeblock-text: #e4e4e7; + --mdx-codeblock-gutter: #a1a1aa; + --mdx-codeblock-active: #27272a; + --mdx-codeblock-selection: rgba(125, 111, 152, 0.35); + --mdx-codeblock-bracket: rgba(125, 111, 152, 0.45); + --shadow-glow: 0 0 0 1px rgba(63, 63, 70, 0.65), 0 18px 44px rgba(0, 0, 0, 0.45); } :root[data-theme='light'] { - --bg-page: #f7f7fb; - --surface-bg: rgba(255, 255, 255, 0.9); - --surface-bg-strong: rgba(255, 255, 255, 0.98); - --surface-border: rgba(161, 161, 170, 0.45); - --surface-divider: rgba(212, 212, 216, 0.9); - --text-primary: #52485c; - --text-secondary: #514e60; - --text-muted: #52525b; - --text-soft: #71717a; - --field-bg: rgba(253, 253, 253, 0.8); - --field-border: #d4d4d8; - --field-disabled-bg: rgba(248, 248, 248, 0.8); - --field-disabled-border: #d7d7d7; - --field-disabled-text: #71717a; - --field-disabled-placeholder: #a1a1aa; - --ghost-bg: rgba(255, 255, 255, 0.88); - --ghost-border: #d4d4d8; - --ghost-hover: #f4f4f5; - --ghost-disabled-bg: #f4f4f5; - --ghost-disabled-border: #e4e4e7; - --ghost-disabled-text: #a1a1aa; - --primary-disabled-bg: #e4e4e7; - --primary-disabled-text: #a1a1aa; - --table-head-bg: #f4f4f5; - --table-row-divider: #e4e4e7; - --auth-chrome-bg: rgba(255, 255, 255, 0.7); - --auth-glass-blur: 15px; - --error-border: rgba(248, 113, 113, 0.35); - --error-bg: rgba(254, 226, 226, 0.8); - --error-text: #991b1b; - --mdx-link: #7d6f98; - --mdx-link-hover: #6a5d84; - --mdx-inline-code-bg: #f4f4f5; - --mdx-inline-code-border: #d4d4d8; - --mdx-codeblock-bg: #ffffff; - --mdx-codeblock-border: #d4d4d8; - --mdx-codeblock-text: #18181b; - --mdx-codeblock-gutter: #71717a; - --mdx-codeblock-active: #f4f4f5; - --mdx-codeblock-selection: rgba(125, 111, 152, 0.22); - --mdx-codeblock-bracket: rgba(125, 111, 152, 0.32); - --shadow-glow: 0 0 0 1px rgba(212, 212, 216, 0.9), 0 18px 36px rgba(15, 23, 42, 0.08); + --bg-page: #f7f7fb; + --surface-bg: rgba(255, 255, 255, 0.9); + --surface-bg-strong: rgba(255, 255, 255, 0.98); + --surface-border: rgba(161, 161, 170, 0.45); + --surface-divider: rgba(212, 212, 216, 0.9); + --text-primary: #52485c; + --text-secondary: #514e60; + --text-muted: #52525b; + --text-soft: #71717a; + --field-bg: rgba(253, 253, 253, 0.8); + --field-border: #d4d4d8; + --field-disabled-bg: rgba(248, 248, 248, 0.8); + --field-disabled-border: #d7d7d7; + --field-disabled-text: #71717a; + --field-disabled-placeholder: #a1a1aa; + --ghost-bg: rgba(255, 255, 255, 0.88); + --ghost-border: #d4d4d8; + --ghost-hover: #f4f4f5; + --ghost-disabled-bg: #f4f4f5; + --ghost-disabled-border: #e4e4e7; + --ghost-disabled-text: #a1a1aa; + --primary-disabled-bg: #e4e4e7; + --primary-disabled-text: #a1a1aa; + --table-head-bg: #f4f4f5; + --table-row-divider: #e4e4e7; + --auth-chrome-bg: rgba(255, 255, 255, 0.7); + --auth-glass-blur: 15px; + --error-border: rgba(248, 113, 113, 0.35); + --error-bg: rgba(254, 226, 226, 0.8); + --error-text: #991b1b; + --mdx-link: #7d6f98; + --mdx-link-hover: #6a5d84; + --mdx-inline-code-bg: #f4f4f5; + --mdx-inline-code-border: #d4d4d8; + --mdx-codeblock-bg: #ffffff; + --mdx-codeblock-border: #d4d4d8; + --mdx-codeblock-text: #18181b; + --mdx-codeblock-gutter: #71717a; + --mdx-codeblock-active: #f4f4f5; + --mdx-codeblock-selection: rgba(125, 111, 152, 0.22); + --mdx-codeblock-bracket: rgba(125, 111, 152, 0.32); + --shadow-glow: 0 0 0 1px rgba(212, 212, 216, 0.9), 0 18px 36px rgba(15, 23, 42, 0.08); } diff --git a/src/styles/components.css b/src/styles/components.css index 516c827..f5cf7bd 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -1,289 +1,289 @@ .surface { - border: 1px solid var(--surface-border); - background-color: var(--surface-bg); - box-shadow: var(--shadow-glow); - @apply rounded-2xl backdrop-blur-xl; + border: 1px solid var(--surface-border); + background-color: var(--surface-bg); + box-shadow: var(--shadow-glow); + @apply rounded-2xl backdrop-blur-xl; } .field { - border: 1px solid var(--field-border); - background-color: var(--field-bg); - color: var(--text-primary); - @apply w-full rounded-xl px-3 py-2 text-sm outline-none transition focus:border-accent-400 focus:ring-2 focus:ring-accent-400/30; + border: 1px solid var(--field-border); + background-color: var(--field-bg); + color: var(--text-primary); + @apply w-full rounded-xl px-3 py-2 text-sm outline-none transition focus:border-accent-400 focus:ring-2 focus:ring-accent-400/30; } .field::placeholder { - color: var(--text-soft); + color: var(--text-soft); } .field:disabled { - border-color: var(--field-disabled-border); - background-color: var(--field-disabled-bg); - color: var(--field-disabled-text); + border-color: var(--field-disabled-border); + background-color: var(--field-disabled-bg); + color: var(--field-disabled-text); } .field:disabled::placeholder { - color: var(--field-disabled-placeholder); + color: var(--field-disabled-placeholder); } .btn-solid, .btn-outlined, .btn-noborder { - @apply inline-flex items-center justify-center rounded-xl px-4 py-2 text-sm font-semibold transition; + @apply inline-flex items-center justify-center rounded-xl px-4 py-2 text-sm font-semibold transition; } .btn-solid { - border: 1px solid transparent; + border: 1px solid transparent; } .btn-outlined { - border: 1px solid var(--ghost-border); + border: 1px solid var(--ghost-border); } .btn-noborder { - border: 1px solid transparent; + border: 1px solid transparent; } .btn-solid.btn-primary { - @apply bg-accent-500 text-white hover:bg-accent-400 disabled:opacity-100; + @apply bg-accent-500 text-white hover:bg-accent-400 disabled:opacity-100; } .btn-solid.btn-primary:disabled { - background-color: var(--primary-disabled-bg); - color: var(--primary-disabled-text); + background-color: var(--primary-disabled-bg); + color: var(--primary-disabled-text); } .btn-solid.btn-secondary { - border-color: var(--ghost-border); - background-color: var(--ghost-border); - color: var(--text-primary); + border-color: var(--ghost-border); + background-color: var(--ghost-border); + color: var(--text-primary); } .btn-solid.btn-secondary:hover { - background-color: var(--ghost-hover); + background-color: var(--ghost-hover); } .btn-solid.btn-secondary:disabled { - border-color: var(--ghost-disabled-border); - background-color: var(--ghost-disabled-bg); - color: var(--ghost-disabled-text); + border-color: var(--ghost-disabled-border); + background-color: var(--ghost-disabled-bg); + color: var(--ghost-disabled-text); } .btn-solid.btn-important { - border-color: #dc2626; - @apply bg-red-600 text-white hover:bg-red-500 disabled:opacity-100; + border-color: #dc2626; + @apply bg-red-600 text-white hover:bg-red-500 disabled:opacity-100; } .btn-solid.btn-important:disabled { - border-color: #7f1d1d; - background-color: #7f1d1d; - color: #fecaca; + border-color: #7f1d1d; + background-color: #7f1d1d; + color: #fecaca; } .btn-outlined.btn-secondary { - border-color: var(--ghost-border); - background-color: var(--ghost-bg); - color: var(--text-secondary); + border-color: var(--ghost-border); + background-color: var(--ghost-bg); + color: var(--text-secondary); } .btn-outlined.btn-secondary:hover { - background-color: var(--ghost-hover); + background-color: var(--ghost-hover); } .btn-outlined.btn-secondary:disabled { - border-color: var(--ghost-disabled-border); - background-color: var(--ghost-disabled-bg); - color: var(--ghost-disabled-text); + border-color: var(--ghost-disabled-border); + background-color: var(--ghost-disabled-bg); + color: var(--ghost-disabled-text); } .btn-outlined.btn-primary { - @apply border-accent-500 text-accent-300; - background-color: transparent; + @apply border-accent-500 text-accent-300; + background-color: transparent; } .btn-outlined.btn-primary:hover { - @apply bg-accent-500/15 text-accent-300; + @apply bg-accent-500/15 text-accent-300; } .btn-outlined.btn-primary:disabled { - @apply border-accent-500/40 text-accent-300/60; - background-color: transparent; + @apply border-accent-500/40 text-accent-300/60; + background-color: transparent; } .btn-outlined.btn-important { - @apply border-red-500 text-red-400; - background-color: transparent; + @apply border-red-500 text-red-400; + background-color: transparent; } .btn-outlined.btn-important:hover { - @apply bg-red-500/10 text-red-300; + @apply bg-red-500/10 text-red-300; } .btn-outlined.btn-important:disabled { - @apply border-red-900 text-red-900; - background-color: transparent; + @apply border-red-900 text-red-900; + background-color: transparent; } .btn-noborder.btn-secondary { - background-color: transparent; - color: var(--text-secondary); + background-color: transparent; + color: var(--text-secondary); } .btn-noborder.btn-secondary:hover { - background-color: var(--ghost-hover); + background-color: var(--ghost-hover); } .btn-noborder.btn-secondary:disabled { - background-color: transparent; - color: var(--ghost-disabled-text); + background-color: transparent; + color: var(--ghost-disabled-text); } .btn-noborder.btn-primary { - @apply text-accent-300; - background-color: transparent; + @apply text-accent-300; + background-color: transparent; } .btn-noborder.btn-primary:hover { - @apply bg-accent-500/15 text-accent-300; + @apply bg-accent-500/15 text-accent-300; } .btn-noborder.btn-primary:disabled { - @apply text-accent-300/60; - background-color: transparent; + @apply text-accent-300/60; + background-color: transparent; } .btn-noborder.btn-important { - @apply text-red-400; - background-color: transparent; + @apply text-red-400; + background-color: transparent; } .btn-noborder.btn-important:hover { - @apply bg-red-500/10 text-red-300; + @apply bg-red-500/10 text-red-300; } .btn-noborder.btn-important:disabled { - @apply text-red-900; - background-color: transparent; + @apply text-red-900; + background-color: transparent; } .ui-kicker { - color: var(--text-muted); + color: var(--text-muted); } .ui-title { - color: var(--text-primary); + color: var(--text-primary); } .ui-body-secondary { - color: var(--text-muted); + color: var(--text-muted); } .ui-code { - border: 1px solid var(--surface-divider); - background-color: var(--ghost-bg); - color: var(--text-primary); - @apply rounded-md px-1.5 py-0.5; + border: 1px solid var(--surface-divider); + background-color: var(--ghost-bg); + color: var(--text-primary); + @apply rounded-md px-1.5 py-0.5; } .ui-body-primary { - color: var(--text-secondary); + color: var(--text-secondary); } .ui-loading { - color: var(--text-muted); + color: var(--text-muted); } .ui-empty { - color: var(--text-soft); + color: var(--text-soft); } .ui-link { - color: var(--text-secondary); - @apply font-semibold transition; + color: var(--text-secondary); + @apply font-semibold transition; } .ui-link:hover { - color: var(--text-primary); + color: var(--text-primary); } .ui-label { - color: var(--text-secondary); + color: var(--text-secondary); } .ui-label-disabled { - color: var(--text-soft); + color: var(--text-soft); } .ui-error { - color: var(--error-text); + color: var(--error-text); } .chip-root { - @apply inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-semibold leading-none; + @apply inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-semibold leading-none; } .chip-solid { - border-color: var(--ghost-border); - background-color: var(--ghost-border); - color: var(--text-primary); + border-color: var(--ghost-border); + background-color: var(--ghost-border); + color: var(--text-primary); } .chip-outlined { - background-color: transparent; - border-color: var(--ghost-border); - color: var(--text-secondary); + background-color: transparent; + border-color: var(--ghost-border); + color: var(--text-secondary); } .alert-error { - border: 1px solid var(--error-border); - background-color: var(--error-bg); - color: var(--error-text); - @apply rounded-lg px-3 py-2 text-sm; + border: 1px solid var(--error-border); + background-color: var(--error-bg); + color: var(--error-text); + @apply rounded-lg px-3 py-2 text-sm; } .table-shell { - border: 1px solid var(--surface-divider); - background-color: var(--surface-bg-strong); - @apply overflow-hidden rounded-xl; + border: 1px solid var(--surface-divider); + background-color: var(--surface-bg-strong); + @apply overflow-hidden rounded-xl; } .table-scroll { - @apply overflow-x-auto; + @apply overflow-x-auto; } .table-root { - @apply min-w-full; + @apply min-w-full; } .table-head { - background-color: var(--table-head-bg); - border-bottom: 1px solid var(--surface-divider); + background-color: var(--table-head-bg); + border-bottom: 1px solid var(--surface-divider); } .table-head-cell { - color: var(--text-primary); - @apply px-4 py-3 text-left text-sm font-semibold tracking-wider; + color: var(--text-primary); + @apply px-4 py-3 text-left text-sm font-semibold tracking-wider; } .table-sort-button { - @apply inline-flex items-center gap-1.5 text-left; + @apply inline-flex items-center gap-1.5 text-left; } .table-sort-icon { - color: var(--text-muted); - @apply inline-flex items-center; + color: var(--text-muted); + @apply inline-flex items-center; } .table-body-row { - border-top: 1px solid var(--table-row-divider); + border-top: 1px solid var(--table-row-divider); } .table-cell-primary { - color: var(--text-primary); - @apply px-4 py-3 text-sm; + color: var(--text-primary); + @apply px-4 py-3 text-sm; } .table-cell-secondary { - color: var(--text-secondary); - @apply px-4 py-3 text-sm; + color: var(--text-secondary); + @apply px-4 py-3 text-sm; } diff --git a/src/types/sort.ts b/src/types/sort.ts index ccb8d1d..bc1890d 100644 --- a/src/types/sort.ts +++ b/src/types/sort.ts @@ -1,6 +1,6 @@ export type SortDirection = 'asc' | 'desc'; export type SortState = { - field: string; - direction: SortDirection; + field: string; + direction: SortDirection; }; diff --git a/tailwind-preset.cjs b/tailwind-preset.cjs index 7812c4d..8ade7ee 100644 --- a/tailwind-preset.cjs +++ b/tailwind-preset.cjs @@ -1,21 +1,21 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - theme: { - extend: { - colors: { - accent: { - 300: '#a89bbf', - 400: '#9587ad', - 500: '#7d6f98', - 600: '#6a5d84', + theme: { + extend: { + colors: { + accent: { + 300: '#a89bbf', + 400: '#9587ad', + 500: '#7d6f98', + 600: '#6a5d84', + }, + }, + fontFamily: { + sans: ['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'], + }, + boxShadow: { + glow: '0 0 0 1px rgba(63,63,70,0.65), 0 18px 44px rgba(0,0,0,0.45)', + }, }, - }, - fontFamily: { - sans: ['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'], - }, - boxShadow: { - glow: '0 0 0 1px rgba(63,63,70,0.65), 0 18px 44px rgba(0,0,0,0.45)', - }, }, - }, }; diff --git a/tailwind.build.config.cjs b/tailwind.build.config.cjs index aa8e524..00c55d3 100644 --- a/tailwind.build.config.cjs +++ b/tailwind.build.config.cjs @@ -2,13 +2,13 @@ const webUiPreset = require('./tailwind-preset.cjs'); /** @type {import('tailwindcss').Config} */ module.exports = { - presets: [webUiPreset], - content: ['./src/**/*.{ts,tsx,js,jsx}'], - corePlugins: { - preflight: false, - }, - theme: { - extend: {}, - }, - plugins: [], + presets: [webUiPreset], + content: ['./src/**/*.{ts,tsx,js,jsx}'], + corePlugins: { + preflight: false, + }, + theme: { + extend: {}, + }, + plugins: [], }; diff --git a/tailwind.storybook.config.cjs b/tailwind.storybook.config.cjs index 8e911e8..c6a4e31 100644 --- a/tailwind.storybook.config.cjs +++ b/tailwind.storybook.config.cjs @@ -2,10 +2,10 @@ const webUiPreset = require('./tailwind-preset.cjs'); /** @type {import('tailwindcss').Config} */ module.exports = { - presets: [webUiPreset], - content: ['./src/**/*.{ts,tsx,js,jsx,mdx}', './.storybook/**/*.{ts,tsx,js,jsx,mdx}'], - theme: { - extend: {}, - }, - plugins: [], + presets: [webUiPreset], + content: ['./src/**/*.{ts,tsx,js,jsx,mdx}', './.storybook/**/*.{ts,tsx,js,jsx,mdx}'], + theme: { + extend: {}, + }, + plugins: [], }; diff --git a/tsconfig.build.json b/tsconfig.build.json index 482d036..250c6b1 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,13 +1,13 @@ { - "extends": "./tsconfig.json", - "compilerOptions": { - "noEmit": false, - "declaration": true, - "emitDeclarationOnly": true, - "rootDir": "src", - "outDir": "dist", - "declarationMap": true - }, - "include": ["src"], - "exclude": ["src/**/*.stories.ts", "src/**/*.stories.tsx"] + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "emitDeclarationOnly": true, + "rootDir": "src", + "outDir": "dist", + "declarationMap": true + }, + "include": ["src"], + "exclude": ["src/**/*.stories.ts", "src/**/*.stories.tsx"] } diff --git a/tsconfig.json b/tsconfig.json index 491788e..19a415a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,17 @@ { - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "Bundler", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "jsx": "react-jsx", - "strict": true, - "skipLibCheck": true, - "resolveJsonModule": true, - "isolatedModules": true, - "allowImportingTsExtensions": false, - "noEmit": true, - "types": ["react", "react-dom"] - }, - "include": ["src"] + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "allowImportingTsExtensions": false, + "noEmit": true, + "types": ["react", "react-dom"] + }, + "include": ["src"] } diff --git a/vite.config.ts b/vite.config.ts index ba049d4..23e0795 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,19 +3,28 @@ import react from '@vitejs/plugin-react'; import { resolve } from 'node:path'; export default defineConfig({ - plugins: [react()], - build: { - lib: { - entry: { - index: resolve(__dirname, 'src/index.ts'), - 'components/MDXEditorField': resolve(__dirname, 'src/components/MDXEditorField.tsx'), - }, - name: 'PanicWebUi', - formats: ['es'], - fileName: (_format, entryName) => `${entryName}.js`, + plugins: [react()], + build: { + lib: { + entry: { + index: resolve(__dirname, 'src/index.ts'), + 'components/MDXEditorField': resolve( + __dirname, + 'src/components/MDXEditorField.tsx', + ), + }, + name: 'PanicWebUi', + formats: ['es'], + fileName: (_format, entryName) => `${entryName}.js`, + }, + rollupOptions: { + external: [ + 'react', + 'react-dom', + 'react-router-dom', + '@heroicons/react', + '@mdxeditor/editor', + ], + }, }, - rollupOptions: { - external: ['react', 'react-dom', 'react-router-dom', '@heroicons/react', '@mdxeditor/editor'], - }, - }, });