Files
deepseek-harness/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx
imccyu 1e10966ef6 wip
fix: docs
2026-07-31 02:13:53 +08:00

135 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Hero chrome for the blank-draft phase of ConversationRoot: fish headline,
// glow backdrop, and the workspace row. Pure presentation — the resident
// composer is NOT rendered here (it keeps its own stable tree position in
// ConversationRoot so the textarea survives the hero → composer flip); CSS
// positions it over this shell's glow area during the hero phase.
import { useId } from 'react'
import type { ReactNode, RefObject } from 'react'
import {
FishLogo, IconChevronDownOutline14, IconFolderClose16, IconFolderOpen16,
} from '@deepseek-ai/dsh-client-ui-primitives'
import { workspaceTitleOf } from '@deepseek-ai/dsh-client-runtime/client'
import type { ConversationSlotProps } from '../contract/slots.ts'
import css from './HeroShell.module.css'
/** The owner's locale seat type, passed to hero chrome as a plain prop. */
type HeroTranslate = ConversationSlotProps['t']
/**
* Basename label for the workspace chip (the shared derivation);
* separator-only paths echo the raw cwd.
* @param cwd - workspace directory path (non-empty).
* @returns chip label.
*/
export function workspaceLabel(cwd: string): string {
const base = workspaceTitleOf(cwd)
return base !== '' ? base : cwd
}
/**
* The workspace chip (folder + label + chevron), always interactive: before
* the first message the workspace stays switchable — picking another one
* moves the New Session flow to that workspace's blank session. Without a
* label the chip renders its placeholder state: closed folder + the
* "Choose workspace" call to action.
* @param props.label - chip label (see {@link workspaceLabel}); omitted → placeholder.
* @param props.menuOpen - menu expansion echo.
* @param props.onClick - menu toggle.
* @returns the chip button element.
*/
export function WorkspaceChip({ buttonRef, label, menuOpen = false, onClick, t }: {
buttonRef?: RefObject<HTMLButtonElement>
label?: string | undefined
menuOpen?: boolean
onClick?: () => void
t: HeroTranslate
}) {
return (
<button
ref={buttonRef}
type="button"
className={css.workspace}
aria-label={t('hero.chooseWorkspace')}
aria-haspopup="menu"
aria-expanded={menuOpen}
onClick={onClick}
>
{label === undefined
? <IconFolderClose16 className={css.folder} size={16} />
: <IconFolderOpen16 className={css.folder} size={16} />}
<span className={css.workspaceLabel}>{label ?? t('hero.chooseWorkspace')}</span>
<IconChevronDownOutline14 className={css.chevron} size={12} />
</button>
)
}
/**
* The soft blue backdrop ellipse (figma 313:14109). Rendered by the hero
* owner (ConversationRoot), not HeroShell, so it can center on the input
* card; the owner's className supplies all positioning.
* @param props.className - positioning class from the owner.
* @returns the blurred-ellipse svg element.
*/
export function HeroGlow({ className }: { className?: string | undefined }) {
// Stable filter id so multiple hero mounts do not collide in the DOM.
const glowFilterId = `empty-glow-${useId().replace(/:/g, '')}`
return (
<svg className={className} viewBox="0 0 1051 468" fill="none" aria-hidden="true">
<defs>
<filter
id={glowFilterId}
x="0"
y="0"
width="1051"
height="468"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="50" result="effect1_foregroundBlur" />
</filter>
</defs>
<g filter={`url(#${glowFilterId})`}>
<ellipse cx="525.5" cy="234" rx="425.5" ry="134" fill="#6187D8" fillOpacity="0.08" />
</g>
</svg>
)
}
/** Hero chrome props. The workspace row rides the InputBar accessory hole, not here. */
export interface HeroShellProps {
/** The owner's locale seat, passed down as a plain prop. */
t: HeroTranslate
/** Overlay content after the stack (modals). */
children?: ReactNode
}
/**
* Render the hero chrome (headline only; no glow, no composer, no workspace
* row — the glow is the owner's {@link HeroGlow}).
* @param props - see {@link HeroShellProps}.
* @returns the centered hero element tree.
*/
export function HeroShell({ t, children }: HeroShellProps) {
return (
<div className={css.root}>
<div className={css.stack}>
<div className={css.headline}>
{/* figma 34:10412: fish 34×25 leading the headline, gap 10. */}
<FishLogo size={34} className={css.fish} />
{t('hero.headline')}
</div>
<div className={css.body}>
{/* The resident composer (ConversationRoot wrapActiveBody seat; the
workspace row rides the stack above the card) is CSS-centered in
the session scroll body during hero — see
ConversationRoot.module.css [data-phase='hero']. */}
</div>
</div>
{children}
</div>
)
}