Skip to content

UI Component Library - Deep Dive Documentation

Generated: 2026-02-25 Scope: Sass-boilerplate-frontend-v1/src/components/ (ui/, custom/, editor/, layout/) Files Analyzed: 98 (40 ui/ + 34 custom/ + 15 editor/ + 9 layout/) Lines of Code: ~13,700 Known Issues Found: ~405 (9 Critical, 40 High, ~160 Medium, ~200 Low) Dead Code Files: 18 Workflow Mode: Exhaustive Deep-Dive Verification: Tailwind config verified, 5 user flows traced, modal/dialog audit completed


Overview

The UI Component Library is a four-layer system providing the visual foundation for the SaaS boilerplate frontend (Next.js 15 + React 19 + TailwindCSS 4):

Layer Directory Files LOC Role
Primitives components/ui/ 40 ~4,100 shadcn/Radix UI wrappers + custom primitives
Composed components/custom/ 34 ~6,700 Application-level wrappers, form fields, layout widgets
Editor components/editor/ 15 ~2,350 BlockNote rich text editor ecosystem
Layout components/layout/ 9 ~570 Header, footer, navbar shells (public site)

Key Architectural Decisions: - shadcn/ui v2 pattern (no forwardRef, uses data-slot attributes) — React 19 compatible - class-variance-authority (cva) used only in button.jsx and badge.jsx - cn() from @/lib/utils (clsx + tailwind-merge) used in ~80% of components - Custom Tailwind tokens: text-light, text-light-4, text-light-50, shadow-box, border-gold, bg-gold - No test files exist for any component


Critical Issues Summary

# Severity File Issue
C1 CRITICAL editor/blocknote/CustomToolbar.jsx:26 Hooks called after conditional return — violates React Rules of Hooks, will crash
C2 CRITICAL custom/input.jsx:42 Switch fallthrough bugcase "small" falls through to default, "small" size completely broken. Also h-0 makes input invisible
C3 CRITICAL editor/blocknote/IframeEmbed.jsx XSS risk — user-provided URLs embedded as iframe src without sanitization or allowlist
C4 CRITICAL custom/permission-wrapper.jsx:41 router.push() called during render — React anti-pattern, causes "Cannot update during render" warnings
C5 CRITICAL editor/createEditor.jsx Hardcoded external API URLs to landing-pages-shoshin-tech.onrender.com (different service, "articals" typo)
C6 CRITICAL ui/popover.jsx:17 CSS class in data-slot attributedata-slot="popover-trigger !shadow-none" puts Tailwind class in wrong attribute
C7 CRITICAL custom/pricing-plan.jsx:97 Crash on empty datadata?.tabs[0]?.id crashes when data=[] (default) since arrays have no .tabs
C8 CRITICAL globals.css / 7 component files gold color tokens COMPLETELY UNDEFINEDtext-gold, bg-gold, border-gold have no @theme inline mapping or CSS var. 16 uses across 7 files silently fail (tabs, multi-select, blog cards, select, form focus states)
C9 CRITICAL custom/modal.jsx (32 consumers) Zero accessibility on the most-used modal — no role="dialog", no aria-modal, no focus trap, no Escape key. Used by 18 direct consumers + 14 via ConfirmationModal. Radix AlertDialog (defined but never imported) is the correct pattern for the 14 destructive confirmations

Complete File Inventory

Layer 1: components/ui/ — 40 files, ~4,100 LOC

Radix UI Wrappers (17 files)

File LOC Radix Package Exports Issues Dead?
accordion.jsx 67 @radix-ui/react-accordion Accordion, AccordionItem, AccordionTrigger, AccordionContent 4 — expandIconClassName misapplied to trigger not icon
alert-dialog.jsx 139 @radix-ui/react-alert-dialog AlertDialog + 10 sub-components 2 — data-slot on Portal ignored, AlertDialogAction missing data-slot DEAD
avatar.jsx 47 @radix-ui/react-avatar Avatar, AvatarImage, AvatarFallback 0
checkbox.jsx 26 @radix-ui/react-checkbox Checkbox 0
collapsible.jsx 23 @radix-ui/react-collapsible Collapsible, CollapsibleTrigger, CollapsibleContent 3 — no cn(), no className destructuring, inconsistent
dialog.jsx 134 @radix-ui/react-dialog Dialog + 9 sub-components 3 — no prop to hide close X button
dropdown-menu.jsx 224 @radix-ui/react-dropdown-menu 15 sub-components 4 — bg-card vs bg-popover inconsistency, dead DropdownMenuPortal export
label.jsx 24 @radix-ui/react-label Label 4 — text-light-4 custom class, unused React import
popover.jsx 48 @radix-ui/react-popover Popover, PopoverTrigger, PopoverContent, PopoverAnchor 1 — C6: CSS class in data-slot attribute
progress.jsx 30 @radix-ui/react-progress Progress 0
radio-group.jsx 44 @radix-ui/react-radio-group RadioGroup, RadioGroupItem 0
select.jsx 167 @radix-ui/react-select 10 sub-components 4 — hardcoded text-black, !py-5 override, w-fit default
sheet.jsx 141 @radix-ui/react-dialog (repurposed) Sheet + 7 sub-components 1 — no side prop validation
slider.jsx 57 @radix-ui/react-slider Slider 2 — defaults to 2 thumbs when no value given
switch.jsx 30 @radix-ui/react-switch Switch 0
tabs.jsx 63 @radix-ui/react-tabs Tabs, TabsList, TabsTrigger, TabsContent 5 — border-gold, shadow-box custom classes
tooltip.jsx 56 @radix-ui/react-tooltip Tooltip, TooltipTrigger, TooltipContent, TooltipProvider 6 — light/dark text color mismatch (white on white), each tooltip creates own Provider (breaks group delay)

