@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.
@@ -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
@@ -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
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-academy/react-fancy",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "React UI component library — React port of the fancy-flux Blade component library",
5
5
  "repository": {
6
6
  "type": "git",