# SilkeCommandTextField

A rich text input component that supports inline commands (mentions and actions) with autocomplete suggestions, and file attachments via paste or drag-and-drop. Perfect for chat interfaces, command palettes, or any input that needs to reference users, actions, or other entities.

## Features

- **Mentions (`@`)**: Reference users, entities, or any mentionable items
- **Actions (`/`)**: Trigger commands or actions
- **Autocomplete**: Show suggestions as users type
- **File attachments**: Attach images, PDF, Word, CSV, Excel, TXT, or any file type via paste, drag-and-drop, or programmatically via `openFilePicker()`
- **Configurable file types**: Control which file categories are accepted with `acceptedFileTypes`
- **Keyboard navigation**: Navigate and select suggestions with keyboard
- **Placeholder support**: Show placeholder text when empty
- **Disabled state**: Prevent editing when needed
- **XSS protection**: Safe HTML rendering of user input

## Basic Usage

```js
const [value, setValue] = React.useState([]);
const availableCommands = [
  { type: SilkeCommandType.MENTION, value: 'john' },
  { type: SilkeCommandType.MENTION, value: 'jane' },
  { type: SilkeCommandType.ACTION, value: 'help' },
];

<SilkeCommandTextField
  availableCommands={availableCommands}
  onChange={setValue}
  placeholder="Type @ to mention someone or / for commands..."
  value={value}
/>;
```

## With Autocomplete Suggestions

```js
const [value, setValue] = React.useState([]);
const ref = React.useRef(null);
const [suggestions, setSuggestions] = React.useState([]);

const availableCommands = [
  { type: SilkeCommandType.MENTION, value: 'alice' },
  { type: SilkeCommandType.MENTION, value: 'bob' },
  { type: SilkeCommandType.MENTION, value: 'charlie' },
  { type: SilkeCommandType.ACTION, value: 'help' },
  { type: SilkeCommandType.ACTION, value: 'search' },
  { type: SilkeCommandType.ACTION, value: 'settings' },
];

<SilkeBox column gap="s">
  <SilkeCommandTextField
    autoFocus
    availableCommands={availableCommands}
    onChange={setValue}
    onKeyDown={(e) => {
      if (e.key === 'Enter' && suggestions.length > 0) {
        e.preventDefault();
        ref.current?.update(suggestions[0]);
      }
    }}
    onSuggestions={setSuggestions}
    placeholder="Type @ or / to see suggestions..."
    ref={ref}
    value={value}
  />
  {suggestions.length > 0 && (
    <SilkeBox
      column
      gap="xs"
      style={{ background: 'var(--color-neutral-10)', padding: 8, borderRadius: 4 }}
    >
      <SilkeText size="s" weight="medium">
        Suggestions:
      </SilkeText>
      {suggestions.map((suggestion) => (
        <SilkeBox
          key={suggestion.type + suggestion.value}
          onClick={() => ref.current?.update(suggestion)}
          style={{ cursor: 'pointer', padding: '4px 8px' }}
        >
          <SilkeText>
            {suggestion.type}
            {suggestion.value}
          </SilkeText>
        </SilkeBox>
      ))}
    </SilkeBox>
  )}
</SilkeBox>;
```

## With Command Focus Tracking

Track which command the user is currently editing:

```js
const [value, setValue] = React.useState([]);
const [activeIndex, setActiveIndex] = React.useState(0);
const [activeCommand, setActiveCommand] = React.useState(null);

const availableCommands = [
  { type: SilkeCommandType.MENTION, value: 'user' },
  { type: SilkeCommandType.ACTION, value: 'run' },
];

<SilkeBox column gap="s">
  <SilkeCommandTextField
    availableCommands={availableCommands}
    onChange={setValue}
    onCommandFocus={(index, command) => {
      setActiveIndex(index);
      setActiveCommand(command);
    }}
    placeholder="Type something with @mentions..."
    value={value}
  />
  <SilkeBox gap="m">
    <SilkeText size="s">Active index: {activeIndex}</SilkeText>
    <SilkeText size="s">Active type: {activeCommand?.type || 'text'}</SilkeText>
  </SilkeBox>
</SilkeBox>;
```

## Pre-populated Value

Start with existing commands:

