fix(web): clamp preset card descriptions instead of sizing the roster
A preset publishes its own description, of any length, and `.cards` sizes rows with `grid-auto-rows: 1fr` — which makes every implicit row the same height, not just the row holding the tall card. One long description therefore set the height of the whole roster. The description now clamps to four lines and offers the rest through the shared Tooltip, attached only while the element actually overflows. Card height stays derived: with the description bounded, `grid-auto-rows: 1fr` already equalizes, and a card carrying the broken-preset reason or a revealed path still sizes itself. Tooltip gains an optional `maxWidth`; its default half-viewport cap renders a description wider than the settings dialog it belongs to. The failed-shape-check badge reads "Failed to load" rather than "Broken": discovery reports a composition that is missing, unreadable, or malformed, which overstates as damage. Fixes #2238
This commit is contained in:
@@ -161,15 +161,28 @@
|
||||
color: var(--dsw-alias-bg-layer-3);
|
||||
}
|
||||
|
||||
/* Bounded to four lines. A preset publishes its own description, so one long
|
||||
one would otherwise stretch every card in its grid row (`.cards` sizes rows
|
||||
1fr). Clamping is CSS alone: the whole text stays in the DOM for assistive
|
||||
tech, and the card offers it on hover when it is actually cut off. The
|
||||
description does not grow to fill the card — `-webkit-line-clamp` on a
|
||||
flex-stretched box leaves the clamp height and the box height disagreeing,
|
||||
so `.cardId` takes the free space with an auto margin instead. */
|
||||
.cardDesc {
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
flex: 1;
|
||||
min-height: 42px;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 4;
|
||||
overflow: hidden;
|
||||
/* A user-authored description may carry an unbreakable path or URL. */
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.cardId {
|
||||
margin-top: auto;
|
||||
font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
||||
font-size: 11px;
|
||||
color: var(--dsw-alias-label-dimmed);
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
* mounted once at session creation and nothing re-reads the file.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
Button, IconBrowseOutline16, IconCopyOutline16, IconFolderOpenOutline16, IconPlusOutline16, IconTrashOutline16, Modal,
|
||||
Button, IconBrowseOutline16, IconCopyOutline16, IconFolderOpenOutline16, IconPlusOutline16, IconTrashOutline16, Modal, Tooltip,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
@@ -137,6 +137,39 @@ function CopyDialog({ state, t, actions }: CopyDialogProps): ReactNode {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render one card's description, clamped by CSS and offered in full on hover.
|
||||
* The tooltip is attached only while the text is actually cut off, so a short
|
||||
* description does not answer a hover with a bubble repeating the card.
|
||||
* @param props.text - the description as rendered, already localized.
|
||||
* @returns the description element, tooltip-anchored while it overflows.
|
||||
*/
|
||||
function CardDescription({ text }: { text: string }): ReactNode {
|
||||
const ref = useRef<HTMLSpanElement | null>(null)
|
||||
const [truncated, setTruncated] = useState(false)
|
||||
useLayoutEffect(() => {
|
||||
const el = ref.current
|
||||
/* v8 ignore next -- the ref is attached before layout effects run. */
|
||||
if (el === null) return
|
||||
const measure = () => { setTruncated(el.scrollHeight > el.clientHeight) }
|
||||
measure()
|
||||
// Card width follows the settings pane, which resizes with the window.
|
||||
if (typeof ResizeObserver === 'undefined') return
|
||||
const observer = new ResizeObserver(measure)
|
||||
observer.observe(el)
|
||||
return () => { observer.disconnect() }
|
||||
}, [text])
|
||||
return (
|
||||
// Capped near the card's own width: the default half-viewport bubble would
|
||||
// spill a description out of the settings dialog and across the app behind it.
|
||||
<Tooltip label={text} side="bottom" delayMs={400} disabled={!truncated} maxWidth={360}>
|
||||
{/* The empty title stops the card body's native tooltip from climbing to
|
||||
this span: a cut-off description answers with one bubble, not two. */}
|
||||
<span ref={ref} className={css.cardDesc} title="">{text}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the Agent presets section content column.
|
||||
* @param props - composed slot props.
|
||||
@@ -247,7 +280,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
|
||||
</span>
|
||||
{row.isDefault ? <span className={css.inUse}>{t('inUse')}</span> : null}
|
||||
</span>
|
||||
<span className={css.cardDesc}>{text.description ?? t('noDescription')}</span>
|
||||
<CardDescription text={text.description ?? t('noDescription')} />
|
||||
{row.broken === undefined
|
||||
? null
|
||||
: <span className={css.cardBrokenReason} role="alert">{row.broken}</span>}
|
||||
|
||||
@@ -57,8 +57,8 @@ export const en: Record<AgentPresetSettingsKey, string> = {
|
||||
builtInGroup: 'Built-in',
|
||||
customGroup: 'Custom',
|
||||
noDescription: 'No description.',
|
||||
brokenBadge: 'Broken',
|
||||
brokenNoCopy: 'Broken presets cannot be duplicated',
|
||||
brokenBadge: 'Failed to load',
|
||||
brokenNoCopy: 'A preset that failed to load cannot be duplicated',
|
||||
copyOf: 'Copied from',
|
||||
composition: 'Composition (agent.cordis.yml)',
|
||||
cancel: 'Cancel',
|
||||
@@ -117,8 +117,8 @@ export const zh: Record<AgentPresetSettingsKey, string> = {
|
||||
builtInGroup: '内置',
|
||||
customGroup: '自定义',
|
||||
noDescription: '暂无描述。',
|
||||
brokenBadge: '已损坏',
|
||||
brokenNoCopy: '预设已损坏,无法复制',
|
||||
brokenBadge: '加载失败',
|
||||
brokenNoCopy: '预设加载失败,不能复制',
|
||||
copyOf: '复制自',
|
||||
composition: '组装(agent.cordis.yml)',
|
||||
cancel: '取消',
|
||||
|
||||
Reference in New Issue
Block a user