Compare commits
5 Commits
v0.1.8
...
d1266f24a3
| Author | SHA1 | Date | |
|---|---|---|---|
| d1266f24a3 | |||
| 5cc3e3646c | |||
| 29a4e8c2ee | |||
| 3ddd108186 | |||
| 836d24943e |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@panic/web-ui",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.11",
|
||||
"license": "AGPL-3.0-only",
|
||||
"description": "Core components for panic.haus web applications",
|
||||
"type": "module",
|
||||
@@ -54,7 +54,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@eslint/js": "^9",
|
||||
"@eslint/js": "^10",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@mdxeditor/editor": "^3.52.4",
|
||||
@@ -67,7 +67,7 @@
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"eslint": "^9",
|
||||
"eslint": "^10",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.1",
|
||||
"globals": "^17.3.0",
|
||||
@@ -77,7 +77,7 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.0.0",
|
||||
"storybook": "^10.2.10",
|
||||
"tailwindcss": "^3.4.16",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.6.2",
|
||||
"typescript-eslint": "^8.56.0",
|
||||
"vite": "^7.0.0",
|
||||
|
||||
@@ -39,6 +39,12 @@ const meta = {
|
||||
control: 'inline-radio',
|
||||
table: { type: { summary: "'sm' | 'md' | 'lg' | 'full'" } },
|
||||
},
|
||||
width: {
|
||||
description: 'Button width behavior.',
|
||||
options: ['sm', 'md', 'lg', 'full'],
|
||||
control: 'inline-radio',
|
||||
table: { type: { summary: "'sm' | 'md' | 'lg' | 'full'" } },
|
||||
},
|
||||
to: {
|
||||
description: 'Navigation path. When set, the component renders a `<Link>`.',
|
||||
control: 'text',
|
||||
@@ -80,6 +86,7 @@ const meta = {
|
||||
type: 'solid',
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
width: 'md',
|
||||
label: 'Save',
|
||||
},
|
||||
} satisfies Meta<typeof Button>;
|
||||
@@ -132,7 +139,7 @@ export const SizeMatrix: Story = {
|
||||
<Button {...args} size="sm" label="Small" />
|
||||
<Button {...args} size="md" label="Medium" />
|
||||
<Button {...args} size="lg" label="Large" />
|
||||
<Button {...args} size="full" label="Full width" />
|
||||
<Button {...args} size="md" width="full" label="Full width" />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ type ButtonProps = {
|
||||
type: ButtonType;
|
||||
variant?: ButtonVariant;
|
||||
size?: ComponentSize;
|
||||
width?: ComponentSize;
|
||||
to?: string;
|
||||
htmlType?: NativeButtonType;
|
||||
onClick?: MouseEventHandler<HTMLElement>;
|
||||
@@ -24,14 +25,14 @@ const SIZE_CLASS: Record<ComponentSize, string> = {
|
||||
sm: 'h-8 px-3 text-xs',
|
||||
md: 'h-10 px-4 text-sm',
|
||||
lg: 'h-12 px-5 text-base',
|
||||
full: 'h-10 w-full px-4 text-sm',
|
||||
full: 'h-10 px-4 text-sm',
|
||||
};
|
||||
|
||||
const ICON_ONLY_SIZE_CLASS: Record<ComponentSize, string> = {
|
||||
sm: 'h-8 w-8 !p-0',
|
||||
md: 'h-10 w-10 !p-0',
|
||||
lg: 'h-12 w-12 !p-0',
|
||||
full: 'h-10 w-full !p-0',
|
||||
full: 'h-10 w-10 !p-0',
|
||||
};
|
||||
|
||||
const ICON_CLASS: Record<ComponentSize, string> = {
|
||||
@@ -48,6 +49,13 @@ const ICON_ONLY_CLASS: Record<ComponentSize, string> = {
|
||||
full: 'h-5 w-5',
|
||||
};
|
||||
|
||||
const WIDTH_CLASS: Record<ComponentSize, string> = {
|
||||
sm: 'max-w-xs',
|
||||
md: '',
|
||||
lg: 'max-w-md',
|
||||
full: 'w-full max-w-none',
|
||||
};
|
||||
|
||||
const TYPE_CLASS: Record<ButtonType, string> = {
|
||||
solid: 'btn-solid',
|
||||
outlined: 'btn-outlined',
|
||||
@@ -73,6 +81,7 @@ export function Button({
|
||||
type,
|
||||
variant,
|
||||
size = 'md',
|
||||
width = 'md',
|
||||
to,
|
||||
htmlType = 'button',
|
||||
onClick,
|
||||
@@ -87,6 +96,7 @@ export function Button({
|
||||
TYPE_CLASS[type],
|
||||
VARIANT_CLASS[resolvedVariant],
|
||||
isIconOnly ? ICON_ONLY_SIZE_CLASS[size] : SIZE_CLASS[size],
|
||||
WIDTH_CLASS[width],
|
||||
Icon && label ? 'gap-1.5' : '',
|
||||
disabled ? 'pointer-events-none cursor-not-allowed opacity-45 saturate-50' : '',
|
||||
className,
|
||||
|
||||
240
src/components/DatePicker.stories.tsx
Normal file
240
src/components/DatePicker.stories.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
import { useState } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { CalendarDaysIcon } from '@heroicons/react/24/solid';
|
||||
import { DatePicker } from './DatePicker';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/DatePicker',
|
||||
component: DatePicker,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Date selection field with InputField-compatible API, supporting date/time/datetime-local values, size/layout variants, and validation state.',
|
||||
},
|
||||
},
|
||||
},
|
||||
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 date input type.',
|
||||
options: ['date', 'datetime-local', 'time'],
|
||||
control: 'inline-radio',
|
||||
table: { type: { summary: "'date' | 'datetime-local' | 'time'" } },
|
||||
},
|
||||
size: {
|
||||
description: 'Input size.',
|
||||
options: ['sm', 'md', 'lg', 'full'],
|
||||
control: 'inline-radio',
|
||||
table: { type: { summary: "'sm' | 'md' | 'lg' | 'full'" } },
|
||||
},
|
||||
width: {
|
||||
description: 'Input width constraint.',
|
||||
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.',
|
||||
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 `<input>` element.',
|
||||
control: 'text',
|
||||
table: { type: { summary: 'string' } },
|
||||
},
|
||||
onChange: {
|
||||
description: 'Change handler callback.',
|
||||
action: 'changed',
|
||||
table: { type: { summary: 'ChangeEventHandler<HTMLInputElement>' } },
|
||||
},
|
||||
onBlur: {
|
||||
description: 'Blur handler callback.',
|
||||
control: false,
|
||||
table: { type: { summary: 'FocusEventHandler<HTMLInputElement>' } },
|
||||
},
|
||||
inputRef: {
|
||||
description: 'Ref forwarded to the native `<input>` element.',
|
||||
control: false,
|
||||
table: { type: { summary: 'Ref<HTMLInputElement>' } },
|
||||
},
|
||||
},
|
||||
args: {
|
||||
label: 'Schedule at',
|
||||
type: 'datetime-local',
|
||||
value: '',
|
||||
size: 'md',
|
||||
width: 'md',
|
||||
layout: 'stacked',
|
||||
},
|
||||
} satisfies Meta<typeof DatePicker>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const DateOnly: Story = {
|
||||
args: {
|
||||
type: 'date',
|
||||
label: 'Publish date',
|
||||
},
|
||||
render: (args) => {
|
||||
const [value, setValue] = useState('2031-05-20');
|
||||
return (
|
||||
<DatePicker
|
||||
{...args}
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
setValue(event.target.value);
|
||||
args.onChange?.(event);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const DateTime: Story = {
|
||||
args: {
|
||||
type: 'datetime-local',
|
||||
label: 'Schedule at',
|
||||
},
|
||||
render: (args) => {
|
||||
const [value, setValue] = useState('2031-05-20T14:30');
|
||||
return (
|
||||
<DatePicker
|
||||
{...args}
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
setValue(event.target.value);
|
||||
args.onChange?.(event);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const TimeOnlyInline: Story = {
|
||||
args: {
|
||||
type: 'time',
|
||||
label: 'Start time',
|
||||
layout: 'inline',
|
||||
size: 'sm',
|
||||
rightIcon: <CalendarDaysIcon className="h-4 w-4 ui-body-secondary" />,
|
||||
},
|
||||
render: (args) => {
|
||||
const [value, setValue] = useState('09:00');
|
||||
return (
|
||||
<DatePicker
|
||||
{...args}
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
setValue(event.target.value);
|
||||
args.onChange?.(event);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
type: 'datetime-local',
|
||||
label: 'Schedule at',
|
||||
value: '',
|
||||
error: 'Pick a valid future date and time',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
type: 'datetime-local',
|
||||
label: 'Published at',
|
||||
value: '2031-05-20T14:30',
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const SizeMatrix: Story = {
|
||||
args: {
|
||||
type: 'datetime-local',
|
||||
label: 'Schedule at',
|
||||
},
|
||||
render: (args) => {
|
||||
const [value, setValue] = useState('2031-05-20T14:30');
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<DatePicker
|
||||
{...args}
|
||||
value={value}
|
||||
size="sm"
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
/>
|
||||
<DatePicker
|
||||
{...args}
|
||||
value={value}
|
||||
size="md"
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
/>
|
||||
<DatePicker
|
||||
{...args}
|
||||
value={value}
|
||||
size="lg"
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
/>
|
||||
<DatePicker
|
||||
{...args}
|
||||
value={value}
|
||||
size="full"
|
||||
width="full"
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
97
src/components/DatePicker.tsx
Normal file
97
src/components/DatePicker.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { ChangeEventHandler, FocusEventHandler, ReactNode, Ref } from 'react';
|
||||
import type { ComponentSize } from './types';
|
||||
|
||||
type DatePickerKind = 'date' | 'datetime-local' | 'time';
|
||||
type Layout = 'stacked' | 'inline';
|
||||
|
||||
export type DatePickerProps = {
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
type: DatePickerKind;
|
||||
size?: ComponentSize;
|
||||
width?: ComponentSize;
|
||||
layout?: Layout;
|
||||
value: string;
|
||||
name?: string;
|
||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
error?: string;
|
||||
rightIcon?: ReactNode;
|
||||
className?: string;
|
||||
inputClassName?: string;
|
||||
};
|
||||
|
||||
export function DatePicker({
|
||||
label,
|
||||
placeholder = '',
|
||||
type,
|
||||
size = 'md',
|
||||
width = 'md',
|
||||
layout = 'stacked',
|
||||
value,
|
||||
name,
|
||||
onChange,
|
||||
onBlur,
|
||||
inputRef,
|
||||
disabled = false,
|
||||
required = false,
|
||||
error,
|
||||
rightIcon,
|
||||
className = '',
|
||||
inputClassName = '',
|
||||
}: Readonly<DatePickerProps>) {
|
||||
const containerWidthClass = {
|
||||
sm: 'max-w-xs',
|
||||
md: 'max-w-sm',
|
||||
lg: 'max-w-md',
|
||||
full: 'max-w-none',
|
||||
}[width];
|
||||
|
||||
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 hasTrailingIcon = Boolean(rightIcon);
|
||||
const inputWrapperClass = layout === 'inline' ? 'relative' : 'relative mt-1';
|
||||
|
||||
return (
|
||||
<label
|
||||
className={`${wrapperClass} text-sm font-medium ${disabled ? 'ui-label-disabled' : 'ui-label'} ${containerWidthClass} ${className}`.trim()}
|
||||
>
|
||||
{label ? <span className={labelClass}>{label}</span> : null}
|
||||
<div className={inputWrapperClass}>
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
ref={inputRef}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
className={`field w-full ${hasTrailingIcon ? 'pr-10' : ''} ${inputSizeClass} ${error ? 'border-red-400/70 focus:border-red-400 focus:ring-red-400/30' : ''} ${inputClassName}`.trim()}
|
||||
/>
|
||||
{rightIcon ? (
|
||||
<span className="pointer-events-none absolute inset-y-0 right-2 inline-flex items-center justify-center px-1">
|
||||
{rightIcon}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{error ? (
|
||||
<span className="mt-1 block text-xs" style={{ color: 'var(--error-text)' }}>
|
||||
{error}
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -42,6 +42,12 @@ const meta = {
|
||||
control: 'inline-radio',
|
||||
table: { type: { summary: "'sm' | 'md' | 'lg' | 'full'" } },
|
||||
},
|
||||
width: {
|
||||
description: 'Control width constraint.',
|
||||
options: ['sm', 'md', 'lg', 'full'],
|
||||
control: 'inline-radio',
|
||||
table: { type: { summary: "'sm' | 'md' | 'lg' | 'full'" } },
|
||||
},
|
||||
layout: {
|
||||
description: 'Label/control layout mode.',
|
||||
options: ['stacked', 'inline'],
|
||||
@@ -84,6 +90,7 @@ const meta = {
|
||||
value: 'draft',
|
||||
choices,
|
||||
size: 'md',
|
||||
width: 'md',
|
||||
layout: 'stacked',
|
||||
},
|
||||
} satisfies Meta<typeof Dropdown>;
|
||||
@@ -147,7 +154,14 @@ export const SizeMatrix: Story = {
|
||||
<Dropdown {...args} value={value} size="sm" label="Small" onChange={setValue} />
|
||||
<Dropdown {...args} value={value} size="md" label="Medium" onChange={setValue} />
|
||||
<Dropdown {...args} value={value} size="lg" label="Large" onChange={setValue} />
|
||||
<Dropdown {...args} value={value} size="full" label="Full" onChange={setValue} />
|
||||
<Dropdown
|
||||
{...args}
|
||||
value={value}
|
||||
size="full"
|
||||
width="full"
|
||||
label="Full"
|
||||
onChange={setValue}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ type DropdownProps = {
|
||||
value: string;
|
||||
choices: DropdownChoice[];
|
||||
size?: ComponentSize;
|
||||
width?: ComponentSize;
|
||||
layout?: DropdownLayout;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
@@ -28,6 +29,7 @@ export function Dropdown({
|
||||
value,
|
||||
choices,
|
||||
size = 'md',
|
||||
width = 'md',
|
||||
layout = 'stacked',
|
||||
disabled = false,
|
||||
required = false,
|
||||
@@ -36,12 +38,12 @@ export function Dropdown({
|
||||
className = '',
|
||||
selectClassName = '',
|
||||
}: Readonly<DropdownProps>) {
|
||||
const containerSizeClass = {
|
||||
const containerWidthClass = {
|
||||
sm: 'max-w-xs',
|
||||
md: 'max-w-sm',
|
||||
lg: 'max-w-md',
|
||||
full: 'max-w-none',
|
||||
}[size];
|
||||
}[width];
|
||||
|
||||
const selectSizeClass = {
|
||||
sm: 'h-8 !text-xs',
|
||||
@@ -62,7 +64,7 @@ export function Dropdown({
|
||||
|
||||
return (
|
||||
<label
|
||||
className={`${wrapperClass} text-sm font-medium ${disabled ? 'ui-label-disabled' : 'ui-label'} ${containerSizeClass} ${className}`.trim()}
|
||||
className={`${wrapperClass} text-sm font-medium ${disabled ? 'ui-label-disabled' : 'ui-label'} ${containerWidthClass} ${className}`.trim()}
|
||||
>
|
||||
{label ? <span className={labelClass}>{label}</span> : null}
|
||||
<div className={selectWrapperClass}>
|
||||
|
||||
@@ -80,6 +80,7 @@ export const WithActions: Story = {
|
||||
value={title}
|
||||
onChange={(event) => setTitle(event.target.value)}
|
||||
size="full"
|
||||
width="full"
|
||||
/>
|
||||
</div>
|
||||
<Dropdown
|
||||
|
||||
@@ -38,6 +38,12 @@ const meta = {
|
||||
control: 'inline-radio',
|
||||
table: { type: { summary: "'sm' | 'md' | 'lg' | 'full'" } },
|
||||
},
|
||||
width: {
|
||||
description: 'Input width constraint.',
|
||||
options: ['sm', 'md', 'lg', 'full'],
|
||||
control: 'inline-radio',
|
||||
table: { type: { summary: "'sm' | 'md' | 'lg' | 'full'" } },
|
||||
},
|
||||
layout: {
|
||||
description: 'Label/input layout mode.',
|
||||
options: ['stacked', 'inline'],
|
||||
@@ -107,6 +113,7 @@ const meta = {
|
||||
placeholder: 'name@example.com',
|
||||
value: '',
|
||||
size: 'md',
|
||||
width: 'md',
|
||||
layout: 'stacked',
|
||||
},
|
||||
} satisfies Meta<typeof InputField>;
|
||||
@@ -229,6 +236,7 @@ export const SizeMatrix: Story = {
|
||||
{...args}
|
||||
value={value}
|
||||
size="full"
|
||||
width="full"
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ type InputFieldProps = {
|
||||
placeholder?: string;
|
||||
type: InputKind;
|
||||
size?: ComponentSize;
|
||||
width?: ComponentSize;
|
||||
layout?: Layout;
|
||||
value: string;
|
||||
name?: string;
|
||||
@@ -31,6 +32,7 @@ export function InputField({
|
||||
placeholder = '',
|
||||
type,
|
||||
size = 'md',
|
||||
width = 'md',
|
||||
layout = 'stacked',
|
||||
value,
|
||||
name,
|
||||
@@ -45,12 +47,12 @@ export function InputField({
|
||||
inputClassName = '',
|
||||
}: Readonly<InputFieldProps>) {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const containerSizeClass = {
|
||||
const containerWidthClass = {
|
||||
sm: 'max-w-xs',
|
||||
md: 'max-w-sm',
|
||||
lg: 'max-w-md',
|
||||
full: 'max-w-none',
|
||||
}[size];
|
||||
}[width];
|
||||
|
||||
const inputSizeClass = {
|
||||
sm: 'h-8 !text-xs',
|
||||
@@ -69,7 +71,7 @@ export function InputField({
|
||||
|
||||
return (
|
||||
<label
|
||||
className={`${wrapperClass} text-sm font-medium ${disabled ? 'ui-label-disabled' : 'ui-label'} ${containerSizeClass} ${className}`.trim()}
|
||||
className={`${wrapperClass} text-sm font-medium ${disabled ? 'ui-label-disabled' : 'ui-label'} ${containerWidthClass} ${className}`.trim()}
|
||||
>
|
||||
{label ? <span className={labelClass}>{label}</span> : null}
|
||||
<div className={inputWrapperClass}>
|
||||
|
||||
@@ -28,7 +28,7 @@ export function SidebarNavItem({
|
||||
onClick={onClick}
|
||||
className={({ isActive }) =>
|
||||
`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'
|
||||
isActive ? 'ui-accent-active' : 'ui-body-secondary hover:bg-zinc-500/15'
|
||||
}`
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { Button } from './components/Button';
|
||||
export { Chip } from './components/Chip';
|
||||
export { DatePicker } from './components/DatePicker';
|
||||
export { Dropdown } from './components/Dropdown';
|
||||
export { Form } from './components/Form';
|
||||
export { InputField } from './components/InputField';
|
||||
@@ -8,5 +9,6 @@ export { SidebarNavItem } from './components/SidebarNavItem';
|
||||
export { Table } from './components/Table';
|
||||
|
||||
export type { TableHeader } from './components/Table';
|
||||
export type { DatePickerProps } from './components/DatePicker';
|
||||
export type { ComponentSize } from './components/types';
|
||||
export type { SortDirection, SortState } from './types/sort';
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
/* Consumer projects can override these accent tokens after importing @panic/web-ui styles. */
|
||||
--accent-300: 168 155 191;
|
||||
--accent-400: 149 135 173;
|
||||
--accent-500: 125 111 152;
|
||||
--accent-600: 106 93 132;
|
||||
--accent-contrast: 255 255 255;
|
||||
--bg-page: #16121a;
|
||||
--surface-bg: rgba(24, 24, 27, 0.45);
|
||||
--surface-bg-strong: rgba(24, 24, 27, 0.62);
|
||||
@@ -33,8 +39,8 @@
|
||||
--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-link: rgb(var(--accent-500));
|
||||
--mdx-link-hover: rgb(var(--accent-400));
|
||||
--mdx-inline-code-bg: #27272a;
|
||||
--mdx-inline-code-border: #3f3f46;
|
||||
--mdx-codeblock-bg: #18181b;
|
||||
@@ -42,8 +48,8 @@
|
||||
--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);
|
||||
--mdx-codeblock-selection: rgb(var(--accent-500) / 0.35);
|
||||
--mdx-codeblock-bracket: rgb(var(--accent-500) / 0.45);
|
||||
--shadow-glow: 0 0 0 1px rgba(63, 63, 70, 0.65), 0 18px 44px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
@@ -78,8 +84,8 @@
|
||||
--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-link: rgb(var(--accent-500));
|
||||
--mdx-link-hover: rgb(var(--accent-600));
|
||||
--mdx-inline-code-bg: #f4f4f5;
|
||||
--mdx-inline-code-border: #d4d4d8;
|
||||
--mdx-codeblock-bg: #ffffff;
|
||||
@@ -87,7 +93,7 @@
|
||||
--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);
|
||||
--mdx-codeblock-selection: rgb(var(--accent-500) / 0.22);
|
||||
--mdx-codeblock-bracket: rgb(var(--accent-500) / 0.32);
|
||||
--shadow-glow: 0 0 0 1px rgba(212, 212, 216, 0.9), 0 18px 36px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-webkit-text-fill-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;
|
||||
@apply w-full rounded-xl px-3 py-2 text-sm outline-none transition;
|
||||
}
|
||||
|
||||
.field:focus {
|
||||
border-color: rgb(var(--accent-400));
|
||||
box-shadow: 0 0 0 2px rgb(var(--accent-400) / 0.3);
|
||||
}
|
||||
|
||||
.field::placeholder {
|
||||
@@ -55,7 +60,13 @@
|
||||
}
|
||||
|
||||
.btn-solid.btn-primary {
|
||||
@apply bg-accent-500 text-white hover:bg-accent-400 disabled:opacity-100;
|
||||
background-color: rgb(var(--accent-500));
|
||||
color: rgb(var(--accent-contrast));
|
||||
@apply disabled:opacity-100;
|
||||
}
|
||||
|
||||
.btn-solid.btn-primary:hover {
|
||||
background-color: rgb(var(--accent-400));
|
||||
}
|
||||
|
||||
.btn-solid.btn-primary:disabled {
|
||||
@@ -107,16 +118,19 @@
|
||||
}
|
||||
|
||||
.btn-outlined.btn-primary {
|
||||
@apply border-accent-500 text-accent-300;
|
||||
border-color: rgb(var(--accent-500));
|
||||
color: rgb(var(--accent-300));
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.btn-outlined.btn-primary:hover {
|
||||
@apply bg-accent-500/15 text-accent-300;
|
||||
background-color: rgb(var(--accent-500) / 0.15);
|
||||
color: rgb(var(--accent-300));
|
||||
}
|
||||
|
||||
.btn-outlined.btn-primary:disabled {
|
||||
@apply border-accent-500/40 text-accent-300/60;
|
||||
border-color: rgb(var(--accent-500) / 0.4);
|
||||
color: rgb(var(--accent-300) / 0.6);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@@ -149,16 +163,17 @@
|
||||
}
|
||||
|
||||
.btn-noborder.btn-primary {
|
||||
@apply text-accent-300;
|
||||
color: rgb(var(--accent-300));
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.btn-noborder.btn-primary:hover {
|
||||
@apply bg-accent-500/15 text-accent-300;
|
||||
background-color: rgb(var(--accent-500) / 0.15);
|
||||
color: rgb(var(--accent-300));
|
||||
}
|
||||
|
||||
.btn-noborder.btn-primary:disabled {
|
||||
@apply text-accent-300/60;
|
||||
color: rgb(var(--accent-300) / 0.6);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@@ -228,6 +243,15 @@
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
.ui-accent-active {
|
||||
background-color: rgb(var(--accent-500));
|
||||
color: rgb(var(--accent-contrast));
|
||||
}
|
||||
|
||||
.ui-accent-active:hover {
|
||||
background-color: rgb(var(--accent-400));
|
||||
}
|
||||
|
||||
.chip-root {
|
||||
@apply inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-semibold leading-none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user