diff --git a/apps/demo/src/App.scss b/apps/demo/src/App.scss
index e92e76c..e9abb4f 100644
--- a/apps/demo/src/App.scss
+++ b/apps/demo/src/App.scss
@@ -33,6 +33,13 @@
flex-wrap: wrap;
}
+ &__button-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(140px, max-content));
+ gap: 1rem;
+ align-items: center;
+ }
+
&__switches,
&__checkboxes {
display: flex;
@@ -87,4 +94,43 @@
margin: 0;
}
}
+
+ &__select-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 2rem;
+ }
+
+ &__select-column {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ &__select-item {
+ max-width: 220px;
+ }
+}
+
+.is-demo-hover.mdk-button--variant-primary {
+ background: linear-gradient(
+ 0deg,
+ var(--mdk-button-primary-bg-hover-overlay) 0%,
+ var(--mdk-button-primary-bg-hover-overlay) 100%
+ ),
+ var(--mdk-button-primary-bg);
+ color: var(--mdk-button-primary-text-hover);
+}
+
+.is-demo-hover.mdk-button--variant-secondary {
+ border-color: var(--mdk-button-secondary-border-hover);
+}
+
+.is-demo-hover.mdk-button--variant-danger {
+ opacity: 0.9;
+}
+
+.is-demo-hover.mdk-button--variant-outline {
+ background: hsl(var(--accent));
+ color: hsl(var(--accent-foreground));
}
diff --git a/apps/demo/src/App.tsx b/apps/demo/src/App.tsx
index a94faa2..ebf4f07 100644
--- a/apps/demo/src/App.tsx
+++ b/apps/demo/src/App.tsx
@@ -24,13 +24,36 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
+ Dropdown,
Label,
+ Select,
Switch,
} from '@mining-sdk/core'
import './App.scss'
function App(): JSX.Element {
+ const selectOptions = [
+ { value: 'item-1', label: 'Item 1' },
+ { value: 'item-2', label: 'Item 2' },
+ { value: 'item-3', label: 'Item 3' },
+ { value: 'item-4', label: 'Item 4' },
+ ]
+
+ const dropdownItems = [
+ {
+ key: 'group-1',
+ type: 'group' as const,
+ label: 'Items',
+ children: [
+ { key: 'item-1', label: 'Item 1' },
+ { key: 'item-2', label: 'Item 2' },
+ { key: 'item-3', label: 'Item 3' },
+ { key: 'item-4', label: 'Item 4' },
+ ],
+ },
+ ]
+
return (
@mining-sdk/core Component Demo
@@ -39,18 +62,87 @@ function App(): JSX.Element {
{/* Buttons */}
Buttons
-
-
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+ {/* Select & Dropdown */}
+
+ Select & Dropdown
+
+
+
States
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Dropdown
+
+
+
+
+
+
+
@@ -73,7 +165,7 @@ function App(): JSX.Element {
-
+
@@ -84,7 +176,7 @@ function App(): JSX.Element {
Alert Dialog
-
+
diff --git a/packages/core/USAGE.md b/packages/core/USAGE.md
index d23679d..4c0ac30 100644
--- a/packages/core/USAGE.md
+++ b/packages/core/USAGE.md
@@ -59,8 +59,8 @@ function MyComponent() {
import { Button } from '@mining-sdk/core'
// Variants
-
-
+
+
@@ -68,7 +68,7 @@ import { Button } from '@mining-sdk/core'
// Sizes
-
+
```
@@ -169,7 +169,7 @@ function AlertDialogDemo() {
return (
-
+
diff --git a/packages/core/src/components/button/index.tsx b/packages/core/src/components/button/index.tsx
index 74eab75..e6ff8e0 100644
--- a/packages/core/src/components/button/index.tsx
+++ b/packages/core/src/components/button/index.tsx
@@ -2,29 +2,102 @@ import * as React from 'react'
import { cn } from '../../utils'
-export type ButtonVariant = 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
-export type ButtonSize = 'default' | 'sm' | 'lg' | 'icon'
-
+export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'outline' | 'ghost' | 'link'
+export type ButtonSize = 'sm' | 'md' | 'lg' | 'icon'
+export type ButtonIconPosition = 'left' | 'right'
+export type ButtonAntdSize = 'small' | 'middle' | 'large'
export type ButtonProps = {
variant?: ButtonVariant
- size?: ButtonSize
+ size?: ButtonSize | ButtonAntdSize
+ loading?: boolean
+ icon?: React.ReactNode
+ iconPosition?: ButtonIconPosition
+ fullWidth?: boolean
+ block?: boolean
} & React.ButtonHTMLAttributes
+const sizeMap: Record = {
+ sm: 'sm',
+ small: 'sm',
+ md: 'md',
+ middle: 'md',
+ lg: 'lg',
+ large: 'lg',
+ icon: 'icon',
+}
+
+function sizeToSize(size?: ButtonSize | ButtonAntdSize): ButtonSize {
+ return size ? sizeMap[size] : 'md'
+}
+
/**
* Button component with multiple variants and sizes
*
* @example
* ```tsx
- *
+ *
*
*
* ```
*/
const Button = React.forwardRef(
- ({ className, variant = 'default', size = 'default', ...props }, ref) => {
- const classes = cn('mdk-button', `mdk-button--${variant}`, `mdk-button--${size}`, className)
+ (
+ {
+ className,
+ variant,
+ size = 'md',
+ loading,
+ icon,
+ iconPosition = 'left',
+ fullWidth,
+ block,
+ disabled,
+ type: nativeType,
+ children,
+ ...props
+ },
+ ref,
+ ) => {
+ const resolvedVariant = variant ?? 'secondary'
+ const resolvedSize = sizeToSize(size)
+ const resolvedHtmlType = nativeType ?? 'button'
+ const isIconOnly = Boolean(icon) && !children
+
+ const classes = cn(
+ 'mdk-button',
+ `mdk-button--variant-${resolvedVariant}`,
+ `mdk-button--size-${resolvedSize}`,
+ {
+ 'mdk-button--full-width': fullWidth || block,
+ 'mdk-button--loading': loading,
+ 'mdk-button--icon-only': isIconOnly,
+ },
+ className,
+ )
- return
+ return (
+
+ )
},
)
Button.displayName = 'Button'
diff --git a/packages/core/src/components/button/styles.scss b/packages/core/src/components/button/styles.scss
index 1c45cec..d237d62 100644
--- a/packages/core/src/components/button/styles.scss
+++ b/packages/core/src/components/button/styles.scss
@@ -6,15 +6,21 @@
display: inline-flex;
align-items: center;
justify-content: center;
- border-radius: var(--radius);
- font-size: 0.875rem;
- font-weight: 500;
+ border-radius: var(--mdk-radius);
+ font-size: var(--mdk-font-size);
+ line-height: var(--mdk-line-height);
+ font-weight: var(--mdk-font-weight);
+ gap: 0.5rem;
+ border: 1px solid transparent;
+ cursor: pointer;
transition: colors 0.2s;
+ box-shadow: var(--mdk-shadow);
&:focus-visible {
outline: none;
- ring: 2px solid hsl(var(--ring));
- ring-offset: 2px;
+ box-shadow: 0 0 0 2px hsl(var(--ring));
+ outline: 2px solid transparent;
+ outline-offset: 2px;
}
&:disabled {
@@ -23,28 +29,37 @@
}
// Variants
- &--default {
- background: hsl(var(--primary));
- color: hsl(var(--primary-foreground));
+ &--variant-primary {
+ background: var(--mdk-button-primary-bg);
+ color: var(--mdk-button-primary-text);
&:hover {
- background: hsl(var(--primary) / 0.9);
+ background: linear-gradient(
+ 0deg,
+ var(--mdk-button-primary-bg-hover-overlay) 0%,
+ var(--mdk-button-primary-bg-hover-overlay) 100%
+ ),
+ var(--mdk-button-primary-bg);
+ color: var(--mdk-button-primary-text-hover);
}
}
- &--destructive {
- background: hsl(var(--destructive));
- color: hsl(var(--destructive-foreground));
+ &--variant-danger {
+ background: var(--mdk-button-danger-bg);
+ color: var(--mdk-button-danger-text);
+ border-color: var(--mdk-button-danger-border);
+ box-shadow: none;
&:hover {
- background: hsl(var(--destructive) / 0.9);
+ opacity: 0.9;
}
}
- &--outline {
+ &--variant-outline {
border: 1px solid hsl(var(--input));
background: hsl(var(--background));
color: hsl(var(--foreground));
+ box-shadow: none;
&:hover {
background: hsl(var(--accent));
@@ -52,18 +67,21 @@
}
}
- &--secondary {
- background: hsl(var(--secondary));
- color: hsl(var(--secondary-foreground));
+ &--variant-secondary {
+ background: var(--mdk-button-secondary-bg);
+ color: var(--mdk-button-secondary-text);
+ border-color: var(--mdk-button-secondary-border);
+ box-shadow: none;
&:hover {
- background: hsl(var(--secondary) / 0.8);
+ border-color: var(--mdk-button-secondary-border-hover);
}
}
- &--ghost {
+ &--variant-ghost {
background: transparent;
color: hsl(var(--foreground));
+ box-shadow: none;
&:hover {
background: hsl(var(--accent));
@@ -71,11 +89,12 @@
}
}
- &--link {
+ &--variant-link {
background: transparent;
color: hsl(217 91% 60%); // Bright blue for visibility
text-decoration: underline;
text-underline-offset: 4px;
+ box-shadow: none;
&:hover {
text-decoration: underline;
@@ -84,24 +103,60 @@
}
// Sizes
- &--sm {
+ &--size-sm {
height: 2.25rem;
padding: 0 0.75rem;
border-radius: calc(var(--radius) - 2px);
}
- &--default {
+ &--size-md {
height: 2.5rem;
- padding: 0.5rem 1rem;
+ padding: 0.5625rem 3.5rem;
+ min-width: 8.75rem;
}
- &--lg {
+ &--size-lg {
height: 2.75rem;
padding: 0.5rem 2rem;
}
- &--icon {
+ &--size-icon {
height: 2.5rem;
width: 2.5rem;
+ padding: 0;
+ }
+}
+
+.mdk-button--full-width {
+ width: 100%;
+}
+
+.mdk-button--icon-only {
+ padding: 0;
+ aspect-ratio: 1 / 1;
+}
+
+.mdk-button__spinner {
+ width: 1rem;
+ height: 1rem;
+ border-radius: 9999px;
+ border: 2px solid hsl(var(--foreground) / 0.3);
+ border-top-color: hsl(var(--foreground));
+ animation: mdk-button-spin 0.7s linear infinite;
+}
+
+.mdk-button__icon {
+ display: inline-flex;
+ align-items: center;
+}
+
+.mdk-button__label {
+ display: inline-flex;
+ align-items: center;
+}
+
+@keyframes mdk-button-spin {
+ to {
+ transform: rotate(360deg);
}
}
diff --git a/packages/core/src/components/dropdown-menu/index.tsx b/packages/core/src/components/dropdown-menu/index.tsx
index 4f3d925..e0713a3 100644
--- a/packages/core/src/components/dropdown-menu/index.tsx
+++ b/packages/core/src/components/dropdown-menu/index.tsx
@@ -1 +1,228 @@
+import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
+import * as React from 'react'
+
+import { cn } from '../../utils'
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+const DropdownMenuSubTrigger = DropdownMenuPrimitive.SubTrigger
+const DropdownMenuSubContent = DropdownMenuPrimitive.SubContent
+const DropdownMenuCheckboxItem = DropdownMenuPrimitive.CheckboxItem
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+const DropdownMenuRadioItem = DropdownMenuPrimitive.RadioItem
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuArrow = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuArrow.displayName = DropdownMenuPrimitive.Arrow.displayName
+
+type DropdownMenuItemDefinition = {
+ key: string
+ label?: React.ReactNode
+ icon?: React.ReactNode
+ disabled?: boolean
+ danger?: boolean
+ type?: 'group' | 'divider'
+ children?: DropdownMenuItemDefinition[]
+ onClick?: (info: { key: string }) => void
+}
+
+type DropdownMenuProps = {
+ menu?: {
+ items?: DropdownMenuItemDefinition[]
+ onClick?: (info: { key: string }) => void
+ }
+ popupRender?: () => React.ReactNode
+ trigger?: Array<'click' | 'hover' | 'contextMenu'>
+ placement?: string
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+ disabled?: boolean
+ overlayClassName?: string
+ className?: string
+ arrow?: boolean
+ children: React.ReactNode
+}
+
+function mapPlacement(placement?: string): {
+ side: 'top' | 'bottom' | 'left' | 'right'
+ align: 'start' | 'center' | 'end'
+} {
+ switch (placement) {
+ case 'bottomLeft':
+ return { side: 'bottom', align: 'start' }
+ case 'bottomRight':
+ return { side: 'bottom', align: 'end' }
+ case 'topLeft':
+ return { side: 'top', align: 'start' }
+ case 'topRight':
+ return { side: 'top', align: 'end' }
+ case 'leftTop':
+ return { side: 'left', align: 'start' }
+ case 'leftBottom':
+ return { side: 'left', align: 'end' }
+ case 'rightTop':
+ return { side: 'right', align: 'start' }
+ case 'rightBottom':
+ return { side: 'right', align: 'end' }
+ default:
+ return { side: 'bottom', align: 'start' }
+ }
+}
+
+function renderMenuItems(
+ items: DropdownMenuItemDefinition[] = [],
+ menuOnClick?: (info: { key: string }) => void,
+): React.ReactNode {
+ return items.map((item) => {
+ if (item.type === 'divider') {
+ return
+ }
+
+ if (item.type === 'group' || item.children?.length) {
+ return (
+
+ {item.label && {item.label}}
+ {renderMenuItems(item.children ?? [], menuOnClick)}
+
+ )
+ }
+
+ return (
+ {
+ item.onClick?.({ key: item.key })
+ menuOnClick?.({ key: item.key })
+ }}
+ className={cn(item.danger ? 'mdk-dropdown-menu__item--danger' : null)}
+ >
+ {item.icon && {item.icon}}
+ {item.label}
+
+ )
+ })
+}
+
+function Dropdown({
+ menu,
+ popupRender,
+ trigger,
+ placement,
+ open,
+ onOpenChange,
+ disabled,
+ overlayClassName,
+ className,
+ arrow,
+ children,
+}: DropdownMenuProps): JSX.Element {
+ const { side, align } = mapPlacement(placement)
+ const hasMenuItems = Boolean(menu?.items && menu.items.length)
+ const content = popupRender
+ ? popupRender()
+ : hasMenuItems
+ ? renderMenuItems(menu?.items, menu?.onClick)
+ : null
+
+ return (
+
+
+ {children}
+
+ {content && (
+
+ {content}
+ {arrow && }
+
+ )}
+
+ )
+}
+
+export {
+ Dropdown,
+ DropdownMenu,
+ DropdownMenuArrow,
+ DropdownMenuCheckboxItem,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuPortal,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger,
+}
export * from '@radix-ui/react-dropdown-menu'
diff --git a/packages/core/src/components/dropdown-menu/styles.scss b/packages/core/src/components/dropdown-menu/styles.scss
new file mode 100644
index 0000000..5443738
--- /dev/null
+++ b/packages/core/src/components/dropdown-menu/styles.scss
@@ -0,0 +1,81 @@
+/**
+ * Dropdown Menu Component Styles
+ */
+
+.mdk-dropdown-menu__content {
+ min-width: 12rem;
+ background: var(--mdk-dropdown-bg);
+ color: var(--mdk-dropdown-item-text);
+ border: 1px solid var(--mdk-dropdown-border);
+ border-radius: var(--mdk-input-radius);
+ box-shadow: var(--mdk-dropdown-shadow);
+ padding: 0.25rem;
+ z-index: 50;
+ max-height: 14rem;
+ overflow-y: auto;
+}
+
+.mdk-dropdown-menu__item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 0.75rem;
+ border-radius: calc(var(--mdk-input-radius) + 2px);
+ cursor: pointer;
+ font-size: 0.875rem;
+ color: var(--mdk-dropdown-item-text);
+
+ &:not(:last-child) {
+ border-bottom: 1px solid var(--mdk-dropdown-divider);
+ }
+
+ &[data-disabled] {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ &[data-highlighted] {
+ background: var(--mdk-dropdown-item-hover-bg);
+ color: var(--mdk-dropdown-item-hover-text);
+ outline: none;
+ }
+
+ &[data-state='checked'] {
+ background: var(--mdk-dropdown-item-selected-bg);
+ color: var(--mdk-dropdown-item-selected-text);
+ }
+}
+
+.mdk-dropdown-menu__item--danger {
+ color: var(--mdk-color-error);
+}
+
+.mdk-dropdown-menu__item-icon {
+ display: inline-flex;
+ align-items: center;
+}
+
+.mdk-dropdown-menu__item-label {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+.mdk-dropdown-menu__label {
+ padding: 0.5rem 0.75rem 0.25rem;
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: var(--mdk-input-placeholder);
+}
+
+.mdk-dropdown-menu__separator {
+ height: 1px;
+ margin: 0.25rem 0;
+ background: var(--mdk-dropdown-divider);
+}
+
+.mdk-dropdown-menu__arrow {
+ fill: var(--mdk-dropdown-bg);
+ stroke: var(--mdk-dropdown-border);
+}
diff --git a/packages/core/src/components/select/index.tsx b/packages/core/src/components/select/index.tsx
index 29774b3..63a8d02 100644
--- a/packages/core/src/components/select/index.tsx
+++ b/packages/core/src/components/select/index.tsx
@@ -1 +1,620 @@
-export * from '@radix-ui/react-select'
+import * as PopoverPrimitive from '@radix-ui/react-popover'
+import * as SelectPrimitive from '@radix-ui/react-select'
+import * as React from 'react'
+
+import { cn } from '../../utils'
+
+export type SelectStatus = '' | 'error' | 'warning' | 'success'
+export type SelectSize = 'sm' | 'md' | 'lg'
+export type SelectAntdSize = 'small' | 'middle' | 'large'
+export type SelectMode = 'multiple' | 'tags'
+
+export type SelectOptionValue = string
+export type SelectOption = {
+ value: SelectOptionValue
+ label?: React.ReactNode
+ disabled?: boolean
+}
+
+export type SelectProps = {
+ options?: SelectOption[]
+ placeholder?: string
+ value?: SelectOptionValue | SelectOptionValue[] | null
+ defaultValue?: SelectOptionValue | SelectOptionValue[]
+ onChange?: (value: SelectOptionValue | SelectOptionValue[] | undefined) => void
+ onSelect?: (value: SelectOptionValue) => void
+ onClear?: () => void
+ allowClear?: boolean
+ mode?: SelectMode
+ tokenSeparators?: string[]
+ status?: SelectStatus
+ size?: SelectSize | SelectAntdSize
+ loading?: boolean
+ disabled?: boolean
+ className?: string
+ dropdownClassName?: string
+ suffixIcon?: React.ReactNode
+ children?: React.ReactNode
+}
+
+export type SelectOptionProps = {
+ value: SelectOptionValue
+ disabled?: boolean
+ children?: React.ReactNode
+}
+
+const SelectOption = (_props: SelectOptionProps): React.ReactElement | null => null
+SelectOption.displayName = 'SelectOption'
+
+const sizeMap: Record = {
+ sm: 'sm',
+ small: 'sm',
+ md: 'md',
+ middle: 'md',
+ lg: 'lg',
+ large: 'lg',
+}
+
+function sizeToSize(size?: SelectSize | SelectAntdSize): SelectSize {
+ return size ? sizeMap[size] : 'md'
+}
+
+function getOptionsFromChildren(children: React.ReactNode): SelectOption[] {
+ const options: SelectOption[] = []
+ React.Children.forEach(children, (child) => {
+ if (!React.isValidElement(child)) {
+ return
+ }
+ const childType = child.type as { displayName?: string }
+ if (childType === SelectOption || childType?.displayName === SelectOption.displayName) {
+ const { value, disabled, children: label } = child.props as SelectOptionProps
+ options.push({ value, label, disabled })
+ }
+ })
+ return options
+}
+
+function normalizeOptions(options?: SelectOption[], children?: React.ReactNode): SelectOption[] {
+ if (options?.length) {
+ return options
+ }
+ if (!children) {
+ return []
+ }
+ return getOptionsFromChildren(children)
+}
+
+function getOptionLabel(option: SelectOption): React.ReactNode {
+ return option.label ?? option.value
+}
+
+function getOptionText(option: SelectOption): string {
+ if (typeof option.label === 'string') {
+ return option.label
+ }
+ return String(option.value)
+}
+
+function escapeRegex(value: string): string {
+ return value.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
+}
+
+function useControllableValue(value: T | undefined, defaultValue: T): [T, (next: T) => void] {
+ const [internalValue, setInternalValue] = React.useState(defaultValue)
+ const isControlled = value !== undefined
+ const currentValue = isControlled ? value : internalValue
+ const setValue = React.useCallback(
+ (next: T) => {
+ if (!isControlled) {
+ setInternalValue(next)
+ }
+ },
+ [isControlled],
+ )
+ return [currentValue, setValue]
+}
+
+function isValueSelected(value: SelectOptionValue[], optionValue: SelectOptionValue): boolean {
+ return value.includes(optionValue)
+}
+
+function removeValue(
+ value: SelectOptionValue[],
+ optionValue: SelectOptionValue,
+): SelectOptionValue[] {
+ return value.filter((item) => item !== optionValue)
+}
+
+function addValue(value: SelectOptionValue[], optionValue: SelectOptionValue): SelectOptionValue[] {
+ if (value.includes(optionValue)) {
+ return value
+ }
+ return [...value, optionValue]
+}
+
+function buildTagsInputValue(
+ rawValue: SelectOptionValue | SelectOptionValue[] | null | undefined,
+): SelectOptionValue[] {
+ if (!rawValue) {
+ return []
+ }
+ if (Array.isArray(rawValue)) {
+ return rawValue
+ }
+ return [rawValue]
+}
+
+type SingleSelectProps = {
+ options: SelectOption[]
+ placeholder?: string
+ value?: SelectOptionValue | null
+ defaultValue?: SelectOptionValue
+ onChange?: (value: SelectOptionValue | SelectOptionValue[] | undefined) => void
+ onSelect?: (value: SelectOptionValue) => void
+ onClear?: () => void
+ allowClear?: boolean
+ status?: SelectStatus
+ size?: SelectSize
+ loading?: boolean
+ disabled?: boolean
+ className?: string
+ dropdownClassName?: string
+ suffixIcon?: React.ReactNode
+}
+
+const SingleSelect = React.forwardRef(
+ (
+ {
+ options,
+ placeholder,
+ value,
+ defaultValue,
+ onChange,
+ onSelect,
+ onClear,
+ allowClear,
+ status,
+ size = 'md',
+ loading,
+ disabled,
+ className,
+ dropdownClassName,
+ suffixIcon,
+ },
+ ref,
+ ) => {
+ const [currentValue, setCurrentValue] = useControllableValue(
+ value === null ? undefined : value,
+ defaultValue,
+ )
+ const showClear = allowClear && Boolean(currentValue)
+ const handleValueChange = (nextValue: string): void => {
+ setCurrentValue(nextValue)
+ onChange?.(nextValue)
+ onSelect?.(nextValue)
+ }
+
+ const handleClear = (): void => {
+ setCurrentValue(undefined)
+ onClear?.()
+ onChange?.(undefined)
+ }
+
+ return (
+
+
+
+
+
+
+ {showClear && (
+
+ )}
+
+ {loading ? : (suffixIcon ?? )}
+
+
+
+
+
+ {options.length ? (
+ options.map((option) => (
+
+ {getOptionLabel(option)}
+
+
+
+
+ ))
+ ) : (
+ No options
+ )}
+
+
+
+
+
+ )
+ },
+)
+SingleSelect.displayName = 'SingleSelect'
+
+type TagsSelectProps = {
+ options: SelectOption[]
+ placeholder?: string
+ value: SelectOptionValue[]
+ defaultValue: SelectOptionValue[]
+ onChange?: (value: SelectOptionValue[] | undefined) => void
+ onSelect?: (value: SelectOptionValue) => void
+ onClear?: () => void
+ allowClear?: boolean
+ tokenSeparators: string[]
+ status?: SelectStatus
+ size?: SelectSize
+ loading?: boolean
+ disabled?: boolean
+ className?: string
+ dropdownClassName?: string
+ suffixIcon?: React.ReactNode
+ allowCustomValues?: boolean
+}
+
+const TagsSelect = React.forwardRef(
+ (
+ {
+ options,
+ placeholder,
+ value,
+ defaultValue,
+ onChange,
+ onSelect,
+ onClear,
+ allowClear,
+ tokenSeparators,
+ status,
+ size = 'md',
+ loading,
+ disabled,
+ className,
+ dropdownClassName,
+ suffixIcon,
+ allowCustomValues = true,
+ },
+ ref,
+ ) => {
+ const [currentValue, setCurrentValue] = useControllableValue(
+ value,
+ defaultValue,
+ )
+ const [inputValue, setInputValue] = React.useState('')
+ const [open, setOpen] = React.useState(false)
+ const inputRef = React.useRef(null)
+ const showClear = allowClear && currentValue.length > 0
+
+ const filteredOptions = React.useMemo(() => {
+ if (!inputValue) {
+ return options
+ }
+ const term = inputValue.toLowerCase()
+ return options.filter((option) => getOptionText(option).toLowerCase().includes(term))
+ }, [options, inputValue])
+
+ const handleSelectionChange = (nextValue: SelectOptionValue[]): void => {
+ setCurrentValue(nextValue)
+ onChange?.(nextValue)
+ }
+
+ const handleAddTag = (tagValue: string): void => {
+ const trimmed = tagValue.trim()
+ if (!trimmed) {
+ return
+ }
+ const nextValue = addValue(currentValue, trimmed)
+ handleSelectionChange(nextValue)
+ onSelect?.(trimmed)
+ }
+
+ const handleRemoveTag = (tagValue: string): void => {
+ const nextValue = removeValue(currentValue, tagValue)
+ handleSelectionChange(nextValue)
+ }
+
+ const handleInputChange = (event: React.ChangeEvent): void => {
+ const nextValue = event.target.value
+ if (tokenSeparators.some((separator) => nextValue.includes(separator))) {
+ const separatorPattern = new RegExp(
+ `[${tokenSeparators.map((separator) => escapeRegex(separator)).join('')}]`,
+ )
+ const parts = nextValue.split(separatorPattern).map((part) => part.trim())
+ const lastPart = parts[parts.length - 1] ?? ''
+ const toAdd = parts.slice(0, -1).filter(Boolean)
+ if (allowCustomValues) {
+ toAdd.forEach(handleAddTag)
+ }
+ setInputValue(lastPart)
+ return
+ }
+ setInputValue(nextValue)
+ }
+
+ const handleInputKeyDown = (event: React.KeyboardEvent): void => {
+ if (event.key === 'Backspace' && !inputValue && currentValue.length) {
+ handleRemoveTag(currentValue[currentValue.length - 1])
+ return
+ }
+ if (event.key === 'Enter' || tokenSeparators.includes(event.key)) {
+ if (inputValue && allowCustomValues) {
+ event.preventDefault()
+ handleAddTag(inputValue)
+ setInputValue('')
+ }
+ }
+ }
+
+ const handleClear = (): void => {
+ setCurrentValue([])
+ onClear?.()
+ onChange?.([])
+ }
+
+ return (
+
+
+
+ inputRef.current?.focus()}
+ role="presentation"
+ >
+
+ {currentValue.map((tag) => (
+
+ {tag}
+
+
+ ))}
+ setOpen(true)}
+ disabled={disabled}
+ />
+
+ {showClear && (
+
+ )}
+
+ {loading ? (
+
+ ) : (
+ (suffixIcon ?? )
+ )}
+
+
+
+
+
+
+ {filteredOptions.length ? (
+ filteredOptions.map((option) => {
+ const selected = isValueSelected(currentValue, option.value)
+ return (
+
+ )
+ })
+ ) : (
+
No options
+ )}
+
+
+
+
+
+ )
+ },
+)
+TagsSelect.displayName = 'TagsSelect'
+
+const Select = React.forwardRef(
+ (
+ {
+ options: optionsProp,
+ placeholder,
+ value,
+ defaultValue,
+ onChange,
+ onSelect,
+ onClear,
+ allowClear,
+ mode,
+ tokenSeparators = [','],
+ status = '',
+ size,
+ loading,
+ disabled,
+ className,
+ dropdownClassName,
+ suffixIcon,
+ children,
+ },
+ ref,
+ ) => {
+ const resolvedSize = sizeToSize(size)
+ const options = React.useMemo(
+ () => normalizeOptions(optionsProp, children),
+ [optionsProp, children],
+ )
+ const isTagMode = mode === 'tags' || mode === 'multiple'
+
+ if (isTagMode) {
+ return (
+ onChange?.(next)}
+ onSelect={onSelect}
+ onClear={onClear}
+ allowClear={allowClear}
+ tokenSeparators={tokenSeparators}
+ status={status}
+ size={resolvedSize}
+ loading={loading}
+ disabled={disabled}
+ className={className}
+ dropdownClassName={dropdownClassName}
+ suffixIcon={suffixIcon}
+ allowCustomValues={mode === 'tags'}
+ />
+ )
+ }
+
+ return (
+
+ )
+ },
+)
+Select.displayName = 'Select'
+
+function ChevronIcon(): React.ReactElement {
+ return (
+
+ )
+}
+
+function CheckIcon(): React.ReactElement {
+ return (
+
+ )
+}
+
+const SelectWithOption = Object.assign(Select, { Option: SelectOption })
+
+export { SelectWithOption as Select, SelectOption }
diff --git a/packages/core/src/components/select/styles.scss b/packages/core/src/components/select/styles.scss
new file mode 100644
index 0000000..808ed82
--- /dev/null
+++ b/packages/core/src/components/select/styles.scss
@@ -0,0 +1,235 @@
+/**
+ * Select Component Styles
+ */
+
+.mdk-select {
+ display: inline-flex;
+ flex-direction: column;
+ width: 100%;
+ font-size: var(--mdk-font-size);
+ color: var(--mdk-input-text);
+}
+
+.mdk-select__control {
+ position: relative;
+ display: flex;
+ align-items: center;
+ min-height: 2.5rem;
+ border: 1px solid var(--mdk-input-border);
+ background: var(--mdk-input-bg);
+ border-radius: var(--mdk-input-radius);
+ padding: 0 2.5rem 0 0.75rem;
+ gap: 0.5rem;
+ transition: border-color 0.2s, box-shadow 0.2s;
+
+ &:hover {
+ border-color: var(--mdk-input-border-hover);
+ background: var(--mdk-input-bg-hover);
+ }
+
+ &:focus-within {
+ border-color: var(--mdk-input-border-active);
+ box-shadow: var(--mdk-input-focus-shadow);
+ }
+}
+
+.mdk-select__trigger {
+ all: unset;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ cursor: pointer;
+}
+
+.mdk-select__value {
+ flex: 1;
+
+ &[data-placeholder] {
+ color: var(--mdk-input-placeholder);
+ }
+}
+
+.mdk-select__suffix {
+ position: absolute;
+ right: 0.75rem;
+ display: inline-flex;
+ align-items: center;
+ color: var(--mdk-input-placeholder);
+}
+
+.mdk-select__clear {
+ position: absolute;
+ right: 2.25rem;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 1.25rem;
+ height: 1.25rem;
+ border: none;
+ background: transparent;
+ color: var(--mdk-input-placeholder);
+ cursor: pointer;
+}
+
+.mdk-select__chevron {
+ width: 1rem;
+ height: 1rem;
+}
+
+.mdk-select__spinner {
+ width: 1rem;
+ height: 1rem;
+ border-radius: 9999px;
+ border: 2px solid rgba(255, 255, 255, 0.2);
+ border-top-color: var(--mdk-input-text);
+ animation: mdk-select-spin 0.7s linear infinite;
+}
+
+.mdk-select__content {
+ min-width: 12rem;
+ background: var(--mdk-dropdown-bg);
+ color: var(--mdk-dropdown-item-text);
+ border: 1px solid var(--mdk-dropdown-border);
+ border-radius: var(--mdk-input-radius);
+ box-shadow: var(--mdk-dropdown-shadow);
+ padding: 0.25rem;
+ z-index: 50;
+ max-height: 14rem;
+ overflow-y: auto;
+}
+
+.mdk-select__viewport {
+ display: flex;
+ flex-direction: column;
+ gap: 0.125rem;
+}
+
+.mdk-select__item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.5rem 0.75rem;
+ border-radius: calc(var(--mdk-input-radius) + 2px);
+ cursor: pointer;
+ font-size: 0.875rem;
+ color: var(--mdk-dropdown-item-text);
+
+ &:not(:last-child) {
+ border-bottom: 1px solid var(--mdk-dropdown-divider);
+ }
+
+ &[data-disabled] {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ &[data-highlighted] {
+ background: var(--mdk-dropdown-item-hover-bg);
+ color: var(--mdk-dropdown-item-hover-text);
+ outline: none;
+ }
+
+ &[data-state='checked'] {
+ background: var(--mdk-dropdown-item-selected-bg);
+ color: var(--mdk-dropdown-item-selected-text);
+ }
+}
+
+.mdk-select__item--selected {
+ background: var(--mdk-dropdown-item-selected-bg);
+ color: var(--mdk-dropdown-item-selected-text);
+}
+
+.mdk-select__item-indicator {
+ display: inline-flex;
+ align-items: center;
+ color: currentColor;
+}
+
+.mdk-select__empty {
+ padding: 0.5rem 0.75rem;
+ color: var(--mdk-input-placeholder);
+ font-size: 0.875rem;
+}
+
+.mdk-select--tags .mdk-select__control {
+ flex-wrap: wrap;
+ padding: 0.25rem 2.5rem 0.25rem 0.5rem;
+}
+
+.mdk-select__tags {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.25rem;
+ flex: 1;
+}
+
+.mdk-select__tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+ padding: 0.125rem 0.5rem;
+ background: rgba(255, 255, 255, 0.08);
+ color: var(--mdk-input-text);
+ border-radius: 9999px;
+ font-size: 0.75rem;
+}
+
+.mdk-select__tag-remove {
+ border: none;
+ background: transparent;
+ color: inherit;
+ cursor: pointer;
+ font-size: 0.75rem;
+}
+
+.mdk-select__input {
+ flex: 1;
+ min-width: 6rem;
+ border: none;
+ background: transparent;
+ color: var(--mdk-input-text);
+ font-size: 0.875rem;
+ outline: none;
+ padding: 0.25rem;
+}
+
+.mdk-select__input::placeholder {
+ color: var(--mdk-input-placeholder);
+}
+
+.mdk-select--size-sm .mdk-select__control {
+ min-height: 2rem;
+ font-size: 0.8125rem;
+}
+
+.mdk-select--size-lg .mdk-select__control {
+ min-height: 3rem;
+ font-size: 1rem;
+}
+
+.mdk-select--status-error .mdk-select__control {
+ border-color: var(--mdk-input-border-error);
+ box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
+}
+
+.mdk-select--disabled {
+ opacity: 0.6;
+
+ .mdk-select__control {
+ cursor: not-allowed;
+ background: #1f1f1f;
+ border-color: #2a2a2a;
+ }
+
+ .mdk-select__trigger {
+ cursor: not-allowed;
+ }
+}
+
+@keyframes mdk-select-spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index ca418c6..ada0b53 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -15,12 +15,13 @@ export * from './components/checkbox'
export * from './components/dialog'
// Re-export Radix primitives with namespaces to avoid conflicts
export * as DropdownMenu from './components/dropdown-menu'
+export { Dropdown } from './components/dropdown-menu'
export * from './components/label'
export * as Popover from './components/popover'
export * as Progress from './components/progress'
export * as RadioGroup from './components/radio-group'
-export * as Select from './components/select'
+export * from './components/select'
export * as Separator from './components/separator'
export * as Slider from './components/slider'
export * from './components/switch'
diff --git a/packages/core/src/styles.scss b/packages/core/src/styles.scss
index 94ced37..1f65bfc 100644
--- a/packages/core/src/styles.scss
+++ b/packages/core/src/styles.scss
@@ -11,7 +11,9 @@
@use './components/button/styles' as button;
@use './components/checkbox/styles' as checkbox;
@use './components/dialog/styles' as dialog;
+@use './components/dropdown-menu/styles' as dropdownMenu;
@use './components/label/styles' as label;
+@use './components/select/styles' as select;
@use './components/switch/styles' as switch;
// CSS Variables
@@ -56,6 +58,50 @@
--mdk-switch-unchecked-bg: var(--mdk-color-background-light);
--mdk-switch-thumb-shadow: rgba(0, 0, 0, 0.2);
--mdk-switch-thumb-shadow-checked: rgba(0, 0, 0, 0.3);
+ /* MDK button tokens (moria baseline) */
+ --mdk-radius: 0px;
+ --mdk-font-size: 14px;
+ --mdk-line-height: 22px;
+ --mdk-font-weight: 600;
+ --mdk-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.04);
+
+ --mdk-button-primary-bg: #f7931a;
+ --mdk-button-primary-bg-hover: #c4730f;
+ --mdk-button-primary-text: #000000;
+ --mdk-button-primary-text-hover: #ffffff;
+ --mdk-button-primary-bg-hover-overlay: rgba(255, 255, 255, 0.15);
+
+ --mdk-button-secondary-text: #ffffffcc;
+ --mdk-button-secondary-border: #ffffff33;
+ --mdk-button-secondary-border-hover: #f7931a80;
+ --mdk-button-secondary-bg: #17130f;
+
+ --mdk-button-danger-bg: #ef4444;
+ --mdk-button-danger-text: #ffffff;
+ --mdk-button-danger-border: #ef4444;
+
+ /* Input / Select tokens (moria baseline) */
+ --mdk-input-bg: #2f2f2f;
+ --mdk-input-bg-hover: #3a3a3a;
+ --mdk-input-text: #d7d7d7;
+ --mdk-input-placeholder: #8b8b8b;
+ --mdk-input-border: #3a3a3a;
+ --mdk-input-border-hover: #5a5a5a;
+ --mdk-input-border-active: #f7931a;
+ --mdk-input-border-error: #ef4444;
+ --mdk-input-radius: 2px;
+ --mdk-input-focus-shadow: 0 0 0 2px rgba(247, 147, 26, 0.25);
+
+ /* Dropdown / menu tokens */
+ --mdk-dropdown-bg: #1b140f;
+ --mdk-dropdown-border: #3a2d1d;
+ --mdk-dropdown-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
+ --mdk-dropdown-item-text: #c9c9c9;
+ --mdk-dropdown-item-hover-bg: #2d2116;
+ --mdk-dropdown-item-hover-text: #ffffff;
+ --mdk-dropdown-item-selected-bg: #3a2a18;
+ --mdk-dropdown-item-selected-text: #f7931a;
+ --mdk-dropdown-divider: #2b2118;
}
.dark {