Skip to content

Comments

feat: Add Copy as Customer Quote button#5

Open
nikman21 wants to merge 4 commits intomainfrom
feature/copy-as-quote
Open

feat: Add Copy as Customer Quote button#5
nikman21 wants to merge 4 commits intomainfrom
feature/copy-as-quote

Conversation

@nikman21
Copy link
Contributor

@nikman21 nikman21 commented Feb 11, 2026

What I Built

Added a Copy as Customer Quote button to the FeedbackList component that formats feedback as a ready-to-use customer quote.

Changes

  • Added function that formats feedback as markdown quote
  • Quote format: + attribution line
  • New "Copy Quote" button with Quote icon

Why It Matters

Saves PMs time when they need to share customer quotes in docs, Slack, or emails.

How to Test

  1. Go to a project with feedback
  2. Click "Copy Quote" button
  3. Verify copied text has proper markdown quote format

Summary by CodeRabbit

Release Notes

  • New Features

    • Decision export functionality supporting multiple formats: Cursor, Linear, Jira, Slack, Notion, Markdown, and JSON
    • AI-driven analysis dashboard with interactive priority matrix and opportunity insights
    • Decision creation and detail view pages
    • Ability to copy customer feedback as formatted quotes
  • Improvements

    • Enhanced priority matrix with improved visualization of impact and effort metrics

## What Was Built

A comprehensive Decision Export System for PM Analyzer that enables
product managers to export decisions in multiple formats for different
downstream tools.

### Components Created

1. **DecisionExportDialog.tsx** - A reusable export modal component
   - 7 export formats: Cursor/Claude Code, Linear, Jira, Slack, Notion, Markdown, JSON
   - Live preview of export format
   - Copy to clipboard functionality
   - Download as file

2. **Export API Route** (`/api/projects/[id]/decisions/[decisionId]/export`)
   - GET endpoint for programmatic export
   - Supports all 7 formats
   - Returns appropriate Content-Type headers

3. **Decision Detail Page** (`/projects/[id]/decisions/[decisionId]`)
   - Full decision view with all metadata
   - Embedded Export dialog
   - Confidence score badges
   - Linked feedback display

4. **Decisions List Page** (`/projects/[id]/decisions`)
   - All decisions in one place
   - Export button on each decision card
   - Quick filtering by status

### Export Formats

| Format | Use Case |
|--------|----------|
| Cursor/Claude Code | AI pair programming handoff with structured prompts |
| Linear | Issue tracker format for sprint planning |
| Jira | Enterprise issue tracking |
| Slack | Stakeholder communication with emoji confidence |
| Notion | Documentation and wiki export |
| Markdown | General documentation |
| JSON | Programmatic access and integration |

### Why This Matters

Product managers need to hand off decisions to:
- Engineering teams (Linear/Jira)
- AI coding assistants (Cursor)
- Stakeholders (Slack/Notion)
- Documentation systems (Markdown/JSON)

This feature bridges the gap between PM Analyzer's decision-making
capabilities and the tools teams actually use to build software.

---

**Nightly Build (2026-02-07)**
- Created missing Analysis page at /projects/[id]/analysis/
- Added PriorityMatrix component to display opportunities in Eisenhower matrix format
- Fixed bug where items with 'medium' impact/effort weren't appearing in any quadrant
- Added balance indicator showing % of actionable opportunities
- Added sortable quadrants with priority scoring
- Added legend and visual improvements
- Created new decision creation page at /projects/[id]/decisions/new
- Added form with title, summary, scope, non-goals, acceptance criteria, risks, and confidence slider
- Fixed API bug: confidence field was using wrong name
- Added UI components: Slider, Alert, Textarea
- Added Create Decision buttons to analysis opportunities list
- Fixed decision API to use confidenceScore (Float) instead of confidence (String)
- Added Quote icon import from lucide-react
- Added handleCopyAsQuote function that formats feedback as a markdown quote
- Added copiedQuoteId state to track quote copy status
- Added new Copy Quote button next to Copy for Cursor button
- Quote format: > "feedback text" >\n> — {tier} customer via {source}
@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a comprehensive decision management feature, including new Next.js pages for viewing and creating decisions, an analysis dashboard with priority matrix visualization, an export system supporting multiple formats (Markdown, JSON, Jira, Slack, Linear, Notion, Cursor), and supporting UI components (Alert, Slider, Textarea). The changes also update the decision creation API to use numeric confidence scores instead of strings.

Changes

