feat(app): updates to project avatar (#30964)

This commit is contained in:
Aarav Sareen 2026-06-06 03:43:04 +05:30 committed by GitHub
parent 93a58f55ca
commit 24347f336c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 77 additions and 33 deletions

View File

@ -1,14 +1,24 @@
[data-component="project-avatar-v2"] {
position: relative;
flex-shrink: 0;
width: 16px;
height: 16px;
}
[data-component="project-avatar-v2"][data-unread] {
overflow: visible;
}
[data-slot="project-avatar-surface"] {
--project-avatar-bg: var(--v2-avatar-bg-gray);
--project-avatar-border: var(--v2-avatar-border-gray);
position: relative;
box-sizing: border-box;
display: flex;
flex-shrink: 0;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
overflow: hidden;
border-radius: 4px;
background:
@ -26,56 +36,56 @@
-webkit-user-select: none;
}
[data-component="project-avatar-v2"][data-variant="orange"] {
[data-slot="project-avatar-surface"][data-variant="orange"] {
--project-avatar-bg: var(--v2-avatar-bg-orange);
--project-avatar-border: var(--v2-avatar-border-orange);
}
[data-component="project-avatar-v2"][data-variant="yellow"] {
[data-slot="project-avatar-surface"][data-variant="yellow"] {
--project-avatar-bg: var(--v2-avatar-bg-yellow);
--project-avatar-border: var(--v2-avatar-border-yellow);
}
[data-component="project-avatar-v2"][data-variant="cyan"] {
[data-slot="project-avatar-surface"][data-variant="cyan"] {
--project-avatar-bg: var(--v2-avatar-bg-cyan);
--project-avatar-border: var(--v2-avatar-border-cyan);
}
[data-component="project-avatar-v2"][data-variant="green"] {
[data-slot="project-avatar-surface"][data-variant="green"] {
--project-avatar-bg: var(--v2-avatar-bg-green);
--project-avatar-border: var(--v2-avatar-border-green);
}
[data-component="project-avatar-v2"][data-variant="red"] {
[data-slot="project-avatar-surface"][data-variant="red"] {
--project-avatar-bg: var(--v2-avatar-bg-red);
--project-avatar-border: var(--v2-avatar-border-red);
}
[data-component="project-avatar-v2"][data-variant="pink"] {
[data-slot="project-avatar-surface"][data-variant="pink"] {
--project-avatar-bg: var(--v2-avatar-bg-pink);
--project-avatar-border: var(--v2-avatar-border-pink);
}
[data-component="project-avatar-v2"][data-variant="blue"] {
[data-slot="project-avatar-surface"][data-variant="blue"] {
--project-avatar-bg: var(--v2-avatar-bg-blue);
--project-avatar-border: var(--v2-avatar-border-blue);
}
[data-component="project-avatar-v2"][data-variant="purple"] {
[data-slot="project-avatar-surface"][data-variant="purple"] {
--project-avatar-bg: var(--v2-avatar-bg-purple);
--project-avatar-border: var(--v2-avatar-border-purple);
}
[data-component="project-avatar-v2"][data-variant="gray"] {
[data-slot="project-avatar-surface"][data-variant="gray"] {
--project-avatar-bg: var(--v2-avatar-bg-gray);
--project-avatar-border: var(--v2-avatar-border-gray);
}
[data-component="project-avatar-v2"][data-has-image] {
[data-slot="project-avatar-surface"][data-has-image] {
background: var(--project-avatar-bg);
}
[data-component="project-avatar-v2"] [data-slot="project-avatar-image"] {
[data-slot="project-avatar-surface"] [data-slot="project-avatar-image"] {
position: relative;
z-index: 1;
display: block;
@ -88,11 +98,11 @@
-webkit-user-drag: none;
}
[data-component="project-avatar-v2"] [data-slot="project-avatar-loader"] {
[data-slot="project-avatar-surface"] [data-slot="project-avatar-loader"] {
position: absolute;
inset: 0;
inset: -3px;
z-index: 2;
border-radius: 4px;
border-radius: 0;
background: conic-gradient(
from 180deg at 50% 50%,
var(--v2-grey-100) 0deg,
@ -111,6 +121,27 @@
}
}
[data-component="project-avatar-v2"][data-unread] [data-slot="project-avatar-surface"] {
-webkit-mask-image: radial-gradient(
circle 4.5px at calc(100% - 1px) 1px,
transparent 4.5px,
black 4.5px
);
mask-image: radial-gradient(circle 4.5px at calc(100% - 1px) 1px, transparent 4.5px, black 4.5px);
}
[data-slot="project-avatar-unread-dot"] {
position: absolute;
z-index: 3;
width: 6px;
height: 6px;
right: -2px;
top: -2px;
border-radius: 9999px;
background: var(--v2-background-bg-accent);
pointer-events: none;
}
[data-slot="project-avatar-slot"] {
display: flex;
flex-shrink: 0;
@ -120,9 +151,3 @@
height: 22px;
overflow: visible;
}
[data-component="project-avatar-v2"][data-unread] {
overflow: visible;
outline: 2px solid var(--v2-background-bg-accent);
outline-offset: 1px;
}

View File

@ -3,7 +3,7 @@ import { For } from "solid-js"
import { ProjectAvatar, PROJECT_AVATAR_VARIANTS } from "./project-avatar-v2"
const docs = `### Overview
Saturated 16px project avatar with color variants and optional unread ring.
Saturated 16px project avatar with color variants and optional unread dot.
### API
- Required: \`fallback\` string.
@ -12,7 +12,7 @@ Saturated 16px project avatar with color variants and optional unread ring.
### Variants
- Color: orange, yellow, cyan, green, red, pink, blue, purple, gray.
- Image vs initial content state.
- Unread ring when \`unread\` is set.
- Unread dot with corner mask when \`unread\` is set.
### Theming
- Uses \`--v2-avatar-bg-*\` and \`--v2-avatar-border-*\` tokens with inset box-shadow borders.
@ -70,6 +70,18 @@ export const Unread = {
},
}
export const AllVariantsUnread = {
render: () => (
<div style={{ display: "flex", gap: "16px", "align-items": "center" }}>
<For each={PROJECT_AVATAR_VARIANTS}>
{(variant) => (
<ProjectAvatar fallback={variant[0].toUpperCase()} variant={variant} unread />
)}
</For>
</div>
),
}
export const Loading = {
args: {
fallback: "O",

View File

@ -50,21 +50,28 @@ export function ProjectAvatar(props: ProjectAvatarProps) {
<div
{...rest}
data-component="project-avatar-v2"
data-variant={split.variant ?? "gray"}
data-has-image={src ? "" : undefined}
data-unread={split.unread ? "" : undefined}
data-loading={split.loading ? "" : undefined}
classList={{
...split.classList,
[split.class ?? ""]: !!split.class,
}}
style={typeof split.style === "object" ? split.style : undefined}
>
<Show when={src} fallback={first(split.fallback)}>
{(value) => <img src={value()} draggable={false} data-slot="project-avatar-image" />}
</Show>
<Show when={split.loading}>
<span data-slot="project-avatar-loader" aria-hidden="true" />
<div
data-slot="project-avatar-surface"
data-variant={split.variant ?? "gray"}
data-has-image={src ? "" : undefined}
data-loading={split.loading ? "" : undefined}
>
<Show when={src} fallback={first(split.fallback)}>
{(value) => <img src={value()} draggable={false} data-slot="project-avatar-image" />}
</Show>
<Show when={split.loading}>
<span data-slot="project-avatar-loader" aria-hidden="true" />
</Show>
</div>
<Show when={split.unread}>
<span data-slot="project-avatar-unread-dot" aria-hidden="true" />
</Show>
</div>
)