fix(web-ui): pixel loading language, tool-row sweep, composer polish
- StateDot ongoing: gradient spin ring replaced by an 8-cell pixel chase (2px matrix cells, stepped trail, no tweening) - Chat: streaming pulse block replaced by a turn-level 4-pixel chase at the flow tail — rides the whole running turn (first-token wait, tools, streaming) instead of flickering with partial presence - Tool rows (ToolRow/BashRow): running no longer swaps the icon for a dot; an animated mask band sweeps the row content, gliding off on exit via mask-position transition - Composer: send/stop unified on the blue fill (bigger stop glyph, static white arrow), textarea box-sizing overflow fix, settling phase hides the composer while replay decides hero vs docked, workspace-placeholder fallback disables the bar - Hero: glow moved behind (z-index) with lower opacity; tool-row hover icon crossfade at 100ms
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/* Ongoing blue has no alias token (state-business-primary is the 500 step,
|
||||
* not this 450) — component-level var pinned to the static scale instead. */
|
||||
.dot,
|
||||
.ring {
|
||||
.matrix {
|
||||
--dsh-state-ongoing: var(--dsw-static-deepseek-450);
|
||||
}
|
||||
|
||||
@@ -42,24 +42,24 @@
|
||||
color: var(--dsw-alias-state-error-primary);
|
||||
}
|
||||
|
||||
.ring {
|
||||
/* Pixel chase: each outer cell holds a discrete brightness step (flat keyframe
|
||||
* holds, no tweening — the retro feel), peaking when the chase hits it and
|
||||
* decaying over the next three cells. Phase offsets come from per-rect
|
||||
* animation-delay (index * -125ms) set inline by the component. */
|
||||
.matrix {
|
||||
flex: none;
|
||||
color: var(--dsh-state-ongoing);
|
||||
animation: dsh-state-dot-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.stopFrom {
|
||||
stop-color: currentColor;
|
||||
stop-opacity: 1;
|
||||
.cell {
|
||||
fill: currentColor;
|
||||
opacity: 0.15;
|
||||
animation: dsh-state-dot-chase 1s infinite;
|
||||
}
|
||||
|
||||
.stopTo {
|
||||
stop-color: currentColor;
|
||||
stop-opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes dsh-state-dot-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
@keyframes dsh-state-dot-chase {
|
||||
0%, 12.4% { opacity: 1; }
|
||||
12.5%, 24.9% { opacity: 0.6; }
|
||||
25%, 37.4% { opacity: 0.35; }
|
||||
37.5%, 100% { opacity: 0.15; }
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
// StateDot: session state indicator (figma nodes 14:3303/3305/3312, 122:9182).
|
||||
// done/warning/error: 10x10 halo (same color, 10% opacity) around a 6x6 solid
|
||||
// core. ongoing: 10x10 ring, 1px inside stroke, color fading out along a
|
||||
// linear gradient, spinning. Colors resolve through --dsw-* tokens only.
|
||||
// core. ongoing: a pixel-art chase — the 8 outer cells of a 3x3 matrix light
|
||||
// up clockwise with a stepped trail. Colors resolve through --dsw-* tokens only.
|
||||
|
||||
import { useId } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import css from './StateDot.module.css'
|
||||
|
||||
/** Four-color session state semantic (green done / amber approval-waiting / blue running ring / red error). */
|
||||
export type StateDotState = 'done' | 'warning' | 'ongoing' | 'error'
|
||||
|
||||
/** Outer 3x3 matrix cells (2px pixels on a 10px grid), clockwise from top-left. */
|
||||
const MATRIX_CELLS: readonly (readonly [number, number])[] = [
|
||||
[0, 0], [4, 0], [8, 0], [8, 4], [8, 8], [4, 8], [0, 8], [0, 4],
|
||||
]
|
||||
|
||||
/**
|
||||
* Render a state dot.
|
||||
* @param props.state - which of the four states to show.
|
||||
@@ -22,25 +26,29 @@ export function StateDot({ state, size = 10, className }: {
|
||||
size?: number
|
||||
className?: string
|
||||
}) {
|
||||
const gradientId = useId()
|
||||
if (state === 'ongoing') {
|
||||
return (
|
||||
<svg
|
||||
className={clsx(css.ring, className)}
|
||||
className={clsx(css.matrix, className)}
|
||||
data-state="ongoing"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 10 10"
|
||||
shapeRendering="crispEdges"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
{/* Gradient handles from the figma node: (0.1,0) -> (0.85,1). */}
|
||||
<linearGradient id={gradientId} x1="1" y1="0" x2="8.5" y2="10" gradientUnits="userSpaceOnUse">
|
||||
<stop className={css.stopFrom} offset="0" />
|
||||
<stop className={css.stopTo} offset="1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="5" cy="5" r="4.5" fill="none" strokeWidth="1" stroke={`url(#${gradientId})`} />
|
||||
{MATRIX_CELLS.map(([x, y], index) => (
|
||||
<rect
|
||||
key={`${x}-${y}`}
|
||||
className={css.cell}
|
||||
x={x}
|
||||
y={y}
|
||||
width="2"
|
||||
height="2"
|
||||
/* Negative delay phases the chase so every cell animates from mount. */
|
||||
style={{ animationDelay: `${(index - MATRIX_CELLS.length) * 125}ms` }}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,16 +14,17 @@ describe('StateDot', () => {
|
||||
expect(dot.getAttribute('aria-hidden')).toBe('true')
|
||||
})
|
||||
|
||||
it('solid states are spans; ongoing is an svg gradient ring', () => {
|
||||
it('solid states are spans; ongoing is an svg pixel matrix', () => {
|
||||
const { container, rerender } = render(<StateDot state="done" />)
|
||||
expect(container.firstElementChild?.tagName).toBe('SPAN')
|
||||
rerender(<StateDot state="ongoing" />)
|
||||
const ring = container.firstElementChild as SVGSVGElement
|
||||
expect(ring.tagName).toBe('svg')
|
||||
const circle = ring.querySelector('circle')
|
||||
expect(circle?.getAttribute('stroke-width')).toBe('1')
|
||||
expect(circle?.getAttribute('stroke')).toMatch(/^url\(#/)
|
||||
expect(ring.querySelector('linearGradient')).not.toBeNull()
|
||||
const matrix = container.firstElementChild as SVGSVGElement
|
||||
expect(matrix.tagName).toBe('svg')
|
||||
const cells = matrix.querySelectorAll('rect')
|
||||
expect(cells).toHaveLength(8)
|
||||
// Chase phase: every cell carries its own negative animation delay.
|
||||
const delays = [...cells].map(cell => (cell).style.animationDelay)
|
||||
expect(new Set(delays).size).toBe(8)
|
||||
})
|
||||
|
||||
it('sizes via the size prop in both shapes', () => {
|
||||
|
||||
Reference in New Issue
Block a user