Skip to content

chore: migrate @bruddle/react → @doctorproject/react (bruddle-design-system)#2

Open
drproject-coding wants to merge 30 commits intomainfrom
doctorpost-ui-revamp
Open

chore: migrate @bruddle/react → @doctorproject/react (bruddle-design-system)#2
drproject-coding wants to merge 30 commits intomainfrom
doctorpost-ui-revamp

Conversation

@drproject-coding
Copy link
Copy Markdown
Owner

@drproject-coding drproject-coding commented Mar 10, 2026

Summary

  • Replaces the outdated @bruddle/react (from drproject-coding/bruddle-react) with the up-to-date @doctorproject/react (from drproject-coding/bruddle-design-system)
  • Created new standalone repo drproject-coding/doctorproject-react from bruddle-design-system/packages/react for clean npm install
  • Full CSS migration from local styles/bruddle/ to styles/doctorproject/ (sourced directly from the design system)
  • All CSS custom properties, Tailwind classes, and component imports updated across the entire codebase

Changes

  • Package: @bruddle/react removed, @doctorproject/react added
  • CSS tokens: --bru-*--drp-* across 80+ files
  • Tailwind classes: bru-*drp-* across 80+ files
  • Import paths: @bruddle/react@doctorproject/react in 56 source files
  • CSS files: styles/bruddle/ deleted, styles/doctorproject/ added (17 files from design system)
  • Tests: Updated 6 test files to use drp- class selectors

Test plan

  • npm run build exits 0 (TypeScript + Next.js build)
  • tsc --noEmit — zero errors
  • 905/910 tests pass (5 pre-existing failures unrelated to migration)
  • All 14 pages return HTTP 200 (verified via catastrophiser agent)
  • Rendered HTML confirmed to contain drp- CSS classes, zero bru- classes
  • @doctorproject/react JS chunk loads with 200 OK
  • Zero remaining @bruddle/react imports in source
  • Zero remaining --bru-* CSS var references in source

Summary by CodeRabbit

  • Style

    • Major design-system refresh: new visual tokens, colors, spacing, and animations for a cohesive look.
  • UI Improvements

    • Updated buttons, inputs, modals, alerts, loaders, empty states, badges, tabs, progress visuals and many pages (onboarding, dashboard, calendar, campaigns, library, settings, factory, studio, knowledge).
  • New Features

    • Brand-aware recommendations: Create uses profile context to suggest smarter defaults while preserving manual overrides.
  • Tests

    • Updated tests and mocks to reflect the new UI components.
  • Chores

    • Migrated to the refreshed component library and consolidated styling assets.

- knowledge/page.tsx: replace lucide Loader with bruddle Loader, raw
  input with Input, raw spans with Tag(purple/filled) + Badge(primary),
  empty state Card with EmptyState, inactive filter buttons get ghost
  variant and sm size; removed unused lucide imports (Loader, Plus, Search)
- learning/page.tsx: replace manual tab button row with Tabs component
  (activeKey/onChange API), loading state in Refresh button with bruddle
  Loader, empty proposals paragraph with EmptyState; removed unused Badge
- DocumentEditor.tsx: replace raw textarea with Textarea, raw input with
  Input, manual feedback div with Alert (success/error variant + onClose),
  System span badge with Badge(primary); hardcoded #0066FF color removed
- calendar/page.tsx: TabItem `id` -> `key`, Tabs `activeTab` -> `activeKey`
- library/page.tsx: same TabItem/Tabs fix; Pagination `page`/`onChange` -> `currentPage`/`onPageChange`
- factory/page.tsx: Alert requires children, add empty string child
- settings/page.tsx: Input `error` from string to boolean via `!!` coercion (3 instances)
- PostEditorModal.tsx: Alert does not accept `style` prop; wrap in `<div style={...}>` instead
- Create drproject-coding/doctorproject-react GitHub repo from
  bruddle-design-system/packages/react (latest, up-to-date)
- Replace @bruddle/react with @doctorproject/react in package.json
- Migrate CSS: styles/bruddle/ (17 files) → styles/doctorproject/
  (CSS now from bruddle-design-system source of truth)
- Update all CSS custom properties: --bru-* → --drp-*
- Update all Tailwind utility classes: bru-* → drp-*
- Update all import paths across 56 source files
- Update 80+ files total (pages, components, layouts)
- Fix @font-face paths to point to public/fonts/
- Update __tests__/ class selectors: bru- → drp-
- Build passes, 905/910 tests pass (5 pre-existing failures unrelated)
- All 14 pages verified rendering correctly via catastrophiser
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
doctorpost-v12 Error Error Mar 16, 2026 5:11pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR migrates the app from the Bruddle design system to DoctorProject: replaces UI library imports, renames CSS tokens/classes (bru-* → drp-*), deletes legacy Bruddle CSS, adds DoctorProject CSS modules, updates many components/pages/tests, refreshes the manifest, and adds planning docs for brand-aware features.

Changes

Cohort / File(s) Summary
Package & manifest
package.json, .vexp/manifest.json, .vexp/mcp.port
Switched dependency to @doctorproject/react; manifest reindexed with many new/updated entries; added .vexp/mcp.port.
Global styles
app/globals.css, styles/bruddle/* (deleted), styles/doctorproject/* (new)
Removed Bruddle CSS/tokens; added DoctorProject tokens, buttons, forms, cards, charts, animations, composites, and updated index imports.
Layouts & shell
app/layout.tsx, app/(onboarding)/layout.tsx, app/(protected)/layout.tsx, components/Header.tsx, components/Sidebar.tsx, components/ConfirmDialog.tsx
Rewrote layout/shell to use DashboardLayout and DS components; introduced new nav model/icon handling; replaced dialog/header controls with DS Button/Icon usage.
Auth & onboarding pages
app/login/page.tsx, app/signup/page.tsx, app/forgot-password/page.tsx, app/reset-password/page.tsx, app/(onboarding)/onboarding/*
Replaced inputs, buttons, alerts, loaders with DoctorProject components and migrated styling tokens to drp-*; preserved control flow.
Protected pages (bulk UI)
app/(protected)/* (dashboard, analytics, factory, studio, create, calendar, campaigns, library, settings, knowledge, learning, etc.)
Massive UI migration: swapped components (Button, Input, Textarea, Loader, EmptyState, Tabs, Tag, Badge, ProgressBar, Alert, Pagination), renamed tokens to drp-*, updated inline styles; data/business logic largely preserved.
Components (many)
components/** (PostEditorModal, PostGenerator, Toast, factory/, knowledge/, learning/, campaigns/, brand/, calendar/, etc.)
Replaced native elements and Bru primitives with DoctorProject components, updated classNames/tokens, and adjusted a few small APIs (notably CalendarView prop addition).
Create & AI recommendations (docs)
app/(protected)/create/page.tsx, lib/api.ts (in docs), docs/plans/*
Create page UI migrated; planning documents add brand-aware recommendation design and a proposed getPostRecommendations(..., brandContext?) signature (docs only in this diff).
Tests & mocks
__tests__/**/*.test.tsx
Updated tests and mocks to reference @doctorproject/react, adjusted assertions for drp-* tokens and new mock components (Loader, ProgressBar, Badge, Select).
Docs & plans
docs/plans/*
Added multiple planning documents (design-system migration, Create redesign, Brand page, campaign lifecycle, full-spec, brand-aware recommendations, etc.).

Sequence Diagram(s)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Poem

🐰 I hopped through tokens, swapped bru for drp,
Buttons in my paws, I styled things spryly.
Pages repainted, components all new,
Docs and manifests followed the cue.
Hooray — the UI blooms in DoctorProject view!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch doctorpost-ui-revamp

Copy link
Copy Markdown

@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: 15

Caution

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

⚠️ Outside diff range comments (13)
components/brand/sections/PositioningSection.tsx (1)

16-59: ⚠️ Potential issue | 🔴 Critical

Move the useState hook above the editing early return to prevent hook order violations.

The useState call at line 59 is only reachable when editing === false. If the editing prop changes between renders, React will see a different number of hooks and throw a runtime error. Hooks must be called unconditionally in the same order on every render.

Proposed fix
 const PositioningSection: React.FC<PositioningSectionProps> = ({
   profile,
   editing,
   onChange,
 }) => {
+  const [expanded, setExpanded] = useState(false);
+
   if (editing) {
     return (
       <div>
         <textarea
           value={profile.positioning ?? ""}
           onChange={(e) => onChange({ positioning: e.target.value })}
           placeholder="e.g. The only LinkedIn coach who teaches engineers to write without sounding like a LinkedIn post."
           rows={5}
           style={{
             width: "100%",
             resize: "vertical",
             padding: "10px 12px",
             fontFamily: "var(--drp-font-primary)",
             fontSize: "var(--drp-text-sm)",
             color: "var(--drp-black)",
             background: "var(--drp-white)",
             border: "1px solid rgba(0,0,0,0.18)",
             borderRadius: 0,
             outline: "none",
             lineHeight: 1.6,
             boxSizing: "border-box",
           }}
         />
         <p
           style={{
             marginTop: "6px",
             fontFamily: "var(--drp-font-primary)",
             fontSize: "var(--drp-text-xs)",
             color: "rgba(18,18,18,0.5)",
           }}
         >
           Describe how you&apos;re different from others in your space.
           What&apos;s your unique angle?
         </p>
       </div>
     );
   }
-
-  const [expanded, setExpanded] = useState(false);
 
   const text = profile.positioning ?? "";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brand/sections/PositioningSection.tsx` around lines 16 - 59, The
useState hook call for expanded is placed after the early return and must be
moved above the early return to avoid hook order violations; in the
PositioningSection component, lift the useState([...]) declaration (e.g., const
[expanded, setExpanded] = useState(false)) to the top of the component body
before the if (editing) return branch so hooks are invoked unconditionally and
then continue to use expanded/setExpanded where needed.
components/factory/LearningPhaseResult.tsx (1)

50-81: ⚠️ Potential issue | 🟠 Major

Fix invalid disclosure button markup.

This button currently wraps an <h3>, which violates HTML content model rules—a <button> can only contain phrasing content, not heading elements. Additionally, without type="button", the button defaults to type="submit" and will submit any parent form. Move the heading outside the button (or use a <span>), add type="button", and expose the expanded state with aria-expanded.

Suggested fix
-      <button
-        onClick={() => setExpanded(!expanded)}
+      <h3 style={{ margin: 0 }}>
+        <button
+          type="button"
+          aria-expanded={expanded}
+          onClick={() => setExpanded(!expanded)}
           style={{
             width: "100%",
             display: "flex",
             justifyContent: "space-between",
             alignItems: "center",
             background: "none",
             border: "none",
             cursor: "pointer",
             padding: 0,
             marginBottom: expanded ? "var(--drp-space-4)" : 0,
           }}
-      >
+        >
           <div
             style={{
               display: "flex",
               alignItems: "center",
               gap: "var(--drp-space-2)",
             }}
           >
             <Brain size={18} style={{ color: "var(--drp-purple)" }} />
-          <h3
+          <span
             style={{
               fontSize: "var(--drp-text-h5)",
               fontWeight: 700,
               margin: 0,
               color: "var(--drp-success-dark, `#2d7a3a`)",
             }}
           >
             Learning Phase Complete
-          </h3>
+          </span>
           </div>
           <div
             style={{
               display: "flex",
               alignItems: "center",
               gap: "var(--drp-space-2)",
             }}
           >
             <span
               style={{
                 fontSize: "var(--drp-text-xs)",
                 color: "var(--drp-grey)",
               }}
             >
               {output.signals.length} signal
               {output.signals.length !== 1 ? "s" : ""} captured
             </span>
             {expanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
           </div>
-      </button>
+        </button>
+      </h3>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/factory/LearningPhaseResult.tsx` around lines 50 - 81, The button
in LearningPhaseResult (the element that calls onClick={() =>
setExpanded(!expanded)} and currently wraps the <h3> with text "Learning Phase
Complete" and the Brain icon) violates HTML rules and misses accessibility
attributes; change it to type="button", move the <h3> outside the button (or
replace the <h3> with a <span> if it must remain inside), keep the Brain icon
and visual layout inside the clickable element, and add aria-expanded={expanded}
to the button so the expanded state is exposed for assistive tech (refer to
setExpanded and expanded to wire the state).
components/ConfirmDialog.tsx (1)

104-125: ⚠️ Potential issue | 🟠 Major

Add type="button" to both dialog actions.

Line 104 and Line 110 currently rely on the default button type, so opening this dialog inside a form can submit that form when the user clicks Cancel or Confirm.

Proposed fix
               <button
+                type="button"
                 className="drp-btn drp-btn--secondary drp-btn--sm"
                 onClick={() => handleClose(false)}
               >
                 {state.cancelLabel ?? "Cancel"}
               </button>
               <button
+                type="button"
                 className="drp-btn drp-btn--sm"
                 style={
                   state.danger
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ConfirmDialog.tsx` around lines 104 - 125, The Cancel and Confirm
buttons in ConfirmDialog.tsx are missing explicit types, which allows them to
act as form submitters when the dialog is rendered inside a form; update both
button elements that call handleClose(false) and handleClose(true) to include
type="button" on each button to prevent accidental form submission (reference
the button with onClick={() => handleClose(false)} using state.cancelLabel and
the button with onClick={() => handleClose(true)} that uses state.danger).
components/factory/PipelineStepper.tsx (2)

137-149: ⚠️ Potential issue | 🟠 Major

Make completed phases keyboard-accessible.

Line 137 turns each completed step into a mouse-only clickable div. That prevents keyboard users from opening earlier phases to review them.

Proposed fix
             <div
               key={step.phase}
               onClick={canClick ? () => onPhaseClick(step.phase) : undefined}
+              role={canClick ? "button" : undefined}
+              tabIndex={canClick ? 0 : undefined}
+              onKeyDown={
+                canClick
+                  ? (e) => {
+                      if (e.key === "Enter" || e.key === " ") {
+                        e.preventDefault();
+                        onPhaseClick(step.phase);
+                      }
+                    }
+                  : undefined
+              }
               style={{
                 flex: 1,
                 display: "flex",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/factory/PipelineStepper.tsx` around lines 137 - 149, The step
container currently renders as a non-focusable div (keyed by step.phase) with
only an onClick handler, which blocks keyboard users; update the element used
for completed/interactive steps (where canClick is true) to be
keyboard-accessible by either rendering a semantic button or adding
role="button", tabIndex={0}, aria-disabled appropriately, and attaching an
onKeyDown handler that invokes onPhaseClick(step.phase) for Enter and Space;
ensure the cursor and visual styles remain the same and that non-clickable steps
keep no tab stop (no tabIndex or aria-disabled="true").

231-259: ⚠️ Potential issue | 🟠 Major

Set type="button" on the stepper action buttons.

The retry, previous, and next controls currently use the default submit behavior. If this component is ever rendered inside a form, these clicks will submit it unexpectedly.

Proposed fix
                 {canRetry && (
                   <button
+                    type="button"
                     onClick={(e) => {
                       e.stopPropagation();
                       onRetryPhase(step.phase);
@@
           <button
+            type="button"
             onClick={handlePrev}
             disabled={!canGoPrev}
             style={{
@@
           <button
+            type="button"
             onClick={handleNext}
             disabled={!canGoNext}
             style={{

Also applies to: 277-342

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/factory/PipelineStepper.tsx` around lines 231 - 259, The stepper
action buttons (retry, previous, next) in the PipelineStepper component are
missing an explicit type and default to "submit", which can submit surrounding
forms; update each <button> used for step actions (the retry button that calls
onRetryPhase(step.phase) and the previous/next control buttons in the same
component) to include type="button" to prevent accidental form submission,
keeping the existing props, event handlers, and styles unchanged.
components/brand/sections/PillarsSection.tsx (1)

258-280: ⚠️ Potential issue | 🟠 Major

Reject duplicate pillar ids before creating a custom pillar.

This path uses the trimmed label as the persisted id without checking for collisions. If a user adds a custom pillar with the same text as an existing predefined/custom pillar, React keys stop being unique and later toggle/delete calls will target the wrong entries.

Proposed fix
             <button
               type="button"
               className="drp-btn drp-btn--secondary drp-btn--sm"
               disabled={!newPillarLabel.trim()}
               onClick={() => {
                 const label = newPillarLabel.trim();
                 if (!label) return;
+                const idExists = allPillars.some(
+                  (pillar) => pillar.id.toLowerCase() === label.toLowerCase(),
+                );
+                if (idExists) return;
+
                 const newCustom: CustomPillar = {
                   id: label,
                   label,
                   description: newPillarDesc.trim(),
                 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brand/sections/PillarsSection.tsx` around lines 258 - 280, The
code creates a new CustomPillar using the trimmed newPillarLabel as id without
checking for collisions; update the onClick handler to compute const id =
newPillarLabel.trim(); then check against existing ids from
profile.customPillars (profile.customPillars?.map(p=>p.id) ) and the app's
predefined pillar ids (e.g., the pillars or availablePillars list in scope) and
if id already exists, reject the creation (early return) and surface feedback
(disable button, set an error state, or show a toast). Only when the id is
unique proceed to build the CustomPillar, call applyUpdate({ pillars:
[...selectedIds, id], customPillars: updatedCustomPillars }), and clear inputs.
components/EnhancedDropdown.tsx (2)

243-253: ⚠️ Potential issue | 🟡 Minor

Set the filter chips to type="button".

These buttons default to submit, so using the dropdown inside a form can submit the form when a user is only toggling filters.

Suggested fix
               <button
                 key={tag.id}
+                type="button"
                 className={`enhanced-dropdown-filter-tag ${activeFilters.includes(tag.filter) || (tag.id === "all" && activeFilters.length === 0) ? "active" : ""}`}
                 onClick={() =>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/EnhancedDropdown.tsx` around lines 243 - 253, The filter chip
buttons in EnhancedDropdown are missing an explicit type and will default to
submit inside forms; update the button element rendering the filter chips (the
element using key={tag.id}, className "enhanced-dropdown-filter-tag" and
handlers setActiveFilters/handleFilterToggle) to include type="button" so
toggling filters does not submit parent forms.

229-305: ⚠️ Potential issue | 🟠 Major

Make the popup keyboard-operable, not just clickable.

The menu items are div elements with role="option", but the popup itself is not a listbox, and there is no arrow-key/focus management. Keyboard users cannot interact with this like a real dropdown.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/EnhancedDropdown.tsx` around lines 229 - 305, The dropdown is not
keyboard-operable: convert the popup container to role="listbox" (give it an id
and set the toggle button's aria-controls/aria-expanded) and implement roving
focus/focusedIndex state in the EnhancedDropdown component so options (currently
rendered as divs in Object.entries(filteredOptions) with role="option") receive
tabIndex={focusedIndex === index ? 0 : -1} and focus when focusedIndex changes;
add an onKeyDown on the listbox to handle ArrowDown/ArrowUp/Home/End to move
focusedIndex, Enter/Space to invoke handleSelect(option.value), and Escape to
close the menu (toggling isOpen), and ensure each option also handles onFocus to
set focusedIndex and onKeyDown as a fallback. Ensure handleSelect and isOpen
toggle logic are used to close the menu after selection and update aria-selected
accordingly.
app/(protected)/analytics/page.tsx (1)

45-57: ⚠️ Potential issue | 🟠 Major

Wait for user.id before fetching analytics.

getAnalytics(user?.id ?? "") runs once even while auth is still loading, so this can issue a request for "" and flip the page into the error path before the real user is available.

Suggested fix
   useEffect(() => {
+    if (!user?.id) {
+      return;
+    }
+
     const fetchData = async () => {
       try {
-        const result = await getAnalytics(user?.id ?? "");
+        const result = await getAnalytics(user.id);
         setData(result);
       } catch (error) {
         console.error("Failed to load analytics:", error);
       } finally {
         setLoading(false);
       }
     };
     void fetchData();
   }, [user?.id]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/analytics/page.tsx around lines 45 - 57, The effect
currently calls getAnalytics with a fallback empty string and runs while auth is
still resolving; update the useEffect to wait for a real user id by
short-circuiting when user?.id is falsy (e.g., if (!user?.id) return;) and
remove the "" fallback so you call getAnalytics(user.id) only when available;
keep the existing try/catch/finally around fetchData but ensure fetchData is
only invoked when user?.id is present.
components/create/PostStructureCards.tsx (1)

372-448: ⚠️ Potential issue | 🟠 Major

Avoid nesting a second button-like control inside the card button.

This card is a real <button>, but the preview toggle inside it is also exposed as an interactive control (role="button" + tabIndex). That invalid nesting breaks expected keyboard and assistive-tech behavior. Make the preview toggle a sibling button, or change the outer wrapper to a non-button element with explicit selection semantics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/create/PostStructureCards.tsx` around lines 372 - 448, The card
currently renders as a real <button> (the element using handleSelect) but
contains an inner interactive span using role="button" that calls togglePreview,
which creates invalid nested interactive controls; fix by making the preview
toggle a sibling <button> (or other native control) outside the card button and
ensure it calls togglePreview, stops event propagation to avoid triggering
handleSelect (use e.stopPropagation()), and keep keyboard activation by using
onKeyDown/onKeyUp on that sibling button; locate the outer card button (the
element calling handleSelect) and the inner toggle invocation (togglePreview,
option.id) and move/replace the span into a separate button sibling that
prevents propagation so selection and preview behavior work independently.
__tests__/components/DocumentEditor.test.tsx (1)

167-182: ⚠️ Potential issue | 🟡 Minor

Missing jest.useFakeTimers() setup for timer test.

Line 181 calls jest.advanceTimersByTime(3000) but jest.useFakeTimers() doesn't appear to be called in this test file. Without fake timers enabled, this call won't affect the test behavior.

🐛 Proposed fix

Add fake timer setup to the test or test suite:

+  beforeAll(() => {
+    jest.useFakeTimers();
+  });
+
+  afterAll(() => {
+    jest.useRealTimers();
+  });

Or within the specific test:

   it("hides feedback after 3 seconds", async () => {
+    jest.useFakeTimers();
     render(<DocumentEditor {...mockProps} />);
     // ... rest of test
+    jest.useRealTimers();
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/components/DocumentEditor.test.tsx` around lines 167 - 182, The
test "hides feedback after 3 seconds" in DocumentEditor.test.tsx uses
jest.advanceTimersByTime(3000) but never enables fake timers; update the test to
call jest.useFakeTimers() before triggering the timeout behavior (and after
assertions call jest.useRealTimers() or restore timers) so
jest.advanceTimersByTime affects the component's setTimeout-based feedback hide
logic; apply this to the specific it("hides feedback after 3 seconds") test (or
in the surrounding describe/ beforeEach) that interacts with DocumentEditor and
the save feedback.
app/(protected)/library/page.tsx (2)

19-29: ⚠️ Potential issue | 🟡 Minor

Use a truthy check when deriving the source tag.

post.strategyOutput ?? post.format keeps "", so a post with an empty strategyOutput and a valid format now renders with the wrong source tag/color.

Proposed fix
 function getSource(post: ScheduledPost): "Studio" | "Factory" | "Create" {
-  if (post.strategyOutput ?? post.format) return "Studio";
+  if (post.strategyOutput || post.format) return "Studio";
   if (post.factoryScore !== undefined) return "Factory";
   return "Create";
 }

Also applies to: 265-267

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/library/page.tsx around lines 19 - 29, The current getSource
function uses the nullish coalescing expression post.strategyOutput ??
post.format which treats empty strings as valid, causing posts with an empty
strategyOutput to incorrectly pick "Studio"; change the check to a truthy check
(e.g., post.strategyOutput || post.format or an explicit non-empty string test)
so empty strategyOutput falls through to check factoryScore and then Create;
update the analogous logic at the other occurrence (the block referenced at
lines 265-267) and ensure the SOURCE_TAG_COLOR mapping (Studio/Factory/Create)
remains unchanged.

126-133: ⚠️ Potential issue | 🟠 Major

Clamp page after the result set shrinks.

If the last item on a later page is deleted, page can stay greater than totalPages. pagedPosts then goes empty and the new empty state renders even though earlier pages still contain posts.

Proposed fix
   const totalPages = Math.max(
     1,
     Math.ceil(filteredPosts.length / POSTS_PER_PAGE),
   );
+
+  useEffect(() => {
+    setPage((current) => Math.min(current, totalPages));
+  }, [totalPages]);
+
   const pagedPosts = filteredPosts.slice(
     (page - 1) * POSTS_PER_PAGE,
     page * POSTS_PER_PAGE,
   );

Also applies to: 363-369

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/library/page.tsx around lines 126 - 133, After computing
totalPages from filteredPosts and POSTS_PER_PAGE, clamp the page variable to the
valid range (1..totalPages) before calculating pagedPosts so that page is
reduced when the result set shrinks; then slice filteredPosts using the clamped
page to produce pagedPosts. Apply the same clamp-before-slice fix to the other
identical pagination block that computes pagedPosts (the block analogous to
totalPages/pagedPosts around the later occurrence).
🧹 Nitpick comments (23)
app/(protected)/library/[id]/page.tsx (1)

366-369: Duplicate @keyframes spin definition.

The same keyframe animation is defined twice (lines 368 and 604). Consider extracting to a single definition or using an existing CSS animation utility.

♻️ Suggested consolidation

Define the keyframes once outside the component or move to a shared CSS file:

+const spinKeyframes = `@keyframes spin { to { transform: rotate(360deg); } }`;
+
 export default function PostDetailPage() {
   // ...
   if (loading) {
     return (
       <div ...>
         <Loader size={20} style={{ animation: "spin 1s linear infinite" }} />
         <span>Loading…</span>
-        <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
+        <style>{spinKeyframes}</style>
       </div>
     );
   }
   // ...
   return (
     <div style={{ padding: "24px 24px 64px" }}>
       {/* ... */}
