feat: add auth, real-time collaboration, sharing, font control, and UI fixes
Phase 1 - Bug fixes:
- Fix color labels not showing on active line in format preview
- Replace eye emoji with SVG icon showing clear preview/raw state
- Replace // button with comment icon + better tooltip
- Fix ThemePicker accent colors when using system theme
Phase 2 - Font:
- Load JetBrains Mono via Google Fonts with offline fallback
- Add font size control (A-/A+) with keyboard shortcuts
- Persist font size preference in localStorage
Phase 3 - Auth:
- Supabase-based email/password authentication
- Device session management with configurable password renewal TTL
- AuthModal, UserMenu, SecuritySettings components
Phase 4 - Cloud sync:
- Document metadata sync to Supabase PostgreSQL
- Legacy localStorage migration on first login
- IndexedDB persistence via y-indexeddb
Phase 5 - Real-time collaboration:
- Y.js CRDT integration with CodeMirror 6
- Hocuspocus WebSocket server with JWT auth
- Collaborative cursor awareness
- CollabIndicator component
Phase 6 - Sharing:
- Share links with view/edit permissions
- ShareDialog component with copy-to-clipboard
- Minimal client-side router for /s/{token} URLs
Infrastructure:
- Docker Compose with PostgreSQL, GoTrue, PostgREST, Hocuspocus
- Nginx reverse proxy for all backend services
- SQL migrations with RLS policies
- Production-ready Dockerfile with build args
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,13 @@ import { StatusBar } from './components/StatusBar.tsx'
|
||||
import { AlignToolbar } from './components/AlignToolbar.tsx'
|
||||
import type { Alignment } from './components/AlignToolbar.tsx'
|
||||
import { FormatToolbar } from './components/FormatToolbar.tsx'
|
||||
import { FontSizeControl } from './components/FontSizeControl.tsx'
|
||||
import { useFontSize } from './hooks/useFontSize.ts'
|
||||
import { MobileResultsTray } from './components/MobileResultsTray.tsx'
|
||||
import { UserMenu } from './components/UserMenu.tsx'
|
||||
import { SecuritySettings } from './components/SecuritySettings.tsx'
|
||||
import { useAuth } from './auth/AuthProvider.tsx'
|
||||
import { AuthModal } from './auth/AuthModal.tsx'
|
||||
import './styles/app.css'
|
||||
|
||||
function App() {
|
||||
@@ -31,8 +37,13 @@ function App() {
|
||||
const isOnline = useOnlineStatus()
|
||||
const installPrompt = useInstallPrompt()
|
||||
const themeCtx = useTheme()
|
||||
const fontSizeCtx = useFontSize()
|
||||
const auth = useAuth()
|
||||
const store = useDocumentStore()
|
||||
|
||||
const [showAuthModal, setShowAuthModal] = useState(false)
|
||||
const [showSecuritySettings, setShowSecuritySettings] = useState(false)
|
||||
|
||||
const [editorView, setEditorView] = useState<EditorView | null>(null)
|
||||
const resultsPanelRef = useRef<HTMLDivElement>(null)
|
||||
const [modifiedIds, setModifiedIds] = useState<Set<string>>(new Set())
|
||||
@@ -207,11 +218,32 @@ function App() {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Ctrl+= — increase font size
|
||||
if (mod && (e.key === '=' || e.key === '+')) {
|
||||
e.preventDefault()
|
||||
fontSizeCtx.setFontSize(fontSizeCtx.fontSize + 1)
|
||||
return
|
||||
}
|
||||
|
||||
// Ctrl+- — decrease font size
|
||||
if (mod && e.key === '-') {
|
||||
e.preventDefault()
|
||||
fontSizeCtx.setFontSize(fontSizeCtx.fontSize - 1)
|
||||
return
|
||||
}
|
||||
|
||||
// Ctrl+0 — reset font size
|
||||
if (mod && e.key === '0') {
|
||||
e.preventDefault()
|
||||
fontSizeCtx.resetFontSize()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKey)
|
||||
return () => document.removeEventListener('keydown', handleKey)
|
||||
}, [store.activeTabId, store.openTabIds, handleNewTab, handleTabClose, handleTabClick, sidebarState.visible, setSidebarVisible])
|
||||
}, [store.activeTabId, store.openTabIds, handleNewTab, handleTabClose, handleTabClick, sidebarState.visible, setSidebarVisible, fontSizeCtx])
|
||||
|
||||
// Compute flex styles from divider position
|
||||
const editorStyle: React.CSSProperties = dividerX !== null
|
||||
@@ -249,12 +281,23 @@ function App() {
|
||||
onEditorAlignChange={setEditorAlign}
|
||||
onResultsAlignChange={setResultsAlign}
|
||||
/>
|
||||
<FontSizeControl
|
||||
fontSize={fontSizeCtx.fontSize}
|
||||
onFontSizeChange={fontSizeCtx.setFontSize}
|
||||
min={fontSizeCtx.MIN_SIZE}
|
||||
max={fontSizeCtx.MAX_SIZE}
|
||||
/>
|
||||
<ThemePicker
|
||||
theme={themeCtx.theme}
|
||||
resolvedTheme={themeCtx.resolvedTheme}
|
||||
accentColor={themeCtx.accentColor}
|
||||
onThemeChange={themeCtx.setTheme}
|
||||
onAccentChange={themeCtx.setAccent}
|
||||
/>
|
||||
<UserMenu
|
||||
onOpenAuth={() => setShowAuthModal(true)}
|
||||
onOpenSecurity={() => setShowSecuritySettings(true)}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -340,6 +383,21 @@ function App() {
|
||||
onInstall={installPrompt.handleInstall}
|
||||
onDismiss={installPrompt.handleDismiss}
|
||||
/>
|
||||
|
||||
{showAuthModal && (
|
||||
<AuthModal onClose={() => setShowAuthModal(false)} />
|
||||
)}
|
||||
|
||||
{auth.needsPasswordRenewal && (
|
||||
<AuthModal
|
||||
onClose={() => auth.clearPasswordRenewal()}
|
||||
renewalMode
|
||||
/>
|
||||
)}
|
||||
|
||||
{showSecuritySettings && (
|
||||
<SecuritySettings onClose={() => setShowSecuritySettings(false)} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user