dsh-taskboard 0.6.0 → 0.6.2
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/README.md +5 -1
- package/lib/client.js +9 -6253
- package/package.json +11 -1
- package/src/client/board/SlashPromptInput.tsx +74 -6
- package/src/client/styles.ts +4 -2
- package/src/shared/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-taskboard",
|
|
3
3
|
"description": "Agent-first task board for the DSH web GUI: host-authoritative task ledger with taskboard_* agent tools, project (= workspace) claim boundaries, per-task model execution in fresh sessions, optional per-task git-worktree isolation (dedicated task branches, commit evidence, one-click merge), host-side cron scheduling, and a live SSE kanban view. Mounts via the official dsh plugin system — no DSH source changes.",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
"client": {
|
|
18
18
|
"inject": [],
|
|
19
19
|
"platform": "web"
|
|
20
|
+
},
|
|
21
|
+
"compatibility": {
|
|
22
|
+
"dshReleases": {
|
|
23
|
+
"0.1.0-rc.8": "unknown",
|
|
24
|
+
"0.1.1-rc.1": "unknown",
|
|
25
|
+
"0.1.1-rc.2": "compatible"
|
|
26
|
+
}
|
|
20
27
|
}
|
|
21
28
|
},
|
|
22
29
|
"files": [
|
|
@@ -27,6 +34,9 @@
|
|
|
27
34
|
"LICENSE"
|
|
28
35
|
],
|
|
29
36
|
"license": "Apache-2.0",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22"
|
|
39
|
+
},
|
|
30
40
|
"author": "cloader",
|
|
31
41
|
"repository": {
|
|
32
42
|
"type": "git",
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module dsh-taskboard/client/board/SlashPromptInput
|
|
8
8
|
*/
|
|
9
|
-
import { useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent } from 'react'
|
|
9
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ChangeEvent, type CSSProperties, type KeyboardEvent } from 'react'
|
|
10
|
+
import { createPortal } from 'react-dom'
|
|
10
11
|
import type { BoardController } from '../controller.ts'
|
|
11
12
|
import type { PromptCompletionItem } from '../../shared/api.ts'
|
|
12
13
|
import { useT, type Translate } from '../i18n/runtime.ts'
|
|
@@ -83,6 +84,10 @@ export function SlashPromptInput({
|
|
|
83
84
|
}: SlashPromptInputProps) {
|
|
84
85
|
const t = useT()
|
|
85
86
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
87
|
+
const popupRef = useRef<HTMLDivElement>(null)
|
|
88
|
+
const listRef = useRef<HTMLDivElement>(null)
|
|
89
|
+
// Inline fixed-position style for the portaled popup (set by positionPopup).
|
|
90
|
+
const [popupStyle, setPopupStyle] = useState<CSSProperties>({})
|
|
86
91
|
|
|
87
92
|
// Autocomplete state: only HOST-provided items are stateful; the built-in
|
|
88
93
|
// defaults are re-derived per render so their descriptions follow the
|
|
@@ -131,6 +136,22 @@ export function SlashPromptInput({
|
|
|
131
136
|
}
|
|
132
137
|
}, [filteredItems.length, selectedIndex])
|
|
133
138
|
|
|
139
|
+
// Keep the keyboard-highlighted option visible inside the scrolling list:
|
|
140
|
+
// mouse hovering only ever targets rendered rows, but ArrowUp/ArrowDown can
|
|
141
|
+
// move the highlight past the clipped edge. Adjust the list's scrollTop
|
|
142
|
+
// directly from rect deltas — NOT scrollIntoView, which would also scroll
|
|
143
|
+
// ancestor containers (the modal body behind the portaled popup).
|
|
144
|
+
useLayoutEffect(() => {
|
|
145
|
+
if (!popupOpen) return
|
|
146
|
+
const list = listRef.current
|
|
147
|
+
const active = list?.children[selectedIndex]
|
|
148
|
+
if (list === null || !(active instanceof HTMLElement)) return
|
|
149
|
+
const listRect = list.getBoundingClientRect()
|
|
150
|
+
const itemRect = active.getBoundingClientRect()
|
|
151
|
+
if (itemRect.top < listRect.top) list.scrollTop -= listRect.top - itemRect.top
|
|
152
|
+
else if (itemRect.bottom > listRect.bottom) list.scrollTop += itemRect.bottom - listRect.bottom
|
|
153
|
+
}, [popupOpen, selectedIndex, filteredItems])
|
|
154
|
+
|
|
134
155
|
// Detect slash typing on cursor movement or text change
|
|
135
156
|
const checkSlashTrigger = (): void => {
|
|
136
157
|
const el = textareaRef.current
|
|
@@ -207,6 +228,51 @@ export function SlashPromptInput({
|
|
|
207
228
|
}
|
|
208
229
|
}
|
|
209
230
|
|
|
231
|
+
// The popup is portaled to document.body and fixed-positioned from the
|
|
232
|
+
// textarea's viewport rect: an absolute popup inside the scrollable modal
|
|
233
|
+
// body was clipped at the container's top edge (0.6.0 field report).
|
|
234
|
+
// Opens above by preference, flips below when the top is tight, and clamps
|
|
235
|
+
// to the viewport (maxHeight shrinks; the list scrolls internally).
|
|
236
|
+
const positionPopup = useCallback((): void => {
|
|
237
|
+
const anchor = textareaRef.current
|
|
238
|
+
if (anchor === null) return
|
|
239
|
+
const rect = anchor.getBoundingClientRect()
|
|
240
|
+
const gap = 6
|
|
241
|
+
const margin = 8
|
|
242
|
+
const vh = window.innerHeight
|
|
243
|
+
const measured = popupRef.current?.offsetHeight ?? 0
|
|
244
|
+
const natural = measured > 0 ? measured : 240
|
|
245
|
+
const roomAbove = rect.top - gap - margin
|
|
246
|
+
const roomBelow = vh - margin - (rect.bottom + gap)
|
|
247
|
+
const openBelow = roomBelow > roomAbove
|
|
248
|
+
const height = Math.min(natural, Math.max(openBelow ? roomBelow : roomAbove, 120))
|
|
249
|
+
const top = openBelow ? rect.bottom + gap : rect.top - gap - height
|
|
250
|
+
// Bail out (return prev) when unchanged: the layout effect below runs on
|
|
251
|
+
// every open render, and a fresh object here would re-render forever.
|
|
252
|
+
setPopupStyle(prev => (prev.left === rect.left && prev.top === top && prev.width === rect.width && prev.maxHeight === height
|
|
253
|
+
? prev
|
|
254
|
+
: { position: 'fixed', left: rect.left, top, width: rect.width, maxHeight: height, zIndex: 100 }))
|
|
255
|
+
}, [])
|
|
256
|
+
|
|
257
|
+
// Reposition on every open render: the popup height follows the filtered
|
|
258
|
+
// item count, so typing changes the geometry too.
|
|
259
|
+
useLayoutEffect(() => {
|
|
260
|
+
if (!popupOpen) return
|
|
261
|
+
positionPopup()
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
// Follow scrolling and viewport resizes while open (capture phase: the
|
|
265
|
+
// modal body scrolls, the window itself does not).
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (!popupOpen) return
|
|
268
|
+
window.addEventListener('scroll', positionPopup, true)
|
|
269
|
+
window.addEventListener('resize', positionPopup)
|
|
270
|
+
return () => {
|
|
271
|
+
window.removeEventListener('scroll', positionPopup, true)
|
|
272
|
+
window.removeEventListener('resize', positionPopup)
|
|
273
|
+
}
|
|
274
|
+
}, [popupOpen, positionPopup])
|
|
275
|
+
|
|
210
276
|
return (
|
|
211
277
|
<div className={`dsh-atb-prompt-wrap ${className ?? ''}`}>
|
|
212
278
|
<div className="dsh-atb-prompt-inner">
|
|
@@ -229,14 +295,15 @@ export function SlashPromptInput({
|
|
|
229
295
|
onKeyDown={handleKeyDown}
|
|
230
296
|
/>
|
|
231
297
|
|
|
232
|
-
{/* Slash Autocomplete Popup
|
|
233
|
-
|
|
234
|
-
|
|
298
|
+
{/* Slash Autocomplete Popup — portaled to document.body so the
|
|
299
|
+
scrollable modal body can never clip it (see positionPopup). */}
|
|
300
|
+
{popupOpen && filteredItems.length > 0 && createPortal(
|
|
301
|
+
<div ref={popupRef} className="dsh-atb-slash-popup" style={popupStyle} role="listbox" aria-label={t('slash.aria')}>
|
|
235
302
|
<div className="dsh-atb-slash-head">
|
|
236
303
|
<span className="dsh-atb-slash-title">{t('slash.title')}</span>
|
|
237
304
|
<span className="dsh-atb-slash-hint">{t('slash.hint')}</span>
|
|
238
305
|
</div>
|
|
239
|
-
<div className="dsh-atb-slash-list">
|
|
306
|
+
<div ref={listRef} className="dsh-atb-slash-list">
|
|
240
307
|
{filteredItems.map((item, idx) => (
|
|
241
308
|
<div
|
|
242
309
|
key={`${item.kind}-${item.name}`}
|
|
@@ -257,7 +324,8 @@ export function SlashPromptInput({
|
|
|
257
324
|
</div>
|
|
258
325
|
))}
|
|
259
326
|
</div>
|
|
260
|
-
</div
|
|
327
|
+
</div>,
|
|
328
|
+
document.body,
|
|
261
329
|
)}
|
|
262
330
|
</div>
|
|
263
331
|
|
package/src/client/styles.ts
CHANGED
|
@@ -868,9 +868,11 @@ button.dsh-atb-chip2.dsh-atb-chip-btn:hover {
|
|
|
868
868
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary, #1f2328) 18%, transparent);
|
|
869
869
|
}
|
|
870
870
|
|
|
871
|
-
/* Slash Autocomplete Popup
|
|
871
|
+
/* Slash Autocomplete Popup. Fixed positioning (left/top/width/maxHeight/z-index)
|
|
872
|
+
* is set INLINE by SlashPromptInput: the popup is portaled to document.body and
|
|
873
|
+
* anchored to the textarea's viewport rect, so the scrollable modal body can no
|
|
874
|
+
* longer clip its top (0.6.0 field report). Only the visual shell lives here. */
|
|
872
875
|
.dsh-atb-slash-popup {
|
|
873
|
-
position: absolute; left: 0; bottom: calc(100% + 6px); width: 100%; max-height: 240px; z-index: 100;
|
|
874
876
|
display: flex; flex-direction: column; overflow: hidden; border-radius: 10px;
|
|
875
877
|
background: var(--dsw-alias-bg-overlay, #fff); color: var(--dsw-alias-label-primary, inherit);
|
|
876
878
|
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.28));
|
package/src/shared/version.ts
CHANGED