Skip to content

Commit

Permalink
✨ feat: add Command components (#68)
Browse files Browse the repository at this point in the history
* ✨ feat: add Command components and update package configuration

* ✨ feat: implement Command context and dialog components with variants

* ✨ feat: enhance Command component with theme selection and grouping

* ✨ feat: replace twMerge with cn utility for class name handling in Command components

* 🚧 WIP: implement toggle functionality in Command context and enhance CommandDialog layout

* ⬆️ chore: update Node.js version in .nvmrc to v22.13.1

* 🔧 chore: change npm ci to npm install in GitHub Actions workflow

* WIP: Is just a backup

* 🔧 chore: reorder CI script commands for improved execution flow

* 👷 chore: the package.json have been update

* 🔧 chore: enhance Command component with new props and improved styling

* 🔧 chore: update Command stories and package dependencies
  • Loading branch information
futjesus authored Feb 19, 2025
1 parent ff0d5a6 commit 22792bd
Show file tree
Hide file tree
Showing 23 changed files with 1,290 additions and 653 deletions.
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
legacy-peer-deps=false
auto-install-peers=true
117 changes: 117 additions & 0 deletions lib/components/Command/Command.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useContext, useEffect } from 'react';
import { Database, Moon, Sun } from 'react-feather';

import { ThemeProvider, useTheme } from '@/contexts';

import { Command as CommandComponent } from './Command';
import {
CommandGroup,
CommandInput,
CommandItem,
CommandSeparator,
} from './components';
import { CommandContext, CommandProvider } from './contexts';

type Story = StoryObj<typeof CommandComponent>;

const meta: Meta<typeof CommandComponent> = {
title: 'In Review/Command',
component: CommandComponent,
};

export const Command: Story = {
render: () => {
const Wrapper = () => {
const { theme, setTheme } = useTheme();
const { isOpen, setOpen, toggleOpen } = useContext(CommandContext);

useEffect(() => {
const down = (e: KeyboardEvent) => {
if ((e.key === 'k' && (e.metaKey || e.ctrlKey)) || e.key === '/') {
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return;
}

e.preventDefault();
toggleOpen();
}
};

document.addEventListener('keydown', down);

return () => {
document.removeEventListener('keydown', down);
};
}, [toggleOpen]);

return (
<>
<div className="flex gap-3 items-center">
<p>
Current Theme: <span>{theme}</span>
</p>

<kbd className="pointer-events-none h-5 select-none flex items-center gap-1 rounded-md bg-kubefirst-secondary px-1.5 font-mono text-[10px] font-medium opacity-100 text-white">
<span className="text-xs"></span>K
</kbd>
</div>

<CommandComponent open={isOpen} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandGroup heading="Theme">
<CommandItem onSelect={() => setTheme!('kubefirst')}>
<Sun />
Kubefirst
</CommandItem>

<CommandItem onSelect={() => setTheme!('colony')}>
<Moon />
Colony
</CommandItem>

<CommandItem onSelect={() => setTheme!('colony')}>
<Database />
Civo
</CommandItem>
</CommandGroup>

<CommandSeparator />

<CommandGroup heading="Theme">
<CommandItem onSelect={() => setTheme!('kubefirst')}>
<Sun />
Kubefirst
</CommandItem>

<CommandItem onSelect={() => setTheme!('colony')}>
<Moon />
Colony
</CommandItem>

<CommandItem onSelect={() => setTheme!('colony')}>
<Database />
Civo
</CommandItem>
</CommandGroup>
</CommandComponent>
</>
);
};

return (
<ThemeProvider theme="kubefirst">
<CommandProvider>
<Wrapper />
</CommandProvider>
</ThemeProvider>
);
},
};

export default meta;
39 changes: 39 additions & 0 deletions lib/components/Command/Command.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { DialogTitle, Root } from '@radix-ui/react-dialog';
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
import { FC } from 'react';

import { cn } from '../../utils';

import { CommandProps } from './Command.types';
import {
Command as CommandBase,
CommandGroup,
CommandItem,
CommandSeparator,
DialogContent,
} from './components';
import { wrapperVariants } from './Command.variants';

const Command: FC<CommandProps> = ({
children,
title,
titleClassName,
wrapperClassName,
...props
}) => (
<Root {...props}>
<DialogContent className="overflow-hidden p-0 border-0 !rounded-none">
<DialogTitle className={cn('hidden', titleClassName)}>
<VisuallyHidden>{title}</VisuallyHidden>
</DialogTitle>

<CommandBase
className={cn(wrapperVariants({ className: wrapperClassName }))}
>
{children}
</CommandBase>
</DialogContent>
</Root>
);