```js
const [value, setValue] = React.useState([
  { type: 'text', value: 'Hello ' },
  { type: SilkeCommandType.MENTION, value: 'alice', partial: false },
  { type: 'text', value: ' please ' },
  { type: SilkeCommandType.ACTION, value: 'review', partial: false },
  { type: 'text', value: ' this PR' },
]);

const availableCommands = [
  { type: SilkeCommandType.MENTION, value: 'alice' },
  { type: SilkeCommandType.ACTION, value: 'review' },
];

<SilkeCommandTextField availableCommands={availableCommands} onChange={setValue} value={value} />;
```

## With File Attachments

Accept images and document files via paste, drag-and-drop, or programmatically via the `openFilePicker()` ref method:

```js
const [value, setValue] = React.useState([]);
const ref = React.useRef(null);

const availableCommands = [
  { type: SilkeCommandType.MENTION, value: 'alice' },
  { type: SilkeCommandType.ACTION, value: 'analyze' },
];

<SilkeBox column gap="s">
  <SilkeCommandTextField
    ref={ref}
    acceptedFileTypes={['image', 'text']}
    availableCommands={availableCommands}
    onChange={setValue}
    placeholder="Type a message or paste/drop files..."
    value={value}
  >
    <SilkeButton
      kind="ghost"
      size="s"
      icon="add"
      onClick={() => ref.current?.openFilePicker()}
      aria-label="Add file"
    />
  </SilkeCommandTextField>
</SilkeBox>;
```

All attachments are displayed as horizontally scrollable chips. Image chips show a small thumbnail (click to preview full-size). File chips show an icon and filename. Each chip has a remove button. Use `openFilePicker()` via the ref to open the system file picker from an external button.

By default, no file attachments are accepted. You must explicitly provide `acceptedFileTypes` to enable them.

### Images only

```js
const [value, setValue] = React.useState([]);

<SilkeCommandTextField
  acceptedFileTypes={['image']}
  availableCommands={[]}
  onChange={setValue}
  value={value}
/>
```

### Documents only (no images)

```js
const [value, setValue] = React.useState([]);

<SilkeCommandTextField
  acceptedFileTypes={['text']}
  availableCommands={[]}
  onChange={setValue}
  value={value}
/>
```

## Disabled State

```js
const [value, setValue] = React.useState([{ type: 'text', value: 'This field is disabled' }]);

<SilkeCommandTextField availableCommands={[]} disabled onChange={setValue} value={value} />;
```

## Using the Ref Handle

Access component methods via ref:

```js
const ref = React.useRef(null);
const [value, setValue] = React.useState([]);

const availableCommands = [{ type: SilkeCommandType.MENTION, value: 'team' }];

<SilkeBox column gap="s">
  <SilkeCommandTextField
    availableCommands={availableCommands}
    onChange={setValue}
    placeholder="Use the buttons below..."
    ref={ref}
    value={value}
  />
  <SilkeBox gap="s">
    <SilkeButton label="Focus" onClick={() => ref.current?.focus()} />
    <SilkeButton label="Blur" onClick={() => ref.current?.blur()} />
    <SilkeButton
      label="Insert @team"
      onClick={() =>
        ref.current?.update({ type: SilkeCommandType.MENTION, value: 'team', partial: false })
      }
    />
    <SilkeButton label="Get as String" onClick={() => alert(ref.current?.toString())} />
  </SilkeBox>
</SilkeBox>;
```

## Full Interactive Example

```js
const [value, setValue] = React.useState([]);
const ref = React.useRef(null);
const [activeIndex, setActiveIndex] = React.useState(0);
const [suggestions, setSuggestions] = React.useState([]);

const availableCommands = [
  { type: SilkeCommandType.MENTION, value: 'alice' },
  { type: SilkeCommandType.MENTION, value: 'bob' },
  { type: SilkeCommandType.MENTION, value: 'charlie' },
  { type: SilkeCommandType.ACTION, value: 'help' },
  { type: SilkeCommandType.ACTION, value: 'search' },
  { type: SilkeCommandType.ACTION, value: 'clear' },
];

<SilkeBox column gap="s">
  <SilkeText weight="medium">Active index: {activeIndex}</SilkeText>
  <SilkeCommandTextField
    autoFocus
    availableCommands={availableCommands}
    onChange={setValue}
    onCommandFocus={setActiveIndex}
    onKeyDown={(e) => {
      if (e.key === 'Enter' && suggestions.length > 0) {
        e.preventDefault();
        ref.current?.update(suggestions[0]);
      }
    }}
    onSuggestions={setSuggestions}
    placeholder="Type @ to mention or / for actions..."
    ref={ref}
    value={value}
  />
  {suggestions.length > 0 && (
    <SilkeBox column gap="xs">
      <SilkeText size="s" weight="medium">
        Suggestions (press Enter to select first):
      </SilkeText>
      {suggestions.map((suggestion) => (
        <SilkeBox
          key={suggestion.type + suggestion.value}
          onClick={() => ref.current?.update(suggestion)}
          style={{
            cursor: 'pointer',
            padding: '4px 8px',
            background: 'var(--color-neutral-10)',
            borderRadius: 4,
          }}
        >
          <SilkeText>
            {suggestion.type}
            {suggestion.value}
          </SilkeText>
        </SilkeBox>
      ))}
    </SilkeBox>
  )}
  <SilkeBox column gap="xs">
    <SilkeText size="s" weight="medium">
      Output as string:
    </SilkeText>
    <SilkeCodeSnippet code={ref.current?.toString() || ''} />
  </SilkeBox>
  <SilkeBox column gap="xs">
    <SilkeText size="s" weight="medium">
      Value structure:
    </SilkeText>
    <SilkeCodeSnippet code={JSON.stringify(value, null, 2)} />
  </SilkeBox>
</SilkeBox>;
```

