-
Notifications
You must be signed in to change notification settings - Fork 3
feat: chat UI components from dpcode #64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| // FILE: ActivePlanCard.tsx | ||
| // Purpose: Renders the skinny inline checklist for active turn plans above the composer. | ||
| // Layer: Chat UI component | ||
| // Depends on: session-logic active plan state and shared button/icon primitives | ||
|
|
||
| import { memo } from "react"; | ||
|
|
||
| import type { ActivePlanState } from "../../session-logic"; | ||
| import { BotIcon, CheckIcon, ChevronRightIcon, ListTodoIcon, LoaderIcon } from "lucide-react"; | ||
| import { cn } from "~/lib/utils"; | ||
| import { Button } from "../ui/button"; | ||
|
|
||
| interface ActivePlanCardProps { | ||
| activePlan: ActivePlanState; | ||
| backgroundTaskCount?: number; | ||
| onOpenSidebar: () => void; | ||
| } | ||
|
|
||
| function stepStatusIcon(status: ActivePlanState["steps"][number]["status"]) { | ||
| if (status === "completed") { | ||
| return <CheckIcon className="size-3.5" />; | ||
| } | ||
| if (status === "inProgress") { | ||
| return <LoaderIcon className="size-3.5 animate-spin" />; | ||
| } | ||
| return <span className="block size-2 rounded-full border border-current/65" />; | ||
| } | ||
|
|
||
| export const ActivePlanCard = memo(function ActivePlanCard({ | ||
| activePlan, | ||
| backgroundTaskCount = 0, | ||
| onOpenSidebar, | ||
| }: ActivePlanCardProps) { | ||
| const totalCount = activePlan.steps.length; | ||
| const completedCount = activePlan.steps.filter((step) => step.status === "completed").length; | ||
| const stepOccurrenceCount = new Map<string, number>(); | ||
|
|
||
| return ( | ||
| <div className="mx-auto mb-3 w-full max-w-3xl"> | ||
| <div className="overflow-hidden rounded-[24px] border border-border/70 bg-card/80 shadow-sm"> | ||
| <div className="flex items-center justify-between gap-3 px-4 py-3"> | ||
| <div className="flex min-w-0 items-center gap-2 text-[13px] text-muted-foreground/80"> | ||
| <ListTodoIcon className="size-4 shrink-0" /> | ||
| <span className="truncate"> | ||
| {completedCount} out of {totalCount} tasks completed | ||
| </span> | ||
| </div> | ||
| <Button | ||
| type="button" | ||
| variant="ghost" | ||
| size="icon-xs" | ||
| className="shrink-0 rounded-full text-muted-foreground/65 hover:text-foreground/80" | ||
| onClick={onOpenSidebar} | ||
| aria-label="Open plan sidebar" | ||
| title="Open plan sidebar" | ||
| > | ||
| <ChevronRightIcon className="size-3.5" /> | ||
| </Button> | ||
| </div> | ||
|
|
||
| <ol className="space-y-0.5 px-4 pb-3"> | ||
| {activePlan.steps.map((step, index) => { | ||
| const occurrence = (stepOccurrenceCount.get(step.step) ?? 0) + 1; | ||
| stepOccurrenceCount.set(step.step, occurrence); | ||
|
|
||
| return ( | ||
| <li key={`${step.step}:${occurrence}`} className="flex items-start gap-3 py-1.5"> | ||
| <div | ||
| className={cn( | ||
| "mt-0.5 flex min-w-0 shrink-0 items-center gap-2 text-[13px]", | ||
| step.status === "completed" | ||
| ? "text-muted-foreground/45" | ||
| : step.status === "inProgress" | ||
| ? "text-foreground/80" | ||
| : "text-muted-foreground/60", | ||
| )} | ||
| > | ||
| <span className="flex size-4 items-center justify-center"> | ||
| {stepStatusIcon(step.status)} | ||
| </span> | ||
| <span className="tabular-nums">{index + 1}.</span> | ||
| </div> | ||
| <p | ||
| className={cn( | ||
| "min-w-0 flex-1 text-[15px] leading-6 text-foreground/88", | ||
| step.status === "completed" && "text-muted-foreground/50 line-through", | ||
| )} | ||
| > | ||
| {step.step} | ||
| </p> | ||
| </li> | ||
| ); | ||
| })} | ||
| </ol> | ||
|
|
||
| {backgroundTaskCount > 0 ? ( | ||
| <div className="flex items-center justify-between gap-3 border-t border-border/60 px-4 py-2.5 text-[12px] text-muted-foreground/72"> | ||
| <div className="flex min-w-0 items-center gap-2"> | ||
| <BotIcon className="size-3.5 shrink-0" /> | ||
| <span className="truncate"> | ||
| {backgroundTaskCount} background agent{backgroundTaskCount === 1 ? "" : "s"} | ||
| </span> | ||
| </div> | ||
| <Button | ||
| type="button" | ||
| variant="ghost" | ||
| size="icon-xs" | ||
| className="shrink-0 rounded-full text-muted-foreground/65 hover:text-foreground/80" | ||
| onClick={onOpenSidebar} | ||
| aria-label="Open plan sidebar" | ||
| title="Open plan sidebar" | ||
| > | ||
| <ChevronRightIcon className="size-3.5" /> | ||
| </Button> | ||
| </div> | ||
| ) : null} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }); | ||
|
|
||
| export type { ActivePlanCardProps }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { memo } from "react"; | ||
|
|
||
| export const ChatEmptyStateHero = memo(function ChatEmptyStateHero({ | ||
| projectName, | ||
| }: { | ||
| projectName: string | undefined; | ||
| }) { | ||
| return ( | ||
| <div className="flex flex-col items-center gap-4 select-none"> | ||
| <img | ||
| alt="DP Code logo" | ||
| className="size-14 rounded-lg object-contain" | ||
| draggable={false} | ||
| height={112} | ||
| src="/dpcode-hero.png" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify the referenced hero image exists in the repository.
fd -i 'dpcode-hero\.png$'
rg -n 'src="/dpcode-hero\.png"' --type=tsxRepository: aaditagrawal/t3code Length of output: 90 🏁 Script executed: # Search for the image file more broadly
find . -name "*dpcode-hero*" -o -name "*dpcode*" 2>/dev/null
# Check directory structure for public/static
ls -la public/ 2>/dev/null || echo "public dir not found"
ls -la public/static/ 2>/dev/null || echo "public/static dir not found"
# Search for all references to dpcode-hero in the codebase (all file types)
rg 'dpcode-hero' --type-list 2>/dev/null || rg 'dpcode-hero'
# List files in ChatEmptyStateHero.tsx location
ls -la apps/web/src/components/chat/ 2>/dev/null || echo "chat dir not found"Repository: aaditagrawal/t3code Length of output: 4490 🏁 Script executed: # Read the ChatEmptyStateHero.tsx file
cat -n apps/web/src/components/chat/ChatEmptyStateHero.tsx
# Search for common build/config files
find . -maxdepth 2 -name "next.config.*" -o -name "vite.config.*" -o -name "tsconfig.json" -o -name "package.json" | head -20
# Check if there's a public folder elsewhere in the monorepo
find . -type d -name "public" 2>/dev/null
# Look for assets/images directory
find . -type d \( -name "assets" -o -name "images" -o -name "static" \) 2>/dev/nullRepository: aaditagrawal/t3code Length of output: 1075 🏁 Script executed: # List contents of apps/web/public
ls -la apps/web/public 2>/dev/null || echo "apps/web/public not accessible"
# Search specifically for the image file
find apps/web/public -name "*dpcode*" 2>/dev/null || echo "No dpcode files found"
# List all files in assets directory
ls -la assets/ 2>/dev/null || echo "assets dir not found"Repository: aaditagrawal/t3code Length of output: 742 🏁 Script executed: # Check what's in assets/dev and assets/prod
ls -la assets/dev/ 2>/dev/null
ls -la assets/prod/ 2>/dev/null
# Double-check for the image anywhere in the repo
find . -name "dpcode-hero.png" 2>/dev/null || echo "File not found anywhere"Repository: aaditagrawal/t3code Length of output: 1613 🏁 Script executed: # Check git status to see if file is untracked or gitignored
cd apps/web && git status --short 2>/dev/null | grep -i dpcode || echo "No dpcode in git status"
cd ../..
# Check .gitignore to see if images might be excluded
cat apps/web/.gitignore 2>/dev/null | grep -i "png\|image\|asset" || echo "No image patterns in gitignore"
# Check if there's a next.config.js that might explain the image serving
cat apps/web/next.config.js 2>/dev/null | head -50Repository: aaditagrawal/t3code Length of output: 117 Add missing hero image to public assets. The file 🤖 Prompt for AI Agents |
||
| width={112} | ||
| /> | ||
|
|
||
| <div className="flex flex-col items-center gap-0.5"> | ||
| <h1 className="text-2xl font-semibold tracking-tight text-foreground/90">Let's build</h1> | ||
| {projectName && <span className="text-lg text-muted-foreground/40">{projectName}</span>} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { renderToStaticMarkup } from "react-dom/server"; | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { ComposerImageAttachmentChip } from "./ComposerImageAttachmentChip"; | ||
|
|
||
| describe("ComposerImageAttachmentChip", () => { | ||
| it("renders a filename-first pill instead of the old thumbnail tile", () => { | ||
| const markup = renderToStaticMarkup( | ||
| <ComposerImageAttachmentChip | ||
| image={{ | ||
| id: "image-1", | ||
| type: "image", | ||
| name: "CleanShot 2026-04-11 at 20.00.33@2x.png", | ||
| mimeType: "image/png", | ||
| sizeBytes: 1024, | ||
| previewUrl: "blob:image-1", | ||
| file: new File(["image"], "CleanShot 2026-04-11 at 20.00.33@2x.png", { | ||
| type: "image/png", | ||
| }), | ||
| }} | ||
| images={[ | ||
| { | ||
| id: "image-1", | ||
| type: "image", | ||
| name: "CleanShot 2026-04-11 at 20.00.33@2x.png", | ||
| mimeType: "image/png", | ||
| sizeBytes: 1024, | ||
| previewUrl: "blob:image-1", | ||
| file: new File(["image"], "CleanShot 2026-04-11 at 20.00.33@2x.png", { | ||
| type: "image/png", | ||
| }), | ||
| }, | ||
| ]} | ||
| nonPersisted={false} | ||
| onExpandImage={() => {}} | ||
| onRemoveImage={() => {}} | ||
| />, | ||
| ); | ||
|
|
||
| expect(markup).toContain("CleanShot 2026-04-11 at 20.00.33@2x.png"); | ||
| expect(markup).toContain("rounded-full"); | ||
| expect(markup).toContain("Preview CleanShot 2026-04-11 at 20.00.33@2x.png"); | ||
| expect(markup).toContain("Remove CleanShot 2026-04-11 at 20.00.33@2x.png"); | ||
| expect(markup).not.toContain("absolute right-1 top-1"); | ||
| expect(markup).not.toContain("h-14 w-14"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,87 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| // FILE: ComposerImageAttachmentChip.tsx | ||||||||||||||||||||||||||||||||||||||||||||||
| // Purpose: Renders filename-first composer image attachments as compact pills with preview/remove actions. | ||||||||||||||||||||||||||||||||||||||||||||||
| // Layer: Chat composer presentation | ||||||||||||||||||||||||||||||||||||||||||||||
| // Depends on: composer draft image metadata, shared chip styles, and expanded image preview helpers. | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import { memo } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { type ComposerImageAttachment } from "../../composerDraftStore"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { CircleAlertIcon, XIcon } from "lucide-react"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { cn } from "~/lib/utils"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { COMPOSER_INLINE_CHIP_DISMISS_BUTTON_CLASS_NAME } from "../composerInlineChip"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { buildExpandedImagePreview, type ExpandedImagePreview } from "./ExpandedImagePreview"; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| interface ComposerImageAttachmentChipProps { | ||||||||||||||||||||||||||||||||||||||||||||||
| image: ComposerImageAttachment; | ||||||||||||||||||||||||||||||||||||||||||||||
| images: readonly ComposerImageAttachment[]; | ||||||||||||||||||||||||||||||||||||||||||||||
| nonPersisted: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||
| onExpandImage: (preview: ExpandedImagePreview) => void; | ||||||||||||||||||||||||||||||||||||||||||||||
| onRemoveImage: (imageId: string) => void; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export const ComposerImageAttachmentChip = memo(function ComposerImageAttachmentChip({ | ||||||||||||||||||||||||||||||||||||||||||||||
| image, | ||||||||||||||||||||||||||||||||||||||||||||||
| images, | ||||||||||||||||||||||||||||||||||||||||||||||
| nonPersisted, | ||||||||||||||||||||||||||||||||||||||||||||||
| onExpandImage, | ||||||||||||||||||||||||||||||||||||||||||||||
| onRemoveImage, | ||||||||||||||||||||||||||||||||||||||||||||||
| }: ComposerImageAttachmentChipProps) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <div className="inline-flex min-w-0 max-w-full items-center gap-0.5 rounded-full border border-border/60 bg-background/92 p-0.5 shadow-[0_1px_0_rgba(255,255,255,0.14)_inset]"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||
| className="flex min-w-0 max-w-[232px] items-center gap-1.5 rounded-full py-0 pl-0 pr-0.5 text-left transition-colors hover:bg-accent/28 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" | ||||||||||||||||||||||||||||||||||||||||||||||
| aria-label={`Preview ${image.name}`} | ||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => { | ||||||||||||||||||||||||||||||||||||||||||||||
| const preview = buildExpandedImagePreview(images, image.id); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!preview) return; | ||||||||||||||||||||||||||||||||||||||||||||||
| onExpandImage(preview); | ||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| <span className="flex size-6 shrink-0 items-center justify-center overflow-hidden rounded-full border border-border/55 bg-muted/35"> | ||||||||||||||||||||||||||||||||||||||||||||||
| {image.previewUrl ? ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <img src={image.previewUrl} alt={image.name} className="size-full object-cover" /> | ||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <span className="px-1 text-[9px] font-medium uppercase tracking-[0.08em] text-muted-foreground/70"> | ||||||||||||||||||||||||||||||||||||||||||||||
| IMG | ||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||
| <span className="min-w-0 truncate text-[12px] font-medium text-foreground/84"> | ||||||||||||||||||||||||||||||||||||||||||||||
| {image.name} | ||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| {nonPersisted && ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <Tooltip> | ||||||||||||||||||||||||||||||||||||||||||||||
| <TooltipTrigger | ||||||||||||||||||||||||||||||||||||||||||||||
| render={ | ||||||||||||||||||||||||||||||||||||||||||||||
| <span | ||||||||||||||||||||||||||||||||||||||||||||||
| role="img" | ||||||||||||||||||||||||||||||||||||||||||||||
| aria-label="Draft attachment may not persist" | ||||||||||||||||||||||||||||||||||||||||||||||
| className="inline-flex size-5 shrink-0 items-center justify-center rounded-full text-amber-600" | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| <CircleAlertIcon className="size-3" /> | ||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+57
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Verify how TooltipTrigger handles `render` children (focus/keyboard semantics).
TOOLTIP_FILE="$(fd -i 'tooltip.tsx' apps/web/src/components | head -n 1)"
echo "Inspecting: ${TOOLTIP_FILE}"
sed -n '1,260p' "${TOOLTIP_FILE}"
rg -n -C3 'TooltipTrigger|render|asChild|tabIndex|onKeyDown|onPointerDown' "${TOOLTIP_FILE}"Repository: aaditagrawal/t3code Length of output: 3389 🏁 Script executed: #!/bin/bash
set -euo pipefail
# 1. Check the ComposerImageAttachmentChip.tsx file to see the full context
echo "=== ComposerImageAttachmentChip.tsx (lines 50-75) ==="
sed -n '50,75p' apps/web/src/components/chat/ComposerImageAttachmentChip.tsx
echo ""
echo "=== Searching for other TooltipTrigger usages with render prop ==="
rg -B2 -A5 'TooltipTrigger.*render=' apps/web/src/components/ | head -50Repository: aaditagrawal/t3code Length of output: 2890 🏁 Script executed: #!/bin/bash
# Check `@base-ui/react/tooltip` library for render prop support and keyboard handling
# Check package.json for version
echo "=== Checking `@base-ui/react/tooltip` version ==="
rg '"@base-ui/react/tooltip"' package.json
echo ""
echo "=== Checking node_modules for `@base-ui/react/tooltip` docs ==="
# Look for any docs or JSDoc comments
if [ -d "node_modules/@base-ui" ]; then
find node_modules/@base-ui/react -name "*.d.ts" -o -name "*.ts" | grep -i tooltip | head -5
else
echo "node_modules not present in sandbox"
fiRepository: aaditagrawal/t3code Length of output: 209 🌐 Web query:
💡 Result: In Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
# Check what elements are actually rendered in other TooltipTrigger render props
echo "=== sidebar.tsx render prop context ==="
rg -B5 'TooltipTrigger render=' apps/web/src/components/ui/sidebar.tsx | head -40
echo ""
echo "=== ConnectionsSettings.tsx render prop context ==="
rg -B8 'TooltipTrigger render=' apps/web/src/components/settings/ConnectionsSettings.tsx | head -40
echo ""
echo "=== ComposerPromptEditor.tsx render prop context ==="
rg -B8 'TooltipTrigger render=' apps/web/src/components/ComposerPromptEditor.tsx | head -40Repository: aaditagrawal/t3code Length of output: 690 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Check the type signature of TooltipPrimitive.Trigger.Props to understand requirements
# Also check if there are existing patterns for keyboard-accessible tooltip triggers
echo "=== Searching for other tooltip trigger patterns (non-render) ==="
rg -B2 -A2 '<TooltipTrigger' apps/web/src/components/ | grep -v render | head -40Repository: aaditagrawal/t3code Length of output: 2821 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Check what 'chip' is in ComposerPromptEditor.tsx
echo "=== ComposerPromptEditor.tsx - chip definition ==="
rg -B15 'return.*chip;' apps/web/src/components/ComposerPromptEditor.tsx | grep -A15 'const chip'
echo ""
echo "=== Verify the dot element in ConnectionsSettings.tsx ==="
rg -B20 'return.*<Tooltip>' apps/web/src/components/settings/ConnectionsSettings.tsx | tail -30Repository: aaditagrawal/t3code Length of output: 113 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Search for all span-based TooltipTrigger render usages to see if this is a pattern issue
echo "=== All TooltipTrigger render usages with span elements ==="
rg -A3 'render=\{' apps/web/src/components/ | grep -B3 '<span' | head -30Repository: aaditagrawal/t3code Length of output: 1788 Make the tooltip trigger keyboard-accessible. The Suggested adjustment- <TooltipTrigger
- render={
- <span
- role="img"
- aria-label="Draft attachment may not persist"
- className="inline-flex size-5 shrink-0 items-center justify-center rounded-full text-amber-600"
- >
- <CircleAlertIcon className="size-3" />
- </span>
- }
- />
+ <TooltipTrigger
+ render={
+ <button
+ type="button"
+ aria-label="Draft attachment may not persist"
+ className="inline-flex size-5 shrink-0 items-center justify-center rounded-full text-amber-600 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
+ >
+ <CircleAlertIcon className="size-3" />
+ </button>
+ }
+ />📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| <TooltipPopup side="top" className="max-w-64 whitespace-normal leading-tight"> | ||||||||||||||||||||||||||||||||||||||||||||||
| Draft attachment could not be saved locally and may be lost on navigation. | ||||||||||||||||||||||||||||||||||||||||||||||
| </TooltipPopup> | ||||||||||||||||||||||||||||||||||||||||||||||
| </Tooltip> | ||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||||||||||||||||||
| COMPOSER_INLINE_CHIP_DISMISS_BUTTON_CLASS_NAME, | ||||||||||||||||||||||||||||||||||||||||||||||
| "size-5 rounded-full text-muted-foreground/62 hover:bg-accent/36 hover:text-foreground", | ||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => onRemoveImage(image.id)} | ||||||||||||||||||||||||||||||||||||||||||||||
| aria-label={`Remove ${image.name}`} | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| <XIcon className="size-3" /> | ||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Expose step status text for screen readers.
Step state is currently visual-only (icon/color/strikethrough), so assistive tech users won’t reliably get “completed/in progress/pending” per item.
♿ Suggested accessibility fix
{activePlan.steps.map((step, index) => { + const statusLabel = + step.status === "completed" + ? "Completed" + : step.status === "inProgress" + ? "In progress" + : "Pending"; const occurrence = (stepOccurrenceCount.get(step.step) ?? 0) + 1; stepOccurrenceCount.set(step.step, occurrence); return ( <li key={`${step.step}:${occurrence}`} className="flex items-start gap-3 py-1.5"> @@ <p className={cn( "min-w-0 flex-1 text-[15px] leading-6 text-foreground/88", step.status === "completed" && "text-muted-foreground/50 line-through", )} > + <span className="sr-only">{statusLabel}: </span> {step.step} </p> </li> ); })}🤖 Prompt for AI Agents