Cohort / File(s) Summary
Decision Export & API
app/api/projects/[id]/decisions/[decisionId]/export/route.ts, app/api/projects/[id]/decisions/route.ts
Added GET endpoint for exporting decisions in multiple formats (cursor, linear, jira, slack, notion, markdown, json) with validation and content-disposition headers. Updated decision creation API to use confidenceScore (numeric, default 0.5) instead of confidence string.
Decision Management Pages
app/projects/[id]/decisions/page.tsx, app/projects/[id]/decisions/[decisionId]/page.tsx, app/projects/[id]/decisions/new/page.tsx
Added decisions list view with filtering and export dialogs, decision detail page with confidence visualization and linked feedback display, and create decision form with state management and client-side validation.
Analysis Dashboard
app/projects/[id]/analysis/page.tsx
New analysis dashboard page that fetches project data, parses analysis content, organizes opportunities by impact/effort, and renders Eisenhower Priority Matrix, opportunities list, and unclustered feedback panels with create-decision CTAs.
Decision Export Component
components/DecisionExportDialog.tsx
New modal component with multiple formatters (formatAsCursor, formatAsLinear, formatAsJira, formatAsSlack, formatAsNotion, formatAsMarkdown, formatAsJson) for exporting decision data, plus format selection UI, live preview, and copy/download actions.
UI Components
components/ui/alert.tsx, components/ui/slider.tsx, components/ui/textarea.tsx
Added three new UI components: Alert (with destructive variant support), Slider (Radix UI-based range input), and Textarea (standard textarea wrapper).
Supporting Components
components/FeedbackList.tsx, components/PriorityMatrix.tsx
Added copy-quote feature to FeedbackList with clipboard integration and confirmation feedback. Significantly enhanced PriorityMatrix with config-driven quadrant metadata, intra-quadrant sorting by priority score, balance metrics, impact/effort badges per item, and visual legend.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Client
    participant Page as Export Page
    participant API as Export API
    participant DB as Database<br/>(Prisma)
    participant Formatter as Format Engine
    participant Response as Response

    User->>Page: Request decision export
    Page->>Page: Select format (JSON/Markdown/etc)
    Page->>API: GET /api/projects/{id}/decisions/{decisionId}/export?format={fmt}
    
    API->>API: Validate format against allowlist
    API->>DB: Fetch decision + project name
    DB-->>API: Return decision record
    
    API->>API: Validate decision exists & belongs to project
    API->>Formatter: formatDecision(data, format)
    
    alt Format = JSON
        Formatter-->>API: JSON string
        API->>Response: Content-Type: application/json
    else Format = Markdown/Text
        Formatter-->>API: Formatted text
        API->>Response: Content-Type: text/plain<br/>Content-Disposition: attachment
    end
    
    Response-->>User: Download/Display content
