Merge remote-tracking branch 'github/master' into xtr/trajectory-inspection-ui
# Conflicts: # packages/client/ui-primitives/src/Menu.tsx # packages/client/ui-trajectory/src/client/TrajectoryCell.tsx # packages/client/ui-trajectory/src/client/TrajectoryView.tsx # packages/client/ui-trajectory/tests/layout.spec.tsx # pnpm-lock.yaml # pnpm-workspace.yaml
This commit is contained in:
@@ -19,7 +19,7 @@ export function Button({ variant = 'ghost', size = 'md', icon, className, childr
|
||||
variant?: ButtonVariant
|
||||
size?: 'md' | 'sm'
|
||||
icon?: ReactNode
|
||||
className?: string
|
||||
className?: string | undefined
|
||||
children?: ReactNode
|
||||
} & ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
|
||||
@@ -23,21 +23,21 @@ const OBJECT_COPY_MENU_ITEMS: readonly MenuEntry[] = [
|
||||
]
|
||||
|
||||
const TREE_STYLES: NonNullable<LiteJsonViewProps['style']> = {
|
||||
container: css.container!,
|
||||
childFieldsContainer: css.children!,
|
||||
basicChildStyle: css.row!,
|
||||
label: css.label!,
|
||||
clickableLabel: `${css.label!} ${css.clickableLabel!}`,
|
||||
nullValue: css.keywordValue!,
|
||||
undefinedValue: css.keywordValue!,
|
||||
numberValue: css.numberValue!,
|
||||
stringValue: css.stringValue!,
|
||||
booleanValue: css.keywordValue!,
|
||||
otherValue: css.otherValue!,
|
||||
punctuation: css.punctuation!,
|
||||
expandIcon: `${css.expander!} ${css.expandIcon!}`,
|
||||
collapseIcon: `${css.expander!} ${css.collapseIcon!}`,
|
||||
collapsedContent: css.collapsedContent!,
|
||||
container: clsx(css.container),
|
||||
childFieldsContainer: clsx(css.children),
|
||||
basicChildStyle: clsx(css.row),
|
||||
label: clsx(css.label),
|
||||
clickableLabel: clsx(css.label, css.clickableLabel),
|
||||
nullValue: clsx(css.keywordValue),
|
||||
undefinedValue: clsx(css.keywordValue),
|
||||
numberValue: clsx(css.numberValue),
|
||||
stringValue: clsx(css.stringValue),
|
||||
booleanValue: clsx(css.keywordValue),
|
||||
otherValue: clsx(css.otherValue),
|
||||
punctuation: clsx(css.punctuation),
|
||||
expandIcon: clsx(css.expander, css.expandIcon),
|
||||
collapseIcon: clsx(css.expander, css.collapseIcon),
|
||||
collapsedContent: clsx(css.collapsedContent),
|
||||
noQuotesForStringValues: false,
|
||||
quotesForFieldNames: false,
|
||||
stringifyStringValues: true,
|
||||
@@ -49,7 +49,7 @@ const TREE_STYLES: NonNullable<LiteJsonViewProps['style']> = {
|
||||
|
||||
const EXPANDED_TOP_LEVEL_TREE_STYLES: NonNullable<LiteJsonViewProps['style']> = {
|
||||
...TREE_STYLES,
|
||||
container: `${css.container!} ${css.expandedTopLevelContainer!}`,
|
||||
container: clsx(css.container, css.expandedTopLevelContainer),
|
||||
}
|
||||
|
||||
function previewPrimitive(value: unknown): ReactNode {
|
||||
@@ -63,7 +63,16 @@ function previewPrimitive(value: unknown): ReactNode {
|
||||
if (typeof value === 'boolean') {
|
||||
return <span className={css.keywordValue}>{String(value)}</span>
|
||||
}
|
||||
return <span className={css.otherValue}>{String(value)}</span>
|
||||
if (typeof value === 'bigint') {
|
||||
return <span className={css.otherValue}>{value.toString()}</span>
|
||||
}
|
||||
if (typeof value === 'undefined') {
|
||||
return <span className={css.otherValue}>undefined</span>
|
||||
}
|
||||
if (typeof value === 'symbol') {
|
||||
return <span className={css.otherValue}>{value.description ?? 'Symbol'}</span>
|
||||
}
|
||||
return <span className={css.otherValue}>{value.name || 'Function'}</span>
|
||||
}
|
||||
|
||||
function previewValue(value: unknown, depth: number): ReactNode {
|
||||
@@ -84,17 +93,17 @@ function previewValue(value: unknown, depth: number): ReactNode {
|
||||
{depth >= PREVIEW_DEPTH_LIMIT
|
||||
? <span className={css.previewEllipsis}>…</span>
|
||||
: visible.map(([key, item], index) => (
|
||||
<span key={key}>
|
||||
{index > 0 && <span className={css.punctuation}>, </span>}
|
||||
{!array && (
|
||||
<>
|
||||
<span className={css.previewProperty}>{key}</span>
|
||||
<span className={css.punctuation}>: </span>
|
||||
</>
|
||||
)}
|
||||
{previewValue(item, depth + 1)}
|
||||
</span>
|
||||
))}
|
||||
<span key={key}>
|
||||
{index > 0 && <span className={css.punctuation}>, </span>}
|
||||
{!array && (
|
||||
<>
|
||||
<span className={css.previewProperty}>{key}</span>
|
||||
<span className={css.punctuation}>: </span>
|
||||
</>
|
||||
)}
|
||||
{previewValue(item, depth + 1)}
|
||||
</span>
|
||||
))}
|
||||
{depth < PREVIEW_DEPTH_LIMIT && entries.length > limit && (
|
||||
<span className={css.previewEllipsis}>{visible.length > 0 ? ', …' : '…'}</span>
|
||||
)}
|
||||
@@ -117,10 +126,10 @@ interface CopyTarget {
|
||||
|
||||
function fieldOf(row: HTMLElement): string | undefined {
|
||||
const label = Array.from(row.children).find(
|
||||
child => child instanceof HTMLElement && child.classList.contains(css.label!),
|
||||
child => child instanceof HTMLElement && child.classList.contains(clsx(css.label)),
|
||||
)
|
||||
const text = label?.textContent
|
||||
return text === undefined || text === null ? undefined : text.slice(0, -1)
|
||||
return text === undefined ? undefined : text.slice(0, -1)
|
||||
}
|
||||
|
||||
function resolveRow(data: object | unknown[], row: HTMLElement, expandTopLevel: boolean): {
|
||||
@@ -172,12 +181,16 @@ function formattedPath(path: readonly (number | string)[]): string {
|
||||
function copyText(target: CopyTarget, mode: 'json' | 'path' | 'prettyJson' | 'value'): string {
|
||||
if (mode === 'path') return formattedPath(target.path)
|
||||
if (mode === 'prettyJson') return JSON.stringify(target.value, null, 2)
|
||||
if (mode === 'json') return JSON.stringify(target.value) ?? String(target.value)
|
||||
if (mode === 'json') return JSON.stringify(target.value)
|
||||
if (typeof target.value === 'string') return target.value
|
||||
if (typeof target.value === 'object' && target.value !== null) {
|
||||
return JSON.stringify(target.value, null, 2)
|
||||
}
|
||||
return JSON.stringify(target.value) ?? String(target.value)
|
||||
if (typeof target.value === 'undefined') return 'undefined'
|
||||
if (typeof target.value === 'bigint') return target.value.toString()
|
||||
if (typeof target.value === 'symbol') return target.value.description ?? 'Symbol'
|
||||
if (typeof target.value === 'function') return target.value.name || 'Function'
|
||||
return JSON.stringify(target.value)
|
||||
}
|
||||
|
||||
/** Props for the read-only, token-themed JSON tree. */
|
||||
@@ -303,7 +316,7 @@ export function JsonTree({
|
||||
setCopyState('failed')
|
||||
}
|
||||
if (resetTimer.current !== undefined) clearTimeout(resetTimer.current)
|
||||
resetTimer.current = setTimeout(() => setCopyState('idle'), 1_500)
|
||||
resetTimer.current = setTimeout(() => { setCopyState('idle') }, 1_500)
|
||||
}
|
||||
|
||||
const copyTargetIsObject = typeof copyTarget?.value === 'object' && copyTarget.value !== null
|
||||
@@ -326,34 +339,34 @@ export function JsonTree({
|
||||
>
|
||||
{expandTopLevel
|
||||
? (
|
||||
<div className={css.expandedTopLevel}>
|
||||
<div className={clsx(css.row, css.topLevelBracket)} data-json-root-row>
|
||||
<span className={css.punctuation}>{Array.isArray(data) ? '[' : '{'}</span>
|
||||
</div>
|
||||
<JsonView
|
||||
aria-label={label}
|
||||
compactTopLevel
|
||||
data={data}
|
||||
style={EXPANDED_TOP_LEVEL_TREE_STYLES}
|
||||
shouldExpandNode={collapseAllNested}
|
||||
clickToExpandNode
|
||||
renderExpandableValue={renderExpandableValue}
|
||||
/>
|
||||
<div className={clsx(css.row, css.topLevelBracket)}>
|
||||
<span className={css.punctuation}>{Array.isArray(data) ? ']' : '}'}</span>
|
||||
</div>
|
||||
<div className={css.expandedTopLevel}>
|
||||
<div className={clsx(css.row, css.topLevelBracket)} data-json-root-row>
|
||||
<span className={css.punctuation}>{Array.isArray(data) ? '[' : '{'}</span>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<JsonView
|
||||
aria-label={label}
|
||||
compactTopLevel
|
||||
data={data}
|
||||
style={TREE_STYLES}
|
||||
style={EXPANDED_TOP_LEVEL_TREE_STYLES}
|
||||
shouldExpandNode={collapseAllNested}
|
||||
clickToExpandNode
|
||||
renderExpandableValue={renderExpandableValue}
|
||||
/>
|
||||
)}
|
||||
<div className={clsx(css.row, css.topLevelBracket)}>
|
||||
<span className={css.punctuation}>{Array.isArray(data) ? ']' : '}'}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<JsonView
|
||||
aria-label={label}
|
||||
data={data}
|
||||
style={TREE_STYLES}
|
||||
shouldExpandNode={collapseAllNested}
|
||||
clickToExpandNode
|
||||
renderExpandableValue={renderExpandableValue}
|
||||
/>
|
||||
)}
|
||||
{copyTarget !== undefined && (
|
||||
<span
|
||||
className={css.copyAnchor}
|
||||
|
||||
@@ -160,63 +160,63 @@ export function Menu({ open, anchor, items, selectedId, onSelect, onClose, align
|
||||
// (open/toggle) after onSelect.
|
||||
onClick={(e) => { e.stopPropagation() }}
|
||||
>
|
||||
{items.map(entry => {
|
||||
if (isSeparator(entry)) {
|
||||
return <div key={entry.id} className={css.separator} role="separator" />
|
||||
}
|
||||
if (isLabel(entry)) {
|
||||
return <div key={entry.id} className={css.label} role="presentation">{entry.text}</div>
|
||||
}
|
||||
const hasSub = entry.submenu !== undefined && entry.submenu.length > 0
|
||||
const subOpen = hasSub && openSubmenuId === entry.id
|
||||
return (
|
||||
<div
|
||||
key={entry.id}
|
||||
className={css.itemWrap}
|
||||
onMouseEnter={() => { setOpenSubmenuId(hasSub ? entry.id : null) }}
|
||||
onMouseLeave={() => { setOpenSubmenuId(null) }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={clsx(css.item, entry.id === selectedId && css.selected, entry.danger === true && css.danger)}
|
||||
disabled={entry.disabled}
|
||||
aria-haspopup={hasSub ? 'menu' : undefined}
|
||||
aria-expanded={hasSub ? subOpen : undefined}
|
||||
onFocus={() => { setOpenSubmenuId(hasSub ? entry.id : null) }}
|
||||
onClick={() => {
|
||||
if (hasSub) {
|
||||
setOpenSubmenuId(entry.id)
|
||||
return
|
||||
}
|
||||
onSelect(entry.id)
|
||||
}}
|
||||
>
|
||||
{entry.icon !== undefined && <span className={css.itemIcon}>{entry.icon}</span>}
|
||||
<span className={css.itemLabel}>{entry.label}</span>
|
||||
{/* Selection marker is a trailing check (figma .Menu_cell), not a fill. */}
|
||||
{entry.id === selectedId && <IconCheckOutline16 className={css.check} />}
|
||||
</button>
|
||||
{subOpen && entry.submenu !== undefined && (
|
||||
<div className={clsx(css.submenu, compact && css.compactList)} role="menu">
|
||||
{entry.submenu.map(sub => (
|
||||
<button
|
||||
key={sub.id}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={css.item}
|
||||
disabled={sub.disabled}
|
||||
onClick={() => { onSelect(sub.id) }}
|
||||
>
|
||||
{sub.icon !== undefined && <span className={css.itemIcon}>{sub.icon}</span>}
|
||||
<span className={css.itemLabel}>{sub.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{items.map((entry) => {
|
||||
if (isSeparator(entry)) {
|
||||
return <div key={entry.id} className={css.separator} role="separator" />
|
||||
}
|
||||
if (isLabel(entry)) {
|
||||
return <div key={entry.id} className={css.label} role="presentation">{entry.text}</div>
|
||||
}
|
||||
const hasSub = entry.submenu !== undefined && entry.submenu.length > 0
|
||||
const subOpen = hasSub && openSubmenuId === entry.id
|
||||
return (
|
||||
<div
|
||||
key={entry.id}
|
||||
className={css.itemWrap}
|
||||
onMouseEnter={() => { setOpenSubmenuId(hasSub ? entry.id : null) }}
|
||||
onMouseLeave={() => { setOpenSubmenuId(null) }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={clsx(css.item, entry.id === selectedId && css.selected, entry.danger === true && css.danger)}
|
||||
disabled={entry.disabled}
|
||||
aria-haspopup={hasSub ? 'menu' : undefined}
|
||||
aria-expanded={hasSub ? subOpen : undefined}
|
||||
onFocus={() => { setOpenSubmenuId(hasSub ? entry.id : null) }}
|
||||
onClick={() => {
|
||||
if (hasSub) {
|
||||
setOpenSubmenuId(entry.id)
|
||||
return
|
||||
}
|
||||
onSelect(entry.id)
|
||||
}}
|
||||
>
|
||||
{entry.icon !== undefined && <span className={css.itemIcon}>{entry.icon}</span>}
|
||||
<span className={css.itemLabel}>{entry.label}</span>
|
||||
{/* Selection marker is a trailing check (figma .Menu_cell), not a fill. */}
|
||||
{entry.id === selectedId && <IconCheckOutline16 className={css.check} />}
|
||||
</button>
|
||||
{subOpen && entry.submenu !== undefined && (
|
||||
<div className={clsx(css.submenu, compact && css.compactList)} role="menu">
|
||||
{entry.submenu.map(sub => (
|
||||
<button
|
||||
key={sub.id}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={css.item}
|
||||
disabled={sub.disabled}
|
||||
onClick={() => { onSelect(sub.id) }}
|
||||
>
|
||||
{sub.icon !== undefined && <span className={css.itemIcon}>{sub.icon}</span>}
|
||||
<span className={css.itemLabel}>{sub.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@@ -27,7 +27,8 @@ interface AnchorProps {
|
||||
* Attach a hover/focus tooltip to an anchor element.
|
||||
* @param props.label - bubble text.
|
||||
* @param props.side - placement relative to the anchor (default 'right').
|
||||
* @param props.disabled - suppress the bubble while true; the anchor renders identically so toggling never remounts it (which would cut its CSS transitions).
|
||||
* @param props.disabled - suppress the bubble while true; the anchor renders identically so
|
||||
* toggling never remounts it (which would cut its CSS transitions).
|
||||
* @param props.children - a single anchor element; its own ref (callback or object) is forwarded alongside the tooltip's.
|
||||
* @returns the cloned anchor plus a fixed-position bubble while hovered/focused.
|
||||
*/
|
||||
|
||||
@@ -544,14 +544,14 @@ export const IconApiOutline14 = ({ size = 14, className }: IconProps) => (
|
||||
<path transform="translate(0.6689 1.073)" d="M11.4818 5.57813C11.4818 4.45301 11.4807 3.66237 11.4075 3.05908C11.3359 2.46953 11.2024 2.13852 10.9939 1.89441C10.9247 1.81341 10.8493 1.73801 10.7683 1.66882C10.5242 1.46033 10.1932 1.32686 9.60364 1.25525C9.00034 1.18198 8.20974 1.18091 7.0846 1.18091L5.57813 1.18091C4.45301 1.18091 3.66238 1.18198 3.05908 1.25525C2.46953 1.32686 2.13852 1.46033 1.89441 1.66882C1.81341 1.73801 1.73801 1.81341 1.66882 1.89441C1.46033 2.13852 1.32686 2.46953 1.25525 3.05908C1.18198 3.66238 1.18091 4.45301 1.18091 5.57813L1.18091 6.2771C1.18091 7.40218 1.18197 8.19288 1.25525 8.79614C1.32687 9.38553 1.46036 9.71674 1.66882 9.96082C1.73797 10.0417 1.81347 10.1173 1.89441 10.1864C2.13851 10.3948 2.46965 10.5275 3.05908 10.5991C3.66238 10.6724 4.45298 10.6735 5.57813 10.6735L7.0846 10.6735C8.20977 10.6735 9.00033 10.6724 9.60364 10.5991C10.1931 10.5275 10.5242 10.3948 10.7683 10.1864C10.8493 10.1173 10.9247 10.0417 10.9939 9.96082C11.2024 9.71674 11.3358 9.38553 11.4075 8.79614C11.4808 8.19288 11.4818 7.40218 11.4818 6.2771L11.4818 5.57813ZM12.6627 6.2771C12.6627 7.37222 12.6637 8.247 12.5798 8.93799C12.4942 9.64284 12.3133 10.2359 11.8928 10.7282C11.7834 10.8562 11.6637 10.9751 11.5356 11.0845C11.0434 11.5049 10.4511 11.6867 9.74634 11.7723C9.05525 11.8563 8.17999 11.8552 7.0846 11.8552L5.57813 11.8552C4.48273 11.8552 3.60747 11.8563 2.91638 11.7723C2.21157 11.6867 1.61933 11.5049 1.12708 11.0845C0.99901 10.9751 0.879281 10.8562 0.769898 10.7282C0.349454 10.2359 0.168506 9.64284 0.0828864 8.93799C-0.00101964 8.247 4.88512e-07 7.37222 6.47206e-07 6.2771L6.47206e-07 5.57813C6.47206e-07 4.48273 -0.00106163 3.60747 0.0828864 2.91638C0.168502 2.21168 0.349594 1.61928 0.769898 1.12708C0.879302 0.998981 0.998981 0.879302 1.12708 0.769898C1.61928 0.349594 2.21168 0.168502 2.91638 0.0828864C3.60747 -0.00106163 4.48273 6.47206e-07 5.57813 6.47206e-07L7.0846 6.47206e-07C8.17999 6.47206e-07 9.05525 -0.00106163 9.74634 0.0828864C10.451 0.168505 11.0434 0.349587 11.5356 0.769898C11.6637 0.879302 11.7834 0.998981 11.8928 1.12708C12.3131 1.61928 12.4942 2.21169 12.5798 2.91638C12.6638 3.60747 12.6627 4.48273 12.6627 5.57813L12.6627 6.2771Z" fill="currentColor"/>
|
||||
<path transform="translate(0.6689 1.073)" d="M6.02607 5.50955L6.44306 5.9274L3.84284 8.52762L3.425 8.11063L3.00715 7.69278L4.77253 5.9274L3.00715 4.16202L3.84284 3.32633L6.02607 5.50955Z" fill="currentColor"/>
|
||||
<path transform="translate(0.6689 1.073)" d="M9.23789 7.35397L9.23789 8.53488L6.96238 8.53488L6.96238 7.35397L9.23789 7.35397Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** ic_ds_personalization_outline_16 (figma extract) */
|
||||
export const IconPersonalizationOutline16 = ({ size = 16, className }: IconProps) => (
|
||||
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none">
|
||||
<path transform="translate(1.292 1.3)" d="M10.3232 9.18164C11.2868 9.18164 12.0985 9.82833 12.3506 10.7109L13.415 10.7109L13.415 11.8711L12.3496 11.8711C12.0971 12.7532 11.2864 13.3994 10.3232 13.3994C9.36031 13.3992 8.55012 12.7531 8.29785 11.8711L0 11.8711L0 10.7109L8.29688 10.7109C8.54876 9.82845 9.35988 9.18186 10.3232 9.18164ZM10.3232 10.3418C9.7999 10.3421 9.37534 10.7667 9.375 11.29C9.375 11.8137 9.79969 12.239 10.3232 12.2393C10.847 12.2393 11.2725 11.8138 11.2725 11.29C11.2721 10.7666 10.8468 10.3418 10.3232 10.3418ZM12.4326 11.291C12.4326 11.3549 12.4284 11.418 12.4229 11.4805C12.4287 11.4181 12.4326 11.355 12.4326 11.291ZM8.21484 11.2832C8.21484 11.2856 8.21484 11.2886 8.21484 11.291L8.21484 11.29C8.21484 11.2878 8.21484 11.2855 8.21484 11.2832ZM3.08301 4.59082C4.04605 4.59095 4.85696 5.23717 5.10938 6.11914L13.415 6.11914L13.415 7.2793L5.11035 7.2793C4.85833 8.16202 4.04648 8.80846 3.08301 8.80859C2.11972 8.80843 1.30963 8.16179 1.05762 7.2793L0 7.2793L0 6.11914L1.05762 6.11914C1.30994 5.23728 2.12006 4.59098 3.08301 4.59082ZM3.08301 5.75098C2.55962 5.75117 2.13512 6.17587 2.13477 6.69922C2.13477 7.22287 2.5594 7.64824 3.08301 7.64844C3.60665 7.64828 4.03223 7.2229 4.03223 6.69922C4.03187 6.17585 3.60643 5.75113 3.08301 5.75098ZM5.19238 6.69922C5.19238 6.763 5.18816 6.82633 5.18262 6.88867C5.18846 6.82629 5.19238 6.76313 5.19238 6.69922C5.19236 6.63495 5.18853 6.57152 5.18262 6.50879C5.18826 6.57154 5.19236 6.635 5.19238 6.69922ZM0.982422 6.52344C0.977382 6.58136 0.97463 6.63999 0.974609 6.69922C0.974609 6.75775 0.977496 6.81579 0.982422 6.87305C0.977758 6.81579 0.974609 6.75767 0.974609 6.69922C0.974628 6.64 0.977618 6.58142 0.982422 6.52344ZM10.3232 0C11.2869 0 12.0986 0.646596 12.3506 1.5293L13.415 1.5293L13.415 2.68945L12.3496 2.68945C12.363 2.64266 12.3754 2.59488 12.3857 2.54688C12.1838 3.50118 11.3376 4.21777 10.3232 4.21777C9.36037 4.21756 8.55018 3.57139 8.29785 2.68945L0 2.68945L0 1.5293L8.29688 1.5293C8.5487 0.646717 9.35981 0.00021854 10.3232 0ZM10.3232 1.16016C9.79984 1.16042 9.37524 1.58499 9.375 2.1084C9.375 2.63201 9.79969 3.05735 10.3232 3.05762C10.847 3.05762 11.2725 2.63217 11.2725 2.1084C11.2722 1.58483 10.8469 1.16016 10.3232 1.16016ZM12.4229 2.29883C12.4287 2.23641 12.4326 2.17331 12.4326 2.10938C12.4326 2.17327 12.4284 2.23638 12.4229 2.29883ZM8.21484 2.10938L8.21484 2.1084L8.21484 2.10938ZM8.22266 1.93359C8.21785 1.98897 8.21506 2.04499 8.21484 2.10156C8.21503 2.04501 8.2181 1.98902 8.22266 1.93359ZM8.22266 11.1162C8.2179 11.1713 8.21507 11.227 8.21484 11.2832C8.21504 11.227 8.21814 11.1713 8.22266 11.1162Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** ic_ds_project_add_outline_16 (figma extract) */
|
||||
@@ -559,7 +559,7 @@ export const IconProjectAddOutline16 = ({ size = 16, className }: IconProps) =>
|
||||
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none">
|
||||
<path transform="translate(9.52 2.52)" d="M3.55246 0L3.55246 2.44252L6 2.44252L6 3.55748L3.55246 3.55748L3.55246 6L2.43834 6L2.43834 3.55748L0 3.55748L0 2.44252L2.43834 2.44252L2.43834 0L3.55246 0Z" fill="currentColor"/>
|
||||
<path transform="translate(0.3496 2.35)" d="M4.76367 0C5.36861 1.80598e-05 5.93113 0.310294 6.25488 0.821289L6.78027 1.64941C6.79685 1.67558 6.81791 1.69775 6.83887 1.71973C6.72186 2.15521 6.65702 2.61192 6.65137 3.08301C6.25601 2.96045 5.90909 2.70478 5.68164 2.3457L5.15723 1.5166C5.07183 1.38189 4.92318 1.3008 4.76367 1.30078L2.32422 1.30078C1.7589 1.30078 1.30078 1.7589 1.30078 2.32422L1.30078 10.1338C1.30078 10.6991 1.7589 11.1572 2.32422 11.1572L11.9766 11.1572C12.5419 11.1572 13 10.6991 13 10.1338L13 8.58398C13.4545 8.5135 13.8903 8.38748 14.3008 8.21289L14.3008 10.1338C14.3008 11.4171 13.2598 12.458 11.9766 12.458L2.32422 12.458C1.04093 12.458 0 11.4171 0 10.1338L0 2.32422C0 1.04093 1.04093 0 2.32422 0L4.76367 0Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** folder_open_16 (figma extract): outline at full ink + 20%-opacity inner fill riding the same currentColor. */
|
||||
@@ -567,14 +567,14 @@ export const IconFolderOpen16 = ({ size = 16, className }: IconProps) => (
|
||||
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none">
|
||||
<path d="M5.19629 1.57104C5.81144 1.5711 6.38623 1.8786 6.72754 2.39038L7.19922 3.09839C7.28454 3.22635 7.42824 3.30344 7.58203 3.30347H12.1699C13.5039 3.30348 14.5859 4.38548 14.5859 5.71948V6.62671C15.2694 7.02689 15.6605 7.85012 15.4385 8.68726L14.3848 12.658C14.1037 13.7164 13.1449 14.4527 12.0498 14.4529H2.91699C1.51651 14.4529 0.451662 13.2814 0.501954 11.9519V3.98706C0.501954 2.65305 1.58396 1.57104 2.91797 1.57104H5.19629ZM3.7793 7.75562C3.30994 7.75562 2.89883 8.07153 2.77832 8.52515L1.91602 11.7722C1.74167 12.4291 2.23734 13.073 2.91699 13.073H12.0498C12.5191 13.0728 12.9304 12.757 13.0508 12.3035L14.1045 8.33374C14.1819 8.04202 13.9619 7.756 13.6602 7.75562H3.7793ZM2.91797 2.9519C2.34625 2.9519 1.88281 3.41534 1.88281 3.98706V7.2937C2.33068 6.7269 3.02249 6.37476 3.7793 6.37476H13.2051V5.71948C13.2051 5.14777 12.7416 4.68434 12.1699 4.68433H7.58203C6.96675 4.6843 6.39209 4.37595 6.05078 3.86401L5.5791 3.15601C5.49379 3.02821 5.34995 2.95196 5.19629 2.9519H2.91797Z" fill="currentColor"/>
|
||||
<path opacity="0.2" d="M13.6602 7.75525C13.9618 7.7556 14.1815 8.04179 14.1045 8.33337L13.0508 12.3031C12.9304 12.7567 12.5191 13.0725 12.0498 13.0726H2.91701C2.23744 13.0725 1.7417 12.4287 1.91603 11.7719L2.77834 8.52478C2.89898 8.07146 3.31018 7.75532 3.77931 7.75525H13.6602ZM5.1963 2.95154C5.34985 2.95159 5.49377 3.02803 5.57912 3.15564L6.0508 3.86365C6.39205 4.37553 6.96685 4.68385 7.58205 4.68396H12.1699C12.7416 4.68396 13.2049 5.14754 13.2051 5.71912V6.37439H3.77931C3.02267 6.37444 2.33067 6.72671 1.88283 7.29333V3.98669C1.88299 3.4152 2.34649 2.95168 2.91798 2.95154H5.1963Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** folder_close_16 (figma extract) */
|
||||
export const IconFolderClose16 = ({ size = 16, className }: IconProps) => (
|
||||
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none">
|
||||
<path transform="translate(1.5 2.429)" d="M5.05582 0.518756L4.50669 0.86654L5.05582 0.518756ZM13 9.4837L13.65 9.4837L13.65 3.53962L13 3.53962L12.35 3.53962L12.35 9.4837L13 9.4837ZM11.3264 1.86603L11.3264 1.21603L6.52313 1.21603L6.52313 1.86603L6.52313 2.51603L11.3264 2.51603L11.3264 1.86603ZM5.58054 1.34727L6.12968 0.999489L5.60495 0.170972L5.05582 0.518756L4.50669 0.86654L5.03141 1.69506L5.58054 1.34727ZM4.11323 1.23058e-13L4.11323 -0.65L1.67359 -0.65L1.67359 5.00699e-14L1.67359 0.65L4.11323 0.65L4.11323 1.23058e-13ZM0 1.67359L-0.65 1.67359L-0.65 9.4837L0 9.4837L0.65 9.4837L0.65 1.67359L0 1.67359ZM11.3264 11.1573L11.3264 10.5073L1.67359 10.5073L1.67359 11.1573L1.67359 11.8073L11.3264 11.8073L11.3264 11.1573ZM0 9.4837L-0.65 9.4837C-0.65 10.767 0.390308 11.8073 1.67359 11.8073L1.67359 11.1573L1.67359 10.5073C1.10828 10.5073 0.65 10.049 0.65 9.4837L0 9.4837ZM1.67359 5.00699e-14L1.67359 -0.65C0.390307 -0.65 -0.65 0.390309 -0.65 1.67359L0 1.67359L0.65 1.67359C0.65 1.10828 1.10828 0.65 1.67359 0.65L1.67359 5.00699e-14ZM5.05582 0.518756L5.60495 0.170972C5.28121 -0.340193 4.71829 -0.65 4.11323 -0.65L4.11323 1.23058e-13L4.11323 0.65C4.27282 0.65 4.4213 0.731715 4.50669 0.86654L5.05582 0.518756ZM6.52313 1.86603L6.52313 1.21603C6.36354 1.21603 6.21507 1.13431 6.12968 0.999489L5.58054 1.34727L5.03141 1.69506C5.35515 2.20622 5.91808 2.51603 6.52313 2.51603L6.52313 1.86603ZM13 3.53962L13.65 3.53962C13.65 2.25634 12.6097 1.21603 11.3264 1.21603L11.3264 1.86603L11.3264 2.51603C11.8917 2.51603 12.35 2.97431 12.35 3.53962L13 3.53962ZM13 9.4837L12.35 9.4837C12.35 10.049 11.8917 10.5073 11.3264 10.5073L11.3264 11.1573L11.3264 11.8073C12.6097 11.8073 13.65 10.767 13.65 9.4837L13 9.4837Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** tree_corner_8x10 (figma extract; session-tree "L" connector, stroke geometry pre-expanded) */
|
||||
|
||||
@@ -20,6 +20,9 @@ export interface CodeBlockProps {
|
||||
|
||||
/** @returns true only when the host accepted the write. */
|
||||
async function writeClipboard(text: string): Promise<boolean> {
|
||||
// lib.dom types clipboard non-optional, but insecure contexts omit it —
|
||||
// that runtime gap is exactly what this guard detects.
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */
|
||||
if (navigator.clipboard?.writeText) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
@@ -30,6 +33,9 @@ async function writeClipboard(text: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
// jsdom and older hosts: best-effort execCommand path when present.
|
||||
// execCommand('copy') is the only clipboard fallback where the async API
|
||||
// is missing; deprecated but deliberately retained.
|
||||
/* eslint-disable @typescript-eslint/no-deprecated */
|
||||
const exec = typeof document.execCommand === 'function'
|
||||
? document.execCommand.bind(document)
|
||||
: undefined
|
||||
@@ -48,6 +54,7 @@ async function writeClipboard(text: string): Promise<boolean> {
|
||||
} finally {
|
||||
el.remove()
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-deprecated */
|
||||
}
|
||||
|
||||
export function CodeBlock({ code, lang, className }: CodeBlockProps) {
|
||||
@@ -65,20 +72,20 @@ export function CodeBlock({ code, lang, className }: CodeBlockProps) {
|
||||
void writeClipboard(text).then((ok) => {
|
||||
if (!ok) return
|
||||
setCopied(true)
|
||||
window.setTimeout(() => setCopied(false), 1000)
|
||||
window.setTimeout(() => { setCopied(false) }, 1000)
|
||||
})
|
||||
}, [copied, trimmed])
|
||||
|
||||
const body = html === undefined
|
||||
? (
|
||||
<pre className={css.plain}><code>{trimmed}</code></pre>
|
||||
)
|
||||
<pre className={css.plain}><code>{trimmed}</code></pre>
|
||||
)
|
||||
: (
|
||||
// eslint-disable-next-line react/no-danger -- shiki's output is a static
|
||||
// span tree it generated from `code` (no user HTML passes through), the
|
||||
// sanctioned innerHTML consumption path per shiki's own docs.
|
||||
<div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
)
|
||||
// shiki's output is a static span tree it generated from `code` (no user
|
||||
// HTML passes through), the sanctioned innerHTML consumption path per
|
||||
// shiki's own docs.
|
||||
<div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -15,6 +15,8 @@ export function JsonBlock({ label, payload, defaultOpen = false }: {
|
||||
if (!open) return ''
|
||||
let s: string
|
||||
try {
|
||||
// lib typing hides stringify's undefined arm (undefined/function/symbol payloads).
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
s = JSON.stringify(payload, null, 2) ?? String(payload)
|
||||
} catch {
|
||||
s = String(payload)
|
||||
@@ -23,7 +25,7 @@ export function JsonBlock({ label, payload, defaultOpen = false }: {
|
||||
}, [open, payload])
|
||||
return (
|
||||
<div className={css.root}>
|
||||
<button type="button" className={css.toggle} onClick={() => setOpen((v) => !v)}>
|
||||
<button type="button" className={css.toggle} onClick={() => { setOpen(v => !v) }}>
|
||||
{open ? '▾' : '▸'} {label}
|
||||
</button>
|
||||
{open && <pre className={css.body}>{body}</pre>}
|
||||
|
||||
@@ -27,25 +27,25 @@ const safeUrl: UrlTransform = url => sanitizeUrl(url)
|
||||
/** Build the component table; while `streaming`, fences render the plain arm (see CodeBlock). */
|
||||
function buildComponents(streaming: boolean): Components {
|
||||
return {
|
||||
a: ({ href = '', children }) => {
|
||||
const safeHref = sanitizeUrl(href)
|
||||
if (safeHref === '') return <>{children}</>
|
||||
const external = ['http:', 'https:'].includes(new URL(safeHref).protocol)
|
||||
return (
|
||||
<a
|
||||
href={safeHref}
|
||||
{...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
img: ({ alt = '' }) => <span className={css.imageAlt}>{alt}</span>,
|
||||
table: ({ children }) => (
|
||||
<div className={css.tableScroll}>
|
||||
<table>{children}</table>
|
||||
</div>
|
||||
),
|
||||
a: ({ href = '', children }) => {
|
||||
const safeHref = sanitizeUrl(href)
|
||||
if (safeHref === '') return <>{children}</>
|
||||
const external = ['http:', 'https:'].includes(new URL(safeHref).protocol)
|
||||
return (
|
||||
<a
|
||||
href={safeHref}
|
||||
{...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
img: ({ alt = '' }) => <span className={css.imageAlt}>{alt}</span>,
|
||||
table: ({ children }) => (
|
||||
<div className={css.tableScroll}>
|
||||
<table>{children}</table>
|
||||
</div>
|
||||
),
|
||||
// Fenced blocks route through the shared CodeBlock (shiki for registered
|
||||
// grammars, identical-geometry plain fallback for unknown/absent
|
||||
// languages); inline code keeps the default <code> path (the :not(pre)
|
||||
@@ -53,7 +53,9 @@ function buildComponents(streaming: boolean): Components {
|
||||
// plain arm — retokenizing a growing fence on every chunk is quadratic
|
||||
// main-thread work; the finalize swap highlights it once.
|
||||
pre: ({ children }) => {
|
||||
/* v8 ignore next 2 -- the markdown pipeline always hands `pre` its single `code` element; the undefined arm guards a react-markdown representation change. */
|
||||
// The markdown pipeline always hands `pre` its single `code` element;
|
||||
// the undefined arm guards a react-markdown representation change.
|
||||
/* v8 ignore next 2 */
|
||||
const child = isValidElement<{ className?: string; children?: unknown }>(children) ? children : undefined
|
||||
const raw = child?.props.children
|
||||
// A fence whose content isn't one plain string (e.g. an empty fence)
|
||||
|
||||
@@ -13,7 +13,7 @@ function stubAnchorRect(anchor: HTMLElement, rect: { top: number; right: number
|
||||
wrapper.getBoundingClientRect = () => ({
|
||||
top: rect.top, right: rect.right, left: rect.right - 100, bottom: rect.top + 34,
|
||||
width: 100, height: 34, x: rect.right - 100, y: rect.top, toJSON: () => ({}),
|
||||
} as DOMRect)
|
||||
})
|
||||
}
|
||||
|
||||
function mount(props: { openDelayMs?: number; disabled?: boolean } = {}) {
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('ic_ds_ icon set', () => {
|
||||
expect(iconNames.length).toBe(55)
|
||||
})
|
||||
|
||||
it.each(iconNames)('%s renders an svg with currentColor fills and no hardcoded palette', name => {
|
||||
it.each(iconNames)('%s renders an svg with currentColor fills and no hardcoded palette', (name) => {
|
||||
const Icon = icons[name]!
|
||||
const { container } = render(<Icon />)
|
||||
const svg = container.querySelector('svg')
|
||||
|
||||
@@ -153,7 +153,7 @@ describe('JsonBlock', () => {
|
||||
it('truncates beyond the size cap with a suffix note', () => {
|
||||
const big = 'x'.repeat(30_000)
|
||||
const { container } = render(<JsonBlock label="x" payload={big} defaultOpen />)
|
||||
const body = container.querySelector('pre')!.textContent!
|
||||
const body = container.querySelector('pre')!.textContent
|
||||
expect(body.length).toBeLessThan(30_000)
|
||||
expect(body).toContain('截断')
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { StateDotState } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('StateDot', () => {
|
||||
it.each(['done', 'warning', 'ongoing', 'error'] as const)('renders state %s as data-state', state => {
|
||||
it.each(['done', 'warning', 'ongoing', 'error'] as const)('renders state %s as data-state', (state) => {
|
||||
const { container } = render(<StateDot state={state} />)
|
||||
const dot = container.firstElementChild as HTMLElement
|
||||
expect(dot.dataset['state']).toBe(state)
|
||||
|
||||
Reference in New Issue
Block a user