-      <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
     </div>
   );
 }

Also applies to: 604-604

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/library/[id]/page.tsx around lines 366 - 369, The `@keyframes`
spin animation is defined twice; remove the duplicate inline <style> blocks and
consolidate a single definition for `@keyframes` spin (used by Loader) in a shared
location — e.g., move the keyframes out of the component in page.tsx into a
global stylesheet, a shared CSS module, or a top-level style block so both
usages reference the same `@keyframes` spin; update the component(s) that
reference the animation (the Loader usage in page.tsx) to rely on that single
shared definition.
app/(protected)/campaigns/[campaignId]/idea/[slotOrder]/page.tsx (1)

291-310: Consider using design system tokens for button variant colors.

The action buttons still use hardcoded color values (#FFCCCC, #990000, #0066CC) while the rest of the file migrates to --drp-* tokens. For consistency with the design system migration, consider using semantic color tokens if available (e.g., --drp-danger, --drp-danger-bg, --drp-primary).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/campaigns/[campaignId]/idea/[slotOrder]/page.tsx around
lines 291 - 310, The inline style block for action button rendering (the style
object that checks action.variant and uses background/color) uses hardcoded hex
values; replace those hex literals with the design-system CSS tokens (e.g., use
--drp-danger-bg and --drp-danger for danger background/color, and --drp-primary
for primary background/color) inside the same conditional branches for
action.variant in the style object for the button (refer to the style properties
background and color where action.variant === "danger" / "primary"); keep the
existing fallbacks or add fallback token values if needed and ensure
cursor/opacity logic with the saving variable remains unchanged.
components/campaigns/IdeaInbox.tsx (2)

76-118: Set explicit button types.

Line 76 and Line 132 define <button> elements without type. In HTML those default to submit, so this component can trigger accidental form submission if it is ever rendered inside a form. Please set both to type="button".

Suggested change
       <button
+        type="button"
         onClick={() => setOpen((v) => !v)}
         style={{
           width: "100%",
@@
             <button
               key={idea.id}
+              type="button"
               onClick={() => {
                 onSelect(idea);
                 setOpen(false);

Also applies to: 132-149

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/campaigns/IdeaInbox.tsx` around lines 76 - 118, The buttons in the
IdeaInbox component are missing explicit types and will default to type="submit"
inside forms; update the <button> elements (the toggle button that calls setOpen
and displays open/ideas and the other button around lines 132-149) to include
type="button" so they do not trigger form submission—locate the toggle button
using the setOpen/onClick handler and the second button by its surrounding text
or handler and add the explicit type attribute to both.

100-107: Avoid hard-coded badge colors in the token migration.

This new badge still uses raw hex values, so it will not follow future theme/token updates with the rest of the DoctorProject styles. Prefer the matching --drp-* semantic variables here too.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/campaigns/IdeaInbox.tsx` around lines 100 - 107, The span in
IdeaInbox.tsx uses hard-coded colors ("#0066CC" and "#fff"); replace those with
the corresponding semantic CSS variables (e.g. use var(--drp-primary) for
background and var(--drp-on-primary) or var(--drp-white) for color) so the badge
follows the project's design tokens; update the inline style object on the span
(the one with fontSize/fontWeight/background/color) to reference the --drp-*
variables instead of hex literals.
app/(protected)/factory/page.tsx (1)

1198-1220: Consider removing empty string children from Alert.

The {""} children on the warning Alert (line 1205) appears to be a workaround. If the Alert component doesn't require children when a title is provided, use a self-closing tag instead:

♻️ Suggested cleanup
 <Alert
   variant="warning"
   title="Fix failing guardrails before formatting"
->
-  {""}
-</Alert>
+/>

If the component requires children, consider passing null or updating the component to make children optional when title is sufficient.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/factory/page.tsx around lines 1198 - 1220, The Alert
component rendered inside the {!allGuardrailsPassing()} block currently has an
empty string child ({""}); remove the empty string and make the Alert a
self-closing tag since it already has a title prop (i.e., replace <Alert
variant="warning" title="...">{""}</Alert> with a self-closing <Alert
variant="warning" title="..."/>). If Alert actually requires children, instead
pass null or update the Alert component to make children optional; look for the
Alert usage near the allGuardrailsPassing() conditional and the
handleFormat/Button block to apply the change.
app/reset-password/page.tsx (1)

89-91: Consider adding a label to the Suspense fallback Loader for consistency.

The Loader in ResetPasswordContent (lines 89-91) includes a descriptive label prop, but the Suspense fallback Loader (line 163) does not. While this may be intentional since the initial page load context differs from form submission, adding a generic label like "Loading..." would provide consistent accessibility across loading states.

♻️ Optional: Add label to Suspense fallback Loader
-          <Loader />
+          <Loader label="Loading..." />

Also applies to: 163-163

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/reset-password/page.tsx` around lines 89 - 91, Add a descriptive label to
the Suspense fallback Loader for consistency and accessibility: update the
Loader used as the Suspense fallback in ResetPasswordContent (the Suspense
fallback Loader) to include a generic label prop like "Loading..." so it matches
the other Loader usage (the Loader with label prop in ResetPasswordContent).
__tests__/components/PostEditorModal.test.tsx (1)

74-77: Use a stable selector instead of the design-system class.

Querying .drp-modal__close couples this test to an internal class name, which is exactly what changed in this migration. Prefer exposing an accessible name or data-testid from PostEditorModal and querying that instead.

🧪 Example test-side change once the component exposes an accessible close control
-    const closeButton = document.querySelector(
-      ".drp-modal__close",
-    ) as HTMLElement;
+    const closeButton = screen.getByRole("button", { name: /close/i });
     fireEvent.click(closeButton);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/components/PostEditorModal.test.tsx` around lines 74 - 77, The test
is querying an internal design-system class ".drp-modal__close" (closeButton)
which is unstable; update the component PostEditorModal to expose a stable
selector (either add an accessible name/aria-label to the close control or a
data-testid like "post-editor-close"), then change the test to query that stable
hook (e.g., use getByRole('button', { name: /close/i }) or
getByTestId('post-editor-close') instead of
document.querySelector(".drp-modal__close")). Ensure references to the element
use the same identifier (closeButton) so assertions remain unchanged.
package.json (1)

25-25: Consider pinning @doctorproject/react in package.json for clarity.

While package-lock.json already pins this dependency to a specific commit SHA (7d8311d48e34634950d48c50204eecd0acff20d8), making the pinning explicit in package.json improves code visibility and reduces questions during review. This is optional but aligns with best practices for GitHub dependencies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 25, Update the dependency declaration for
"@doctorproject/react" in package.json to pin it to the commit SHA currently
used in package-lock.json; replace the value
"github:drproject-coding/doctorproject-react" with the git reference including
the commit hash (e.g.
"github:drproject-coding/doctorproject-react#7d8311d48e34634950d48c50204eecd0acff20d8")
so package.json explicitly reflects the exact pinned version.
components/campaigns/CampaignList.tsx (1)

127-135: Consider using design system tokens for status badge colors.

The status badge uses hardcoded hex colors (#E85D04, #00AA66) while other elements use var(--drp-*) tokens. For consistency with the design system, consider using semantic color tokens if available (e.g., --drp-warning, --drp-success).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/campaigns/CampaignList.tsx` around lines 127 - 135, The status
badge in CampaignList (the inline style on the <span> that checks c.status) uses
hardcoded hex colors; change it to use design tokens instead by mapping statuses
to CSS variables (e.g., use "var(--drp-warning)" for planning and
"var(--drp-success)" for non-planning) and apply those tokens in the span's
style or via a small helper/className; update the conditional from background:
c.status === "planning" ? "#E85D04" : "#00AA66" to reference the token names
(with optional fallbacks) and ensure the mapping covers other statuses if
present.
styles/doctorproject/cards.css (1)

86-116: Define base __body / __footer spacing before the mobile overrides.

Right now drp-card__body and drp-card__footer only get padding inside breakpoints, so their default desktop layout is undefined while mobile gets extra spacing. Adding base rules first will make the card API more predictable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@styles/doctorproject/cards.css` around lines 86 - 116, The .drp-card__body
and .drp-card__footer selectors currently only have padding defined inside the
mobile media query, leaving desktop defaults undefined; add base (desktop) rules
for .drp-card__body and .drp-card__footer above the `@media` block (matching the
pattern used for .drp-card__header) so they have sensible default padding (e.g.,
use var(--drp-space-4) and var(--drp-space-3) respectively) and keep the
existing mobile overrides inside `@media` (max-width: 390px) for responsive
adjustments.
app/login/page.tsx (1)

165-181: Password toggle positioning may overlap with Input label.

The toggle button is positioned with bottom: 8 which works if the Input component doesn't have error messages or helper text below it. Consider verifying the toggle aligns correctly with the actual input field rendered by the Input component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/login/page.tsx` around lines 165 - 181, The password-toggle button is
anchored with bottom: 8 which can overlap when the Input component renders a
label or helper/error text; update the wrapper around Input and the toggle
(where Input, showPassword, setShowPassword are used) so the toggle aligns
vertically to the actual input element (e.g., use top: 50% with transform:
translateY(-50%), or make the wrapper a flex container and align-items: center)
and remove the hard bottom offset; ensure the click handler still uses
setShowPassword(!showPassword) and the icon rendering (Eye / EyeOff) remains
unchanged.
components/PostEditorModal.tsx (1)

190-210: Consider migrating hardcoded colors to design tokens.

The read-only metadata section still uses hardcoded values (#888, #f8f8f8, #e0e0e0) instead of design system tokens. For consistency with the rest of the migration, consider using var(--drp-grey), var(--drp-cream), and var(--drp-border-color).

♻️ Suggested token usage
           <div
              style={{
                display: "flex",
                gap: 16,
-               fontSize: 11,
-               color: "#888",
+               fontSize: "var(--drp-text-xs)",
+               color: "var(--drp-grey)",
                marginBottom: 16,
                padding: "6px 10px",
-               background: "#f8f8f8",
-               border: "1px solid `#e0e0e0`",
+               background: "var(--drp-cream)",
+               border: "var(--drp-border)",
              }}
            >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/PostEditorModal.tsx` around lines 190 - 210, Replace the hardcoded
color literals in the read-only metadata block inside PostEditorModal (the
inline style on the surrounding div that currently sets color: "#888",
background: "#f8f8f8", border: "1px solid `#e0e0e0`") with the design token CSS
variables (e.g. color: "var(--drp-grey)", background: "var(--drp-cream)",
border: "1px solid var(--drp-border-color)"); keep the same layout and spacing
values and ensure formatDateTime usage and the conditional rendering logic for
post.createdAt/post.updatedAt remain unchanged.
docs/plans/2026-03-09-ui-revamp-design-system.md (1)

17-41: Add language specifier to fenced code blocks.

The code blocks listing components and CSS classes lack language specifiers, triggering markdownlint warnings. Consider adding text or plaintext to satisfy the linter.

-```
+```text
 Button(variant, size, block, icon)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-09-ui-revamp-design-system.md` around lines 17 - 41, The
fenced code block listing UI components (e.g., Button, Input, Select, Card, Tag,
Badge, StatusDot, Alert, etc.) lacks a language specifier which triggers
markdownlint warnings; update the markdown by adding a language tag such as
"text" or "plaintext" to the opening triple-backtick of the block that contains
the component list so the block becomes ```text (or ```plaintext) to satisfy the
linter and avoid warnings.
__tests__/components/DocumentEditor.test.tsx (1)

217-223: Consider a more robust Alert detection approach.

The selector [class*='alert'] is fragile and may break if the design system changes class naming conventions. Consider using a test-id or role-based query for more stable tests.

♻️ Alternative using role-based query
     await waitFor(() => {
       const feedback = screen.getByText("Saved successfully");
       expect(feedback).toBeInTheDocument();
-      // Feedback is now rendered via <Alert variant="success">
-      const alert =
-        feedback.closest("[class*='alert']") || feedback.parentElement;
-      expect(alert).toBeInTheDocument();
+      // Verify feedback is within an alert element
+      expect(feedback.closest('[role="alert"]')).toBeInTheDocument();
     });

Note: This assumes the Alert component renders with role="alert". Verify the design system's implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/components/DocumentEditor.test.tsx` around lines 217 - 223, The
current test in DocumentEditor.test.tsx locates the rendered success message via
feedback.closest("[class*='alert']"), which is fragile; update the test to use a
stable query: prefer retrieving the alert by role (e.g., getByRole('alert')) or
a data-test-id emitted by the Alert component, falling back to feedback.closest
if necessary. Locate the test block using the variables feedback and alert,
replace the class-based selector with screen.getByRole('alert') or
screen.getByTestId('alert') (matching the Alert implementation), and assert that
this element is in the document.
components/calendar/CalendarView.tsx (2)

22-34: Effect dependency includes state it modifies.

The effect depends on currentDate but also calls setCurrentDate. While the condition check (lines 27-30) prevents infinite loops, this pattern can be confusing. Consider using a ref or adjusting the dependency array.

♻️ Alternative using functional update
   useEffect(() => {
     if (selectedDateFromPicker) {
       const newDate = new Date(selectedDateFromPicker);
-      // Only update if the month/year is different to avoid unnecessary re-renders
-      if (
-        newDate.getFullYear() !== currentDate.getFullYear() ||
-        newDate.getMonth() !== currentDate.getMonth()
-      ) {
-        setCurrentDate(newDate);
-      }
+      setCurrentDate((prev) => {
+        if (
+          newDate.getFullYear() !== prev.getFullYear() ||
+          newDate.getMonth() !== prev.getMonth()
+        ) {
+          return newDate;
+        }
+        return prev;
+      });
     }
-  }, [selectedDateFromPicker, currentDate]);
+  }, [selectedDateFromPicker]);

This removes currentDate from the dependency array by using the functional update pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/calendar/CalendarView.tsx` around lines 22 - 34, The effect
currently lists currentDate in its dependency array while calling
setCurrentDate, which is confusing; update the effect to depend only on
selectedDateFromPicker and use the functional update form of setCurrentDate
inside the effect (e.g., setCurrentDate(prev => { compare
prev.getFullYear()/getMonth() with newDate and return prev or newDate
accordingly }) so you can remove currentDate from the dependency array; locate
the useEffect that references selectedDateFromPicker, currentDate, and
setCurrentDate and apply this change.

97-128: Consider migrating getStatusColorClasses to design tokens.

The component correctly uses drp-* tokens (lines 100, 103, 106, 108, 119), but getStatusColorClasses still uses standard Tailwind color utilities (bg-blue-100, text-blue-800, etc.). The design system already defines semantic status tokens (--drp-success, --drp-error, --drp-warning, --drp-info in styles/doctorproject/tokens.css), but they need to be wired into tailwind.config.ts or the utility function should use CSS custom properties directly to maintain consistency with the design system approach.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/calendar/CalendarView.tsx` around lines 97 - 128,
getStatusColorClasses is still returning Tailwind color utilities (e.g.,
bg-blue-100, text-blue-800) while the rest of CalendarView.tsx uses design
tokens; update this by either (A) wiring the semantic tokens (--drp-success,
--drp-error, --drp-warning, --drp-info) into tailwind.config.ts as custom color
utilities and change getStatusColorClasses to return the matching token-based
classes, or (B) change getStatusColorClasses to output classes that use the CSS
custom properties directly (e.g., bg-[color:var(--drp-success)] /
text-[color:var(--drp-success-contrast)] or inline style objects using
var(--drp-...)) so the status buttons use the design tokens consistently with
CalendarView.tsx. Ensure you update getStatusColorClasses' returned values and
any usage in CalendarView.tsx to the chosen approach so all status styling
references the design tokens.
app/(protected)/brand/page.tsx (1)

283-306: Hardcoded hex colors should use design system tokens.

The audit panel uses hardcoded hex colors (#00AA00, #FF4444, #631DED) instead of design system tokens. Per the migration guidelines, colors should use var(--drp-*) tokens.

♻️ Suggested fix using design tokens
 <div>
-  <strong style={{ color: "#00AA00" }}>Strengths</strong>
+  <strong style={{ color: "var(--drp-success)" }}>Strengths</strong>
   <ul>
     {auditResult.strengths.map((s, i) => (
       <li key={i}>{s}</li>
     ))}
   </ul>
 </div>
 <div>
-  <strong style={{ color: "#FF4444" }}>Gaps</strong>
+  <strong style={{ color: "var(--drp-error)" }}>Gaps</strong>
   <ul>
     {auditResult.gaps.map((g, i) => (
       <li key={i}>{g}</li>
     ))}
   </ul>
 </div>
 <div>
-  <strong style={{ color: "#631DED" }}>Suggestions</strong>
+  <strong style={{ color: "var(--drp-purple)" }}>Suggestions</strong>
   <ul>
     {auditResult.suggestions.map((s, i) => (
       <li key={i}>{s}</li>
     ))}
   </ul>
 </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/brand/page.tsx around lines 283 - 306, Replace the hardcoded
hex color styles used in the audit panel's <strong> elements (the
Strengths/Gaps/Suggestions blocks that reference auditResult.strengths,
auditResult.gaps, auditResult.suggestions) with the design system CSS variables
(var(--drp-*)). Update the inline style objects for each <strong> to use the
appropriate token (e.g., a green token for Strengths, a red token for Gaps, and
a purple token for Suggestions) such as var(--drp-green-500),
var(--drp-red-500), var(--drp-purple-500) or the equivalent tokens in our design
system.
docs/plans/2026-03-05-brand-coherent-recommendations-design.md (1)

53-64: Add language specifier to fenced code block.

Static analysis flagged this code block as missing a language identifier. Adding one improves syntax highlighting and lint compliance.

📝 Suggested fix
-```
+```text
 You are a LinkedIn content strategist for this specific professional:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-05-brand-coherent-recommendations-design.md` around lines
53 - 64, The fenced code block in the plan lacks a language specifier; update
the block start from ``` to include a language (e.g., ```text) so linters and
syntax highlighters recognize it; locate the block containing the LinkedIn
content strategist prompt (the triple-backtick block starting with "You are a
LinkedIn content strategist...") and change its opening fence to include the
chosen language identifier.
app/(protected)/calendar/page.tsx (1)

240-254: Fragile class string manipulation for legend colors.

The chained .split(" ")[0].replace("bg-", "border-") pattern is brittle—it assumes getStatusColorClasses always returns a space-separated string with bg-* as the first class. If the utility's output format changes, this will silently break.

Consider extracting a dedicated helper that returns both background and border classes explicitly.

♻️ Suggested approach
// In calendarUtils.ts
export function getStatusColorTokens(status: PostStatus) {
  const map: Record<PostStatus, { bg: string; border: string }> = {
    draft: { bg: "bg-gray-200", border: "border-gray-200" },
    // ... other mappings
  };
  return map[status] ?? { bg: "bg-gray-200", border: "border-gray-200" };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/calendar/page.tsx around lines 240 - 254, The legend is
using brittle string manipulation on getStatusColorClasses; replace this with a
stable helper that returns explicit tokens (e.g., add getStatusColorTokens in
calendarUtils.ts that maps PostStatus to { bg, border }), update the rendering
to use those tokens for the span classes instead of parsing
getStatusColorClasses, and leave getStatusColorClasses for existing uses or
refactor callers to the new helper so statusOptions rendering uses token.bg and
token.border directly.
app/(protected)/create/page.tsx (2)

10-10: Misleading alias: Loader as BruLoader.

The alias BruLoader implies Bruddle, but this component is imported from @doctorproject/react. This contradicts the migration intent and will confuse future maintainers.

♻️ Rename the alias
 import {
   Alert,
   Button,
   Card,
   Input,
   Textarea,
-  Loader as BruLoader,
+  Loader as DrpLoader,
 } from "@doctorproject/react";

Then update usages at lines 370 and elsewhere.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/create/page.tsx at line 10, The import alias "Loader as
BruLoader" is misleading because the component actually comes from
"@doctorproject/react"; change the alias to a clear, accurate name (e.g.,
"Loader" or "DoctorLoader") in the import statement where "Loader" is currently
aliased to "BruLoader" and update all usages of "BruLoader" (including the
component rendered around the current usage at line ~370) to the new identifier
to keep names consistent with the source module.

39-40: Update comment to reflect new library name.

The comment references "BruLoader" which perpetuates the old naming. Align the comment with the renamed import.

📝 Suggested comment update
-// Loader from lucide-react used only for inline icon buttons (search spinner, generate button)
-// BruLoader from `@doctorproject/react` used for full loading states
+// Loader from lucide-react used only for inline icon buttons (search spinner, generate button)
+// DrpLoader from `@doctorproject/react` used for full loading states
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/create/page.tsx around lines 39 - 40, The file comment still
mentions the old "BruLoader" name; update that comment to the current component
name used from `@doctorproject/react` (match the actual import in this file) so it
correctly reads something like "Loader from lucide-react used only for inline
icon buttons (search spinner, generate button) and <CURRENT_LOADER_NAME> from
`@doctorproject/react` used for full loading states" — replace "BruLoader" with
the real exported symbol the code imports here (the same identifier used in the
import statement).
styles/doctorproject/charts.css (1)

145-147: Misleading modifier name: --round sets border-radius: 0.

The class .drp-chart-legend__dot--round suggests a circular dot, but it sets border-radius: 0, making it square. This is likely a copy-paste oversight from the migration or intentional brutalist design—either way, the name is confusing.

♻️ Options to resolve

Option A: If round dots are needed, fix the value:

 .drp-chart-legend__dot--round {
-  border-radius: 0;
+  border-radius: 50%;
 }

Option B: If square is intentional (brutalist), rename the modifier:

-.drp-chart-legend__dot--round {
+.drp-chart-legend__dot--square {
   border-radius: 0;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@styles/doctorproject/charts.css` around lines 145 - 147, The modifier name
.drp-chart-legend__dot--round is misleading because it sets border-radius: 0;
either make it actually round or rename it to reflect square/brutalist styling:
change the rule in styles/doctorproject/charts.css for
.drp-chart-legend__dot--round to use border-radius: 50% (or an appropriate
radiused value) if round dots are intended, or rename the class (e.g.,
.drp-chart-legend__dot--square or --brutalist) and update all usages to match if
square dots are intentional, and remove or repurpose the old
.drp-chart-legend__dot--round symbol to avoid confusion.
styles/doctorproject/animations.css (1)

111-121: Inconsistent duration/easing tokens in animation utilities.

drp-anim-bounce and drp-anim-pulse use hardcoded values (1s ease infinite, 2s ease infinite), while other utilities reference design tokens (var(--drp-duration-fast), var(--drp-ease)). Consider using tokens for consistency, or document why these animations require specific fixed values.

♻️ Token-based alternative
 .drp-anim-bounce {
-  animation: drp-bounce 1s ease infinite;
+  animation: drp-bounce var(--drp-duration-slow, 1s) var(--drp-ease) infinite;
 }

 .drp-anim-pulse {
-  animation: drp-pulse 2s ease infinite;
+  animation: drp-pulse var(--drp-duration-slower, 2s) var(--drp-ease) infinite;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@styles/doctorproject/animations.css` around lines 111 - 121, .dr
p-anim-bounce and .drp-anim-pulse use hardcoded durations/easing which is
inconsistent with .drp-anim-shake that uses tokens; update .drp-anim-bounce and
.drp-anim-pulse to reference the design tokens (e.g., var(--drp-duration-fast)
or a new token like --drp-duration-medium and var(--drp-ease)) and add sensible
CSS fallback values if needed, or alternatively add a short
comment/documentation explaining why those two animations require fixed values;
ensure the token names exist in the tokens file or create them (reference
classes: .drp-anim-bounce, .drp-anim-pulse, and existing .drp-anim-shake and
token names like --drp-duration-fast and --drp-ease).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 132a1f56-0a35-4b0f-a379-8aaccb06f537

📥 Commits

Reviewing files that changed from the base of the PR and between bff0226 and 6721ade.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (123)
  • .github/copilot-instructions.md
  • .vexp/manifest.json
  • .vexp/mcp.port
  • __tests__/components/DocumentEditor.test.tsx
  • __tests__/components/FormattedOutput.test.tsx
  • __tests__/components/ImportFlow.test.tsx
  • __tests__/components/PipelineStepper.test.tsx
  • __tests__/components/PostEditorModal.test.tsx
  • __tests__/components/ResearchBrief.test.tsx
  • app/(onboarding)/layout.tsx
  • app/(onboarding)/onboarding/review/page.tsx
  • app/(onboarding)/onboarding/start/page.tsx
  • app/(onboarding)/onboarding/upload/page.tsx
  • app/(onboarding)/onboarding/wizard/[step]/page.tsx
  • app/(protected)/analytics/page.tsx
  • app/(protected)/brand/page.tsx
  • app/(protected)/calendar/page.tsx
  • app/(protected)/campaigns/[campaignId]/idea/[slotOrder]/page.tsx
  • app/(protected)/campaigns/[campaignId]/page.tsx
  • app/(protected)/campaigns/page.tsx
  • app/(protected)/create/page.tsx
  • app/(protected)/dashboard/page.tsx
  • app/(protected)/factory/page.tsx
  • app/(protected)/knowledge/page.tsx
  • app/(protected)/layout.tsx
  • app/(protected)/learning/page.tsx
  • app/(protected)/library/[id]/page.tsx
  • app/(protected)/library/page.tsx
  • app/(protected)/settings/page.tsx
  • app/(protected)/settings/profile/page.tsx
  • app/(protected)/settings/subscription/page.tsx
  • app/(protected)/studio/page.tsx
  • app/auth/callback/page.tsx
  • app/forgot-password/page.tsx
  • app/globals.css
  • app/layout.tsx
  • app/login/page.tsx
  • app/reset-password/page.tsx
  • app/signup/page.tsx
  • components/ConfirmDialog.tsx
  • components/EnhancedDropdown.tsx
  • components/Header.tsx
  • components/PostEditorModal.tsx
  • components/PostGenerator.tsx
  • components/PostGoalSelector.tsx
  • components/SchedulePostModal.tsx
  • components/Toast.tsx
  • components/TonePromptPreviewModal.tsx
  • components/brand/BrandSection.tsx
  • components/brand/sections/AiToolsSection.tsx
  • components/brand/sections/OffersSection.tsx
  • components/brand/sections/PillarsSection.tsx
  • components/brand/sections/PositioningSection.tsx
  • components/brand/sections/ProfileSection.tsx
  • components/brand/sections/StrategySection.tsx
  • components/brand/sections/VoiceSection.tsx
  • components/calendar/CalendarView.tsx
  • components/calendar/ScoreBadge.tsx
  • components/campaigns/BatchProgress.tsx
  • components/campaigns/CampaignAnalytics.tsx
  • components/campaigns/CampaignCalendar.tsx
  • components/campaigns/CampaignFilters.tsx
  • components/campaigns/CampaignList.tsx
  • components/campaigns/CampaignSetup.tsx
  • components/campaigns/CampaignSummaryRow.tsx
  • components/campaigns/IdeaInbox.tsx
  • components/campaigns/IdeaStatusBadge.tsx
  • components/create/ContentAngleChips.tsx
  • components/create/PostStructureCards.tsx
  • components/factory/CreatePostDialog.tsx
  • components/factory/DraftEditor.tsx
  • components/factory/EvidencePack.tsx
  • components/factory/FormattedOutput.tsx
  • components/factory/GuardrailRecovery.tsx
  • components/factory/LearningPhaseResult.tsx
  • components/factory/PipelineStepper.tsx
  • components/factory/PostReview.tsx
  • components/factory/ResearchBrief.tsx
  • components/factory/RewriteInstructions.tsx
  • components/factory/Scorecard.tsx
  • components/factory/SessionHistory.tsx
  • components/factory/TopicProposals.tsx
  • components/knowledge/DocumentEditor.tsx
  • components/knowledge/ExtractFlow.tsx
  • components/knowledge/ImportFlow.tsx
  • components/knowledge/VersionHistory.tsx
  • components/learning/FeedbackHistory.tsx
  • components/learning/PatternList.tsx
  • components/learning/RuleProposal.tsx
  • components/learning/SignalCounts.tsx
  • components/settings/StraicoModelPicker.tsx
  • docs/plans/2026-03-05-brand-coherent-recommendations-design.md
  • docs/plans/2026-03-05-brand-coherent-recommendations.md
  • docs/plans/2026-03-05-brand-page.md
  • docs/plans/2026-03-05-full-spec-implementation.md
  • docs/plans/2026-03-07-campaign-idea-lifecycle.md
  • docs/plans/2026-03-08-create-redesign.md
  • docs/plans/2026-03-09-doctorproject-react-migration.md
  • docs/plans/2026-03-09-ui-revamp-design-system.md
  • package.json
  • styles/bruddle/cards.css
  • styles/bruddle/tables.css
  • styles/bruddle/tags.css
  • styles/bruddle/tokens.css
  • styles/bruddle/utilities.css
  • styles/doctorproject/animations.css
  • styles/doctorproject/buttons.css
  • styles/doctorproject/cards.css
  • styles/doctorproject/charts.css
  • styles/doctorproject/composites.css
  • styles/doctorproject/forms.css
  • styles/doctorproject/index.css
  • styles/doctorproject/layout.css
  • styles/doctorproject/media.css
  • styles/doctorproject/modals.css
  • styles/doctorproject/pagination.css
  • styles/doctorproject/responsive.css
  • styles/doctorproject/tables.css
  • styles/doctorproject/tags.css
  • styles/doctorproject/tokens.css
  • styles/doctorproject/typography.css
  • styles/doctorproject/utilities.css
  • tailwind.config.ts
💤 Files with no reviewable changes (5)
  • styles/bruddle/utilities.css
  • styles/bruddle/tables.css
  • styles/bruddle/tokens.css
  • styles/bruddle/tags.css
  • styles/bruddle/cards.css

Comment on lines +247 to +253
"indexed_at_commit": "7d60622114f6e30caabc11d63204d8c75d3a42e4",
"indexed_at_timestamp": "2026-03-09T20:18:37.929056+00:00",
"schema_version": 3,
"stats": {
"total_edges": 479,
"total_files": 171,
"total_nodes": 610
"total_edges": 678,
"total_files": 243,
"total_nodes": 898
Copy link
Copy Markdown

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:

#!/bin/bash
set -euo pipefail

python - <<'PY'
import json
import pathlib
import subprocess

manifest = json.loads(pathlib.Path('.vexp/manifest.json').read_text())
head = subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip()
tracked = len(manifest['file_hashes'])
total_files = manifest['stats']['total_files']

print('indexed_at_commit   :', manifest['indexed_at_commit'])
print('git HEAD            :', head)
print('head matches        :', manifest['indexed_at_commit'] == head)
print('indexed_at_timestamp:', manifest['indexed_at_timestamp'])
print('tracked file hashes :', tracked)
print('stats.total_files   :', total_files)
print('count matches       :', tracked == total_files)
PY

Repository: drproject-coding/doctorpost-v12

Length of output: 362


Regenerate the manifest from the final PR head.

The manifest was indexed at commit 7d60622... but the current HEAD is 6721ade.... All file hashes and aggregate stats are stale. Rerun the manifest generator on the final tree to ensure the indexed data reflects the actual PR state.

Internal consistency check passed (243 tracked files match stats.total_files), but the commit mismatch must be resolved.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.vexp/manifest.json around lines 247 - 253, The manifest's indexed_at_commit
and aggregate stats are stale—regenerate the manifest from the final PR head
(current HEAD 6721ade...) so indexed_at_commit, indexed_at_timestamp, and the
"stats" block (total_edges, total_files, total_nodes, and all file hashes)
reflect the actual PR tree; run the repository's manifest generator on the final
tree/HEAD, replace the .vexp/manifest.json fields indexed_at_commit and
indexed_at_timestamp with the new commit hash and timestamp, and ensure the
"stats" section is updated to match the regenerated file list and hashes.

border: `2px dashed ${isDragging ? "var(--drp-purple)" : "var(--drp-grey, #999)"}`,
background: isDragging
? "rgba(var(--bru-purple-rgb, 100, 50, 200), 0.04)"
? "rgba(var(--drp-purple-rgb, 100, 50, 200), 0.04)"
Copy link
Copy Markdown

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:

#!/bin/bash
# Search for the definition of --drp-purple-rgb in CSS files
rg -n '\-\-drp-purple-rgb' --type css

Repository: drproject-coding/doctorpost-v12

Length of output: 57


🏁 Script executed:

# Search for --drp-purple-rgb in all files (not just CSS)
rg -n '\-\-drp-purple-rgb'

Repository: drproject-coding/doctorpost-v12

Length of output: 190


🏁 Script executed:

# Also check the context around line 296 in the file to understand the usage better
cat -n "app/(onboarding)/onboarding/upload/page.tsx" | sed -n '290,305p'

Repository: drproject-coding/doctorpost-v12

Length of output: 849


🏁 Script executed:

# Search for other --drp- variables to understand the naming pattern and where they might be defined
rg -n '\-\-drp-' | head -20

Repository: drproject-coding/doctorpost-v12

Length of output: 1838


🏁 Script executed:

# Search for :root or html selectors where CSS variables are typically defined
rg -n '(:root|--drp-)' --type css -A 5 | head -60

Repository: drproject-coding/doctorpost-v12

Length of output: 4017


🏁 Script executed:

# Look for global CSS files that might define theme variables
fd -e css -e scss | xargs grep -l '\-\-drp-purple' 2>/dev/null

Repository: drproject-coding/doctorpost-v12

Length of output: 517


🏁 Script executed:

# Search for --drp-purple definition specifically
rg -n '\-\-drp-purple[^-]' --type css

Repository: drproject-coding/doctorpost-v12

Length of output: 5100


🏁 Script executed:

cat -n styles/doctorproject/tokens.css

Repository: drproject-coding/doctorpost-v12

Length of output: 9995


Add --drp-purple-rgb to the design system tokens or use an alternative approach.

The CSS variable --drp-purple-rgb is not defined in styles/doctorproject/tokens.css. The fallback value 100, 50, 200 will always be used, resulting in incorrect color rendering (it should be 99, 29, 237 to match --drp-purple: #631DED``). Either add --drp-purple-rgb: 99, 29, 237; to the tokens file, use an existing opacity variant like `--drp-purple-20`, or directly use `rgba(`#631DED`, 0.04)`.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(onboarding)/onboarding/upload/page.tsx at line 296, The CSS variable
--drp-purple-rgb used in onboarding/upload/page.tsx currently falls back to
100,50,200; add --drp-purple-rgb: 99, 29, 237; to your design tokens
(styles/doctorproject/tokens.css) so rgba(var(--drp-purple-rgb), 0.04) renders
correctly, or alternatively change the usage in onboarding/upload/page.tsx to
reference an existing token like --drp-purple-20 or use an explicit
rgba(`#631DED`, 0.04) value; update whichever you choose and ensure the variable
name (--drp-purple-rgb) and the component using it are consistent.

Comment on lines +292 to +303
<EmptyState
icon="📅"
title="No upcoming posts"
description="Create and schedule a post to see it here."
action={
<Link href="/create">
<Button variant="primary" size="sm">
Create Post
</Button>
</Link>
}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Render the empty-state action as one control.

Wrapping a button in a link creates nested interactive elements, which is invalid HTML and tends to break keyboard/screen-reader navigation.

Proposed fix
-                  action={
-                    <Link href="/create">
-                      <Button variant="primary" size="sm">
-                        Create Post
-                      </Button>
-                    </Link>
-                  }
+                  action={
+                    <Link
+                      href="/create"
+                      className="drp-btn drp-btn--sm drp-btn--primary"
+                      style={{ textDecoration: "none" }}
+                    >
+                      Create Post
+                    </Link>
+                  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/dashboard/page.tsx around lines 292 - 303, The EmptyState
action currently nests a Button inside a Link (EmptyState action prop using Link
+ Button), which creates nested interactive elements; fix by rendering a single
interactive control: either render the Link as the action and style it to look
like the Button (use the Link component as the clickable control passed to
EmptyState's action) or render the Button as the control and render the Link
inside it via the Button's "asChild" / passthrough prop if your Button supports
it—update the EmptyState action so only one interactive element (use EmptyState,
Link, and Button identifiers to locate the spot) is rendered.

Comment on lines +117 to +118
<Button onClick={fetchData} disabled={loading} variant="outline">
{loading ? <Loader size="sm" label="Loading..." /> : "Refresh"}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep the refresh action recoverable while loading.

This disables the only retry control, but the requests have no timeout or abort path. If either call stalls, the page is stuck with a spinner and no in-app way to recover. Either keep the button usable with request dedupe/abort, or bound the fetch duration before disabling it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/learning/page.tsx around lines 117 - 118, The Refresh button
currently disables during loading (Button onClick tied to fetchData and loading
state) which makes recovery impossible if requests stall; modify fetchData to
use an AbortController stored in a ref (e.g., currentAbortRef) and pass its
signal into any fetch/XHR calls, add a bounded timeout (e.g., setTimeout that
aborts after ~15s) as a fallback, and change the Button onClick so when clicked
while loading it aborts the current controller and immediately creates a new
AbortController to restart fetchData; ensure loading state is set/cleared around
the new request and that the Loader UI remains while allowing the user to retry
by clicking the button to trigger abort+restart.

Comment on lines +80 to +87
```
User clicks Generate
→ POST /api/studio/strategist (SSE)
→ POST /api/studio/writer (SSE)
→ POST /api/studio/scorer (SSE)
→ POST /api/studio/formatter (SSE)
→ Save to content_history
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a language to this fenced block.

markdownlint is already flagging the anonymous fence here. Use text (or another appropriate lexer) to keep docs lint clean.

Suggested fix
-```
+```text
 User clicks Generate
   → POST /api/studio/strategist (SSE)
   → POST /api/studio/writer (SSE)
   → POST /api/studio/scorer (SSE)
   → POST /api/studio/formatter (SSE)
   → Save to content_history
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-05-full-spec-implementation.md` around lines 80 - 87, Add
a language tag to the anonymous fenced code block so markdownlint stops flagging
it: change the triple-backtick fence around the sequence starting with "User
clicks Generate" to use an explicit lexer such as ```text (e.g., replace ```
with ```text) so the block is treated as plain text; update the fenced block
that contains the POST /api/studio/... lines accordingly.

Comment on lines +19 to +27
- Status system for campaign ideas (waiting_review → validated / rejected → in_progress → published)
- Campaign list page — load existing campaigns from DB, not just current session
- Campaign calendar color-coded by status
- Filter bar (by status, by pillar) + bulk actions (validate all, reject all)
- Summary row showing idea counts per status
- Idea detail full page (`/campaigns/[campaignId]/idea/[slotOrder]`) — view, edit, change status
- "Write this post" button on validated ideas → navigates to `/create` with topic card pre-filled
- Validated ideas inbox on Create page — pick a validated idea to pre-fill the form
- Campaign analytics — idea funnel (total generated → reviewed → validated → written → published)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Define the state machine and funnel derivations explicitly.

The plan lists a lifecycle, but it never specifies which transitions are valid or how funnel stages like reviewed and written are computed. If updateCampaignPostStatus stays generic, one screen can treat reviewed = validated + rejected while another uses a different rule, and invalid jumps like waiting_review -> published can slip through.

Suggested plan adjustment
 ## Data Model Changes
+
+### Transition rules
+
+- `waiting_review` -> `validated` | `rejected`
+- `validated` -> `in_progress`
+- `in_progress` -> `published`
+- Disallow direct jumps unless explicitly documented/admin-only
+
+### Analytics definitions
+
+- `reviewed` = `validated` + `rejected`
+- `written` = `in_progress` + `published` (or `post_uuid IS NOT NULL` if that is the source of truth)

Also applies to: 95-97, 121-121

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-07-campaign-idea-lifecycle.md` around lines 19 - 27, The
lifecycle description lacks an explicit state machine and deterministic funnel
derivation rules, so define an authoritative enum of statuses (e.g.,
waiting_review, validated, rejected, in_progress, written, published) and a
transition matrix listing allowed transitions (disallow direct jumps like
waiting_review -> published); update the central function
updateCampaignPostStatus to validate transitions against that matrix and return
errors on invalid moves; additionally, add canonical funnel derivation functions
(e.g., computeReviewedCount, computeWrittenCount) that derive stages from status
membership (document rules such as reviewed := validated + rejected, written :=
in_progress + written or whatever the agreed rule is) and use those functions
consistently in the campaign list, analytics, and Create page so every screen
uses the same definitions.

Comment on lines +67 to +70
### Migration

- All existing rows with `generation_status = 'pending'` → rename semantically (handled in NCB via execute_sql)
- Add `post_uuid` column
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add an explicit status backfill/migration task.

This plan renames pending to waiting_review, but Task 2 only adds post_uuid. Without a concrete DB backfill/update for existing generation_status values, legacy rows will fall outside the new status model and break filters, badges, and analytics.

Suggested plan adjustment
-| 2   | Add post_uuid column via NCB execute_sql                                                       | NCB migration                                                      | —     | 1     |
+| 2   | Add post_uuid column and backfill `generation_status` (`pending` -> `waiting_review`) via NCB execute_sql | NCB migration                                           | —     | 1     |

Also call out any enum/check-constraint updates in the migration notes if the column is constrained.

Also applies to: 107-107

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-07-campaign-idea-lifecycle.md` around lines 67 - 70, Add
an explicit DB migration step that updates existing rows where generation_status
= 'pending' to the new value 'waiting_review' (in the same migration that adds
post_uuid), e.g., run an UPDATE using the NCB execute_sql path so legacy rows
are included; also update any enum/check-constraint definitions for
generation_status to include 'waiting_review' (and remove 'pending' if desired)
and note these enum changes in the migration notes so filters, badges, and
analytics continue to work.

Comment on lines +94 to +95
- `app/(protected)/create/page.tsx` — add idea inbox, handle `?topicCard=` URL params
- `lib/knowledge/api.ts` — add `updateCampaignPostStatus`, `getValidatedIdeas`, `linkPostToCampaignSlot`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Pass an idea identifier, not the full topic card, through the URL.

Using ?topicCard=... to carry the prefill payload is fragile and leaks idea content into browser history, logs, and referrers. Prefer /create?campaignId=...&slotOrder=... or a dedicated idea ID/post UUID, then load the card data on the Create page.

Suggested plan adjustment
-- `app/(protected)/create/page.tsx` — add idea inbox, handle `?topicCard=` URL params
+- `app/(protected)/create/page.tsx` — add idea inbox, handle idea identifiers in URL and hydrate prefill data from DB/API
-- [ ] "Write this post" navigates to `/create?topicCard=...` with correct pre-fill
+- [ ] "Write this post" navigates to `/create` with campaign/idea identifiers, and the page hydrates the correct pre-fill data

Also applies to: 155-155

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-07-campaign-idea-lifecycle.md` around lines 94 - 95, The
Create page currently passes the entire prefill payload via the topicCard URL
param; change this to pass a stable identifier instead (e.g., use
/create?campaignId=...&slotOrder=... or /create?ideaId=POST_UUID) by updating
app/(protected)/create/page.tsx to stop reading/parsing query param topicCard
and instead accept campaignId/slotOrder or ideaId and fetch the topic card
server- or client-side; add or reuse an API helper in lib/knowledge/api.ts
(e.g., getValidatedIdeas or a new getTopicCardById) to load the card by id, and
ensure linkPostToCampaignSlot/updateCampaignPostStatus callers are adapted to
work with the id-based flow so no idea content is embedded in browser history,
logs, or referrers.

Comment on lines +1 to +9
# UI Revamp — Bruddle Design System Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Systematically replace all raw/inconsistent UI patterns across every page with components and CSS classes from the Bruddle Design System, achieving a fully consistent, on-brand UI.

**Architecture:** All UI must come exclusively from `@bruddle/react` components or `bru-*` CSS classes from `styles/bruddle/`. No new components or custom UI patterns are allowed. Tailwind color utilities (`text-gray-*`, `text-green-*`, etc.) must be replaced with `--bru-*` CSS variables. All pages are in `app/(protected)/` except auth pages.

**Tech Stack:** Next.js 16 (App Router), `@bruddle/react` (Button, Input, Select, Textarea, Card, Tag, Badge, Alert, EmptyState, Loader, Skeleton, Tabs, Pagination, Modal, ProgressBar, StatusDot, Switch, Checkbox, Divider, Tooltip), Bruddle CSS classes (`bru-btn`, `bru-card`, `bru-input`, `bru-field`, etc.), TypeScript
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Outdated design system references in planning document.

This planning document references @bruddle/react and bru-* tokens throughout, but the PR objectives indicate the migration is moving TO @doctorproject/react with drp-* tokens. This creates confusion:

  • Line 7: References --bru-* CSS variables
  • Line 9: References @bruddle/react components

If this document is intended as historical context, consider adding a note at the top clarifying it's superseded by the DoctorProject migration. Otherwise, update the references to align with the new design system.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-09-ui-revamp-design-system.md` around lines 1 - 9, The
document incorrectly references the old Bruddle design system (e.g.,
`@bruddle/react`, bru-* classes, and --bru-* CSS variables); either prepend a
clear “superseded/historical” note stating this file documents the
Bruddle-to-DoctorProject migration, or update all references to the new design
system by replacing `@bruddle/react` with `@doctorproject/react`, bru-* CSS class
tokens with drp-* equivalents, and --bru-* CSS variables with --drp-* throughout
the plan (search for symbols like "@bruddle/react", "bru-", and "--bru-" and
update or add the superseded note accordingly).

…stem

- Migrate all onboarding pages (layout, start, wizard, review, upload)
- Migrate remaining components: ConfirmDialog, PostGoalSelector, Toast,
  CampaignFilters, CampaignSummaryRow, ContentAngleChips, PostStructureCards,
  LearningPhaseResult, GuardrailRecovery, PipelineStepper, RewriteInstructions,
  EvidencePack, StraicoModelPicker, EnhancedDropdown
- Replace all raw <button>/<input>/<select>/<textarea> with DS equivalents
- All DS component props corrected (Select children, Alert no style prop,
  Loader size sm|lg only, Badge valid variants, ProgressBar no max/style,
  Button iconLeft/iconRight, no variant="mint")
- Zero TypeScript errors
- Fix mock package name from @bruddle/react to @doctorproject/react
- Add missing DS component mocks (Badge, Select, Loader, ProgressBar)
- Update PostEditorModal test selector to use aria-label instead of class
- Update CampaignCalendar assertions for DS Button elements
Copy link
Copy Markdown

@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: 10

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (12)
components/brand/sections/StrategySection.tsx (1)

16-31: ⚠️ Potential issue | 🟠 Major

Associate each label with its textarea.

The <label> rendered by FieldLabel is visually correct, but it is not bound to either Textarea, so assistive tech will not announce a proper field name and clicking the label will not focus the control. This makes the edit form harder to complete for keyboard/screen-reader users.

♿ Proposed fix
-const FieldLabel: React.FC<{ children: React.ReactNode }> = ({ children }) => (
+const FieldLabel: React.FC<{
+  children: React.ReactNode;
+  htmlFor?: string;
+}> = ({ children, htmlFor }) => (
   <label
+    htmlFor={htmlFor}
     className="drp-field__label"
     style={{
       display: "block",
       fontFamily: "var(--drp-font-primary)",
@@
-        <FieldLabel>Content Strategy</FieldLabel>
+        <FieldLabel htmlFor={editing ? "content-strategy" : undefined}>
+          Content Strategy
+        </FieldLabel>
         {editing ? (
           <>
             <Textarea
+              id="content-strategy"
               rows={4}
               maxLength={MAX_STRATEGY}
               value={profile.contentStrategy}
               onChange={(e) => onChange({ contentStrategy: e.target.value })}
@@
-        <FieldLabel>Brand Definition</FieldLabel>
+        <FieldLabel htmlFor={editing ? "brand-definition" : undefined}>
+          Brand Definition
+        </FieldLabel>
         {editing ? (
           <>
             <Textarea
+              id="brand-definition"
               rows={3}
               maxLength={MAX_DEFINITION}
               value={profile.definition}
               onChange={(e) => onChange({ definition: e.target.value })}

Also applies to: 93-107, 120-134

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brand/sections/StrategySection.tsx` around lines 16 - 31, The
FieldLabel component is not associated with its textarea, so update FieldLabel
to accept an optional htmlFor or id prop and render it as the label's htmlFor
attribute (e.g., change FieldLabel signature to accept { children, htmlFor?:
string } and add htmlFor={htmlFor} to the <label>), then ensure each Textarea
usage in this file (the textarea instances referenced around the blocks
currently at lines ~16 and the other occurrences near the 93-107 and 120-134
ranges) is given a matching id prop so the label's htmlFor matches the textarea
id; this will bind the label to the control and restore keyboard/screen-reader
focus behavior.
components/factory/LearningPhaseResult.tsx (1)

50-100: ⚠️ Potential issue | 🟡 Minor

Add aria-expanded attribute for accessibility.

The Button component with variant="ghost" is appropriately used as a disclosure toggle here. The style overrides and complex nested children are standard for this pattern. However, add the aria-expanded attribute to properly communicate the expanded/collapsed state to assistive technologies:

<Button
  variant="ghost"
  aria-expanded={expanded}
  onClick={() => setExpanded(!expanded)}
  style={{
    // ... existing styles
  }}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/factory/LearningPhaseResult.tsx` around lines 50 - 100, Add the
aria-expanded attribute to the disclosure Button used to toggle the
learning-phase panel to expose state to assistive tech: update the Button (the
element that uses variant="ghost", onClick={() => setExpanded(!expanded)}) to
include aria-expanded={expanded} so the Button reflects the current expanded
boolean managed by setExpanded and matches the visual toggling between ChevronUp
and ChevronDown.
app/(protected)/settings/profile/page.tsx (1)

233-253: ⚠️ Potential issue | 🟡 Minor

Clear the previous success timer before showing Saved again.

The button now reflects saved, but the timeout that resets it is still fire-and-forget. If the user saves twice within 3 seconds, the first timer can clear the second success state early.

Suggested fix
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
@@
 export default function SettingsProfilePage() {
   const { user } = useAuth();
+  const savedResetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
+    null
+  );
@@
+  useEffect(() => {
+    return () => {
+      if (savedResetTimeoutRef.current) {
+        clearTimeout(savedResetTimeoutRef.current);
+      }
+    };
+  }, []);
+
   const handleSave = async () => {
@@
-      setSaved(true);
-      setTimeout(() => setSaved(false), 3000);
+      setSaved(true);
+      if (savedResetTimeoutRef.current) {
+        clearTimeout(savedResetTimeoutRef.current);
+      }
+      savedResetTimeoutRef.current = setTimeout(() => {
+        setSaved(false);
+        savedResetTimeoutRef.current = null;
+      }, 3000);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/settings/profile/page.tsx around lines 233 - 253, The
success timeout that clears the saved state must be cleared before starting a
new one to avoid an earlier timer wiping out a later "Saved" state; update the
save flow in handleSave to store the timeout id (e.g., a useRef like
savedTimeoutRef), call clearTimeout(savedTimeoutRef.current) before creating a
new setTimeout that resets saved, assign the new id to savedTimeoutRef.current,
and also clear that timeout on component unmount; adjust references around
saved/saving/displayName/user?.name and the Button render accordingly.
components/settings/StraicoModelPicker.tsx (2)

197-206: ⚠️ Potential issue | 🟡 Minor

Let clicks pass through the search icon.

The absolutely positioned icon sits above the input with zIndex: 1. Without disabling pointer events, clicks on the left side can hit the icon instead of focusing the field.

Suggested fix
           <Search
+            aria-hidden="true"
             size={14}
             style={{
               position: "absolute",
               left: 10,
               top: "50%",
               transform: "translateY(-50%)",
               color: "var(--drp-grey)",
               zIndex: 1,
+              pointerEvents: "none",
             }}
           />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/settings/StraicoModelPicker.tsx` around lines 197 - 206, The
Search icon overlay in StraicoModelPicker (the <Search /> element with the
inline style block) is intercepting clicks; update its style to include
pointerEvents: "none" so pointer events pass through to the underlying input
(preserving position, color, zIndex, and transform) and ensure the input element
(the text field inside StraicoModelPicker) still receives focus when users click
the left side.

208-255: ⚠️ Potential issue | 🟠 Major

Add accessible names to the search and sort controls.

Line 208 and Line 245 render unlabeled form controls. Placeholder text and option text do not give these controls a reliable accessible name, so screen-reader users cannot tell which field searches vs. sorts.

Suggested fix
           <Input
+            aria-label="Search models"
             type="text"
             value={search}
             onChange={(e) => setSearch(e.target.value)}
             placeholder="Search models..."
             style={{ width: "100%", paddingLeft: 32, fontSize: 12 }}
           />
@@
         <Select
+          aria-label="Sort models"
           value={sort}
           onChange={(e) => setSort(e.target.value as SortOption)}
           style={{ width: "100%", fontSize: 12 }}
         >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/settings/StraicoModelPicker.tsx` around lines 208 - 255, The
search Input and the Sort Select in StraicoModelPicker.tsx lack accessible
names; update the Input (value={search} onChange setSearch) and the Select
(value={sort} onChange setSort) to include accessible labels by either adding an
associated <label htmlFor="...">Search</label> and <label
htmlFor="...">Sort</label> with matching id attributes, or by adding clear
aria-label attributes (e.g., aria-label="Search models" and aria-label="Sort
models") to those elements, ensuring the ids/labels match and screen readers
receive a reliable name.
components/knowledge/ExtractFlow.tsx (1)

124-134: ⚠️ Potential issue | 🟠 Major

Add an accessible name to the icon-only back button.

This renders as an unlabeled control for screen-reader users. That matters most in the loading states, where it's the only obvious way to leave the flow.

Suggested fix
-        <Button variant="ghost" onClick={onCancel}>
+        <Button
+          variant="ghost"
+          onClick={onCancel}
+          aria-label="Cancel extract flow"
+        >
           <ArrowLeft size={16} />
         </Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/knowledge/ExtractFlow.tsx` around lines 124 - 134, The back button
in ExtractFlow.tsx is an icon-only Button using onClick={onCancel} with
<ArrowLeft />, which lacks an accessible name; add an aria-label (e.g.,
aria-label="Back" or aria-label="Go back") or include visually-hidden text
inside the Button so screen readers can identify it (ensure the accessible name
is present even during loading states and update the Button JSX where onCancel
and ArrowLeft are used).
app/(protected)/settings/subscription/page.tsx (1)

115-121: ⚠️ Potential issue | 🟠 Major

Preserve the unlimited sentinel for the power tier.

TIER_LIMITS.power is null, but Line 116 turns it into 5 with ?? 5. That makes the UI treat unlimited users as capped, so this branch renders a progress bar and the counter above shows / 5 instead of (unlimited).

Proposed fix
-  const limit = TIER_LIMITS[tier] ?? 5;
+  const limit = TIER_LIMITS[tier] === undefined ? 5 : TIER_LIMITS[tier];

Also applies to: 273-276

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/settings/subscription/page.tsx around lines 115 - 121, The
code incorrectly replaces the `null` unlimited sentinel for the "power" tier by
using `?? 5`; update the `limit` assignment to preserve null (e.g., `const limit
= TIER_LIMITS[tier]` or `TIER_LIMITS[tier] ?? null`) and change the percent
calculation to only compute when `limit` is non-null (e.g., `const usagePct =
limit != null ? Math.min(100, (used / limit) * 100) : 0`), and ensure the UI
rendering that shows the counter uses `limit == null` to display "(unlimited)"
instead of "/ 5"; apply the same changes to the duplicate block that also
computes `limit`, `used`, and `usagePct`.
components/brand/sections/ProfileSection.tsx (1)

29-34: ⚠️ Potential issue | 🔴 Critical

Keep audience input in a draft state; defer parsing to blur or save.

The current implementation normalizes the audience string on every keystroke via onChange, which strips trailing commas and prevents natural multi-value entry. When the user types "Founders, Marketers,", the onChange handler immediately parses it to ["Founders", "Marketers"], and the re-rendered input value becomes "Founders, Marketers" (comma lost). Use local draft state to capture the raw input, then parse only on blur or form commit. If the Input component does not support onBlur, wire the commit into the parent save handler instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brand/sections/ProfileSection.tsx` around lines 29 - 34, The
audience input is being normalized on every keystroke in handleAudienceChange
which strips trailing commas and prevents natural multi-value entry; fix by
introducing a local draft state (e.g., audienceDraft) in the ProfileSection
component, have the Input component bind to audienceDraft and update it in
handleAudienceChange without calling props.onChange, then parse audienceDraft
into tags and call onChange({ audience: tags }) only on blur
(handleAudienceBlur) or when the parent save/commit handler runs; if the Input
used here does not support onBlur, wire the parse-and-commit logic into the
parent's save handler instead and reference handleAudienceChange,
handleAudienceBlur (new), audienceDraft, and the existing onChange prop to
locate where to apply these changes.
app/(protected)/library/[id]/page.tsx (2)

25-29: ⚠️ Potential issue | 🟠 Major

Copy the rendered caption/post text, not always post.content.

VisualView and CarouselView can replace post.content with formatted_output.post_text, but the shared handler still copies the raw content. The button can therefore copy something different from what the user sees.

Proposed fix
 interface ViewProps {
   post: Post;
   copied: boolean;
-  onCopy: () => void;
+  onCopy: (text: string) => void;
 }

 function SimpleView({ post, copied, onCopy }: ViewProps) {
   return (
@@
-        <CopyButton copied={copied} onCopy={onCopy} />
+        <CopyButton copied={copied} onCopy={() => onCopy(post.content)} />
@@
 function VisualView({ post, copied, onCopy }: ViewProps) {
@@
-        <CopyButton copied={copied} onCopy={onCopy} />
+        <CopyButton copied={copied} onCopy={() => onCopy(postText)} />
@@
 function CarouselView({ post, copied, onCopy }: ViewProps) {
@@
-        <CopyButton copied={copied} onCopy={onCopy} />
+        <CopyButton copied={copied} onCopy={() => onCopy(postText)} />
@@
-  const handleCopy = async () => {
-    if (!post) return;
-    await navigator.clipboard.writeText(post.content);
+  const handleCopy = async (text: string) => {
+    await navigator.clipboard.writeText(text);
     setCopied(true);
     setTimeout(() => setCopied(false), 2000);
   };
@@
-            onCopy={() => void handleCopy()}
+            onCopy={(text) => void handleCopy(text)}
@@
-            onCopy={() => void handleCopy()}
+            onCopy={(text) => void handleCopy(text)}
@@
-            onCopy={() => void handleCopy()}
+            onCopy={(text) => void handleCopy(text)}

Also applies to: 101-112, 150-150, 168-180, 267-267, 338-343, 503-518

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/library/[id]/page.tsx around lines 25 - 29, The shared copy
handler currently always copies post.content instead of the text actually
rendered by VisualView/CarouselView (which may use formatted_output.post_text);
update the copy flow so the handler copies the displayed caption text: either
change the onCopy signature to accept the final string (e.g.,
onCopy(displayedText: string)) and pass formatted_output.post_text from
VisualView/CarouselView, or add a small helper (e.g., getRenderedPostText(post))
used by the click handler to prefer post.formatted_output?.post_text fallback to
post.content; adjust all usages (including ViewProps, VisualView, CarouselView
and the shared button click) so the copied text matches what the user sees.

345-360: ⚠️ Potential issue | 🟠 Major

Don't flip the UI to published on HTTP failures.

fetch() will resolve on 4xx/5xx responses, so this code still updates local state when the backend rejects the publish request. The new button can end up showing a successful publish that never persisted.

Proposed fix
-      await fetch(`/api/data/update/posts/${post.id}`, {
+      const res = await fetch(`/api/data/update/posts/${post.id}`, {
         method: "PUT",
         credentials: "include",
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify({ status: "published" }),
       });
-      setPost({ ...post, status: "published" });
+      if (!res.ok) {
+        throw new Error(`Failed to update post (${res.status})`);
+      }
+      setPost((current) =>
+        current ? { ...current, status: "published" } : current,
+      );

Also applies to: 525-537

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/library/[id]/page.tsx around lines 345 - 360, The handler
handleMarkPublished updates local state to "published" even when the HTTP
request fails because fetch resolves for 4xx/5xx; change it to check the fetch
Response.ok (and optionally read error body) before calling setPost, e.g. await
fetch(...); if (!response.ok) throw or return without mutating UI so the local
setPost({ ...post, status: "published" }) only runs on success; keep
setMarkingPublished(false) in the finally block. Apply the same fix to the other
publish-path with the same pattern (the other block that calls fetch and
setPost).
components/calendar/CalendarView.tsx (1)

23-34: ⚠️ Potential issue | 🟠 Major

Keep picker sync one-way and parse date-only values locally.

With currentDate in this effect's dependency list (line 34), clicking Previous/Next month snaps the grid back to selectedDateFromPicker as soon as a picker value exists. Also, new Date("YYYY-MM-DD") is UTC in JavaScript, so a U.S. user selecting 2026-03-10 can end up highlighting March 9 locally (lines 25 and 85). Parse the picker value into a local date and only resync when selectedDateFromPicker itself changes.

🛠️ Suggested fix
+ const parseLocalDate = (value: string) => {
+   const [year, month, day] = value.split("-").map(Number);
+   return new Date(year, month - 1, day);
+ };
+
  useEffect(() => {
-   if (selectedDateFromPicker) {
-     const newDate = new Date(selectedDateFromPicker);
-     // Only update if the month/year is different to avoid unnecessary re-renders
-     if (
-       newDate.getFullYear() !== currentDate.getFullYear() ||
-       newDate.getMonth() !== currentDate.getMonth()
-     ) {
-       setCurrentDate(newDate);
-     }
-   }
- }, [selectedDateFromPicker, currentDate]); // Depend on selectedDateFromPicker and currentDate
+   if (!selectedDateFromPicker) return;
+   const nextDate = parseLocalDate(selectedDateFromPicker);
+   setCurrentDate((prev) =>
+     prev.getFullYear() === nextDate.getFullYear() &&
+     prev.getMonth() === nextDate.getMonth()
+       ? prev
+       : nextDate,
+   );
+ }, [selectedDateFromPicker]);
...
-      const isSelected =
-        selectedDateFromPicker &&
-        date.toDateString() === new Date(selectedDateFromPicker).toDateString();
+      const selectedDate = selectedDateFromPicker
+        ? parseLocalDate(selectedDateFromPicker)
+        : null;
+      const isSelected =
+        selectedDate &&
+        date.toDateString() === selectedDate.toDateString();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/calendar/CalendarView.tsx` around lines 23 - 34, The effect is
resyncing two-ways because currentDate is in the dependency list and it uses new
Date(string) (UTC); update the useEffect that watches
selectedDateFromPicker/currentDate so it only depends on selectedDateFromPicker,
and parse the picker value as a local date (e.g., split "YYYY-MM-DD" into year,
month, day and call new Date(year, month-1, day)) before comparing with
currentDate; then call setCurrentDate(newLocalDate) only when the month/year
differ. Target the useEffect, selectedDateFromPicker, currentDate, and
setCurrentDate in CalendarView to implement this change.
app/(protected)/factory/page.tsx (1)

1032-1045: ⚠️ Potential issue | 🟠 Major

Add labels to the session override inputs.

Both Inputs rely on placeholder text only. Once the user types, the prompt disappears and the fields remain unnamed to assistive tech.

Suggested fix
               <Input
+                label="Tone override"
                 placeholder="Tone override (optional)"
                 value={toneInput}
                 onChange={(e) => {
@@
                 <Input
+                  label="Template name"
                   placeholder="Template name (optional)"
                   value={templateInput}
                   onChange={(e) => {

Also applies to: 1129-1140

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/factory/page.tsx around lines 1032 - 1045, The two
session-override Input fields (the one bound to toneInput via
setToneInput/setState updating toneOverride and the other at 1129-1140)
currently use only placeholders; add visible labels or at minimum accessible
labels so assistive tech can identify them after content is entered: give each
Input a stable id, add a corresponding <label htmlFor="..."> (or
aria-label/aria-labelledby tied to that id) that describes the field (e.g.,
"Tone override" / appropriate label for the other input), and ensure the
onChange handlers and state bindings (toneInput, setToneInput, setState) remain
unchanged.
♻️ Duplicate comments (2)
app/(protected)/learning/page.tsx (2)

117-118: ⚠️ Potential issue | 🟠 Major

Keep refresh recoverable while loading.

fetchData still has no abort/timeout path, but the only retry control is disabled for the full request. If either fetch stalls, the page is stuck with a spinner and no in-app recovery.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/learning/page.tsx around lines 117 - 118, The Refresh button
currently disables while loading and fetchData has no abort/timeout so a stalled
request leaves the UI stuck; update fetchData to use an AbortController with a
timeout (or expose a cancel path) and handle AbortError by resetting loading and
state gracefully, and either keep the Button enabled during loading or add a
separate Cancel control that calls controller.abort(); ensure fetchData, the
loading state, and the Loader/ Button onClick handlers correctly clear the
timeout and call setLoading(false) on all exit paths so the user can retry or
cancel a stalled request.

204-209: ⚠️ Potential issue | 🟡 Minor

Only show the empty proposals state after a successful proposals load.

proposals.length === 0 is also true when the proposals request fails, and while the first load is still in flight if the user switches tabs early. Gate this on a completed successful proposals fetch instead of raw emptiness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/learning/page.tsx around lines 204 - 209, The EmptyState is
shown when proposals.length === 0 even during failed or in-flight loads; change
the render condition to only show it after a successful fetch by gating on a
success flag (e.g., useQuery's isSuccess or a local proposalsLoaded boolean).
Update the fetch logic (fetchProposals / useProposals / the effect that calls
setProposals) to set proposalsLoaded/isSuccess only after a successful response
(and clear or set an error flag on failure), then replace the JSX conditional
with something like proposalsLoaded && proposals.length === 0 so EmptyState only
appears after a completed successful load.
🟡 Minor comments (15)
components/campaigns/CampaignSummaryRow.tsx-38-47 (1)

38-47: ⚠️ Potential issue | 🟡 Minor

Expose the selected filter state with aria-pressed.

These act as toggle buttons, but the active state is only visual right now. Add aria-pressed so assistive tech can tell which filter is selected.

Suggested fix
       <Button
+        aria-pressed={activeFilter == null}
         variant={
           activeFilter === null || activeFilter === undefined
             ? "dark"
             : "ghost-bordered"
         }
         onClick={() => onFilterClick(null)}
       >
         All {total}
       </Button>
@@
           <Button
             key={s}
+            aria-pressed={isActive}
             variant="ghost-bordered"
             onClick={() => onFilterClick(isActive ? null : s)}
             style={{
               background: isActive ? cfg.bg : "transparent",
               color: isActive

Also applies to: 54-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/campaigns/CampaignSummaryRow.tsx` around lines 38 - 47, The filter
buttons in CampaignSummaryRow render visual active state only; add aria-pressed
to each Button so screen readers know which filter is selected: set aria-pressed
to a boolean expression comparing activeFilter to the filter value (e.g., for
the "All" button use aria-pressed={activeFilter == null} and for other filter
buttons use aria-pressed={activeFilter === '<filterValue>'}), keeping the
existing onClick handlers (onFilterClick) and variants; apply the same change to
the other Button instances around the 54-69 block.
components/factory/RewriteInstructions.tsx-105-135 (1)

105-135: ⚠️ Potential issue | 🟡 Minor

Add aria-expanded for accessibility.

The button controls collapsible content but lacks aria-expanded, which screen readers need to announce the current state.

♿ Proposed fix
       <Button
         variant="ghost"
         onClick={() => setExpanded(!expanded)}
+        aria-expanded={expanded}
         style={{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/factory/RewriteInstructions.tsx` around lines 105 - 135, The
Button that toggles the collapsible panel (the JSX Button using onClick={() =>
setExpanded(!expanded)} and the expanded state) is missing an accessibility
attribute; add aria-expanded={expanded} to that Button so screen readers can
announce open/closed state, and optionally include aria-controls referencing the
collapsible region's id (match the id on the collapsible element) to improve
assistive navigation.
app/forgot-password/page.tsx-120-121 (1)

120-121: ⚠️ Potential issue | 🟡 Minor

Keep the submit label visible while loading.

When loading is true, this button becomes spinner-only. That can leave it without a useful accessible name and gives assistive tech no status text during submission.

♿ Suggested fix
-              <Button type="submit" variant="primary" disabled={loading} block>
-                {loading ? <Loader size="sm" /> : "Send Reset Link"}
+              <Button
+                type="submit"
+                variant="primary"
+                disabled={loading}
+                block
+                aria-busy={loading}
+              >
+                {loading ? (
+                  <>
+                    <span aria-hidden="true">
+                      <Loader size="sm" />
+                    </span>
+                    Sending reset link…
+                  </>
+                ) : (
+                  "Send Reset Link"
+                )}
               </Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/forgot-password/page.tsx` around lines 120 - 121, The submit Button
currently replaces its text with only a Loader when loading; update the Button
children in page.tsx (the Button and Loader JSX) to always render a visible
label for screen readers and sighted users while showing the spinner (e.g.,
render the Loader with aria-hidden="true" next to the label or use a short
status label like "Sending…" rather than hiding text), and ensure the Button has
aria-busy={loading} (or aria-live region) so assistive tech receives submission
status; change only the Button/Loader rendering logic that uses the loading
prop.
app/(protected)/analytics/page.tsx-181-186 (1)

181-186: ⚠️ Potential issue | 🟡 Minor

Remove unsupported color="mint" prop from ProgressBar.

The ProgressBar component from @doctorproject/react does not accept a color prop. All six other usages of ProgressBar in the codebase use only the value prop. The test mock confirms this pattern. Remove the color="mint" argument.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/analytics/page.tsx around lines 181 - 186, Remove the
unsupported color="mint" prop from the ProgressBar usage in the analytics page:
update the ProgressBar component (the element rendering ProgressBar with
value={Math.round((pillar.impressions / maxImpressions) * 100)}) to only pass
the value prop, matching other usages and the test mock.
app/(protected)/settings/profile/page.tsx-233-238 (1)

233-238: ⚠️ Potential issue | 🟡 Minor

Use the trimmed name in the disabled check too.

Line 237 compares raw strings, but the request body is trimmed before submit. Adding only trailing/leading whitespace still enables the button and sends a no-op update.

Suggested fix
   const handleSave = async () => {
-    if (!user?.id || !displayName.trim()) return;
+    const trimmedDisplayName = displayName.trim();
+    if (!user?.id || !trimmedDisplayName) return;
@@
-        body: JSON.stringify({ name: displayName.trim() }),
+        body: JSON.stringify({ name: trimmedDisplayName }),
@@
-              disabled={
-                saving || !displayName.trim() || displayName === user?.name
-              }
+              disabled={
+                saving ||
+                !displayName.trim() ||
+                displayName.trim() === (user?.name ?? "").trim()
+              }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/settings/profile/page.tsx around lines 233 - 238, The
disabled check uses the raw displayName value, allowing whitespace-only edits to
enable the button; update the Button.disabled condition to use the trimmed
displayName (and compare against a trimmed user?.name) so that
displayName.trim() === (user?.name ?? '').trim() (or equivalent) prevents no-op
saves; modify the disabled expression in the Button props that reference
displayName, user?.name, saved, and saving accordingly and keep handleSave()
unchanged.
components/knowledge/ImportFlow.tsx-149-155 (1)

149-155: ⚠️ Potential issue | 🟡 Minor

Make the upload label accurate and programmatically associated.

The label says only .md, but the input also accepts .txt, and it is not bound to the file control. Adding htmlFor/id fixes click-to-open and improves screen-reader announcement.

Suggested tweak
-            <label className="drp-field__label">Upload .md file</label>
+            <label className="drp-field__label" htmlFor="knowledge-upload">
+              Upload .md or .txt file
+            </label>
             <input
+              id="knowledge-upload"
               type="file"
               accept=".md,.txt"
               onChange={handleFileUpload}
               className="drp-input"
               style={{ padding: "var(--drp-space-2)" }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/knowledge/ImportFlow.tsx` around lines 149 - 155, Update the
Upload label in the ImportFlow component to accurately reflect accepted file
types and bind it to the file input for accessibility: change the label text
from "Upload .md file" to something like "Upload .md or .txt file", give the
input element a unique id (e.g., id="import-file-input") and add
htmlFor="import-file-input" to the label so clicking the label opens the file
dialog and screen readers correctly associate the label with the input; ensure
the existing onChange handler (handleFileUpload) remains attached.
components/knowledge/ImportFlow.tsx-238-239 (1)

238-239: ⚠️ Potential issue | 🟡 Minor

Use plain text instead of <label> for the preview heading.

<label> is meant for form controls, but this caption belongs to a <pre>. A <div>, <p>, or heading element with the same class is the better semantic fit.

Suggested tweak
-            <div className="drp-field">
-              <label className="drp-field__label">Content Preview</label>
+            <div className="drp-field">
+              <div className="drp-field__label">Content Preview</div>
               <pre
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/knowledge/ImportFlow.tsx` around lines 238 - 239, The preview
heading currently uses a <label> inside the ImportFlow component (class
"drp-field" with "drp-field__label") but it is not associated with a form
control; replace that <label> with a semantic container element (e.g., <div>,
<p>, or an appropriate heading like <h3>) that preserves the "drp-field__label"
class and is adjacent to the <pre> preview so the caption is semantically
correct and styling remains unchanged.
components/knowledge/ExtractFlow.tsx-361-369 (1)

361-369: ⚠️ Potential issue | 🟡 Minor

The success alert never renders after a successful save.

handleSave() leaves the flow in "saving", so this preview-only alert is skipped on success and the user just sees the spinner until onComplete fires. Either surface success inside the saving branch or switch back to preview before setting the success feedback.

Also applies to: 374-394

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/knowledge/ExtractFlow.tsx` around lines 361 - 369, The success
Alert never shows because handleSave() keeps the flow state as "saving", and the
JSX only renders feedback when not in the saving/preview mismatch; fix
handleSave() to switch the UI back to preview before setting the success
feedback (or alternately render success while in "saving"). Concretely, in
ExtractFlow.tsx update the save success path inside handleSave() to call
setFlowState("preview") (or setFlowState with the variable name used)
immediately before calling setFeedback(...) so the success Alert variant
renders; apply the same change to the other save-success branches mentioned (the
other save handlers around the 374-394 region) to ensure all success paths
surface feedback.
app/login/page.tsx-89-100 (1)

89-100: ⚠️ Potential issue | 🟡 Minor

Label the initial full-page loader as well.

The first render is currently just a spinner, so screen readers get no context about what is loading. The inline submit loader already uses a label; this state should too.

💡 Suggested patch
-        <Loader />
+        <Loader label="Loading sign-in options..." />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/login/page.tsx` around lines 89 - 100, The initial full-page loading
branch (the conditional using loadingAuth or !providers in page.tsx) renders
only <Loader /> with no accessible label; update that branch so the loader has
an accessible label/context for screen readers (e.g., add role="status" and an
aria-label like "Loading authentication" or include a visually-hidden
descriptive <span> and reference it via aria-describedby on the container or the
Loader component) so screen readers know what is loading; target the conditional
that returns the fullscreen div and modify the div or the Loader element to
include the accessibility attributes.
app/login/page.tsx-165-179 (1)

165-179: ⚠️ Potential issue | 🟡 Minor

Restore autofill metadata on the login fields.

The DS Input swap dropped the native name/autoComplete hints, which makes browser autofill and password-manager detection less reliable on this form.

💡 Suggested patch
                   <Input
                     label="Email"
+                    name="email"
                     type="email"
+                    autoComplete="email"
+                    autoCapitalize="none"
+                    autoCorrect="off"
                     placeholder="Email address"
                     value={email}
                     onChange={(e) => setEmail(e.target.value)}
                   />
                   <div style={{ position: "relative" }}>
                     <Input
                       label="Password"
+                      name="password"
                       type={showPassword ? "text" : "password"}
+                      autoComplete="current-password"
                       placeholder="Password"
                       value={password}
                       onChange={(e) => setPassword(e.target.value)}
                     />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/login/page.tsx` around lines 165 - 179, The Input swap removed native
autofill hints causing browsers/password managers to not recognize the login
fields; update the Input usages for the email and password fields (the Input
components that bind to email via setEmail and password via setPassword, and the
password Input using showPassword) to pass appropriate name and autoComplete
props (e.g., name="email" autoComplete="email" for the email field and
name="current-password" autoComplete="current-password" for the password field)
so native autofill and password managers can detect them.
components/learning/FeedbackHistory.tsx-51-61 (1)

51-61: ⚠️ Potential issue | 🟡 Minor

Give the filter control an accessible name.

This Select is rendered without a visible label or aria-label, so assistive tech will only get an unlabeled combobox. Please name it explicitly.

♿ Minimal fix
         {signalTypes.length > 1 && (
           <Select
+            aria-label="Filter feedback history by type"
             value={filterType}
             onChange={(e) => setFilterType(e.target.value)}
             style={{ width: "auto", fontSize: "var(--drp-text-sm)" }}
           >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/learning/FeedbackHistory.tsx` around lines 51 - 61, The Select
control rendering in FeedbackHistory is missing an accessible name; update the
Select (the element using value={filterType}, onChange={(e) =>
setFilterType(e.target.value)} and rendering filterOptions) to include an
explicit accessible name—either add an aria-label (e.g., aria-label="Filter
feedback by type") or associate it with a visible <label> via aria-labelledby—so
screen readers receive a clear name for the combobox.
app/reset-password/page.tsx-157-165 (1)

157-165: ⚠️ Potential issue | 🟡 Minor

Ensure the Suspense fallback loader is labeled for accessibility.

The in-page loader uses label, but the fallback <Loader /> provides no accessible name. This creates an inconsistency: screen readers will skip the fallback silently while announcing the in-page loading state. Align both by adding the label prop to the fallback.

Suggested update
-          <Loader />
+          <Loader label="Loading reset password page..." />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/reset-password/page.tsx` around lines 157 - 165, The Suspense fallback
uses <Loader /> without an accessible name; update the fallback instance to pass
the same label prop used for the in-page loader (e.g., label="Loading…" or the
exact string used elsewhere) so screen readers announce it consistently—locate
the fallback <Loader /> in the reset-password page and add the label prop to
match the in-page Loader component.
__tests__/components/CampaignCalendar.test.tsx-85-86 (1)

85-86: ⚠️ Potential issue | 🟡 Minor

These assertions no longer prove the pillar rendered in the slot card.

getAllByText(...).length >= 1 is satisfied by the filter controls alone, so the slot content can drop the pillar and these tests will still pass. Scope the query to the slot row/card, or assert the count the current layout should produce.

Also applies to: 127-128, 184-187

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/components/CampaignCalendar.test.tsx` around lines 85 - 86, The
assertion currently uses screen.getAllByText("Authority").length >= 1 which can
match the filter controls instead of the slot card; change these to scope the
query to the slot row/card (use within(slotCardElement).getByText("Authority")
or within(slotRow).queryByText(...) on the specific DOM node that represents the
slot) or assert the exact expected count for the current layout (e.g.,
expect(screen.getAllByText("Authority")).toHaveLength(expectedCount)); update
the occurrences that use screen.getAllByText("Authority") (the failing
assertions) to either a within-scoped getByText on the slot card element or an
explicit length assertion so the test proves the pillar is rendered in the slot
card.
app/(protected)/campaigns/[campaignId]/idea/[slotOrder]/page.tsx-180-185 (1)

180-185: ⚠️ Potential issue | 🟡 Minor

Add a descriptive label to the back button.

Right now the accessible name is just the arrow glyph. Give it something like aria-label="Back to campaign" so the control is understandable in the tab order and to screen-reader users.

♿ Suggested tweak
         <Button
           variant="ghost"
+          aria-label="Back to campaign"
           onClick={() => router.push(`/campaigns/${campaignId}`)}
         >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/campaigns/[campaignId]/idea/[slotOrder]/page.tsx around
lines 180 - 185, The back Button component rendering the arrow glyph lacks an
accessible name; update the Button (the component instance using onClick={() =>
router.push(`/campaigns/${campaignId}`)}) to include an explicit accessible
label such as aria-label="Back to campaign" (or a suitable localized string) so
screen readers and keyboard users can understand its purpose while keeping the
existing onClick/router.push and visual content unchanged.
app/(protected)/calendar/page.tsx-93-95 (1)

93-95: ⚠️ Potential issue | 🟡 Minor

Use the existing display label instead of formatting the status slug inline.

Hyphenated states render as To-review / To-publish here. lib/calendarUtils.ts lines 3-10 already has the user-facing labels.

Suggested fix
-                  <Tag color={getStatusTagColor(post.status)} filled>
-                    {post.status.charAt(0).toUpperCase() + post.status.slice(1)}
+                  <Tag color={getStatusTagColor(post.status)} filled>
+                    {statusOptions.find((option) => option.value === post.status)
+                      ?.label ?? post.status}
                   </Tag>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/calendar/page.tsx around lines 93 - 95, Replace the inline
formatting of post.status inside the Tag with the user-facing label exported
from lib/calendarUtils.ts (instead of post.status.charAt...); import and use the
existing label helper or map (the file exposes the display labels used for
hyphenated statuses) alongside the existing getStatusTagColor and Tag
components, and provide a sensible fallback to post.status if the helper/map
returns undefined.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c2f76697-606c-462d-9116-c68ad80e862d

📥 Commits

Reviewing files that changed from the base of the PR and between 6721ade and ed1094b.

📒 Files selected for processing (71)
  • .github/copilot-instructions.md
  • __tests__/components/CampaignCalendar.test.tsx
  • __tests__/components/FeedbackHistory.test.tsx
  • __tests__/components/PostEditorModal.test.tsx
  • __tests__/components/PostGenerator.test.tsx
  • __tests__/components/RuleProposal.test.tsx
  • app/(onboarding)/layout.tsx
  • app/(onboarding)/onboarding/review/page.tsx
  • app/(onboarding)/onboarding/start/page.tsx
  • app/(onboarding)/onboarding/upload/page.tsx
  • app/(onboarding)/onboarding/wizard/[step]/page.tsx
  • app/(protected)/analytics/page.tsx
  • app/(protected)/brand/page.tsx
  • app/(protected)/calendar/page.tsx
  • app/(protected)/campaigns/[campaignId]/idea/[slotOrder]/page.tsx
  • app/(protected)/campaigns/[campaignId]/page.tsx
  • app/(protected)/create/page.tsx
  • app/(protected)/factory/page.tsx
  • app/(protected)/layout.tsx
  • app/(protected)/learning/page.tsx
  • app/(protected)/library/[id]/page.tsx
  • app/(protected)/library/page.tsx
  • app/(protected)/settings/page.tsx
  • app/(protected)/settings/profile/page.tsx
  • app/(protected)/settings/subscription/page.tsx
  • app/(protected)/studio/page.tsx
  • app/forgot-password/page.tsx
  • app/login/page.tsx
  • app/reset-password/page.tsx
  • app/signup/page.tsx
  • components/ConfirmDialog.tsx
  • components/EnhancedDropdown.tsx
  • components/PostEditorModal.tsx
  • components/PostGenerator.tsx
  • components/PostGoalSelector.tsx
  • components/SchedulePostModal.tsx
  • components/Toast.tsx
  • components/TonePromptPreviewModal.tsx
  • components/brand/BrandSection.tsx
  • components/brand/sections/AiToolsSection.tsx
  • components/brand/sections/OffersSection.tsx
  • components/brand/sections/PillarsSection.tsx
  • components/brand/sections/PositioningSection.tsx
  • components/brand/sections/ProfileSection.tsx
  • components/brand/sections/StrategySection.tsx
  • components/brand/sections/VoiceSection.tsx
  • components/calendar/CalendarView.tsx
  • components/campaigns/CampaignAnalytics.tsx
  • components/campaigns/CampaignFilters.tsx
  • components/campaigns/CampaignList.tsx
  • components/campaigns/CampaignSetup.tsx
  • components/campaigns/CampaignSummaryRow.tsx
  • components/campaigns/IdeaInbox.tsx
  • components/create/ContentAngleChips.tsx
  • components/create/PostStructureCards.tsx
  • components/factory/CreatePostDialog.tsx
  • components/factory/DraftEditor.tsx
  • components/factory/EvidencePack.tsx
  • components/factory/FormattedOutput.tsx
  • components/factory/GuardrailRecovery.tsx
  • components/factory/LearningPhaseResult.tsx
  • components/factory/PipelineStepper.tsx
  • components/factory/PostReview.tsx
  • components/factory/RewriteInstructions.tsx
  • components/factory/SessionHistory.tsx
  • components/knowledge/ExtractFlow.tsx
  • components/knowledge/ImportFlow.tsx
  • components/learning/FeedbackHistory.tsx
  • components/learning/PatternList.tsx
  • components/learning/RuleProposal.tsx
  • components/settings/StraicoModelPicker.tsx
🚧 Files skipped from review as they are similar to previous changes (11)
  • app/(protected)/campaigns/[campaignId]/page.tsx
  • components/PostGoalSelector.tsx
  • components/EnhancedDropdown.tsx
  • components/factory/SessionHistory.tsx
  • components/campaigns/IdeaInbox.tsx
  • app/signup/page.tsx
  • tests/components/PostEditorModal.test.tsx
  • components/campaigns/CampaignAnalytics.tsx
  • components/SchedulePostModal.tsx
  • components/create/PostStructureCards.tsx
  • components/factory/PipelineStepper.tsx

Comment on lines +46 to +48
<span style={{ animation: "spin 1s linear infinite", display: "flex" }}>
<Loader size="sm" />
</span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if spin keyframes are defined in global styles
rg -n '@keyframes spin' --type css

Repository: drproject-coding/doctorpost-v12

Length of output: 57


🏁 Script executed:

# Search for spin keyframes in all file types
rg -n 'spin' -t ts -t tsx -t js -t json | head -50

Repository: drproject-coding/doctorpost-v12

Length of output: 102


🏁 Script executed:

# Read the layout.tsx file to see full context
cat -n app/\(onboarding\)/layout.tsx | head -80

Repository: drproject-coding/doctorpost-v12

Length of output: 2660


🏁 Script executed:

# Check if Tailwind is being used (animate-spin)
rg -l 'tailwind' | head -10

Repository: drproject-coding/doctorpost-v12

Length of output: 310


🏁 Script executed:

# Search for Loader component usage patterns
rg -n 'Loader' -t tsx -t ts | head -30

Repository: drproject-coding/doctorpost-v12

Length of output: 102


🏁 Script executed:

# Search for keyframes without file type restrictions
rg -n '@keyframes' --type-list

Repository: drproject-coding/doctorpost-v12

Length of output: 464


🏁 Script executed:

# Search for spin in all files
rg -n 'spin' | grep -i 'keyframe\|animation\|@keyframes'

Repository: drproject-coding/doctorpost-v12

Length of output: 796


🏁 Script executed:

# Read tailwind config
cat -n tailwind.config.ts | head -100

Repository: drproject-coding/doctorpost-v12

Length of output: 1948


🏁 Script executed:

# Find other uses of Loader component
rg -n 'Loader' | head -40

Repository: drproject-coding/doctorpost-v12

Length of output: 3564


🏁 Script executed:

# Read the utilities.css around the drp-spin definition
sed -n '269,280p' styles/doctorproject/utilities.css

Repository: drproject-coding/doctorpost-v12

Length of output: 307


🏁 Script executed:

# Check if there's a global CSS file that might define spin keyframes
fd -e css | xargs grep -l 'keyframes spin' 2>/dev/null || echo "No 'keyframes spin' in CSS files"

Repository: drproject-coding/doctorpost-v12

Length of output: 107


🏁 Script executed:

# Check the entire utilities.css to see what's available
wc -l styles/doctorproject/utilities.css

Repository: drproject-coding/doctorpost-v12

Length of output: 113


Remove the animation wrapper or define @keyframes spin locally.

The span with animation: "spin 1s linear infinite" won't animate because @keyframes spin is not defined globally or in this file. Other pages in the codebase (e.g., upload/page.tsx, review/page.tsx) define this keyframe locally to make it work.

Either:

  1. Define @keyframes spin { to { transform: rotate(360deg); } } in this file's styles, or
  2. Remove the wrapper span if the Loader component from @doctorproject/react already provides animation (as suggested by its usage throughout the codebase without custom animation)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(onboarding)/layout.tsx around lines 46 - 48, The span wrapper in
app/(onboarding)/layout.tsx uses animation: "spin 1s linear infinite" but no
`@keyframes` spin is defined here, so either remove the wrapper span around
<Loader /> if the Loader component already animates, or add a local `@keyframes`
spin definition in this component's styles (e.g., define `@keyframes` spin { to {
transform: rotate(360deg); } }) so the span's animation works; locate the span
and the <Loader /> usage in layout.tsx and apply one of these two fixes.

Comment on lines +393 to +400
<Textarea
id="copyGuideline"
label="Copy Guideline"
hint="Any additional instructions for your writing style (optional)."
>
<textarea
style={textareaStyle}
value={state.copyGuideline}
onChange={(e) =>
onChange({ ...state, copyGuideline: e.target.value })
}
placeholder="e.g. Always lead with data. Keep sentences under 20 words. Never use passive voice."
/>
</LabeledField>
value={state.copyGuideline}
onChange={(e) => onChange({ ...state, copyGuideline: e.target.value })}
placeholder="e.g. Always lead with data. Keep sentences under 20 words. Never use passive voice."
className="drp-textarea--resize-vertical"
/>
Copy link
Copy Markdown

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:

#!/bin/bash
# Check if drp-textarea--resize-vertical class is defined
rg -n "drp-textarea--resize-vertical" -g '*.css' -g '*.scss'

# Search for Textarea usage patterns
rg -n '<Textarea' --type-add 'tsx:*.tsx' -g '!node_modules' -A3 | head -30

Repository: drproject-coding/doctorpost-v12

Length of output: 2202


🏁 Script executed:

# Search for the class name in all file types
rg -n "drp-textarea--resize-vertical" --type-add 'all:*'

# Find and examine the Textarea component definition
fd -name "*.tsx" -o -name "*.ts" | xargs grep -l "export.*Textarea" | head -5

Repository: drproject-coding/doctorpost-v12

Length of output: 365


🏁 Script executed:

# Search for similar drp- class patterns to understand naming convention
rg -n "drp-[a-z]+--.+" -g '*.css' -g '*.scss' | head -20

Repository: drproject-coding/doctorpost-v12

Length of output: 1255


🏁 Script executed:

# Check Textarea component imports and definition in the file we're reviewing
head -50 "app/(onboarding)/onboarding/wizard/[step]/page.tsx" | grep -E "import|from"

Repository: drproject-coding/doctorpost-v12

Length of output: 353


🏁 Script executed:

# Find the Textarea component definition
find . -name "*.tsx" -o -name "*.ts" | xargs grep -l "export.*function Textarea\|export.*const Textarea" | grep -v node_modules | head -5

Repository: drproject-coding/doctorpost-v12

Length of output: 57


🏁 Script executed:

# Check if `@doctorproject/react` is a local package
ls -la "packages/" 2>/dev/null || ls -la | grep -i "doctorproject"

Repository: drproject-coding/doctorpost-v12

Length of output: 57


🏁 Script executed:

# Search for any existing Textarea with className usage to see patterns
rg -n '<Textarea' -A5 -B1 | grep -E "className|style" | head -20

Repository: drproject-coding/doctorpost-v12

Length of output: 1714


🏁 Script executed:

# Check if there are any CSS files specifically for textarea
fd "textarea\|Textarea" -t f | grep -E "\.(css|scss)$"

Repository: drproject-coding/doctorpost-v12

Length of output: 57


Remove the invalid className="drp-textarea--resize-vertical" and use inline styling instead.

The class drp-textarea--resize-vertical does not exist in the design system. Based on patterns throughout the codebase, Textarea resizing should be handled via inline styles (e.g., style={{ resize: "vertical" }} or style={{ minHeight: ... }}), not CSS classes. The Textarea component does properly support the label prop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(onboarding)/onboarding/wizard/[step]/page.tsx around lines 393 - 400,
Remove the invalid className prop on the Textarea (the instance with id
"copyGuideline" and value={state.copyGuideline}) and replace it with an inline
style that enforces vertical resizing (e.g., style={{ resize: "vertical" }} or
style with a minHeight) so resizing is handled inline; keep the existing id,
label, value and onChange handler intact.

Comment on lines +162 to +179
<span
style={{
fontSize: "var(--drp-text-base)",
fontWeight: 700,
color: "var(--drp-text)",
}}
>
{pillar.name}
</span>
<span className="text-sm font-bold text-gray-500">
{pillar.impressions.toLocaleString()}
</span>
</div>
<div className="w-full bg-gray-200 rounded-bru-md border border-black h-3">
<div
className="bg-bru-purple h-3 rounded-bru-md"
<span
style={{
width: `${(pillar.impressions / maxImpressions) * 100}%`,
fontSize: "var(--drp-text-sm)",
fontWeight: 700,
color: "var(--drp-grey)",
}}
></div>
>
{pillar.impressions.toLocaleString()}
</span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for --drp-text-base and --drp-text-sm token definitions in CSS files.

# Find token definitions
rg -n '\-\-drp-text-base|\-\-drp-text-sm' --type=css -g '!node_modules'

# Also check for font-size related tokens
rg -n '\-\-drp-font-size|\-\-drp-text-size' --type=css -g '!node_modules'

Repository: drproject-coding/doctorpost-v12

Length of output: 2185


🏁 Script executed:

#!/bin/bash
# Check the tokens.css file for all text-related and color tokens
echo "=== Content of tokens.css (looking for text and color definitions) ==="
rg -n '\-\-drp-(text|color|grey)' styles/doctorproject/tokens.css

echo ""
echo "=== Search specifically for --drp-text-base ==="
rg -r '\-\-drp-text-base' --type=css

echo ""
echo "=== Search for --drp-text and --drp-grey across all CSS ==="
rg -n '\-\-drp-text[^-]|\-\-drp-grey' --type=css -g '!node_modules' | head -20

Repository: drproject-coding/doctorpost-v12

Length of output: 2937


🏁 Script executed:

#!/bin/bash
# Read the analytics page to see the exact code at lines 162-179
echo "=== Content of analytics/page.tsx around lines 162-179 ==="
sed -n '150,190p' app/\(protected\)/analytics/page.tsx | cat -n

Repository: drproject-coding/doctorpost-v12

Length of output: 1818


Fix undefined CSS custom property tokens.

Two tokens used for styling the pillar name are undefined in the design system:

  • var(--drp-text-base) for fontSize — no base variant exists; use var(--drp-text-md) (14px) or var(--drp-text-lg) (16px) instead
  • var(--drp-text) for color — use var(--drp-text-primary) instead

Undefined variables will silently fail to apply, leaving text unstyled. The tokens --drp-text-sm and --drp-grey are correctly defined.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/analytics/page.tsx around lines 162 - 179, The span
rendering pillar.name uses undefined CSS custom properties; update its style to
use a defined fontSize token (replace var(--drp-text-base) with
var(--drp-text-md) or var(--drp-text-lg)—prefer var(--drp-text-md) for 14px) and
replace the undefined color token var(--drp-text) with var(--drp-text-primary);
modify the inline style on the span that contains pillar.name accordingly while
leaving the pillar.impressions span tokens unchanged.

Comment on lines +246 to +263
<Select
label=""
value={filterStatus}
onChange={(e) =>
setFilterStatus(
(e as React.ChangeEvent<HTMLSelectElement>).target
.value as FilterStatus,
)
}
>
<option value="all">All Statuses</option>
<option value="past">Past Posts</option>
{statusOptions.map((option) => (
<option key={option.id} value={option.value}>
{option.label}
</option>
))}
</Select>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Give the status filter an accessible name.

label="" leaves this control unnamed for assistive tech. Use a real label here, or an aria-label if you need it visually hidden.

Suggested fix
-          <Select
-            label=""
+          <Select
+            label="Filter by status"
             value={filterStatus}
             onChange={(e) =>
               setFilterStatus(
📝 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
<Select
label=""
value={filterStatus}
onChange={(e) =>
setFilterStatus(
(e as React.ChangeEvent<HTMLSelectElement>).target
.value as FilterStatus,
)
}
>
<option value="all">All Statuses</option>
<option value="past">Past Posts</option>
{statusOptions.map((option) => (
<option key={option.id} value={option.value}>
{option.label}
</option>
))}
</Select>
<Select
label="Filter by status"
value={filterStatus}
onChange={(e) =>
setFilterStatus(
(e as React.ChangeEvent<HTMLSelectElement>).target
.value as FilterStatus,
)
}
>
<option value="all">All Statuses</option>
<option value="past">Past Posts</option>
{statusOptions.map((option) => (
<option key={option.id} value={option.value}>
{option.label}
</option>
))}
</Select>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/calendar/page.tsx around lines 246 - 263, The Select control
rendering the status filter (value={filterStatus}, onChange calling
setFilterStatus) currently has label="" which leaves it unnamed to assistive
tech; update the Select (or its wrapper) to provide an accessible name by
replacing label="" with a meaningful label like "Status filter" or, if you must
keep it visually hidden, add an aria-label="Status filter" (or aria-labelledby
pointing to a visible label element) so screen readers can identify the control;
keep the rest of the logic (statusOptions mapping, value, onChange) unchanged.

Comment on lines +223 to +230
<div style={{ marginBottom: "var(--drp-space-4)" }}>
<Input
label=""
type="text"
placeholder="Search by title, content or pillar…"
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{
width: "100%",
padding: "8px 12px",
fontSize: 14,
border: "2px solid #000",
outline: "none",
fontFamily: "inherit",
}}
style={{ width: "100%" }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Give the search input an accessible name.

label="" leaves this field unnamed for assistive tech, and the placeholder is not a reliable replacement. Please provide a real label or at least an aria-label such as "Search posts".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/library/page.tsx around lines 223 - 230, The search Input
currently has an empty label prop which makes it inaccessible to assistive
technologies; update the Input usage in the component (the Input element that
uses props: label, type="text", placeholder, value={search}, onChange={(e) =>
setSearch(e.target.value)}) to provide an accessible name by supplying a
descriptive label (e.g., label="Search posts") or, if a visible label is not
desired, add an aria-label prop (e.g., aria-label="Search posts") so screen
readers can identify the control. Ensure you remove the empty label="" if you
add aria-label, or keep a meaningful label value instead.

Comment on lines 446 to 453
<a
href="mailto:[email protected]?subject=Upgrade%20Request"
style={{
display: "inline-flex",
alignItems: "center",
gap: 8,
padding: "12px 24px",
background: "var(--bru-purple)",
color: "#fff",
fontWeight: 800,
fontSize: 14,
textDecoration: "none",
flexShrink: 0,
}}
style={{ textDecoration: "none", flexShrink: 0 }}
>
<Zap size={15} />
{tier === "free" ? "Upgrade to Pro" : "Upgrade to Power"}
<Button variant="primary" iconLeft={<Zap size={15} />}>
{tier === "free" ? "Upgrade to Pro" : "Upgrade to Power"}
</Button>
</a>
Copy link
Copy Markdown

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

🌐 Web query:

In the drproject-coding/doctorproject-reactpackage or repository, how does theButton component render by default, and what is the supported way to use it as a link (href, as, asChild, etc.)?

💡 Result:

  • Default render: Button always renders a native <button> element with base class drp-btn plus optional modifier classes (drp-btn--{variant}, drp-btn--{size}, drp-btn--block, drp-btn--icon) and forwards the remaining props onto that <button>. [1]

  • Using it as a link: There is no supported “link mode” (href, as, asChild, polymorphic rendering, etc.). The props are based on ButtonHTMLAttributes<HTMLButtonElement>, so an href prop is not part of the component API, and it will still render a <button>. [1]
    Supported approach: use a real link component/element (<a> / router <Link>) and apply the same classes/styles separately (i.e., don’t try to make Button behave like a link). [1]

Sources
[1] (github.com)

Citations:


Remove the Button wrapper from the anchor element to fix invalid interactive nesting.

The Button component from @doctorproject/react always renders a native <button> element and does not support link modes (href, as, asChild, etc.). Wrapping it in an <a> tag (lines 446–450) creates invalid nested interactive elements (<a><button /></a>), which breaks keyboard navigation and screen reader behavior. Style the anchor directly instead, or use a styled link component without the Button wrapper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/settings/subscription/page.tsx around lines 446 - 453, The
current anchor wrapping the Button component causes invalid nested interactive
elements; remove the Button component usage inside the <a> and instead render
the anchor as the clickable element for the mailto (use the same href
"mailto:[email protected]?subject=Upgrade%20Request"), applying the Button's
visual styling and the Zap icon/text directly to the anchor (keep the Zap icon
component and the conditional text "{tier === 'free' ? 'Upgrade to Pro' :
'Upgrade to Power'}"); ensure the anchor has the same style props
(textDecoration: "none", flexShrink: 0) and include any ARIA attributes or
role/tabIndex if necessary for keyboard/accessibility.

Comment on lines +2069 to +2085
<Button
onClick={() =>
setCurrentSlide((s) => Math.max(0, s - 1))
}
disabled={currentSlide === 0}
variant="outline"
style={{
width: 40,
height: 40,
border: "2px solid var(--bru-black)",
background: "var(--bru-cream)",
cursor:
currentSlide === 0 ? "not-allowed" : "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
opacity: currentSlide === 0 ? 0.35 : 1,
}}
>
<ChevronLeft size={20} />
</button>
</Button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Label the carousel navigation buttons.

These buttons only render chevron icons right now, so assistive tech gets unnamed controls. Add explicit labels for previous/next navigation.

Suggested fix
                           <Button
+                            aria-label="Previous slide"
                             onClick={() =>
                               setCurrentSlide((s) => Math.max(0, s - 1))
                             }
@@
                           <Button
+                            aria-label="Next slide"
                             onClick={() =>
                               setCurrentSlide((s) =>
                                 Math.min(totalSlides - 1, s + 1),

Also applies to: 2097-2116

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(protected)/studio/page.tsx around lines 2069 - 2085, The carousel nav
buttons currently render only icons (e.g., the Button wrapping <ChevronLeft />
and the matching next button), leaving them unlabeled for assistive tech; update
the Button components used for carousel navigation (the Button around
ChevronLeft and the corresponding ChevronRight button that uses setCurrentSlide
and currentSlide) to include an accessible label—add a prop such as
aria-label="Previous slide" for the left button and aria-label="Next slide" for
the right button (or an equivalent accessibleName) and ensure disabled state
remains intact so screen readers receive the label even when disabled.

Comment on lines 88 to 92
<Card
key={c.id}
variant="flat"
style={{ padding: "var(--bru-space-3)", cursor: "pointer" }}
style={{ padding: "var(--drp-space-3)", cursor: "pointer" }}
onClick={() => onSelect(c)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In drproject-coding/doctorproject-react, what DOM element does the Card component render, and does passing onClick make it keyboard-accessible by default?

💡 Result:

  • What DOM element does Card render? It always renders a <div> (return <div className={classes} {...props}>…</div>), and its props are typed as HTMLAttributes<HTMLDivElement>. [1]

  • Does passing onClick make it keyboard-accessible by default? No. Because it’s still a plain <div>, passing onClick only adds mouse/pointer click handling; it does not automatically add keyboard focus (tabIndex), a semantic role (e.g. role="button"), or Enter/Space key handling. [1]

Sources
[1] src/components/Card/Card.tsx in drproject-coding/doctorproject-react (GitHub). (github.com)

Citations:


Make the Card keyboard-accessible

The Card component renders a plain <div> element. Passing onClick to a <div> does not make it keyboard-accessible—keyboard users cannot tab to it or activate it with Enter/Space. This is an accessibility violation.

Wrap the Card in a <button> element or use an interactive element like <a>. Alternatively, add role="button", tabIndex={0}, and keyboard event handlers (Enter/Space) to the Card itself.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/campaigns/CampaignList.tsx` around lines 88 - 92, The Card
currently only has onClick so it's not keyboard-accessible; update
CampaignList.tsx to make the Card interactive by either wrapping the Card in a
semantic interactive element (e.g., a <button> that preserves the Card styles)
or by adding role="button", tabIndex={0}, and an onKeyDown handler that calls
onSelect(c) when Enter or Space is pressed; ensure the onClick handler still
calls onSelect(c), prevent default for Space key to avoid page scroll, and keep
the visual styles intact for the Card key={c.id} usage.

Comment on lines +199 to 210
<div style={{ marginTop: "var(--drp-space-3)" }}>
<Textarea
value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
style={{
width: "100%",
minHeight: 300,
fontFamily: "monospace",
fontSize: "var(--bru-text-sm)",
fontSize: "var(--drp-text-sm)",
lineHeight: 1.6,
}}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add an accessible name to the manual edit field.

The new Textarea has no label, so assistive tech will expose it as an unnamed control. That makes the manual fallback flow difficult to use for screen-reader users.

Suggested fix
         <div style={{ marginTop: "var(--drp-space-3)" }}>
+          <label
+            htmlFor="guardrail-recovery-editor"
+            style={{
+              display: "block",
+              marginBottom: "var(--drp-space-2)",
+              fontSize: "var(--drp-text-sm)",
+              fontWeight: 500,
+            }}
+          >
+            Edit content manually
+          </label>
           <Textarea
+            id="guardrail-recovery-editor"
             value={editedContent}
             onChange={(e) => setEditedContent(e.target.value)}
             style={{
               width: "100%",
               minHeight: 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
<div style={{ marginTop: "var(--drp-space-3)" }}>
<Textarea
value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
style={{
width: "100%",
minHeight: 300,
fontFamily: "monospace",
fontSize: "var(--bru-text-sm)",
fontSize: "var(--drp-text-sm)",
lineHeight: 1.6,
}}
/>
<div style={{ marginTop: "var(--drp-space-3)" }}>
<label
htmlFor="guardrail-recovery-editor"
style={{
display: "block",
marginBottom: "var(--drp-space-2)",
fontSize: "var(--drp-text-sm)",
fontWeight: 500,
}}
>
Edit content manually
</label>
<Textarea
id="guardrail-recovery-editor"
value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
style={{
width: "100%",
minHeight: 300,
fontFamily: "monospace",
fontSize: "var(--drp-text-sm)",
lineHeight: 1.6,
}}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/factory/GuardrailRecovery.tsx` around lines 199 - 210, The
Textarea field (value={editedContent}, onChange={setEditedContent}) is unlabeled
and will be exposed as an unnamed control to assistive tech; add an accessible
name by either giving the Textarea a descriptive aria-label or associating it
with a visible or visually-hidden <label> via an id/for pairing (ensure the id
is unique), e.g., add id="manual-edit" to the Textarea and render a
corresponding label or set aria-label="Manual fallback edit" so screen readers
announce the control.

Comment on lines +220 to 240
<Button
type="button"
size="sm"
variant={!providerFilter ? "primary" : "ghost-bordered"}
onClick={() => setProviderFilter(null)}
style={!providerFilter ? chipActive : chipBase}
>
All
</button>
</Button>
{providers.map((p) => (
<button
<Button
type="button"
size="sm"
key={p}
variant={providerFilter === p ? "primary" : "ghost-bordered"}
onClick={() =>
setProviderFilter(providerFilter === p ? null : p)
}
style={providerFilter === p ? chipActive : chipBase}
>
{p}
</button>
</Button>
))}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Expose the active filter/model state to assistive tech.

These buttons only show selection visually. Add pressed/selected semantics so the current provider and current model are announced non-visually too.

Suggested fix
             <Button
               type="button"
               size="sm"
               variant={!providerFilter ? "primary" : "ghost-bordered"}
+              aria-pressed={!providerFilter}
               onClick={() => setProviderFilter(null)}
             >
               All
             </Button>
             {providers.map((p) => (
               <Button
                 type="button"
                 size="sm"
                 key={p}
                 variant={providerFilter === p ? "primary" : "ghost-bordered"}
+                aria-pressed={providerFilter === p}
                 onClick={() =>
                   setProviderFilter(providerFilter === p ? null : p)
                 }
               >
                 {p}
               </Button>
             ))}
@@
               <Button
                 type="button"
                 variant={isSelected ? "ghost-bordered" : "ghost-bordered"}
+                aria-pressed={isSelected}
                 onClick={() => onSelectModel(model.id)}
                 style={{

Also applies to: 293-310

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/settings/StraicoModelPicker.tsx` around lines 220 - 240, The
provider and model filter buttons only show visual selection; update
StraicoModelPicker to expose state to assistive tech by adding the appropriate
ARIA pressed/selected attributes: for each provider Button (the buttons rendered
from providers and the "All" Button) set aria-pressed={providerFilter === p}
(and for "All" aria-pressed={providerFilter === null}) and likewise for the
model selection buttons set aria-pressed or aria-selected against the current
model state; ensure the attributes are bound to the same state used by
setProviderFilter and the model setter so screen readers announce the active
provider and model.

…tire codebase

- Remove all lucide-react imports from app pages and components
- Replace icon JSX with Unicode text characters per DS icon strategy:
  Upload→⬆, Wand2→✦, FileText→▣, X/Close→✕, Check→✓
  Edit2→✎, User→◉, Mic→♪, Target→◎, Users→◈, Eye→⊙, EyeOff→⊘
  Copy→⧉, ChevronLeft/ArrowLeft→‹, AlertCircle→⚠
- Use DS Loader component from @doctorproject/react for loading states
- Fix Button iconLeft prop: string-only values, no JSX (DS requirement)
- Migrate all pages to @doctorproject/react component primitives
- Remove spin keyframe animations (replaced by Loader component)
- Covers: all protected pages, onboarding flow, auth, settings,
  factory, knowledge, campaigns, calendar, library, components
- Remove ResponsiveGrid (not in @doctorproject/react), replace with div
- Fix Heading level prop: string literals → numeric (1|2|3|4|5|6)
- Move style props off Heading to wrapping div (Heading rejects style)
- Fix Text: remove color prop (not accepted by @doctorproject/react Text)
- Fix Button: ensure children present even for icon-only buttons
- Add transpilePackages for @doctorproject/react in next.config.ts
Tailwind Preflight (included in @tailwind base) resets body font-family,
background, and color because our design system CSS imports come before
@tailwind base in globals.css. Re-apply the correct base styles after
the Tailwind directives so the design system wins the cascade.

- font-family: var(--drp-font-primary) → Visby font
- background: var(--drp-cream) → #F2F2F2
- color: var(--drp-black) → #121212
- Toast: use DS Toast + Button (keep ToastContext/useToast API)
- ConfirmDialog: use DS Modal (keep ConfirmContext/useConfirm API)
- Header: replace raw buttons with DS Button (with aria-label on icon buttons)
- Sidebar: replace sidebar toggle raw button with DS Button ghost/icon
- PostGenerator: replace raw textarea with DS Textarea; fix ref via wrapper div + querySelector pattern
- create/page: replace raw input[text] with DS Input; adjust overlay button to bottom:8
- AiToolsSection: replace raw input[radio] with DS Radio
- studio/page: replace raw input[radio] with DS Radio; convert label card to div+onClick
- EvidencePack: replace raw input[checkbox] with DS Checkbox

All raw <input>, <button>, <textarea> replaced except intentional exceptions:
- EnhancedDropdown search input (breaks custom dropdown layout)
- Hidden file inputs (no DS equivalent; Dropzone UX incompatible)
- Inline tag-chip input in onboarding wizard (transparent, embedded in chip container)
…X syntax

Step 1 atomic sweep: replaced the only remaining raw <button> in
debug/page.tsx with DS <Button variant="primary">. Added exception
comments for raw elements in EnhancedDropdown.tsx that must stay raw
(dropdown trigger + filter tags depend on custom CSS classes). Fixed
invalid JSX syntax where a comment was inside a .map() callback.
…p 2)

Wrap all protected pages with DS Container for consistent max-width and
responsive padding. Replace raw <h1>/<h3>/<h4> elements with DS Heading
component across dashboard, calendar, library, learning, knowledge,
analytics, settings, login, and reset-password pages.
Refactor brand, campaigns, create, factory, and studio pages to use
Stack, Heading, Tabs, and other DS components instead of raw HTML
elements and inline flex styles. Only pure layout containers converted;
divs with mixed concerns (borders, backgrounds, child flex) left as-is.
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