Library Wrappers (2 files)

File LOC Library Exports Issues Dead?
calendar.jsx 175 react-day-picker Calendar, CalendarDayButton 5 — buttonVariant name confusion
command.jsx 155 cmdk Command + 8 sub-components 2 DEAD

CVA-Powered Components (2 files)

File LOC Variants Exports Issues
button.jsx 56 7 visual + 4 sizes Button, buttonVariants 3 — no focus-visible ring on most variants (a11y), success uses hardcoded green
badge.jsx 44 4 visual Badge, badgeVariants 2 — secondary hardcodes text-white

Custom Components (19 files)

File LOC Purpose Issues Dead?
alert.jsx 104 Alert banners (4 variants) 5 — variant prop not inherited via Context, each sub-component needs manual variant
author-card.jsx 23 Author attribution card 6 — no dark mode on text-gray-600 DEAD
card.jsx 101 Card layout system 5 — text-light, shadow-box custom tokens, CardTitle renders div not heading
chart.jsx 310 Recharts wrapper with theming 6 — {item.value && ...} won't render when value=0, dangerouslySetInnerHTML
code-box.jsx 47 Code snippet with copy 7 — children assumed string, no aria-label on copy button, memory leak potential
dashboard-skeletons.jsx 172 Dashboard loading skeletons 6 — h-68 not standard Tailwind, no a11y loading indication DEAD
date-inputs.jsx 629 5 date/time pickers 8 — TimePicker icon logic inverted, onChange fires on mount, useEffect missing deps, labels lack htmlFor
heading.jsx 11 Section heading (h2) 6 — hardcoded h2, text-light custom class, unused React import
input.jsx 25 Text input primitive 4 — unused React import, no size variants
loading.jsx 28 Loading spinner animation 8 — zero accessibility (no role, aria-busy, aria-label), external CSS dependency
multi-select.jsx 421 Feature-rich multi-select 9 — no keyboard navigation, no ARIA attributes, dead ellipsis prop, z-index 99999
SearchableDropdown.jsx 146 Searchable dropdown (old) 5 — DUPLICATE of lowercase version, no dark mode
searchable-dropdown.jsx 146 Searchable dropdown (new) 4 — DUPLICATE, sm:static positioning bug DEAD
section-heading.jsx 11 Section heading (h2) 2 — unused React import, hardcoded h2
skeleton.jsx 16 Loading placeholder 3 — hardcoded colors instead of bg-muted, no a11y
spinner.jsx 19 Inline loading spinner 0 — cleanest component (has role="status", aria-label)
textarea.jsx 39 Auto-resize textarea 6 — textareaClass instead of className, no forwardRef (breaks React Hook Form), missing "use client"
theme-toggle.jsx 87 Light/Dark/System toggle 5 — layout shift on hydration (returns null until mounted), inconsistent icon sizing

Layer 2: components/custom/ — 34 files, ~6,700 LOC

Form Field Wrappers (8 files)

File LOC Wraps Key Props Issues
input.jsx 103 ui/input + ui/label label, error, icon, size, forgotPassBtn 7 — C2: switch fallthrough (small size broken), password toggle not keyboard accessible, missing "use client"
checkbox.jsx 66 ui/checkbox + ui/label label, error, labelPosition, checked 4 — hardcoded error ID, no aria-describedby
textarea.jsx 76 ui/textarea + ui/label label, error, size, autoResize 7 — hardcoded error ID, no aria-describedby, !important overuse
radio.jsx 119 ui/radio-group + ui/label label, options, error, orientation 5 — dual callback (onChange + onValueChange), border-red ineffective on RadioGroup
date-picker.jsx 145 ui/date-inputs inputType, label, error 6 — relative imports, abbreviated prop names confusing
file-upload.jsx 509 ui/input + ui/label label, multiple, preview, labelStyle 9 — "drag and drop" text is a lie (no onDrop handler), fake progress bar, Next Image missing width/height, memory leak
select.jsx (MultiSelect) 337 None (custom) options, value, isMulti, enableApiSearch 8 — no keyboard nav, search filters on value not label, tag displays value not label
select.jsx (NestedSelect) ui/select data, configs, variant — (included in above LOC) — className silently ignored on Radix Select

