feat(app): new session progress indicator (#32662)

Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
This commit is contained in:
Aarav Sareen
2026-06-23 15:57:07 +05:30
committed by GitHub
parent a21e74773f
commit d24848359d
9 changed files with 572 additions and 97 deletions

View File

@@ -98,29 +98,6 @@
-webkit-user-drag: none;
}
[data-slot="project-avatar-surface"] [data-slot="project-avatar-loader"] {
position: absolute;
inset: -3px;
z-index: 2;
border-radius: 0;
background: conic-gradient(
from 180deg at 50% 50%,
var(--v2-grey-100) 0deg,
var(--v2-grey-1200) 0.04deg,
var(--v2-alpha-dark-50) 90deg,
var(--v2-grey-100) 360deg
);
mix-blend-mode: soft-light;
pointer-events: none;
animation: project-avatar-v2-loader-spin 1.2s linear infinite;
}
@keyframes project-avatar-v2-loader-spin {
to {
transform: rotate(360deg);
}
}
[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);

View File

@@ -79,20 +79,3 @@ export const AllVariantsUnread = {
</div>
),
}
export const Loading = {
args: {
fallback: "O",
variant: "orange",
loading: true,
},
}
export const LoadingAndUnread = {
args: {
fallback: "O",
variant: "blue",
loading: true,
unread: true,
},
}

View File

