@refraction-ui/react 0.20.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../diff-viewer/src/diff-viewer.ts", "../../diff-viewer/src/diff-viewer.styles.ts", "../../react-diff-viewer/src/DiffViewer.tsx"],
4
- "sourcesContent": ["import type { AccessibilityProps } from '@refraction-ui/shared'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type DiffViewerTheme = 'light' | 'dark'\nexport type DiffViewMode = 'side-by-side' | 'inline'\n\nexport type DiffFileStatus = 'added' | 'modified' | 'deleted' | 'renamed'\n\nexport interface DiffFile {\n /** File path */\n path: string\n /** Change status */\n status: DiffFileStatus\n /** Lines added */\n additions: number\n /** Lines deleted */\n deletions: number\n /** Unified diff content for this file */\n diff?: string\n /** Old path for renamed files */\n oldPath?: string\n}\n\nexport interface DiffViewerProps {\n /** Files in this diff */\n files?: DiffFile[]\n /** Index of the active file */\n activeFileIndex?: number\n /** View mode */\n viewMode?: DiffViewMode\n /** Theme */\n theme?: DiffViewerTheme\n /** Language for syntax highlighting */\n language?: string\n /** Whether the sidebar is visible */\n sidebarOpen?: boolean\n /** Callback when active file changes */\n onFileSelect?: (index: number) => void\n /** Callback when view mode changes */\n onViewModeChange?: (mode: DiffViewMode) => void\n /** Callback when sidebar toggled */\n onSidebarToggle?: () => void\n}\n\nexport interface DiffViewerState {\n activeFileIndex: number\n viewMode: DiffViewMode\n theme: DiffViewerTheme\n language: string\n sidebarOpen: boolean\n files: DiffFile[]\n}\n\nexport interface DiffViewerAPI {\n /** Current viewer state */\n state: DiffViewerState\n /** ARIA attributes for the root element */\n ariaProps: Partial<AccessibilityProps> & Record<string, unknown>\n /** Data attributes for CSS styling hooks */\n dataAttributes: Record<string, string>\n /** Select a file by index */\n selectFile: (index: number) => void\n /** Move to next file */\n nextFile: () => void\n /** Move to previous file */\n prevFile: () => void\n /** Toggle view mode */\n toggleViewMode: () => void\n /** Toggle sidebar */\n toggleSidebar: () => void\n /** Get language label for a file path */\n getLanguageForFile: (path: string) => string\n /** Get human-readable file status icon */\n getFileStatusIcon: (status: DiffFileStatus) => string\n /** Get total additions across all files */\n totalAdditions: () => number\n /** Get total deletions across all files */\n totalDeletions: () => number\n}\n\n// ---------------------------------------------------------------------------\n// Language detection\n// ---------------------------------------------------------------------------\n\nconst extToLanguage: Record<string, string> = {\n ts: 'typescript',\n tsx: 'typescript',\n js: 'javascript',\n jsx: 'javascript',\n py: 'python',\n go: 'go',\n rs: 'rust',\n rb: 'ruby',\n java: 'java',\n swift: 'swift',\n kt: 'kotlin',\n dart: 'dart',\n php: 'php',\n lua: 'lua',\n r: 'r',\n scala: 'scala',\n json: 'json',\n yaml: 'yaml',\n yml: 'yaml',\n md: 'markdown',\n html: 'html',\n css: 'css',\n scss: 'scss',\n sql: 'sql',\n sh: 'shell',\n bash: 'shell',\n zsh: 'shell',\n xml: 'xml',\n toml: 'toml',\n proto: 'protobuf',\n dockerfile: 'dockerfile',\n cpp: 'cpp',\n c: 'c',\n}\n\nfunction getLanguageForFile(filePath: string): string {\n const ext = filePath.split('.').pop()?.toLowerCase() || ''\n const basename = filePath.split('/').pop()?.toLowerCase() || ''\n if (basename === 'dockerfile') return 'dockerfile'\n return extToLanguage[ext] || 'plaintext'\n}\n\n// ---------------------------------------------------------------------------\n// Status icons\n// ---------------------------------------------------------------------------\n\nconst statusIcons: Record<DiffFileStatus, string> = {\n added: '\\u{1F7E2}', // green circle\n modified: '\\u{1F7E1}', // yellow circle\n deleted: '\\u{1F534}', // red circle\n renamed: '\\u{1F535}', // blue circle\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a headless diff viewer API.\n *\n * Manages file selection, view mode, sidebar state, and produces ARIA/data\n * attributes. The actual rendering (Monaco DiffEditor, custom renderer, etc.)\n * is handled by framework-specific wrappers.\n */\nexport function createDiffViewer(props: DiffViewerProps = {}): DiffViewerAPI {\n const {\n files = [],\n activeFileIndex = 0,\n viewMode = 'side-by-side',\n theme = 'dark',\n language,\n sidebarOpen = true,\n onFileSelect,\n onViewModeChange,\n onSidebarToggle,\n } = props\n\n let currentIndex = Math.min(activeFileIndex, Math.max(0, files.length - 1))\n let currentViewMode = viewMode\n let currentSidebarOpen = sidebarOpen\n\n const state: DiffViewerState = {\n activeFileIndex: currentIndex,\n viewMode: currentViewMode,\n theme,\n language: language || (files[currentIndex] ? getLanguageForFile(files[currentIndex].path) : 'plaintext'),\n sidebarOpen: currentSidebarOpen,\n files,\n }\n\n const ariaProps: Partial<AccessibilityProps> & Record<string, unknown> = {\n role: 'region',\n 'aria-label': 'Diff viewer',\n }\n\n const dataAttributes: Record<string, string> = {\n 'data-view-mode': currentViewMode,\n 'data-theme': theme,\n 'data-file-count': String(files.length),\n }\n\n if (!currentSidebarOpen) {\n dataAttributes['data-sidebar-collapsed'] = ''\n }\n\n function selectFile(index: number) {\n if (index >= 0 && index < files.length) {\n currentIndex = index\n state.activeFileIndex = index\n state.language = language || getLanguageForFile(files[index].path)\n onFileSelect?.(index)\n }\n }\n\n function nextFile() {\n if (currentIndex < files.length - 1) {\n selectFile(currentIndex + 1)\n }\n }\n\n function prevFile() {\n if (currentIndex > 0) {\n selectFile(currentIndex - 1)\n }\n }\n\n function toggleViewMode() {\n currentViewMode = currentViewMode === 'side-by-side' ? 'inline' : 'side-by-side'\n state.viewMode = currentViewMode\n dataAttributes['data-view-mode'] = currentViewMode\n onViewModeChange?.(currentViewMode)\n }\n\n function toggleSidebar() {\n currentSidebarOpen = !currentSidebarOpen\n state.sidebarOpen = currentSidebarOpen\n if (currentSidebarOpen) {\n delete dataAttributes['data-sidebar-collapsed']\n } else {\n dataAttributes['data-sidebar-collapsed'] = ''\n }\n onSidebarToggle?.()\n }\n\n function totalAdditions(): number {\n return files.reduce((sum, f) => sum + f.additions, 0)\n }\n\n function totalDeletions(): number {\n return files.reduce((sum, f) => sum + f.deletions, 0)\n }\n\n return {\n state,\n ariaProps,\n dataAttributes,\n selectFile,\n nextFile,\n prevFile,\n toggleViewMode,\n toggleSidebar,\n getLanguageForFile,\n getFileStatusIcon: (status: DiffFileStatus) => statusIcons[status] || '\\u{26AA}',\n totalAdditions,\n totalDeletions,\n }\n}\n", "import { cva } from '@refraction-ui/shared'\nimport type { TokenContract } from '@refraction-ui/shared'\n\nexport const diffViewerTokens: TokenContract = {\n name: 'diff-viewer',\n tokens: {\n bg: { variable: '--rfr-diff-bg', fallback: 'hsl(var(--background))' },\n fg: { variable: '--rfr-diff-fg', fallback: 'hsl(var(--foreground))' },\n sidebarBg: { variable: '--rfr-diff-sidebar-bg', fallback: 'hsl(var(--muted))' },\n headerBg: { variable: '--rfr-diff-header-bg', fallback: 'hsl(var(--muted))' },\n border: { variable: '--rfr-diff-border', fallback: 'hsl(var(--border))' },\n addBg: { variable: '--rfr-diff-add-bg', fallback: 'rgba(46, 160, 67, 0.15)' },\n delBg: { variable: '--rfr-diff-del-bg', fallback: 'rgba(248, 81, 73, 0.15)' },\n addFg: { variable: '--rfr-diff-add-fg', fallback: '#3fb950' },\n delFg: { variable: '--rfr-diff-del-fg', fallback: '#f85149' },\n statusbar: { variable: '--rfr-diff-statusbar', fallback: 'hsl(var(--primary))' },\n statusbarFg: { variable: '--rfr-diff-statusbar-fg', fallback: 'hsl(var(--primary-foreground))' },\n },\n}\n\nexport const diffViewerVariants = cva({\n base: 'flex flex-col overflow-hidden font-sans text-sm',\n variants: {\n theme: {\n light: 'bg-white text-gray-900',\n dark: 'bg-gray-950 text-gray-100',\n },\n fullscreen: {\n 'true': 'h-screen',\n 'false': '',\n },\n },\n defaultVariants: {\n theme: 'dark',\n fullscreen: 'true',\n },\n})\n\nexport const sidebarVariants = cva({\n base: 'border-r overflow-auto flex-shrink-0',\n variants: {\n theme: {\n light: 'bg-gray-50 border-gray-200',\n dark: 'bg-gray-900 border-gray-800',\n },\n },\n defaultVariants: {\n theme: 'dark',\n },\n})\n\nexport const sidebarItemVariants = cva({\n base: 'px-2.5 py-1 cursor-pointer text-xs font-mono transition-colors',\n variants: {\n active: {\n 'true': 'border-l-2 border-blue-500',\n 'false': 'border-l-2 border-transparent',\n },\n theme: {\n light: '',\n dark: '',\n },\n },\n compoundVariants: [\n { active: 'true', theme: 'dark', class: 'bg-gray-800 text-gray-100' },\n { active: 'false', theme: 'dark', class: 'text-gray-400 hover:bg-gray-800/50' },\n { active: 'true', theme: 'light', class: 'bg-blue-50 text-gray-900' },\n { active: 'false', theme: 'light', class: 'text-gray-600 hover:bg-gray-100' },\n ],\n defaultVariants: {\n active: 'false',\n theme: 'dark',\n },\n})\n\nexport const tabBarVariants = cva({\n base: 'flex items-center border-b overflow-auto flex-shrink-0',\n variants: {\n theme: {\n light: 'bg-gray-50 border-gray-200',\n dark: 'bg-gray-900 border-gray-800',\n },\n },\n defaultVariants: {\n theme: 'dark',\n },\n})\n\nexport const tabVariants = cva({\n base: 'px-3 h-[35px] flex items-center gap-1 cursor-pointer text-xs font-mono border-r shrink-0',\n variants: {\n active: {\n 'true': '',\n 'false': '',\n },\n theme: {\n light: 'border-gray-200',\n dark: 'border-gray-800',\n },\n },\n compoundVariants: [\n { active: 'true', theme: 'dark', class: 'bg-gray-950 text-gray-100 border-b-gray-950 -mb-px' },\n { active: 'false', theme: 'dark', class: 'text-gray-400' },\n { active: 'true', theme: 'light', class: 'bg-white text-gray-900 border-b-white -mb-px' },\n { active: 'false', theme: 'light', class: 'text-gray-500' },\n ],\n defaultVariants: {\n active: 'false',\n theme: 'dark',\n },\n})\n\nexport const statusBarVariants = cva({\n base: 'h-[22px] flex items-center px-2.5 gap-3 text-[11px] font-mono flex-shrink-0',\n variants: {\n theme: {\n light: 'bg-blue-600 text-white',\n dark: 'bg-blue-700 text-white',\n },\n },\n defaultVariants: {\n theme: 'dark',\n },\n})\n", "import * as React from 'react'\nimport {\n createDiffViewer,\n diffViewerVariants,\n sidebarVariants,\n sidebarItemVariants,\n tabBarVariants,\n tabVariants,\n statusBarVariants,\n type DiffFile,\n type DiffViewerTheme,\n type DiffViewMode,\n} from '@refraction-ui/diff-viewer'\nimport { cn } from '@refraction-ui/shared'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface DiffViewerProps {\n /** Files in this diff */\n files: DiffFile[]\n /** Original file content (left side) */\n original?: string\n /** Modified file content (right side) */\n modified?: string\n /** Language for syntax highlighting (auto-detected from file if omitted) */\n language?: string\n /** Monaco theme name */\n monacoTheme?: string\n /** Color theme */\n theme?: DiffViewerTheme\n /** View mode */\n viewMode?: DiffViewMode\n /** Whether to show the file sidebar */\n showSidebar?: boolean\n /** Whether to show the tab bar */\n showTabs?: boolean\n /** Whether to show the status bar */\n showStatusBar?: boolean\n /** Sidebar width in pixels */\n sidebarWidth?: number\n /** Active file index */\n activeFileIndex?: number\n /** Called when the active file changes */\n onFileSelect?: (index: number) => void\n /** Called when view mode changes */\n onViewModeChange?: (mode: DiffViewMode) => void\n /** Title shown in the status bar */\n statusBarTitle?: string\n /** Status text shown in the status bar */\n statusBarStatus?: string\n /** Additional class name */\n className?: string\n /** Monaco DiffEditor options override */\n editorOptions?: Record<string, unknown>\n}\n\n// ---------------------------------------------------------------------------\n// Lazy Monaco import\n// ---------------------------------------------------------------------------\n\nconst MonacoDiffEditor = React.lazy(() =>\n import('@monaco-editor/react').then((m) => ({ default: m.DiffEditor })),\n)\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport const DiffViewer = React.forwardRef<HTMLDivElement, DiffViewerProps>(\n function DiffViewer(\n {\n files,\n original = '',\n modified = '',\n language,\n monacoTheme = 'vs-dark',\n theme = 'dark',\n viewMode: controlledViewMode = 'side-by-side',\n showSidebar = true,\n showTabs = true,\n showStatusBar = true,\n sidebarWidth = 220,\n activeFileIndex: controlledIndex = 0,\n onFileSelect,\n onViewModeChange,\n statusBarTitle,\n statusBarStatus,\n className,\n editorOptions,\n },\n ref,\n ) {\n const [activeIdx, setActiveIdx] = React.useState(controlledIndex)\n const [sidebarOpen, setSidebarOpen] = React.useState(showSidebar)\n const [viewMode, setViewMode] = React.useState<DiffViewMode>(controlledViewMode)\n\n // Sync controlled props\n React.useEffect(() => setActiveIdx(controlledIndex), [controlledIndex])\n React.useEffect(() => setViewMode(controlledViewMode), [controlledViewMode])\n\n const api = React.useMemo(\n () =>\n createDiffViewer({\n files,\n activeFileIndex: activeIdx,\n viewMode,\n theme,\n language,\n sidebarOpen,\n onFileSelect: (i: number) => {\n setActiveIdx(i)\n onFileSelect?.(i)\n },\n onViewModeChange: (m: DiffViewMode) => {\n setViewMode(m)\n onViewModeChange?.(m)\n },\n onSidebarToggle: () => setSidebarOpen((s) => !s),\n }),\n [files, activeIdx, viewMode, theme, language, sidebarOpen, onFileSelect, onViewModeChange],\n )\n\n const activeFile = files[activeIdx]\n const detectedLang = language || (activeFile ? api.getLanguageForFile(activeFile.path) : 'plaintext')\n\n // Keyboard navigation\n React.useEffect(() => {\n function onKey(e: KeyboardEvent) {\n if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return\n if (e.key === 'j') {\n setActiveIdx((i) => {\n const next = Math.min(i + 1, files.length - 1)\n onFileSelect?.(next)\n return next\n })\n } else if (e.key === 'k') {\n setActiveIdx((i) => {\n const prev = Math.max(i - 1, 0)\n onFileSelect?.(prev)\n return prev\n })\n } else if (e.key === 'b') {\n setSidebarOpen((s) => !s)\n }\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [files.length, onFileSelect])\n\n return (\n <div\n ref={ref}\n className={cn(diffViewerVariants({ theme, fullscreen: 'true' }), className)}\n {...api.ariaProps}\n {...api.dataAttributes}\n >\n {/* Main area: sidebar + editor */}\n <div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>\n {/* File sidebar */}\n {sidebarOpen && (\n <div className={sidebarVariants({ theme })} style={{ width: sidebarWidth }}>\n <div className=\"px-2.5 py-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground\">\n {`Files (${files.length})`}\n </div>\n {files.map((f, i) => {\n const active = i === activeIdx\n const fname = f.path.split('/').pop() || f.path\n const dir = f.path.split('/').slice(0, -1).join('/')\n return (\n <div\n key={f.path}\n onClick={() => {\n setActiveIdx(i)\n onFileSelect?.(i)\n }}\n className={sidebarItemVariants({ active: active ? 'true' : 'false', theme })}\n >\n <div className=\"flex items-center gap-1\">\n <span className=\"text-[10px]\">{api.getFileStatusIcon(f.status)}</span>\n <span className={active ? '' : 'opacity-70'}>{fname}</span>\n <span className=\"ml-auto text-[10px]\">\n <span className=\"text-green-500\">{`+${f.additions}`}</span>\n {f.deletions > 0 && <span className=\"text-red-500\">{`-${f.deletions}`}</span>}\n </span>\n </div>\n {dir && (\n <div className=\"text-[10px] opacity-50 mt-0.5 pl-4\">{dir}</div>\n )}\n </div>\n )\n })}\n </div>\n )}\n\n {/* Editor area */}\n <div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>\n {/* Tab bar */}\n {showTabs && activeFile && (\n <div className={tabBarVariants({ theme })}>\n {files.map((f, i) => {\n const active = i === activeIdx\n const fname = f.path.split('/').pop() || f.path\n return (\n <div\n key={f.path}\n onClick={() => {\n setActiveIdx(i)\n onFileSelect?.(i)\n }}\n className={tabVariants({ active: active ? 'true' : 'false', theme })}\n >\n <span className=\"text-[9px]\">{api.getFileStatusIcon(f.status)}</span>\n {fname}\n </div>\n )\n })}\n </div>\n )}\n\n {/* Monaco DiffEditor */}\n <div style={{ flex: 1 }}>\n <React.Suspense\n fallback={\n <div className=\"flex items-center justify-center h-full text-xs opacity-50\">\n Loading editor...\n </div>\n }\n >\n <MonacoDiffEditor\n original={original}\n modified={modified}\n language={detectedLang}\n theme={monacoTheme}\n options={{\n readOnly: true,\n renderSideBySide: viewMode === 'side-by-side',\n minimap: { enabled: true, scale: 1, showSlider: 'mouseover' },\n fontSize: 13,\n lineHeight: 20,\n fontFamily: \"'JetBrains Mono', 'Fira Code', 'SF Mono', Menlo, monospace\",\n fontLigatures: true,\n scrollBeyondLastLine: false,\n renderOverviewRuler: true,\n scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 },\n padding: { top: 8, bottom: 8 },\n renderWhitespace: 'selection',\n bracketPairColorization: { enabled: true },\n guides: { indentation: true },\n smoothScrolling: true,\n cursorBlinking: 'smooth',\n ...editorOptions,\n }}\n />\n </React.Suspense>\n </div>\n </div>\n </div>\n\n {/* Status bar */}\n {showStatusBar && (\n <div className={statusBarVariants({ theme })}>\n {statusBarTitle && <span>{statusBarTitle}</span>}\n {activeFile && <span>{activeFile.path}</span>}\n <span style={{ marginLeft: 'auto' }}>\n {`+${api.totalAdditions()} -${api.totalDeletions()}`}\n </span>\n {statusBarStatus && <span>{statusBarStatus}</span>}\n <span>\n {`${files.length} ${files.length === 1 ? 'file' : 'files'}`}\n </span>\n </div>\n )}\n </div>\n )\n },\n)\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { AccessibilityProps } from '@refraction-ui/shared'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type DiffViewerTheme = 'light' | 'dark'\nexport type DiffViewMode = 'side-by-side' | 'inline'\n\nexport type DiffFileStatus = 'added' | 'modified' | 'deleted' | 'renamed'\n\nexport interface DiffFile {\n /** File path */\n path: string\n /** Change status */\n status: DiffFileStatus\n /** Lines added */\n additions: number\n /** Lines deleted */\n deletions: number\n /** Unified diff content for this file */\n diff?: string\n /** Old path for renamed files */\n oldPath?: string\n}\n\nexport interface DiffViewerProps {\n /** Files in this diff */\n files?: DiffFile[]\n /** Index of the active file */\n activeFileIndex?: number\n /** View mode */\n viewMode?: DiffViewMode\n /** Theme */\n theme?: DiffViewerTheme\n /** Language for syntax highlighting */\n language?: string\n /** Whether the sidebar is visible */\n sidebarOpen?: boolean\n /** Callback when active file changes */\n onFileSelect?: (index: number) => void\n /** Callback when view mode changes */\n onViewModeChange?: (mode: DiffViewMode) => void\n /** Callback when sidebar toggled */\n onSidebarToggle?: () => void\n}\n\nexport interface DiffViewerState {\n activeFileIndex: number\n viewMode: DiffViewMode\n theme: DiffViewerTheme\n language: string\n sidebarOpen: boolean\n files: DiffFile[]\n}\n\nexport interface DiffViewerAPI {\n /** Current viewer state */\n state: DiffViewerState\n /** ARIA attributes for the root element */\n ariaProps: Partial<AccessibilityProps> & Record<string, unknown>\n /** Data attributes for CSS styling hooks */\n dataAttributes: Record<string, string>\n /** Select a file by index */\n selectFile: (index: number) => void\n /** Move to next file */\n nextFile: () => void\n /** Move to previous file */\n prevFile: () => void\n /** Toggle view mode */\n toggleViewMode: () => void\n /** Toggle sidebar */\n toggleSidebar: () => void\n /** Get language label for a file path */\n getLanguageForFile: (path: string) => string\n /** Get human-readable file status icon */\n getFileStatusIcon: (status: DiffFileStatus) => string\n /** Get total additions across all files */\n totalAdditions: () => number\n /** Get total deletions across all files */\n totalDeletions: () => number\n}\n\n// ---------------------------------------------------------------------------\n// Language detection\n// ---------------------------------------------------------------------------\n\nconst extToLanguage: Record<string, string> = {\n ts: 'typescript',\n tsx: 'typescript',\n js: 'javascript',\n jsx: 'javascript',\n py: 'python',\n go: 'go',\n rs: 'rust',\n rb: 'ruby',\n java: 'java',\n swift: 'swift',\n kt: 'kotlin',\n dart: 'dart',\n php: 'php',\n lua: 'lua',\n r: 'r',\n scala: 'scala',\n json: 'json',\n yaml: 'yaml',\n yml: 'yaml',\n md: 'markdown',\n html: 'html',\n css: 'css',\n scss: 'scss',\n sql: 'sql',\n sh: 'shell',\n bash: 'shell',\n zsh: 'shell',\n xml: 'xml',\n toml: 'toml',\n proto: 'protobuf',\n dockerfile: 'dockerfile',\n cpp: 'cpp',\n c: 'c',\n}\n\nfunction getLanguageForFile(filePath: string): string {\n const ext = filePath.split('.').pop()?.toLowerCase() || ''\n const basename = filePath.split('/').pop()?.toLowerCase() || ''\n if (basename === 'dockerfile') return 'dockerfile'\n return extToLanguage[ext] || 'plaintext'\n}\n\n// ---------------------------------------------------------------------------\n// Status icons\n// ---------------------------------------------------------------------------\n\nconst statusIcons: Record<DiffFileStatus, string> = {\n added: '\\u{1F7E2}', // green circle\n modified: '\\u{1F7E1}', // yellow circle\n deleted: '\\u{1F534}', // red circle\n renamed: '\\u{1F535}', // blue circle\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a headless diff viewer API.\n *\n * Manages file selection, view mode, sidebar state, and produces ARIA/data\n * attributes. The actual rendering (Monaco DiffEditor, custom renderer, etc.)\n * is handled by framework-specific wrappers.\n */\nexport function createDiffViewer(props: DiffViewerProps = {}): DiffViewerAPI {\n const {\n files = [],\n activeFileIndex = 0,\n viewMode = 'side-by-side',\n theme = 'dark',\n language,\n sidebarOpen = true,\n onFileSelect,\n onViewModeChange,\n onSidebarToggle,\n } = props\n\n let currentIndex = Math.min(activeFileIndex, Math.max(0, files.length - 1))\n let currentViewMode = viewMode\n let currentSidebarOpen = sidebarOpen\n\n const state: DiffViewerState = {\n activeFileIndex: currentIndex,\n viewMode: currentViewMode,\n theme,\n language: language || (files[currentIndex] ? getLanguageForFile(files[currentIndex].path) : 'plaintext'),\n sidebarOpen: currentSidebarOpen,\n files,\n }\n\n const ariaProps: Partial<AccessibilityProps> & Record<string, unknown> = {\n role: 'region',\n 'aria-label': 'Diff viewer',\n }\n\n const dataAttributes: Record<string, string> = {\n 'data-view-mode': currentViewMode,\n 'data-theme': theme,\n 'data-file-count': String(files.length),\n }\n\n if (!currentSidebarOpen) {\n dataAttributes['data-sidebar-collapsed'] = ''\n }\n\n function selectFile(index: number) {\n if (index >= 0 && index < files.length) {\n currentIndex = index\n state.activeFileIndex = index\n state.language = language || getLanguageForFile(files[index].path)\n onFileSelect?.(index)\n }\n }\n\n function nextFile() {\n if (currentIndex < files.length - 1) {\n selectFile(currentIndex + 1)\n }\n }\n\n function prevFile() {\n if (currentIndex > 0) {\n selectFile(currentIndex - 1)\n }\n }\n\n function toggleViewMode() {\n currentViewMode = currentViewMode === 'side-by-side' ? 'inline' : 'side-by-side'\n state.viewMode = currentViewMode\n dataAttributes['data-view-mode'] = currentViewMode\n onViewModeChange?.(currentViewMode)\n }\n\n function toggleSidebar() {\n currentSidebarOpen = !currentSidebarOpen\n state.sidebarOpen = currentSidebarOpen\n if (currentSidebarOpen) {\n delete dataAttributes['data-sidebar-collapsed']\n } else {\n dataAttributes['data-sidebar-collapsed'] = ''\n }\n onSidebarToggle?.()\n }\n\n function totalAdditions(): number {\n return files.reduce((sum, f) => sum + f.additions, 0)\n }\n\n function totalDeletions(): number {\n return files.reduce((sum, f) => sum + f.deletions, 0)\n }\n\n return {\n state,\n ariaProps,\n dataAttributes,\n selectFile,\n nextFile,\n prevFile,\n toggleViewMode,\n toggleSidebar,\n getLanguageForFile,\n getFileStatusIcon: (status: DiffFileStatus) => statusIcons[status] || '\\u{26AA}',\n totalAdditions,\n totalDeletions,\n }\n}\n", "import { cva } from '@refraction-ui/shared'\nimport type { TokenContract } from '@refraction-ui/shared'\n\nexport const diffViewerTokens: TokenContract = {\n name: 'diff-viewer',\n tokens: {\n bg: { variable: '--rfr-diff-bg', fallback: 'hsl(var(--background))' },\n fg: { variable: '--rfr-diff-fg', fallback: 'hsl(var(--foreground))' },\n sidebarBg: { variable: '--rfr-diff-sidebar-bg', fallback: 'hsl(var(--muted))' },\n headerBg: { variable: '--rfr-diff-header-bg', fallback: 'hsl(var(--muted))' },\n border: { variable: '--rfr-diff-border', fallback: 'hsl(var(--border))' },\n // Diff add/del colors are syntax semantics (like code highlighting), not\n // theme status tones — deliberately NOT routed through the success/\n // destructive tokens. The --rfr-diff-* variables are the theming seam.\n addBg: { variable: '--rfr-diff-add-bg', fallback: 'rgba(46, 160, 67, 0.15)' },\n delBg: { variable: '--rfr-diff-del-bg', fallback: 'rgba(248, 81, 73, 0.15)' },\n addFg: { variable: '--rfr-diff-add-fg', fallback: '#3fb950' },\n delFg: { variable: '--rfr-diff-del-fg', fallback: '#f85149' },\n statusbar: { variable: '--rfr-diff-statusbar', fallback: 'hsl(var(--primary))' },\n statusbarFg: { variable: '--rfr-diff-statusbar-fg', fallback: 'hsl(var(--primary-foreground))' },\n },\n}\n\nexport const diffViewerVariants = cva({\n base: 'flex flex-col overflow-hidden font-sans text-sm',\n variants: {\n theme: {\n light: 'bg-white text-gray-900',\n dark: 'bg-gray-950 text-gray-100',\n },\n fullscreen: {\n 'true': 'h-screen',\n 'false': '',\n },\n },\n defaultVariants: {\n theme: 'dark',\n fullscreen: 'true',\n },\n})\n\nexport const sidebarVariants = cva({\n base: 'border-r overflow-auto flex-shrink-0',\n variants: {\n theme: {\n light: 'bg-gray-50 border-gray-200',\n dark: 'bg-gray-900 border-gray-800',\n },\n },\n defaultVariants: {\n theme: 'dark',\n },\n})\n\nexport const sidebarItemVariants = cva({\n base: 'px-2.5 py-1 cursor-pointer text-xs font-mono transition-colors',\n variants: {\n active: {\n 'true': 'border-l-2 border-blue-500',\n 'false': 'border-l-2 border-transparent',\n },\n theme: {\n light: '',\n dark: '',\n },\n },\n compoundVariants: [\n { active: 'true', theme: 'dark', class: 'bg-gray-800 text-gray-100' },\n { active: 'false', theme: 'dark', class: 'text-gray-400 hover:bg-gray-800/50' },\n { active: 'true', theme: 'light', class: 'bg-blue-50 text-gray-900' },\n { active: 'false', theme: 'light', class: 'text-gray-600 hover:bg-gray-100' },\n ],\n defaultVariants: {\n active: 'false',\n theme: 'dark',\n },\n})\n\nexport const tabBarVariants = cva({\n base: 'flex items-center border-b overflow-auto flex-shrink-0',\n variants: {\n theme: {\n light: 'bg-gray-50 border-gray-200',\n dark: 'bg-gray-900 border-gray-800',\n },\n },\n defaultVariants: {\n theme: 'dark',\n },\n})\n\nexport const tabVariants = cva({\n base: 'px-3 h-[35px] flex items-center gap-1 cursor-pointer text-xs font-mono border-r shrink-0',\n variants: {\n active: {\n 'true': '',\n 'false': '',\n },\n theme: {\n light: 'border-gray-200',\n dark: 'border-gray-800',\n },\n },\n compoundVariants: [\n { active: 'true', theme: 'dark', class: 'bg-gray-950 text-gray-100 border-b-gray-950 -mb-px' },\n { active: 'false', theme: 'dark', class: 'text-gray-400' },\n { active: 'true', theme: 'light', class: 'bg-white text-gray-900 border-b-white -mb-px' },\n { active: 'false', theme: 'light', class: 'text-gray-500' },\n ],\n defaultVariants: {\n active: 'false',\n theme: 'dark',\n },\n})\n\nexport const statusBarVariants = cva({\n base: 'h-[22px] flex items-center px-2.5 gap-3 text-[11px] font-mono flex-shrink-0',\n variants: {\n theme: {\n light: 'bg-blue-600 text-white',\n dark: 'bg-blue-700 text-white',\n },\n },\n defaultVariants: {\n theme: 'dark',\n },\n})\n", "import * as React from 'react'\nimport {\n createDiffViewer,\n diffViewerVariants,\n sidebarVariants,\n sidebarItemVariants,\n tabBarVariants,\n tabVariants,\n statusBarVariants,\n type DiffFile,\n type DiffViewerTheme,\n type DiffViewMode,\n} from '@refraction-ui/diff-viewer'\nimport { cn } from '@refraction-ui/shared'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface DiffViewerProps {\n /** Files in this diff */\n files: DiffFile[]\n /** Original file content (left side) */\n original?: string\n /** Modified file content (right side) */\n modified?: string\n /** Language for syntax highlighting (auto-detected from file if omitted) */\n language?: string\n /** Monaco theme name */\n monacoTheme?: string\n /** Color theme */\n theme?: DiffViewerTheme\n /** View mode */\n viewMode?: DiffViewMode\n /** Whether to show the file sidebar */\n showSidebar?: boolean\n /** Whether to show the tab bar */\n showTabs?: boolean\n /** Whether to show the status bar */\n showStatusBar?: boolean\n /** Sidebar width in pixels */\n sidebarWidth?: number\n /** Active file index */\n activeFileIndex?: number\n /** Called when the active file changes */\n onFileSelect?: (index: number) => void\n /** Called when view mode changes */\n onViewModeChange?: (mode: DiffViewMode) => void\n /** Title shown in the status bar */\n statusBarTitle?: string\n /** Status text shown in the status bar */\n statusBarStatus?: string\n /** Additional class name */\n className?: string\n /** Monaco DiffEditor options override */\n editorOptions?: Record<string, unknown>\n}\n\n// ---------------------------------------------------------------------------\n// Lazy Monaco import\n// ---------------------------------------------------------------------------\n\nconst MonacoDiffEditor = React.lazy(() =>\n import('@monaco-editor/react').then((m) => ({ default: m.DiffEditor })),\n)\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport const DiffViewer = React.forwardRef<HTMLDivElement, DiffViewerProps>(\n function DiffViewer(\n {\n files,\n original = '',\n modified = '',\n language,\n monacoTheme = 'vs-dark',\n theme = 'dark',\n viewMode: controlledViewMode = 'side-by-side',\n showSidebar = true,\n showTabs = true,\n showStatusBar = true,\n sidebarWidth = 220,\n activeFileIndex: controlledIndex = 0,\n onFileSelect,\n onViewModeChange,\n statusBarTitle,\n statusBarStatus,\n className,\n editorOptions,\n },\n ref,\n ) {\n const [activeIdx, setActiveIdx] = React.useState(controlledIndex)\n const [sidebarOpen, setSidebarOpen] = React.useState(showSidebar)\n const [viewMode, setViewMode] = React.useState<DiffViewMode>(controlledViewMode)\n\n // Sync controlled props\n React.useEffect(() => setActiveIdx(controlledIndex), [controlledIndex])\n React.useEffect(() => setViewMode(controlledViewMode), [controlledViewMode])\n\n const api = React.useMemo(\n () =>\n createDiffViewer({\n files,\n activeFileIndex: activeIdx,\n viewMode,\n theme,\n language,\n sidebarOpen,\n onFileSelect: (i: number) => {\n setActiveIdx(i)\n onFileSelect?.(i)\n },\n onViewModeChange: (m: DiffViewMode) => {\n setViewMode(m)\n onViewModeChange?.(m)\n },\n onSidebarToggle: () => setSidebarOpen((s) => !s),\n }),\n [files, activeIdx, viewMode, theme, language, sidebarOpen, onFileSelect, onViewModeChange],\n )\n\n const activeFile = files[activeIdx]\n const detectedLang = language || (activeFile ? api.getLanguageForFile(activeFile.path) : 'plaintext')\n\n // Keyboard navigation\n React.useEffect(() => {\n function onKey(e: KeyboardEvent) {\n if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return\n if (e.key === 'j') {\n setActiveIdx((i) => {\n const next = Math.min(i + 1, files.length - 1)\n onFileSelect?.(next)\n return next\n })\n } else if (e.key === 'k') {\n setActiveIdx((i) => {\n const prev = Math.max(i - 1, 0)\n onFileSelect?.(prev)\n return prev\n })\n } else if (e.key === 'b') {\n setSidebarOpen((s) => !s)\n }\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [files.length, onFileSelect])\n\n return (\n <div\n ref={ref}\n className={cn(diffViewerVariants({ theme, fullscreen: 'true' }), className)}\n {...api.ariaProps}\n {...api.dataAttributes}\n >\n {/* Main area: sidebar + editor */}\n <div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>\n {/* File sidebar */}\n {sidebarOpen && (\n <div className={sidebarVariants({ theme })} style={{ width: sidebarWidth }}>\n <div className=\"px-2.5 py-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground\">\n {`Files (${files.length})`}\n </div>\n {files.map((f, i) => {\n const active = i === activeIdx\n const fname = f.path.split('/').pop() || f.path\n const dir = f.path.split('/').slice(0, -1).join('/')\n return (\n <div\n key={f.path}\n onClick={() => {\n setActiveIdx(i)\n onFileSelect?.(i)\n }}\n className={sidebarItemVariants({ active: active ? 'true' : 'false', theme })}\n >\n <div className=\"flex items-center gap-1\">\n <span className=\"text-[10px]\">{api.getFileStatusIcon(f.status)}</span>\n <span className={active ? '' : 'opacity-70'}>{fname}</span>\n <span className=\"ml-auto text-[10px]\">\n <span className=\"text-green-500\">{`+${f.additions}`}</span>\n {f.deletions > 0 && <span className=\"text-red-500\">{`-${f.deletions}`}</span>}\n </span>\n </div>\n {dir && (\n <div className=\"text-[10px] opacity-50 mt-0.5 pl-4\">{dir}</div>\n )}\n </div>\n )\n })}\n </div>\n )}\n\n {/* Editor area */}\n <div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>\n {/* Tab bar */}\n {showTabs && activeFile && (\n <div className={tabBarVariants({ theme })}>\n {files.map((f, i) => {\n const active = i === activeIdx\n const fname = f.path.split('/').pop() || f.path\n return (\n <div\n key={f.path}\n onClick={() => {\n setActiveIdx(i)\n onFileSelect?.(i)\n }}\n className={tabVariants({ active: active ? 'true' : 'false', theme })}\n >\n <span className=\"text-[9px]\">{api.getFileStatusIcon(f.status)}</span>\n {fname}\n </div>\n )\n })}\n </div>\n )}\n\n {/* Monaco DiffEditor */}\n <div style={{ flex: 1 }}>\n <React.Suspense\n fallback={\n <div className=\"flex items-center justify-center h-full text-xs opacity-50\">\n Loading editor...\n </div>\n }\n >\n <MonacoDiffEditor\n original={original}\n modified={modified}\n language={detectedLang}\n theme={monacoTheme}\n options={{\n readOnly: true,\n renderSideBySide: viewMode === 'side-by-side',\n minimap: { enabled: true, scale: 1, showSlider: 'mouseover' },\n fontSize: 13,\n lineHeight: 20,\n fontFamily: \"'JetBrains Mono', 'Fira Code', 'SF Mono', Menlo, monospace\",\n fontLigatures: true,\n scrollBeyondLastLine: false,\n renderOverviewRuler: true,\n scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 },\n padding: { top: 8, bottom: 8 },\n renderWhitespace: 'selection',\n bracketPairColorization: { enabled: true },\n guides: { indentation: true },\n smoothScrolling: true,\n cursorBlinking: 'smooth',\n ...editorOptions,\n }}\n />\n </React.Suspense>\n </div>\n </div>\n </div>\n\n {/* Status bar */}\n {showStatusBar && (\n <div className={statusBarVariants({ theme })}>\n {statusBarTitle && <span>{statusBarTitle}</span>}\n {activeFile && <span>{activeFile.path}</span>}\n <span style={{ marginLeft: 'auto' }}>\n {`+${api.totalAdditions()} -${api.totalDeletions()}`}\n </span>\n {statusBarStatus && <span>{statusBarStatus}</span>}\n <span>\n {`${files.length} ${files.length === 1 ? 'file' : 'files'}`}\n </span>\n </div>\n )}\n </div>\n )\n },\n)\n"],
5
+ "mappings": "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",
6
6
  "names": ["DiffViewer"]