## Props

| Prop                | Type                                                            | Default      | Description                                                                          |
| ------------------- | --------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------ |
| `availableCommands` | `SilkeCommand[]`                                                | Required     | List of valid commands for autocomplete and validation                                |
| `value`             | `SilkeCommandTextFieldValue[]`                                  | Required     | Current value of the field                                                           |
| `onChange`          | `(value: SilkeCommandTextFieldValue[]) => void`                 | Required     | Called when the value changes                                                        |
| `acceptedFileTypes` | `SilkeFileCategory[]`                                           | `[]`         | File categories to accept: `'image'` and/or `'text'`                                 |
| `onSuggestions`     | `(suggestions: SilkeCommand[]) => void`                         | -            | Called with matching suggestions when typing a partial command                        |
| `onCommandFocus`    | `(index: number, command?: SilkeCommandTextFieldValue) => void` | -            | Called when the active command changes                                                |
| `onKeyDown`         | `(e: React.KeyboardEvent) => void`                              | -            | Called on key down events                                                            |
| `placeholder`       | `string`                                                        | -            | Placeholder text shown when empty                                                    |
| `disabled`          | `boolean`                                                       | `false`      | Disable editing                                                                      |
| `autoFocus`         | `boolean`                                                       | `false`      | Focus on mount                                                                       |

## Ref Methods

| Method                    | Type                                               | Description                                                                                |
| ------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `focus()`                 | `() => void`                                       | Focus the input                                                                            |
| `blur()`                  | `() => void`                                       | Remove focus from the input                                                                |
| `openFilePicker()`        | `() => void`                                       | Open the system file picker (requires `acceptedFileTypes` to be set)                       |
| `update(command, index?)` | `(command: SilkeCommand, index?: number) => void`  | Insert or update a command at the given index (defaults to active index, use -1 to append) |
| `toString(value?)`        | `(value?: SilkeCommandTextFieldValue[]) => string` | Convert value to plain string                                                              |

## Types

```typescript
enum SilkeCommandType {
  ACTION = '/',
  MENTION = '@',
}

type SilkeCommand = {
  partial?: boolean;
  type: SilkeCommandType;
  value: string;
};

type SilkeTextValue = {
  type: 'text';
  value: string;
};

type SilkeImageValue = {
  type: 'image';
  /** Base64-encoded image data */
  data: string;
  /** MIME type (e.g., 'image/png', 'image/jpeg') */
  mimeType: string;
  /** Optional filename */
  name?: string;
};

type SilkeFileValue = {
  type: 'file';
  /** Base64-encoded file data */
  data: string;
  /** MIME type (e.g., 'text/csv', 'application/vnd.ms-excel') */
  mimeType: string;
  /** Filename */
  name: string;
};

type SilkeCommandTextFieldValue = SilkeCommand | SilkeTextValue | SilkeImageValue | SilkeFileValue;
```

### File categories

The `acceptedFileTypes` prop accepts an array of category strings:

| Category | Accepted file types |
| -------- | ------------------- |
| `'image'` | All image types (PNG, JPEG, GIF, WebP, SVG, etc.) |
| `'text'` | PDF, Word (.doc/.docx), Excel (.xls/.xlsx), PowerPoint (.ppt/.pptx), CSV, plain text, Markdown, HTML, JSON |
