feat(web): align attachment display with DeepSeek Chat via ui-attachment atoms

Single-click original preview in the composer rail and chat history; remove
control inside the thumbnail, revealed on hover/focus (always on touch);
hidden-scrollbar rail overflow paged by edge arrows with wheel panning and
end-reveal on add; image-intake rejections and prompt failures announce as a
transient top-center toast instead of inline strips.

The attachment atoms move to a new zero-cordis package
@deepseek-ai/dsh-client-ui-attachment (rail, message gallery, lightbox),
seeded as a platform module; the toast is a ui-primitives atom. Strings
arrive as label props bridged from the conversation dictionary.
This commit is contained in:
creatixchu
2026-08-11 17:01:29 +08:00
parent 5d591e55c1
commit e611e825b1
56 changed files with 1366 additions and 251 deletions

View File

@@ -0,0 +1,112 @@
/* Thumbnail geometry mirrors DeepSeek Chat's composer rail: 64px cards with a
16px radius, remove control fully inside the card, arrows overlaid at the
edges instead of a scrollbar. */
.root {
position: relative;
min-width: 0;
}
.rail {
display: flex;
gap: 10px;
overflow-x: auto;
overflow-y: hidden;
/* Edge arrows page the overflow; the scrollbar stays hidden (both engines). */
scrollbar-width: none;
/* The rail scrolls on the composer's elevated input surface: bind the l2
pair (ui-theme styles/scrollbar.css rebinding contract) so anything that
does draw a thumb here matches the surface. */
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
}
.rail::-webkit-scrollbar {
display: none;
}
.item {
position: relative;
flex: 0 0 64px;
width: 64px;
height: 64px;
}
.thumbnail {
width: 64px;
height: 64px;
padding: 0;
overflow: hidden;
border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
border-radius: 16px;
background: var(--dsw-alias-interactive-bg-hover);
cursor: zoom-in;
}
.thumbnail img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.remove {
position: absolute;
top: 4px;
right: 4px;
z-index: 1;
display: grid;
place-items: center;
width: 18px;
height: 18px;
padding: 0;
border: none;
border-radius: 50%;
background: var(--dsw-alias-button-contrast-fill);
color: var(--dsw-alias-label-primary-inverted);
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.item:hover .remove,
.remove:focus-visible {
opacity: 1;
}
/* Touch surfaces have no hover to reveal the control. */
@media (pointer: coarse) {
.remove {
opacity: 1;
}
}
.arrow {
position: absolute;
top: 50%;
z-index: 2;
display: grid;
place-items: center;
width: 24px;
height: 24px;
padding: 0;
border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
border-radius: 999px;
background: var(--dsw-specific-input-major);
color: var(--dsw-alias-label-secondary);
box-shadow: var(--dsw-shadow-lv2);
cursor: pointer;
transform: translateY(-50%);
}
.arrow:hover {
background: var(--dsw-alias-interactive-bg-hover-solid);
}
.arrowLeft {
left: 4px;
}
.arrowRight {
right: 4px;
}