Skip to content

Commit 2c17a98

Browse files
committed
refactor(ui): extract dock prompt shell
1 parent b784c92 commit 2c17a98

4 files changed

Lines changed: 333 additions & 227 deletions

File tree

packages/app/src/components/question-dock.tsx

Lines changed: 158 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid-js"
22
import { createStore } from "solid-js/store"
33
import { Button } from "@opencode-ai/ui/button"
4+
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
45
import { Icon } from "@opencode-ai/ui/icon"
56
import { showToast } from "@opencode-ai/ui/toast"
67
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
@@ -232,9 +233,11 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
232233
}
233234

234235
return (
235-
<div data-component="question-prompt" ref={(el) => (root = el)}>
236-
<div data-slot="question-body">
237-
<div data-slot="question-header">
236+
<DockPrompt
237+
kind="question"
238+
ref={(el) => (root = el)}
239+
header={
240+
<>
238241
<div data-slot="question-header-title">{summary()}</div>
239242
<div data-slot="question-progress">
240243
<For each={questions()}>
@@ -254,172 +257,169 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
254257
)}
255258
</For>
256259
</div>
257-
</div>
258-
259-
<div data-slot="question-content">
260-
<div data-slot="question-text">{question()?.question}</div>
261-
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
262-
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
263-
</Show>
264-
<div data-slot="question-options">
265-
<For each={options()}>
266-
{(opt, i) => {
267-
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
268-
return (
269-
<button
270-
data-slot="question-option"
271-
data-picked={picked()}
272-
role={multi() ? "checkbox" : "radio"}
273-
aria-checked={picked()}
274-
disabled={store.sending}
275-
onClick={() => selectOption(i())}
276-
>
277-
<span data-slot="question-option-check" aria-hidden="true">
278-
<span
279-
data-slot="question-option-box"
280-
data-type={multi() ? "checkbox" : "radio"}
281-
data-picked={picked()}
282-
>
283-
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
284-
<Icon name="check-small" size="small" />
285-
</Show>
286-
</span>
287-
</span>
288-
<span data-slot="question-option-main">
289-
<span data-slot="option-label">{opt.label}</span>
290-
<Show when={opt.description}>
291-
<span data-slot="option-description">{opt.description}</span>
292-
</Show>
293-
</span>
294-
</button>
295-
)
296-
}}
297-
</For>
298-
299-
<Show
300-
when={store.editing}
301-
fallback={
302-
<button
303-
data-slot="question-option"
304-
data-custom="true"
305-
data-picked={on()}
306-
role={multi() ? "checkbox" : "radio"}
307-
aria-checked={on()}
308-
disabled={store.sending}
309-
onClick={customOpen}
310-
>
260+
</>
261+
}
262+
footer={
263+
<>
264+
<Button variant="ghost" size="large" disabled={store.sending} onClick={reject}>
265+
{language.t("ui.common.dismiss")}
266+
</Button>
267+
<div data-slot="question-footer-actions">
268+
<Show when={store.tab > 0}>
269+
<Button variant="secondary" size="large" disabled={store.sending} onClick={back}>
270+
{language.t("ui.common.back")}
271+
</Button>
272+
</Show>
273+
<Button variant={last() ? "primary" : "secondary"} size="large" disabled={store.sending} onClick={next}>
274+
{last() ? language.t("ui.common.submit") : language.t("ui.common.next")}
275+
</Button>
276+
</div>
277+
</>
278+
}
279+
>
280+
<div data-slot="question-text">{question()?.question}</div>
281+
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
282+
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
283+
</Show>
284+
<div data-slot="question-options">
285+
<For each={options()}>
286+
{(opt, i) => {
287+
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
288+
return (
289+
<button
290+
data-slot="question-option"
291+
data-picked={picked()}
292+
role={multi() ? "checkbox" : "radio"}
293+
aria-checked={picked()}
294+
disabled={store.sending}
295+
onClick={() => selectOption(i())}
296+
>
297+
<span data-slot="question-option-check" aria-hidden="true">
311298
<span
312-
data-slot="question-option-check"
313-
aria-hidden="true"
314-
onClick={(e) => {
315-
e.preventDefault()
316-
e.stopPropagation()
317-
customToggle()
318-
}}
299+
data-slot="question-option-box"
300+
data-type={multi() ? "checkbox" : "radio"}
301+
data-picked={picked()}
319302
>
320-
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
321-
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
322-
<Icon name="check-small" size="small" />
323-
</Show>
324-
</span>
325-
</span>
326-
<span data-slot="question-option-main">
327-
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
328-
<span data-slot="option-description">
329-
{input() || language.t("ui.question.custom.placeholder")}
330-
</span>
303+
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
304+
<Icon name="check-small" size="small" />
305+
</Show>
331306
</span>
332-
</button>
307+
</span>
308+
<span data-slot="question-option-main">
309+
<span data-slot="option-label">{opt.label}</span>
310+
<Show when={opt.description}>
311+
<span data-slot="option-description">{opt.description}</span>
312+
</Show>
313+
</span>
314+
</button>
315+
)
316+
}}
317+
</For>
318+
319+
<Show
320+
when={store.editing}
321+
fallback={
322+
<button
323+
data-slot="question-option"
324+
data-custom="true"
325+
data-picked={on()}
326+
role={multi() ? "checkbox" : "radio"}
327+
aria-checked={on()}
328+
disabled={store.sending}
329+
onClick={customOpen}
330+
>
331+
<span
332+
data-slot="question-option-check"
333+
aria-hidden="true"
334+
onClick={(e) => {
335+
e.preventDefault()
336+
e.stopPropagation()
337+
customToggle()
338+
}}
339+
>
340+
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
341+
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
342+
<Icon name="check-small" size="small" />
343+
</Show>
344+
</span>
345+
</span>
346+
<span data-slot="question-option-main">
347+
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
348+
<span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
349+
</span>
350+
</button>
351+
}
352+
>
353+
<form
354+
data-slot="question-option"
355+
data-custom="true"
356+
data-picked={on()}
357+
role={multi() ? "checkbox" : "radio"}
358+
aria-checked={on()}
359+
onMouseDown={(e) => {
360+
if (store.sending) {
361+
e.preventDefault()
362+
return
333363
}
364+
if (e.target instanceof HTMLTextAreaElement) return
365+
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
366+
if (input instanceof HTMLTextAreaElement) input.focus()
367+
}}
368+
onSubmit={(e) => {
369+
e.preventDefault()
370+
commitCustom()
371+
}}
372+
>
373+
<span
374+
data-slot="question-option-check"
375+
aria-hidden="true"
376+
onClick={(e) => {
377+
e.preventDefault()
378+
e.stopPropagation()
379+
customToggle()
380+
}}
334381
>
335-
<form
336-
data-slot="question-option"
337-
data-custom="true"
338-
data-picked={on()}
339-
role={multi() ? "checkbox" : "radio"}
340-
aria-checked={on()}
341-
onMouseDown={(e) => {
342-
if (store.sending) {
382+
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
383+
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
384+
<Icon name="check-small" size="small" />
385+
</Show>
386+
</span>
387+
</span>
388+
<span data-slot="question-option-main">
389+
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
390+
<textarea
391+
ref={(el) =>
392+
setTimeout(() => {
393+
el.focus()
394+
el.style.height = "0px"
395+
el.style.height = `${el.scrollHeight}px`
396+
}, 0)
397+
}
398+
data-slot="question-custom-input"
399+
placeholder={language.t("ui.question.custom.placeholder")}
400+
value={input()}
401+
rows={1}
402+
disabled={store.sending}
403+
onKeyDown={(e) => {
404+
if (e.key === "Escape") {
343405
e.preventDefault()
406+
setStore("editing", false)
344407
return
345408
}
346-
if (e.target instanceof HTMLTextAreaElement) return
347-
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
348-
if (input instanceof HTMLTextAreaElement) input.focus()
349-
}}
350-
onSubmit={(e) => {
409+
if (e.key !== "Enter" || e.shiftKey) return
351410
e.preventDefault()
352411
commitCustom()
353412
}}
354-
>
355-
<span
356-
data-slot="question-option-check"
357-
aria-hidden="true"
358-
onClick={(e) => {
359-
e.preventDefault()
360-
e.stopPropagation()
361-
customToggle()
362-
}}
363-
>
364-
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
365-
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
366-
<Icon name="check-small" size="small" />
367-
</Show>
368-
</span>
369-
</span>
370-
<span data-slot="question-option-main">
371-
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
372-
<textarea
373-
ref={(el) =>
374-
setTimeout(() => {
375-
el.focus()
376-
el.style.height = "0px"
377-
el.style.height = `${el.scrollHeight}px`
378-
}, 0)
379-
}
380-
data-slot="question-custom-input"
381-
placeholder={language.t("ui.question.custom.placeholder")}
382-
value={input()}
383-
rows={1}
384-
disabled={store.sending}
385-
onKeyDown={(e) => {
386-
if (e.key === "Escape") {
387-
e.preventDefault()
388-
setStore("editing", false)
389-
return
390-
}
391-
if (e.key !== "Enter" || e.shiftKey) return
392-
e.preventDefault()
393-
commitCustom()
394-
}}
395-
onInput={(e) => {
396-
customUpdate(e.currentTarget.value)
397-
e.currentTarget.style.height = "0px"
398-
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
399-
}}
400-
/>
401-
</span>
402-
</form>
403-
</Show>
404-
</div>
405-
</div>
406-
</div>
407-
408-
<div data-slot="question-footer">
409-
<Button variant="ghost" size="large" disabled={store.sending} onClick={reject}>
410-
{language.t("ui.common.dismiss")}
411-
</Button>
412-
<div data-slot="question-footer-actions">
413-
<Show when={store.tab > 0}>
414-
<Button variant="secondary" size="large" disabled={store.sending} onClick={back}>
415-
{language.t("ui.common.back")}
416-
</Button>
417-
</Show>
418-
<Button variant={last() ? "primary" : "secondary"} size="large" disabled={store.sending} onClick={next}>
419-
{last() ? language.t("ui.common.submit") : language.t("ui.common.next")}
420-
</Button>
421-
</div>
413+
onInput={(e) => {
414+
customUpdate(e.currentTarget.value)
415+
e.currentTarget.style.height = "0px"
416+
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
417+
}}
418+
/>
419+
</span>
420+
</form>
421+
</Show>
422422
</div>
423-
</div>
423+
</DockPrompt>
424424
)
425425
}

0 commit comments

Comments
 (0)