| ... | @@ -8,16 +8,31 @@ | ... | @@ -8,16 +8,31 @@ |
| 8 | * number of bars costs one opacity animation. | 8 | * number of bars costs one opacity animation. |
| 9 | */ | 9 | */ |
| 10 | import type { PullRequestListEntry, PullRequestSummary } from "@t3tools/contracts"; | 10 | import type { PullRequestListEntry, PullRequestSummary } from "@t3tools/contracts"; |
| 11 | import { ArrowLeftIcon } from "lucide-react"; | 11 | import { |
| | 12 | ArrowLeftIcon, |
| | 13 | ChevronRightIcon, |
| | 14 | EllipsisIcon, |
| | 15 | ExternalLinkIcon, |
| | 16 | FileDiffIcon, |
| | 17 | PanelRightIcon, |
| | 18 | TagIcon, |
| | 19 | UserPlusIcon, |
| | 20 | UsersIcon, |
| | 21 | } from "lucide-react"; |
| 12 | import type { ReactNode } from "react"; | 22 | import type { ReactNode } from "react"; |
| 13 | | 23 | |
| | 24 | import { readLocalApi } from "~/localApi"; |
| 14 | import { cn } from "~/lib/utils"; | 25 | import { cn } from "~/lib/utils"; |
| 15 | import { formatRelativeTimeLabel } from "~/timestampFormat"; | 26 | import { formatRelativeTimeLabel } from "~/timestampFormat"; |
| 16 | | 27 | |
| | 28 | import { Button, InlineButton } from "../ui/button"; |
| | 29 | import { Toggle, ToggleGroup } from "../ui/toggle-group"; |
| | 30 | import { PullRequestCopyableCode } from "./PullRequestCopyableCode"; |
| 17 | import { pullRequestLabelColor } from "./pullRequestList.logic"; | 31 | import { pullRequestLabelColor } from "./pullRequestList.logic"; |
| 18 | import { | 32 | import { |
| 19 | PullRequestActorLabel, | 33 | PullRequestActorLabel, |
| 20 | PullRequestDiffStat, | 34 | PullRequestDiffStat, |
| | 35 | PullRequestMetaLine, |
| 21 | pullRequestChecksStatePresentation, | 36 | pullRequestChecksStatePresentation, |
| 22 | resolvePullRequestState, | 37 | resolvePullRequestState, |
| 23 | } from "./pullRequestPresentation"; | 38 | } from "./pullRequestPresentation"; |
| ... | @@ -29,6 +44,11 @@ function GhostBar({ className }: { className?: string | undefined }) { | ... | @@ -29,6 +44,11 @@ function GhostBar({ className }: { className?: string | undefined }) { |
| 29 | /** Widths cycle rather than randomize, so the ghost renders the same on every pass. */ | 44 | /** Widths cycle rather than randomize, so the ghost renders the same on every pass. */ |
| 30 | const TITLE_WIDTHS = ["w-3/5", "w-2/5", "w-1/2", "w-2/3", "w-2/5", "w-3/5", "w-1/2"]; | 45 | const TITLE_WIDTHS = ["w-3/5", "w-2/5", "w-1/2", "w-2/3", "w-2/5", "w-3/5", "w-1/2"]; |
| 31 | const META_WIDTHS = ["w-2/5", "w-1/3", "w-2/5", "w-1/4", "w-1/3", "w-2/5", "w-1/3"]; | 46 | const META_WIDTHS = ["w-2/5", "w-1/3", "w-2/5", "w-1/4", "w-1/3", "w-2/5", "w-1/3"]; |
| | 47 | const DEFAULT_DETAIL_TABS = [ |
| | 48 | { value: "summary", label: "Summary" }, |
| | 49 | { value: "timeline", label: "Timeline" }, |
| | 50 | { value: "code", label: "Code" }, |
| | 51 | ] as const; |
| 32 | | 52 | |
| 33 | /** Rows in the list's own grid — glyph, title over meta, time over diffstat. */ | 53 | /** Rows in the list's own grid — glyph, title over meta, time over diffstat. */ |
| 34 | export function PullRequestListGhost({ | 54 | export function PullRequestListGhost({ |
| ... | @@ -77,10 +97,25 @@ export function PullRequestDetailGhost({ | ... | @@ -77,10 +97,25 @@ export function PullRequestDetailGhost({ |
| 77 | seed: entry, | 97 | seed: entry, |
| 78 | summary, | 98 | summary, |
| 79 | actions, | 99 | actions, |
| | 100 | checkoutCommand, |
| | 101 | tabs = DEFAULT_DETAIL_TABS, |
| | 102 | activeTab, |
| | 103 | number, |
| | 104 | onBack, |
| | 105 | onClose, |
| | 106 | onCheckoutError, |
| 80 | }: { | 107 | }: { |
| 81 | seed?: PullRequestListEntry | null; | 108 | seed?: PullRequestListEntry | null; |
| 82 | summary?: PullRequestSummary | null; | 109 | summary?: PullRequestSummary | null; |
| 83 | actions?: ReactNode; | 110 | actions?: ReactNode; |
| | 111 | checkoutCommand?: string | null; |
| | 112 | tabs?: ReadonlyArray<{ value: string; label: string }>; |
| | 113 | /** The panel's current tab, so the highlight does not jump when the detail arrives. */ |
| | 114 | activeTab?: string; |
| | 115 | number?: number; |
| | 116 | onBack?: (() => void) | undefined; |
| | 117 | onClose?: (() => void) | undefined; |
| | 118 | onCheckoutError?: ((error: Error) => void) | undefined; |
| 84 | }) { | 119 | }) { |
| 85 | const seed = summary | 120 | const seed = summary |
| 86 | ? { | 121 | ? { |
| ... | @@ -100,6 +135,10 @@ export function PullRequestDetailGhost({ | ... | @@ -100,6 +135,10 @@ export function PullRequestDetailGhost({ |
| 100 | seed?.checksState === "failing" || seed?.checksState === "pending" | 135 | seed?.checksState === "failing" || seed?.checksState === "pending" |
| 101 | ? pullRequestChecksStatePresentation(seed.checksState) | 136 | ? pullRequestChecksStatePresentation(seed.checksState) |
| 102 | : null; | 137 | : null; |
| | 138 | const checkout = checkoutCommand ?? null; |
| | 139 | const changedFiles = summary?.changedFiles ?? null; |
| | 140 | const selectedTab = |
| | 141 | tabs.find((item) => item.value === activeTab)?.value ?? tabs[0]?.value ?? "summary"; |
| 103 | | 142 | |
| 104 | return ( | 143 | return ( |
| 105 | <div | 144 | <div |
| ... | @@ -110,100 +149,178 @@ export function PullRequestDetailGhost({ | ... | @@ -110,100 +149,178 @@ export function PullRequestDetailGhost({ |
| 110 | !seed && "motion-safe:animate-skeleton", | 149 | !seed && "motion-safe:animate-skeleton", |
| 111 | )} | 150 | )} |
| 112 | > | 151 | > |
| 113 | <div className="@container/pr-header shrink-0 border-b border-border/60"> | 152 | <div className="@container/pr-header grid min-w-0 shrink-0 grid-cols-[minmax(0,1fr)_auto] items-start gap-x-2 border-b border-border/60"> |
| 114 | <div className="flex h-7 items-center justify-between gap-3 px-4"> | 153 | <div className="pl-4 grid h-7 min-w-0 items-center overflow-hidden"> |
| 115 | <div className="flex min-w-0 flex-1 items-center gap-1.5"> | 154 | <div className="flex min-w-0 items-center gap-1 text-sm text-muted-foreground sm:text-xs"> |
| 116 | {seed && statePresentation ? ( | 155 | {onBack ? ( |
| | 156 | <Button |
| | 157 | size="icon-micro" |
| | 158 | variant="ghost-muted" |
| | 159 | className="-ml-1.5" |
| | 160 | onClick={onBack} |
| | 161 | aria-label="Back to this thread's pull requests" |
| | 162 | > |
| | 163 | <ArrowLeftIcon aria-hidden className="size-3.5" /> |
| | 164 | </Button> |
| | 165 | ) : null} |
| | 166 | {seed ? ( |
| 117 | <> | 167 | <> |
| 118 | <span className="min-w-0 truncate text-xs font-medium text-muted-foreground"> | 168 | <span className="min-w-0 truncate font-medium">{seed.repository}</span> |
| 119 | {seed.repository} | 169 | <InlineButton |
| 120 | </span> | 170 | onClick={() => void readLocalApi()?.shell.openExternal(seed.url)} |
| 121 | <span | 171 | className={cn( |
| 122 | className={cn("shrink-0 text-xs font-medium", statePresentation.toneClassName)} | 172 | "font-medium underline-offset-2 hover:underline", |
| | 173 | statePresentation?.toneClassName, |
| | 174 | )} |
| | 175 | aria-label={`Open pull request #${seed.number} on host`} |
| 123 | > | 176 | > |
| 124 | #{seed.number} | 177 | #{seed.number} |
| 125 | </span> | 178 | <ExternalLinkIcon aria-hidden className="size-2.5" /> |
| | 179 | </InlineButton> |
| 126 | </> | 180 | </> |
| 127 | ) : ( | 181 | ) : ( |
| 128 | <> | 182 | <> |
| 129 | <GhostBar className="w-24" /> | 183 | <GhostBar className="w-24" /> |
| 130 | <GhostBar className="w-9" /> | 184 | <span className="shrink-0 text-xs text-muted-foreground">#{number ?? "…"}</span> |
| 131 | </> | 185 | </> |
| 132 | )} | 186 | )} |
| 133 | </div> | 187 | </div> |
| 134 | <div className="flex shrink-0 items-center gap-1"> | 188 | </div> |
| 135 | {actions ?? <GhostBar className="h-5 w-16 rounded-md" />} | 189 | <div className="mr-4 flex h-7 shrink-0 items-center justify-end gap-1"> |
| 136 | <GhostBar className="size-6 rounded-md" /> | 190 | {actions ?? <GhostBar className="h-6 w-16 rounded-md" />} |
| 137 | </div> | 191 | <Button size="icon-xs" variant="ghost" disabled aria-label="Pull request actions loading"> |
| | 192 | <EllipsisIcon aria-hidden className="size-4" /> |
| | 193 | </Button> |
| | 194 | {onClose ? ( |
| | 195 | <Button |
| | 196 | size="icon-xs" |
| | 197 | variant="ghost" |
| | 198 | aria-label="Collapse pull request panel" |
| | 199 | onClick={onClose} |
| | 200 | > |
| | 201 | <PanelRightIcon aria-hidden className="size-3.5" /> |
| | 202 | </Button> |
| | 203 | ) : null} |
| 138 | </div> | 204 | </div> |
| 139 | | 205 | |
| 140 | <div className="px-4 pb-4 pt-1"> | 206 | <div className="col-span-2 grid grid-rows-[1fr]"> |
| 141 | {seed ? ( | 207 | <div className="min-h-0 overflow-hidden"> |
| 142 | <h1 className="truncate text-base font-semibold leading-snug">{seed.title}</h1> | 208 | <div className="col-span-2 mt-1 min-w-0 px-4 pb-4"> |
| 143 | ) : ( | | |
| 144 | <GhostBar className="h-5 w-4/5 max-w-md" /> | | |
| 145 | )} | | |
| 146 | <div className="mt-2 flex items-center gap-1.5"> | | |
| 147 | {seed ? ( | | |
| 148 | <> | | |
| 149 | <PullRequestActorLabel | | |
| 150 | actor={seed.author ?? null} | | |
| 151 | className="font-medium" | | |
| 152 | tooltip={false} | | |
| 153 | /> | | |
| 154 | <span className="text-xs text-muted-foreground"> | | |
| 155 | updated {formatRelativeTimeLabel(seed.updatedAt)} | | |
| 156 | </span> | | |
| 157 | </> | | |
| 158 | ) : ( | | |
| 159 | <> | | |
| 160 | <GhostBar className="size-4 rounded-full" /> | | |
| 161 | <GhostBar className="w-24" /> | | |
| 162 | </> | | |
| 163 | )} | | |
| 164 | </div> | | |
| 165 | <div className="mt-4 flex min-w-0 items-center gap-2"> | | |
| 166 | {seed ? ( | | |
| 167 | <span className="flex min-w-0 flex-1 items-center gap-1.5 font-mono text-xs text-muted-foreground/70"> | | |
| 168 | <code className="min-w-0 max-w-[40%] shrink-0 truncate">{seed.baseBranch}</code> | | |
| 169 | <ArrowLeftIcon | | |
| 170 | aria-label="receives changes from" | | |
| 171 | className="size-3.5 shrink-0 opacity-60" | | |
| 172 | /> | | |
| 173 | <code className="min-w-0 flex-1 truncate">{seed.headBranch}</code> | | |
| 174 | </span> | | |
| 175 | ) : ( | | |
| 176 | <> | | |
| 177 | <GhostBar className="h-6 w-24 rounded-md" /> | | |
| 178 | <GhostBar className="size-3 rounded-full" /> | | |
| 179 | <GhostBar className="h-6 w-32 rounded-md" /> | | |
| 180 | </> | | |
| 181 | )} | | |
| 182 | <div className="ml-auto flex shrink-0 items-center gap-2"> | | |
| 183 | <GhostBar className="w-10" /> | | |
| 184 | {seed ? ( | 209 | {seed ? ( |
| 185 | <PullRequestDiffStat | 210 | <div className="flex min-h-7 min-w-0 items-center sm:min-h-6"> |
| 186 | additions={seed.additions ?? 0} | 211 | <h1 className="truncate text-base font-semibold leading-snug">{seed.title}</h1> |
| 187 | deletions={seed.deletions ?? 0} | 212 | </div> |
| 188 | className="font-mono text-xs" | | |
| 189 | /> | | |
| 190 | ) : ( | 213 | ) : ( |
| 191 | <GhostBar className="w-20" /> | 214 | <div className="flex min-h-7 min-w-0 items-center sm:min-h-6"> |
| | 215 | <GhostBar className="h-5 w-4/5 max-w-md" /> |
| | 216 | </div> |
| 192 | )} | 217 | )} |
| | 218 | <div className="mt-2 flex min-h-5 min-w-0 items-center gap-2 text-xs text-muted-foreground"> |
| | 219 | {seed ? ( |
| | 220 | <PullRequestMetaLine className="min-w-0 whitespace-nowrap"> |
| | 221 | <PullRequestActorLabel |
| | 222 | actor={seed.author ?? null} |
| | 223 | className="font-medium" |
| | 224 | tooltip={false} |
| | 225 | /> |
| | 226 | <span>updated {formatRelativeTimeLabel(seed.updatedAt)}</span> |
| | 227 | </PullRequestMetaLine> |
| | 228 | ) : ( |
| | 229 | <PullRequestMetaLine className="min-w-0 whitespace-nowrap"> |
| | 230 | <span className="flex items-center gap-1.5"> |
| | 231 | <GhostBar className="size-4 rounded-full" /> |
| | 232 | <GhostBar className="h-3 w-14" /> |
| | 233 | </span> |
| | 234 | <GhostBar className="h-3 w-16" /> |
| | 235 | </PullRequestMetaLine> |
| | 236 | )} |
| | 237 | {checkout ? ( |
| | 238 | <PullRequestCopyableCode |
| | 239 | key={checkout} |
| | 240 | value={checkout} |
| | 241 | target="pull request checkout command" |
| | 242 | copyLabel="Copy checkout command" |
| | 243 | copiedLabel="Checkout command copied" |
| | 244 | className="ml-auto font-mono" |
| | 245 | tooltipSide="bottom" |
| | 246 | {...(onCheckoutError ? { onError: onCheckoutError } : {})} |
| | 247 | /> |
| | 248 | ) : null} |
| | 249 | </div> |
| | 250 | |
| | 251 | <div className="mt-4 flex min-h-5 min-w-0 items-center gap-2 text-xs text-muted-foreground"> |
| | 252 | <span className="flex min-w-0 flex-1 items-center gap-1.5 font-mono text-xs text-muted-foreground/70"> |
| | 253 | {seed ? ( |
| | 254 | <span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1"> |
| | 255 | <code className="min-w-0 truncate">{seed.baseBranch}</code> |
| | 256 | </span> |
| | 257 | ) : ( |
| | 258 | <span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1"> |
| | 259 | <GhostBar className="h-3 w-12" /> |
| | 260 | </span> |
| | 261 | )} |
| | 262 | <ArrowLeftIcon |
| | 263 | aria-label="receives changes from" |
| | 264 | className="size-3.5 shrink-0 opacity-60" |
| | 265 | /> |
| | 266 | {seed ? ( |
| | 267 | <PullRequestCopyableCode |
| | 268 | key={seed.headBranch} |
| | 269 | value={seed.headBranch} |
| | 270 | target="branch name" |
| | 271 | copyLabel="Copy pull request branch" |
| | 272 | copiedLabel="Branch name copied" |
| | 273 | className="min-w-0 font-mono" |
| | 274 | /> |
| | 275 | ) : ( |
| | 276 | <GhostBar className="h-3 w-32 flex-1" /> |
| | 277 | )} |
| | 278 | </span> |
| | 279 | <span className="ml-auto inline-flex shrink-0 items-center justify-end gap-2"> |
| | 280 | <span className="inline-flex min-w-16 items-center justify-end gap-1.5 tabular-nums"> |
| | 281 | <FileDiffIcon aria-hidden className="size-3.5" /> |
| | 282 | {changedFiles === null ? ( |
| | 283 | <GhostBar className="h-3 w-10" /> |
| | 284 | ) : ( |
| | 285 | `${changedFiles.toLocaleString()} ${changedFiles === 1 ? "file" : "files"}` |
| | 286 | )} |
| | 287 | </span> |
| | 288 | {seed ? ( |
| | 289 | <PullRequestDiffStat |
| | 290 | additions={seed.additions ?? 0} |
| | 291 | deletions={seed.deletions ?? 0} |
| | 292 | className="shrink-0 font-mono text-xs" |
| | 293 | /> |
| | 294 | ) : ( |
| | 295 | <GhostBar className="h-3 w-20" /> |
| | 296 | )} |
| | 297 | </span> |
| | 298 | </div> |
| 193 | </div> | 299 | </div> |
| 194 | </div> | 300 | </div> |
| 195 | </div> | 301 | </div> |
| 196 | | 302 | |
| 197 | <div className="flex min-h-10 items-center justify-between gap-3 border-t border-border/60 px-4 py-2"> | 303 | <nav |
| 198 | <div className="flex items-center gap-1 p-0.5"> | 304 | className="col-span-2 flex min-w-0 flex-wrap items-center gap-2 border-t border-border/60 px-4 py-2" |
| 199 | <GhostBar className="h-6 w-16 rounded-md" /> | 305 | aria-label="Pull request tabs" |
| 200 | <GhostBar className="h-6 w-16 rounded-md" /> | 306 | inert |
| 201 | <GhostBar className="h-6 w-12 rounded-md" /> | 307 | > |
| 202 | </div> | 308 | <ToggleGroup |
| | 309 | className="shrink-0" |
| | 310 | size="segmented" |
| | 311 | variant="segmented" |
| | 312 | value={[selectedTab]} |
| | 313 | > |
| | 314 | {tabs.map((tab) => ( |
| | 315 | <Toggle key={tab.value} value={tab.value} tabIndex={-1}> |
| | 316 | {tab.label} |
| | 317 | </Toggle> |
| | 318 | ))} |
| | 319 | </ToggleGroup> |
| 203 | {checksPresentation ? ( | 320 | {checksPresentation ? ( |
| 204 | <span | 321 | <span |
| 205 | className={cn( | 322 | className={cn( |
| 206 | "inline-flex items-center gap-1.5 text-xs", | 323 | "ml-auto inline-flex items-center gap-1.5 text-xs", |
| 207 | checksPresentation.toneClassName, | 324 | checksPresentation.toneClassName, |
| 208 | )} | 325 | )} |
| 209 | > | 326 | > |
| ... | @@ -211,74 +328,85 @@ export function PullRequestDetailGhost({ | ... | @@ -211,74 +328,85 @@ export function PullRequestDetailGhost({ |
| 211 | {checksPresentation.label} | 328 | {checksPresentation.label} |
| 212 | </span> | 329 | </span> |
| 213 | ) : ( | 330 | ) : ( |
| 214 | <GhostBar className="w-20" /> | 331 | <GhostBar className="ml-auto h-3 w-32" /> |
| 215 | )} | 332 | )} |
| 216 | </div> | 333 | </nav> |
| 217 | </div> | 334 | </div> |
| 218 | | 335 | |
| 219 | <div className="min-h-0 flex-1 overflow-hidden"> | 336 | <div className="min-h-0 flex-1 overflow-hidden"> |
| 220 | <section className="px-4 py-3"> | 337 | <section className="px-4 pt-2.5 pb-1"> |
| 221 | <div className="grid min-h-8 grid-cols-[6rem_minmax(0,1fr)] items-center gap-2"> | 338 | <div className="space-y-2"> |
| 222 | <div className="flex items-center gap-1.5"> | 339 | <div className="grid min-h-7 min-w-0 grid-cols-[6rem_minmax(0,1fr)] items-center gap-2 text-xs sm:min-h-6"> |
| 223 | <GhostBar className="size-3.5 rounded-full" /> | 340 | <span className="flex items-center gap-1.5 text-muted-foreground"> |
| 224 | <GhostBar className="w-14" /> | 341 | <UsersIcon aria-hidden className="size-3.5" /> |
| 225 | </div> | 342 | Reviewers |
| 226 | <div className="flex items-center gap-1"> | 343 | </span> |
| 227 | <GhostBar className="size-4 rounded-full" /> | 344 | <span className="flex min-w-0 items-center gap-1.5 text-foreground"> |
| 228 | <GhostBar className="size-4 rounded-full" /> | 345 | <GhostBar className="h-3 w-10" /> |
| 229 | <GhostBar className="ml-1 size-5 rounded-md" /> | 346 | <Button size="icon-xs" variant="ghost" disabled aria-label="Reviewers loading"> |
| 230 | </div> | 347 | <UserPlusIcon aria-hidden className="size-3.5" /> |
| 231 | </div> | 348 | </Button> |
| 232 | <div className="grid min-h-8 grid-cols-[6rem_minmax(0,1fr)] items-center gap-2"> | 349 | </span> |
| 233 | <div className="flex items-center gap-1.5"> | | |
| 234 | <GhostBar className="size-3.5 rounded-full" /> | | |
| 235 | <GhostBar className="w-10" /> | | |
| 236 | </div> | | |
| 237 | <div className="flex items-center gap-1"> | | |
| 238 | {seed?.labels ? ( | | |
| 239 | seed.labels.slice(0, 3).map((label) => { | | |
| 240 | const color = pullRequestLabelColor(label.color); | | |
| 241 | return ( | | |
| 242 | <span | | |
| 243 | key={label.name} | | |
| 244 | className="inline-flex h-5 max-w-32 items-center gap-1 truncate rounded-full border border-border/70 bg-muted/40 px-2 text-[10px] text-muted-foreground" | | |
| 245 | > | | |
| 246 | <span | | |
| 247 | aria-hidden | | |
| 248 | className="size-2 shrink-0 rounded-full bg-muted-foreground" | | |
| 249 | {...(color ? { style: { backgroundColor: color } } : {})} | | |
| 250 | /> | | |
| 251 | <span className="truncate">{label.name}</span> | | |
| 252 | </span> | | |
| 253 | ); | | |
| 254 | }) | | |
| 255 | ) : ( | | |
| 256 | <> | | |
| 257 | <GhostBar className="h-5 w-24 rounded-full" /> | | |
| 258 | <GhostBar className="h-5 w-20 rounded-full" /> | | |
| 259 | </> | | |
| 260 | )} | | |
| 261 | </div> | 350 | </div> |
| 262 | </div> | 351 | <div className="grid min-h-7 min-w-0 grid-cols-[6rem_minmax(0,1fr)] items-center gap-2 text-xs sm:min-h-6"> |
| 263 | <div className="grid min-h-8 grid-cols-[6rem_minmax(0,1fr)] items-center gap-2"> | 352 | <span className="flex items-center gap-1.5 text-muted-foreground"> |
| 264 | <div className="flex items-center gap-1.5"> | 353 | <TagIcon aria-hidden className="size-3.5" /> |
| 265 | <GhostBar className="size-3.5 rounded-full" /> | 354 | Labels |
| 266 | <GhostBar className="w-14" /> | 355 | </span> |
| | 356 | <span className="flex min-w-0 flex-wrap items-center gap-1 text-foreground"> |
| | 357 | {entry?.labels ? ( |
| | 358 | entry.labels.length > 0 ? ( |
| | 359 | entry.labels.map((label) => { |
| | 360 | const color = pullRequestLabelColor(label.color); |
| | 361 | return ( |
| | 362 | <span |
| | 363 | key={label.name} |
| | 364 | className="inline-flex max-w-48 items-center gap-1.5 truncate rounded-full bg-muted/40 py-0.5 pl-1.5 pr-2 text-xs" |
| | 365 | > |
| | 366 | <span |
| | 367 | aria-hidden |
| | 368 | className="size-2 shrink-0 rounded-full bg-muted-foreground" |
| | 369 | {...(color ? { style: { backgroundColor: color } } : {})} |
| | 370 | /> |
| | 371 | <span className="truncate">{label.name}</span> |
| | 372 | </span> |
| | 373 | ); |
| | 374 | }) |
| | 375 | ) : ( |
| | 376 | <span className="text-muted-foreground">None</span> |
| | 377 | ) |
| | 378 | ) : ( |
| | 379 | <> |
| | 380 | <GhostBar className="h-4 w-24 rounded-full" /> |
| | 381 | <GhostBar className="h-4 w-20 rounded-full" /> |
| | 382 | </> |
| | 383 | )} |
| | 384 | <Button size="icon-xs" variant="ghost" disabled aria-label="Labels loading"> |
| | 385 | <TagIcon aria-hidden className="size-3.5" /> |
| | 386 | </Button> |
| | 387 | </span> |
| 267 | </div> | 388 | </div> |
| 268 | <GhostBar className="w-20" /> | | |
| 269 | </div> | 389 | </div> |
| 270 | </section> | 390 | </section> |
| 271 | | 391 | |
| 272 | <section className="border-t border-border/60"> | 392 | <section> |
| 273 | <div className="flex min-h-11 items-center gap-1.5 px-4 py-3"> | 393 | <div className="sticky top-0 z-10 flex w-full items-center bg-background pr-4"> |
| 274 | <GhostBar className="h-4 w-24" /> | 394 | <div className="flex min-w-0 flex-1 items-center gap-1.5 px-4 py-3 text-left text-xs font-medium text-muted-foreground"> |
| 275 | <GhostBar className="size-3.5 rounded-full" /> | 395 | <span>Description</span> |
| | 396 | <ChevronRightIcon |
| | 397 | aria-hidden |
| | 398 | className="size-3.5 rotate-90 text-muted-foreground/60" |
| | 399 | /> |
| | 400 | </div> |
| 276 | </div> | 401 | </div> |
| 277 | <div className="space-y-2 px-4 pb-4"> | 402 | <div className="space-y-3 px-4 pb-4"> |
| 278 | <GhostBar className="w-full" /> | 403 | <GhostBar className="h-5 w-40" /> |
| 279 | <GhostBar className="w-11/12" /> | 404 | <div className="space-y-2"> |
| 280 | <GhostBar className="w-4/5" /> | 405 | <GhostBar className="h-4 w-full" /> |
| 281 | <GhostBar className="w-2/3" /> | 406 | <GhostBar className="h-4 w-11/12" /> |
| | 407 | <GhostBar className="h-4 w-4/5" /> |
| | 408 | <GhostBar className="h-4 w-2/3" /> |
| | 409 | </div> |
| 282 | </div> | 410 | </div> |
| 283 | </section> | 411 | </section> |
| 284 | </div> | 412 | </div> |