export { Command, CommandGroup, CommandItem, CommandSeparator };
16 changes: 16 additions & 0 deletions lib/components/Command/Command.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DialogProps } from '@radix-ui/react-dialog';
import { VariantProps } from 'class-variance-authority';
import { PropsWithChildren } from 'react';

import { commandVariants } from './Command.variants';

export interface CommandProps
extends VariantProps<typeof commandVariants>,
PropsWithChildren,
DialogProps {
emptyResults?: string;
placeholder?: string;
title?: string;
titleClassName?: string;
wrapperClassName?: string;
}
98 changes: 98 additions & 0 deletions lib/components/Command/Command.variants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { cva } from 'class-variance-authority';

export const commandVariants = cva([], {
variants: {
theme: {
colony: '',
kubefirst: '',
civo: '',
},
},
defaultVariants: {
theme: 'kubefirst',
},
});

export const wrapperVariants = cva(
[
'[&_[cmdk-group-heading]]:font-medium',
'[&_[cmdk-group-heading]]:px-2',
'[&_[cmdk-group-heading]]:text-muted-foreground',
'[&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0',
'[&_[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-3',
'[&_[cmdk-item]_svg]:h-5',
'[&_[cmdk-item]_svg]:w-5',
'bg-popover',
'flex',
'items-center',
'justify-center',
'overflow-hidden',
'text-popover-foreground',
'border',
'rounded-md',
'border-neutral-900/35',
'bg-neutral-900/30',
],
{
variants: {
theme: {
colony: '',
kubefirst: '',
civo: '',
},
},
defaultVariants: {
theme: 'kubefirst',
},
},
);

export const inputVariants = cva(
[
'bg-transparent',
'disabled:cursor-not-allowed',
'disabled:opacity-50',
'flex',
'h-10',
'outline-none',
'placeholder:text-muted-foreground',
'py-3',
'rounded-md',
'text-sm',
'w-full',
'text-white/80',
],
{
variants: {
theme: {
colony: '',
kubefirst: '',
civo: '',
},
},
defaultVariants: {
theme: 'kubefirst',
},
},
);

export const searchInconInputVariants = cva(
['mr-2', 'h-4', 'w-4', 'shrink-0', 'opacity-80', 'text-white'],
{
variants: {
theme: {
colony: '',
kubefirst: '',
civo: '',
},
},
defaultVariants: {
theme: 'kubefirst',
},
},
);
15 changes: 15 additions & 0 deletions lib/components/Command/components/Command.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from 'react';
import { Command as CommandPrimitive } from 'cmdk';

const Command = forwardRef<
ComponentRef<typeof CommandPrimitive>,
ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, children, ...props }, ref) => (
<CommandPrimitive ref={ref} className={className} {...props}>
<div className="w-[500px]">{children}</div>
</CommandPrimitive>
));

Command.displayName = CommandPrimitive.displayName;

export { Command };
17 changes: 17 additions & 0 deletions lib/components/Command/components/CommandEmpty.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from 'react';
import { Command as CommandPrimitive } from 'cmdk';

const CommandEmpty = forwardRef<
ComponentRef<typeof CommandPrimitive.Empty>,
ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));

CommandEmpty.displayName = CommandPrimitive.Empty.displayName;

export { CommandEmpty };
22 changes: 22 additions & 0 deletions lib/components/Command/components/CommandGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from 'react';
import { Command as CommandPrimitive } from 'cmdk';

import { cn } from '../../../utils';

const CommandGroup = forwardRef<
ComponentRef<typeof CommandPrimitive.Group>,
ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
className,
)}
{...props}
/>
));

CommandGroup.displayName = CommandPrimitive.Group.displayName;

export { CommandGroup };
28 changes: 28 additions & 0 deletions lib/components/Command/components/CommandInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Command as CommandPrimitive } from 'cmdk';
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from 'react';
import { Search } from 'react-feather';

import { cn } from '../../../utils';
import { inputVariants, searchInconInputVariants } from '../Command.variants';

const CommandInput = forwardRef<
ComponentRef<typeof CommandPrimitive.Input>,
ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div
className="flex items-center px-3 border-b border-neutral-900/35"
cmdk-input-wrapper=""
>
<Search className={cn(searchInconInputVariants({ className }))} />

<CommandPrimitive.Input
ref={ref}
className={cn(inputVariants({ className }))}
{...props}
/>
</div>
));

CommandInput.displayName = CommandPrimitive.Input.displayName;

export { CommandInput };
Loading

0 comments on commit 22792bd

Please sign in to comment.