Change component names

This commit is contained in:
Nate Kelley 2025-09-29 12:26:19 -06:00
parent 8f2663c3b2
commit e2a298bf9e
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
17 changed files with 240 additions and 219 deletions

View File

@ -1,22 +0,0 @@
import type React from 'react';
import { cn } from '@/lib/classMerge';
import type { BusterInputContainerProps } from './BusterInput.types';
export const BusterInputContainer: React.FC<BusterInputContainerProps> = ({
children,
className,
style,
sendIcon,
secondaryActions,
submitting,
disabled,
onStop,
onSubmit,
variant,
}) => {
return (
<div data-testid="buster-input-container" className="flex flex-col gap-2">
{children}
</div>
);
};

View File

@ -1,62 +0,0 @@
import type {
BusterInputProps,
BusterOnSelectParams,
GroupOverrideProps,
} from './BusterInput.types';
import { BusterInputGroup } from './BusterInputGroup';
import { BusterInputItem } from './BusterInputItem';
import { BusterInputSeparator } from './BusterInputSeparator';
export const BusterItemSelector = ({
item,
onSelect,
addValueToInput,
closeOnSelect,
}: {
item: BusterInputProps['suggestionItems'][number];
onSelect: (params: BusterOnSelectParams) => void;
} & GroupOverrideProps) => {
if (item.type === 'separator') {
return <BusterInputSeparator />;
}
if (item.type === 'group') {
return <BusterInputGroup {...item} onSelect={onSelect} />;
}
return (
<BusterInputItem
{...item}
onSelect={onSelect}
addValueToInput={item?.addValueToInput ?? addValueToInput}
closeOnSelect={item?.closeOnSelect ?? closeOnSelect}
/>
);
};
export const BusterItemsSelector = ({
suggestionItems,
onSelect,
addValueToInput,
closeOnSelect,
}: {
suggestionItems: BusterInputProps['suggestionItems'];
onSelect: (params: BusterOnSelectParams) => void;
} & GroupOverrideProps) => {
if (!suggestionItems) return null;
return suggestionItems.map((item, index) => (
<BusterItemSelector
key={keySelector(item, index)}
item={item}
onSelect={onSelect}
addValueToInput={addValueToInput}
closeOnSelect={closeOnSelect}
/>
));
};
const keySelector = (item: BusterInputProps['suggestionItems'][number], index: number) => {
if (item.type === 'separator') return `separator${index}`;
if (item.type === 'group') return `${item.label} + index`;
return item.value;
};

View File

@ -1,42 +0,0 @@
/** biome-ignore-all lint/complexity/noUselessFragments: Intersting bug when NOT using fragments */
import { Command } from 'cmdk';
import React, { forwardRef } from 'react';
import { MentionInput, type MentionInputProps, type MentionInputRef } from '../MentionInput';
import type { BusterInputProps } from './BusterInput.types';
export type BusterMentionsInputProps = Pick<
BusterInputProps,
| 'mentions'
| 'value'
| 'placeholder'
| 'defaultValue'
| 'shouldFilter'
| 'filter'
| 'onMentionItemClick'
> & {
onChange: MentionInputProps['onChange'];
onPressEnter: MentionInputProps['onPressEnter'];
className?: string;
style?: React.CSSProperties;
autoFocus?: boolean;
readOnly?: boolean;
commandListNavigatedRef?: React.RefObject<boolean>;
};
export const BusterMentionsInput = forwardRef<MentionInputRef, BusterMentionsInputProps>(
({ value: valueProp, placeholder, defaultValue, mentions, value, ...props }, ref) => {
return (
<React.Fragment>
<MentionInput ref={ref} mentions={mentions} defaultValue={value} {...props} />
<Command.Input
value={value}
autoFocus={false}
className="sr-only hidden h-0 border-0 p-0 pointer-events-none w-full"
aria-hidden="true"
/>
</React.Fragment>
);
}
);
BusterMentionsInput.displayName = 'BusterMentionsInput';

View File

