From 8df62d6a45b0dc556702b867522e8710307ff733 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Tue, 18 Nov 2025 09:58:54 -0600 Subject: [PATCH] feat: add dynamic layout preview component for orientation selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create LayoutPreview component: - Visual preview showing page orientation and problem grid - SVG-based rendering with proper aspect ratios - Full theme support (light/dark mode) - Can act as a button with onClick and isSelected props - Shows actual layout (cols × rows) or default for unselected orientation Update tab navigation: - Use LayoutPreview for layout button icon - Show actual worksheet layout in button preview - Pass pages count to layout button subtitle Update orientation selection: - Replace text labels and SVG icons with LayoutPreview - Buttons ARE the preview (no nested styling) - Show current layout if selected, defaults if not - Portrait: 3×5 (15 problems default), Landscape: 4×5 (20 problems default) - Clean, visual interface without text labels Benefits: - Instantly see what each orientation looks like - Preview matches actual worksheet layout - Consistent visual language across UI - More intuitive than text/emoji 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/OrientationPanel.tsx | 275 ++---------------- .../config-sidebar/LayoutPreview.tsx | 121 ++++++++ .../config-sidebar/TabNavigation.tsx | 14 +- 3 files changed, 163 insertions(+), 247 deletions(-) create mode 100644 apps/web/src/app/create/worksheets/components/config-sidebar/LayoutPreview.tsx diff --git a/apps/web/src/app/create/worksheets/components/OrientationPanel.tsx b/apps/web/src/app/create/worksheets/components/OrientationPanel.tsx index e14faf28..daf273b2 100644 --- a/apps/web/src/app/create/worksheets/components/OrientationPanel.tsx +++ b/apps/web/src/app/create/worksheets/components/OrientationPanel.tsx @@ -4,6 +4,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import * as Tooltip from '@radix-ui/react-tooltip' import { css } from '@styled/css' import { useMemo, useState } from 'react' +import { LayoutPreview } from './config-sidebar/LayoutPreview' import { validateProblemSpace } from '../utils/validateProblemSpace' import type { ProblemSpaceValidation } from '../utils/validateProblemSpace' import { getDefaultColsForProblemsPerPage } from '../utils/layoutCalculations' @@ -25,7 +26,7 @@ interface OrientationPanelProps { digitRange?: { min: number; max: number } pAnyStart?: number operator?: 'addition' | 'subtraction' | 'mixed' - mode?: 'smart' | 'mastery' + mode?: 'custom' | 'mastery' // Layout options problemNumbers?: 'always' | 'never' cellBorders?: 'always' | 'never' @@ -49,7 +50,7 @@ export function OrientationPanel({ digitRange = { min: 2, max: 2 }, pAnyStart = 0, operator = 'addition', - mode = 'smart', + mode = 'custom', problemNumbers = 'always', cellBorders = 'always', onProblemNumbersChange, @@ -188,250 +189,36 @@ export function OrientationPanel({ }, })} > - - + isSelected={orientation === 'landscape'} + /> diff --git a/apps/web/src/app/create/worksheets/components/config-sidebar/LayoutPreview.tsx b/apps/web/src/app/create/worksheets/components/config-sidebar/LayoutPreview.tsx new file mode 100644 index 00000000..f6786ddf --- /dev/null +++ b/apps/web/src/app/create/worksheets/components/config-sidebar/LayoutPreview.tsx @@ -0,0 +1,121 @@ +'use client' + +import { css } from '@styled/css' +import { useTheme } from '@/contexts/ThemeContext' + +interface LayoutPreviewProps { + orientation: 'portrait' | 'landscape' + cols: number + rows: number + className?: string + onClick?: () => void + isSelected?: boolean +} + +/** + * Visual preview of worksheet layout showing page orientation and problem grid + * Can act as a button when onClick is provided + */ +export function LayoutPreview({ + orientation, + cols, + rows, + className, + onClick, + isSelected = false, +}: LayoutPreviewProps) { + const { resolvedTheme } = useTheme() + const isDark = resolvedTheme === 'dark' + + // Page dimensions (aspect ratios) + const pageAspect = orientation === 'portrait' ? 8.5 / 11 : 11 / 8.5 + + // Scale to fit in button (max dimensions) + const maxSize = 48 + let pageWidth: number + let pageHeight: number + + if (orientation === 'portrait') { + pageHeight = maxSize + pageWidth = pageHeight * pageAspect + } else { + pageWidth = maxSize + pageHeight = pageWidth / pageAspect + } + + // Problem cell dimensions with padding + const padding = 3 + const cellWidth = (pageWidth - padding * 2) / cols + const cellHeight = (pageHeight - padding * 2) / rows + const cellPadding = 1 + + const svgContent = ( + + {/* Problem grid */} + {Array.from({ length: rows }).map((_, rowIdx) => + Array.from({ length: cols }).map((_, colIdx) => ( + + )) + )} + + ) + + // If used as a button, wrap in button element with styling + if (onClick) { + return ( + + ) + } + + // Otherwise just return the SVG with border + return ( +
+ {svgContent} +
+ ) +} diff --git a/apps/web/src/app/create/worksheets/components/config-sidebar/TabNavigation.tsx b/apps/web/src/app/create/worksheets/components/config-sidebar/TabNavigation.tsx index b7ad0bee..70cf57ce 100644 --- a/apps/web/src/app/create/worksheets/components/config-sidebar/TabNavigation.tsx +++ b/apps/web/src/app/create/worksheets/components/config-sidebar/TabNavigation.tsx @@ -3,6 +3,7 @@ import { css } from '@styled/css' import { useTheme } from '@/contexts/ThemeContext' import type { DisplayRules } from '../../displayRules' +import { LayoutPreview } from './LayoutPreview' import { ProblemPreview } from './ProblemPreview' export interface Tab { @@ -36,7 +37,7 @@ export const TABS: Tab[] = [ { id: 'layout', label: 'Layout', - icon: '📐', + icon: 'preview', subtitle: ({ orientation, problemsPerPage, cols, pages }) => { if (!orientation || !problemsPerPage || !cols || !pages) return null const orientationLabel = orientation === 'portrait' ? 'Portrait' : 'Landscape' @@ -182,7 +183,8 @@ export function TabNavigation({ {TABS.map((tab) => { const icon = getTabIcon(tab) const subtitle = getTabSubtitle(tab) - const showPreviewIcon = tab.icon === 'preview' && displayRules + const showLayoutPreview = tab.id === 'layout' && orientation && problemsPerPage && cols + const showScaffoldingPreview = tab.id === 'scaffolding' && displayRules // All operator symbols (+, −, ±) are ASCII characters that need larger font size const isOperatorSymbol = tab.id === 'operator' @@ -240,7 +242,13 @@ export function TabNavigation({ flexShrink: 0, })} > - {showPreviewIcon ? ( + {showLayoutPreview ? ( + + ) : showScaffoldingPreview ? (