feat(app): new session progress indicator (#32662)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
This commit is contained in:
parent
a21e74773f
commit
d24848359d
@ -22,7 +22,7 @@ import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||
|
||||
import { getProjectAvatarVariant, LayoutRoute, useLayout, type LocalProject } from "@/context/layout"
|
||||
import { LayoutRoute, useLayout } from "@/context/layout"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@ -30,9 +30,8 @@ import { useSettings } from "@/context/settings"
|
||||
import { WindowsAppMenu } from "./windows-app-menu"
|
||||
import { applyPath, backPath, forwardPath } from "./titlebar-history"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||
import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/layout/helpers"
|
||||
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
|
||||
import { projectForSession } from "@/pages/layout/helpers"
|
||||
import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
@ -868,7 +867,7 @@ function TabNavItem(props: {
|
||||
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base"
|
||||
>
|
||||
<span data-slot="project-avatar-slot">
|
||||
<ProjectTabAvatar
|
||||
<SessionTabAvatar
|
||||
project={project()}
|
||||
directory={session().directory}
|
||||
sessionId={session().id}
|
||||
@ -904,26 +903,6 @@ function TabNavItem(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function ProjectTabAvatar(props: {
|
||||
project?: LocalProject
|
||||
directory: string
|
||||
sessionId: string
|
||||
activeServer: boolean
|
||||
}) {
|
||||
const directory = () => props.directory
|
||||
const sessionId = () => props.sessionId
|
||||
const state = useSessionTabAvatarState(directory, sessionId, () => props.activeServer)
|
||||
return (
|
||||
<ProjectAvatar
|
||||
fallback={displayName(props.project ?? { worktree: props.directory })}
|
||||
src={getProjectAvatarSource(props.project?.id, props.project?.icon)}
|
||||
variant={getProjectAvatarVariant(props.project?.icon?.color)}
|
||||
unread={state.unread()}
|
||||
loading={state.loading()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DraftTabItem(props: {
|
||||
ref?: HTMLDivElement
|
||||
href: string
|
||||
|
||||
@ -38,7 +38,7 @@ import {
|
||||
sortedRootSessions,
|
||||
toggleHomeProjectSelection,
|
||||
} from "@/pages/layout/helpers"
|
||||
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
|
||||
import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar"
|
||||
import { sessionTitle } from "@/utils/session-title"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
import { useGlobal } from "@/context/global"
|
||||
@ -755,21 +755,6 @@ function HomeProjectAvatar(props: { project: LocalProject }) {
|
||||
)
|
||||
}
|
||||
|
||||
function HomeSessionAvatar(props: { project: LocalProject; session: Session; activeServer: boolean }) {
|
||||
const directory = () => props.session.directory
|
||||
const sessionId = () => props.session.id
|
||||
const state = useSessionTabAvatarState(directory, sessionId, () => props.activeServer)
|
||||
return (
|
||||
<ProjectAvatar
|
||||
fallback={displayName(props.project)}
|
||||
src={getProjectAvatarSource(props.project.id, props.project.icon)}
|
||||
variant={getProjectAvatarVariant(props.project.icon?.color)}
|
||||
unread={state.unread()}
|
||||
loading={state.loading()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function HomeSessionLeading(props: {
|
||||
project: LocalProject
|
||||
session: Session
|
||||
@ -787,7 +772,12 @@ function HomeSessionLeading(props: {
|
||||
style={{ right: "calc(100% + 12px)" }}
|
||||
/>
|
||||
</Show>
|
||||
<HomeSessionAvatar project={props.project} session={props.session} activeServer={props.activeServer} />
|
||||
<SessionTabAvatar
|
||||
project={props.project}
|
||||
directory={props.session.directory}
|
||||
sessionId={props.session.id}
|
||||
activeServer={props.activeServer}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
33
packages/app/src/pages/layout/session-tab-avatar.tsx
Normal file
33
packages/app/src/pages/layout/session-tab-avatar.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import type { LocalProject } from "@/context/layout"
|
||||
import { getProjectAvatarVariant } from "@/context/layout"
|
||||
import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
|
||||
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
|
||||
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||
import { SessionProgressIndicatorV2 } from "@opencode-ai/ui/v2/session-progress-indicator-v2"
|
||||
import { Show } from "solid-js"
|
||||
|
||||
export function SessionTabAvatar(props: {
|
||||
project?: LocalProject
|
||||
directory: string
|
||||
sessionId: string
|
||||
activeServer: boolean
|
||||
}) {
|
||||
const directory = () => props.directory
|
||||
const sessionId = () => props.sessionId
|
||||
const state = useSessionTabAvatarState(directory, sessionId, () => props.activeServer)
|
||||
return (
|
||||
<Show
|
||||
when={state.loading()}
|
||||
fallback={
|
||||
<ProjectAvatar
|
||||
fallback={displayName(props.project ?? { worktree: props.directory })}
|
||||
src={getProjectAvatarSource(props.project?.id, props.project?.icon)}
|
||||
variant={getProjectAvatarVariant(props.project?.icon?.color)}
|
||||
unread={state.unread()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SessionProgressIndicatorV2 class="shrink-0" />
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@ -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" />
|
||||
|
||||
425
packages/ui/src/v2/components/session-progress-indicator-v2.css
Normal file
425
packages/ui/src/v2/components/session-progress-indicator-v2.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
),
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user