@ -3,6 +3,7 @@ import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import { EditorContent, EditorContext, useEditor } from '@tiptap/react';
import { cva } from 'class-variance-authority';
import { forwardRef, useImperativeHandle, useMemo } from 'react';
import { cn } from '@/lib/utils';
import { MentionExtension } from './MentionExtension';
@ -14,6 +15,15 @@ import type {
import { SubmitOnEnter } from './SubmitEnterExtension';
import { onUpdateTransformer } from './update-transformers';
const variants = cva('outline-0', {
variants: {
variant: {
default: '',
ghost: '',
},
},
});
export const MentionInput = forwardRef<MentionInputRef, MentionInputProps>(
(
{
@ -29,9 +39,11 @@ export const MentionInput = forwardRef<MentionInputRef, MentionInputProps>(
disabled,
onPressEnter,
commandListNavigatedRef,
variant = 'default',
},
ref
) => {
const classes = variants({ variant });
const mentionsByTrigger = useMemo(() => {
return mentions.reduce(
(acc, mention) => {
@ -61,7 +73,7 @@ export const MentionInput = forwardRef<MentionInputRef, MentionInputProps>(
editable: !disabled && !readOnly,
editorProps: {
attributes: {
class: cn('p-1 border rounded outline-0', className),
class: cn('p-1', classes, className),
},
},
onUpdate: ({ editor }) => {

View File

@ -100,6 +100,7 @@ export type MentionInputProps = {
readOnly?: boolean;
disabled?: boolean;
commandListNavigatedRef?: React.RefObject<boolean>;
variant?: 'default' | 'ghost';
};
export type MentionInputRef = {

View File

@ -1,11 +1,11 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import { BusterInput } from './BusterInput';
import type { BusterInputProps } from './BusterInput.types';
import { MentionInputSuggestions } from './MentionInputSuggestions';
import type { MentionInputSuggestionsProps } from './MentionInputSuggestions.types';
const meta: Meta<typeof BusterInput> = {
title: 'UI/Inputs/BusterInput',
component: BusterInput,
const meta: Meta<typeof MentionInputSuggestions> = {
title: 'UI/Inputs/MentionInputSuggestions',
component: MentionInputSuggestions,
decorators: [
(Story) => (
<div style={{ width: '300px', minHeight: '500px' }}>
@ -16,9 +16,9 @@ const meta: Meta<typeof BusterInput> = {
};
export default meta;
type Story = StoryObj<typeof BusterInput>;
type Story = StoryObj<typeof MentionInputSuggestions>;
const items: BusterInputProps['suggestionItems'] = [
const items: MentionInputSuggestionsProps['suggestionItems'] = [
...Array.from({ length: 3 }, (_, i) => ({
label: `Item ${i + 1}`,
value: `item${i + 1}`,
@ -40,7 +40,7 @@ const items: BusterInputProps['suggestionItems'] = [
},
];
const mentions: BusterInputProps['mentions'] = [
const mentions: MentionInputSuggestionsProps['mentions'] = [
// {
// trigger: '@',
// items: [
@ -79,5 +79,6 @@ export const Default: Story = {
suggestionItems: items,
mentions,
onSubmit: fn(),
children: <div className="bg-red-100 min-h-20">Hello</div>,
},
};

View File

@ -2,14 +2,17 @@ import { Command } from 'cmdk';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import type { MentionInputRef } from '../MentionInput';
import type { BusterInputProps, BusterOnSelectParams } from './BusterInput.types';
import { BusterInputContainer } from './BusterInputContainer';
import { BusterInputEmpty } from './BusterInputEmpty';
import { BusterInputList } from './BusterInputList';
import { BusterItemsSelector } from './BusterItemSelector';
import { BusterMentionsInput } from './BusterMentionsInput';
import type {
MentionInputSuggestionsOnSelectParams,
MentionInputSuggestionsProps,
} from './MentionInputSuggestions.types';
import { MentionInputSuggestionsContainer } from './MentionInputSuggestionsContainer';
import { MentionInputSuggestionsEmpty } from './MentionInputSuggestionsEmpty';
import { MentionInputSuggestionsItemsSelector } from './MentionInputSuggestionsItemSelector';
import { MentionInputSuggestionsList } from './MentionInputSuggestionsList';
import { MentionInputSuggestionsMentionsInput } from './MentionInputSuggestionsMentionsInput';
export const BusterInput = ({
export const MentionInputSuggestions = ({
placeholder,
defaultValue,
value: valueProp,
@ -23,9 +26,10 @@ export const BusterInput = ({
secondaryActions,
variant = 'default',
onChange,
ariaLabel = 'Buster Input',
ariaLabel = 'Mention Input Suggestions',
readOnly,
autoFocus,
children,
//suggestions
suggestionItems,
closeSuggestionOnSelect = true,
@ -36,7 +40,7 @@ export const BusterInput = ({
//mentions
onMentionItemClick,
mentions,
}: BusterInputProps) => {
}: MentionInputSuggestionsProps) => {
const [hasClickedSelect, setHasClickedSelect] = useState(false);
const [value, setValue] = useState(valueProp ?? defaultValue);
const commandListNavigatedRef = useRef(false);
@ -54,29 +58,31 @@ export const BusterInput = ({
onChange?.(value);
}, []);
const onSelectItem = useMemoizedFn(({ onClick, ...params }: BusterOnSelectParams) => {
const { addValueToInput, loading, inputValue, label, disabled } = params;
if (disabled) {
console.warn('Item is disabled', params);
return;
const onSelectItem = useMemoizedFn(
({ onClick, ...params }: MentionInputSuggestionsOnSelectParams) => {
const { addValueToInput, loading, inputValue, label, disabled } = params;
if (disabled) {
console.warn('Item is disabled', params);
return;
}
if (submitting) {
console.warn('Input is submitting');
return;
}
if (loading) {
console.warn('Item is loading', params);
return;
}
if (addValueToInput) {
const stringValue = inputValue ?? String(label);
mentionsInputRef.current?.editor?.commands.setContent(stringValue);
setValue(stringValue);
}
onClick?.();
if (closeSuggestionOnSelect) setHasClickedSelect(true);
onSuggestionItemClick?.(params);
}
if (submitting) {
console.warn('Input is submitting');
return;
}
if (loading) {
console.warn('Item is loading', params);
return;
}
if (addValueToInput) {
const stringValue = inputValue ?? String(label);
mentionsInputRef.current?.editor?.commands.setContent(stringValue);
setValue(stringValue);
}
onClick?.();
if (closeSuggestionOnSelect) setHasClickedSelect(true);
onSuggestionItemClick?.(params);
});
);
const onSubmitPreflight = useMemoizedFn((value: string) => {
if (submitting) {
@ -123,16 +129,8 @@ export const BusterInput = ({
return (
<Command ref={commandRef} value={value} label={ariaLabel} className="relative">
<BusterInputContainer
onSubmit={onSubmitPreflight}
onStop={onStopPreflight}
submitting={submitting}
disabled={disabledGlobal}
sendIcon={sendIcon}
secondaryActions={secondaryActions}
variant={variant}
>
<BusterMentionsInput
<MentionInputSuggestionsContainer>
<MentionInputSuggestionsMentionsInput
ref={mentionsInputRef}
defaultValue={defaultValue}
readOnly={readOnly}
@ -147,16 +145,19 @@ export const BusterInput = ({
onPressEnter={onPressEnter || onSubmit}
commandListNavigatedRef={commandListNavigatedRef}
/>
</BusterInputContainer>
<BusterInputList show={showSuggestionList}>
<BusterItemsSelector
{children}
</MentionInputSuggestionsContainer>
<MentionInputSuggestionsList show={showSuggestionList}>
<MentionInputSuggestionsItemsSelector
suggestionItems={suggestionItems}
onSelect={onSelectItem}
addValueToInput={addSuggestionValueToInput}
closeOnSelect={closeSuggestionOnSelect}
/>
{emptyComponent && <BusterInputEmpty>{emptyComponent}</BusterInputEmpty>}
</BusterInputList>
{emptyComponent && (
<MentionInputSuggestionsEmpty>{emptyComponent}</MentionInputSuggestionsEmpty>
)}
</MentionInputSuggestionsList>
</Command>
);
};

View File

@ -11,7 +11,7 @@ export type GroupOverrideProps = {
closeOnSelect: boolean | undefined;
};
export type BusterInputDropdownItem<T = string> = {
export type MentionInputSuggestionsDropdownItem<T = string> = {
value: T;
inputValue?: string; //if this is undefined, the label will be used (string casted), must have addValueToInput set to true
label: string | React.ReactNode;
@ -25,9 +25,9 @@ export type BusterInputDropdownItem<T = string> = {
addValueToInput?: boolean; //defaults to group addValueToInput
};
export type BusterOnSelectParams = NonNullable<
export type MentionInputSuggestionsOnSelectParams = NonNullable<
Pick<
NonNullable<BusterInputDropdownItem>,
NonNullable<MentionInputSuggestionsDropdownItem>,
| 'value'
| 'label'
| 'addValueToInput'
@ -39,19 +39,19 @@ export type BusterOnSelectParams = NonNullable<
>
>;
export type BusterInputDropdownGroup<T = string> = {
export type MentionInputSuggestionsDropdownGroup<T = string> = {
label: string | React.ReactNode;
suggestionItems: BusterInputDropdownItem<T>[];
suggestionItems: MentionInputSuggestionsDropdownItem<T>[];
addValueToInput?: boolean;
closeOnSelect?: boolean;
type: 'group';
};
export type BusterInputSeperator = {
export type MentionInputSuggestionsSeparator = {
type: 'separator';
};
export type BusterInputProps<T = string> = {
export type MentionInputSuggestionsProps<T = string> = {
defaultValue: string;
value?: string;
onChange?: (value: string) => void;
@ -63,30 +63,31 @@ export type BusterInputProps<T = string> = {
onStop: () => void;
variant?: 'default';
autoFocus?: boolean;
sendIcon?: React.ReactNode;
secondaryActions?: React.ReactNode;
placeholder?: string;
ariaLabel?: string;
emptyComponent?: React.ReactNode | string | false; //if false, no empty component will be shown
children?: React.ReactNode;
sendIcon?: React.ReactNode;
secondaryActions?: React.ReactNode;
//mentions
onMentionItemClick?: (params: MentionTriggerItem<T>) => void;
mentions: MentionSuggestionExtension[];
//suggestions
suggestionItems: (
| BusterInputDropdownItem<T>
| BusterInputDropdownGroup<T>
| BusterInputSeperator
| MentionInputSuggestionsDropdownItem<T>
| MentionInputSuggestionsDropdownGroup<T>
| MentionInputSuggestionsSeparator
)[];
onSuggestionItemClick?: (params: Omit<BusterOnSelectParams, 'onClick'>) => void;
onSuggestionItemClick?: (params: Omit<MentionInputSuggestionsOnSelectParams, 'onClick'>) => void;
addSuggestionValueToInput?: boolean; //defaults to true
closeSuggestionOnSelect?: boolean; //defaults to true
} & Pick<React.ComponentProps<typeof Command>, 'filter' | 'shouldFilter'>;
export type BusterInputContainerProps = {
export type MentionInputSuggestionsContainerProps = {
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
} & Pick<
BusterInputProps,
MentionInputSuggestionsProps,
'sendIcon' | 'secondaryActions' | 'submitting' | 'disabled' | 'onStop' | 'onSubmit' | 'variant'
>;

View File

@ -0,0 +1,20 @@
import type React from 'react';
import type { PropsWithChildren } from 'react';
import { cn } from '@/lib/classMerge';
export const MentionInputSuggestionsContainer: React.FC<
PropsWithChildren<{
className?: string;
style?: React.CSSProperties;
}>
> = ({ children, className, style }) => {
return (
<div
data-testid="mention-input-suggestions-container"
className={cn('flex flex-col border rounded overflow-hidden', className)}
style={style}
>
{children}
</div>
);
};

View File

@ -2,7 +2,7 @@ import { Command } from 'cmdk';
import type React from 'react';
import { cn } from '@/lib/utils';
export const BusterInputEmpty = ({
export const MentionInputSuggestionsEmpty = ({
children,
...props
}: React.ComponentPropsWithoutRef<typeof Command.Empty>) => {

View File

@ -1,17 +1,17 @@
import { Command } from 'cmdk';
import type React from 'react';
import { cn } from '@/lib/utils';
import type { BusterInputDropdownGroup, BusterOnSelectParams } from './BusterInput.types';
import { BusterItemsSelector } from './BusterItemSelector';
import type { MentionInputSuggestionsDropdownGroup, MentionInputSuggestionsOnSelectParams } from './MentionInputSuggestions.types';
import { MentionInputSuggestionsItemsSelector } from './MentionInputSuggestionsItemSelector';
export type BusterInputGroupProps = BusterInputDropdownGroup & {
onSelect: (params: BusterOnSelectParams) => void;
export type MentionInputSuggestionsGroupProps = MentionInputSuggestionsDropdownGroup & {
onSelect: (params: MentionInputSuggestionsOnSelectParams) => void;
} & {
className?: string;
style?: React.CSSProperties;
};
export const BusterInputGroup = ({
export const MentionInputSuggestionsGroup = ({
suggestionItems,
label,
onSelect,
@ -19,7 +19,7 @@ export const BusterInputGroup = ({
className,
closeOnSelect,
style,
}: BusterInputGroupProps) => {
}: MentionInputSuggestionsGroupProps) => {
return (
<Command.Group
className={cn(
@ -29,7 +29,7 @@ export const BusterInputGroup = ({
style={style}
heading={label}
>
<BusterItemsSelector
<MentionInputSuggestionsItemsSelector
suggestionItems={suggestionItems}
onSelect={onSelect}
addValueToInput={addValueToInput}

View File

@ -1,16 +1,16 @@
import { Command } from 'cmdk';
import type React from 'react';
import { cn } from '@/lib/utils';
import type { BusterInputDropdownItem, BusterOnSelectParams } from './BusterInput.types';
import type { MentionInputSuggestionsDropdownItem, MentionInputSuggestionsOnSelectParams } from './MentionInputSuggestions.types';
export type BusterInputItemProps = {
onSelect: (d: BusterOnSelectParams) => void;
} & BusterInputDropdownItem & {
export type MentionInputSuggestionsItemProps = {
onSelect: (d: MentionInputSuggestionsOnSelectParams) => void;
} & MentionInputSuggestionsDropdownItem & {
className?: string;
style?: React.CSSProperties;
};
export const BusterInputItem = ({
export const MentionInputSuggestionsItem = ({
value,
inputValue,
label,
@ -24,7 +24,7 @@ export const BusterInputItem = ({
addValueToInput,
onSelect,
...props
}: BusterInputItemProps) => {
}: MentionInputSuggestionsItemProps) => {
return (
<Command.Item
className={cn(

View File

@ -0,0 +1,65 @@
import type {
MentionInputSuggestionsProps,
MentionInputSuggestionsOnSelectParams,
GroupOverrideProps,
} from './MentionInputSuggestions.types';
import { MentionInputSuggestionsGroup } from './MentionInputSuggestionsGroup';
import { MentionInputSuggestionsItem } from './MentionInputSuggestionsItem';
import { MentionInputSuggestionsSeparator } from './MentionInputSuggestionsSeparator';
export const MentionInputSuggestionsItemSelector = ({
item,
onSelect,
addValueToInput,
closeOnSelect,
}: {
item: MentionInputSuggestionsProps['suggestionItems'][number];
onSelect: (params: MentionInputSuggestionsOnSelectParams) => void;
} & GroupOverrideProps) => {
if (item.type === 'separator') {
return <MentionInputSuggestionsSeparator />;
}
if (item.type === 'group') {
return <MentionInputSuggestionsGroup {...item} onSelect={onSelect} />;
}
return (
<MentionInputSuggestionsItem
{...item}
onSelect={onSelect}
addValueToInput={item?.addValueToInput ?? addValueToInput}
closeOnSelect={item?.closeOnSelect ?? closeOnSelect}
/>
);
};
export const MentionInputSuggestionsItemsSelector = ({
suggestionItems,
onSelect,
addValueToInput,
closeOnSelect,
}: {
suggestionItems: MentionInputSuggestionsProps['suggestionItems'];
onSelect: (params: MentionInputSuggestionsOnSelectParams) => void;
} & GroupOverrideProps) => {
if (!suggestionItems) return null;
return suggestionItems.map((item, index) => (
<MentionInputSuggestionsItemSelector
key={keySelector(item, index)}
item={item}
onSelect={onSelect}
addValueToInput={addValueToInput}
closeOnSelect={closeOnSelect}
/>
));
};
const keySelector = (
item: MentionInputSuggestionsProps['suggestionItems'][number],
index: number
) => {
if (item.type === 'separator') return `separator${index}`;
if (item.type === 'group') return `${item.label}${index}`;
return item.value;
};

View File

@ -1,19 +1,19 @@
import { Command } from 'cmdk';
import type React from 'react';
interface BusterInputListProps {
interface MentionInputSuggestionsListProps {
className?: string;
style?: React.CSSProperties;
children: React.ReactNode;
show?: boolean;
}
export const BusterInputList = ({
export const MentionInputSuggestionsList = ({
className,
style,
children,
show = true,
}: BusterInputListProps) => {
}: MentionInputSuggestionsListProps) => {
if (!show) return null;
return (
<Command.List className={className} style={style}>

View File

@ -0,0 +1,43 @@
/** biome-ignore-all lint/complexity/noUselessFragments: Intersting bug when NOT using fragments */
import { Command } from 'cmdk';
import React, { forwardRef } from 'react';
import { MentionInput, type MentionInputProps, type MentionInputRef } from '../MentionInput';
import type { MentionInputSuggestionsProps } from './MentionInputSuggestions.types';
export type MentionInputSuggestionsMentionsInputProps = Pick<
MentionInputSuggestionsProps,
| 'mentions'
| 'value'
| 'placeholder'
| 'defaultValue'
| 'shouldFilter'
| 'filter'
| 'onMentionItemClick'
> & {
onChange: MentionInputProps['onChange'];
onPressEnter: MentionInputProps['onPressEnter'];
className?: string;
style?: React.CSSProperties;
autoFocus?: boolean;
readOnly?: boolean;
commandListNavigatedRef?: React.RefObject<boolean>;
};
export const MentionInputSuggestionsMentionsInput = forwardRef<
MentionInputRef,
MentionInputSuggestionsMentionsInputProps
>(({ value: valueProp, placeholder, defaultValue, mentions, value, ...props }, ref) => {
return (
<React.Fragment>
<MentionInput ref={ref} mentions={mentions} defaultValue={value} {...props} />
<Command.Input
value={value}
autoFocus={false}
className="sr-only hidden h-0 border-0 p-0 pointer-events-none w-full"
aria-hidden="true"
/>
</React.Fragment>
);
});
MentionInputSuggestionsMentionsInput.displayName = 'MentionInputSuggestionsMentionsInput';

View File

@ -2,7 +2,7 @@ import { Command } from 'cmdk';
import type React from 'react';
import { cn } from '@/lib/utils';
export const BusterInputSeparator = ({
export const MentionInputSuggestionsSeparator = ({
children,
...props
}: React.ComponentPropsWithoutRef<typeof Command.Separator>) => {

View File

@ -1,20 +1,23 @@
import type { MentionTriggerItem } from '../MentionInput';
import type { BusterInputDropdownGroup, BusterInputDropdownItem } from './BusterInput.types';
import type {
MentionInputSuggestionsDropdownGroup,
MentionInputSuggestionsDropdownItem,
} from './MentionInputSuggestions.types';
export const createInputItem = <T = string>() => {
return (item: BusterInputDropdownItem<T>) => item;
return (item: MentionInputSuggestionsDropdownItem<T>) => item;
};
export const createInputItems = <T = string>() => {
return (items: BusterInputDropdownItem<T>[]) => items;
return (items: MentionInputSuggestionsDropdownItem<T>[]) => items;
};
export const createInputGroup = <T = string>() => {
return (group: BusterInputDropdownGroup<T>) => group;
return (group: MentionInputSuggestionsDropdownGroup<T>) => group;
};
export const createInputGroups = <T = string>() => {
return (groups: BusterInputDropdownGroup<T>[]) => groups;
return (groups: MentionInputSuggestionsDropdownGroup<T>[]) => groups;
};
export const createInputMention = <T = string>() => {