@particle-academy/react-fancy 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +494 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +198 -3
- package/dist/index.d.ts +198 -3
- package/dist/index.js +489 -2
- package/dist/index.js.map +1 -1
- package/docs/ChatDrawer.md +75 -0
- package/docs/InputTag.md +155 -0
- package/docs/PromptInput.md +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# ChatDrawer
|
|
2
|
+
|
|
3
|
+
A tabbed, collapsible drawer that mounts in `PromptInput`'s `aboveInput` slot so the drawer and composer share one rounded shell. Each tab gets a numbered chip and a content panel; only one panel renders at a time. Slot-driven — you decide what each tab shows.
|
|
4
|
+
|
|
5
|
+
Added in `3.2.0`.
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { ChatDrawer, PromptInput } from "@particle-academy/react-fancy";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Basic Usage
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
const [tab, setTab] = useState("tools");
|
|
17
|
+
const [open, setOpen] = useState(true);
|
|
18
|
+
|
|
19
|
+
<PromptInput
|
|
20
|
+
budgetTokens={200_000}
|
|
21
|
+
placeholder={tab === "deal" ? "Ask about this deal…" : "Type a message…"}
|
|
22
|
+
onSubmit={send}
|
|
23
|
+
aboveInput={
|
|
24
|
+
<ChatDrawer
|
|
25
|
+
tabs={[
|
|
26
|
+
{ id: "files", label: "Files" },
|
|
27
|
+
{ id: "tools", label: "Chat Tools" },
|
|
28
|
+
{ id: "prompts", label: "Chat Prompts" },
|
|
29
|
+
{ id: "deal", label: "IBM Analytics Platform", number: null },
|
|
30
|
+
]}
|
|
31
|
+
activeTabId={tab}
|
|
32
|
+
onTabChange={setTab}
|
|
33
|
+
open={open}
|
|
34
|
+
onToggle={setOpen}
|
|
35
|
+
>
|
|
36
|
+
{tab === "files" && <FilesPanel />}
|
|
37
|
+
{tab === "tools" && <ToolsGrid />}
|
|
38
|
+
{tab === "prompts" && <PromptsList />}
|
|
39
|
+
{tab === "deal" && <DealContext />}
|
|
40
|
+
</ChatDrawer>
|
|
41
|
+
}
|
|
42
|
+
/>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Props
|
|
46
|
+
|
|
47
|
+
| Prop | Type | Default | Description |
|
|
48
|
+
| --------------- | -------------------------- | ------------ | ------------------------------------------------------ |
|
|
49
|
+
| `tabs` | `ChatDrawerTab[]` | — | Ordered list of tabs. |
|
|
50
|
+
| `activeTabId` | `string` | — | Currently selected tab. |
|
|
51
|
+
| `onTabChange` | `(id: string) => void` | — | Fires when a chip is clicked. |
|
|
52
|
+
| `open` | `boolean` | `true` | Body visibility. |
|
|
53
|
+
| `onToggle` | `(open: boolean) => void` | — | Fires when the chevron is clicked. |
|
|
54
|
+
| `children` | `ReactNode` | — | Body content for the active tab. |
|
|
55
|
+
| `minBodyHeight` | `number` | `140` | Min height of the body (px) when open. |
|
|
56
|
+
| `className` | `string` | — | Extra class on the outer container. |
|
|
57
|
+
|
|
58
|
+
### Tab
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
type ChatDrawerTab = {
|
|
62
|
+
id: string;
|
|
63
|
+
label: string;
|
|
64
|
+
/** Override the numbered chip. `null` hides the number entirely
|
|
65
|
+
* (handy for context-specific tabs like "IBM Analytics Platform"). */
|
|
66
|
+
number?: number | null;
|
|
67
|
+
};
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
By default tabs are numbered by position (1, 2, 3, …). Pass `number: null` to suppress.
|
|
71
|
+
|
|
72
|
+
## See Also
|
|
73
|
+
|
|
74
|
+
- [PromptInput](./PromptInput.md) — pairs via the `aboveInput` slot so they share one rounded panel
|
|
75
|
+
- [InputTag](./InputTag.md) — drop into the same composer to add `/` and `@` autocomplete
|
package/docs/InputTag.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# InputTag
|
|
2
|
+
|
|
3
|
+
A trigger-driven autocomplete picker that attaches to *any* text surface via an adapter. Type a configured trigger character (`/`, `@`, `#`, `:`, anything) at a word boundary to open a filtered menu; ↑↓ to move, Enter or Tab to insert, Esc to dismiss.
|
|
4
|
+
|
|
5
|
+
Added in `3.2.0`.
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import {
|
|
11
|
+
InputTag,
|
|
12
|
+
textareaAdapter,
|
|
13
|
+
inputAdapter,
|
|
14
|
+
contentEditableAdapter,
|
|
15
|
+
controlledAdapter,
|
|
16
|
+
} from "@particle-academy/react-fancy";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Basic Usage
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { useRef, useState } from "react";
|
|
23
|
+
import { InputTag, textareaAdapter } from "@particle-academy/react-fancy";
|
|
24
|
+
|
|
25
|
+
const COMMANDS = [
|
|
26
|
+
{ name: "/explain", hint: "explain the selection" },
|
|
27
|
+
{ name: "/rewrite", hint: "rewrite in a different tone" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const MENTIONS = [
|
|
31
|
+
{ id: "ada", name: "Ada", kind: "person" },
|
|
32
|
+
{ id: "readme", name: "README.md", kind: "file" },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function ChatInput() {
|
|
36
|
+
const [text, setText] = useState("");
|
|
37
|
+
const ref = useRef<HTMLTextAreaElement>(null);
|
|
38
|
+
const adapter = useMemo(() => textareaAdapter(ref), []);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<textarea ref={ref} value={text} onChange={(e) => setText(e.target.value)} />
|
|
43
|
+
<InputTag
|
|
44
|
+
adapter={adapter}
|
|
45
|
+
triggers={{
|
|
46
|
+
"/": { items: COMMANDS, insert: (c) => `${c.name} ` },
|
|
47
|
+
"@": { items: MENTIONS, insert: (m) => `@${m.name} ` },
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The component renders nothing until a trigger is active. When the trigger fires, a floating menu anchored to the input shows filtered items. Picking inserts the configured replacement and closes the menu.
|
|
56
|
+
|
|
57
|
+
## Triggers
|
|
58
|
+
|
|
59
|
+
Each trigger character maps to a config:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
{
|
|
63
|
+
"/": {
|
|
64
|
+
items: COMMANDS,
|
|
65
|
+
insert: (item, query) => `${item.name} `, // replaces "/<query>" with this
|
|
66
|
+
filter?: (item, query) => boolean, // default: prefix match against name/id
|
|
67
|
+
render?: (item, active) => ReactNode, // default: item.name | item.id
|
|
68
|
+
keyOf?: (item) => string, // default: same as render fallback
|
|
69
|
+
label?: "Commands", // optional header
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`insert` receives the matched item and the current query (text typed after the trigger character).
|
|
75
|
+
|
|
76
|
+
`filter` defaults to a case-insensitive substring check against `keyOf(item)`. Pass `() => true` to bypass filtering — handy when `items` is being driven by an async source.
|
|
77
|
+
|
|
78
|
+
`render` lets you customize each row. The second arg is `active` (current keyboard cursor).
|
|
79
|
+
|
|
80
|
+
## Adapters
|
|
81
|
+
|
|
82
|
+
`<InputTag>` is surface-agnostic. The component calls into an adapter for read, write, anchor positioning, and key interception. Four built-in adapters cover the DOM cases:
|
|
83
|
+
|
|
84
|
+
| Adapter | Surface |
|
|
85
|
+
| ------------------------------------ | ------------------------------------ |
|
|
86
|
+
| `textareaAdapter(ref)` | `<textarea>` |
|
|
87
|
+
| `inputAdapter(ref)` | `<input>` |
|
|
88
|
+
| `contentEditableAdapter(ref)` | any `contenteditable` element |
|
|
89
|
+
| `controlledAdapter({ anchorRef, onReplaceRange })` | hosts that fully own text state |
|
|
90
|
+
|
|
91
|
+
Non-DOM surfaces (code editors, sheet cell editors, whiteboard sticky notes) ship adapters from their own packages — e.g. `<CodeEditorInputTag>` from `@particle-academy/fancy-code`. For ad-hoc cases, write your own (~30 lines) against the contract:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
type InputTagAdapter = {
|
|
95
|
+
subscribe: (fn: (state: { text: string; caretIndex: number }) => void) => () => void;
|
|
96
|
+
replaceRange: (start: number, end: number, replacement: string) => void;
|
|
97
|
+
getAnchorRect: () => DOMRect | null;
|
|
98
|
+
onKey: (handler: (e: KeyboardEvent) => boolean) => () => void;
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The DOM adapters use React's native value setter so the consumer's controlled `onChange` fires correctly when the picker writes back — no state-sync gotchas.
|
|
103
|
+
|
|
104
|
+
## Props
|
|
105
|
+
|
|
106
|
+
| Prop | Type | Default | Description |
|
|
107
|
+
| ------------- | ----------------------------- | ---------------- | ------------------------------------------- |
|
|
108
|
+
| `adapter` | `InputTagAdapter` | — | Surface adapter. |
|
|
109
|
+
| `triggers` | `Record<string, …>` | — | Per-trigger-char config (see above). |
|
|
110
|
+
| `maxItems` | `number` | `8` | Max rows shown. |
|
|
111
|
+
| `placement` | `"bottom-left"` \| `"bottom-right"` \| `"top-left"` \| `"top-right"` | `"bottom-left"` | Anchor position relative to surface. |
|
|
112
|
+
| `className` | `string` | — | Class on the popover container. |
|
|
113
|
+
| `style` | `CSSProperties` | — | Inline style on the popover container. |
|
|
114
|
+
| `onPick` | `(info) => void` | — | Fires after each insertion. |
|
|
115
|
+
|
|
116
|
+
## Trigger Detection
|
|
117
|
+
|
|
118
|
+
A trigger is active when one of the configured characters appears between the caret and the nearest preceding whitespace (or start of text), with no other non-trigger / non-whitespace breaks. The "query" is everything between the trigger character and the caret.
|
|
119
|
+
|
|
120
|
+
Examples:
|
|
121
|
+
|
|
122
|
+
- `hello /expl|` → query is `expl`, opens `/` picker
|
|
123
|
+
- `email me at user@gma|` → query is `gma`, opens `@` picker
|
|
124
|
+
- `path/to/fi|` → no trigger (caret is in a word that's not preceded by whitespace)
|
|
125
|
+
- `#urgent #wo|nt` → opens `#` picker with query `wo`
|
|
126
|
+
|
|
127
|
+
## Custom Surfaces
|
|
128
|
+
|
|
129
|
+
For non-DOM surfaces, write an adapter. Minimal `controlledAdapter` example for a host that owns text state:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
const anchorRef = useRef<HTMLDivElement>(null);
|
|
133
|
+
|
|
134
|
+
const adapter = useMemo(
|
|
135
|
+
() =>
|
|
136
|
+
controlledAdapter({
|
|
137
|
+
anchorRef,
|
|
138
|
+
onReplaceRange: (start, end, replacement) => {
|
|
139
|
+
setText((cur) => cur.slice(0, start) + replacement + cur.slice(end));
|
|
140
|
+
setCaret(start + replacement.length);
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
[],
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// host pushes updates whenever text/caret changes
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
adapter.notify({ text, caretIndex: caret });
|
|
149
|
+
}, [text, caret]);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## See Also
|
|
153
|
+
|
|
154
|
+
- [PromptInput](./PromptInput.md) — uses similar trigger logic internally; use `InputTag` when you want the picker without the rest of the composer
|
|
155
|
+
- [ChatDrawer](./ChatDrawer.md) — composes with `PromptInput`'s `aboveInput` slot for tabbed-tools drawer UX
|
package/docs/PromptInput.md
CHANGED
|
@@ -50,6 +50,7 @@ import { PromptInput } from "@particle-academy/react-fancy";
|
|
|
50
50
|
| charsPerToken | `number` | `4` | Rough estimator for the token meter. |
|
|
51
51
|
| mentionColor | `Record<string, string>` | sensible defaults | Override the chip colour per mention kind. |
|
|
52
52
|
| maxHeight | `number` | `280` | Max textarea height in pixels. |
|
|
53
|
+
| aboveInput | `ReactNode` | — | Rendered inside the rounded shell, above the textarea. Use this for a drawer of tools/files/prompts/etc. so the drawer and composer share one visual panel — see [ChatDrawer](./ChatDrawer.md). Added in `3.2.0`. |
|
|
53
54
|
|
|
54
55
|
## Types
|
|
55
56
|
|