diff --git a/apps/web/src/app/create/worksheets/components/OrientationPanel.tsx b/apps/web/src/app/create/worksheets/components/OrientationPanel.tsx index e2f81d58..e14faf28 100644 --- a/apps/web/src/app/create/worksheets/components/OrientationPanel.tsx +++ b/apps/web/src/app/create/worksheets/components/OrientationPanel.tsx @@ -3,8 +3,9 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import * as Tooltip from '@radix-ui/react-tooltip' import { css } from '@styled/css' -import { useMemo } from 'react' -import { estimateUniqueProblemSpace } from '../utils/validateProblemSpace' +import { useMemo, useState } from 'react' +import { validateProblemSpace } from '../utils/validateProblemSpace' +import type { ProblemSpaceValidation } from '../utils/validateProblemSpace' import { getDefaultColsForProblemsPerPage } from '../utils/layoutCalculations' interface OrientationPanelProps { @@ -65,45 +66,66 @@ export function OrientationPanel({ onProblemsPerPageChange(count, newCols) } + const [dropdownOpen, setDropdownOpen] = useState(false) + const total = problemsPerPage * pages const problemsForOrientation = orientation === 'portrait' ? [6, 8, 10, 12, 15] : [8, 10, 12, 15, 16, 20] - // Calculate problem space and determine risk for each page count - const estimatedSpace = useMemo(() => { + /** + * Get validation result for a specific page count + * Uses the same validateProblemSpace() function as the banner warning + */ + const getValidationForPageCount = (pageCount: number): ProblemSpaceValidation | null => { // Skip validation for mastery + mixed mode (same logic as WorksheetPreviewContext) if (mode === 'mastery' && operator === 'mixed') { - return Infinity // No validation + return null } - return estimateUniqueProblemSpace(digitRange, pAnyStart, operator) - }, [digitRange, pAnyStart, operator, mode]) - - // Helper to get duplicate risk for a given page count - const getDuplicateRisk = (pageCount: number): 'none' | 'caution' | 'danger' => { - if (estimatedSpace === Infinity) return 'none' - - const requestedProblems = problemsPerPage * pageCount - const ratio = requestedProblems / estimatedSpace - - if (ratio < 0.5) return 'none' - if (ratio < 0.8) return 'caution' - return 'danger' + return validateProblemSpace(problemsPerPage, pageCount, digitRange, pAnyStart, operator) } - // Helper to get tooltip message for a page count - const getTooltipMessage = (pageCount: number): string | null => { - if (estimatedSpace === Infinity) return null + /** + * Map duplicate risk levels to UI warning states + * - none: No visual warning (green/default) + * - low/medium: Caution (yellow) + * - high/extreme: Danger (red) + */ + const getRiskLevel = ( + validation: ProblemSpaceValidation | null + ): 'none' | 'caution' | 'danger' => { + if (!validation) return 'none' + const { duplicateRisk } = validation + if (duplicateRisk === 'none') return 'none' + if (duplicateRisk === 'low' || duplicateRisk === 'medium') return 'caution' + return 'danger' // high or extreme + } - const requestedProblems = problemsPerPage * pageCount - const ratio = requestedProblems / estimatedSpace + /** + * Format validation warnings for tooltip display + */ + const getTooltipMessage = (validation: ProblemSpaceValidation | null): string | null => { + if (!validation || validation.warnings.length === 0) return null + return validation.warnings.join('\n\n') + } - if (ratio < 0.5) return null // No warning needed + /** + * Get the mildest (most severe) warning among dropdown items (4, 10, 25, 50, 100) + * Returns 'none' if no warnings, 'caution' if any caution, 'danger' if any danger + */ + const getDropdownMildestWarning = (): 'none' | 'caution' | 'danger' => { + const dropdownPageCounts = [4, 10, 25, 50, 100] + let hasCaution = false + let hasDanger = false - if (ratio < 0.8) { - return `⚠️ Limited variety: ${requestedProblems} problems requested, ~${Math.floor(estimatedSpace)} unique available.\n\nSome duplicates may occur.` + for (const pageCount of dropdownPageCounts) { + const risk = getRiskLevel(getValidationForPageCount(pageCount)) + if (risk === 'danger') hasDanger = true + if (risk === 'caution') hasCaution = true } - return `🚫 Too many duplicates: ${requestedProblems} problems requested, only ~${Math.floor(estimatedSpace)} unique available.\n\nConsider:\n• Reduce to ${Math.max(1, Math.floor((estimatedSpace * 0.5) / problemsPerPage))} pages\n• Increase digit range\n• Lower regrouping %` + if (hasDanger) return 'danger' + if (hasCaution) return 'caution' + return 'none' } return ( @@ -448,8 +470,9 @@ export function OrientationPanel({ {/* Quick select buttons for 1-3 pages */} {[1, 2, 3].map((pageCount) => { const isSelected = pages === pageCount - const risk = getDuplicateRisk(pageCount) - const tooltipMessage = getTooltipMessage(pageCount) + const validation = getValidationForPageCount(pageCount) + const risk = getRiskLevel(validation) + const tooltipMessage = getTooltipMessage(validation) const button = ( - + + + + + + {pages > 3 && getTooltipMessage(getValidationForPageCount(pages)) && ( + + + {getTooltipMessage(getValidationForPageCount(pages))} + + + + )} + + {[4, 10, 25, 50, 100].map((pageCount) => { const isSelected = pages === pageCount - return ( + const validation = getValidationForPageCount(pageCount) + const risk = getRiskLevel(validation) + const tooltipMessage = getTooltipMessage(validation) + + const menuItem = ( - {pageCount} - {isSelected && } +
+ {/* Warning indicator dot (same style as page buttons 1-3) */} + {risk !== 'none' && ( + + )} + {pageCount} pages +
+
+ {isSelected && } +
) + + // Wrap in tooltip if there's a warning message + if (tooltipMessage) { + return ( + + + {menuItem} + + + {tooltipMessage} + + + + + + ) + } + + return menuItem })}