Layout & Display Components (14 files)

File LOC Purpose Consumers Issues Dead?
accordion.jsx 143 Data-driven accordion (4 variants) 1 2
advance-table.jsx 129 Compound table system 1 4 — near-duplicate of data-table.jsx
analytic-card.jsx 88 Dashboard KPI card 5 5 — change always green regardless of ±
avatar.jsx 128 Avatar with status indicator 1 5 — no alt text on images, status invisible to screen readers
breadcrumb.jsx 65 Data-driven breadcrumbs 1 3 — misleading separator prop name
breadcrumb2.jsx 63 Hero-style breadcrumb banner 4 5 — CSS typo: to gray-50 missing hyphen breaks gradient
carousel.jsx 410 3 carousel variants 1 7 — thumbnails not keyboard accessible, no pause on hover, hardcoded SVG colors
data-not-found.jsx 15 Empty state placeholder 1 3
data-table.jsx 150 Compound table (near-dup of advance-table) 16 5 — near-duplicate of advance-table.jsx, missing displayName on Heading
pagination.jsx 335 Client + Server pagination 19 6 — 95% duplicated between Client/Server variants, zero items edge case
permission-badge-group.jsx 33 Permission badges with tooltip 1 6
pricing-plan.jsx 598 3 pricing display variants 1 8 — C7: crash on empty data, hardcoded 4-col grid, no onClick on Buy buttons
roles-badge-group.jsx 67 Role badges with tooltip 0 7 — hardcoded demo data as default, duplicate IDs DEAD
user-avatar-group.jsx 75 Overlapping avatar stack 1 9 — imports TooltipProvider but never uses it, returns undefined
File LOC Purpose Consumers Issues Dead?
dropdown-menu.jsx 225 Recursive navigation dropdown 0 7 — complex triple state management, no depth limit DEAD
mobile-navigation.jsx 98 Mobile hamburger menu overlay 1 6 — hardcoded auth links, "Get Started" button does nothing
nav-permission-wrapper.jsx 63 Permission-based nav visibility 3 6 — singular/plural naming mismatch (permission vs permissions)
navItem.jsx 51 Polymorphic nav item renderer 1 6 — camelCase filename (all others kebab-case), empty href default
File LOC Purpose Consumers Issues
modal.jsx 90 Custom modal with transitions 18 8 — no focus trap, no Escape key handler, no role="dialog", no scroll lock, double close race condition
confirmation-modal.jsx 151 Delete/warning/confirm dialog 14 4 — custom spinner instead of Button isLoading, onClose not called on confirm
unauthorized-error.jsx 73 Full-screen access denied 0 4 — no focus trap on fullscreen overlay

Auth & Permission Guards (2 files)

File LOC Purpose Consumers Issues
permission-wrapper.jsx 67 Route/section permission guard 44 8 — C4: router.push during render, logic ordering bug, hasPermission called 4x per render
nav-permission-wrapper.jsx 63 Nav item permission gate 3 6

Feedback Components (5 files)

File LOC Purpose Consumers Issues Dead?
alerts.jsx 186 Dismissible alert (7 themes) 1 4 — no cn() usage, JSDoc default mismatch
button.jsx 120 Button with ripple effect 45 6 — duplicate secondary key (second overwrites first), ripple CSS external
error-page.jsx 729 Animated error pages (8 variants) 0 7 — styled-jsx may not work with App Router, no dark mode DEAD
success-page.jsx 627 Animated success pages (7 variants) 0 8 — variants object recreated every render (600 LOC per render), no dark mode DEAD
toggle-switch.jsx 135 Toggle switch with loading 0 5 — thumb translate conflict between hardcoded and size config DEAD

Layer 3: components/editor/ — 15 files, ~2,350 LOC

