wip(web): agent preset UI flow — creator intro, custom group, subagent flash, chrome polish
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||||
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||||
import { IconThinkOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
import { IconAgentPresetOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||||
// Type-only: pulls the ui-conversation SlotMap merge (the header actions).
|
// Type-only: pulls the ui-conversation SlotMap merge (the header actions).
|
||||||
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||||
import type { AgentPresetSettingsState } from './settings-store.ts'
|
import type { AgentPresetSettingsState } from './settings-store.ts'
|
||||||
@@ -57,7 +57,7 @@ export function AgentPresetLabel({
|
|||||||
const text = option === undefined ? undefined : presetDisplayText(option, t)
|
const text = option === undefined ? undefined : presetDisplayText(option, t)
|
||||||
return (
|
return (
|
||||||
<span className={css.label} title={text?.description ?? t('headerHint')}>
|
<span className={css.label} title={text?.description ?? t('headerHint')}>
|
||||||
<IconThinkOutline16 className={css.icon} />
|
<IconAgentPresetOutline16 className={css.icon} />
|
||||||
{text?.name ?? preset}
|
{text?.name ?? preset}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -36,6 +36,59 @@
|
|||||||
color: var(--dsw-alias-label-primary);
|
color: var(--dsw-alias-label-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Introduce cue: the icon eases in on an overshoot-free expo curve, then the
|
||||||
|
name's characters fade up on a stagger (delays set inline per character).
|
||||||
|
All chars occupy their width from the start, so nothing reflows mid-run. */
|
||||||
|
.introIcon {
|
||||||
|
animation: seat-icon-in 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes seat-icon-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wraps the staggered characters into one flex item, so the chip's gap
|
||||||
|
applies around the name as a whole rather than between characters. */
|
||||||
|
.introText {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.introChar {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: pre;
|
||||||
|
opacity: 0;
|
||||||
|
animation: seat-char-in 0.4s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes seat-char-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.introIcon,
|
||||||
|
.introChar {
|
||||||
|
animation: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.chevron {
|
.chevron {
|
||||||
flex: none;
|
flex: none;
|
||||||
color: var(--dsw-alias-label-caption);
|
color: var(--dsw-alias-label-caption);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||||
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||||
import { IconChevronDownOutline14, IconThinkOutline16, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
|
import { IconAgentPresetOutline16, IconChevronDownOutline14, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||||
// Type-only: pulls the ui-conversation SlotMap merge (the hero seat).
|
// Type-only: pulls the ui-conversation SlotMap merge (the hero seat).
|
||||||
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||||
import type { AgentPresetSeatState } from './seat-store.ts'
|
import type { AgentPresetSeatState } from './seat-store.ts'
|
||||||
@@ -32,8 +32,17 @@ export interface AgentPresetSeatInjected {
|
|||||||
load: () => Promise<void>
|
load: () => Promise<void>
|
||||||
/** Stage one preset for the next session. */
|
/** Stage one preset for the next session. */
|
||||||
select: (id: string) => Promise<void>
|
select: (id: string) => Promise<void>
|
||||||
|
/** Clear the one-shot introduce cue once the chip has played it. */
|
||||||
|
introduced: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Introduce timeline: the icon eases in first; the name's characters start
|
||||||
|
fading up once the icon has mostly landed, one every stagger tick, each
|
||||||
|
taking the fade duration to settle. The cue clears after the last one. */
|
||||||
|
const INTRO_TEXT_DELAY_MS = 300
|
||||||
|
const INTRO_CHAR_STAGGER_MS = 60
|
||||||
|
const INTRO_CHAR_FADE_MS = 400
|
||||||
|
|
||||||
/** Full component props. */
|
/** Full component props. */
|
||||||
export type AgentPresetSeatProps =
|
export type AgentPresetSeatProps =
|
||||||
PropsRuntime<'conversation.hero.agentPreset'>
|
PropsRuntime<'conversation.hero.agentPreset'>
|
||||||
@@ -45,7 +54,7 @@ export type AgentPresetSeatProps =
|
|||||||
* @param props - composed slot props.
|
* @param props - composed slot props.
|
||||||
* @returns the chip, or null when the deployment composes no presets.
|
* @returns the chip, or null when the deployment composes no presets.
|
||||||
*/
|
*/
|
||||||
export function AgentPresetSeat({ load, select, useAgentPresetSeat, t }: AgentPresetSeatProps) {
|
export function AgentPresetSeat({ load, select, introduced, useAgentPresetSeat, t }: AgentPresetSeatProps) {
|
||||||
const state = useAgentPresetSeat(snapshot => snapshot)
|
const state = useAgentPresetSeat(snapshot => snapshot)
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
@@ -53,12 +62,52 @@ export function AgentPresetSeat({ load, select, useAgentPresetSeat, t }: AgentPr
|
|||||||
void load()
|
void load()
|
||||||
}, [load])
|
}, [load])
|
||||||
|
|
||||||
// Nothing to choose between: the deployment composes no presets and every
|
|
||||||
// session shares the host composition.
|
|
||||||
if (state.options.length === 0 || state.current === '') return null
|
|
||||||
|
|
||||||
const chosen = state.options.find(option => option.id === state.current)
|
const chosen = state.options.find(option => option.id === state.current)
|
||||||
const chosenText = chosen === undefined ? undefined : presetDisplayText(chosen, t)
|
const chosenText = chosen === undefined ? undefined : presetDisplayText(chosen, t)
|
||||||
|
const label = chosenText?.name ?? state.current
|
||||||
|
const ready = state.options.length > 0 && state.current !== ''
|
||||||
|
|
||||||
|
// The introduce cue: the pick was staged from another screen (the settings
|
||||||
|
// creator entry), so the chip announces it — the icon eases in and each
|
||||||
|
// character of the name fades up on a stagger (CSS owns the motion; this
|
||||||
|
// effect only arms it and acknowledges the cue once the run is over).
|
||||||
|
const [introducing, setIntroducing] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!state.introduce || !ready) return
|
||||||
|
const characters = Array.from(label)
|
||||||
|
if (characters.length === 0 || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||||
|
introduced()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setIntroducing(true)
|
||||||
|
const done = window.setTimeout(() => {
|
||||||
|
setIntroducing(false)
|
||||||
|
introduced()
|
||||||
|
}, INTRO_TEXT_DELAY_MS + characters.length * INTRO_CHAR_STAGGER_MS + INTRO_CHAR_FADE_MS)
|
||||||
|
return () => { window.clearTimeout(done) }
|
||||||
|
}, [state.introduce, ready, label, introduced])
|
||||||
|
|
||||||
|
// Nothing to choose between: the deployment composes no presets and every
|
||||||
|
// session shares the host composition.
|
||||||
|
if (!ready) return null
|
||||||
|
|
||||||
|
// One wrapper span: the chip is a flex row with a gap, so loose character
|
||||||
|
// spans would each pick up the gap between them.
|
||||||
|
const shownLabel = introducing
|
||||||
|
? (
|
||||||
|
<span className={css.introText}>
|
||||||
|
{Array.from(label).map((character, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className={css.introChar}
|
||||||
|
style={{ animationDelay: `${INTRO_TEXT_DELAY_MS + index * INTRO_CHAR_STAGGER_MS}ms` }}
|
||||||
|
>
|
||||||
|
{character}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
: label
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
@@ -95,8 +144,8 @@ export function AgentPresetSeat({ load, select, useAgentPresetSeat, t }: AgentPr
|
|||||||
disabled={state.busy}
|
disabled={state.busy}
|
||||||
onClick={() => { setOpen(value => !value) }}
|
onClick={() => { setOpen(value => !value) }}
|
||||||
>
|
>
|
||||||
<IconThinkOutline16 className={css.seatIcon} />
|
<IconAgentPresetOutline16 className={introducing ? `${css.seatIcon} ${css.introIcon}` : css.seatIcon} />
|
||||||
{chosenText?.name ?? state.current}
|
{shownLabel}
|
||||||
<IconChevronDownOutline14 className={css.chevron} />
|
<IconChevronDownOutline14 className={css.chevron} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -363,6 +363,7 @@
|
|||||||
create button vacated. Dashed like the Models page's add affordances: it
|
create button vacated. Dashed like the Models page's add affordances: it
|
||||||
reads as a place a preset will appear, not a command. */
|
reads as a place a preset will appear, not a command. */
|
||||||
.creatorButton {
|
.creatorButton {
|
||||||
|
box-sizing: border-box;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -372,17 +373,18 @@
|
|||||||
border: 1px dashed var(--dsw-alias-border-l3);
|
border: 1px dashed var(--dsw-alias-border-l3);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
background: none;
|
line-height: 22px;
|
||||||
color: inherit;
|
background: transparent;
|
||||||
|
color: var(--dsw-alias-label-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.creatorButton:hover:not(:disabled) {
|
.creatorButton:hover:not(:disabled) {
|
||||||
background: var(--dsw-alias-bg-layer-1);
|
background: var(--dsw-alias-interactive-bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.creatorButton:disabled {
|
.creatorButton:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.4;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,6 +171,30 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The guided alternative to copying: the self-referential preset can
|
||||||
|
read this very composition and author a new one in conversation.
|
||||||
|
Offered only where that preset is actually on the roster and a
|
||||||
|
session can be landed; without a writable root the draft could
|
||||||
|
never be discovered, so the reason rides the disabled button. */
|
||||||
|
const creatorButton = props.startCreatorDraft !== undefined && state.rows.some(row => row.id === 'cordis')
|
||||||
|
? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={css.creatorButton}
|
||||||
|
disabled={!state.authorable}
|
||||||
|
title={state.authorable ? undefined : t('duplicateUnavailable')}
|
||||||
|
onClick={() => {
|
||||||
|
props.startCreatorDraft?.()
|
||||||
|
props.close()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Same glyph as the Models page's add affordances. */}
|
||||||
|
<IconPlusOutline16 size={14} />
|
||||||
|
{t('creatorDraft')}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={css.section}>
|
<div className={css.section}>
|
||||||
<h2 className={css.title}>{t('nav')}</h2>
|
<h2 className={css.title}>{t('nav')}</h2>
|
||||||
@@ -180,147 +204,130 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
|
|||||||
const group = state.rows
|
const group = state.rows
|
||||||
.filter(row => row.trust === trust)
|
.filter(row => row.trust === trust)
|
||||||
.map(row => ({ row, text: presetDisplayText(row, t) }))
|
.map(row => ({ row, text: presetDisplayText(row, t) }))
|
||||||
if (group.length === 0) return null
|
// The custom group is where a preset of one's own will appear, so it
|
||||||
|
// stays on screen even while empty: heading plus the creator entry.
|
||||||
|
const tail = trust === 'user' ? creatorButton : null
|
||||||
|
if (group.length === 0 && tail === null) return null
|
||||||
return (
|
return (
|
||||||
<section key={trust} className={css.group}>
|
<section key={trust} className={css.group}>
|
||||||
<h3 className={css.groupHead}>{heading}</h3>
|
<h3 className={css.groupHead}>{heading}</h3>
|
||||||
<ul className={css.cards}>
|
{group.length === 0 ? null : (
|
||||||
{group.map(({ row, text }) => (
|
<ul className={css.cards}>
|
||||||
<li
|
{group.map(({ row, text }) => (
|
||||||
key={row.id}
|
<li
|
||||||
className={row.broken !== undefined
|
key={row.id}
|
||||||
? `${css.card} ${css.cardBroken}`
|
className={row.broken !== undefined
|
||||||
: row.isDefault ? `${css.card} ${css.cardActive}` : css.card}
|
? `${css.card} ${css.cardBroken}`
|
||||||
>
|
: row.isDefault ? `${css.card} ${css.cardActive}` : css.card}
|
||||||
{/* The card body IS the control: picking a preset is the
|
>
|
||||||
|
{/* The card body IS the control: picking a preset is the
|
||||||
common act, so it should not hide behind a small button.
|
common act, so it should not hide behind a small button.
|
||||||
The action row sits outside it — nesting buttons is
|
The action row sits outside it — nesting buttons is
|
||||||
invalid, and these act on the card rather than select it.
|
invalid, and these act on the card rather than select it.
|
||||||
A broken preset cannot compose a session, so its body is
|
A broken preset cannot compose a session, so its body is
|
||||||
disabled and the card says why instead of offering it. */}
|
disabled and the card says why instead of offering it. */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={css.cardMain}
|
className={css.cardMain}
|
||||||
aria-pressed={row.isDefault}
|
aria-pressed={row.isDefault}
|
||||||
disabled={row.isDefault || row.broken !== undefined}
|
disabled={row.isDefault || row.broken !== undefined}
|
||||||
// Without this the name is the whole card read aloud —
|
// Without this the name is the whole card read aloud —
|
||||||
// title, badge, description, id.
|
// title, badge, description, id.
|
||||||
aria-label={`${row.broken !== undefined ? t('brokenBadge') : row.isDefault ? t('inUse') : t('setDefault')}: ${text.name}`}
|
aria-label={`${row.broken !== undefined ? t('brokenBadge') : row.isDefault ? t('inUse') : t('setDefault')}: ${text.name}`}
|
||||||
title={row.broken ?? (row.isDefault ? t('inUse') : t('setDefault'))}
|
title={row.broken ?? (row.isDefault ? t('inUse') : t('setDefault'))}
|
||||||
onClick={() => { void props.makeDefault(row.id) }}
|
onClick={() => { void props.makeDefault(row.id) }}
|
||||||
>
|
>
|
||||||
<span className={css.cardHead}>
|
<span className={css.cardHead}>
|
||||||
<span className={css.cardName}>{text.name}</span>
|
<span className={css.cardName}>{text.name}</span>
|
||||||
{row.broken !== undefined
|
{row.broken !== undefined
|
||||||
? <span className={css.brokenBadge}>{t('brokenBadge')}</span>
|
? <span className={css.brokenBadge}>{t('brokenBadge')}</span>
|
||||||
: null}
|
: null}
|
||||||
<span className={css.badge}>
|
<span className={css.badge}>
|
||||||
{row.trust === 'user' ? t('userTrust') : t('builtIn')}
|
{row.trust === 'user' ? t('userTrust') : t('builtIn')}
|
||||||
|
</span>
|
||||||
|
{row.isDefault ? <span className={css.inUse}>{t('inUse')}</span> : null}
|
||||||
</span>
|
</span>
|
||||||
{row.isDefault ? <span className={css.inUse}>{t('inUse')}</span> : null}
|
<span className={css.cardDesc}>{text.description ?? t('noDescription')}</span>
|
||||||
</span>
|
{row.broken === undefined
|
||||||
<span className={css.cardDesc}>{text.description ?? t('noDescription')}</span>
|
? null
|
||||||
{row.broken === undefined
|
: <span className={css.cardBrokenReason} role="alert">{row.broken}</span>}
|
||||||
? null
|
<code className={css.cardId}>{row.id}</code>
|
||||||
: <span className={css.cardBrokenReason} role="alert">{row.broken}</span>}
|
</button>
|
||||||
<code className={css.cardId}>{row.id}</code>
|
<div className={css.cardFoot}>
|
||||||
</button>
|
{/* Shipped presets are the compositions a copy starts
|
||||||
<div className={css.cardFoot}>
|
|
||||||
{/* Shipped presets are the compositions a copy starts
|
|
||||||
from, so READING one is the point; a custom preset is
|
from, so READING one is the point; a custom preset is
|
||||||
edited in its files instead, which the location action
|
edited in its files instead, which the location action
|
||||||
leads to. A broken shipped preset has no readable
|
leads to. A broken shipped preset has no readable
|
||||||
composition to offer, so its viewer is withheld; a
|
composition to offer, so its viewer is withheld; a
|
||||||
broken custom one keeps the location action — the
|
broken custom one keeps the location action — the
|
||||||
files are where it gets fixed. */}
|
files are where it gets fixed. */}
|
||||||
{row.trust === 'system'
|
{row.trust === 'system'
|
||||||
? row.broken === undefined
|
? row.broken === undefined
|
||||||
? (
|
? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={css.iconButton}
|
||||||
|
data-tip={t('view')}
|
||||||
|
aria-label={`${t('view')}: ${text.name}`}
|
||||||
|
onClick={() => { void props.view(row.id) }}
|
||||||
|
>
|
||||||
|
<IconBrowseOutline16 />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
: (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={css.iconButton}
|
className={css.iconButton}
|
||||||
data-tip={t('view')}
|
data-tip={state.hasDocument ? t('openLocation') : t('showLocation')}
|
||||||
aria-label={`${t('view')}: ${text.name}`}
|
aria-label={`${state.hasDocument ? t('openLocation') : t('showLocation')}: ${text.name}`}
|
||||||
onClick={() => { void props.view(row.id) }}
|
onClick={() => { void props.openLocation(row.id) }}
|
||||||
>
|
>
|
||||||
<IconBrowseOutline16 />
|
<IconFolderOpenOutline16 />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={css.iconButton}
|
||||||
|
disabled={!state.authorable || row.broken !== undefined}
|
||||||
|
data-tip={row.broken !== undefined
|
||||||
|
? t('brokenNoCopy')
|
||||||
|
: state.authorable ? t('duplicate') : t('duplicateUnavailable')}
|
||||||
|
aria-label={`${t('duplicate')}: ${text.name}`}
|
||||||
|
onClick={() => { props.beginCopy(row.id) }}
|
||||||
|
>
|
||||||
|
<IconCopyOutline16 />
|
||||||
|
</button>
|
||||||
|
{row.trust === 'user'
|
||||||
|
? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${css.iconButton} ${css.iconDanger}`}
|
||||||
|
data-tip={t('delete')}
|
||||||
|
aria-label={`${t('delete')}: ${text.name}`}
|
||||||
|
onClick={() => { props.confirmDelete(row.id) }}
|
||||||
|
>
|
||||||
|
<IconTrashOutline16 />
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
: null
|
: null}
|
||||||
|
</div>
|
||||||
|
{state.revealedPaths[row.id] === undefined
|
||||||
|
? null
|
||||||
: (
|
: (
|
||||||
<button
|
<p className={css.revealedPath}>
|
||||||
type="button"
|
<span className={css.revealedPathLabel}>{t('revealedPathLabel')}</span>
|
||||||
className={css.iconButton}
|
<code>{state.revealedPaths[row.id]}</code>
|
||||||
data-tip={state.hasDocument ? t('openLocation') : t('showLocation')}
|
</p>
|
||||||
aria-label={`${state.hasDocument ? t('openLocation') : t('showLocation')}: ${text.name}`}
|
|
||||||
onClick={() => { void props.openLocation(row.id) }}
|
|
||||||
>
|
|
||||||
<IconFolderOpenOutline16 />
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
<button
|
</li>
|
||||||
type="button"
|
))}
|
||||||
className={css.iconButton}
|
</ul>
|
||||||
disabled={!state.authorable || row.broken !== undefined}
|
)}
|
||||||
data-tip={row.broken !== undefined
|
{tail}
|
||||||
? t('brokenNoCopy')
|
|
||||||
: state.authorable ? t('duplicate') : t('duplicateUnavailable')}
|
|
||||||
aria-label={`${t('duplicate')}: ${text.name}`}
|
|
||||||
onClick={() => { props.beginCopy(row.id) }}
|
|
||||||
>
|
|
||||||
<IconCopyOutline16 />
|
|
||||||
</button>
|
|
||||||
{row.trust === 'user'
|
|
||||||
? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`${css.iconButton} ${css.iconDanger}`}
|
|
||||||
data-tip={t('delete')}
|
|
||||||
aria-label={`${t('delete')}: ${text.name}`}
|
|
||||||
onClick={() => { props.confirmDelete(row.id) }}
|
|
||||||
>
|
|
||||||
<IconTrashOutline16 />
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
{state.revealedPaths[row.id] === undefined
|
|
||||||
? null
|
|
||||||
: (
|
|
||||||
<p className={css.revealedPath}>
|
|
||||||
<span className={css.revealedPathLabel}>{t('revealedPathLabel')}</span>
|
|
||||||
<code>{state.revealedPaths[row.id]}</code>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
{/* The guided alternative to copying: the self-referential preset can
|
|
||||||
read this very composition and author a new one in conversation.
|
|
||||||
Offered only where that preset is actually on the roster and a
|
|
||||||
session can be landed; without a writable root the draft could
|
|
||||||
never be discovered, so the reason rides the disabled button. */}
|
|
||||||
{props.startCreatorDraft !== undefined && state.rows.some(row => row.id === 'cordis')
|
|
||||||
? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={css.creatorButton}
|
|
||||||
disabled={!state.authorable}
|
|
||||||
title={state.authorable ? undefined : t('duplicateUnavailable')}
|
|
||||||
onClick={() => {
|
|
||||||
props.startCreatorDraft?.()
|
|
||||||
props.close()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Same glyph as the Models page's add affordances. */}
|
|
||||||
<IconPlusOutline16 size={14} />
|
|
||||||
{t('creatorDraft')}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
<CopyDialog
|
<CopyDialog
|
||||||
state={state}
|
state={state}
|
||||||
t={t}
|
t={t}
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export function apply(ctx: ClientContext): void {
|
|||||||
hooks: { agentPresetSeat: seat.store },
|
hooks: { agentPresetSeat: seat.store },
|
||||||
load: () => seat.load(),
|
load: () => seat.load(),
|
||||||
select: (id: string) => seat.select(id),
|
select: (id: string) => seat.select(id),
|
||||||
|
introduced: () => { seat.introduced() },
|
||||||
})
|
})
|
||||||
|
|
||||||
const labelInjected = (): AgentPresetLabelInjected => ({
|
const labelInjected = (): AgentPresetLabelInjected => ({
|
||||||
@@ -146,7 +147,9 @@ export function apply(ctx: ClientContext): void {
|
|||||||
// on: the chip's list-change applier composes the blank session the
|
// on: the chip's list-change applier composes the blank session the
|
||||||
// workspace connect produces or reuses.
|
// workspace connect produces or reuses.
|
||||||
creatorDraft = () => {
|
creatorDraft = () => {
|
||||||
seat.stage('cordis')
|
// The introduce cue makes the chip announce the pick the user never
|
||||||
|
// made on this screen — the stage happened back in settings.
|
||||||
|
seat.stage('cordis', true)
|
||||||
scope.workspaces.startSession()
|
scope.workspaces.startSession()
|
||||||
}
|
}
|
||||||
const chip = scope.slots.register({
|
const chip = scope.slots.register({
|
||||||
|
|||||||
@@ -26,10 +26,16 @@ export interface AgentPresetSeatState {
|
|||||||
/** A rejected apply's message, cleared by the next attempt. */
|
/** A rejected apply's message, cleared by the next attempt. */
|
||||||
error: string | null
|
error: string | null
|
||||||
busy: boolean
|
busy: boolean
|
||||||
|
/**
|
||||||
|
* One-shot cue that the chip should introduce itself (the creator-draft
|
||||||
|
* entry staged the pick from another screen, so the user never touched the
|
||||||
|
* chip); the renderer clears it via `introduced()` once played.
|
||||||
|
*/
|
||||||
|
introduce: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const INITIAL: AgentPresetSeatState = {
|
const INITIAL: AgentPresetSeatState = {
|
||||||
options: [], current: '', error: null, busy: false,
|
options: [], current: '', error: null, busy: false, introduce: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One session's identity and whether it has started. */
|
/** One session's identity and whether it has started. */
|
||||||
@@ -121,10 +127,18 @@ export class AgentPresetSeatController {
|
|||||||
* list-change applier, which fires when the started session becomes
|
* list-change applier, which fires when the started session becomes
|
||||||
* current.
|
* current.
|
||||||
* @param id - the preset to stage.
|
* @param id - the preset to stage.
|
||||||
|
* @param introduce - true when the stage came from another screen and the
|
||||||
|
* chip should announce itself on the session it lands on.
|
||||||
*/
|
*/
|
||||||
stage(id: string): void {
|
stage(id: string, introduce = false): void {
|
||||||
this.staged = id
|
this.staged = id
|
||||||
this.set({ current: id, error: null })
|
this.set({ current: id, error: null, introduce })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Acknowledge the introduction cue once the chip has played it. */
|
||||||
|
introduced(): void {
|
||||||
|
if (!this.store.getSnapshot().introduce) return
|
||||||
|
this.set({ introduce: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const SEAT_READY: AgentPresetSeatState = {
|
|||||||
],
|
],
|
||||||
busy: false,
|
busy: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
introduce: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRow(state: Partial<AgentPresetSettingsState> = {}) {
|
function renderRow(state: Partial<AgentPresetSettingsState> = {}) {
|
||||||
@@ -56,7 +57,11 @@ function renderRow(state: Partial<AgentPresetSettingsState> = {}) {
|
|||||||
|
|
||||||
function renderSeat(state: Partial<AgentPresetSeatState> = {}) {
|
function renderSeat(state: Partial<AgentPresetSeatState> = {}) {
|
||||||
const store = createSnapshotStore<AgentPresetSeatState>({ ...SEAT_READY, ...state })
|
const store = createSnapshotStore<AgentPresetSeatState>({ ...SEAT_READY, ...state })
|
||||||
const actions = { load: vi.fn(() => Promise.resolve()), select: vi.fn(() => Promise.resolve()) }
|
const actions = {
|
||||||
|
load: vi.fn(() => Promise.resolve()),
|
||||||
|
select: vi.fn(() => Promise.resolve()),
|
||||||
|
introduced: vi.fn(),
|
||||||
|
}
|
||||||
render(<AgentPresetSeat {...({
|
render(<AgentPresetSeat {...({
|
||||||
...actions,
|
...actions,
|
||||||
useAgentPresetSeat: bindSnapshotSelector(store),
|
useAgentPresetSeat: bindSnapshotSelector(store),
|
||||||
|
|||||||
@@ -147,10 +147,10 @@ export function PermissionSelect({ value, locked, command, t }: PermissionSelect
|
|||||||
<span className={css.triggerIcon} aria-hidden>{permissionGlyph(currentValue)}</span>
|
<span className={css.triggerIcon} aria-hidden>{permissionGlyph(currentValue)}</span>
|
||||||
)}
|
)}
|
||||||
<span className={css.triggerLabel}>{current === undefined ? displayName(currentValue) : optionLabel(current)}</span>
|
<span className={css.triggerLabel}>{current === undefined ? displayName(currentValue) : optionLabel(current)}</span>
|
||||||
{/* Same glyph + open rotation as the sibling ModelSelect trigger. */}
|
{/* Same glyph + open rotation as the sibling ModelSelect trigger;
|
||||||
<span className={clsx(css.chevron, open && css.chevronOpen)} aria-hidden>
|
class on the svg itself — an inline wrapper span leaves
|
||||||
<IconChevronDownOutline14 />
|
baseline descent under the icon and floats it off-center. */}
|
||||||
</span>
|
<IconChevronDownOutline14 className={clsx(css.chevron, open && css.chevronOpen)} />
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -349,6 +349,35 @@ export const IconThinkOutline16 = ({ size = 16, className }: IconProps) => (
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** ic_ds_agent_preset_outline_16. The three node interiors knock out to transparency via mask so the glyph sits on any background. */
|
||||||
|
export const IconAgentPresetOutline16 = ({ size = 16, className }: IconProps) => (
|
||||||
|
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="mask0_agent_preset_16" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
|
||||||
|
<rect width="16" height="16" fill="white" />
|
||||||
|
<circle cx="7.9995" cy="3.28319" r="1.712" fill="black" />
|
||||||
|
<circle cx="3.51122" cy="11.3855" r="1.712" fill="black" />
|
||||||
|
<circle cx="12.4878" cy="11.3855" r="1.712" fill="black" />
|
||||||
|
</mask>
|
||||||
|
<path
|
||||||
|
mask="url(#mask0_agent_preset_16)"
|
||||||
|
d="M12.2881 11.0425C12.6002 11.3723 13.0413 11.5786 13.5312 11.5786L13.5342 11.5776C13.1476 12.3233 12.6119 12.9785 11.9639 13.5005C10.9327 14.3309 9.6199 14.8286 8.19336 14.8286C7.29864 14.8285 6.45056 14.6313 5.6875 14.2808C6.08309 14.0281 6.36707 13.6189 6.45215 13.1392C6.99022 13.3561 7.57767 13.476 8.19336 13.4761C9.30019 13.4761 10.3157 13.0915 11.1152 12.4478C11.5935 12.0626 11.9924 11.5848 12.2881 11.0425ZM4.14746 4.36475C4.25569 4.83228 4.55488 5.2247 4.95898 5.4585C4.07956 6.30639 3.53144 7.49605 3.53125 8.81396C3.53125 9.69534 3.77613 10.5202 4.20117 11.2231C3.74959 11.3817 3.38395 11.7232 3.19531 12.1597C2.5541 11.2032 2.17969 10.052 2.17969 8.81396C2.17989 7.05087 2.93868 5.4646 4.14746 4.36475ZM8.19336 2.80029C8.85717 2.80029 9.49784 2.90834 10.0967 3.10791C12.3237 3.85044 13.9725 5.86061 14.1846 8.28369C13.9832 8.20048 13.7627 8.15382 13.5312 8.15381C13.2802 8.15381 13.042 8.20907 12.8271 8.30615C12.6281 6.47264 11.3666 4.95616 9.66895 4.39014C9.2063 4.236 8.70989 4.15186 8.19336 4.15186C7.96112 4.15189 7.7329 4.16981 7.50977 4.20264C7.51947 4.12886 7.52637 4.05348 7.52637 3.97705C7.52628 3.56604 7.3811 3.18914 7.13965 2.89404C7.48183 2.83352 7.83381 2.80033 8.19336 2.80029Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M9.1123 3.28271C9.11205 2.66858 8.61322 2.17041 7.99902 2.17041C7.38504 2.17067 6.88697 2.66874 6.88672 3.28271C6.88672 3.89691 7.38489 4.39574 7.99902 4.396C8.61338 4.396 9.1123 3.89707 9.1123 3.28271ZM10.3115 3.28271C10.3115 4.55981 9.27612 5.59521 7.99902 5.59521C6.72214 5.59496 5.6875 4.55965 5.6875 3.28271C5.68776 2.00599 6.7223 0.971447 7.99902 0.971191C9.27596 0.971191 10.3113 2.00584 10.3115 3.28271Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M4.62402 11.385C4.62377 10.7709 4.12494 10.2727 3.51074 10.2727C2.89676 10.273 2.39869 10.771 2.39844 11.385C2.39844 11.9992 2.89661 12.498 3.51074 12.4983C4.1251 12.4983 4.62402 11.9994 4.62402 11.385ZM5.82324 11.385C5.82324 12.6621 4.78784 13.6975 3.51074 13.6975C2.23386 13.6973 1.19922 12.6619 1.19922 11.385C1.19947 10.1083 2.23402 9.07374 3.51074 9.07349C4.78768 9.07349 5.82299 10.1081 5.82324 11.385Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M13.6006 11.385C13.6003 10.7709 13.1015 10.2727 12.4873 10.2727C11.8733 10.273 11.3753 10.771 11.375 11.385C11.375 11.9992 11.8732 12.498 12.4873 12.4983C13.1017 12.4983 13.6006 11.9994 13.6006 11.385ZM14.7998 11.385C14.7998 12.6621 13.7644 13.6975 12.4873 13.6975C11.2104 13.6973 10.1758 12.6619 10.1758 11.385C10.176 10.1083 11.2106 9.07374 12.4873 9.07349C13.7642 9.07349 14.7995 10.1081 14.7998 11.385Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
/** ic_ds_browse_outline_16 */
|
/** ic_ds_browse_outline_16 */
|
||||||
export const IconBrowseOutline16 = ({ size = 16, className }: IconProps) => (
|
export const IconBrowseOutline16 = ({ size = 16, className }: IconProps) => (
|
||||||
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 800px;
|
width: 800px;
|
||||||
height: min(800px, calc(100vh - 48px));
|
height: min(824px, calc(100vh - 48px));
|
||||||
max-width: calc(100vw - 48px);
|
max-width: calc(100vw - 48px);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -205,11 +205,11 @@
|
|||||||
background: var(--dsw-alias-interactive-bg-hover);
|
background: var(--dsw-alias-interactive-bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Options area (figma Options 501:29983): pad (24,0,24,8), scrolls. */
|
/* Options area (figma Options 501:29983): pad (24,0,24,24), scrolls. */
|
||||||
.options {
|
.options {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0 24px 8px;
|
padding: 0 24px 24px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
import { useCallback, useEffect, useId, useRef, useState } from 'react'
|
import { useCallback, useEffect, useId, useRef, useState } from 'react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import {
|
import {
|
||||||
IconCloseOutline16, IconDataOutline16, IconSettingsOutline16, IconThinkOutline16,
|
IconAgentPresetOutline16, IconCloseOutline16, IconDataOutline16, IconSettingsOutline16,
|
||||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||||
import type { SettingsRootComponentProps, SettingsSectionRow } from './contract/slots.ts'
|
import type { SettingsRootComponentProps, SettingsSectionRow } from './contract/slots.ts'
|
||||||
import css from './SettingsRoot.module.css'
|
import css from './SettingsRoot.module.css'
|
||||||
@@ -22,7 +22,7 @@ import css from './SettingsRoot.module.css'
|
|||||||
/** Nav glyph by section id; unknown ids fall back to the settings gear. */
|
/** Nav glyph by section id; unknown ids fall back to the settings gear. */
|
||||||
function navIcon(id: string) {
|
function navIcon(id: string) {
|
||||||
if (id === 'models') return <IconDataOutline16 className={css.navIcon} size={16} />
|
if (id === 'models') return <IconDataOutline16 className={css.navIcon} size={16} />
|
||||||
if (id === 'agent-presets') return <IconThinkOutline16 className={css.navIcon} size={16} />
|
if (id === 'agent-presets') return <IconAgentPresetOutline16 className={css.navIcon} size={16} />
|
||||||
return <IconSettingsOutline16 className={css.navIcon} size={16} />
|
return <IconSettingsOutline16 className={css.navIcon} size={16} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
padding: 8px 0 8px 4px;
|
padding: 8px 0 8px 4px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 8px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -157,8 +157,8 @@
|
|||||||
color: var(--dsw-alias-label-primary);
|
color: var(--dsw-alias-label-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* New Session: 38px capsule (figma 133:7634); collapsed it renders as the
|
/* New Session: 38px bar, 12px radius (figma 133:7634 geometry, squared-off
|
||||||
rail's plain icon control. */
|
corners); collapsed it renders as the rail's plain icon control. */
|
||||||
.newSession {
|
.newSession {
|
||||||
flex: none;
|
flex: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
margin: 0 2px 20px; /* bottom: former headerBlock padBottom 12 + root gap 8 */
|
margin: 0 2px 20px; /* bottom: former headerBlock padBottom 12 + root gap 8 */
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid var(--dsw-alias-border-l2);
|
border: 1px solid var(--dsw-alias-border-l2);
|
||||||
border-radius: 24px;
|
border-radius: 12px;
|
||||||
background: var(--dsw-alias-button-elevated-fill);
|
background: var(--dsw-alias-button-elevated-fill);
|
||||||
color: var(--dsw-alias-label-primary);
|
color: var(--dsw-alias-label-primary);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
@@ -519,8 +519,14 @@ export function SubagentCatalogAction({
|
|||||||
observedCatalogs.current.clear()
|
observedCatalogs.current.clear()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Visibility needs evidence of children (entries, summary-known descendants,
|
||||||
|
// or a failed load worth retrying). A bare loading catalog is not evidence:
|
||||||
|
// selecting any session schedules a refresh whose loading snapshot would
|
||||||
|
// otherwise flash the action in and out on childless sessions.
|
||||||
const visible = presentedCatalog !== undefined
|
const visible = presentedCatalog !== undefined
|
||||||
&& (presentedCatalog.state !== 'ready' || presentedCatalog.entries.length > 0)
|
&& (presentedCatalog.state === 'error'
|
||||||
|
|| presentedCatalog.entries.length > 0
|
||||||
|
|| descendantCount > 0)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible || !open) return
|
if (visible || !open) return
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
|
|||||||
@@ -522,22 +522,23 @@ describe('SubagentCatalogAction', () => {
|
|||||||
expect(staleEmpty.openChild).not.toHaveBeenCalled()
|
expect(staleEmpty.openChild).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders empty loading and fallback error states without focusable rows', async () => {
|
it('hides a bare loading catalog and keeps the error fallback without focusable rows', async () => {
|
||||||
|
// Selecting any session schedules a catalog refresh; a loading snapshot
|
||||||
|
// with no other evidence of children must not flash the action in.
|
||||||
const loading = props(catalog({ entries: [], state: 'loading' }))
|
const loading = props(catalog({ entries: [], state: 'loading' }))
|
||||||
const view = render(<SubagentCatalogAction {...loading} />)
|
const view = render(<SubagentCatalogAction {...loading} />)
|
||||||
const trigger = screen.getByRole('button', { name: /0 个子代理/ })
|
expect(screen.queryByRole('button')).toBeNull()
|
||||||
fireEvent.click(trigger)
|
|
||||||
expect(screen.getByText('正在加载子代理…')).toBeTruthy()
|
|
||||||
fireEvent.keyDown(trigger, { key: 'ArrowDown' })
|
|
||||||
await Promise.resolve()
|
|
||||||
expect(screen.getByRole('tree')).toBeTruthy()
|
|
||||||
fireEvent.keyDown(screen.getByRole('tree'), { key: 'ArrowUp' })
|
|
||||||
view.unmount()
|
view.unmount()
|
||||||
|
|
||||||
const failed = props(catalog({ entries: [], state: 'error', error: null }))
|
const failed = props(catalog({ entries: [], state: 'error', error: null }))
|
||||||
render(<SubagentCatalogAction {...failed} />)
|
render(<SubagentCatalogAction {...failed} />)
|
||||||
fireEvent.click(screen.getByRole('button', { name: /0 个子代理/ }))
|
const trigger = screen.getByRole('button', { name: /0 个子代理/ })
|
||||||
|
fireEvent.click(trigger)
|
||||||
expect(screen.getByText('无法加载子代理')).toBeTruthy()
|
expect(screen.getByText('无法加载子代理')).toBeTruthy()
|
||||||
|
fireEvent.keyDown(trigger, { key: 'ArrowDown' })
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(screen.getByRole('tree')).toBeTruthy()
|
||||||
|
fireEvent.keyDown(screen.getByRole('tree'), { key: 'ArrowUp' })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('navigates from outside the tree and tolerates a deferred focus after unmount', async () => {
|
it('navigates from outside the tree and tolerates a deferred focus after unmount', async () => {
|
||||||
|
|||||||
@@ -64,7 +64,8 @@
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search input: 38px capsule (figma 133:7649); rail state renders it as the
|
/* Search input: 38px bar, 12px radius (figma 133:7649 geometry, squared-off
|
||||||
|
corners); rail state renders it as the
|
||||||
region's search control. Upstream binds a dedicated design-system variable
|
region's search control. Upstream binds a dedicated design-system variable
|
||||||
(light #F1F3F5 / dark #1B1B1C) matching no shipped alias — a component
|
(light #F1F3F5 / dark #1B1B1C) matching no shipped alias — a component
|
||||||
token pinned to the static scale mirrors it. */
|
token pinned to the static scale mirrors it. */
|
||||||
@@ -79,7 +80,7 @@
|
|||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid var(--dsw-alias-border-l2);
|
border: 1px solid var(--dsw-alias-border-l2);
|
||||||
border-radius: 24px;
|
border-radius: 12px;
|
||||||
background: var(--dsh-search-input-fill);
|
background: var(--dsh-search-input-fill);
|
||||||
color: var(--dsw-alias-label-caption);
|
color: var(--dsw-alias-label-caption);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
Reference in New Issue
Block a user