anky-ui 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Ankush Patil
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,473 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # Anky UI
2
+
3
+ [![npm](https://img.shields.io/npm/v/@erankushpatil/anky-ui?label=%40erankushpatil%2Fanky-ui)](https://www.npmjs.com/package/@erankushpatil/anky-ui)
4
+ [![npm cli](https://img.shields.io/npm/v/anky-ui?label=anky-ui%20cli)](https://www.npmjs.com/package/anky-ui)
5
+ [![license](https://img.shields.io/badge/license-ISC-blue.svg)](./LICENSE)
6
+ [![types](https://img.shields.io/badge/types-TypeScript-3178c6.svg)](#api-reference)
7
+
8
+ **Anky UI is an AI UI library for React: ambient, inline AI primitives.** Select text to open a floating AI action palette, stream a rewrite into a live inline diff, accept or reject it, get Copilot-style ghost text autocomplete in a textarea, and undo/redo every AI change.
9
+
10
+ Headless by default, Tailwind-ready, zero runtime dependencies besides `@floating-ui/react`. Bring your own model endpoint. Works with Next.js (App Router and Pages), Vite and any React 18/19 app.
11
+
12
+ - Floating AI action palette anchored to the user's text selection (Cursor-like inline prompt)
13
+ - React inline diff with streaming tokens, word or character mode
14
+ - Ghost text autocomplete textarea with Tab / → to accept
15
+ - Optimistic AI state with rollback, undo/redo history and accept/reject
16
+ - Copy-paste distribution via CLI in the shadcn style: `npx anky-ui add ...`
17
+
18
+ ## Contents
19
+
20
+ - [Install](#install)
21
+ - [Quick start](#quick-start)
22
+ - [Components](#components)
23
+ - [SelectionPalette](#selectionpalette)
24
+ - [InlineDiff](#inlinediff)
25
+ - [GhostInput](#ghostinput)
26
+ - [Hooks and utilities](#hooks-and-utilities)
27
+ - [useOptimisticAIState](#useoptimisticaistate)
28
+ - [useSelectionAnchor](#useselectionanchor)
29
+ - [computeDiff](#computediff)
30
+ - [streamTextReader](#streamtextreader)
31
+ - [Provider streaming recipes](#provider-streaming-recipes)
32
+ - [Editor adapters](#editor-adapters)
33
+ - [Accessibility](#accessibility)
34
+ - [Styling and Tailwind](#styling-and-tailwind)
35
+ - [CLI](#cli)
36
+ - [Server and Next.js notes](#server-and-nextjs-notes)
37
+ - [Playground](#playground)
38
+ - [Development](#development)
39
+ - [Contributing](#contributing)
40
+ - [FAQ](#faq)
41
+ - [Keywords](#keywords)
42
+ - [License](#license)
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ npm install @erankushpatil/anky-ui
48
+ ```
49
+
50
+ Requires React and React DOM 18 or 19. `@floating-ui/react` is installed as a dependency.
51
+
52
+ Prefer to own the source? Copy the components into your project instead:
53
+
54
+ ```bash
55
+ npx anky-ui add selection-palette
56
+ npx anky-ui add inline-diff
57
+ npx anky-ui add ghost-input
58
+ ```
59
+
60
+ ## Quick start
61
+
62
+ Highlight text, pick an action, watch the rewrite stream in, then accept or reject it.
63
+
64
+ ```tsx
65
+ "use client";
66
+
67
+ import {
68
+ InlineDiff,
69
+ SelectionPalette,
70
+ useOptimisticAIState,
71
+ } from "@erankushpatil/anky-ui";
72
+
73
+ export function Editor() {
74
+ const ai = useOptimisticAIState({ initialState: { original: "", modified: "" } });
75
+
76
+ function run(prompt: string, selectedText: string) {
77
+ void ai.mutateWithStream(
78
+ (signal) =>
79
+ fetch("/api/rewrite", {
80
+ method: "POST",
81
+ headers: { "Content-Type": "application/json" },
82
+ body: JSON.stringify({ prompt, text: selectedText }),
83
+ signal,
84
+ }),
85
+ (state, chunk) => ({ original: selectedText, modified: state.modified + chunk }),
86
+ );
87
+ }
88
+
89
+ return (
90
+ <>
91
+ <article contentEditable suppressContentEditableWarning>
92
+ Select any sentence in this paragraph to try the AI palette.
93
+ </article>
94
+
95
+ <SelectionPalette onSelectPrompt={run} />
96
+
97
+ {ai.currentState.original && (
98
+ <InlineDiff
99
+ originalText={ai.currentState.original}
100
+ modifiedText={ai.currentState.modified}
101
+ isStreaming={ai.isStreaming}
102
+ onAccept={ai.accept}
103
+ onReject={ai.reject}
104
+ />
105
+ )}
106
+ </>
107
+ );
108
+ }
109
+ ```
110
+
111
+ The endpoint can return raw UTF-8 text or Server-Sent Events whose `data:` fields hold text. Convert provider-specific JSON (OpenAI, Anthropic, ...) to text on your server.
112
+
113
+ ## Components
114
+
115
+ ### SelectionPalette
116
+
117
+ A floating bar anchored to the highlighted text, built on `@floating-ui/react`. It flips and clamps to the viewport, follows scrolling overflow parents, and hides when the selection scrolls out of view.
118
+
119
+ ```tsx
120
+ <SelectionPalette
121
+ actions={[
122
+ { label: "Fix Grammar", prompt: "Fix the grammar of the following text." },
123
+ { label: "Shorten", prompt: "Make this shorter.", icon: "↘" },
124
+ ]}
125
+ onSelectPrompt={(prompt, selectedText) => runAI(prompt, selectedText)}
126
+ customInputPlaceholder="Ask AI to edit…"
127
+ containerRef={editorRef}
128
+ />
129
+ ```
130
+
131
+ | Prop | Type | Default | Description |
132
+ | --- | --- | --- | --- |
133
+ | `onSelectPrompt` | `(prompt: string, selectedText: string) => void` | required | Called with a preset prompt or the freeform input, and the highlighted text. |
134
+ | `actions` | `Array<{ label: string; icon?: ReactNode; prompt: string }>` | Fix Grammar, Shorten, Translate | Preset pills. Pass `[]` for none. |
135
+ | `customInputPlaceholder` | `string` | `"Ask AI to edit…"` | Placeholder of the freeform input. |
136
+ | `children` | `ReactNode` | none | Extra content at the end of the bar. |
137
+ | `containerRef` | `RefObject<HTMLElement \| null>` | `document.body` | Only selections inside this element open the palette. |
138
+ | `minChars` | `number` | `1` | Minimum selected characters (whitespace-trimmed). |
139
+ | `enabled` | `boolean` | `true` | Disable to detach all listeners. |
140
+ | `placement` | floating-ui `Placement` | `"top"` | Preferred side. |
141
+ | `sideOffset` | `number` | `8` | Gap to the selection, in px. |
142
+ | `closeOnSelect` | `boolean` | `true` | Close and restore the selection after a prompt is chosen. |
143
+ | `classNames` | `{ root?, actions?, action?, actionIcon?, input? }` | none | Extra classes per slot. |
144
+ | `aria-label` | `string` | `"AI actions"` | Accessible name of the toolbar. |
145
+
146
+ Keyboard: `Enter` in the input submits (ignored during IME composition). `Escape` closes the palette and returns focus and the selection to the editor. Arrow keys, `Home` and `End` move between pills.
147
+
148
+ Semantic classes: `anky-ui-palette-root`, `anky-ui-palette-actions`, `anky-ui-palette-action`, `anky-ui-palette-action-icon`, `anky-ui-palette-input`. Use `StyledSelectionPalette` for the Tailwind preset.
149
+
150
+ ### InlineDiff
151
+
152
+ Live word or character comparison between the original and a streaming rewrite, with Accept and Reject controls.
153
+
154
+ ```tsx
155
+ <InlineDiff
156
+ originalText={original}
157
+ modifiedText={modified}
158
+ mode="word"
159
+ isStreaming={isStreaming}
160
+ onAccept={() => commit(modified)}
161
+ onReject={() => rollback()}
162
+ />
163
+ ```
164
+
165
+ | Prop | Type | Default | Description |
166
+ | --- | --- | --- | --- |
167
+ | `originalText` | `string` | required | Text before the edit. |
168
+ | `modifiedText` | `string` | required | Text after the edit (may be partial while streaming). |
169
+ | `onAccept` | `() => void` | required | Accept. Triggered by the button or `Cmd/Ctrl+Enter`. Disabled while streaming. |
170
+ | `onReject` | `() => void` | required | Reject. Triggered by the button or `Escape`. |
171
+ | `mode` | `"word" \| "char"` | `"word"` | Diff granularity. |
172
+ | `isStreaming` | `boolean` | `false` | Pulses the diff, disables Accept, and shows the not-yet-rewritten tail as pending. |
173
+ | `toolbar` | `"floating" \| "inline" \| "none"` | `"floating"` | Where Accept / Reject render. |
174
+ | `toolbarPlacement` | floating-ui `Placement` | `"bottom-start"` | Floating toolbar placement. |
175
+ | `shortcuts` | `boolean` | `true` | Global `Cmd/Ctrl+Enter` and `Escape`. Ignored while typing in another field. |
176
+ | `unstyled` | `boolean` | `false` | Drop the built-in red and green colours. |
177
+ | `classNames` | `{ root?, delete?, insert?, pending?, toolbar?, action?, accept?, reject?, kbd? }` | none | Extra classes per slot. |
178
+ | `acceptLabel`, `rejectLabel` | `ReactNode` | `"Accept"`, `"Reject"` | Button labels. |
179
+
180
+ Deletions render as `<del class="anky-ui-diff-delete">` (red, strikethrough) and insertions as `<ins class="anky-ui-diff-insert">` (green). Theme with CSS variables `--anky-ui-diff-delete-bg`, `--anky-ui-diff-delete-fg`, `--anky-ui-diff-insert-bg` and `--anky-ui-diff-insert-fg`. The pulse uses the Web Animations API and respects `prefers-reduced-motion`. Use `StyledInlineDiff` for the Tailwind toolbar preset.
181
+
182
+ ### GhostInput
183
+
184
+ Copilot-style inline autocomplete. A bottom layer renders your text plus a faint ghost suggestion (`#9CA3AF`); a transparent `<textarea>` on top handles input, caret and selection.
185
+
186
+ ```tsx
187
+ const [value, setValue] = useState("");
188
+
189
+ <GhostInput
190
+ value={value}
191
+ onChange={setValue}
192
+ debounceMs={300}
193
+ placeholder="Start typing…"
194
+ className="rounded-lg border p-3 font-sans text-sm leading-6"
195
+ fetchSuggestion={async (current, { signal }) => {
196
+ const res = await fetch("/api/complete", {
197
+ method: "POST",
198
+ body: JSON.stringify({ text: current }),
199
+ signal,
200
+ });
201
+ return res.text(); // only the continuation, not the full text
202
+ }}
203
+ />;
204
+ ```
205
+
206
+ | Prop | Type | Default | Description |
207
+ | --- | --- | --- | --- |
208
+ | `value` | `string` | required | Controlled value. |
209
+ | `onChange` | `(value: string) => void` | required | Called on every edit and when a suggestion is accepted. |
210
+ | `fetchSuggestion` | `(current: string, options: { signal: AbortSignal }) => Promise<string>` | required | Return the text to append. Return `""` for none. |
211
+ | `debounceMs` | `number` | `300` | Idle time after the last keystroke before fetching. |
212
+ | `onSuggestionError` | `(error: unknown) => void` | none | Fetch failures never surface in the UI. |
213
+ | `onAcceptSuggestion` | `(suggestion: string) => void` | none | Called after Tab or → accepts. |
214
+ | `className`, `style` | | none | Applied to the root, which owns font, line-height, letter-spacing, padding, border and colour. Both layers inherit from it so they always line up. |
215
+
216
+ All native `<textarea>` props (such as `rows`, `placeholder`, `disabled`) are forwarded. A `ref` exposes `{ textarea, acceptSuggestion(), dismissSuggestion() }`.
217
+
218
+ Keyboard: `Tab` or `ArrowRight` with the caret at the end accepts. `Escape` clears the suggestion. Typing that matches the suggestion keeps the rest visible; anything else invalidates it. Stale responses are discarded. Override the colour with `--anky-ui-ghost-color`.
219
+
220
+ ## Hooks and utilities
221
+
222
+ ### useOptimisticAIState
223
+
224
+ Optimistic state for streamed AI output with snapshot rollback and a 20-entry undo/redo history.
225
+
226
+ ```tsx
227
+ const ai = useOptimisticAIState({
228
+ initialState: { text: "" },
229
+ endpoint: "/api/rewrite", // optional, used by streamFromEndpoint
230
+ onStateChange: (state) => save(state),
231
+ });
232
+
233
+ await ai.mutateWithStream(
234
+ (signal) => fetch("/api/rewrite", { method: "POST", signal }),
235
+ (state, chunk) => ({ text: state.text + chunk }),
236
+ );
237
+ ```
238
+
239
+ | Member | Description |
240
+ | --- | --- |
241
+ | `currentState` | Optimistic state, including partially streamed output. |
242
+ | `confirmedState` | Last state committed with `accept()`. |
243
+ | `history` | `{ past, future }` undo and redo stacks, 20 entries each. |
244
+ | `isStreaming`, `error` | Stream status and the last failure or user abort. |
245
+ | `canUndo`, `canRedo` | Whether history navigation is possible. |
246
+ | `mutateWithStream(source, transformFn, options?)` | Captures an atomic snapshot, sets `isStreaming`, folds each token in with `transformFn`. On error or abort it rolls back to the snapshot and sets `error`. Returns an outcome and never rejects. |
247
+ | `streamFromEndpoint(body, transformFn, options?)` | POSTs JSON to `endpoint` and streams the response through `mutateWithStream`. |
248
+ | `abort()` | Cancel the in-flight stream and roll back. |
249
+ | `undo()`, `redo()` | Move through history. While streaming, `undo()` cancels the stream. |
250
+ | `accept()` | Commit `currentState` as the new baseline and clear history. |
251
+ | `reject()` | Revert to `confirmedState` (undoable). |
252
+ | `update(next)` | Synchronous, undoable change, such as a manual user edit. |
253
+
254
+ Concurrency: the last call wins. Starting a stream while another is running cancels the older one, rolls it back, and builds the new one on the same pre-mutation snapshot. Stale streams can never write state. `source` accepts a `Response`, a `ReadableStream`, a promise of either, or a factory `(signal) => ...` so the hook can abort the underlying `fetch`.
255
+
256
+ ### useSelectionAnchor
257
+
258
+ The primitive behind `SelectionPalette`. It returns a floating-ui virtual element for the current text selection.
259
+
260
+ ```tsx
261
+ const { isOpen, selectedText, virtualElement, close, restoreSelection } = useSelectionAnchor({
262
+ minChars: 3,
263
+ containerRef: editorRef,
264
+ });
265
+ ```
266
+
267
+ | Option | Default | Description |
268
+ | --- | --- | --- |
269
+ | `enabled` | `true` | Detach all listeners and reset state when `false`. |
270
+ | `minChars` | `1` | Minimum whitespace-trimmed selection length. |
271
+ | `containerRef` | `document.body` | Only selections inside this element are tracked. |
272
+ | `floatingRef` | none | Your floating element. Focusing or clicking inside it keeps the anchor open. |
273
+
274
+ Behaviour: `selectionchange` is debounced by 100 ms so dragging doesn't flicker; collapsed or too-short selections are ignored; both `mouseup` and `touchend` are handled without double-firing; the rectangle is recalculated on scroll and the anchor hides when the selection leaves the viewport. The virtual element is created only inside effects, so it is safe for server rendering and hydration. `restoreSelection()` re-selects the original text using saved range offsets, for example after an AI run re-rendered it, and returns `false` if the text can't be found.
275
+
276
+ ### computeDiff
277
+
278
+ ```ts
279
+ import { computeDiff, type DiffSegment } from "@erankushpatil/anky-ui";
280
+
281
+ const segments: DiffSegment[] = computeDiff("The quick fox", "The swift fox", "word");
282
+ // [{ type: "equal", value: "The " }, { type: "delete", value: "quick" },
283
+ // { type: "insert", value: "swift" }, { type: "equal", value: " fox" }]
284
+ ```
285
+
286
+ `DiffSegment = { type: "equal" | "insert" | "delete"; value: string }`. Zero-dependency Myers diff with `"word"` and `"char"` modes, a work budget that bounds worst-case latency and memory (pass `{ maxCost }` as the fourth argument), and no module-level state.
287
+
288
+ ### streamTextReader
289
+
290
+ ```ts
291
+ import { streamTextReader } from "@erankushpatil/anky-ui";
292
+
293
+ const controller = new AbortController();
294
+ for await (const token of streamTextReader(await fetch(url), { signal: controller.signal })) {
295
+ output += token;
296
+ }
297
+ ```
298
+
299
+ Accepts a `ReadableStream<Uint8Array>` or a `Response`. Decodes with `TextDecoderStream` or `TextDecoder`, handles multi-byte characters split across chunks, strips `data:` prefixes automatically for SSE, passes raw text through unchanged, and ends on `[DONE]`. Options: `signal`, `framing` (`"auto"`, `"sse"` or `"raw"`), `doneSentinel`. On abort it cancels the source and rejects with `signal.reason`.
300
+
301
+ ## Provider streaming recipes
302
+
303
+ `providerTextReader` and `providerTextStream` decode vendor wire formats into plain text tokens, so you can point the hooks straight at a provider response.
304
+
305
+ ```ts
306
+ import { providerTextStream, useOptimisticAIState } from "@erankushpatil/anky-ui";
307
+
308
+ const ai = useOptimisticAIState({ initialState: "" });
309
+ ai.mutateWithStream(
310
+ async () => providerTextStream(await fetch("/api/chat", { method: "POST", body }), "openai"),
311
+ (_current, chunk) => chunk,
312
+ { framing: "raw" },
313
+ );
314
+ ```
315
+
316
+ | Format | Value | Notes |
317
+ | --- | --- | --- |
318
+ | OpenAI | `"openai"` | Chat Completions deltas and Responses API `output_text.delta`. |
319
+ | Anthropic | `"anthropic"` | `content_block_delta` text deltas. Stream errors raise `ProviderStreamError`. |
320
+ | Vercel AI SDK | `"ai-sdk"` | v5 UI-message SSE (`text-delta`), v4 data stream (`0:"..."`) and plain text responses. |
321
+
322
+ Full server and client examples are on the **Streaming recipes** docs page.
323
+
324
+ ## Editor adapters
325
+
326
+ `SelectionPalette` and `InlineDiff` work on plain DOM text. For rich-text editors, wrap the editor in an adapter and pass the result of `useEditorAnchor` to the palette. Adapters are structurally typed, so Anky UI never installs Tiptap, ProseMirror or Lexical.
327
+
328
+ ```tsx
329
+ import { SelectionPalette, createTiptapAdapter, useEditorAnchor } from "@erankushpatil/anky-ui";
330
+
331
+ const adapter = useMemo(() => (editor ? createTiptapAdapter(editor) : null), [editor]);
332
+ const anchor = useEditorAnchor(adapter);
333
+
334
+ <SelectionPalette anchor={anchor} onSelectPrompt={async (prompt, text) => anchor.replaceSelection(await rewrite(prompt, text))} />;
335
+ ```
336
+
337
+ Also available: `createProseMirrorAdapter(view, { setSelection })` and `createLexicalAdapter(editor, { $getSelection, $isRangeSelection, $createRangeSelection, $setSelection })`. Implement the small `EditorAdapter` interface for any other editor. Replacement refuses when the document changed under the range. The adapters are tested against structural fakes, so verify them against your editor version.
338
+
339
+ ## Accessibility
340
+
341
+ - `InlineDiff` is a `role="group"` labelled "AI suggested edit" and announces progress and the final insertion/deletion counts through a polite live region.
342
+ - `SelectionPalette` is a labelled toolbar with arrow-key navigation, announces when it opens, and `Alt+/` moves focus into it. `Escape` closes it and returns focus to the editor.
343
+ - `GhostInput` announces suggestions through its own live region.
344
+ - `announce(message, "polite" | "assertive")` is exported for your own messages.
345
+
346
+ ## Styling and Tailwind
347
+
348
+ Components are unstyled by default and only emit `anky-ui-*` classes (the diff also ships inline red and green colours unless `unstyled` is set). You have three options:
349
+
350
+ 1. Style the `anky-ui-*` classes in your own CSS.
351
+ 2. Pass `classNames` to add your own classes per slot.
352
+ 3. Use the ready-made Tailwind presets: `StyledSelectionPalette`, `StyledInlineDiff`, `selectionPaletteTailwindPreset`, `inlineDiffTailwindPreset`.
353
+
354
+ Tailwind only generates classes it can see, so add the package to your sources:
355
+
356
+ ```css
357
+ /* Tailwind v4 */
358
+ @source "../node_modules/@erankushpatil/anky-ui/dist";
359
+ ```
360
+
361
+ ```js
362
+ // Tailwind v3 (tailwind.config.js)
363
+ content: ["./src/**/*.{ts,tsx}", "./node_modules/@erankushpatil/anky-ui/dist/**/*.js"];
364
+ ```
365
+
366
+ ## CLI
367
+
368
+ Copy components straight into your project and own the code, as with shadcn/ui.
369
+
370
+ ```bash
371
+ npx anky-ui add selection-palette
372
+ npx anky-ui add inline-diff ghost-input
373
+ npx anky-ui add --all
374
+ npx anky-ui list
375
+ ```
376
+
377
+ Files are written to `src/components/anky/`, or `components/anky/` when the project has no `src` folder. The CLI looks for the nearest `package.json` and detects `tailwind.config.*`, TypeScript and your package manager. Internal imports are rewritten to sit side by side, and `@floating-ui/react` is the only dependency it installs.
378
+
379
+ | Command / flag | Description |
380
+ | --- | --- |
381
+ | `add <component...>` | Copy components and the files they import. |
382
+ | `list` | Show available components. |
383
+ | `-a`, `--all` | Add every component. |
384
+ | `-p`, `--path <dir>` | Output directory, relative to the project root. |
385
+ | `-o`, `--overwrite` | Replace existing files that differ. Without it, edited files are kept. |
386
+ | `--dry-run` | Print what would change without writing or installing. |
387
+ | `--no-install` | Print the dependency install command instead of running it. |
388
+ | `--cwd <dir>` | Run as if started in another directory. |
389
+ | `-h`, `--help`, `-v`, `--version` | Help and version. |
390
+
391
+ Components: `selection-palette`, `inline-diff`, `ghost-input`, `use-selection-anchor`, `use-optimistic-ai-state`, `diff`, `stream-parser`, `announce`, `stream-providers`, `editor-adapter`, `prosemirror-adapter`, `tiptap-adapter`, `lexical-adapter`, `use-editor-anchor`. Dependencies are added automatically, for example `selection-palette` also copies `use-selection-anchor`.
392
+
393
+ ## Server and Next.js notes
394
+
395
+ - Components and hooks include the `"use client"` directive. Import them from Client Components in the App Router.
396
+ - Rendering is SSR-safe: selection virtual elements, media queries and platform detection are initialised in `useEffect`, so there are no hydration mismatches.
397
+ - A minimal streaming route handler:
398
+
399
+ ```ts
400
+ // app/api/rewrite/route.ts
401
+ export async function POST(req: Request) {
402
+ const { prompt, text } = await req.json();
403
+ const upstream = await callYourModel(prompt, text); // returns a ReadableStream<Uint8Array> of plain text
404
+ return new Response(upstream, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
405
+ }
406
+ ```
407
+
408
+ ## Playground
409
+
410
+ The repository is a [pnpm](https://pnpm.io) workspace (pnpm 12, Node 18.17+).
411
+
412
+ ```bash
413
+ git clone https://github.com/ErAnkushPatil/anky-ui.git
414
+ cd anky-ui
415
+ pnpm install
416
+ pnpm dev:demo
417
+ ```
418
+
419
+ The demo shows a 500-word editable article with the palette and inline diff, an email draft with ghost completions, and global undo (`Cmd/Ctrl+Z`) and redo (`Cmd/Ctrl+Shift+Z`). It uses a mock stream by default. Set `VITE_ANKY_ENDPOINT` to a POST endpoint that accepts `{ "prompt": "...", "text": "..." }` to use a real backend. Open `#stress` for the 50 tokens/second streaming stress test.
420
+
421
+ ### Docs site
422
+
423
+ ```bash
424
+ pnpm dev:docs
425
+ ```
426
+
427
+ The docs site has live demos for the components, state hook, provider streaming recipes (OpenAI, Anthropic, Vercel AI SDK), editor adapters and accessibility announcements. Its **Benchmarks** page measures `computeDiff` speed (5 ms budget at 5,000 characters) and the streaming frame rate into `InlineDiff` at 50, 100 and 200 tokens/second, live in your browser.
428
+
429
+ ## Development
430
+
431
+ ```bash
432
+ pnpm install
433
+ pnpm verify # typecheck, test and build every package
434
+ pnpm test # tests only
435
+ pnpm typecheck # typecheck only
436
+ pnpm build # build the library, CLI and apps
437
+ ```
438
+
439
+ The workspace root is the publishable `anky-ui` CLI package. `packages/cli` is its private implementation (`@anky/cli`). `packages/core` is published separately as `@erankushpatil/anky-ui`. `pnpm pack` at the root builds the CLI and bundles the component registry.
440
+
441
+ ### Releasing
442
+
443
+ ```bash
444
+ pnpm verify
445
+ pnpm --filter @erankushpatil/anky-ui publish # the library
446
+ pnpm publish # the anky-ui CLI
447
+ ```
448
+
449
+ Bump `version` in `package.json` and `packages/core/package.json` first. The CLI copies the library's source, so release both together.
450
+
451
+ ## Contributing
452
+
453
+ Issues and pull requests are welcome at [github.com/ErAnkushPatil/anky-ui](https://github.com/ErAnkushPatil/anky-ui/issues). Run `pnpm verify` before opening a pull request.
454
+
455
+ ## FAQ
456
+
457
+ **Do I need a specific AI provider?** No. Anky UI only consumes a text stream (raw or SSE). Use any model behind your own endpoint.
458
+
459
+ **Does it need Tailwind?** No. Tailwind presets are optional, and the CLI copies unstyled components.
460
+
461
+ **Is it SSR and Next.js safe?** Yes. Client-only browser APIs are touched only inside effects.
462
+
463
+ **Does it work on touch devices?** Yes. Selection tracking handles `touchend` as well as `mouseup` without double triggers.
464
+
465
+ **How fast is the diff?** Strings up to 5,000 characters diff in under 5 ms, and a work budget bounds the worst case.
466
+
467
+ ## Keywords
468
+
469
+ ai ui library, inline ai, ambient ai, copilot ui components, shadcn ai, react inline diff streaming, cursor-like inline prompt react, ghost text autocomplete textarea, floating ai action palette, undo redo ai state react, nextjs generative ui, headless ai primitives, tailwind ai components, radix floating ai toolbar.
470
+
471
+ ## License
472
+
473
+ [ISC](./LICENSE)
package/package.json CHANGED
@@ -1,6 +1,74 @@
1
1
  {
2
2
  "name": "anky-ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Copy Anky UI's React AI components into your project.",
5
+ "bin": {
6
+ "anky-ui": "./packages/cli/dist/index.js"
7
+ },
8
+ "files": [
9
+ "LICENSE",
10
+ "packages/cli/dist",
11
+ "packages/cli/registry",
12
+ "packages/cli/package.json"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18.17"
16
+ },
17
+ "keywords": [
18
+ "ai ui library",
19
+ "inline ai",
20
+ "ambient ai",
21
+ "copilot ui components",
22
+ "shadcn ai",
23
+ "react inline diff streaming",
24
+ "cursor-like inline prompt react",
25
+ "ghost text autocomplete textarea",
26
+ "floating ai action palette",
27
+ "undo redo ai state react",
28
+ "nextjs generative ui",
29
+ "headless ai primitives",
30
+ "tailwind ai components",
31
+ "radix floating ai toolbar",
32
+ "cli"
33
+ ],
34
+ "license": "ISC",
35
+ "author": "Ankush Patil",
36
+ "homepage": "https://github.com/ErAnkushPatil/anky-ui#readme",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/ErAnkushPatil/anky-ui.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/ErAnkushPatil/anky-ui/issues"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "devEngines": {
48
+ "packageManager": {
49
+ "name": "pnpm",
50
+ "version": "12.15.0",
51
+ "onFail": "download"
52
+ }
53
+ },
54
+ "type": "module",
55
+ "devDependencies": {
56
+ "@typescript/typescript6": "^6.0.2",
57
+ "@types/node": "^26.6.5",
58
+ "@types/react": "^19.3.0",
59
+ "@types/react-dom": "^19.3.0",
60
+ "tsup": "^9.0.0",
61
+ "typescript": "^7.0.2",
62
+ "vitest": "^5.0.5"
63
+ },
64
+ "scripts": {
65
+ "build": "pnpm -r build",
66
+ "build:cli": "pnpm --filter @anky/cli build",
67
+ "build:core": "pnpm --filter @erankushpatil/anky-ui build",
68
+ "dev:demo": "pnpm --filter @anky/demo dev",
69
+ "dev:docs": "pnpm --filter @anky/docs dev",
70
+ "test": "pnpm -r test",
71
+ "typecheck": "pnpm -r typecheck",
72
+ "verify": "pnpm -r typecheck && pnpm -r test && pnpm -r build"
73
+ }
6
74
  }