@workerdeck/ui 0.6.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 +21 -0
- package/README.md +74 -0
- package/build/index.d.mts +927 -0
- package/build/index.mjs +5236 -0
- package/build/index.mjs.map +1 -0
- package/package.json +73 -0
- package/src/components/agent/Composer.tsx +139 -0
- package/src/components/agent/Conversation.tsx +59 -0
- package/src/components/agent/FileCard.tsx +50 -0
- package/src/components/agent/Loader.tsx +21 -0
- package/src/components/agent/Message.tsx +44 -0
- package/src/components/agent/ModelSelect.tsx +87 -0
- package/src/components/agent/PermissionModeSelect.tsx +94 -0
- package/src/components/agent/PermissionPrompt.tsx +52 -0
- package/src/components/agent/QuestionPrompt.tsx +193 -0
- package/src/components/agent/Reasoning.tsx +58 -0
- package/src/components/agent/Response.tsx +31 -0
- package/src/components/agent/SessionList.tsx +93 -0
- package/src/components/agent/SessionPanel.tsx +149 -0
- package/src/components/agent/StatusBar.tsx +140 -0
- package/src/components/agent/ToolCallCard.tsx +94 -0
- package/src/components/agent/Transcript.tsx +114 -0
- package/src/components/agent/status.ts +16 -0
- package/src/components/prompt-area/animated-placeholder.tsx +42 -0
- package/src/components/prompt-area/clipboard-helpers.ts +206 -0
- package/src/components/prompt-area/cursor-helpers.ts +244 -0
- package/src/components/prompt-area/dom-helpers.ts +721 -0
- package/src/components/prompt-area/file-strip.tsx +250 -0
- package/src/components/prompt-area/html-to-markdown.ts +278 -0
- package/src/components/prompt-area/image-strip.tsx +49 -0
- package/src/components/prompt-area/index.ts +23 -0
- package/src/components/prompt-area/prompt-area-engine.ts +705 -0
- package/src/components/prompt-area/prompt-area-list-ops.ts +499 -0
- package/src/components/prompt-area/prompt-area.tsx +375 -0
- package/src/components/prompt-area/remove-button.tsx +37 -0
- package/src/components/prompt-area/segment-helpers.ts +62 -0
- package/src/components/prompt-area/trigger-popover.tsx +139 -0
- package/src/components/prompt-area/trigger-presets.ts +143 -0
- package/src/components/prompt-area/types.ts +360 -0
- package/src/components/prompt-area/use-markdown-mode.ts +113 -0
- package/src/components/prompt-area/use-prompt-area-events.ts +470 -0
- package/src/components/prompt-area/use-prompt-area-state.ts +131 -0
- package/src/components/prompt-area/use-prompt-area.ts +1507 -0
- package/src/components/prompt-area/use-trigger-search.ts +115 -0
- package/src/components/ui/AlertDialog.tsx +56 -0
- package/src/components/ui/Badge.tsx +42 -0
- package/src/components/ui/Button.tsx +47 -0
- package/src/components/ui/Card.tsx +29 -0
- package/src/components/ui/CodeBlock.tsx +31 -0
- package/src/components/ui/CopyButton.tsx +28 -0
- package/src/components/ui/Input.tsx +20 -0
- package/src/components/ui/ProgressRing.tsx +49 -0
- package/src/components/ui/Select.tsx +80 -0
- package/src/components/ui/Sonner.tsx +22 -0
- package/src/components/ui/Spinner.tsx +6 -0
- package/src/components/ui/Textarea.tsx +21 -0
- package/src/components/ui/Tooltip.tsx +34 -0
- package/src/index.ts +99 -0
- package/src/lib/format.ts +67 -0
- package/src/lib/utils.ts +33 -0
- package/src/styles/theme.css +413 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor/selection utilities for the PromptArea contentEditable.
|
|
3
|
+
*
|
|
4
|
+
* Invariants:
|
|
5
|
+
* - All functions are synchronous. Never return a promise or cross a microtask
|
|
6
|
+
* boundary after a DOM mutation — that can cause the browser to fire
|
|
7
|
+
* `selectionchange` and reset the caret.
|
|
8
|
+
* - Never cache a `Selection` or `Range` across calls. Ranges become detached
|
|
9
|
+
* after DOM mutations. Each function calls `window.getSelection()` or
|
|
10
|
+
* `getSelectionRange()` fresh.
|
|
11
|
+
* - Chip nodes are treated atomically via `isChipElement` — we never descend
|
|
12
|
+
* into a contentEditable=false subtree when mapping offsets.
|
|
13
|
+
*/
|
|
14
|
+
import {
|
|
15
|
+
chipNodeTextLength,
|
|
16
|
+
getDirectChildContaining,
|
|
17
|
+
getSelectionRange,
|
|
18
|
+
indexOfChildNode,
|
|
19
|
+
isBRElement,
|
|
20
|
+
isChipElement,
|
|
21
|
+
isHTMLElement,
|
|
22
|
+
} from './dom-helpers.ts'
|
|
23
|
+
|
|
24
|
+
export type SavedCursor = {
|
|
25
|
+
nodeIndex: number
|
|
26
|
+
offset: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function saveCursorPosition(editor: HTMLElement): SavedCursor | null {
|
|
30
|
+
const range = getSelectionRange()
|
|
31
|
+
if (!range) return null
|
|
32
|
+
if (!editor.contains(range.startContainer)) return null
|
|
33
|
+
|
|
34
|
+
const node = range.startContainer
|
|
35
|
+
if (node === editor) {
|
|
36
|
+
return { nodeIndex: range.startOffset, offset: 0 }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Walk up to find the direct child of editor using type-safe helper
|
|
40
|
+
const directChild = getDirectChildContaining(editor, node)
|
|
41
|
+
if (!directChild) return null
|
|
42
|
+
|
|
43
|
+
const nodeIndex = indexOfChildNode(editor, directChild)
|
|
44
|
+
return { nodeIndex, offset: range.startOffset }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function restoreCursorPosition(editor: HTMLElement, saved: SavedCursor): void {
|
|
48
|
+
const sel = window.getSelection()
|
|
49
|
+
if (!sel) return
|
|
50
|
+
|
|
51
|
+
const childNodes = editor.childNodes
|
|
52
|
+
if (childNodes.length === 0) return
|
|
53
|
+
|
|
54
|
+
const range = document.createRange()
|
|
55
|
+
|
|
56
|
+
if (saved.nodeIndex >= childNodes.length) {
|
|
57
|
+
const lastChild = childNodes[childNodes.length - 1]
|
|
58
|
+
if (lastChild.nodeType === Node.TEXT_NODE) {
|
|
59
|
+
range.setStart(lastChild, (lastChild.textContent ?? '').length)
|
|
60
|
+
} else {
|
|
61
|
+
range.setStartAfter(lastChild)
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
const targetNode = childNodes[saved.nodeIndex]
|
|
65
|
+
if (targetNode.nodeType === Node.TEXT_NODE) {
|
|
66
|
+
const maxOffset = (targetNode.textContent ?? '').length
|
|
67
|
+
range.setStart(targetNode, Math.min(saved.offset, maxOffset))
|
|
68
|
+
} else {
|
|
69
|
+
range.setStartAfter(targetNode)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
range.collapse(true)
|
|
74
|
+
sel.removeAllRanges()
|
|
75
|
+
sel.addRange(range)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getCursorOffset(editor: HTMLElement): number | null {
|
|
79
|
+
const range = getSelectionRange()
|
|
80
|
+
if (!range) return null
|
|
81
|
+
if (!editor.contains(range.startContainer)) return null
|
|
82
|
+
|
|
83
|
+
const preRange = document.createRange()
|
|
84
|
+
preRange.selectNodeContents(editor)
|
|
85
|
+
preRange.setEnd(range.startContainer, range.startOffset)
|
|
86
|
+
|
|
87
|
+
return getTextLengthInRange(preRange)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create a collapsed Range at the given plain-text offset inside the editor.
|
|
92
|
+
* Returns null if the offset can't be mapped to a DOM position.
|
|
93
|
+
*/
|
|
94
|
+
export function createRangeAtOffset(editor: HTMLElement, targetOffset: number): Range | null {
|
|
95
|
+
const pos = findDOMPosition(editor, targetOffset)
|
|
96
|
+
if (!pos) return null
|
|
97
|
+
|
|
98
|
+
const range = document.createRange()
|
|
99
|
+
range.setStart(pos.node, pos.offset)
|
|
100
|
+
range.collapse(true)
|
|
101
|
+
return range
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function setCursorAtOffset(editor: HTMLElement, targetOffset: number): void {
|
|
105
|
+
const sel = window.getSelection()
|
|
106
|
+
if (!sel) return
|
|
107
|
+
|
|
108
|
+
const pos = findDOMPosition(editor, targetOffset)
|
|
109
|
+
if (pos) {
|
|
110
|
+
const range = document.createRange()
|
|
111
|
+
range.setStart(pos.node, pos.offset)
|
|
112
|
+
range.collapse(true)
|
|
113
|
+
sel.removeAllRanges()
|
|
114
|
+
sel.addRange(range)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Fallback: place cursor at end
|
|
119
|
+
const range = document.createRange()
|
|
120
|
+
range.selectNodeContents(editor)
|
|
121
|
+
range.collapse(false)
|
|
122
|
+
sel.removeAllRanges()
|
|
123
|
+
sel.addRange(range)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getTextLengthInRange(range: Range): number {
|
|
127
|
+
const fragment = range.cloneContents()
|
|
128
|
+
let length = 0
|
|
129
|
+
|
|
130
|
+
const walk = (node: Node): void => {
|
|
131
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
132
|
+
length += (node.textContent ?? '').length
|
|
133
|
+
} else if (isChipElement(node)) {
|
|
134
|
+
length += chipNodeTextLength(node)
|
|
135
|
+
} else if (isHTMLElement(node) && node.tagName === 'BR') {
|
|
136
|
+
if (node.dataset.sentinel) return // skip sentinel <br>
|
|
137
|
+
length += 1
|
|
138
|
+
} else if (isHTMLElement(node)) {
|
|
139
|
+
node.childNodes.forEach(walk)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fragment.childNodes.forEach(walk)
|
|
144
|
+
return length
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Returns the start and end plain-text offsets of the current selection.
|
|
149
|
+
* Returns null if there's no selection or it's outside the editor.
|
|
150
|
+
*/
|
|
151
|
+
export function getSelectionOffsets(editor: HTMLElement): { start: number; end: number } | null {
|
|
152
|
+
const range = getSelectionRange()
|
|
153
|
+
if (!range) return null
|
|
154
|
+
if (!editor.contains(range.startContainer)) return null
|
|
155
|
+
|
|
156
|
+
const startRange = document.createRange()
|
|
157
|
+
startRange.selectNodeContents(editor)
|
|
158
|
+
startRange.setEnd(range.startContainer, range.startOffset)
|
|
159
|
+
const start = getTextLengthInRange(startRange)
|
|
160
|
+
|
|
161
|
+
if (range.collapsed) return { start, end: start }
|
|
162
|
+
|
|
163
|
+
const endRange = document.createRange()
|
|
164
|
+
endRange.selectNodeContents(editor)
|
|
165
|
+
endRange.setEnd(range.endContainer, range.endOffset)
|
|
166
|
+
const end = getTextLengthInRange(endRange)
|
|
167
|
+
|
|
168
|
+
return { start, end }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Sets a (potentially non-collapsed) selection at the given plain-text offsets.
|
|
173
|
+
* Used to restore selection after markdown wrap/unwrap operations.
|
|
174
|
+
*/
|
|
175
|
+
export function setSelectionAtOffsets(
|
|
176
|
+
editor: HTMLElement,
|
|
177
|
+
startOffset: number,
|
|
178
|
+
endOffset: number,
|
|
179
|
+
): void {
|
|
180
|
+
const sel = window.getSelection()
|
|
181
|
+
if (!sel) return
|
|
182
|
+
|
|
183
|
+
if (startOffset === endOffset) {
|
|
184
|
+
setCursorAtOffset(editor, startOffset)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const startPos = findDOMPosition(editor, startOffset)
|
|
189
|
+
const endPos = findDOMPosition(editor, endOffset)
|
|
190
|
+
if (!startPos || !endPos) return
|
|
191
|
+
|
|
192
|
+
const range = document.createRange()
|
|
193
|
+
range.setStart(startPos.node, startPos.offset)
|
|
194
|
+
range.setEnd(endPos.node, endPos.offset)
|
|
195
|
+
sel.removeAllRanges()
|
|
196
|
+
sel.addRange(range)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Maps a plain-text offset to a DOM node + offset pair.
|
|
201
|
+
* Recurses into decoration elements (markdown spans, URL anchors).
|
|
202
|
+
*/
|
|
203
|
+
export function findDOMPosition(
|
|
204
|
+
container: HTMLElement,
|
|
205
|
+
targetOffset: number,
|
|
206
|
+
): { node: Node; offset: number } | null {
|
|
207
|
+
let remaining = targetOffset
|
|
208
|
+
|
|
209
|
+
for (let i = 0; i < container.childNodes.length; i++) {
|
|
210
|
+
const child = container.childNodes[i]
|
|
211
|
+
|
|
212
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
213
|
+
const len = (child.textContent ?? '').length
|
|
214
|
+
if (remaining <= len) {
|
|
215
|
+
return { node: child, offset: remaining }
|
|
216
|
+
}
|
|
217
|
+
remaining -= len
|
|
218
|
+
} else if (isChipElement(child)) {
|
|
219
|
+
const chipLen = chipNodeTextLength(child)
|
|
220
|
+
if (remaining <= chipLen) {
|
|
221
|
+
// Position after the chip element
|
|
222
|
+
return { node: container, offset: i + 1 }
|
|
223
|
+
}
|
|
224
|
+
remaining -= chipLen
|
|
225
|
+
} else if (isBRElement(child)) {
|
|
226
|
+
if (child.dataset.sentinel) continue // skip sentinel <br>
|
|
227
|
+
if (remaining <= 1) {
|
|
228
|
+
return { node: container, offset: i + 1 }
|
|
229
|
+
}
|
|
230
|
+
remaining -= 1
|
|
231
|
+
} else if (isHTMLElement(child)) {
|
|
232
|
+
// Decoration element (markdown span, URL anchor) — recurse
|
|
233
|
+
const textLen = (child.textContent ?? '').length
|
|
234
|
+
if (remaining <= textLen) {
|
|
235
|
+
const result = findDOMPosition(child, remaining)
|
|
236
|
+
if (result) return result
|
|
237
|
+
}
|
|
238
|
+
remaining -= textLen
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Fallback: end of container
|
|
243
|
+
return { node: container, offset: container.childNodes.length }
|
|
244
|
+
}
|