File LOC Purpose Issues Dead?
blocknote/BlocknoteEditor.jsx 109 Core BlockNote editor 3 — dead state variables (isLink, selectedBlocks), private TipTap API access
blocknote/BlocknoteEditorWrapper.jsx 23 SSR-disabled dynamic wrapper 1 — redundant wrapper
blocknote/BlocknoteViewer.jsx 51 HTML content viewer/editor 5 — misleading name ("Viewer" but editable), missing html dependency in useEffect, unused useState import DEAD
blocknote/Blockquote.jsx 115 Custom blockquote block 4 — no dark mode (hardcoded #f5f5f5), 36 lines commented-out code
blocknote/CodeBlock.jsx 68 Custom code block 4 — language prop unused (no syntax highlighting), conflicting overflow properties
blocknote/CustomDragMenu.jsx 157 Drag-handle context menu 3 — no keyboard accessibility for submenu, ResetBlockTypeItem exported but unused
blocknote/CustomSlashMenu.jsx 23 Slash command menu 2 — missing ARIA listbox/option roles
blocknote/CustomToolbar.jsx 435 Formatting toolbar 7 — C1: hooks after conditional return (WILL CRASH), dead state vars, private API access, duplicate dark:invert class
blocknote/Divider.jsx 30 Horizontal divider block 3 — no dark mode, parse targets <section> instead of <hr>
blocknote/IframeEmbed.jsx 305 Video/iframe embed block 4 — C3: XSS risk (unsanitized user URLs as iframe src), deprecated frameBorder
blocknote/LinkBlock.jsx 186 Link block with popover 3 — popover starts open by default, no URL validation
blocknote/Mention.jsx 22 @mention inline content 3 — no dark mode, no parse function (can't round-trip HTML)
blocknote/TextEditor.jsx 155 High-level editor wrapper 5 — dead useEffect, artificial 100ms delay, console.log in production
blocknote/editor-styles.css 471 Editor stylesheet 3 — excessive !important, duplicate properties, inconsistent CSS vars
createEditor.jsx 286 Legacy "Create Article" form 11 — C5: hardcoded external API URLs, console.logs, typo hanldeText, hardcoded initial content DEAD

Layer 4: components/layout/ — 9 files, ~570 LOC

File LOC Purpose Issues Dead?
footer.jsx 210 Public site footer 6 — duplicate formatLink function, 63 lines commented-out social media code, generic "Logo" alt text
footer-wrapper.jsx 17 Server component data fetcher 2 — no error handling, sequential fetches (should be Promise.all)
footer-skeleton.jsx 45 Footer loading skeleton 2 — trailing spaces in classNames
footer-credit.jsx 39 Dashboard footer credit bar 6 — missing rel="noopener noreferrer" on target="_blank" (tabnapping vulnerability), hardcoded algorizetech.com fallback
header.jsx 179 Public site header 5 — duplicated placeholder header (unmounted branch), "Get Started" button has no href/action
header-wrapper.jsx 10 Server component data fetcher 2 — no error handling, sequential fetches
header-skeleton.jsx 30 Header loading skeleton 2 — truncated class size-9 -full (should be rounded-full) — skeleton circles won't be round
navbar.jsx 22 Minimal blog navbar 5 — hardcoded "Logo" text, !important overrides DEAD
no-scroll-restoration.jsx 18 Disable scroll restoration 2 — no cleanup on unmount (leaves scrollRestoration=manual forever) DEAD

Dead Code Analysis

18 Files Never Imported (Total ~2,700 LOC)

File LOC Reason Action
ui/alert-dialog.jsx 139 Zero imports found Delete or document as available primitive
ui/author-card.jsx 23 Zero imports found Delete
ui/dashboard-skeletons.jsx 172 Zero imports found Delete
ui/command.jsx 155 Zero imports found Delete or document as available primitive
ui/searchable-dropdown.jsx 146 Duplicate of PascalCase version, lowercase never imported Delete
custom/dropdown-menu.jsx 225 Zero imports found (NOT the ui/ dropdown-menu) Delete — mobile-navigation uses it via relative import... actually: imported by navItem.jsx and mobile-navigation.jsx. Re-check: agent found consumers. This is NOT dead.
custom/error-page.jsx 729 Zero imports found Keep — useful error boundary component, just unused so far
custom/success-page.jsx 627 Zero imports found Keep — useful success page, just unused so far
custom/toggle-switch.jsx 135 Zero imports found Keep — functional component, just unused
custom/unauthorized-error.jsx 73 Only imported by permission-wrapper.jsx which IS used NOT dead — consumer agent was wrong
custom/roles-badge-group.jsx 67 Zero imports found by grep Likely dead
layout/navbar.jsx 22 Zero imports found Delete — superseded by header.jsx
layout/no-scroll-restoration.jsx 18 Commented out in main layout Delete or uncomment
editor/blocknote/BlocknoteViewer.jsx 51 Zero imports found Delete
editor/createEditor.jsx 286 Zero imports found, uses external API Delete — legacy/copied code

Note: custom/dropdown-menu.jsx and custom/unauthorized-error.jsx initially flagged as dead but actually have consumers via relative imports. Corrected above.

Confirmed Dead: ~14 files, ~2,400 LOC removable


Cross-Cutting Issues

1. Accessibility Gaps (Systemic)

Components with ZERO accessibility: multi-select.jsx, loading.jsx, modal.jsx, dashboard-skeletons.jsx, all 3 carousels, all editor custom blocks

Common missing patterns: - aria-describedby for error messages (ALL form wrappers: input, checkbox, textarea, radio, date-picker, file-upload) - aria-required for required fields (ALL form wrappers show visual * only) - role="dialog" + aria-modal on modals (modal.jsx has neither) - Focus trap in modals/overlays (modal.jsx, unauthorized-error.jsx) - Keyboard navigation in custom dropdowns (multi-select.jsx, SearchableDropdown.jsx, all carousel thumbnails) - aria-label on icon-only buttons (copy button in code-box.jsx, password toggle in input.jsx)

Only spinner.jsx has proper accessibility (role="status", aria-label="Loading")

2. Duplicate / Near-Duplicate Components

Pair Overlap Recommendation
advance-table.jsx vs data-table.jsx ~90% identical structure, minor styling differences Consolidate into one configurable component
SearchableDropdown.jsx vs searchable-dropdown.jsx Same export name, nearly identical code, one has dark mode Delete lowercase version
heading.jsx vs section-heading.jsx Both render <h2> with similar styling Consolidate
PaginationClient vs PaginationServer ~95% identical rendering logic, different state source Extract shared rendering component
ui/button vs custom/button Pages import both — unclear which to use Document canonical usage; custom/button adds ripple/loading
error-page.jsx vs success-page.jsx Same architecture (variant map + inline SVG + styled-jsx) Could share base component

3. Custom Tailwind Tokens (Require Config)

Token Used In Standard Alternative
text-light card, heading, breadcrumb2, user-avatar-group text-foreground
text-light-4 label, card, analytic-card, permission-badge-group, user-avatar-group text-muted-foreground
text-light-50 file-upload, textarea, date-picker description text-muted-foreground
shadow-box card, tabs, dashboard-skeletons, header shadow-sm or shadow-md
border-gold tabs trigger border-primary
bg-gold/50, text-gold multi-select, custom/select bg-primary/50, text-primary
custom-shadow editor, advance-table Standard Tailwind shadow
h-68 dashboard-skeletons Not standard; needs config entry or use h-[17rem]

4. Export Pattern Inconsistencies

Pattern Files Convention
Named exports 28 ui/ + 18 custom/ export { Component } or export function Component
Default exports 2 ui/ + 9 custom/ export default Component
Named + default mixed Not used

Default export files (inconsistent): author-card, code-box, heading, loading (ui/), analytic-card, breadcrumb2, button, confirmation-modal, data-not-found, mobile-navigation, navItem, toggle-switch, unauthorized-error (custom/)

5. Import Path Inconsistencies

Layer Pattern Convention
pages/ui/ @/components/ui/... Alias (correct)
pages/custom/ @/components/custom/... Alias (correct)
custom/ui/ ../ui/... Relative (should be alias)
layout/custom/ ../custom/... Relative (should be alias)
layout/ui/ ../ui/... Relative (should be alias)

6. Missing "use client" Directives

Components using hooks but missing the directive (work only because parents are client components):

File Hooks Used
custom/input.jsx useState
ui/textarea.jsx useRef, useCallback, useEffect

7. Private API Access (Fragile)

4 editor files access TipTap internals that will break on upgrades: - editor._tiptapEditor._state?.doc?.textContent (BlocknoteEditor, BlocknoteViewer) - editor._tiptapEditor._state?.history$?.done?.items?.values (CustomToolbar) - editor._tiptapEditor.on("transaction", ...) (BlocknoteViewer, CustomToolbar)


Dependency Graph

Import Flow (Clean, Acyclic)

app/ pages/
    ├──→ components/custom/   (form wrappers, layout widgets, modals)
    │       │
    │       └──→ components/ui/   (shadcn primitives, Radix wrappers)
    │               │
    │               └──→ @radix-ui/*, lucide-react, class-variance-authority, cmdk, react-day-picker
    ├──→ components/layout/   (header, footer, navbar)
    │       │
    │       ├──→ components/custom/   (button, navItem, mobile-navigation)
    │       └──→ components/ui/       (skeleton, button)
    ├──→ components/editor/   (BlockNote editor ecosystem)
    │       │
    │       └──→ @blocknote/*, @/lib/editor, @/lib/icons, @/lib/constant
    └──→ components/ui/   (direct imports bypassing custom/ layer)

No circular dependencies detected. The flow is strictly downward.

Entry Points (not imported by other component files)

  • All layout/*-wrapper.jsx files (server components, imported by app layouts)
  • layout/footer-credit.jsx (imported by app layouts)
  • editor/blocknote/TextEditor.jsx (imported by page components)
  • All custom/ components (imported by page components)

Leaf Nodes (don't import other component files)

  • ui/skeleton.jsx, ui/spinner.jsx, ui/input.jsx, ui/label.jsx (import only from @/lib/utils)
  • ui/collapsible.jsx (imports only from Radix)
  • custom/data-not-found.jsx (imports only lucide icon)

Integration Points

External Libraries

Library Version Used By Purpose
@radix-ui/* (13 packages) Latest 17 ui/ components Accessible primitives
lucide-react 0.510 ~50 components Icons
class-variance-authority button, badge Variant management
@blocknote/core + react + mantine 0.35 12 editor files Rich text editor
react-day-picker calendar.jsx Date selection
cmdk command.jsx (dead) Command palette
framer-motion 12.23 dropdown-menu, mobile-navigation, header Animations
recharts 2.15 chart.jsx Charts
next-themes 0.4 theme-toggle Theme persistence
react-redux nav-permission-wrapper, permission-wrapper, footer-credit, header Auth/settings state
next/image 15.3 author-card, carousel, file-upload, footer, header, createEditor Image optimization
next/link 15.3 breadcrumb2, navItem, mobile-navigation, footer, header, navbar Client-side navigation
next/navigation 15.3 pagination (server), permission-wrapper Router/searchParams
next/dynamic 15.3 BlocknoteEditorWrapper SSR-disabled import

Redux Store Dependencies

Component Selector Data Used
permission-wrapper.jsx state.auth user.permissions
nav-permission-wrapper.jsx state.auth user.permissions
footer-credit.jsx getSettingByGroup("site") site name, copyright
header.jsx (none — receives settings as props)
createEditor.jsx (dead) state.auth.user data.accessToken

Server Component Data Fetching

Component Fetches API
header-wrapper.jsx getNavigationLinks("navbar"), getSettingByGroup("site") Public API
footer-wrapper.jsx getNavigationLinks("footer"), getNavigationLinks("bottombar"), getSettingByGroup("site") Public API

Contributor Checklist

Risks & Gotchas

  1. Two Button components coexist: ui/button (shadcn, stateless) and custom/button (ripple effect, loading state). Pages import both inconsistently. Always use custom/button for interactive buttons, ui/button for composition inside other ui/ components.
  2. Custom Tailwind tokens are project-specific: text-light, text-light-4, shadow-box, etc. must be defined in tailwind.config. If missing, styles silently fail.
  3. Editor uses private TipTap APIs: Any BlockNote upgrade may break the toolbar, undo/redo, and text extraction.
  4. modal.jsx has no focus trap or Escape key: Unlike dialog.jsx (Radix-based), the custom modal is not WCAG compliant for keyboard users.
  5. SearchableDropdown case sensitivity: On Linux/CI (case-sensitive FS), SearchableDropdown.jsx and searchable-dropdown.jsx are different files. On Windows, they collide.
  6. Form field wrappers hide description on error: checkbox.jsx, date-picker.jsx, and textarea.jsx all do !error && description, hiding potentially useful context.

Pre-Change Verification Steps

  1. Search for all consumers of a component before modifying its props: grep -r "from.*components/ui/COMPONENT" src/
  2. Check both @/components/ and ../ import patterns
  3. Verify custom Tailwind tokens exist in tailwind.config.js
  4. Test dark mode for any component with hardcoded colors
  5. Test keyboard navigation for any interactive component

Suggested Tests

  1. Keyboard navigation: Tab through all form components, verify focus ring visible on all button variants
  2. Screen reader audit: Run axe-core on pages using modal.jsx, multi-select.jsx, loading.jsx
  3. Dark mode visual check: All components with hardcoded hex colors (editor blocks, error/success pages, dashboard-skeletons)
  4. Edge cases: pagination with 0 items, pricing-plan with empty data, slider with single value, date-inputs onChange on mount

Architecture & Design Patterns

Component Hierarchy Pattern

ui/ (primitives)         → shadcn/Radix wrappers, zero business logic
custom/ (composed)       → Form fields with label/error/description
    ↑                      Layout widgets (cards, tables, modals)
    ↑                      Permission guards
pages/ (consumers)       → Import from both layers

Variant Management Approaches (4 different patterns)

  1. CVA (button.jsx, badge.jsx): Best practice, type-safe, composable
  2. Object map (alert.jsx, alerts.jsx, pricing-plan.jsx): Manual variant lookup
  3. Data attributes (dropdown-menu.jsx data-variant): CSS-driven variants
  4. Switch statement (custom/input.jsx, custom/textarea.jsx): Imperative, error-prone

State Management Patterns

Pattern Components Recommended?
Stateless/controlled Most ui/ components Yes
Internal useState modal, code-box, date-inputs, multi-select Fine for encapsulated UI state
Semi-controlled (internal sync from prop) toggle-switch Risky — can desync
Redux selector permission-wrapper, nav-permission-wrapper, footer-credit Fine for auth/settings
React Context chart.jsx (ChartContext) Good for compound components

Modification Guidance

To Add a New UI Primitive

  1. Create file in src/components/ui/ following shadcn v2 pattern
  2. Include "use client" if component uses hooks
  3. Add data-slot="component-name" attribute
  4. Use cn() for className merging
  5. Export as named export: export { ComponentName }
  6. If wrapping Radix, import primitives as namespace: import * as Primitive from "@radix-ui/react-*"

To Add a New Form Field Wrapper

  1. Create file in src/components/custom/
  2. Import base primitive from @/components/ui/ (use alias, not relative)
  3. Follow the established pattern: label + input + error + description
  4. Add aria-describedby linking error message to input (currently missing in all wrappers)
  5. Use dynamic error IDs: ${id}-error not hardcoded strings

To Remove Dead Code

Safe to delete (zero consumers confirmed): - ui/author-card.jsx, ui/dashboard-skeletons.jsx, ui/searchable-dropdown.jsx (lowercase) - layout/navbar.jsx - editor/createEditor.jsx, editor/blocknote/BlocknoteViewer.jsx

Review before deleting (may be intentionally kept for future use): - ui/alert-dialog.jsx, ui/command.jsx (standard shadcn primitives, useful to keep) - custom/error-page.jsx, custom/success-page.jsx, custom/toggle-switch.jsx (functional, just unused)


Tailwind Token Verification (Against Actual Config)

The project uses Tailwind CSS v4 — all theme customization via @theme inline in globals.css (no tailwind.config.js).

Token Status

Token Status Impact Files Affected
text-light / bg-light DEFINED Working 87 files (428 uses)
text-light-3 DEFINED Working 19 files (93 uses)
text-light-4 DEFINED Working 60 files (219 uses)
text-light-50 MISSING @theme inline mapping CSS var exists (--light-50) but Tailwind won't generate utility 10 files (15 uses)
text-gold / bg-gold / border-gold COMPLETELY UNDEFINED No CSS var, no theme mapping — all gold colors silently fail 7 files (16 uses)
shadow-box Defined as CSS class in @layer base Working (not a Tailwind utility, plain CSS class) 48 files (64 uses)
custom-shadow Defined as CSS class Working 39 files (39 uses)
h-68 Not in default spacing scale May not generate (default jumps 64→72) 2 files
bg-primary, text-foreground, etc. (shadcn tokens) All DEFINED Working Ubiquitous
animate-accordion-up/down Depends on tw-animate-css package Likely working (verify at runtime) 1 file

Critical: Gold Color Failure

These files reference undefined gold tokens — all gold styling is invisible: - ui/tabs.jsxborder-gold on active tab trigger - ui/multi-select.jsxbg-gold/50, text-gold on selected items (4 uses) - custom/select.jsxbg-gold/50, text-gold on selected values (2 uses) - components/blog-card.jsxbg-gold, text-gold (3 uses) - pages/blogs/blogs-form.jsxfocus:border-gold on inputs (4 uses) - pages/pages/page-form.jsxfocus:border-gold (1 use) - dashboard/settings/profile/page.jsxbg-gold, hover:bg-gold (1 use)

Fix Required for text-light-50

The CSS var exists in :root and .dark blocks but is missing from @theme inline:

/* Add to @theme inline block in globals.css: */
--color-light-50: var(--light-50);

Additional Config Issues

  • --color-light is duplicated on lines 42-43 of globals.css @theme inline block
  • --card is defined twice in :root (lines 63 and 92) — second definition wins
  • --light-80 has CSS var but no @theme inline mapping (currently unused, no impact)

src/lib/utils.js Exports

Export Purpose Issue
cn(...inputs) clsx + twMerge class merger Clean
getReadableDate(date) Date formatting via date-fns Clean
capitalize(string) Capitalize each word Clean
generateAvatar(userName) Returns initials ("JD") Clean
formatText(text) Splits on <br>, renders via dangerouslySetInnerHTML XSS vector if text not sanitized
normalizeSlug(slug) URL-safe slug via transliteration Clean
getIconComponent(iconName) Dynamic Lucide icon lookup Clean
handleDownloadJson(data) Triggers JSON file download Clean

Three Competing Implementations

Implementation File Consumers Accessibility
custom/modal custom/modal.jsx 18 direct + 14 via ConfirmationModal = 32 total ZERO — no focus trap, no Escape, no role="dialog", no aria-modal
ui/dialog (Radix) ui/dialog.jsx 5 (mostly old/inactive code) Full — focus trap, Escape, ARIA, screen reader label
ui/alert-dialog (Radix) ui/alert-dialog.jsx 0 (never imported) Full — blocks Escape dismiss for destructive actions

Plus 3 fully hand-rolled inline modals (email template HTML export/import, AI listening) with zero component reuse.

Consumer Map

custom/modal direct consumers (18 files): Settings (2FA setup, disable 2FA, backup codes, create setting), Contact details, Menu builder (old + v2: MenuSelector, EditItemDialog), Blog preview, Package details, Login (2FA), User form, Invite form, Customize manager (5+ instances), Role form, Permission form, Email templates (preview + save)

custom/confirmation-modal consumers (14 files): All destructive actions: delete setting, delete menu item, delete menu, delete contact, delete/restore/permanent-delete pages, blogs (6 instances), packages (6 instances), users (6 instances), invited users, permissions, permission groups, roles, email templates (6 instances)

Consolidation Recommendation

Phase 1 (Critical): Rewrite ConfirmationModal to wrap ui/alert-dialog instead of custom/modal. This fixes the 14 highest-risk consumers (all destructive actions) in one change. AlertDialog prevents accidental Escape dismissal of delete confirmations.

Phase 2: Create a DialogModal wrapper around ui/dialog matching the Modal/ModalHeader/ModalBody/ModalFooter API. Migrate the 18 form/detail consumers.

Phase 3: Migrate the 3 hand-rolled inline modals. Delete custom/modal.jsx.


User Flow Analysis (5 Flows Traced)

Component Consistency Matrix

Component Login Dashboard Blog Create Blog List Users Landing
Button source custom/ ui/ custom/ ui/ ui/ + custom/ custom/ + ui/
Input source ui/ N/A custom/ + native <input> ui/ ui/ N/A
Checkbox source native <input> N/A custom/Input (as checkbox) ui/Checkbox ui/Checkbox + native N/A
PermissionWrapper No Yes (1x) Yes (3x nested!) Yes Yes (8x) No
Modal system custom/modal (2FA) N/A custom/modal (preview) ConfirmationModal custom/modal + ConfirmationModal N/A

Flow-Specific Findings

Flow 1 — Login: - 2FA modal during login uses custom/modal with zero accessibility — security-critical UI with no focus trap - "Remember Me" is a raw <input type="checkbox"> instead of ui/Checkbox - Cookie set with sameSite: "strict" storing JWT in non-httpOnly cookie

Flow 2 — Dashboard: - WelcomeSection references "Total Games" and "This Month Games" — gaming template remnant, not SaaS - Dead import: TopGamesUpvotes imported then BlogByAuthor re-imported from same file - Sidebar navigation is fully backend-driven via useNavigationLinks("dashboard-sidebar-top"/"bottom")

Flow 3 — Blog Create/Edit: - Triple PermissionWrapper nesting on create: page → page component → form component all check blog.create independently - Blog title/slug use raw native <input> elements, SEO fields use custom/Input, categories use ui/MultiSelect — 3 different input approaches in one form - Blog listing uses ui/button but blog form uses custom/button — different APIs/behavior - BlockNote editor integrates via react-hook-form Controller → TextEditorBlocknoteEditorWrapper (dynamic, SSR=false) → BlocknoteEditor

Flow 4 — User Management: - PermissionWrapper (ALL required) vs NavPermissionWrapper (ANY required) logic mismatch for array permissions - User form submit button gated by permission="role.view" — misleading error message says "not enough permission to create a user" when actual missing permission is role.view - isInviteOnly checkbox is native <input type="checkbox"> while table rows use ui/Checkbox

Flow 5 — Landing Page: - All navigation is backend-driven (Menu Builder), but landing page content (features, testimonials, pricing, FAQ) is hardcoded in @/constants/landing-page-data - "Get Started" buttons in hero and header have no href or onClick — completely non-functional - layout/navbar.jsx exists but is never used — dead code superseded by header.jsx - PricingSection annual tab has pricing data hardcoded in JSX while monthly tab uses constants file

Cross-Flow Issues

  1. Two Button components used interchangeably with no rationale: ui/button (shadcn, stateless, variant-based) vs custom/button (ripple effect, loading state, different variant names). Some pages use both.

  2. Two Input components used interchangeably: ui/input (bare primitive) vs custom/input (wraps with label, error, password toggle). Blog form uses custom + native. User form uses ui.

  3. PermissionWrapper called redundantly: Blog create has 3 nested checks for the same permission. This causes 3 sequential loading states and 3 independent error displays.

  4. Native HTML elements mixed with component library: Native <input type="checkbox">, <input type="date">, <textarea>, and <input type="text"> appear alongside their component library equivalents in the same forms.


Security Analysis (Red Team vs Blue Team)

Full attack-defend analysis: docs/extra-docs/ui-component-attack-scenarios.md

5 Kill Chains Identified

# Kill Chain Severity Blast Radius Fix Effort
KC1 GenericPage Stored XSS → Account Takeover — backend has ZERO sanitization for GenericPage, rendered publicly via dangerouslySetInnerHTML, JWT in non-httpOnly cookie, no CSP CRITICAL ALL public visitors 45 min
KC2 Social Login Token Forgeryjwt-decode doesn't verify signatures, JWT passed as URL query param HIGH OAuth users 2 hrs
KC3 Clickjacking — no X-Frame-Options, no CSP frame-ancestors HIGH Admin users 15 min
KC4 Blog Preview XSS — preview renders raw HTML before backend sanitization MEDIUM Admins reviewing drafts 30 min
KC5 IframeEmbed Phishing — no URL allowlist, no sandbox attribute MEDIUM Editor users 30 min

Defense Gaps (Blue Team)

Gap Status
CSP Headers ABSENT — no security headers at all
Input Sanitization ABSENT — 9 dangerouslySetInnerHTML sites, 0 sanitized, no DOMPurify installed
httpOnly Cookie ABSENT — JWT readable via document.cookie
Iframe sandbox ABSENT — zero sandbox attributes in codebase
Permission enforcement CLIENT-ONLY — trivially bypassed via DevTools
Image domain allowlist Wildcard ** — SSRF via image optimization

P0 Fixes (1.25 hours total)

  1. Add sanitizer to GenericPage backend (45 min) — closes KC1
  2. Add security headers to next.config.mjs (30 min) — closes KC1 + KC3

Generated by document-project workflow (deep-dive mode) — Exhaustive scan, 2026-02-25 Base Documentation: docs/index.md Analysis Mode: Exhaustive — every line of every file read by 14 parallel agent teams (9 scan + 3 verify + 2 security) Verification: Tailwind config tokens verified, 5 user flows traced end-to-end, modal/dialog audit completed Elicitation: Red Team vs Blue Team security analysis — 5 kill chains, defense gap matrix, hardening plan