7
7
  }
@@ -22,8 +22,16 @@ var separatorVariants = cva({
22
22
  base: "",
23
23
  variants: {
24
24
  orientation: {
25
- horizontal: "h-px w-full bg-border",
26
- vertical: "w-px h-full bg-border"
25
+ horizontal: "h-px w-full",
26
+ vertical: "w-px h-full"
27
+ },
28
+ // Line color (issue #485). `default` is the standard border token;
29
+ // `subtle` uses the hairline `border-subtle` token for a lighter rule.
30
+ // Keeping the color here (not in `orientation`) keeps every existing call
31
+ // pixel-identical while adding the subtle option.
32
+ tone: {
33
+ default: "bg-border",
34
+ subtle: "bg-border-subtle"
27
35
  },
28
36
  labeled: {
29
37
  true: "flex items-center",
@@ -32,10 +40,12 @@ var separatorVariants = cva({
32
40
  },
33
41
  defaultVariants: {
34
42
  orientation: "horizontal",
43
+ tone: "default",
35
44
  labeled: "false"
36
45
  }
37
46
  });
38
47
  var separatorLineClass = "h-px flex-1 bg-border";
48
+ var separatorSubtleLineClass = "h-px flex-1 bg-border-subtle";
39
49
  var separatorLabelClass = "px-3 text-xs uppercase tracking-wide text-muted-foreground";
40
50
 
41
51
  // ../react-separator/dist/index.js
@@ -43,23 +53,25 @@ import { jsxs, jsx } from "react/jsx-runtime";
43
53
  var Separator = React.forwardRef(
44
54
  function Separator2({
45
55
  orientation = "horizontal",
56
+ tone = "default",
46
57
  label,
47
58
  decorative = true,
48
59
  className,
49
60
  ...props
50
61
  }, ref) {
51
62
  if (orientation === "horizontal" && label != null) {
63
+ const lineClass = tone === "subtle" ? separatorSubtleLineClass : separatorLineClass;
52
64
  return /* @__PURE__ */ jsxs(
53
65
  "div",
54
66
  {
55
67
  ref,
56
68
  role: "none",
57
- className: cn(separatorVariants({ labeled: "true" }), className),
69
+ className: cn(separatorVariants({ labeled: "true", tone }), className),
58
70
  ...props,
59
71
  children: [
60
- /* @__PURE__ */ jsx("span", { className: separatorLineClass }),
72
+ /* @__PURE__ */ jsx("span", { className: lineClass }),
61
73
  /* @__PURE__ */ jsx("span", { className: separatorLabelClass, children: label }),
62
- /* @__PURE__ */ jsx("span", { className: separatorLineClass })
74
+ /* @__PURE__ */ jsx("span", { className: lineClass })
63
75
  ]
64
76
  }
65
77
  );
@@ -72,7 +84,7 @@ var Separator = React.forwardRef(
72
84
  "div",
73
85
  {
74
86
  ref,
75
- className: cn(separatorVariants({ orientation }), className),
87
+ className: cn(separatorVariants({ orientation, tone }), className),
76
88
  ...ariaProps,
77
89
  ...dataAttributes,
78
90
  ...props
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../separator/src/separator.ts", "../../separator/src/separator.styles.ts", "../../react-separator/src/separator.tsx"],
4
- "sourcesContent": ["import type { AccessibilityProps } from '@refraction-ui/shared'\n\nexport type SeparatorOrientation = 'horizontal' | 'vertical'\n\nexport interface SeparatorProps {\n /** Orientation of the separator. Defaults to `'horizontal'`. */\n orientation?: SeparatorOrientation\n /**\n * When `true` (the default) the separator is purely visual and is hidden\n * from assistive tech (`role=\"none\"`). When `false` it is exposed as a\n * semantic `role=\"separator\"` with the appropriate `aria-orientation`.\n */\n decorative?: boolean\n}\n\n/**\n * ARIA attributes for a separator. Extends the shared base with\n * `aria-orientation`, which is specific to the `separator` role.\n */\nexport interface SeparatorAriaProps extends Partial<AccessibilityProps> {\n 'aria-orientation'?: SeparatorOrientation\n}\n\nexport interface SeparatorAPI {\n /** ARIA attributes to spread on the element. */\n ariaProps: SeparatorAriaProps\n /** Data attributes for CSS styling hooks. */\n dataAttributes: Record<string, string>\n}\n\n/**\n * Headless separator core — computes ARIA semantics and data attributes for\n * a separator rule. JSX-free so it can be shared across framework adapters.\n *\n * Per ARIA, a decorative separator must not be announced (`role=\"none\"`),\n * while a semantic one exposes `role=\"separator\"` and `aria-orientation`.\n */\nexport function createSeparator(props: SeparatorProps = {}): SeparatorAPI {\n const { orientation = 'horizontal', decorative = true } = props\n\n const ariaProps: SeparatorAriaProps = decorative\n ? { role: 'none' }\n : { role: 'separator', 'aria-orientation': orientation }\n\n const dataAttributes: Record<string, string> = {\n 'data-orientation': orientation,\n }\n\n return {\n ariaProps,\n dataAttributes,\n }\n}\n", "import { cva } from '@refraction-ui/shared'\n\n/**\n * Visual variants for the separator rule.\n *\n * - `orientation: horizontal` → a full-width 1px line.\n * - `orientation: vertical` → a full-height 1px line.\n * - `labeled` → flex layout used by the labeled-divider variant, which\n * arranges a centered label between two flanking lines.\n */\nexport const separatorVariants = cva({\n base: '',\n variants: {\n orientation: {\n horizontal: 'h-px w-full bg-border',\n vertical: 'w-px h-full bg-border',\n },\n labeled: {\n true: 'flex items-center',\n false: '',\n },\n },\n defaultVariants: {\n orientation: 'horizontal',\n labeled: 'false',\n },\n})\n\n/** Class for each flanking line in the labeled-divider variant. */\nexport const separatorLineClass = 'h-px flex-1 bg-border'\n\n/** Class for the centered label in the labeled-divider variant. */\nexport const separatorLabelClass =\n 'px-3 text-xs uppercase tracking-wide text-muted-foreground'\n", "import * as React from 'react'\nimport {\n createSeparator,\n separatorVariants,\n separatorLineClass,\n separatorLabelClass,\n type SeparatorOrientation,\n} from '@refraction-ui/separator'\nimport { cn } from '@refraction-ui/shared'\n\nexport interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Orientation of the separator. Defaults to `'horizontal'`. */\n orientation?: SeparatorOrientation\n /**\n * Optional centered label. Only meaningful for horizontal separators —\n * renders a line on each side of the label (a \"labeled divider\").\n */\n label?: React.ReactNode\n /**\n * When `true` (the default) the separator is purely visual and is hidden\n * from assistive tech (`role=\"none\"`). When `false` it is exposed as a\n * semantic `role=\"separator\"` with the appropriate `aria-orientation`.\n */\n decorative?: boolean\n}\n\n/**\n * Separator / LabeledDivider — a thin rule that visually divides content.\n *\n * Supports horizontal and vertical orientations and an optional centered\n * label (the \"labeled divider\" variant, horizontal only). State and ARIA\n * semantics come from the headless @refraction-ui/separator core; styling\n * via Tailwind utility classes (no external CSS-in-JS).\n */\nexport const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(\n function Separator(\n {\n orientation = 'horizontal',\n label,\n decorative = true,\n className,\n ...props\n },\n ref,\n ) {\n // Labeled divider — decorative chrome wrapping a centered label.\n if (orientation === 'horizontal' && label != null) {\n return (\n <div\n ref={ref}\n role=\"none\"\n className={cn(separatorVariants({ labeled: 'true' }), className)}\n {...props}\n >\n <span className={separatorLineClass} />\n <span className={separatorLabelClass}>{label}</span>\n <span className={separatorLineClass} />\n </div>\n )\n }\n\n const { ariaProps, dataAttributes } = createSeparator({\n orientation,\n decorative,\n })\n\n return (\n <div\n ref={ref}\n className={cn(separatorVariants({ orientation }), className)}\n {...ariaProps}\n {...dataAttributes}\n {...props}\n />\n )\n },\n)\n"],
5
- "mappings": ";;;;;;;;;AAqCO,SAAS,gBAAgB,QAAwB,CAAA,GAAkB;AACxE,QAAM,EAAE,cAAc,cAAc,aAAa,KAAA,IAAS;AAE1D,QAAM,YAAgC,aAClC,EAAE,MAAM,OAAA,IACR,EAAE,MAAM,aAAa,oBAAoB,YAAA;AAE7C,QAAM,iBAAyC;IAC7C,oBAAoB;EAAA;AAGtB,SAAO;IACL;IACA;EAAA;AAEJ;AC1CO,IAAM,oBAAoB,IAAI;EACnC,MAAM;EACN,UAAU;IACR,aAAa;MACX,YAAY;MACZ,UAAU;IAAA;IAEZ,SAAS;MACP,MAAM;MACN,OAAO;IAAA;EACT;EAEF,iBAAiB;IACf,aAAa;IACb,SAAS;EAAA;AAEb,CAAC;AAGM,IAAM,qBAAqB;AAG3B,IAAM,sBACX;;;;ACCK,IAAM,YAAkB;EAC7B,SAASA,WACP;IACE,cAAc;IACd;IACA,aAAa;IACb;IACA,GAAG;EAAA,GAEL,KACA;AAEA,QAAI,gBAAgB,gBAAgB,SAAS,MAAM;AACjD,aACE;QAAC;QAAA;UACC;UACA,MAAK;UACL,WAAW,GAAG,kBAAkB,EAAE,SAAS,OAAA,CAAQ,GAAG,SAAS;UAC9D,GAAG;UAEJ,UAAA;YAAA,oBAAC,QAAA,EAAK,WAAW,mBAAA,CAAoB;YACrC,oBAAC,QAAA,EAAK,WAAW,qBAAsB,UAAA,MAAA,CAAM;YAC7C,oBAAC,QAAA,EAAK,WAAW,mBAAA,CAAoB;UAAA;QAAA;MAAA;IAG3C;AAEA,UAAM,EAAE,WAAW,eAAA,IAAmB,gBAAgB;MACpD;MACA;IAAA,CACD;AAED,WACE;MAAC;MAAA;QACC;QACA,WAAW,GAAG,kBAAkB,EAAE,YAAA,CAAa,GAAG,SAAS;QAC1D,GAAG;QACH,GAAG;QACH,GAAG;MAAA;IAAA;EAGV;AACF;",
4
+ "sourcesContent": ["import type { AccessibilityProps } from '@refraction-ui/shared'\n\nexport type SeparatorOrientation = 'horizontal' | 'vertical'\n\nexport interface SeparatorProps {\n /** Orientation of the separator. Defaults to `'horizontal'`. */\n orientation?: SeparatorOrientation\n /**\n * When `true` (the default) the separator is purely visual and is hidden\n * from assistive tech (`role=\"none\"`). When `false` it is exposed as a\n * semantic `role=\"separator\"` with the appropriate `aria-orientation`.\n */\n decorative?: boolean\n}\n\n/**\n * ARIA attributes for a separator. Extends the shared base with\n * `aria-orientation`, which is specific to the `separator` role.\n */\nexport interface SeparatorAriaProps extends Partial<AccessibilityProps> {\n 'aria-orientation'?: SeparatorOrientation\n}\n\nexport interface SeparatorAPI {\n /** ARIA attributes to spread on the element. */\n ariaProps: SeparatorAriaProps\n /** Data attributes for CSS styling hooks. */\n dataAttributes: Record<string, string>\n}\n\n/**\n * Headless separator core — computes ARIA semantics and data attributes for\n * a separator rule. JSX-free so it can be shared across framework adapters.\n *\n * Per ARIA, a decorative separator must not be announced (`role=\"none\"`),\n * while a semantic one exposes `role=\"separator\"` and `aria-orientation`.\n */\nexport function createSeparator(props: SeparatorProps = {}): SeparatorAPI {\n const { orientation = 'horizontal', decorative = true } = props\n\n const ariaProps: SeparatorAriaProps = decorative\n ? { role: 'none' }\n : { role: 'separator', 'aria-orientation': orientation }\n\n const dataAttributes: Record<string, string> = {\n 'data-orientation': orientation,\n }\n\n return {\n ariaProps,\n dataAttributes,\n }\n}\n", "import { cva } from '@refraction-ui/shared'\n\n/**\n * Visual variants for the separator rule.\n *\n * - `orientation: horizontal` → a full-width 1px line.\n * - `orientation: vertical` → a full-height 1px line.\n * - `labeled` → flex layout used by the labeled-divider variant, which\n * arranges a centered label between two flanking lines.\n */\nexport const separatorVariants = cva({\n base: '',\n variants: {\n orientation: {\n horizontal: 'h-px w-full',\n vertical: 'w-px h-full',\n },\n // Line color (issue #485). `default` is the standard border token;\n // `subtle` uses the hairline `border-subtle` token for a lighter rule.\n // Keeping the color here (not in `orientation`) keeps every existing call\n // pixel-identical while adding the subtle option.\n tone: {\n default: 'bg-border',\n subtle: 'bg-border-subtle',\n },\n labeled: {\n true: 'flex items-center',\n false: '',\n },\n },\n defaultVariants: {\n orientation: 'horizontal',\n tone: 'default',\n labeled: 'false',\n },\n})\n\n/** Line tone for the separator rule and labeled-divider flanks. */\nexport type SeparatorTone = 'default' | 'subtle'\n\n/** Class for each flanking line in the labeled-divider variant. */\nexport const separatorLineClass = 'h-px flex-1 bg-border'\n\n/** Hairline variant of {@link separatorLineClass} (issue #485). */\nexport const separatorSubtleLineClass = 'h-px flex-1 bg-border-subtle'\n\n/** Class for the centered label in the labeled-divider variant. */\nexport const separatorLabelClass =\n 'px-3 text-xs uppercase tracking-wide text-muted-foreground'\n", "import * as React from 'react'\nimport {\n createSeparator,\n separatorVariants,\n separatorLineClass,\n separatorSubtleLineClass,\n separatorLabelClass,\n type SeparatorOrientation,\n type SeparatorTone,\n} from '@refraction-ui/separator'\nimport { cn } from '@refraction-ui/shared'\n\nexport interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Orientation of the separator. Defaults to `'horizontal'`. */\n orientation?: SeparatorOrientation\n /**\n * Line tone. `'default'` uses the standard border token; `'subtle'` uses the\n * hairline `border-subtle` token for a lighter rule (issue #485).\n */\n tone?: SeparatorTone\n /**\n * Optional centered label. Only meaningful for horizontal separators —\n * renders a line on each side of the label (a \"labeled divider\").\n */\n label?: React.ReactNode\n /**\n * When `true` (the default) the separator is purely visual and is hidden\n * from assistive tech (`role=\"none\"`). When `false` it is exposed as a\n * semantic `role=\"separator\"` with the appropriate `aria-orientation`.\n */\n decorative?: boolean\n}\n\n/**\n * Separator / LabeledDivider — a thin rule that visually divides content.\n *\n * Supports horizontal and vertical orientations and an optional centered\n * label (the \"labeled divider\" variant, horizontal only). State and ARIA\n * semantics come from the headless @refraction-ui/separator core; styling\n * via Tailwind utility classes (no external CSS-in-JS).\n */\nexport const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(\n function Separator(\n {\n orientation = 'horizontal',\n tone = 'default',\n label,\n decorative = true,\n className,\n ...props\n },\n ref,\n ) {\n // Labeled divider — decorative chrome wrapping a centered label.\n if (orientation === 'horizontal' && label != null) {\n const lineClass = tone === 'subtle' ? separatorSubtleLineClass : separatorLineClass\n return (\n <div\n ref={ref}\n role=\"none\"\n className={cn(separatorVariants({ labeled: 'true', tone }), className)}\n {...props}\n >\n <span className={lineClass} />\n <span className={separatorLabelClass}>{label}</span>\n <span className={lineClass} />\n </div>\n )\n }\n\n const { ariaProps, dataAttributes } = createSeparator({\n orientation,\n decorative,\n })\n\n return (\n <div\n ref={ref}\n className={cn(separatorVariants({ orientation, tone }), className)}\n {...ariaProps}\n {...dataAttributes}\n {...props}\n />\n )\n },\n)\n"],
5
+ "mappings": ";;;;;;;;;AAqCO,SAAS,gBAAgB,QAAwB,CAAA,GAAkB;AACxE,QAAM,EAAE,cAAc,cAAc,aAAa,KAAA,IAAS;AAE1D,QAAM,YAAgC,aAClC,EAAE,MAAM,OAAA,IACR,EAAE,MAAM,aAAa,oBAAoB,YAAA;AAE7C,QAAM,iBAAyC;IAC7C,oBAAoB;EAAA;AAGtB,SAAO;IACL;IACA;EAAA;AAEJ;AC1CO,IAAM,oBAAoB,IAAI;EACnC,MAAM;EACN,UAAU;IACR,aAAa;MACX,YAAY;MACZ,UAAU;IAAA;;;;;IAMZ,MAAM;MACJ,SAAS;MACT,QAAQ;IAAA;IAEV,SAAS;MACP,MAAM;MACN,OAAO;IAAA;EACT;EAEF,iBAAiB;IACf,aAAa;IACb,MAAM;IACN,SAAS;EAAA;AAEb,CAAC;AAMM,IAAM,qBAAqB;AAG3B,IAAM,2BAA2B;AAGjC,IAAM,sBACX;;;;ACPK,IAAM,YAAkB;EAC7B,SAASA,WACP;IACE,cAAc;IACd,OAAO;IACP;IACA,aAAa;IACb;IACA,GAAG;EAAA,GAEL,KACA;AAEA,QAAI,gBAAgB,gBAAgB,SAAS,MAAM;AACjD,YAAM,YAAY,SAAS,WAAW,2BAA2B;AACjE,aACE;QAAC;QAAA;UACC;UACA,MAAK;UACL,WAAW,GAAG,kBAAkB,EAAE,SAAS,QAAQ,KAAA,CAAM,GAAG,SAAS;UACpE,GAAG;UAEJ,UAAA;YAAA,oBAAC,QAAA,EAAK,WAAW,UAAA,CAAW;YAC5B,oBAAC,QAAA,EAAK,WAAW,qBAAsB,UAAA,MAAA,CAAM;YAC7C,oBAAC,QAAA,EAAK,WAAW,UAAA,CAAW;UAAA;QAAA;MAAA;IAGlC;AAEA,UAAM,EAAE,WAAW,eAAA,IAAmB,gBAAgB;MACpD;MACA;IAAA,CACD;AAED,WACE;MAAC;MAAA;QACC;QACA,WAAW,GAAG,kBAAkB,EAAE,aAAa,KAAA,CAAM,GAAG,SAAS;QAChE,GAAG;QACH,GAAG;QACH,GAAG;MAAA;IAAA;EAGV;AACF;",
6
6
  "names": ["Separator"]
7
7
  }
@@ -133,6 +133,8 @@ var slideViewerProgressBarVariants = cva({
133
133
  var slideTypeBadgeVariants = cva({
134
134
  base: "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
135
135
  variants: {
136
+ // Slide-type accents are a content-category palette (lesson/quiz/exercise…),
137
+ // not status tones — deliberately not routed through the theme tokens.
136
138
  type: {
137
139
  lesson: "bg-blue-100 text-blue-800",
138
140
  quiz: "bg-purple-100 text-purple-800",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../slide-viewer/src/slide-viewer.ts", "../../slide-viewer/src/slide-viewer.styles.ts", "../../react-slide-viewer/src/SlideViewer.tsx"],
4
- "sourcesContent": ["import type { AccessibilityProps, KeyboardHandlerMap } from '@refraction-ui/shared'\nimport { Keys } from '@refraction-ui/shared'\n\nexport type SlideType = 'lesson' | 'quiz' | 'exercise' | 'intro' | 'summary'\n\nexport type BookmarkType = 'important' | 'difficult' | 'to-revise'\n\nexport interface SlideData {\n id: string\n type: SlideType\n content: string\n}\n\nexport interface SlideViewerProps {\n slides: SlideData[]\n initialSlide?: number\n onSlideChange?: (index: number) => void\n onComplete?: () => void\n}\n\nexport interface SlideViewerState {\n currentSlide: number\n totalSlides: number\n bookmarks: Map<number, BookmarkType>\n}\n\nexport interface SlideViewerAPI {\n /** Current viewer state */\n state: SlideViewerState\n /** Navigate to the next slide */\n next: () => void\n /** Navigate to the previous slide */\n prev: () => void\n /** Navigate to a specific slide index */\n goTo: (index: number) => void\n /** Toggle a bookmark on the current slide */\n toggleBookmark: (type: BookmarkType) => void\n /** Get all bookmarks as an array of entries */\n getBookmarks: () => Array<{ slide: number; type: BookmarkType }>\n /** Current progress as a number between 0 and 1 */\n progress: number\n /** Keyboard handlers for slide navigation */\n keyboardHandlers: KeyboardHandlerMap\n /** ARIA props for the current slide */\n getSlideAriaProps: () => Partial<AccessibilityProps> & { 'aria-roledescription'?: string }\n}\n\n/**\n * Create a headless slide viewer.\n *\n * Manages slide navigation state, bookmarks, progress tracking,\n * and keyboard navigation. Framework wrappers handle rendering.\n */\nexport function createSlideViewer(props: SlideViewerProps): SlideViewerAPI {\n const { slides, initialSlide = 0, onSlideChange, onComplete } = props\n\n const totalSlides = slides.length\n const bookmarks = new Map<number, BookmarkType>()\n\n const state: SlideViewerState = {\n currentSlide: Math.max(0, Math.min(initialSlide, totalSlides - 1)),\n totalSlides,\n bookmarks,\n }\n\n function updateProgress(): number {\n if (totalSlides <= 1) return 1\n return state.currentSlide / (totalSlides - 1)\n }\n\n function next() {\n if (state.currentSlide < totalSlides - 1) {\n state.currentSlide++\n api.progress = updateProgress()\n onSlideChange?.(state.currentSlide)\n } else {\n // Already on the last slide — trigger complete\n onComplete?.()\n }\n }\n\n function prev() {\n if (state.currentSlide > 0) {\n state.currentSlide--\n api.progress = updateProgress()\n onSlideChange?.(state.currentSlide)\n }\n }\n\n function goTo(index: number) {\n if (index >= 0 && index < totalSlides) {\n state.currentSlide = index\n api.progress = updateProgress()\n onSlideChange?.(state.currentSlide)\n }\n }\n\n function toggleBookmark(type: BookmarkType) {\n const current = state.currentSlide\n if (bookmarks.has(current) && bookmarks.get(current) === type) {\n bookmarks.delete(current)\n } else {\n bookmarks.set(current, type)\n }\n }\n\n function getBookmarks(): Array<{ slide: number; type: BookmarkType }> {\n const result: Array<{ slide: number; type: BookmarkType }> = []\n for (const [slide, type] of bookmarks.entries()) {\n result.push({ slide, type })\n }\n return result.sort((a, b) => a.slide - b.slide)\n }\n\n const keyboardHandlers: KeyboardHandlerMap = {\n [Keys.ArrowRight]: (e) => {\n e.preventDefault()\n next()\n },\n [Keys.ArrowLeft]: (e) => {\n e.preventDefault()\n prev()\n },\n [Keys.Escape]: (e) => {\n e.preventDefault()\n },\n }\n\n function getSlideAriaProps(): Partial<AccessibilityProps> & { 'aria-roledescription'?: string } {\n const slide = slides[state.currentSlide]\n return {\n role: 'region',\n 'aria-roledescription': 'slide',\n 'aria-label': `Slide ${state.currentSlide + 1} of ${totalSlides}: ${slide?.type ?? 'unknown'} slide`,\n 'aria-live': 'polite',\n }\n }\n\n const api: SlideViewerAPI = {\n state,\n next,\n prev,\n goTo,\n toggleBookmark,\n getBookmarks,\n progress: updateProgress(),\n keyboardHandlers,\n getSlideAriaProps,\n }\n\n return api\n}\n", "import { cva } from '@refraction-ui/shared'\nimport type { TokenContract } from '@refraction-ui/shared'\n\nexport const slideViewerTokens: TokenContract = {\n name: 'slide-viewer',\n tokens: {\n bg: { variable: '--rfr-slide-bg', fallback: 'hsl(var(--background))' },\n fg: { variable: '--rfr-slide-fg', fallback: 'hsl(var(--foreground))' },\n progressBg: { variable: '--rfr-slide-progress-bg', fallback: 'hsl(var(--primary))' },\n progressTrack: { variable: '--rfr-slide-progress-track', fallback: 'hsl(var(--muted))' },\n badgeBg: { variable: '--rfr-slide-badge-bg', fallback: 'hsl(var(--secondary))' },\n },\n}\n\nexport const slideViewerVariants = cva({\n base: 'flex flex-col h-full w-full relative overflow-hidden',\n variants: {\n size: {\n compact: 'max-w-2xl',\n default: 'max-w-4xl',\n full: 'max-w-none',\n },\n },\n defaultVariants: {\n size: 'default',\n },\n})\n\nconst slideViewerProgressBarVariants = cva({\n base: 'h-1 w-full bg-muted rounded-full overflow-hidden',\n variants: {\n position: {\n top: 'rounded-none',\n bottom: 'rounded-none',\n inline: 'rounded-full',\n },\n },\n defaultVariants: {\n position: 'top',\n },\n})\n\nexport { slideViewerProgressBarVariants as progressBarVariants }\n\nexport const slideTypeBadgeVariants = cva({\n base: 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',\n variants: {\n type: {\n lesson: 'bg-blue-100 text-blue-800',\n quiz: 'bg-purple-100 text-purple-800',\n exercise: 'bg-green-100 text-green-800',\n intro: 'bg-gray-100 text-gray-800',\n summary: 'bg-orange-100 text-orange-800',\n },\n },\n defaultVariants: {\n type: 'lesson',\n },\n})\n", "import * as React from 'react'\nimport {\n createSlideViewer,\n slideViewerVariants,\n progressBarVariants,\n slideTypeBadgeVariants,\n type SlideData,\n type BookmarkType,\n type SlideType,\n} from '@refraction-ui/slide-viewer'\nimport { cn } from '@refraction-ui/shared'\n\nexport type { SlideData, BookmarkType, SlideType }\n\nexport interface SlideViewerProps {\n slides: SlideData[]\n initialSlide?: number\n onSlideChange?: (index: number) => void\n onComplete?: () => void\n className?: string\n size?: 'compact' | 'default' | 'full'\n /** Render function for custom slide content */\n renderSlide?: (slide: SlideData, index: number) => React.ReactNode\n}\n\n/**\n * SlideViewer component — slide-based content viewer with navigation,\n * progress bar, type badges, and bookmark support.\n *\n * Uses the headless @refraction-ui/slide-viewer core for state and navigation.\n */\nexport const SlideViewer = React.forwardRef<HTMLDivElement, SlideViewerProps>(\n function SlideViewer(\n {\n slides,\n initialSlide,\n onSlideChange,\n onComplete,\n className,\n size,\n renderSlide,\n },\n ref,\n ) {\n const [, setTick] = React.useState(0)\n const rerender = () => setTick((t) => t + 1)\n\n const apiRef = React.useRef(\n createSlideViewer({ slides, initialSlide, onSlideChange, onComplete }),\n )\n const api = apiRef.current\n\n const containerClasses = cn(slideViewerVariants({ size }), className)\n const currentSlide = slides[api.state.currentSlide]\n const slideAriaProps = api.getSlideAriaProps()\n // The core's keyboard handlers mutate state in place; re-render after a\n // handled key so the counter, progress bar, and buttons stay in sync.\n function handleKeyDown(event: KeyboardEvent) {\n const handler =\n api.keyboardHandlers[event.key as keyof typeof api.keyboardHandlers]\n if (!handler) return\n handler(event)\n rerender()\n }\n\n function handlePrev() {\n api.prev()\n rerender()\n }\n\n function handleNext() {\n api.next()\n rerender()\n }\n\n function handleBookmark(type: BookmarkType) {\n api.toggleBookmark(type)\n rerender()\n }\n\n const isBookmarked = api.state.bookmarks.has(api.state.currentSlide)\n\n return (\n <div\n ref={ref}\n className={containerClasses}\n onKeyDown={handleKeyDown as unknown as React.KeyboardEventHandler}\n tabIndex={0}\n >\n {/* Progress bar */}\n <div className={progressBarVariants({ position: 'top' })}>\n <div\n className=\"h-full bg-primary transition-all duration-300\"\n style={{ width: `${api.progress * 100}%` }}\n />\n </div>\n\n {/* Header with type badge and bookmark */}\n <div className=\"flex items-center justify-between px-4 py-2 border-b\">\n <div className=\"flex items-center gap-2\">\n {currentSlide && (\n <span className={slideTypeBadgeVariants({ type: currentSlide.type })}>\n {currentSlide.type}\n </span>\n )}\n <span className=\"text-sm text-muted-foreground\">\n {api.state.currentSlide + 1} / {api.state.totalSlides}\n </span>\n </div>\n <button\n type=\"button\"\n onClick={() => handleBookmark('important')}\n className={cn(\n 'px-2 py-1 rounded text-xs transition-colors',\n isBookmarked\n ? 'bg-yellow-100 text-yellow-800'\n : 'hover:bg-muted text-muted-foreground',\n )}\n aria-pressed={isBookmarked}\n aria-label=\"Toggle bookmark\"\n >\n {isBookmarked ? 'Bookmarked' : 'Bookmark'}\n </button>\n </div>\n\n {/* Slide content */}\n <div className=\"flex-1 overflow-auto p-6\" {...slideAriaProps}>\n {currentSlide &&\n (renderSlide\n ? renderSlide(currentSlide, api.state.currentSlide)\n : <div dangerouslySetInnerHTML={{ __html: currentSlide.content }} />)}\n </div>\n\n {/* Navigation buttons */}\n <div className=\"flex items-center justify-between px-4 py-3 border-t\">\n <button\n type=\"button\"\n onClick={handlePrev}\n disabled={api.state.currentSlide === 0}\n className=\"px-4 py-2 rounded text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed hover:bg-muted transition-colors\"\n aria-label=\"Previous slide\"\n >\n Previous\n </button>\n <button\n type=\"button\"\n onClick={handleNext}\n className=\"px-4 py-2 rounded text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 transition-colors\"\n aria-label={\n api.state.currentSlide === api.state.totalSlides - 1\n ? 'Complete'\n : 'Next slide'\n }\n >\n {api.state.currentSlide === api.state.totalSlides - 1 ? 'Complete' : 'Next'}\n </button>\n </div>\n </div>\n )\n },\n)\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { AccessibilityProps, KeyboardHandlerMap } from '@refraction-ui/shared'\nimport { Keys } from '@refraction-ui/shared'\n\nexport type SlideType = 'lesson' | 'quiz' | 'exercise' | 'intro' | 'summary'\n\nexport type BookmarkType = 'important' | 'difficult' | 'to-revise'\n\nexport interface SlideData {\n id: string\n type: SlideType\n content: string\n}\n\nexport interface SlideViewerProps {\n slides: SlideData[]\n initialSlide?: number\n onSlideChange?: (index: number) => void\n onComplete?: () => void\n}\n\nexport interface SlideViewerState {\n currentSlide: number\n totalSlides: number\n bookmarks: Map<number, BookmarkType>\n}\n\nexport interface SlideViewerAPI {\n /** Current viewer state */\n state: SlideViewerState\n /** Navigate to the next slide */\n next: () => void\n /** Navigate to the previous slide */\n prev: () => void\n /** Navigate to a specific slide index */\n goTo: (index: number) => void\n /** Toggle a bookmark on the current slide */\n toggleBookmark: (type: BookmarkType) => void\n /** Get all bookmarks as an array of entries */\n getBookmarks: () => Array<{ slide: number; type: BookmarkType }>\n /** Current progress as a number between 0 and 1 */\n progress: number\n /** Keyboard handlers for slide navigation */\n keyboardHandlers: KeyboardHandlerMap\n /** ARIA props for the current slide */\n getSlideAriaProps: () => Partial<AccessibilityProps> & { 'aria-roledescription'?: string }\n}\n\n/**\n * Create a headless slide viewer.\n *\n * Manages slide navigation state, bookmarks, progress tracking,\n * and keyboard navigation. Framework wrappers handle rendering.\n */\nexport function createSlideViewer(props: SlideViewerProps): SlideViewerAPI {\n const { slides, initialSlide = 0, onSlideChange, onComplete } = props\n\n const totalSlides = slides.length\n const bookmarks = new Map<number, BookmarkType>()\n\n const state: SlideViewerState = {\n currentSlide: Math.max(0, Math.min(initialSlide, totalSlides - 1)),\n totalSlides,\n bookmarks,\n }\n\n function updateProgress(): number {\n if (totalSlides <= 1) return 1\n return state.currentSlide / (totalSlides - 1)\n }\n\n function next() {\n if (state.currentSlide < totalSlides - 1) {\n state.currentSlide++\n api.progress = updateProgress()\n onSlideChange?.(state.currentSlide)\n } else {\n // Already on the last slide — trigger complete\n onComplete?.()\n }\n }\n\n function prev() {\n if (state.currentSlide > 0) {\n state.currentSlide--\n api.progress = updateProgress()\n onSlideChange?.(state.currentSlide)\n }\n }\n\n function goTo(index: number) {\n if (index >= 0 && index < totalSlides) {\n state.currentSlide = index\n api.progress = updateProgress()\n onSlideChange?.(state.currentSlide)\n }\n }\n\n function toggleBookmark(type: BookmarkType) {\n const current = state.currentSlide\n if (bookmarks.has(current) && bookmarks.get(current) === type) {\n bookmarks.delete(current)\n } else {\n bookmarks.set(current, type)\n }\n }\n\n function getBookmarks(): Array<{ slide: number; type: BookmarkType }> {\n const result: Array<{ slide: number; type: BookmarkType }> = []\n for (const [slide, type] of bookmarks.entries()) {\n result.push({ slide, type })\n }\n return result.sort((a, b) => a.slide - b.slide)\n }\n\n const keyboardHandlers: KeyboardHandlerMap = {\n [Keys.ArrowRight]: (e) => {\n e.preventDefault()\n next()\n },\n [Keys.ArrowLeft]: (e) => {\n e.preventDefault()\n prev()\n },\n [Keys.Escape]: (e) => {\n e.preventDefault()\n },\n }\n\n function getSlideAriaProps(): Partial<AccessibilityProps> & { 'aria-roledescription'?: string } {\n const slide = slides[state.currentSlide]\n return {\n role: 'region',\n 'aria-roledescription': 'slide',\n 'aria-label': `Slide ${state.currentSlide + 1} of ${totalSlides}: ${slide?.type ?? 'unknown'} slide`,\n 'aria-live': 'polite',\n }\n }\n\n const api: SlideViewerAPI = {\n state,\n next,\n prev,\n goTo,\n toggleBookmark,\n getBookmarks,\n progress: updateProgress(),\n keyboardHandlers,\n getSlideAriaProps,\n }\n\n return api\n}\n", "import { cva } from '@refraction-ui/shared'\nimport type { TokenContract } from '@refraction-ui/shared'\n\nexport const slideViewerTokens: TokenContract = {\n name: 'slide-viewer',\n tokens: {\n bg: { variable: '--rfr-slide-bg', fallback: 'hsl(var(--background))' },\n fg: { variable: '--rfr-slide-fg', fallback: 'hsl(var(--foreground))' },\n progressBg: { variable: '--rfr-slide-progress-bg', fallback: 'hsl(var(--primary))' },\n progressTrack: { variable: '--rfr-slide-progress-track', fallback: 'hsl(var(--muted))' },\n badgeBg: { variable: '--rfr-slide-badge-bg', fallback: 'hsl(var(--secondary))' },\n },\n}\n\nexport const slideViewerVariants = cva({\n base: 'flex flex-col h-full w-full relative overflow-hidden',\n variants: {\n size: {\n compact: 'max-w-2xl',\n default: 'max-w-4xl',\n full: 'max-w-none',\n },\n },\n defaultVariants: {\n size: 'default',\n },\n})\n\nconst slideViewerProgressBarVariants = cva({\n base: 'h-1 w-full bg-muted rounded-full overflow-hidden',\n variants: {\n position: {\n top: 'rounded-none',\n bottom: 'rounded-none',\n inline: 'rounded-full',\n },\n },\n defaultVariants: {\n position: 'top',\n },\n})\n\nexport { slideViewerProgressBarVariants as progressBarVariants }\n\nexport const slideTypeBadgeVariants = cva({\n base: 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',\n variants: {\n // Slide-type accents are a content-category palette (lesson/quiz/exercise…),\n // not status tones — deliberately not routed through the theme tokens.\n type: {\n lesson: 'bg-blue-100 text-blue-800',\n quiz: 'bg-purple-100 text-purple-800',\n exercise: 'bg-green-100 text-green-800',\n intro: 'bg-gray-100 text-gray-800',\n summary: 'bg-orange-100 text-orange-800',\n },\n },\n defaultVariants: {\n type: 'lesson',\n },\n})\n", "import * as React from 'react'\nimport {\n createSlideViewer,\n slideViewerVariants,\n progressBarVariants,\n slideTypeBadgeVariants,\n type SlideData,\n type BookmarkType,\n type SlideType,\n} from '@refraction-ui/slide-viewer'\nimport { cn } from '@refraction-ui/shared'\n\nexport type { SlideData, BookmarkType, SlideType }\n\nexport interface SlideViewerProps {\n slides: SlideData[]\n initialSlide?: number\n onSlideChange?: (index: number) => void\n onComplete?: () => void\n className?: string\n size?: 'compact' | 'default' | 'full'\n /** Render function for custom slide content */\n renderSlide?: (slide: SlideData, index: number) => React.ReactNode\n}\n\n/**\n * SlideViewer component — slide-based content viewer with navigation,\n * progress bar, type badges, and bookmark support.\n *\n * Uses the headless @refraction-ui/slide-viewer core for state and navigation.\n */\nexport const SlideViewer = React.forwardRef<HTMLDivElement, SlideViewerProps>(\n function SlideViewer(\n {\n slides,\n initialSlide,\n onSlideChange,\n onComplete,\n className,\n size,\n renderSlide,\n },\n ref,\n ) {\n const [, setTick] = React.useState(0)\n const rerender = () => setTick((t) => t + 1)\n\n const apiRef = React.useRef(\n createSlideViewer({ slides, initialSlide, onSlideChange, onComplete }),\n )\n const api = apiRef.current\n\n const containerClasses = cn(slideViewerVariants({ size }), className)\n const currentSlide = slides[api.state.currentSlide]\n const slideAriaProps = api.getSlideAriaProps()\n // The core's keyboard handlers mutate state in place; re-render after a\n // handled key so the counter, progress bar, and buttons stay in sync.\n function handleKeyDown(event: KeyboardEvent) {\n const handler =\n api.keyboardHandlers[event.key as keyof typeof api.keyboardHandlers]\n if (!handler) return\n handler(event)\n rerender()\n }\n\n function handlePrev() {\n api.prev()\n rerender()\n }\n\n function handleNext() {\n api.next()\n rerender()\n }\n\n function handleBookmark(type: BookmarkType) {\n api.toggleBookmark(type)\n rerender()\n }\n\n const isBookmarked = api.state.bookmarks.has(api.state.currentSlide)\n\n return (\n <div\n ref={ref}\n className={containerClasses}\n onKeyDown={handleKeyDown as unknown as React.KeyboardEventHandler}\n tabIndex={0}\n >\n {/* Progress bar */}\n <div className={progressBarVariants({ position: 'top' })}>\n <div\n className=\"h-full bg-primary transition-all duration-300\"\n style={{ width: `${api.progress * 100}%` }}\n />\n </div>\n\n {/* Header with type badge and bookmark */}\n <div className=\"flex items-center justify-between px-4 py-2 border-b\">\n <div className=\"flex items-center gap-2\">\n {currentSlide && (\n <span className={slideTypeBadgeVariants({ type: currentSlide.type })}>\n {currentSlide.type}\n </span>\n )}\n <span className=\"text-sm text-muted-foreground\">\n {api.state.currentSlide + 1} / {api.state.totalSlides}\n </span>\n </div>\n <button\n type=\"button\"\n onClick={() => handleBookmark('important')}\n className={cn(\n 'px-2 py-1 rounded text-xs transition-colors',\n isBookmarked\n ? 'bg-yellow-100 text-yellow-800'\n : 'hover:bg-muted text-muted-foreground',\n )}\n aria-pressed={isBookmarked}\n aria-label=\"Toggle bookmark\"\n >\n {isBookmarked ? 'Bookmarked' : 'Bookmark'}\n </button>\n </div>\n\n {/* Slide content */}\n <div className=\"flex-1 overflow-auto p-6\" {...slideAriaProps}>\n {currentSlide &&\n (renderSlide\n ? renderSlide(currentSlide, api.state.currentSlide)\n : <div dangerouslySetInnerHTML={{ __html: currentSlide.content }} />)}\n </div>\n\n {/* Navigation buttons */}\n <div className=\"flex items-center justify-between px-4 py-3 border-t\">\n <button\n type=\"button\"\n onClick={handlePrev}\n disabled={api.state.currentSlide === 0}\n className=\"px-4 py-2 rounded text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed hover:bg-muted transition-colors\"\n aria-label=\"Previous slide\"\n >\n Previous\n </button>\n <button\n type=\"button\"\n onClick={handleNext}\n className=\"px-4 py-2 rounded text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 transition-colors\"\n aria-label={\n api.state.currentSlide === api.state.totalSlides - 1\n ? 'Complete'\n : 'Next slide'\n }\n >\n {api.state.currentSlide === api.state.totalSlides - 1 ? 'Complete' : 'Next'}\n </button>\n </div>\n </div>\n )\n },\n)\n"],
5
+ "mappings": "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",
6
6
  "names": ["SlideViewer"]
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Button
3
- } from "./chunk-F7B64KT3.js";
3
+ } from "./chunk-NKCWOPNX.js";
4
4
  import "./chunk-WK2IIUFO.js";
5
5
  import {
6
6
  cn,
@@ -33,6 +33,8 @@ var stickyNoteVariants = cva({
33
33
  "min-w-[160px] min-h-[120px]"
34
34
  ].join(" "),
35
35
  variants: {
36
+ // Named paper colors are product semantics (physical sticky notes),
37
+ // not status tones — deliberately not routed through the theme tokens.
36
38
  color: {
37
39
  yellow: "bg-yellow-100 text-yellow-900",
38
40
  pink: "bg-pink-100 text-pink-900",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../sticky-note/src/sticky-note.ts", "../../sticky-note/src/sticky-note.styles.ts", "../../react-sticky-note/src/sticky-note.tsx"],
4
- "sourcesContent": ["/** The available color variants for a sticky note. */\nexport type StickyNoteColor = 'yellow' | 'pink' | 'blue' | 'green' | 'purple' | 'orange'\n\n/** Ordered list of all sticky note colors — used for cycling and iteration. */\nexport const STICKY_NOTE_COLORS: StickyNoteColor[] = [\n 'yellow',\n 'pink',\n 'blue',\n 'green',\n 'purple',\n 'orange',\n]\n\n/**\n * Returns the next color in the cycle. Wraps from the last color back to the\n * first. Useful for \"add note\" flows that auto-assign colors in sequence.\n */\nexport function nextStickyColor(color: StickyNoteColor): StickyNoteColor {\n const index = STICKY_NOTE_COLORS.indexOf(color)\n const next = (index + 1) % STICKY_NOTE_COLORS.length\n return STICKY_NOTE_COLORS[next]\n}\n\nexport interface CreateStickyNoteOptions {\n /** Color of the note. */\n color: StickyNoteColor\n}\n\nexport interface StickyNoteAPI {\n /** Data attributes for styling hooks; spread onto the note container. */\n dataAttributes: { 'data-color': StickyNoteColor }\n /** ARIA props for the note container. */\n ariaProps: { role: 'group' }\n}\n\n/**\n * Build the framework-agnostic props for a sticky note container.\n *\n * Returns `role=\"group\"` plus the `data-color` attribute that adapters use\n * as a styling hook. Logic is pure and SSR-safe — no Math.random, no Date.\n */\nexport function createStickyNote(options: CreateStickyNoteOptions): StickyNoteAPI {\n return {\n dataAttributes: { 'data-color': options.color },\n ariaProps: { role: 'group' },\n }\n}\n", "import { cva } from '@refraction-ui/shared'\n\n/**\n * Note container with color variant.\n *\n * Sticky notes are intrinsically colorful — an approved exception to the\n * semantic-token-only rule. Each color maps to a soft tint background with a\n * readable dark text.\n */\nexport const stickyNoteVariants = cva({\n base: [\n 'relative inline-flex flex-col gap-2 rounded-md p-4',\n 'shadow-md',\n 'min-w-[160px] min-h-[120px]',\n ].join(' '),\n variants: {\n color: {\n yellow: 'bg-yellow-100 text-yellow-900',\n pink: 'bg-pink-100 text-pink-900',\n blue: 'bg-blue-100 text-blue-900',\n green: 'bg-green-100 text-green-900',\n purple: 'bg-purple-100 text-purple-900',\n orange: 'bg-orange-100 text-orange-900',\n },\n },\n defaultVariants: {\n color: 'yellow',\n },\n})\n\n/** Class for the editable/static text area inside the note. */\nexport const stickyNoteTextClass =\n 'flex-1 w-full resize-none bg-transparent text-sm leading-relaxed placeholder:opacity-60 outline-none'\n\n/** Class for the author chip rendered in the note footer. */\nexport const stickyNoteAuthorClass =\n 'mt-auto text-xs font-medium opacity-70 truncate'\n", "import * as React from 'react'\nimport {\n createStickyNote,\n stickyNoteVariants,\n stickyNoteTextClass,\n stickyNoteAuthorClass,\n type StickyNoteColor,\n} from '@refraction-ui/sticky-note'\nimport { cn } from '@refraction-ui/shared'\n\nexport type { StickyNoteColor }\n\nexport interface StickyNoteProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, 'color' | 'content' | 'onSelect'> {\n /** Color of the sticky note. */\n color?: StickyNoteColor\n /** Text content of the note. */\n text?: string\n /** Called when the text changes (makes text editable via a textarea). */\n onTextChange?: (value: string) => void\n /** Optional author name shown in the note footer. */\n author?: string\n /** Absolute x position in pixels (for board/canvas placement). */\n x?: number\n /** Absolute y position in pixels (for board/canvas placement). */\n y?: number\n /** Whether the note can be dragged. Requires `onMove` to receive updates. */\n draggable?: boolean\n /** Called with the new `{ x, y }` position after a drag completes. */\n onMove?: (position: { x: number; y: number }) => void\n}\n\n/**\n * StickyNote — a draggable colored note for whiteboard / canvas surfaces.\n *\n * - `color` selects the soft-tint palette (yellow, pink, blue, green, purple, orange).\n * - `text` + `onTextChange` switch to an editable textarea; without `onTextChange` the\n * text is static.\n * - `x` / `y` absolutely position the note; pair with `draggable` + `onMove` for\n * drag-to-reposition. Drag is implemented via pointer events and is SSR-safe (no\n * handlers attach until the component mounts on the client).\n * - Forwards the ref to the outer `<div>`.\n */\nexport const StickyNote = React.forwardRef<HTMLDivElement, StickyNoteProps>(\n function StickyNote(\n {\n color = 'yellow',\n text,\n onTextChange,\n author,\n x,\n y,\n draggable = false,\n onMove,\n className,\n style,\n children,\n ...props\n },\n ref,\n ) {\n const { ariaProps, dataAttributes } = createStickyNote({ color })\n\n // Drag state — kept in refs to avoid re-renders during drag.\n const dragOrigin = React.useRef<{ pointerX: number; pointerY: number; noteX: number; noteY: number } | null>(null)\n\n const positionStyle: React.CSSProperties =\n x !== undefined && y !== undefined\n ? { position: 'absolute', left: x, top: y }\n : {}\n\n const handlePointerDown = React.useCallback(\n (e: React.PointerEvent<HTMLDivElement>) => {\n if (!draggable || !onMove) return\n // Only primary button.\n if (e.button !== 0) return\n e.currentTarget.setPointerCapture(e.pointerId)\n dragOrigin.current = {\n pointerX: e.clientX,\n pointerY: e.clientY,\n noteX: x ?? 0,\n noteY: y ?? 0,\n }\n },\n [draggable, onMove, x, y],\n )\n\n const handlePointerMove = React.useCallback(\n (e: React.PointerEvent<HTMLDivElement>) => {\n if (!dragOrigin.current || !onMove) return\n const dx = e.clientX - dragOrigin.current.pointerX\n const dy = e.clientY - dragOrigin.current.pointerY\n onMove({ x: dragOrigin.current.noteX + dx, y: dragOrigin.current.noteY + dy })\n },\n [onMove],\n )\n\n const handlePointerUp = React.useCallback(() => {\n dragOrigin.current = null\n }, [])\n\n return (\n <div\n ref={ref}\n className={cn(stickyNoteVariants({ color }), className)}\n style={{ ...positionStyle, ...style }}\n {...ariaProps}\n {...dataAttributes}\n onPointerDown={draggable && onMove ? handlePointerDown : undefined}\n onPointerMove={draggable && onMove ? handlePointerMove : undefined}\n onPointerUp={draggable && onMove ? handlePointerUp : undefined}\n {...props}\n >\n {onTextChange != null ? (\n <textarea\n className={stickyNoteTextClass}\n value={text ?? ''}\n onChange={(e) => onTextChange(e.target.value)}\n placeholder=\"Type a note…\"\n />\n ) : (\n <p className={`${stickyNoteTextClass} whitespace-pre-wrap`}>{text ?? children}</p>\n )}\n {author != null && (\n <span className={stickyNoteAuthorClass}>{author}</span>\n )}\n </div>\n )\n },\n)\n"],
5
- "mappings": ";;;;;;;;;AAIO,IAAM,qBAAwC;EACnD;EACA;EACA;EACA;EACA;EACA;AACF;AAMO,SAAS,gBAAgB,OAAyC;AACvE,QAAM,QAAQ,mBAAmB,QAAQ,KAAK;AAC9C,QAAM,QAAQ,QAAQ,KAAK,mBAAmB;AAC9C,SAAO,mBAAmB,IAAI;AAChC;AAoBO,SAAS,iBAAiB,SAAiD;AAChF,SAAO;IACL,gBAAgB,EAAE,cAAc,QAAQ,MAAA;IACxC,WAAW,EAAE,MAAM,QAAA;EAAQ;AAE/B;ACrCO,IAAM,qBAAqB,IAAI;EACpC,MAAM;IACJ;IACA;IACA;EAAA,EACA,KAAK,GAAG;EACV,UAAU;IACR,OAAO;MACL,QAAQ;MACR,MAAM;MACN,MAAM;MACN,OAAO;MACP,QAAQ;MACR,QAAQ;IAAA;EACV;EAEF,iBAAiB;IACf,OAAO;EAAA;AAEX,CAAC;AAGM,IAAM,sBACX;AAGK,IAAM,wBACX;;;;ACOK,IAAM,aAAmB;EAC9B,SAASA,YACP;IACE,QAAQ;IACR;IACA;IACA;IACA;IACA;IACA,YAAY;IACZ;IACA;IACA;IACA;IACA,GAAG;EAAA,GAEL,KACA;AACA,UAAM,EAAE,WAAW,eAAA,IAAmB,iBAAiB,EAAE,MAAA,CAAO;AAGhE,UAAM,aAAmB,aAAoF,IAAI;AAEjH,UAAM,gBACJ,MAAM,UAAa,MAAM,SACrB,EAAE,UAAU,YAAY,MAAM,GAAG,KAAK,EAAA,IACtC,CAAA;AAEN,UAAM,oBAA0B;MAC9B,CAAC,MAA0C;AACzC,YAAI,CAAC,aAAa,CAAC,OAAQ;AAE3B,YAAI,EAAE,WAAW,EAAG;AACpB,UAAE,cAAc,kBAAkB,EAAE,SAAS;AAC7C,mBAAW,UAAU;UACnB,UAAU,EAAE;UACZ,UAAU,EAAE;UACZ,OAAO,KAAK;UACZ,OAAO,KAAK;QAAA;MAEhB;MACA,CAAC,WAAW,QAAQ,GAAG,CAAC;IAAA;AAG1B,UAAM,oBAA0B;MAC9B,CAAC,MAA0C;AACzC,YAAI,CAAC,WAAW,WAAW,CAAC,OAAQ;AACpC,cAAM,KAAK,EAAE,UAAU,WAAW,QAAQ;AAC1C,cAAM,KAAK,EAAE,UAAU,WAAW,QAAQ;AAC1C,eAAO,EAAE,GAAG,WAAW,QAAQ,QAAQ,IAAI,GAAG,WAAW,QAAQ,QAAQ,GAAA,CAAI;MAC/E;MACA,CAAC,MAAM;IAAA;AAGT,UAAM,kBAAwB,kBAAY,MAAM;AAC9C,iBAAW,UAAU;IACvB,GAAG,CAAA,CAAE;AAEL,WACE;MAAC;MAAA;QACC;QACA,WAAW,GAAG,mBAAmB,EAAE,MAAA,CAAO,GAAG,SAAS;QACtD,OAAO,EAAE,GAAG,eAAe,GAAG,MAAA;QAC7B,GAAG;QACH,GAAG;QACJ,eAAe,aAAa,SAAS,oBAAoB;QACzD,eAAe,aAAa,SAAS,oBAAoB;QACzD,aAAa,aAAa,SAAS,kBAAkB;QACpD,GAAG;QAEH,UAAA;UAAA,gBAAgB,OACf;YAAC;YAAA;cACC,WAAW;cACX,OAAO,QAAQ;cACf,UAAU,CAAC,MAAM,aAAa,EAAE,OAAO,KAAK;cAC5C,aAAY;YAAA;UAAA,IACd,oBAEC,KAAA,EAAE,WAAW,GAAG,mBAAmB,wBAAyB,UAAA,QAAQ,SAAA,CAAS;UAE/E,UAAU,QACT,oBAAC,QAAA,EAAK,WAAW,uBAAwB,UAAA,OAAA,CAAO;QAAA;MAAA;IAAA;EAIxD;AACF;",
4
+ "sourcesContent": ["/** The available color variants for a sticky note. */\nexport type StickyNoteColor = 'yellow' | 'pink' | 'blue' | 'green' | 'purple' | 'orange'\n\n/** Ordered list of all sticky note colors — used for cycling and iteration. */\nexport const STICKY_NOTE_COLORS: StickyNoteColor[] = [\n 'yellow',\n 'pink',\n 'blue',\n 'green',\n 'purple',\n 'orange',\n]\n\n/**\n * Returns the next color in the cycle. Wraps from the last color back to the\n * first. Useful for \"add note\" flows that auto-assign colors in sequence.\n */\nexport function nextStickyColor(color: StickyNoteColor): StickyNoteColor {\n const index = STICKY_NOTE_COLORS.indexOf(color)\n const next = (index + 1) % STICKY_NOTE_COLORS.length\n return STICKY_NOTE_COLORS[next]\n}\n\nexport interface CreateStickyNoteOptions {\n /** Color of the note. */\n color: StickyNoteColor\n}\n\nexport interface StickyNoteAPI {\n /** Data attributes for styling hooks; spread onto the note container. */\n dataAttributes: { 'data-color': StickyNoteColor }\n /** ARIA props for the note container. */\n ariaProps: { role: 'group' }\n}\n\n/**\n * Build the framework-agnostic props for a sticky note container.\n *\n * Returns `role=\"group\"` plus the `data-color` attribute that adapters use\n * as a styling hook. Logic is pure and SSR-safe — no Math.random, no Date.\n */\nexport function createStickyNote(options: CreateStickyNoteOptions): StickyNoteAPI {\n return {\n dataAttributes: { 'data-color': options.color },\n ariaProps: { role: 'group' },\n }\n}\n", "import { cva } from '@refraction-ui/shared'\n\n/**\n * Note container with color variant.\n *\n * Sticky notes are intrinsically colorful — an approved exception to the\n * semantic-token-only rule. Each color maps to a soft tint background with a\n * readable dark text.\n */\nexport const stickyNoteVariants = cva({\n base: [\n 'relative inline-flex flex-col gap-2 rounded-md p-4',\n 'shadow-md',\n 'min-w-[160px] min-h-[120px]',\n ].join(' '),\n variants: {\n // Named paper colors are product semantics (physical sticky notes),\n // not status tones — deliberately not routed through the theme tokens.\n color: {\n yellow: 'bg-yellow-100 text-yellow-900',\n pink: 'bg-pink-100 text-pink-900',\n blue: 'bg-blue-100 text-blue-900',\n green: 'bg-green-100 text-green-900',\n purple: 'bg-purple-100 text-purple-900',\n orange: 'bg-orange-100 text-orange-900',\n },\n },\n defaultVariants: {\n color: 'yellow',\n },\n})\n\n/** Class for the editable/static text area inside the note. */\nexport const stickyNoteTextClass =\n 'flex-1 w-full resize-none bg-transparent text-sm leading-relaxed placeholder:opacity-60 outline-none'\n\n/** Class for the author chip rendered in the note footer. */\nexport const stickyNoteAuthorClass =\n 'mt-auto text-xs font-medium opacity-70 truncate'\n", "import * as React from 'react'\nimport {\n createStickyNote,\n stickyNoteVariants,\n stickyNoteTextClass,\n stickyNoteAuthorClass,\n type StickyNoteColor,\n} from '@refraction-ui/sticky-note'\nimport { cn } from '@refraction-ui/shared'\n\nexport type { StickyNoteColor }\n\nexport interface StickyNoteProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, 'color' | 'content' | 'onSelect'> {\n /** Color of the sticky note. */\n color?: StickyNoteColor\n /** Text content of the note. */\n text?: string\n /** Called when the text changes (makes text editable via a textarea). */\n onTextChange?: (value: string) => void\n /** Optional author name shown in the note footer. */\n author?: string\n /** Absolute x position in pixels (for board/canvas placement). */\n x?: number\n /** Absolute y position in pixels (for board/canvas placement). */\n y?: number\n /** Whether the note can be dragged. Requires `onMove` to receive updates. */\n draggable?: boolean\n /** Called with the new `{ x, y }` position after a drag completes. */\n onMove?: (position: { x: number; y: number }) => void\n}\n\n/**\n * StickyNote — a draggable colored note for whiteboard / canvas surfaces.\n *\n * - `color` selects the soft-tint palette (yellow, pink, blue, green, purple, orange).\n * - `text` + `onTextChange` switch to an editable textarea; without `onTextChange` the\n * text is static.\n * - `x` / `y` absolutely position the note; pair with `draggable` + `onMove` for\n * drag-to-reposition. Drag is implemented via pointer events and is SSR-safe (no\n * handlers attach until the component mounts on the client).\n * - Forwards the ref to the outer `<div>`.\n */\nexport const StickyNote = React.forwardRef<HTMLDivElement, StickyNoteProps>(\n function StickyNote(\n {\n color = 'yellow',\n text,\n onTextChange,\n author,\n x,\n y,\n draggable = false,\n onMove,\n className,\n style,\n children,\n ...props\n },\n ref,\n ) {\n const { ariaProps, dataAttributes } = createStickyNote({ color })\n\n // Drag state — kept in refs to avoid re-renders during drag.\n const dragOrigin = React.useRef<{ pointerX: number; pointerY: number; noteX: number; noteY: number } | null>(null)\n\n const positionStyle: React.CSSProperties =\n x !== undefined && y !== undefined\n ? { position: 'absolute', left: x, top: y }\n : {}\n\n const handlePointerDown = React.useCallback(\n (e: React.PointerEvent<HTMLDivElement>) => {\n if (!draggable || !onMove) return\n // Only primary button.\n if (e.button !== 0) return\n e.currentTarget.setPointerCapture(e.pointerId)\n dragOrigin.current = {\n pointerX: e.clientX,\n pointerY: e.clientY,\n noteX: x ?? 0,\n noteY: y ?? 0,\n }\n },\n [draggable, onMove, x, y],\n )\n\n const handlePointerMove = React.useCallback(\n (e: React.PointerEvent<HTMLDivElement>) => {\n if (!dragOrigin.current || !onMove) return\n const dx = e.clientX - dragOrigin.current.pointerX\n const dy = e.clientY - dragOrigin.current.pointerY\n onMove({ x: dragOrigin.current.noteX + dx, y: dragOrigin.current.noteY + dy })\n },\n [onMove],\n )\n\n const handlePointerUp = React.useCallback(() => {\n dragOrigin.current = null\n }, [])\n\n return (\n <div\n ref={ref}\n className={cn(stickyNoteVariants({ color }), className)}\n style={{ ...positionStyle, ...style }}\n {...ariaProps}\n {...dataAttributes}\n onPointerDown={draggable && onMove ? handlePointerDown : undefined}\n onPointerMove={draggable && onMove ? handlePointerMove : undefined}\n onPointerUp={draggable && onMove ? handlePointerUp : undefined}\n {...props}\n >\n {onTextChange != null ? (\n <textarea\n className={stickyNoteTextClass}\n value={text ?? ''}\n onChange={(e) => onTextChange(e.target.value)}\n placeholder=\"Type a note…\"\n />\n ) : (\n <p className={`${stickyNoteTextClass} whitespace-pre-wrap`}>{text ?? children}</p>\n )}\n {author != null && (\n <span className={stickyNoteAuthorClass}>{author}</span>\n )}\n </div>\n )\n },\n)\n"],
5
+ "mappings": ";;;;;;;;;AAIO,IAAM,qBAAwC;EACnD;EACA;EACA;EACA;EACA;EACA;AACF;AAMO,SAAS,gBAAgB,OAAyC;AACvE,QAAM,QAAQ,mBAAmB,QAAQ,KAAK;AAC9C,QAAM,QAAQ,QAAQ,KAAK,mBAAmB;AAC9C,SAAO,mBAAmB,IAAI;AAChC;AAoBO,SAAS,iBAAiB,SAAiD;AAChF,SAAO;IACL,gBAAgB,EAAE,cAAc,QAAQ,MAAA;IACxC,WAAW,EAAE,MAAM,QAAA;EAAQ;AAE/B;ACrCO,IAAM,qBAAqB,IAAI;EACpC,MAAM;IACJ;IACA;IACA;EAAA,EACA,KAAK,GAAG;EACV,UAAU;;;IAGR,OAAO;MACL,QAAQ;MACR,MAAM;MACN,MAAM;MACN,OAAO;MACP,QAAQ;MACR,QAAQ;IAAA;EACV;EAEF,iBAAiB;IACf,OAAO;EAAA;AAEX,CAAC;AAGM,IAAM,sBACX;AAGK,IAAM,wBACX;;;;ACKK,IAAM,aAAmB;EAC9B,SAASA,YACP;IACE,QAAQ;IACR;IACA;IACA;IACA;IACA;IACA,YAAY;IACZ;IACA;IACA;IACA;IACA,GAAG;EAAA,GAEL,KACA;AACA,UAAM,EAAE,WAAW,eAAA,IAAmB,iBAAiB,EAAE,MAAA,CAAO;AAGhE,UAAM,aAAmB,aAAoF,IAAI;AAEjH,UAAM,gBACJ,MAAM,UAAa,MAAM,SACrB,EAAE,UAAU,YAAY,MAAM,GAAG,KAAK,EAAA,IACtC,CAAA;AAEN,UAAM,oBAA0B;MAC9B,CAAC,MAA0C;AACzC,YAAI,CAAC,aAAa,CAAC,OAAQ;AAE3B,YAAI,EAAE,WAAW,EAAG;AACpB,UAAE,cAAc,kBAAkB,EAAE,SAAS;AAC7C,mBAAW,UAAU;UACnB,UAAU,EAAE;UACZ,UAAU,EAAE;UACZ,OAAO,KAAK;UACZ,OAAO,KAAK;QAAA;MAEhB;MACA,CAAC,WAAW,QAAQ,GAAG,CAAC;IAAA;AAG1B,UAAM,oBAA0B;MAC9B,CAAC,MAA0C;AACzC,YAAI,CAAC,WAAW,WAAW,CAAC,OAAQ;AACpC,cAAM,KAAK,EAAE,UAAU,WAAW,QAAQ;AAC1C,cAAM,KAAK,EAAE,UAAU,WAAW,QAAQ;AAC1C,eAAO,EAAE,GAAG,WAAW,QAAQ,QAAQ,IAAI,GAAG,WAAW,QAAQ,QAAQ,GAAA,CAAI;MAC/E;MACA,CAAC,MAAM;IAAA;AAGT,UAAM,kBAAwB,kBAAY,MAAM;AAC9C,iBAAW,UAAU;IACvB,GAAG,CAAA,CAAE;AAEL,WACE;MAAC;MAAA;QACC;QACA,WAAW,GAAG,mBAAmB,EAAE,MAAA,CAAO,GAAG,SAAS;QACtD,OAAO,EAAE,GAAG,eAAe,GAAG,MAAA;QAC7B,GAAG;QACH,GAAG;QACJ,eAAe,aAAa,SAAS,oBAAoB;QACzD,eAAe,aAAa,SAAS,oBAAoB;QACzD,aAAa,aAAa,SAAS,kBAAkB;QACpD,GAAG;QAEH,UAAA;UAAA,gBAAgB,OACf;YAAC;YAAA;cACC,WAAW;cACX,OAAO,QAAQ;cACf,UAAU,CAAC,MAAM,aAAa,EAAE,OAAO,KAAK;cAC5C,aAAY;YAAA;UAAA,IACd,oBAEC,KAAA,EAAE,WAAW,GAAG,mBAAmB,wBAAyB,UAAA,QAAQ,SAAA,CAAS;UAE/E,UAAU,QACT,oBAAC,QAAA,EAAK,WAAW,uBAAwB,UAAA,OAAA,CAAO;QAAA;MAAA;IAAA;EAIxD;AACF;",
6
6
  "names": ["StickyNote"]
7
7
  }
package/dist/styles.css CHANGED
@@ -74,6 +74,32 @@
74
74
  --input: 240 6% 92%;
75
75
  --ring: 250 50% 50%;
76
76
 
77
+ /* ─────────────────────────────────────────────────────────────────────
78
+ Extended semantic roles (issue #485) — additive, optional
79
+ ───────────────────────────────────────────────────────────────────── */
80
+ /* Brand depth: hover/active steps + a soft tinted brand fill */
81
+ --primary-hover: 250 50% 44%;
82
+ --primary-active: 250 50% 38%;
83
+ --primary-soft: 250 30% 95%;
84
+ --primary-soft-foreground: 250 50% 40%;
85
+ /* Convention: brand gradient from the primary → primary-active steps */
86
+ --primary-gradient: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--primary-active)));
87
+ /* Second brand accent */
88
+ --tertiary: 173 75% 29%;
89
+ --tertiary-foreground: 0 0% 100%;
90
+ --tertiary-soft: 173 35% 92%;
91
+ --tertiary-soft-foreground: 173 60% 26%;
92
+ /* Ink / surface */
93
+ --placeholder: 240 4% 52%;
94
+ --surface-subtle: 240 5% 97%;
95
+ --border-subtle: 240 6% 95%;
96
+ /* Extended status: resting/positive, caution, done */
97
+ --positive: 142 71% 35%;
98
+ --positive-foreground: 0 0% 100%;
99
+ --caution: 38 92% 50%;
100
+ --caution-foreground: 240 10% 10%;
101
+ --done: 217 91% 60%;
102
+
77
103
  /* Charts (colorblind-safe spread) */
78
104
  --chart-1: 250 50% 50%;
79
105
  --chart-2: 173 80% 36%;
@@ -102,6 +128,9 @@
102
128
  --radius-xl: 0.75rem;
103
129
  --radius-2xl: 1rem;
104
130
  --radius-full: 9999px;
131
+ /* Semantic shape roles (issue #485): pill track + large sheet/drawer corner */
132
+ --radius-pill: 9999px;
133
+ --radius-sheet: 1.5rem;
105
134
 
106
135
  --avatar-radius: 9999px;
107
136
  --badge-radius: 9999px;
@@ -289,6 +318,25 @@
289
318
  --input: 240 5% 16%;
290
319
  --ring: 250 50% 65%;
291
320
 
321
+ /* Extended semantic roles (issue #485) — dark mode */
322
+ --primary-hover: 250 50% 71%;
323
+ --primary-active: 250 50% 77%;
324
+ --primary-soft: 250 30% 20%;
325
+ --primary-soft-foreground: 250 60% 80%;
326
+ --primary-gradient: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--primary-active)));
327
+ --tertiary: 173 60% 55%;
328
+ --tertiary-foreground: 240 10% 4%;
329
+ --tertiary-soft: 173 30% 18%;
330
+ --tertiary-soft-foreground: 173 55% 70%;
331
+ --placeholder: 240 5% 48%;
332
+ --surface-subtle: 240 10% 6%;
333
+ --border-subtle: 240 5% 13%;
334
+ --positive: 142 71% 45%;
335
+ --positive-foreground: 0 0% 98%;
336
+ --caution: 38 92% 50%;
337
+ --caution-foreground: 240 10% 4%;
338
+ --done: 217 91% 65%;
339
+
292
340
  --chart-1: 250 50% 65%;
293
341
  --chart-2: 173 70% 50%;
294
342
  --chart-3: 38 92% 60%;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@refraction-ui/react",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "description": "All Refraction UI React components in one package — headless, accessible, zero-dependency",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../button/src/button.ts", "../../button/src/button.styles.ts", "../../react-button/src/button.tsx"],
4
- "sourcesContent": ["import type { AccessibilityProps, KeyboardHandlerMap } from '@refraction-ui/shared'\nimport { Keys } from '@refraction-ui/shared'\n\n/**\n * `primary` is an alias of `default` — included so the universal React-ecosystem\n * muscle-memory (`<Button variant=\"primary\">`) renders as the most-emphasized\n * button instead of falling through to the unstyled base. See issue #201.\n */\nexport type ButtonVariant =\n | 'default'\n | 'primary'\n | 'destructive'\n | 'outline'\n | 'secondary'\n | 'ghost'\n | 'link'\n\n/** Variants that map to the same visual style as `default`. */\nexport const BUTTON_VARIANT_ALIASES: Partial<Record<ButtonVariant, ButtonVariant>> = {\n primary: 'default',\n}\n\n/** Resolve an alias to its canonical variant. Pass-through for unknown/undefined. */\nexport function resolveButtonVariant(variant: ButtonVariant | undefined): ButtonVariant | undefined {\n if (!variant) return variant\n return BUTTON_VARIANT_ALIASES[variant] ?? variant\n}\nexport type ButtonSize = 'xs' | 'sm' | 'default' | 'lg' | 'icon'\n\nexport interface ButtonProps {\n variant?: ButtonVariant\n size?: ButtonSize\n disabled?: boolean\n loading?: boolean\n asChild?: boolean\n type?: 'button' | 'submit' | 'reset'\n}\n\nexport interface ButtonAPI {\n /** ARIA attributes to spread on the element */\n ariaProps: Partial<AccessibilityProps>\n /** Keyboard handlers to attach */\n keyboardHandlers: KeyboardHandlerMap\n /** Whether interaction should be blocked */\n isInteractive: boolean\n /** Data attributes for CSS styling hooks */\n dataAttributes: Record<string, string>\n}\n\nexport function createButton(props: ButtonProps = {}): ButtonAPI {\n const { disabled = false, loading = false } = props\n const isInteractive = !disabled && !loading\n\n const ariaProps: Partial<AccessibilityProps> = {}\n if (disabled) {\n ariaProps['aria-disabled'] = true\n }\n if (loading) {\n ariaProps['aria-disabled'] = true\n ariaProps['aria-label'] = ariaProps['aria-label'] ?? undefined\n }\n\n const dataAttributes: Record<string, string> = {}\n if (loading) {\n dataAttributes['data-loading'] = ''\n }\n if (disabled) {\n dataAttributes['data-disabled'] = ''\n }\n\n const keyboardHandlers: KeyboardHandlerMap = {}\n if (!isInteractive) {\n keyboardHandlers[Keys.Enter] = (e) => e.preventDefault()\n keyboardHandlers[Keys.Space] = (e) => e.preventDefault()\n }\n\n return {\n ariaProps,\n keyboardHandlers,\n isInteractive,\n dataAttributes,\n }\n}\n\nexport function getButtonType(props: ButtonProps): string {\n return props.type ?? 'button'\n}\n", "import { cva } from '@refraction-ui/shared'\nimport type { TokenContract } from '@refraction-ui/shared'\n\nexport const buttonTokens: TokenContract = {\n name: 'button',\n tokens: {\n bg: { variable: '--rfr-button-bg', fallback: 'hsl(var(--primary))' },\n fg: { variable: '--rfr-button-fg', fallback: 'hsl(var(--primary-foreground))' },\n border: { variable: '--rfr-button-border', fallback: 'hsl(var(--border))' },\n radius: { variable: '--rfr-button-radius', fallback: 'var(--radius)' },\n ring: { variable: '--rfr-button-ring', fallback: 'hsl(var(--ring))' },\n },\n}\n\nexport const buttonVariants = cva({\n base: 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',\n variants: {\n variant: {\n default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',\n // Alias for `default` — see resolveButtonVariant + issue #201. Same classes\n // so styling stays identical even if a consumer reaches into `buttonVariants`\n // directly without going through resolveButtonVariant.\n primary: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',\n destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',\n outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',\n secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',\n ghost: 'hover:bg-accent hover:text-accent-foreground',\n link: 'text-primary underline-offset-4 hover:underline',\n },\n size: {\n xs: 'h-7 rounded px-2 text-xs',\n sm: 'h-8 rounded-md px-3 text-xs',\n default: 'h-9 px-4 py-2',\n lg: 'h-10 rounded-md px-8',\n icon: 'h-9 w-9',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n})\n", "import * as React from 'react'\nimport {\n createButton,\n buttonVariants,\n getButtonType,\n resolveButtonVariant,\n type ButtonVariant,\n type ButtonSize,\n} from '@refraction-ui/button'\nimport { useShortcut, ShortcutHint } from '@refraction-ui/react-keyboard-shortcut'\nimport { cn } from '@refraction-ui/shared'\n\nexport interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n variant?: ButtonVariant\n size?: ButtonSize\n loading?: boolean\n asChild?: boolean\n shortcut?: string\n action?: string\n}\n\n/**\n * Button component — renders a styled button with variant and size support.\n *\n * Uses the headless @refraction-ui/button core for state, ARIA, and keyboard handling.\n * Styling via Tailwind utility classes (no external CSS-in-JS).\n */\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n function Button({ variant, size, loading, asChild, className, disabled, children, shortcut, action, ...props }, ref) {\n // `primary` is an ecosystem-muscle-memory alias for `default` — resolve it\n // here so styling, aria, and data attributes all see the canonical variant.\n // See issue #201.\n const resolvedVariant = resolveButtonVariant(variant)\n const api = createButton({ variant: resolvedVariant, size, disabled, loading, asChild, type: props.type })\n const classes = cn(buttonVariants({ variant: resolvedVariant, size }), className)\n \n const internalRef = React.useRef<HTMLButtonElement>(null)\n const mergedRef = React.useCallback(\n (node: HTMLButtonElement | null) => {\n internalRef.current = node\n if (typeof ref === 'function') ref(node)\n else if (ref) ref.current = node\n },\n [ref]\n )\n\n useShortcut({\n shortcut,\n action,\n enabled: !disabled && !loading && (!!shortcut || !!action),\n onTrigger: () => {\n internalRef.current?.click()\n },\n })\n\n // When asChild, render the child element directly with button props merged in.\n // This allows <Button asChild><a href=\"/\">Link</a></Button>\n if (asChild && React.isValidElement(children)) {\n return React.cloneElement(children as React.ReactElement<Record<string, unknown>>, {\n ref: mergedRef,\n className: cn(classes, (children.props as Record<string, unknown>).className as string, 'relative'),\n type: getButtonType({ type: props.type }),\n ...api.ariaProps,\n ...api.dataAttributes,\n ...props,\n children: (\n <>\n {(shortcut || action) && <ShortcutHint shortcut={shortcut} action={action} className=\"right-4\" />}\n {(children.props as Record<string, unknown>).children}\n </>\n )\n })\n }\n\n return (\n <button\n ref={mergedRef}\n type={getButtonType({ type: props.type }) as 'button' | 'submit' | 'reset'}\n className={cn(classes, 'relative')}\n disabled={disabled || loading}\n {...api.ariaProps}\n {...api.dataAttributes}\n {...props}\n >\n {(shortcut || action) && <ShortcutHint shortcut={shortcut} action={action} className=\"right-4\" />}\n {loading && (\n <svg\n className=\"animate-spin h-4 w-4\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"4\"\n />\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n />\n </svg>\n )}\n {children}\n </button>\n )\n },\n)\n"],
5
- "mappings": "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",
6
- "names": ["Button"]
7
- }