chore: migrate @bruddle/react → @doctorproject/react (bruddle-design-system)#2
chore: migrate @bruddle/react → @doctorproject/react (bruddle-design-system)#2drproject-coding wants to merge 30 commits intomainfrom
Conversation
- 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
…n, CalendarView color tokens
- 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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis 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
Sequence Diagram(s)Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes Possibly related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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 | 🔴 CriticalMove the
useStatehook above theeditingearly return to prevent hook order violations.The
useStatecall at line 59 is only reachable whenediting === false. If theeditingprop 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're different from others in your space. What'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 | 🟠 MajorFix 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, withouttype="button", the button defaults totype="submit"and will submit any parent form. Move the heading outside the button (or use a<span>), addtype="button", and expose the expanded state witharia-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 | 🟠 MajorAdd
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 | 🟠 MajorMake 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 | 🟠 MajorSet
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 | 🟠 MajorReject duplicate pillar ids before creating a custom pillar.
This path uses the trimmed label as the persisted
idwithout 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 | 🟡 MinorSet 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 | 🟠 MajorMake the popup keyboard-operable, not just clickable.
The menu items are
divelements withrole="option", but the popup itself is not alistbox, 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 | 🟠 MajorWait for
user.idbefore 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 | 🟠 MajorAvoid 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 | 🟡 MinorMissing
jest.useFakeTimers()setup for timer test.Line 181 calls
jest.advanceTimersByTime(3000)butjest.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 | 🟡 MinorUse a truthy check when deriving the source tag.
post.strategyOutput ?? post.formatkeeps"", so a post with an emptystrategyOutputand a validformatnow 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 | 🟠 MajorClamp
pageafter the result set shrinks.If the last item on a later page is deleted,
pagecan stay greater thantotalPages.pagedPoststhen 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 spindefinition.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 withouttype. In HTML those default tosubmit, so this component can trigger accidental form submission if it is ever rendered inside a form. Please set both totype="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 theAlertcomponent doesn't require children when atitleis 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
nullor updating the component to make children optional whentitleis 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
LoaderinResetPasswordContent(lines 89-91) includes a descriptivelabelprop, but the Suspense fallbackLoader(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__closecouples this test to an internal class name, which is exactly what changed in this migration. Prefer exposing an accessible name ordata-testidfromPostEditorModaland 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/reactin package.json for clarity.While
package-lock.jsonalready pins this dependency to a specific commit SHA (7d8311d48e34634950d48c50204eecd0acff20d8), making the pinning explicit inpackage.jsonimproves 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 usevar(--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/__footerspacing before the mobile overrides.Right now
drp-card__bodyanddrp-card__footeronly 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: 8which 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 theInputcomponent.🤖 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 usingvar(--drp-grey),var(--drp-cream), andvar(--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
textorplaintextto 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
currentDatebut also callssetCurrentDate. 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
currentDatefrom 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), butgetStatusColorClassesstill 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-infoinstyles/doctorproject/tokens.css), but they need to be wired intotailwind.config.tsor 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 usevar(--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 assumesgetStatusColorClassesalways returns a space-separated string withbg-*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
BruLoaderimplies 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:--roundsetsborder-radius: 0.The class
.drp-chart-legend__dot--roundsuggests a circular dot, but it setsborder-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-bounceanddrp-anim-pulseuse 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
⛔ Files ignored due to path filters (1)
package-lock.jsonis 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.tsxapp/(onboarding)/layout.tsxapp/(onboarding)/onboarding/review/page.tsxapp/(onboarding)/onboarding/start/page.tsxapp/(onboarding)/onboarding/upload/page.tsxapp/(onboarding)/onboarding/wizard/[step]/page.tsxapp/(protected)/analytics/page.tsxapp/(protected)/brand/page.tsxapp/(protected)/calendar/page.tsxapp/(protected)/campaigns/[campaignId]/idea/[slotOrder]/page.tsxapp/(protected)/campaigns/[campaignId]/page.tsxapp/(protected)/campaigns/page.tsxapp/(protected)/create/page.tsxapp/(protected)/dashboard/page.tsxapp/(protected)/factory/page.tsxapp/(protected)/knowledge/page.tsxapp/(protected)/layout.tsxapp/(protected)/learning/page.tsxapp/(protected)/library/[id]/page.tsxapp/(protected)/library/page.tsxapp/(protected)/settings/page.tsxapp/(protected)/settings/profile/page.tsxapp/(protected)/settings/subscription/page.tsxapp/(protected)/studio/page.tsxapp/auth/callback/page.tsxapp/forgot-password/page.tsxapp/globals.cssapp/layout.tsxapp/login/page.tsxapp/reset-password/page.tsxapp/signup/page.tsxcomponents/ConfirmDialog.tsxcomponents/EnhancedDropdown.tsxcomponents/Header.tsxcomponents/PostEditorModal.tsxcomponents/PostGenerator.tsxcomponents/PostGoalSelector.tsxcomponents/SchedulePostModal.tsxcomponents/Toast.tsxcomponents/TonePromptPreviewModal.tsxcomponents/brand/BrandSection.tsxcomponents/brand/sections/AiToolsSection.tsxcomponents/brand/sections/OffersSection.tsxcomponents/brand/sections/PillarsSection.tsxcomponents/brand/sections/PositioningSection.tsxcomponents/brand/sections/ProfileSection.tsxcomponents/brand/sections/StrategySection.tsxcomponents/brand/sections/VoiceSection.tsxcomponents/calendar/CalendarView.tsxcomponents/calendar/ScoreBadge.tsxcomponents/campaigns/BatchProgress.tsxcomponents/campaigns/CampaignAnalytics.tsxcomponents/campaigns/CampaignCalendar.tsxcomponents/campaigns/CampaignFilters.tsxcomponents/campaigns/CampaignList.tsxcomponents/campaigns/CampaignSetup.tsxcomponents/campaigns/CampaignSummaryRow.tsxcomponents/campaigns/IdeaInbox.tsxcomponents/campaigns/IdeaStatusBadge.tsxcomponents/create/ContentAngleChips.tsxcomponents/create/PostStructureCards.tsxcomponents/factory/CreatePostDialog.tsxcomponents/factory/DraftEditor.tsxcomponents/factory/EvidencePack.tsxcomponents/factory/FormattedOutput.tsxcomponents/factory/GuardrailRecovery.tsxcomponents/factory/LearningPhaseResult.tsxcomponents/factory/PipelineStepper.tsxcomponents/factory/PostReview.tsxcomponents/factory/ResearchBrief.tsxcomponents/factory/RewriteInstructions.tsxcomponents/factory/Scorecard.tsxcomponents/factory/SessionHistory.tsxcomponents/factory/TopicProposals.tsxcomponents/knowledge/DocumentEditor.tsxcomponents/knowledge/ExtractFlow.tsxcomponents/knowledge/ImportFlow.tsxcomponents/knowledge/VersionHistory.tsxcomponents/learning/FeedbackHistory.tsxcomponents/learning/PatternList.tsxcomponents/learning/RuleProposal.tsxcomponents/learning/SignalCounts.tsxcomponents/settings/StraicoModelPicker.tsxdocs/plans/2026-03-05-brand-coherent-recommendations-design.mddocs/plans/2026-03-05-brand-coherent-recommendations.mddocs/plans/2026-03-05-brand-page.mddocs/plans/2026-03-05-full-spec-implementation.mddocs/plans/2026-03-07-campaign-idea-lifecycle.mddocs/plans/2026-03-08-create-redesign.mddocs/plans/2026-03-09-doctorproject-react-migration.mddocs/plans/2026-03-09-ui-revamp-design-system.mdpackage.jsonstyles/bruddle/cards.cssstyles/bruddle/tables.cssstyles/bruddle/tags.cssstyles/bruddle/tokens.cssstyles/bruddle/utilities.cssstyles/doctorproject/animations.cssstyles/doctorproject/buttons.cssstyles/doctorproject/cards.cssstyles/doctorproject/charts.cssstyles/doctorproject/composites.cssstyles/doctorproject/forms.cssstyles/doctorproject/index.cssstyles/doctorproject/layout.cssstyles/doctorproject/media.cssstyles/doctorproject/modals.cssstyles/doctorproject/pagination.cssstyles/doctorproject/responsive.cssstyles/doctorproject/tables.cssstyles/doctorproject/tags.cssstyles/doctorproject/tokens.cssstyles/doctorproject/typography.cssstyles/doctorproject/utilities.csstailwind.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
| "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 |
There was a problem hiding this comment.
🧩 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)
PYRepository: 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)" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for the definition of --drp-purple-rgb in CSS files
rg -n '\-\-drp-purple-rgb' --type cssRepository: 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 -20Repository: 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 -60Repository: 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/nullRepository: drproject-coding/doctorpost-v12
Length of output: 517
🏁 Script executed:
# Search for --drp-purple definition specifically
rg -n '\-\-drp-purple[^-]' --type cssRepository: drproject-coding/doctorpost-v12
Length of output: 5100
🏁 Script executed:
cat -n styles/doctorproject/tokens.cssRepository: 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.
| <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> | ||
| } | ||
| /> |
There was a problem hiding this comment.
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.
| <Button onClick={fetchData} disabled={loading} variant="outline"> | ||
| {loading ? <Loader size="sm" label="Loading..." /> : "Refresh"} |
There was a problem hiding this comment.
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.
| ``` | ||
| 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 | ||
| ``` |
There was a problem hiding this comment.
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.
| - 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) |
There was a problem hiding this comment.
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.
| ### Migration | ||
|
|
||
| - All existing rows with `generation_status = 'pending'` → rename semantically (handled in NCB via execute_sql) | ||
| - Add `post_uuid` column |
There was a problem hiding this comment.
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.
| - `app/(protected)/create/page.tsx` — add idea inbox, handle `?topicCard=` URL params | ||
| - `lib/knowledge/api.ts` — add `updateCampaignPostStatus`, `getValidatedIdeas`, `linkPostToCampaignSlot` |
There was a problem hiding this comment.
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 dataAlso 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.
| # 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 |
There was a problem hiding this comment.
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/reactcomponents
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
There was a problem hiding this comment.
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 | 🟠 MajorAssociate each label with its textarea.
The
<label>rendered byFieldLabelis visually correct, but it is not bound to eitherTextarea, 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 | 🟡 MinorAdd
aria-expandedattribute 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 thearia-expandedattribute 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 | 🟡 MinorClear the previous success timer before showing
Savedagain.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 | 🟡 MinorLet 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 | 🟠 MajorAdd 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 | 🟠 MajorAdd 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 | 🟠 MajorPreserve the unlimited sentinel for the
powertier.
TIER_LIMITS.powerisnull, but Line 116 turns it into5with?? 5. That makes the UI treat unlimited users as capped, so this branch renders a progress bar and the counter above shows/ 5instead 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 | 🔴 CriticalKeep 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 supportonBlur, 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 | 🟠 MajorCopy the rendered caption/post text, not always
post.content.
VisualViewandCarouselViewcan replacepost.contentwithformatted_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 | 🟠 MajorDon't flip the UI to
publishedon 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 | 🟠 MajorKeep picker sync one-way and parse date-only values locally.
With
currentDatein this effect's dependency list (line 34), clicking Previous/Next month snaps the grid back toselectedDateFromPickeras soon as a picker value exists. Also,new Date("YYYY-MM-DD")is UTC in JavaScript, so a U.S. user selecting2026-03-10can end up highlighting March 9 locally (lines 25 and 85). Parse the picker value into a local date and only resync whenselectedDateFromPickeritself 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 | 🟠 MajorAdd 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 | 🟠 MajorKeep refresh recoverable while loading.
fetchDatastill 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 | 🟡 MinorOnly show the empty proposals state after a successful proposals load.
proposals.length === 0is 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 | 🟡 MinorExpose the selected filter state with
aria-pressed.These act as toggle buttons, but the active state is only visual right now. Add
aria-pressedso 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: isActiveAlso 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 | 🟡 MinorAdd
aria-expandedfor 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 | 🟡 MinorKeep the submit label visible while loading.
When
loadingis 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 | 🟡 MinorRemove unsupported
color="mint"prop fromProgressBar.The ProgressBar component from
@doctorproject/reactdoes not accept acolorprop. All six other usages of ProgressBar in the codebase use only thevalueprop. The test mock confirms this pattern. Remove thecolor="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 | 🟡 MinorUse 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 | 🟡 MinorMake 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. AddinghtmlFor/idfixes 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 | 🟡 MinorUse 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 | 🟡 MinorThe 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 untilonCompletefires. Either surface success inside the saving branch or switch back topreviewbefore 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 | 🟡 MinorLabel 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 | 🟡 MinorRestore autofill metadata on the login fields.
The DS
Inputswap dropped the nativename/autoCompletehints, 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 | 🟡 MinorGive the filter control an accessible name.
This
Selectis rendered without a visible label oraria-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 | 🟡 MinorEnsure 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 thelabelprop 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 | 🟡 MinorThese assertions no longer prove the pillar rendered in the slot card.
getAllByText(...).length >= 1is 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 | 🟡 MinorAdd 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 | 🟡 MinorUse the existing display label instead of formatting the status slug inline.
Hyphenated states render as
To-review/To-publishhere.lib/calendarUtils.tslines 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
📒 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.tsxapp/(onboarding)/layout.tsxapp/(onboarding)/onboarding/review/page.tsxapp/(onboarding)/onboarding/start/page.tsxapp/(onboarding)/onboarding/upload/page.tsxapp/(onboarding)/onboarding/wizard/[step]/page.tsxapp/(protected)/analytics/page.tsxapp/(protected)/brand/page.tsxapp/(protected)/calendar/page.tsxapp/(protected)/campaigns/[campaignId]/idea/[slotOrder]/page.tsxapp/(protected)/campaigns/[campaignId]/page.tsxapp/(protected)/create/page.tsxapp/(protected)/factory/page.tsxapp/(protected)/layout.tsxapp/(protected)/learning/page.tsxapp/(protected)/library/[id]/page.tsxapp/(protected)/library/page.tsxapp/(protected)/settings/page.tsxapp/(protected)/settings/profile/page.tsxapp/(protected)/settings/subscription/page.tsxapp/(protected)/studio/page.tsxapp/forgot-password/page.tsxapp/login/page.tsxapp/reset-password/page.tsxapp/signup/page.tsxcomponents/ConfirmDialog.tsxcomponents/EnhancedDropdown.tsxcomponents/PostEditorModal.tsxcomponents/PostGenerator.tsxcomponents/PostGoalSelector.tsxcomponents/SchedulePostModal.tsxcomponents/Toast.tsxcomponents/TonePromptPreviewModal.tsxcomponents/brand/BrandSection.tsxcomponents/brand/sections/AiToolsSection.tsxcomponents/brand/sections/OffersSection.tsxcomponents/brand/sections/PillarsSection.tsxcomponents/brand/sections/PositioningSection.tsxcomponents/brand/sections/ProfileSection.tsxcomponents/brand/sections/StrategySection.tsxcomponents/brand/sections/VoiceSection.tsxcomponents/calendar/CalendarView.tsxcomponents/campaigns/CampaignAnalytics.tsxcomponents/campaigns/CampaignFilters.tsxcomponents/campaigns/CampaignList.tsxcomponents/campaigns/CampaignSetup.tsxcomponents/campaigns/CampaignSummaryRow.tsxcomponents/campaigns/IdeaInbox.tsxcomponents/create/ContentAngleChips.tsxcomponents/create/PostStructureCards.tsxcomponents/factory/CreatePostDialog.tsxcomponents/factory/DraftEditor.tsxcomponents/factory/EvidencePack.tsxcomponents/factory/FormattedOutput.tsxcomponents/factory/GuardrailRecovery.tsxcomponents/factory/LearningPhaseResult.tsxcomponents/factory/PipelineStepper.tsxcomponents/factory/PostReview.tsxcomponents/factory/RewriteInstructions.tsxcomponents/factory/SessionHistory.tsxcomponents/knowledge/ExtractFlow.tsxcomponents/knowledge/ImportFlow.tsxcomponents/learning/FeedbackHistory.tsxcomponents/learning/PatternList.tsxcomponents/learning/RuleProposal.tsxcomponents/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
| <span style={{ animation: "spin 1s linear infinite", display: "flex" }}> | ||
| <Loader size="sm" /> | ||
| </span> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if spin keyframes are defined in global styles
rg -n '@keyframes spin' --type cssRepository: 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 -50Repository: 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 -80Repository: drproject-coding/doctorpost-v12
Length of output: 2660
🏁 Script executed:
# Check if Tailwind is being used (animate-spin)
rg -l 'tailwind' | head -10Repository: drproject-coding/doctorpost-v12
Length of output: 310
🏁 Script executed:
# Search for Loader component usage patterns
rg -n 'Loader' -t tsx -t ts | head -30Repository: drproject-coding/doctorpost-v12
Length of output: 102
🏁 Script executed:
# Search for keyframes without file type restrictions
rg -n '@keyframes' --type-listRepository: 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 -100Repository: drproject-coding/doctorpost-v12
Length of output: 1948
🏁 Script executed:
# Find other uses of Loader component
rg -n 'Loader' | head -40Repository: 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.cssRepository: 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.cssRepository: 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:
- Define
@keyframes spin { to { transform: rotate(360deg); } }in this file's styles, or - Remove the wrapper span if the
Loadercomponent from@doctorproject/reactalready 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.
| <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" | ||
| /> |
There was a problem hiding this comment.
🧩 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 -30Repository: 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 -5Repository: 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 -20Repository: 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 -5Repository: 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 -20Repository: 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.
app/(protected)/analytics/page.tsx
Outdated
| <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> |
There was a problem hiding this comment.
🧩 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 -20Repository: 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 -nRepository: 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)forfontSize— no base variant exists; usevar(--drp-text-md)(14px) orvar(--drp-text-lg)(16px) insteadvar(--drp-text)forcolor— usevar(--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.
app/(protected)/calendar/page.tsx
Outdated
| <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> |
There was a problem hiding this comment.
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.
| <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.
app/(protected)/library/page.tsx
Outdated
| <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%" }} |
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
🧩 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:
Buttonalways renders a native<button>element with base classdrp-btnplus 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 onButtonHTMLAttributes<HTMLButtonElement>, so anhrefprop 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 makeButtonbehave like a link). [1]
Sources
[1] (github.com)
Citations:
- 1: https://github.com/drproject-coding/doctorproject-react/blob/main/src/components/Button/Button.tsx
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.
| <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> |
There was a problem hiding this comment.
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.
| <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)} |
There was a problem hiding this comment.
🧩 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
Cardrender? It always renders a<div>(return <div className={classes} {...props}>…</div>), and its props are typed asHTMLAttributes<HTMLDivElement>. [1] -
Does passing
onClickmake it keyboard-accessible by default? No. Because it’s still a plain<div>, passingonClickonly 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.
| <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, | ||
| }} | ||
| /> |
There was a problem hiding this comment.
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.
| <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.
| <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> | ||
| ))} |
There was a problem hiding this comment.
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.
Summary
@bruddle/react(fromdrproject-coding/bruddle-react) with the up-to-date@doctorproject/react(fromdrproject-coding/bruddle-design-system)drproject-coding/doctorproject-reactfrombruddle-design-system/packages/reactfor clean npm installstyles/bruddle/tostyles/doctorproject/(sourced directly from the design system)Changes
@bruddle/reactremoved,@doctorproject/reactadded--bru-*→--drp-*across 80+ filesbru-*→drp-*across 80+ files@bruddle/react→@doctorproject/reactin 56 source filesstyles/bruddle/deleted,styles/doctorproject/added (17 files from design system)drp-class selectorsTest plan
npm run buildexits 0 (TypeScript + Next.js build)tsc --noEmit— zero errorsdrp-CSS classes, zerobru-classes@doctorproject/reactJS chunk loads with 200 OK@bruddle/reactimports in source--bru-*CSS var references in sourceSummary by CodeRabbit
Style
UI Improvements
New Features
Tests
Chores