Loading
sequenceDiagram
    participant User as User/Client
    participant Form as Create Decision Form
    participant API as Decisions API
    participant DB as Database<br/>(Prisma)
    participant Router as Next Router

    User->>Form: Fill decision form fields
    User->>Form: Click "Create Decision"
    
    Form->>Form: Validate fields & normalize data
    Form->>Form: Scale confidence to 0-1 range
    Form->>Form: Parse linkedFeedbackIds from input
    
    Form->>API: POST /api/projects/{projectId}/decisions
    API->>DB: Create decision record
    DB-->>API: Return created decision
    
    API-->>Form: 200 OK + decision data
    Form->>Router: Redirect to decision detail page
    Router-->>User: Navigate to new decision view
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A clever rabbit drafted with care,
Decisions exported everywhere!
From Jira to Slack, from Notion to Markdown bright,
Analysis dances with matrices of might!
Now confidence scores bloom numeric and free,
A feature complete from A unto Z! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title 'feat: Add Copy as Customer Quote button' directly describes the main change (adding a quote copy button), which is reflected in the FeedbackList.tsx summary. However, the title does not reflect the significantly broader scope of this PR, which includes decision export functionality, analysis pages, decision detail/list pages, and multiple UI components—features that appear to be the primary focus based on code volume and effort estimates. Revise the title to reflect the actual primary changes. Consider: 'feat: Add decision export system with analysis dashboard and feedback quote button' or restructure commits to align scope with title, splitting decision features into a separate PR.
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/copy-as-quote

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5dcc40e2a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +352 to +356
const handleExport = (format: ExportFormat) => {
const content = updatePreview(format);
if (onExport) {
onExport(format, content);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Provide a default download path in export handler

The Download button currently has no effect unless onExport is passed, because handleExport only invokes the optional callback and does nothing otherwise. In this commit, both current call sites render DecisionExportDialog without onExport (app/projects/[id]/decisions/page.tsx and app/projects/[id]/decisions/[decisionId]/page.tsx), so users can click Download and get no file or network request.

Useful? React with 👍 / 👎.

risks: risks || "",
nonGoals: nonGoals || "",
confidence: confidence || "medium",
confidenceScore: confidence || 0.5,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep zero confidence values when creating decisions

Using confidence || 0.5 treats a valid 0 confidence input as falsy and silently rewrites it to 0.5. The new decision form allows selecting 0%, so low-confidence decisions will be persisted as 50%, which corrupts confidence-based ranking and downstream exports. Use a nullish check instead so only missing values get the fallback.

Useful? React with 👍 / 👎.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/api/projects/[id]/decisions/route.ts (1)

28-56: ⚠️ Potential issue | 🔴 Critical

acceptanceCriteria from the request body is never persisted.

The create decision form (decisions/new/page.tsx) sends acceptanceCriteria in the POST body, but this route handler doesn't destructure it (line 28-36) or include it in the Prisma create call (line 45-57). User input for acceptance criteria is silently discarded. The export route and detail page later expect decision.acceptanceCriteria to be populated.

Proposed fix
     const {
       title,
       summary,
       scope,
       risks,
       nonGoals,
       confidence,
+      acceptanceCriteria,
       linkedFeedbackIds,
     } = body;

     ...

     const decision = await prisma.decision.create({
       data: {
         projectId,
         title,
         summary,
         scope: scope || "",
         risks: risks || "",
         nonGoals: nonGoals || "",
+        acceptanceCriteria: acceptanceCriteria || "",
         confidenceScore: confidence ?? 0.5,
         status: "draft",
         linkedFeedbackIds: linkedFeedbackIds || [],
       },
     });
🤖 Fix all issues with AI agents
In `@app/api/projects/`[id]/decisions/[decisionId]/export/route.ts:
- Around line 86-94: The filename construction uses raw decision.title which
allows header injection; sanitize the title before building filename by
stripping or replacing characters not safe in HTTP headers (e.g., control chars
\r \n, quotes, backslashes, slashes), collapse multiple spaces, and truncate to
a reasonable length; update the code that builds filename (the filename variable
where decision.title is used) to produce a safe ASCII/URL-safe token (or use a
safe fallback like "untitled-decision" when the sanitized result is empty) and
then pass that sanitized filename into the NextResponse Content-Disposition
header to prevent injection.

In `@app/api/projects/`[id]/decisions/route.ts:
- Line 53: The confidence defaulting uses the || operator which treats 0 as
falsy; update the assignment that sets confidenceScore (where confidence is
used) to use the nullish coalescing operator (??) instead of || so only
null/undefined fall back to 0.5, i.e., change the expression that builds
confidenceScore in route.ts to use confidence ?? 0.5.

In `@app/projects/`[id]/analysis/page.tsx:
- Around line 133-178: The summary stats grid uses a fixed "grid-cols-4" which
breaks on small screens; update the container div that currently has className
"grid grid-cols-4 gap-4" to use responsive Tailwind classes (e.g. "grid
grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4") so the Card items (rendered
via Card and CardContent and using analysisContent and opportunities) flow to 1
column on mobile, 2 on small screens, and 4 on larger screens.
- Around line 92-101: The current mapping in page.tsx wrongly derives
opportunity.effort from impactScore, creating a forced correlation; instead add
an independent effortScore (or effort) field to the ClusterResult interface in
lib/clustering.ts and populate it in the clustering logic using a separate
heuristic (e.g., category, complexity signals, or a dedicated effort heuristic)
so clustering outputs an independent effortScore, then change the mapping in
app/projects/[id]/analysis/page.tsx (where OpportunityItem is constructed from
analysisContent.opportunities) to read effort/effortScore from the cluster
result rather than computing it from impactScore, ensuring the Priority Matrix
can produce high-impact/low-effort and low-impact/high-effort quadrants.

In `@app/projects/`[id]/decisions/[decisionId]/page.tsx:
- Line 62: The code uses decision.status.replace("_", " ") which only replaces
the first underscore; update both occurrences of this call (the expressions
using decision.status.replace("_", " ")) to replace all underscores, e.g.
decision.status.replaceAll("_", " ") or decision.status.replace(/_/g, " "), so
multi-underscore statuses like "in_progress_review" become "in progress review";
pick replaceAll if runtime supports it or the regex alternative for broader
compatibility.

In `@components/DecisionExportDialog.tsx`:
- Around line 362-377: The custom trigger path in DecisionExportDialog opens the
dialog without populating the preview because the onClick handler only calls
setIsOpen(true); modify the custom trigger click handler (the element rendering
when trigger is truthy) to also call updatePreview(selectedFormat) after opening
the dialog so it mirrors the default Button path (i.e., onClick should call
setIsOpen(true) and then updatePreview(selectedFormat)).

In `@components/PriorityMatrix.tsx`:
- Line 195: In the PriorityMatrix component, the JSX uses an invalid attribute
on the status indicator span (the <span> with the red dot); replace the DOM
attribute "class" with React's "className" on that element so the tailwind
classes (w-3 h-3 rounded-full bg-red-500) are applied correctly; search for the
span in PriorityMatrix.tsx (the small dot indicator) and update its attribute to
className.
- Around line 43-52: getQuadrantKey currently only branches on 'high' vs 'low'
and drops any 'medium' effort into q4; update getQuadrantKey (and reference
PriorityItem) to explicitly handle effort === 'medium' by introducing boolean
flags (e.g., lowEffort, mediumEffort, highEffort) and then change the branches
so: high impact + lowEffort => 'q1', high impact + (mediumEffort || highEffort)
=> 'q2', non-high impact + (lowEffort || mediumEffort) => 'q3', otherwise 'q4';
this ensures medium efforts are classified correctly for high/medium/low
impacts.

In `@components/ui/slider.tsx`:
- Line 23: The Thumb has conflicting Tailwind border classes (border-slate-200
vs border-slate-900 and dark:border-slate-800 vs dark:border-slate-50); update
the SliderPrimitive.Thumb class list to remove the redundant pairs so there is
only one border class per theme — keep border-slate-900 for light mode and
dark:border-slate-50 for dark mode (remove border-slate-200 and
dark:border-slate-800) in the SliderPrimitive.Thumb component.
🧹 Nitpick comments (11)
app/api/projects/[id]/decisions/[decisionId]/export/route.ts (2)

3-7: Server route imports formatters from a "use client" component file.

DecisionExportDialog.tsx is marked "use client". While this works at runtime (the imported functions don't use hooks or browser APIs), it couples a server-side API route to a client component module. Extract formatDecision, ExportFormat, DecisionExportData, and the individual formatters into a shared utility (e.g., lib/decision-export.ts) that both the API route and the dialog can import.


18-33: validFormats duplicates the ExportFormat type — they can drift.

Consider deriving validation from the source of truth (e.g., the EXPORT_FORMATS constant or the ExportFormat type) instead of maintaining a separate hardcoded list.

Proposed fix
+import {
+  formatDecision,
+  ExportFormat,
+  DecisionExportData,
+  EXPORT_FORMATS,
+} from "@/components/DecisionExportDialog";
+
 // Validate format
-    const validFormats = [
-      "cursor",
-      "linear",
-      "jira",
-      "slack",
-      "notion",
-      "markdown",
-      "json",
-    ];
+    const validFormats = EXPORT_FORMATS.map((f) => f.id);
     if (!validFormats.includes(format)) {
app/projects/[id]/decisions/new/page.tsx (2)

46-58: Field name mismatch: the form sends confidence but the export system expects confidenceScore.

The POST body uses confidence (line 53) while DecisionExportData and the Prisma schema use confidenceScore. This works today only because the API route manually maps confidenceconfidenceScore (route.ts line 53), but it's a subtle naming inconsistency that could cause confusion. Consider aligning the field name in the request body with the canonical schema name.


70-71: Use unknown instead of any for the catch clause.

catch (err: any) bypasses type safety. With unknown, you can narrow safely.

Proposed fix
-    } catch (err: any) {
-      setError(err.message || "An error occurred while creating the decision");
+    } catch (err: unknown) {
+      setError(err instanceof Error ? err.message : "An error occurred while creating the decision");
components/DecisionExportDialog.tsx (2)

379-398: Modal lacks accessibility: no focus trap, keyboard dismiss, or ARIA attributes.

The custom modal overlay is missing:

  • role="dialog" and aria-modal="true"
  • Escape key handler to close
  • Focus trap (focus remains on background elements)
  • Backdrop click to dismiss

Consider using Radix UI's Dialog primitive (already a dependency via the Slider) or at minimum add keyboard and ARIA support.


115-298: Formatter functions are co-located with a "use client" component but are pure utility functions.

These seven formatters and the formatDecision dispatcher have no React dependencies and are imported by the server-side export API route. Moving them to a shared module (e.g., lib/decision-export.ts) would cleanly separate concerns and avoid importing a client module from server code.

app/projects/[id]/decisions/[decisionId]/page.tsx (2)

1-10: Unused import: Download.

Download is imported on Line 3 but never used in this file's JSX. It is used in the sibling decisions/page.tsx, so this appears to be a copy-paste leftover.

🧹 Remove unused import
-import { ArrowLeft, Scale, Download, CheckCircle, AlertTriangle } from "lucide-react";
+import { ArrowLeft, Scale, CheckCircle, AlertTriangle } from "lucide-react";

146-161: Acceptance criteria rendering assumes non-null string and uses index keys.

If decision.acceptanceCriteria is ever null or empty, calling .split("\n") would throw. Consider a defensive fallback. Also, readOnly checkbox without a checked prop always renders unchecked — if the intent is purely decorative, defaultChecked={false} or a static icon (e.g., a CheckCircle) might convey intent more clearly.

app/projects/[id]/decisions/page.tsx (1)

54-57: Filter button is non-functional.

This button has no onClick handler and no associated filtering logic. Shipping a visible but inert control can confuse users. Consider either wiring it up or removing it until the feature is implemented.

app/projects/[id]/analysis/page.tsx (2)

75-90: Unsafe cast of analysis.content — no runtime validation.

analysis.content is a Prisma Json field cast directly to a specific shape. If the stored JSON is malformed or from an older schema version (e.g., missing opportunities or summary), this will cause a runtime crash. Consider adding a guard or using a validation library (e.g., Zod) to parse and provide a safe fallback.


268-287: Unclustered tab is a placeholder with no actual feedback data.

The tab header shows the unclustered count from analysis data, but the content is static placeholder text. If this is intentional for an upcoming iteration, consider adding a brief inline comment or disabling the tab to set expectations.

Comment on lines +86 to +94
const filename = `decision-${decision.title
.toLowerCase()
.replace(/\s+/g, "-")}.${format === "markdown" ? "md" : format}`;

return new NextResponse(content, {
headers: {
"Content-Type": contentType,
"Content-Disposition": `attachment; filename="${filename}"`,
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unsanitized decision title in Content-Disposition filename — potential header injection.

decision.title is user-supplied and only has whitespace replaced. Characters like ", \n, \r, or / in the title can break or inject into the Content-Disposition header value.

Proposed fix
-    const filename = `decision-${decision.title
-      .toLowerCase()
-      .replace(/\s+/g, "-")}.${format === "markdown" ? "md" : format}`;
+    const safeTitle = decision.title
+      .toLowerCase()
+      .replace(/[^a-z0-9]+/g, "-")
+      .replace(/^-+|-+$/g, "")
+      .slice(0, 100);
+    const ext = format === "markdown" ? "md" : format;
+    const filename = `decision-${safeTitle || "export"}.${ext}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const filename = `decision-${decision.title
.toLowerCase()
.replace(/\s+/g, "-")}.${format === "markdown" ? "md" : format}`;
return new NextResponse(content, {
headers: {
"Content-Type": contentType,
"Content-Disposition": `attachment; filename="${filename}"`,
},
const safeTitle = decision.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 100);
const ext = format === "markdown" ? "md" : format;
const filename = `decision-${safeTitle || "export"}.${ext}`;
return new NextResponse(content, {
headers: {
"Content-Type": contentType,
"Content-Disposition": `attachment; filename="${filename}"`,
},
🤖 Prompt for AI Agents
In `@app/api/projects/`[id]/decisions/[decisionId]/export/route.ts around lines 86
- 94, The filename construction uses raw decision.title which allows header
injection; sanitize the title before building filename by stripping or replacing
characters not safe in HTTP headers (e.g., control chars \r \n, quotes,
backslashes, slashes), collapse multiple spaces, and truncate to a reasonable
length; update the code that builds filename (the filename variable where
decision.title is used) to produce a safe ASCII/URL-safe token (or use a safe
fallback like "untitled-decision" when the sanitized result is empty) and then
pass that sanitized filename into the NextResponse Content-Disposition header to
prevent injection.

risks: risks || "",
nonGoals: nonGoals || "",
confidence: confidence || "medium",
confidenceScore: confidence || 0.5,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: confidence || 0.5 treats a valid 0 confidence as falsy, defaulting it to 0.5.

The || operator falls back on any falsy value including 0. If a user sets the slider to 0%, the stored value will silently become 50%. Use nullish coalescing (??) to only fall back on null/undefined.

Proposed fix
-        confidenceScore: confidence || 0.5,
+        confidenceScore: confidence ?? 0.5,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
confidenceScore: confidence || 0.5,
confidenceScore: confidence ?? 0.5,
🤖 Prompt for AI Agents
In `@app/api/projects/`[id]/decisions/route.ts at line 53, The confidence
defaulting uses the || operator which treats 0 as falsy; update the assignment
that sets confidenceScore (where confidence is used) to use the nullish
coalescing operator (??) instead of || so only null/undefined fall back to 0.5,
i.e., change the expression that builds confidenceScore in route.ts to use
confidence ?? 0.5.

Comment on lines +92 to +101
const opportunities: OpportunityItem[] = analysisContent.opportunities.map((opp) => ({
id: opp.id,
title: opp.title,
description: opp.description,
impact: opp.impact as 'high' | 'medium' | 'low',
effort: opp.impactScore >= 0.7 ? 'high' : opp.impactScore >= 0.4 ? 'medium' : 'low',
impactScore: opp.impactScore,
feedbackCount: opp.feedbackCount,
category: opp.category,
}));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "page.tsx" | grep -E "analysis|projects"

Repository: Fresh-Industries/pm-analyzer

Length of output: 681


🏁 Script executed:

cd app/projects && find . -type f -name "page.tsx" 2>/dev/null | head -20

Repository: Fresh-Industries/pm-analyzer

Length of output: 486


🏁 Script executed:

fd "page.tsx" app/projects --type f

Repository: Fresh-Industries/pm-analyzer

Length of output: 651


🏁 Script executed:

cat -n app/projects/[id]/analysis/page.tsx | head -110

Repository: Fresh-Industries/pm-analyzer

Length of output: 4291


🏁 Script executed:

rg -n "opportunities" app/projects/[id]/analysis/page.tsx -B 2 -A 2

Repository: Fresh-Industries/pm-analyzer

Length of output: 2968


🏁 Script executed:

# Check where analysis content is generated - look for analysis generation code
fd "analysis" app --type f -e ts -e tsx | grep -v node_modules | head -20

Repository: Fresh-Industries/pm-analyzer

Length of output: 54


🏁 Script executed:

# Search for where Analysis data is created/stored to see if effort field exists
rg -n "effort" --type=ts --type=tsx -C 3 | head -100

Repository: Fresh-Industries/pm-analyzer

Length of output: 99


🏁 Script executed:

# Search for where Analysis model is used or generated
rg -n "effort" -t ts -C 3 | head -150

Repository: Fresh-Industries/pm-analyzer

Length of output: 6587


🏁 Script executed:

# Look for API routes or lib functions that generate analysis
fd -e ts -e tsx | grep -E "(api|lib|agent)" | grep -v node_modules | head -30

Repository: Fresh-Industries/pm-analyzer

Length of output: 1284


🏁 Script executed:

# Search for where analysis/opportunities are generated
rg -n "opportunities" -t ts -C 5 | head -200

Repository: Fresh-Industries/pm-analyzer

Length of output: 14115


🏁 Script executed:

# Look at the analyze API route to see where analysis content is generated
cat -n app/api/projects/[id]/analyze/route.ts | head -150

Repository: Fresh-Industries/pm-analyzer

Length of output: 3726


🏁 Script executed:

# Search for where Analysis data model is defined and used
rg -n "analysis.content" -t ts -C 3

Repository: Fresh-Industries/pm-analyzer

Length of output: 1105


🏁 Script executed:

# Find the agent that generates analysis to see if it includes effort
rg -n "opportunities" lib/agents -t ts -C 5 | head -200

Repository: Fresh-Industries/pm-analyzer

Length of output: 2825


🏁 Script executed:

# Check the clustering module to see what fields cluster objects have
cat -n lib/clustering.ts | head -200

Repository: Fresh-Industries/pm-analyzer

Length of output: 7225


🏁 Script executed:

cat -n lib/clustering.ts | tail -200

Repository: Fresh-Industries/pm-analyzer

Length of output: 7668


🏁 Script executed:

# Verify the PriorityMatrix component to confirm the quadrant distribution problem
cat -n components/PriorityMatrix.tsx | head -60

Repository: Fresh-Industries/pm-analyzer

Length of output: 2738


effort derivation from impactScore breaks the Eisenhower matrix quadrant distribution.

The clustering module provides no independent effort field. Line 97 derives effort directly from impact score (0.7+ → high, 0.4+ → medium, <0.4 → low), forcing a 1:1 correlation between impact and effort. This makes the Priority Matrix diagonal instead of 2×2: high-impact items always map to high-effort (Q2), low-impact items always to low-effort (Q3), leaving "Quick Wins" (Q1: high impact, low effort) and "Thankless Tasks" (Q4: low impact, high effort) unreachable.

To fix this, add an independent effort or effortScore field to the ClusterResult interface in lib/clustering.ts and calculate it separately in the clustering logic (e.g., based on category, complexity signals, or a dedicated heuristic), rather than deriving it from impact.

🤖 Prompt for AI Agents
In `@app/projects/`[id]/analysis/page.tsx around lines 92 - 101, The current
mapping in page.tsx wrongly derives opportunity.effort from impactScore,
creating a forced correlation; instead add an independent effortScore (or
effort) field to the ClusterResult interface in lib/clustering.ts and populate
it in the clustering logic using a separate heuristic (e.g., category,
complexity signals, or a dedicated effort heuristic) so clustering outputs an
independent effortScore, then change the mapping in
app/projects/[id]/analysis/page.tsx (where OpportunityItem is constructed from
analysisContent.opportunities) to read effort/effortScore from the cluster
result rather than computing it from impactScore, ensuring the Priority Matrix
can produce high-impact/low-effort and low-impact/high-effort quadrants.

Comment on lines +133 to +178
<div className="grid grid-cols-4 gap-4">
<Card>
<CardContent className="p-4 flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-lg">
<Target className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-2xl font-semibold">{analysisContent.feedbackCount}</p>
<p className="text-sm text-gray-500">Total Feedback</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex items-center gap-3">
<div className="p-2 bg-red-100 rounded-lg">
<AlertTriangle className="w-5 h-5 text-red-600" />
</div>
<div>
<p className="text-2xl font-semibold">{analysisContent.summary.bugs}</p>
<p className="text-sm text-gray-500">Bugs Reported</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex items-center gap-3">
<div className="p-2 bg-green-100 rounded-lg">
<TrendingUp className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-2xl font-semibold">{analysisContent.summary.features}</p>
<p className="text-sm text-gray-500">Feature Requests</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex items-center gap-3">
<div className="p-2 bg-purple-100 rounded-lg">
<Zap className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-2xl font-semibold">{opportunities.length}</p>
<p className="text-sm text-gray-500">Opportunities</p>
</div>
</CardContent>
</Card>
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Summary stats grid is not responsive.

grid-cols-4 is applied without responsive breakpoints. On small screens, four columns will be too narrow to be usable.

Proposed fix
-      <div className="grid grid-cols-4 gap-4">
+      <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="grid grid-cols-4 gap-4">
<Card>
<CardContent className="p-4 flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-lg">
<Target className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-2xl font-semibold">{analysisContent.feedbackCount}</p>
<p className="text-sm text-gray-500">Total Feedback</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex items-center gap-3">
<div className="p-2 bg-red-100 rounded-lg">
<AlertTriangle className="w-5 h-5 text-red-600" />
</div>
<div>
<p className="text-2xl font-semibold">{analysisContent.summary.bugs}</p>
<p className="text-sm text-gray-500">Bugs Reported</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex items-center gap-3">
<div className="p-2 bg-green-100 rounded-lg">
<TrendingUp className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-2xl font-semibold">{analysisContent.summary.features}</p>
<p className="text-sm text-gray-500">Feature Requests</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex items-center gap-3">
<div className="p-2 bg-purple-100 rounded-lg">
<Zap className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-2xl font-semibold">{opportunities.length}</p>
<p className="text-sm text-gray-500">Opportunities</p>
</div>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4 flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-lg">
<Target className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-2xl font-semibold">{analysisContent.feedbackCount}</p>
<p className="text-sm text-gray-500">Total Feedback</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex items-center gap-3">
<div className="p-2 bg-red-100 rounded-lg">
<AlertTriangle className="w-5 h-5 text-red-600" />
</div>
<div>
<p className="text-2xl font-semibold">{analysisContent.summary.bugs}</p>
<p className="text-sm text-gray-500">Bugs Reported</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex items-center gap-3">
<div className="p-2 bg-green-100 rounded-lg">
<TrendingUp className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-2xl font-semibold">{analysisContent.summary.features}</p>
<p className="text-sm text-gray-500">Feature Requests</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex items-center gap-3">
<div className="p-2 bg-purple-100 rounded-lg">
<Zap className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-2xl font-semibold">{opportunities.length}</p>
<p className="text-sm text-gray-500">Opportunities</p>
</div>
</CardContent>
</Card>
</div>
🤖 Prompt for AI Agents
In `@app/projects/`[id]/analysis/page.tsx around lines 133 - 178, The summary
stats grid uses a fixed "grid-cols-4" which breaks on small screens; update the
container div that currently has className "grid grid-cols-4 gap-4" to use
responsive Tailwind classes (e.g. "grid grid-cols-1 sm:grid-cols-2
md:grid-cols-4 gap-4") so the Card items (rendered via Card and CardContent and
using analysisContent and opportunities) flow to 1 column on mobile, 2 on small
screens, and 4 on larger screens.

decision.status === "handed_off" ? "default" : "secondary"
}
>
{decision.status.replace("_", " ")}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

String.replace only replaces the first occurrence.

If a status value ever contains more than one underscore (e.g., "in_progress_review"), only the first _ would be replaced, producing "in progress_review". Use replaceAll or a global regex instead.

The same pattern appears on Line 120 of app/projects/[id]/decisions/page.tsx.

Proposed fix
-              {decision.status.replace("_", " ")}
+              {decision.status.replaceAll("_", " ")}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{decision.status.replace("_", " ")}
{decision.status.replaceAll("_", " ")}
🤖 Prompt for AI Agents
In `@app/projects/`[id]/decisions/[decisionId]/page.tsx at line 62, The code uses
decision.status.replace("_", " ") which only replaces the first underscore;
update both occurrences of this call (the expressions using
decision.status.replace("_", " ")) to replace all underscores, e.g.
decision.status.replaceAll("_", " ") or decision.status.replace(/_/g, " "), so
multi-underscore statuses like "in_progress_review" become "in progress review";
pick replaceAll if runtime supports it or the regex alternative for broader
compatibility.

Comment on lines +362 to +377
<div className="relative">
{trigger ? (
<div onClick={() => setIsOpen(true)}>{trigger}</div>
) : (
<Button
variant="outline"
size="sm"
onClick={() => {
setIsOpen(true);
updatePreview(selectedFormat);
}}
>
<Download className="w-4 h-4 mr-2" />
Export
</Button>
)}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Custom trigger opens the dialog with an empty preview.

When trigger is provided, the onClick only calls setIsOpen(true) (line 364) without calling updatePreview. The default button path (line 371) does call updatePreview(selectedFormat), so only the custom-trigger path is affected — users will see a blank preview pane until they change the format dropdown.

Proposed fix
-        <div onClick={() => setIsOpen(true)}>{trigger}</div>
+        <div onClick={() => {
+          setIsOpen(true);
+          updatePreview(selectedFormat);
+        }}>{trigger}</div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="relative">
{trigger ? (
<div onClick={() => setIsOpen(true)}>{trigger}</div>
) : (
<Button
variant="outline"
size="sm"
onClick={() => {
setIsOpen(true);
updatePreview(selectedFormat);
}}
>
<Download className="w-4 h-4 mr-2" />
Export
</Button>
)}
<div className="relative">
{trigger ? (
<div onClick={() => {
setIsOpen(true);
updatePreview(selectedFormat);
}}>{trigger}</div>
) : (
<Button
variant="outline"
size="sm"
onClick={() => {
setIsOpen(true);
updatePreview(selectedFormat);
}}
>
<Download className="w-4 h-4 mr-2" />
Export
</Button>
)}
🤖 Prompt for AI Agents
In `@components/DecisionExportDialog.tsx` around lines 362 - 377, The custom
trigger path in DecisionExportDialog opens the dialog without populating the
preview because the onClick handler only calls setIsOpen(true); modify the
custom trigger click handler (the element rendering when trigger is truthy) to
also call updatePreview(selectedFormat) after opening the dialog so it mirrors
the default Button path (i.e., onClick should call setIsOpen(true) and then
updatePreview(selectedFormat)).

Comment on lines +43 to +52
// Helper to determine quadrant based on impact and effort
function getQuadrantKey(item: PriorityItem): 'q1' | 'q2' | 'q3' | 'q4' {
const highImpact = item.impact === 'high';
const lowEffort = item.effort === 'low';

if (highImpact && lowEffort) return 'q1'; // Quick Wins
if (highImpact && item.effort === 'high') return 'q2'; // Major Projects
if (!highImpact && lowEffort) return 'q3'; // Fillers
return 'q4'; // Thankless Tasks (low impact + high effort)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Logic bug: all medium-effort items fall into q4 ("Thankless Tasks / Avoid").

The classification only handles the extremes ('high' / 'low'), so any item with effort: 'medium' bypasses all specific checks and lands in q4 regardless of impact:

Impact Effort Expected Actual
high medium q1 or q2 q4
medium medium q3 q4
medium high q4 q4 ✓
low medium q3 q4
Proposed fix — treat medium effort like high for impact-dominant sorting
 function getQuadrantKey(item: PriorityItem): 'q1' | 'q2' | 'q3' | 'q4' {
-  const highImpact = item.impact === 'high';
-  const lowEffort = item.effort === 'low';
-  
-  if (highImpact && lowEffort) return 'q1'; // Quick Wins
-  if (highImpact && item.effort === 'high') return 'q2'; // Major Projects
-  if (!highImpact && lowEffort) return 'q3'; // Fillers
-  return 'q4'; // Thankless Tasks (low impact + high effort)
+  const highImpact = item.impact === 'high' || item.impact === 'medium';
+  const lowEffort = item.effort === 'low' || item.effort === 'medium';
+
+  if (highImpact && lowEffort) return 'q1'; // Quick Wins
+  if (highImpact && !lowEffort) return 'q2'; // Major Projects
+  if (!highImpact && lowEffort) return 'q3'; // Fillers
+  return 'q4'; // Thankless Tasks
 }

Adjust thresholds to match your product's definition of each quadrant. The key point is that 'medium' values must be handled explicitly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Helper to determine quadrant based on impact and effort
function getQuadrantKey(item: PriorityItem): 'q1' | 'q2' | 'q3' | 'q4' {
const highImpact = item.impact === 'high';
const lowEffort = item.effort === 'low';
if (highImpact && lowEffort) return 'q1'; // Quick Wins
if (highImpact && item.effort === 'high') return 'q2'; // Major Projects
if (!highImpact && lowEffort) return 'q3'; // Fillers
return 'q4'; // Thankless Tasks (low impact + high effort)
}
// Helper to determine quadrant based on impact and effort
function getQuadrantKey(item: PriorityItem): 'q1' | 'q2' | 'q3' | 'q4' {
const highImpact = item.impact === 'high' || item.impact === 'medium';
const lowEffort = item.effort === 'low' || item.effort === 'medium';
if (highImpact && lowEffort) return 'q1'; // Quick Wins
if (highImpact && !lowEffort) return 'q2'; // Major Projects
if (!highImpact && lowEffort) return 'q3'; // Fillers
return 'q4'; // Thankless Tasks
}
🤖 Prompt for AI Agents
In `@components/PriorityMatrix.tsx` around lines 43 - 52, getQuadrantKey currently
only branches on 'high' vs 'low' and drops any 'medium' effort into q4; update
getQuadrantKey (and reference PriorityItem) to explicitly handle effort ===
'medium' by introducing boolean flags (e.g., lowEffort, mediumEffort,
highEffort) and then change the branches so: high impact + lowEffort => 'q1',
high impact + (mediumEffort || highEffort) => 'q2', non-high impact + (lowEffort
|| mediumEffort) => 'q3', otherwise 'q4'; this ensures medium efforts are
classified correctly for high/medium/low impacts.

Fillers (Nice to Have)
</div>
<div className="flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-red-500"></span>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

class should be className in JSX.

Line 195 uses class instead of className. The styles won't be applied correctly.

Proposed fix
-        <span class="w-3 h-3 rounded-full bg-red-500"></span>
+        <span className="w-3 h-3 rounded-full bg-red-500"></span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span class="w-3 h-3 rounded-full bg-red-500"></span>
<span className="w-3 h-3 rounded-full bg-red-500"></span>
🤖 Prompt for AI Agents
In `@components/PriorityMatrix.tsx` at line 195, In the PriorityMatrix component,
the JSX uses an invalid attribute on the status indicator span (the <span> with
the red dot); replace the DOM attribute "class" with React's "className" on that
element so the tailwind classes (w-3 h-3 rounded-full bg-red-500) are applied
correctly; search for the span in PriorityMatrix.tsx (the small dot indicator)
and update its attribute to className.

<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800">
<SliderPrimitive.Range className="absolute h-full bg-slate-900 dark:bg-slate-50" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border border-slate-200 border-slate-900 bg-white shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50 dark:border-slate-800 dark:border-slate-50 dark:bg-slate-950 dark:focus-visible:ring-slate-300" />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Conflicting border color classes on the Thumb element.

The Thumb has both border-slate-200 and border-slate-900 (and dark:border-slate-800 with dark:border-slate-50). These conflict — the winning class depends on CSS generation order, which is non-deterministic in Tailwind. Likely one of each pair should be removed.

Proposed fix (assuming the intent is a dark border on light and light border on dark)
-    <SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border border-slate-200 border-slate-900 bg-white shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50 dark:border-slate-800 dark:border-slate-50 dark:bg-slate-950 dark:focus-visible:ring-slate-300" />
+    <SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border border-slate-900/50 bg-white shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50 dark:border-slate-50 dark:bg-slate-950 dark:focus-visible:ring-slate-300" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border border-slate-200 border-slate-900 bg-white shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50 dark:border-slate-800 dark:border-slate-50 dark:bg-slate-950 dark:focus-visible:ring-slate-300" />
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border border-slate-900/50 bg-white shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50 dark:border-slate-50 dark:bg-slate-950 dark:focus-visible:ring-slate-300" />
🤖 Prompt for AI Agents
In `@components/ui/slider.tsx` at line 23, The Thumb has conflicting Tailwind
border classes (border-slate-200 vs border-slate-900 and dark:border-slate-800
vs dark:border-slate-50); update the SliderPrimitive.Thumb class list to remove
the redundant pairs so there is only one border class per theme — keep
border-slate-900 for light mode and dark:border-slate-50 for dark mode (remove
border-slate-200 and dark:border-slate-800) in the SliderPrimitive.Thumb
component.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant