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 bug — case "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 attribute — data-slot="popover-trigger !shadow-none" puts Tailwind class in wrong attribute |
| C7 | CRITICAL | custom/pricing-plan.jsx:97 |
Crash on empty data — data?.tabs[0]?.id crashes when data=[] (default) since arrays have no .tabs |
| C8 | CRITICAL | globals.css / 7 component files |
gold color tokens COMPLETELY UNDEFINED — text-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 |
Navigation Components (4 files)¶
| 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 |
Modal & Dialog Components (3 files)¶
| 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.jsxfiles (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¶
- Two Button components coexist:
ui/button(shadcn, stateless) andcustom/button(ripple effect, loading state). Pages import both inconsistently. Always usecustom/buttonfor interactive buttons,ui/buttonfor composition inside other ui/ components. - Custom Tailwind tokens are project-specific:
text-light,text-light-4,shadow-box, etc. must be defined intailwind.config. If missing, styles silently fail. - Editor uses private TipTap APIs: Any BlockNote upgrade may break the toolbar, undo/redo, and text extraction.
modal.jsxhas no focus trap or Escape key: Unlikedialog.jsx(Radix-based), the custom modal is not WCAG compliant for keyboard users.- SearchableDropdown case sensitivity: On Linux/CI (case-sensitive FS),
SearchableDropdown.jsxandsearchable-dropdown.jsxare different files. On Windows, they collide. - Form field wrappers hide description on error:
checkbox.jsx,date-picker.jsx, andtextarea.jsxall do!error && description, hiding potentially useful context.
Pre-Change Verification Steps¶
- Search for all consumers of a component before modifying its props:
grep -r "from.*components/ui/COMPONENT" src/ - Check both
@/components/and../import patterns - Verify custom Tailwind tokens exist in
tailwind.config.js - Test dark mode for any component with hardcoded colors
- Test keyboard navigation for any interactive component
Suggested Tests¶
- Keyboard navigation: Tab through all form components, verify focus ring visible on all button variants
- Screen reader audit: Run axe-core on pages using
modal.jsx,multi-select.jsx,loading.jsx - Dark mode visual check: All components with hardcoded hex colors (editor blocks, error/success pages, dashboard-skeletons)
- 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)¶
- CVA (
button.jsx,badge.jsx): Best practice, type-safe, composable - Object map (
alert.jsx,alerts.jsx,pricing-plan.jsx): Manual variant lookup - Data attributes (
dropdown-menu.jsxdata-variant): CSS-driven variants - 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¶
- Create file in
src/components/ui/following shadcn v2 pattern - Include
"use client"if component uses hooks - Add
data-slot="component-name"attribute - Use
cn()for className merging - Export as named export:
export { ComponentName } - If wrapping Radix, import primitives as namespace:
import * as Primitive from "@radix-ui/react-*"
To Add a New Form Field Wrapper¶
- Create file in
src/components/custom/ - Import base primitive from
@/components/ui/(use alias, not relative) - Follow the established pattern: label + input + error + description
- Add
aria-describedbylinking error message to input (currently missing in all wrappers) - Use dynamic error IDs:
${id}-errornot 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.jsx — border-gold on active tab trigger
- ui/multi-select.jsx — bg-gold/50, text-gold on selected items (4 uses)
- custom/select.jsx — bg-gold/50, text-gold on selected values (2 uses)
- components/blog-card.jsx — bg-gold, text-gold (3 uses)
- pages/blogs/blogs-form.jsx — focus:border-gold on inputs (4 uses)
- pages/pages/page-form.jsx — focus:border-gold (1 use)
- dashboard/settings/profile/page.jsx — bg-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:
Additional Config Issues¶
--color-lightis duplicated on lines 42-43 ofglobals.css@theme inlineblock--cardis defined twice in:root(lines 63 and 92) — second definition wins--light-80has CSS var but no@theme inlinemapping (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 |
Modal vs Dialog Audit¶
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 → TextEditor → BlocknoteEditorWrapper (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¶
-
Two Button components used interchangeably with no rationale:
ui/button(shadcn, stateless, variant-based) vscustom/button(ripple effect, loading state, different variant names). Some pages use both. -
Two Input components used interchangeably:
ui/input(bare primitive) vscustom/input(wraps with label, error, password toggle). Blog form uses custom + native. User form uses ui. -
PermissionWrapper called redundantly: Blog create has 3 nested checks for the same permission. This causes 3 sequential loading states and 3 independent error displays.
-
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 Forgery — jwt-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)¶
- Add sanitizer to GenericPage backend (45 min) — closes KC1
- 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