refactor(ui): share one collapsible content treatment (#721)

* refactor(ui): share one collapsible content treatment

Three disclosures drove Reka directly with three different motion
treatments, and the chat tool-call one named utilities that do not exist:
`collapsible-up`/`collapsible-down` without the `animate-` prefix, so that
collapse never animated and ignored reduced motion.

Export the shared content treatment from the collapsible theme and use it
in the chat tool call, reasoning, and frame preset disclosures. Their
headers keep their own shape — a status icon, a thought icon, a text
chevron — because they are structurally different from the labeled row
disclosure.

* docs: note the chat disclosure animation fix
This commit is contained in:
Danila Poyarkov 2026-09-18 11:42:50 +03:00 committed by GitHub
parent 81a07c0c9a
commit 3ba79e6c0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 32 additions and 21 deletions

View file

@ -16,6 +16,8 @@
### Fixed
- Animate the AI chat tool-call disclosure, which expanded and collapsed without motion because its animation classes were misspelled.
- Explain why the local MCP server did not start — a missing `@open-pencil/mcp` install, a denied command, an early exit, a rejected local connection, or an unreachable address — with translated guidance and collapsible technical details instead of one generic health failure.
- Find a globally installed `@open-pencil/mcp` when the desktop app is launched from the system shell, by searching the common global binary directories that a graphical launch omits from `PATH`.
- Mark unsaved documents and ask whether to save before closing a tab, the desktop window, or the application, rather than relying on recovery alone.

View file

@ -15,6 +15,7 @@ import AttachmentList from '@/components/chat/attachment/AttachmentList.vue'
import ChatMarkdown from '@/components/chat/ChatMarkdown.vue'
import ReasoningBlock from '@/components/chat/ReasoningBlock.vue'
import IconButton from '@/components/ui/button/IconButton.vue'
import { collapsibleContentMotion } from '@/theme/collapsible/collapsible'
import { classifyToolState } from './tool-state'
@ -142,7 +143,7 @@ function partKey(part: UIMessagePart<UIDataTypes, UITools>, index: number): stri
</CollapsibleTrigger>
<CollapsibleContent
v-if="toolState(part) !== 'pending'"
class="data-[state=closed]:collapsible-up data-[state=open]:collapsible-down overflow-hidden text-[10px]"
:class="[collapsibleContentMotion, 'text-[10px]']"
>
<pre class="mt-1 overflow-x-auto rounded bg-input p-2 text-muted">{{
part.state === 'output-error' && part.errorText

View file

@ -5,6 +5,7 @@ import { computed, ref, watch } from 'vue'
import type { ReasoningDisplay } from '@/app/settings/preferences/store'
import ChatMarkdown from '@/components/chat/ChatMarkdown.vue'
import { collapsibleContentMotion } from '@/theme/collapsible/collapsible'
const {
display = 'collapsed',
@ -84,10 +85,7 @@ function updateOpen(value: boolean): void {
aria-hidden="true"
/>
</CollapsibleTrigger>
<CollapsibleContent
data-slot="chat-reasoning-content"
class="motion-safe:data-[state=closed]:animate-collapsible-up motion-safe:data-[state=open]:animate-collapsible-down animation-duration-150 overflow-hidden"
>
<CollapsibleContent data-slot="chat-reasoning-content" :class="collapsibleContentMotion">
<div class="border-t border-border px-2 py-1.5 text-[11px] leading-relaxed text-muted">
<ChatMarkdown :content="text" :mode="markdownMode" surface="reasoning" />
</div>

View file

@ -5,6 +5,7 @@ import { useI18n } from '@open-pencil/vue'
import { useEditorStore } from '@/app/editor/active-store'
import { FRAME_PRESET_CATEGORIES, type FramePreset } from '@/app/editor/frame-presets'
import { collapsibleContentMotion } from '@/theme/collapsible/collapsible'
const store = useEditorStore()
const { panels } = useI18n()
@ -40,20 +41,23 @@ function createFrame(preset: FramePreset) {
<span class="min-w-0 flex-1 truncate">{{ panels[category.labelKey] }}</span>
</CollapsibleTrigger>
<CollapsibleContent class="pb-1.5">
<button
v-for="preset in category.presets"
:key="preset.id"
type="button"
:data-frame-preset="preset.id"
class="flex h-7 w-full items-center gap-2 px-7 text-left text-[11px] text-surface hover:bg-hover"
@click="createFrame(preset)"
>
<span class="min-w-0 flex-1 truncate">{{ preset.name }}</span>
<span class="shrink-0 tabular-nums text-muted">
{{ preset.width }} × {{ preset.height }}
</span>
</button>
<CollapsibleContent :class="collapsibleContentMotion">
<!-- Padding stays inside the animated wrapper so it cannot snap. -->
<div class="pb-1.5">
<button
v-for="preset in category.presets"
:key="preset.id"
type="button"
:data-frame-preset="preset.id"
class="flex h-7 w-full items-center gap-2 px-7 text-left text-[11px] text-surface hover:bg-hover"
@click="createFrame(preset)"
>
<span class="min-w-0 flex-1 truncate">{{ preset.name }}</span>
<span class="shrink-0 tabular-nums text-muted">
{{ preset.width }} × {{ preset.height }}
</span>
</button>
</div>
</CollapsibleContent>
</CollapsibleRoot>
</section>

View file

@ -1,5 +1,12 @@
import { tv } from 'tailwind-variants'
/**
* Shared content treatment for every collapsible, including the call sites that
* keep their own header shape and drive Reka directly.
*/
export const collapsibleContentMotion =
'overflow-hidden data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-up duration-180 motion-reduce:data-[state=open]:animate-none motion-reduce:data-[state=closed]:animate-none'
export const collapsibleTheme = {
slots: {
root: '',
@ -11,8 +18,7 @@ export const collapsibleTheme = {
actions: 'shrink-0',
// Padding and borders belong to the slot inside this wrapper so they cannot
// snap while the measured height animates.
content:
'overflow-hidden data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-up duration-180 motion-reduce:data-[state=open]:animate-none motion-reduce:data-[state=closed]:animate-none'
content: collapsibleContentMotion
}
} as const