@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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/build/index.d.mts +927 -0
  4. package/build/index.mjs +5236 -0
  5. package/build/index.mjs.map +1 -0
  6. package/package.json +73 -0
  7. package/src/components/agent/Composer.tsx +139 -0
  8. package/src/components/agent/Conversation.tsx +59 -0
  9. package/src/components/agent/FileCard.tsx +50 -0
  10. package/src/components/agent/Loader.tsx +21 -0
  11. package/src/components/agent/Message.tsx +44 -0
  12. package/src/components/agent/ModelSelect.tsx +87 -0
  13. package/src/components/agent/PermissionModeSelect.tsx +94 -0
  14. package/src/components/agent/PermissionPrompt.tsx +52 -0
  15. package/src/components/agent/QuestionPrompt.tsx +193 -0
  16. package/src/components/agent/Reasoning.tsx +58 -0
  17. package/src/components/agent/Response.tsx +31 -0
  18. package/src/components/agent/SessionList.tsx +93 -0
  19. package/src/components/agent/SessionPanel.tsx +149 -0
  20. package/src/components/agent/StatusBar.tsx +140 -0
  21. package/src/components/agent/ToolCallCard.tsx +94 -0
  22. package/src/components/agent/Transcript.tsx +114 -0
  23. package/src/components/agent/status.ts +16 -0
  24. package/src/components/prompt-area/animated-placeholder.tsx +42 -0
  25. package/src/components/prompt-area/clipboard-helpers.ts +206 -0
  26. package/src/components/prompt-area/cursor-helpers.ts +244 -0
  27. package/src/components/prompt-area/dom-helpers.ts +721 -0
  28. package/src/components/prompt-area/file-strip.tsx +250 -0
  29. package/src/components/prompt-area/html-to-markdown.ts +278 -0
  30. package/src/components/prompt-area/image-strip.tsx +49 -0
  31. package/src/components/prompt-area/index.ts +23 -0
  32. package/src/components/prompt-area/prompt-area-engine.ts +705 -0
  33. package/src/components/prompt-area/prompt-area-list-ops.ts +499 -0
  34. package/src/components/prompt-area/prompt-area.tsx +375 -0
  35. package/src/components/prompt-area/remove-button.tsx +37 -0
  36. package/src/components/prompt-area/segment-helpers.ts +62 -0
  37. package/src/components/prompt-area/trigger-popover.tsx +139 -0
  38. package/src/components/prompt-area/trigger-presets.ts +143 -0
  39. package/src/components/prompt-area/types.ts +360 -0
  40. package/src/components/prompt-area/use-markdown-mode.ts +113 -0
  41. package/src/components/prompt-area/use-prompt-area-events.ts +470 -0
  42. package/src/components/prompt-area/use-prompt-area-state.ts +131 -0
  43. package/src/components/prompt-area/use-prompt-area.ts +1507 -0
  44. package/src/components/prompt-area/use-trigger-search.ts +115 -0
  45. package/src/components/ui/AlertDialog.tsx +56 -0
  46. package/src/components/ui/Badge.tsx +42 -0
  47. package/src/components/ui/Button.tsx +47 -0
  48. package/src/components/ui/Card.tsx +29 -0
  49. package/src/components/ui/CodeBlock.tsx +31 -0
  50. package/src/components/ui/CopyButton.tsx +28 -0
  51. package/src/components/ui/Input.tsx +20 -0
  52. package/src/components/ui/ProgressRing.tsx +49 -0
  53. package/src/components/ui/Select.tsx +80 -0
  54. package/src/components/ui/Sonner.tsx +22 -0
  55. package/src/components/ui/Spinner.tsx +6 -0
  56. package/src/components/ui/Textarea.tsx +21 -0
  57. package/src/components/ui/Tooltip.tsx +34 -0
  58. package/src/index.ts +99 -0
  59. package/src/lib/format.ts +67 -0
  60. package/src/lib/utils.ts +33 -0
  61. 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
+ }