brunner56's picture
implement app
0bfe2e3
'use client';
import { cva } from 'class-variance-authority';
import { Command as CommandPrimitive } from 'cmdk';
import * as React from 'react';
import { cn, ComponentAnatomy, defineStyleAnatomy } from '../core/styling';
import { InputAnatomy } from '../input';
import { Modal, ModalProps } from '../modal';
/* -------------------------------------------------------------------------------------------------
* Anatomy
* -----------------------------------------------------------------------------------------------*/
export const CommandAnatomy = defineStyleAnatomy({
root: cva([
'UI-Command__root',
'flex h-full w-full flex-col overflow-hidden rounded-[--radius-md] bg-[--paper] text-[--foreground]',
]),
inputContainer: cva([
'UI-Command__input',
'flex items-center px-3 py-2',
'cmdk-input-wrapper',
]),
inputIcon: cva(['UI-Command__inputIcon', 'mr-2 h-5 w-5 shrink-0 opacity-50']),
list: cva([
'UI-Command__list',
'max-h-[300px] overflow-y-auto overflow-x-hidden',
]),
empty: cva([
'UI-Command__empty',
'py-6 text-center text-base text-[--muted]',
]),
group: cva([
'UI-Command__group',
'overflow-hidden p-1 text-[--foreground]',
'[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-sm [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-[--muted]',
]),
separator: cva(['UI-Command__separator', '-mx-1 h-px bg-[--border]']),
item: cva([
'UI-Command__item',
'relative flex cursor-default select-none items-center rounded-[--radius] px-2 py-1.5 text-base outline-none',
'aria-selected:bg-[--subtle] data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50',
'[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
]),
itemIconContainer: cva([
'UI-Command__itemIconContainer',
'mr-2 text-base shrink-0 w-4',
]),
shortcut: cva([
'UI-Command__shortcut',
'ml-auto text-xs tracking-widest text-[--muted]',
]),
});
export const CommandDialogAnatomy = defineStyleAnatomy({
content: cva(['UI-CommandDialog__content', 'overflow-hidden p-0']),
command: cva([
'UI-CommandDialog__command',
'[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-[--muted]',
'[&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pb-2 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5',
'[&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-2 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-5',
]),
});
/* -------------------------------------------------------------------------------------------------
* Command
* -----------------------------------------------------------------------------------------------*/
const __CommandAnatomyContext = React.createContext<
ComponentAnatomy<typeof CommandAnatomy>
>({});
export type CommandProps = React.ComponentPropsWithoutRef<
typeof CommandPrimitive
> &
ComponentAnatomy<typeof CommandAnatomy>;
export const Command = React.forwardRef<HTMLDivElement, CommandProps>(
(props, ref) => {
const {
className,
inputContainerClass,
inputIconClass,
listClass,
emptyClass,
groupClass,
separatorClass,
itemClass,
itemIconContainerClass,
shortcutClass,
loop = true,
...rest
} = props;
return (
<__CommandAnatomyContext.Provider
value={{
inputContainerClass,
inputIconClass,
listClass,
emptyClass,
groupClass,
separatorClass,
itemClass,
itemIconContainerClass,
shortcutClass,
}}
>
<CommandPrimitive
ref={ref}
className={cn(CommandAnatomy.root(), className)}
loop={loop}
{...rest}
/>
</__CommandAnatomyContext.Provider>
);
}
);
Command.displayName = CommandPrimitive.displayName;
/* -------------------------------------------------------------------------------------------------
* CommandInput
* -----------------------------------------------------------------------------------------------*/
export type CommandInputProps = React.ComponentPropsWithoutRef<
typeof CommandPrimitive.Input
> &
Pick<
ComponentAnatomy<typeof CommandAnatomy>,
'inputContainerClass' | 'inputIconClass'
>;
export const CommandInput = React.forwardRef<
HTMLInputElement,
CommandInputProps
>((props, ref) => {
const { className, inputContainerClass, inputIconClass, ...rest } = props;
const {
inputContainerClass: _inputContainerClass,
inputIconClass: _inputIconClass,
} = React.useContext(__CommandAnatomyContext);
return (
<div
className={cn(
CommandAnatomy.inputContainer(),
_inputContainerClass,
inputContainerClass
)}
cmdk-input-wrapper=""
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={cn(
CommandAnatomy.inputIcon(),
_inputIconClass,
inputIconClass
)}
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
<CommandPrimitive.Input
ref={ref}
className={cn(
InputAnatomy.root({
intent: 'unstyled',
size: 'sm',
isDisabled: rest.disabled,
}),
className
)}
{...rest}
/>
</div>
);
});
CommandInput.displayName = 'CommandInput';
/* -------------------------------------------------------------------------------------------------
* CommandList
* -----------------------------------------------------------------------------------------------*/
export type CommandListProps = React.ComponentPropsWithoutRef<
typeof CommandPrimitive.List
>;
export const CommandList = React.forwardRef<HTMLDivElement, CommandListProps>(
(props, ref) => {
const { className, ...rest } = props;
const { listClass } = React.useContext(__CommandAnatomyContext);
return (
<CommandPrimitive.List
ref={ref}
className={cn(CommandAnatomy.list(), listClass, className)}
{...rest}
/>
);
}
);
CommandList.displayName = 'CommandList';
/* -------------------------------------------------------------------------------------------------
* CommandEmpty
* -----------------------------------------------------------------------------------------------*/
export type CommandEmptyProps = React.ComponentPropsWithoutRef<
typeof CommandPrimitive.Empty
>;
export const CommandEmpty = React.forwardRef<HTMLDivElement, CommandEmptyProps>(
(props, ref) => {
const { className, ...rest } = props;
const { emptyClass } = React.useContext(__CommandAnatomyContext);
return (
<CommandPrimitive.Empty
ref={ref}
className={cn(CommandAnatomy.empty(), emptyClass, className)}
{...rest}
/>
);
}
);
CommandEmpty.displayName = 'CommandEmpty';
/* -------------------------------------------------------------------------------------------------
* CommandGroup
* -----------------------------------------------------------------------------------------------*/
export type CommandGroupProps = React.ComponentPropsWithoutRef<
typeof CommandPrimitive.Group
>;
export const CommandGroup = React.forwardRef<HTMLDivElement, CommandGroupProps>(
(props, ref) => {
const { className, ...rest } = props;
const { groupClass } = React.useContext(__CommandAnatomyContext);
return (
<CommandPrimitive.Group
ref={ref}
className={cn(CommandAnatomy.group(), groupClass, className)}
{...rest}
/>
);
}
);
CommandGroup.displayName = 'CommandGroup';
/* -------------------------------------------------------------------------------------------------
* CommandSeparator
* -----------------------------------------------------------------------------------------------*/
export type CommandSeparatorProps = React.ComponentPropsWithoutRef<
typeof CommandPrimitive.Separator
>;
export const CommandSeparator = React.forwardRef<
HTMLDivElement,
CommandSeparatorProps
>((props, ref) => {
const { className, ...rest } = props;
const { separatorClass } = React.useContext(__CommandAnatomyContext);
return (
<CommandPrimitive.Separator
ref={ref}
className={cn(CommandAnatomy.separator(), separatorClass, className)}
{...rest}
/>
);
});
CommandSeparator.displayName = 'CommandSeparator';
/* -------------------------------------------------------------------------------------------------
* CommandItem
* -----------------------------------------------------------------------------------------------*/
export type CommandItemProps = React.ComponentPropsWithoutRef<
typeof CommandPrimitive.Item
> &
Pick<ComponentAnatomy<typeof CommandAnatomy>, 'itemIconContainerClass'> & {
leftIcon?: React.ReactNode;
};
export const CommandItem = React.forwardRef<HTMLDivElement, CommandItemProps>(
(props, ref) => {
const { className, itemIconContainerClass, leftIcon, children, ...rest } =
props;
const { itemClass, itemIconContainerClass: _itemIconContainerClass } =
React.useContext(__CommandAnatomyContext);
const itemRef = React.useRef<HTMLDivElement | null>(null);
React.useEffect(() => {
const element = itemRef.current;
if (!element) return;
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.attributeName === 'aria-selected' &&
element.getAttribute('aria-selected') === 'true'
) {
element.scrollIntoView({ block: 'nearest' });
}
});
});
observer.observe(element, { attributes: true });
return () => observer.disconnect();
}, []);
const setRefs = React.useCallback(
(node: HTMLDivElement | null) => {
itemRef.current = node;
if (ref) {
if (typeof ref === 'function') {
ref(node);
}
}
},
[ref]
);
return (
<CommandPrimitive.Item
ref={setRefs}
className={cn(CommandAnatomy.item(), itemClass, className)}
{...rest}
data-cmdkvalue={rest.id}
>
{leftIcon && (
<span
className={cn(
CommandAnatomy.itemIconContainer(),
_itemIconContainerClass,
itemIconContainerClass
)}
>
{leftIcon}
</span>
)}
{children}
</CommandPrimitive.Item>
);
}
);
CommandItem.displayName = 'CommandItem';
/* -------------------------------------------------------------------------------------------------
* CommandShortcut
* -----------------------------------------------------------------------------------------------*/
export type CommandShortcutProps = React.ComponentPropsWithoutRef<'span'>;
export const CommandShortcut = React.forwardRef<
HTMLSpanElement,
CommandShortcutProps
>((props, ref) => {
const { className, ...rest } = props;
const { shortcutClass } = React.useContext(__CommandAnatomyContext);
return (
<span
ref={ref}
className={cn(CommandAnatomy.shortcut(), shortcutClass, className)}
{...rest}
/>
);
});
CommandShortcut.displayName = 'CommandShortcut';
/* -------------------------------------------------------------------------------------------------
* CommandDialog
* -----------------------------------------------------------------------------------------------*/
export type CommandDialogProps = ModalProps &
ComponentAnatomy<typeof CommandDialogAnatomy> & {
commandProps: React.ComponentPropsWithoutRef<typeof CommandPrimitive>;
};
export const CommandDialog = (props: CommandDialogProps) => {
const { children, commandClass, contentClass, commandProps, ...rest } = props;
return (
<Modal
{...rest}
contentClass={cn(CommandDialogAnatomy.content(), contentClass)}
>
<Command
shouldFilter={false}
className={cn(CommandDialogAnatomy.command(), commandClass)}
{...commandProps}
>
{children}
</Command>
</Modal>
);
};
CommandDialog.displayName = 'CommandDialog';