@@ -31,20 +31,10 @@ export interface ProjectAvatarProps extends ComponentProps<"div"> {
src?: string
variant?: ProjectAvatarVariant
unread?: boolean
loading?: boolean
}
export function ProjectAvatar(props: ProjectAvatarProps) {
const [split, rest] = splitProps(props, [
"fallback",
"src",
"variant",
"unread",
"loading",
"class",
"classList",
"style",
])
const [split, rest] = splitProps(props, ["fallback", "src", "variant", "unread", "class", "classList", "style"])
const src = split.src
return (
<div
@@ -61,14 +51,10 @@ export function ProjectAvatar(props: ProjectAvatarProps) {
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" />

View File

@@ -0,0 +1,425 @@
[data-component="session-progress-indicator-v2"] {
--_duration: 1200ms;
display: block;
flex-shrink: 0;
color: var(--v2-icon-icon-muted, #808080);
}
[data-component="session-progress-indicator-v2"] [data-dot] {
fill: currentColor;
opacity: 0.2;
animation-duration: var(--_duration);
animation-timing-function: ease-out;
animation-iteration-count: infinite;
animation-fill-mode: both;
}
@keyframes session-progress-indicator-v2-dot-0 {
0% { opacity: 0.2; }
12.5% { opacity: 0.2; }
25% { opacity: 0.2; }
37.5% { opacity: 1; }
50% { opacity: 0.5; }
62.5% { opacity: 0.2; }
75% { opacity: 0.2; }
87.5% { opacity: 0.2; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="0"] {
animation-name: session-progress-indicator-v2-dot-0;
}
@keyframes session-progress-indicator-v2-dot-5 {
0% { opacity: 0.2; }
12.5% { opacity: 0.2; }
25% { opacity: 0.2; }
37.5% { opacity: 0.75; }
50% { opacity: 0.2; }
62.5% { opacity: 0.2; }
75% { opacity: 0.2; }
87.5% { opacity: 0.2; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="5"] {
animation-name: session-progress-indicator-v2-dot-5;
}
@keyframes session-progress-indicator-v2-dot-10 {
0% { opacity: 0.2; }
12.5% { opacity: 0.2; }
25% { opacity: 1; }
37.5% { opacity: 0.5; }
50% { opacity: 0.2; }
62.5% { opacity: 0.2; }
75% { opacity: 0.2; }
87.5% { opacity: 0.2; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="10"] {
animation-name: session-progress-indicator-v2-dot-10;
}
@keyframes session-progress-indicator-v2-dot-15 {
0% { opacity: 0.2; }
12.5% { opacity: 0.2; }
25% { opacity: 0.75; }
37.5% { opacity: 0.2; }
50% { opacity: 0.2; }
62.5% { opacity: 0.2; }
75% { opacity: 0.2; }
87.5% { opacity: 0.2; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="15"] {
animation-name: session-progress-indicator-v2-dot-15;
}
@keyframes session-progress-indicator-v2-dot-20 {
0% { opacity: 0.2; }
12.5% { opacity: 1; }
25% { opacity: 0.5; }
37.5% { opacity: 0.2; }
50% { opacity: 0.2; }
62.5% { opacity: 0.2; }
75% { opacity: 0.2; }
87.5% { opacity: 0.2; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="20"] {
animation-name: session-progress-indicator-v2-dot-20;
}
@keyframes session-progress-indicator-v2-dot-1 {
0% { opacity: 0.2; }
12.5% { opacity: 0.2; }
25% { opacity: 0.2; }
37.5% { opacity: 0.2; }
50% { opacity: 0.75; }
62.5% { opacity: 0.2; }
75% { opacity: 0.2; }
87.5% { opacity: 0.2; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="1"] {
animation-name: session-progress-indicator-v2-dot-1;
}
@keyframes session-progress-indicator-v2-dot-6 {
0% { opacity: 0.2; }
12.5% { opacity: 0.2; }
25% { opacity: 0.2; }
37.5% { opacity: 1; }
50% { opacity: 0.5; }
62.5% { opacity: 0.2; }
75% { opacity: 0.2; }
87.5% { opacity: 0.2; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="6"] {
animation-name: session-progress-indicator-v2-dot-6;
}
@keyframes session-progress-indicator-v2-dot-11 {
0% { opacity: 0.2; }
12.5% { opacity: 0.2; }
25% { opacity: 1; }
37.5% { opacity: 0.75; }
50% { opacity: 0.2; }
62.5% { opacity: 0.2; }
75% { opacity: 0.2; }
87.5% { opacity: 0.2; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="11"] {
animation-name: session-progress-indicator-v2-dot-11;
}
@keyframes session-progress-indicator-v2-dot-16 {
0% { opacity: 0.2; }
12.5% { opacity: 1; }
25% { opacity: 0.5; }
37.5% { opacity: 0.2; }
50% { opacity: 0.2; }
62.5% { opacity: 0.2; }
75% { opacity: 0.2; }
87.5% { opacity: 0.2; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="16"] {
animation-name: session-progress-indicator-v2-dot-16;
}
@keyframes session-progress-indicator-v2-dot-21 {
0% { opacity: 0.2; }
12.5% { opacity: 0.75; }
25% { opacity: 0.2; }
37.5% { opacity: 0.2; }
50% { opacity: 0.2; }
62.5% { opacity: 0.2; }
75% { opacity: 0.2; }
87.5% { opacity: 0.2; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="21"] {
animation-name: session-progress-indicator-v2-dot-21;
}
@keyframes session-progress-indicator-v2-dot-2 {
0% { opacity: 0.2; }
12.5% { opacity: 0.2; }
25% { opacity: 0.2; }
37.5% { opacity: 0.2; }
50% { opacity: 1; }
62.5% { opacity: 0.5; }
75% { opacity: 0.2; }
87.5% { opacity: 0.2; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="2"] {
animation-name: session-progress-indicator-v2-dot-2;
}
@keyframes session-progress-indicator-v2-dot-7 {
0% { opacity: 0.2; }
12.5% { opacity: 0.2; }
25% { opacity: 0.2; }
37.5% { opacity: 0.2; }
50% { opacity: 1; }
62.5% { opacity: 0.75; }
75% { opacity: 0.2; }
87.5% { opacity: 0.2; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="7"] {
animation-name: session-progress-indicator-v2-dot-7;
}
@keyframes session-progress-indicator-v2-dot-12 {
0% { opacity: 1; }
12.5% { opacity: 1; }
25% { opacity: 1; }
37.5% { opacity: 1; }
50% { opacity: 1; }
62.5% { opacity: 1; }
75% { opacity: 1; }
87.5% { opacity: 1; }
100% { opacity: 1; }
}
[data-component="session-progress-indicator-v2"] [data-dot="12"] {
animation-name: session-progress-indicator-v2-dot-12;
}
@keyframes session-progress-indicator-v2-dot-17 {
0% { opacity: 1; }
12.5% { opacity: 0.75; }
25% { opacity: 0.2; }
37.5% { opacity: 0.2; }
50% { opacity: 0.2; }
62.5% { opacity: 0.2; }
75% { opacity: 0.2; }
87.5% { opacity: 0.2; }
100% { opacity: 1; }
}
[data-component="session-progress-indicator-v2"] [data-dot="17"] {
animation-name: session-progress-indicator-v2-dot-17;
}
@keyframes session-progress-indicator-v2-dot-22 {
0% { opacity: 1; }
12.5% { opacity: 0.5; }
25% { opacity: 0.2; }
37.5% { opacity: 0.2; }
50% { opacity: 0.2; }
62.5% { opacity: 0.2; }
75% { opacity: 0.2; }
87.5% { opacity: 0.2; }
100% { opacity: 1; }
}
[data-component="session-progress-indicator-v2"] [data-dot="22"] {
animation-name: session-progress-indicator-v2-dot-22;
}
@keyframes session-progress-indicator-v2-dot-3 {
0% { opacity: 0.2; }
12.5% { opacity: 0.2; }
25% { opacity: 0.2; }
37.5% { opacity: 0.2; }
50% { opacity: 0.2; }
62.5% { opacity: 0.75; }
75% { opacity: 0.2; }
87.5% { opacity: 0.2; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="3"] {
animation-name: session-progress-indicator-v2-dot-3;
}
@keyframes session-progress-indicator-v2-dot-8 {
0% { opacity: 0.2; }
12.5% { opacity: 0.2; }
25% { opacity: 0.2; }
37.5% { opacity: 0.2; }
50% { opacity: 0.2; }
62.5% { opacity: 1; }
75% { opacity: 0.75; }
87.5% { opacity: 0.2; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="8"] {
animation-name: session-progress-indicator-v2-dot-8;
}
@keyframes session-progress-indicator-v2-dot-13 {
0% { opacity: 0.2; }
12.5% { opacity: 0.2; }
25% { opacity: 0.2; }
37.5% { opacity: 0.2; }
50% { opacity: 0.2; }
62.5% { opacity: 0.2; }
75% { opacity: 1; }
87.5% { opacity: 1; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="13"] {
animation-name: session-progress-indicator-v2-dot-13;
}
@keyframes session-progress-indicator-v2-dot-18 {
0% { opacity: 0.5; }
12.5% { opacity: 0.2; }
25% { opacity: 0.2; }
37.5% { opacity: 0.2; }
50% { opacity: 0.2; }
62.5% { opacity: 0.2; }
75% { opacity: 0.2; }
87.5% { opacity: 0.75; }
100% { opacity: 0.5; }
}
[data-component="session-progress-indicator-v2"] [data-dot="18"] {
animation-name: session-progress-indicator-v2-dot-18;
}
@keyframes session-progress-indicator-v2-dot-23 {
0% { opacity: 0.75; }
12.5% { opacity: 0.2; }
25% { opacity: 0.2; }
37.5% { opacity: 0.2; }
50% { opacity: 0.2; }
62.5% { opacity: 0.2; }
75% { opacity: 0.2; }
87.5% { opacity: 0.2; }
100% { opacity: 0.75; }
}
[data-component="session-progress-indicator-v2"] [data-dot="23"] {
animation-name: session-progress-indicator-v2-dot-23;
}
@keyframes session-progress-indicator-v2-dot-4 {
0% { opacity: 0.2; }
12.5% { opacity: 0.2; }
25% { opacity: 0.2; }
37.5% { opacity: 0.2; }
50% { opacity: 0.2; }
62.5% { opacity: 1; }
75% { opacity: 0.5; }
87.5% { opacity: 0.2; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="4"] {
animation-name: session-progress-indicator-v2-dot-4;
}
@keyframes session-progress-indicator-v2-dot-9 {
0% { opacity: 0.2; }
12.5% { opacity: 0.2; }
25% { opacity: 0.2; }
37.5% { opacity: 0.2; }
50% { opacity: 0.2; }
62.5% { opacity: 0.2; }
75% { opacity: 0.75; }
87.5% { opacity: 0.2; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="9"] {
animation-name: session-progress-indicator-v2-dot-9;
}
@keyframes session-progress-indicator-v2-dot-14 {
0% { opacity: 0.2; }
12.5% { opacity: 0.2; }
25% { opacity: 0.2; }
37.5% { opacity: 0.2; }
50% { opacity: 0.2; }
62.5% { opacity: 0.2; }
75% { opacity: 1; }
87.5% { opacity: 1; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="14"] {
animation-name: session-progress-indicator-v2-dot-14;
}
@keyframes session-progress-indicator-v2-dot-19 {
0% { opacity: 0.2; }
12.5% { opacity: 0.2; }
25% { opacity: 0.2; }
37.5% { opacity: 0.2; }
50% { opacity: 0.2; }
62.5% { opacity: 0.2; }
75% { opacity: 0.2; }
87.5% { opacity: 0.75; }
100% { opacity: 0.2; }
}
[data-component="session-progress-indicator-v2"] [data-dot="19"] {
animation-name: session-progress-indicator-v2-dot-19;
}
@keyframes session-progress-indicator-v2-dot-24 {
0% { opacity: 0.5; }
12.5% { opacity: 0.2; }
25% { opacity: 0.2; }
37.5% { opacity: 0.2; }
50% { opacity: 0.2; }
62.5% { opacity: 0.2; }
75% { opacity: 0.2; }
87.5% { opacity: 0.5; }
100% { opacity: 0.5; }
}
[data-component="session-progress-indicator-v2"] [data-dot="24"] {
animation-name: session-progress-indicator-v2-dot-24;
}
@media (prefers-reduced-motion: reduce) {
[data-component="session-progress-indicator-v2"] [data-dot] {
animation: none;
}
[data-component="session-progress-indicator-v2"] [data-dot="12"] {
opacity: 1;
}
}

View File

@@ -0,0 +1,66 @@
// @ts-nocheck
import { SessionProgressIndicatorV2 } from "./session-progress-indicator-v2"
const docs = `### Overview
Animated 5×5 dot grid loader for in-progress session state.
Derived from Figma \`_sessionProgressIndicator\` with 8-frame rotation.
### API
- Accepts standard SVG props.
### Behavior
- CSS keyframes drive per-dot opacity across 8 frames (1.2s loop).
- Center dot stays at full opacity throughout the cycle.
### Accessibility
- Sets \`aria-hidden="true"\` by default.
### Theming
- Uses \`currentColor\` via \`--v2-icon-icon-muted\`.
`
export default {
title: "UI V2/SessionProgressIndicator",
id: "components-session-progress-indicator-v2",
component: SessionProgressIndicatorV2,
tags: ["autodocs"],
parameters: {
docs: {
description: {
component: docs,
},
},
},
}
export const Basic = {
render: () => <SessionProgressIndicatorV2 />,
}
export const Sizes = {
render: () => (
<div style={{ display: "flex", gap: "16px", "align-items": "center" }}>
<SessionProgressIndicatorV2 width={12} height={12} />
<SessionProgressIndicatorV2 />
<SessionProgressIndicatorV2 width={24} height={24} />
</div>
),
}
export const OnDark = {
render: () => (
<div
style={{
display: "flex",
gap: "16px",
"align-items": "center",
padding: "16px",
"background-color": "#171717",
color: "#c7c7c7",
}}
>
<SessionProgressIndicatorV2 />
</div>
),
}

View File

@@ -0,0 +1,36 @@
import { For, splitProps, type ComponentProps } from "solid-js"
import "./session-progress-indicator-v2.css"
const grid = 5
const dot = 2
const gap = 1
const origin = 1.5
const dots = Array.from({ length: grid * grid }, (_, index) => ({
index,
x: origin + (index % grid) * (dot + gap),
y: origin + Math.floor(index / grid) * (dot + gap),
}))
export function SessionProgressIndicatorV2(props: ComponentProps<"svg">) {
const [local, rest] = splitProps(props, ["class", "classList", "width", "height"])
return (
<svg
{...rest}
class={local.class}
classList={local.classList}
width={local.width ?? 16}
height={local.height ?? 16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
data-component="session-progress-indicator-v2"
aria-hidden={rest["aria-hidden"] ?? "true"}
>
<For each={dots}>
{(cell) => (
<rect data-dot={cell.index} x={cell.x} y={cell.y} width={dot} height={dot} />
)}
</For>
</svg>
)
}