Compare commits
No commits in common. "0a95c319747cc89726e5aec6f4e45b483e36dd83" and "a46ce4437513e1d510dc0778d89bc1f84dbfe447" have entirely different histories.
0a95c31974
...
a46ce44375
|
|
@ -4,7 +4,7 @@ title: 'Flip permissions model: everyone edits by default, protected boards opt-
|
||||||
status: Done
|
status: Done
|
||||||
assignee: []
|
assignee: []
|
||||||
created_date: '2025-12-15 17:23'
|
created_date: '2025-12-15 17:23'
|
||||||
updated_date: '2025-12-15 19:26'
|
updated_date: '2025-12-15 18:32'
|
||||||
labels: []
|
labels: []
|
||||||
dependencies: []
|
dependencies: []
|
||||||
priority: high
|
priority: high
|
||||||
|
|
@ -31,7 +31,7 @@ Key changes:
|
||||||
- [x] #3 Global admin (jeffemmett@gmail.com) has admin on all boards
|
- [x] #3 Global admin (jeffemmett@gmail.com) has admin on all boards
|
||||||
- [x] #4 Settings dropdown shows view-only toggle for admins
|
- [x] #4 Settings dropdown shows view-only toggle for admins
|
||||||
- [x] #5 Can add/remove editors on protected boards
|
- [x] #5 Can add/remove editors on protected boards
|
||||||
- [x] #6 Admin request button sends email
|
- [ ] #6 Admin request button sends email
|
||||||
<!-- AC:END -->
|
<!-- AC:END -->
|
||||||
|
|
||||||
## Implementation Notes
|
## Implementation Notes
|
||||||
|
|
@ -68,12 +68,4 @@ INSERT OR IGNORE INTO global_admins (email) VALUES ('jeffemmett@gmail.com');
|
||||||
|
|
||||||
### Remaining
|
### Remaining
|
||||||
- [ ] AC #6: Admin request email flow (Resend integration needed)
|
- [ ] AC #6: Admin request email flow (Resend integration needed)
|
||||||
|
|
||||||
### Resend Email Integration (commit a46ce44)
|
|
||||||
- Added `RESEND_API_KEY` secret to Cloudflare Worker
|
|
||||||
- Fixed from email to use verified domain: `Canvas <noreply@jeffemmett.com>`
|
|
||||||
- Admin request emails will be sent to jeffemmett@gmail.com
|
|
||||||
- Test email sent successfully: ID 7113526b-ce1e-43e7-b18d-42b3d54823d1
|
|
||||||
|
|
||||||
**All acceptance criteria now complete!**
|
|
||||||
<!-- SECTION:NOTES:END -->
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useEffect, useRef, useCallback } from "react"
|
import React, { useState, useEffect, useRef, useCallback } from "react"
|
||||||
import { useEditor } from "tldraw"
|
import { useEditor } from "tldraw"
|
||||||
|
|
||||||
// Command Palette that shows when holding Ctrl+Shift or via global event
|
// Command Palette that shows when holding Ctrl+Shift
|
||||||
// Displays available keyboard shortcuts for custom tools and actions
|
// Displays available keyboard shortcuts for custom tools and actions
|
||||||
|
|
||||||
interface ShortcutItem {
|
interface ShortcutItem {
|
||||||
|
|
@ -13,15 +13,9 @@ interface ShortcutItem {
|
||||||
category: 'tool' | 'action'
|
category: 'tool' | 'action'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global function to open the command palette
|
|
||||||
export function openCommandPalette() {
|
|
||||||
window.dispatchEvent(new CustomEvent('open-command-palette'))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CommandPalette() {
|
export function CommandPalette() {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
const [isVisible, setIsVisible] = useState(false)
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
const [isManuallyOpened, setIsManuallyOpened] = useState(false)
|
|
||||||
const holdTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
const holdTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
const keysHeldRef = useRef({ ctrl: false, shift: false })
|
const keysHeldRef = useRef({ ctrl: false, shift: false })
|
||||||
|
|
||||||
|
|
@ -58,7 +52,6 @@ export function CommandPalette() {
|
||||||
// Handle clicking on a tool/action
|
// Handle clicking on a tool/action
|
||||||
const handleItemClick = useCallback((item: ShortcutItem) => {
|
const handleItemClick = useCallback((item: ShortcutItem) => {
|
||||||
setIsVisible(false)
|
setIsVisible(false)
|
||||||
setIsManuallyOpened(false)
|
|
||||||
|
|
||||||
if (item.category === 'tool') {
|
if (item.category === 'tool') {
|
||||||
// Set the current tool
|
// Set the current tool
|
||||||
|
|
@ -78,44 +71,6 @@ export function CommandPalette() {
|
||||||
}
|
}
|
||||||
}, [editor])
|
}, [editor])
|
||||||
|
|
||||||
// Handle manual open via custom event
|
|
||||||
useEffect(() => {
|
|
||||||
const handleOpenEvent = () => {
|
|
||||||
setIsManuallyOpened(true)
|
|
||||||
setIsVisible(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('open-command-palette', handleOpenEvent)
|
|
||||||
return () => window.removeEventListener('open-command-palette', handleOpenEvent)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Handle Escape key and click outside to close when manually opened
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isManuallyOpened) return
|
|
||||||
|
|
||||||
const handleEscape = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
setIsVisible(false)
|
|
||||||
setIsManuallyOpened(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
|
||||||
const target = e.target as HTMLElement
|
|
||||||
if (target.closest('.command-palette')) return
|
|
||||||
setIsVisible(false)
|
|
||||||
setIsManuallyOpened(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('keydown', handleEscape)
|
|
||||||
window.addEventListener('mousedown', handleClickOutside)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', handleEscape)
|
|
||||||
window.removeEventListener('mousedown', handleClickOutside)
|
|
||||||
}
|
|
||||||
}, [isManuallyOpened])
|
|
||||||
|
|
||||||
// Handle Ctrl+Shift key press/release
|
// Handle Ctrl+Shift key press/release
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAndShowPalette = () => {
|
const checkAndShowPalette = () => {
|
||||||
|
|
@ -138,8 +93,8 @@ export function CommandPalette() {
|
||||||
} else if (e.key === 'Shift') {
|
} else if (e.key === 'Shift') {
|
||||||
keysHeldRef.current.shift = true
|
keysHeldRef.current.shift = true
|
||||||
checkAndShowPalette()
|
checkAndShowPalette()
|
||||||
} else if (isVisible && !isManuallyOpened) {
|
} else if (isVisible) {
|
||||||
// Hide on any other key press (they're using a shortcut) - only if not manually opened
|
// Hide on any other key press (they're using a shortcut)
|
||||||
setIsVisible(false)
|
setIsVisible(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -151,8 +106,8 @@ export function CommandPalette() {
|
||||||
keysHeldRef.current.shift = false
|
keysHeldRef.current.shift = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide palette if either key is released - only if not manually opened
|
// Hide palette if either key is released
|
||||||
if (!isManuallyOpened && (!keysHeldRef.current.ctrl || !keysHeldRef.current.shift)) {
|
if (!keysHeldRef.current.ctrl || !keysHeldRef.current.shift) {
|
||||||
if (holdTimeoutRef.current) {
|
if (holdTimeoutRef.current) {
|
||||||
clearTimeout(holdTimeoutRef.current)
|
clearTimeout(holdTimeoutRef.current)
|
||||||
holdTimeoutRef.current = null
|
holdTimeoutRef.current = null
|
||||||
|
|
@ -171,7 +126,7 @@ export function CommandPalette() {
|
||||||
clearTimeout(holdTimeoutRef.current)
|
clearTimeout(holdTimeoutRef.current)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isVisible, isManuallyOpened])
|
}, [isVisible])
|
||||||
|
|
||||||
if (!isVisible) return null
|
if (!isVisible) return null
|
||||||
|
|
||||||
|
|
@ -191,7 +146,7 @@ export function CommandPalette() {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
zIndex: 999999,
|
zIndex: 999999,
|
||||||
pointerEvents: isManuallyOpened ? 'auto' : 'none',
|
pointerEvents: 'none',
|
||||||
animation: 'fadeIn 0.15s ease-out',
|
animation: 'fadeIn 0.15s ease-out',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { CustomToolbar } from "./CustomToolbar"
|
||||||
import { CustomContextMenu } from "./CustomContextMenu"
|
import { CustomContextMenu } from "./CustomContextMenu"
|
||||||
import { FocusLockIndicator } from "./FocusLockIndicator"
|
import { FocusLockIndicator } from "./FocusLockIndicator"
|
||||||
import { MycelialIntelligenceBar } from "./MycelialIntelligenceBar"
|
import { MycelialIntelligenceBar } from "./MycelialIntelligenceBar"
|
||||||
import { CommandPalette, openCommandPalette } from "./CommandPalette"
|
import { CommandPalette } from "./CommandPalette"
|
||||||
import { NetworkGraphPanel } from "../components/networking"
|
import { NetworkGraphPanel } from "../components/networking"
|
||||||
import CryptIDDropdown from "../components/auth/CryptIDDropdown"
|
import CryptIDDropdown from "../components/auth/CryptIDDropdown"
|
||||||
import StarBoardButton from "../components/StarBoardButton"
|
import StarBoardButton from "../components/StarBoardButton"
|
||||||
|
|
@ -47,11 +47,14 @@ const PERMISSION_CONFIG: Record<PermissionLevel, { label: string; color: string;
|
||||||
|
|
||||||
// Custom SharePanel with layout: CryptID -> Star -> Gear -> Question mark
|
// Custom SharePanel with layout: CryptID -> Star -> Gear -> Question mark
|
||||||
function CustomSharePanel() {
|
function CustomSharePanel() {
|
||||||
|
const tools = useTools()
|
||||||
|
const actions = useActions()
|
||||||
const { addDialog, removeDialog } = useDialogs()
|
const { addDialog, removeDialog } = useDialogs()
|
||||||
const { session } = useAuth()
|
const { session } = useAuth()
|
||||||
const { slug } = useParams<{ slug: string }>()
|
const { slug } = useParams<{ slug: string }>()
|
||||||
const boardId = slug || 'mycofi33'
|
const boardId = slug || 'mycofi33'
|
||||||
|
|
||||||
|
const [showShortcuts, setShowShortcuts] = React.useState(false)
|
||||||
const [showSettingsDropdown, setShowSettingsDropdown] = React.useState(false)
|
const [showSettingsDropdown, setShowSettingsDropdown] = React.useState(false)
|
||||||
// const [showVersionHistory, setShowVersionHistory] = React.useState(false) // TODO: Re-enable when version reversion is ready
|
// const [showVersionHistory, setShowVersionHistory] = React.useState(false) // TODO: Re-enable when version reversion is ready
|
||||||
const [showAISection, setShowAISection] = React.useState(false)
|
const [showAISection, setShowAISection] = React.useState(false)
|
||||||
|
|
@ -70,7 +73,9 @@ function CustomSharePanel() {
|
||||||
|
|
||||||
// Refs for dropdown positioning
|
// Refs for dropdown positioning
|
||||||
const settingsButtonRef = React.useRef<HTMLButtonElement>(null)
|
const settingsButtonRef = React.useRef<HTMLButtonElement>(null)
|
||||||
|
const shortcutsButtonRef = React.useRef<HTMLButtonElement>(null)
|
||||||
const [settingsDropdownPos, setSettingsDropdownPos] = React.useState<{ top: number; right: number } | null>(null)
|
const [settingsDropdownPos, setSettingsDropdownPos] = React.useState<{ top: number; right: number } | null>(null)
|
||||||
|
const [shortcutsDropdownPos, setShortcutsDropdownPos] = React.useState<{ top: number; right: number } | null>(null)
|
||||||
|
|
||||||
// Get current permission from session
|
// Get current permission from session
|
||||||
// Authenticated users default to 'edit', unauthenticated to 'view'
|
// Authenticated users default to 'edit', unauthenticated to 'view'
|
||||||
|
|
@ -131,6 +136,16 @@ function CustomSharePanel() {
|
||||||
}
|
}
|
||||||
}, [showSettingsDropdown])
|
}, [showSettingsDropdown])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (showShortcuts && shortcutsButtonRef.current) {
|
||||||
|
const rect = shortcutsButtonRef.current.getBoundingClientRect()
|
||||||
|
setShortcutsDropdownPos({
|
||||||
|
top: rect.bottom + 8,
|
||||||
|
right: window.innerWidth - rect.right,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [showShortcuts])
|
||||||
|
|
||||||
// ESC key handler for closing dropdowns
|
// ESC key handler for closing dropdowns
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
|
@ -138,14 +153,15 @@ function CustomSharePanel() {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (showSettingsDropdown) setShowSettingsDropdown(false)
|
if (showSettingsDropdown) setShowSettingsDropdown(false)
|
||||||
|
if (showShortcuts) setShowShortcuts(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (showSettingsDropdown) {
|
if (showSettingsDropdown || showShortcuts) {
|
||||||
// Use capture phase to intercept before tldraw
|
// Use capture phase to intercept before tldraw
|
||||||
document.addEventListener('keydown', handleKeyDown, true)
|
document.addEventListener('keydown', handleKeyDown, true)
|
||||||
}
|
}
|
||||||
return () => document.removeEventListener('keydown', handleKeyDown, true)
|
return () => document.removeEventListener('keydown', handleKeyDown, true)
|
||||||
}, [showSettingsDropdown])
|
}, [showSettingsDropdown, showShortcuts])
|
||||||
|
|
||||||
// Detect dark mode - use state to trigger re-render on change
|
// Detect dark mode - use state to trigger re-render on change
|
||||||
const [isDarkMode, setIsDarkMode] = React.useState(
|
const [isDarkMode, setIsDarkMode] = React.useState(
|
||||||
|
|
@ -343,6 +359,84 @@ function CustomSharePanel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to extract label string from tldraw label (can be string or {default, menu} object)
|
||||||
|
const getLabelString = (label: any, fallback: string): string => {
|
||||||
|
if (typeof label === 'string') return label
|
||||||
|
if (label && typeof label === 'object' && 'default' in label) return label.default
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all tools and actions with keyboard shortcuts
|
||||||
|
const allShortcuts = React.useMemo(() => {
|
||||||
|
const shortcuts: { name: string; kbd: string; category: string }[] = []
|
||||||
|
|
||||||
|
// Built-in tools
|
||||||
|
const builtInTools = ['select', 'hand', 'draw', 'eraser', 'arrow', 'text', 'note', 'frame', 'geo', 'line', 'highlight', 'laser']
|
||||||
|
builtInTools.forEach(toolId => {
|
||||||
|
const tool = tools[toolId]
|
||||||
|
if (tool?.kbd) {
|
||||||
|
shortcuts.push({
|
||||||
|
name: getLabelString(tool.label, toolId),
|
||||||
|
kbd: tool.kbd,
|
||||||
|
category: 'Tools'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Custom tools (VideoGen and Map temporarily hidden)
|
||||||
|
const customToolIds = ['VideoChat', 'ChatBox', 'Embed', 'Slide', 'Markdown', 'MycrozineTemplate', 'Prompt', 'ObsidianNote', 'Transcription', 'Holon', 'FathomMeetings', 'ImageGen', 'Multmux']
|
||||||
|
customToolIds.forEach(toolId => {
|
||||||
|
const tool = tools[toolId]
|
||||||
|
if (tool?.kbd) {
|
||||||
|
shortcuts.push({
|
||||||
|
name: getLabelString(tool.label, toolId),
|
||||||
|
kbd: tool.kbd,
|
||||||
|
category: 'Custom Tools'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Built-in actions
|
||||||
|
const builtInActionIds = ['undo', 'redo', 'cut', 'copy', 'paste', 'delete', 'select-all', 'duplicate', 'group', 'ungroup', 'bring-to-front', 'send-to-back', 'zoom-in', 'zoom-out', 'zoom-to-fit', 'zoom-to-100', 'toggle-grid']
|
||||||
|
builtInActionIds.forEach(actionId => {
|
||||||
|
const action = actions[actionId]
|
||||||
|
if (action?.kbd) {
|
||||||
|
shortcuts.push({
|
||||||
|
name: getLabelString(action.label, actionId),
|
||||||
|
kbd: action.kbd,
|
||||||
|
category: 'Actions'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Custom actions
|
||||||
|
const customActionIds = ['copy-link-to-current-view', 'copy-focus-link', 'unlock-camera-focus', 'revert-camera', 'lock-element', 'save-to-pdf', 'search-shapes', 'llm', 'open-obsidian-browser']
|
||||||
|
customActionIds.forEach(actionId => {
|
||||||
|
const action = actions[actionId]
|
||||||
|
if (action?.kbd) {
|
||||||
|
shortcuts.push({
|
||||||
|
name: getLabelString(action.label, actionId),
|
||||||
|
kbd: action.kbd,
|
||||||
|
category: 'Custom Actions'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return shortcuts
|
||||||
|
}, [tools, actions])
|
||||||
|
|
||||||
|
// Group shortcuts by category
|
||||||
|
const groupedShortcuts = React.useMemo(() => {
|
||||||
|
const groups: Record<string, typeof allShortcuts> = {}
|
||||||
|
allShortcuts.forEach(shortcut => {
|
||||||
|
if (!groups[shortcut.category]) {
|
||||||
|
groups[shortcut.category] = []
|
||||||
|
}
|
||||||
|
groups[shortcut.category].push(shortcut)
|
||||||
|
})
|
||||||
|
return groups
|
||||||
|
}, [allShortcuts])
|
||||||
|
|
||||||
// Separator component for unified menu
|
// Separator component for unified menu
|
||||||
const Separator = () => (
|
const Separator = () => (
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|
@ -1063,13 +1157,14 @@ function CustomSharePanel() {
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
{/* Help/Keyboard shortcuts button - rightmost - opens Command Palette */}
|
{/* Help/Keyboard shortcuts button - rightmost */}
|
||||||
<div style={{ padding: '0 4px' }}>
|
<div style={{ padding: '0 4px' }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => openCommandPalette()}
|
ref={shortcutsButtonRef}
|
||||||
|
onClick={() => setShowShortcuts(!showShortcuts)}
|
||||||
className="share-panel-btn"
|
className="share-panel-btn"
|
||||||
style={{
|
style={{
|
||||||
background: 'none',
|
background: showShortcuts ? 'var(--color-muted-2)' : 'none',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
padding: '6px',
|
padding: '6px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
|
@ -1078,7 +1173,7 @@ function CustomSharePanel() {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
color: 'var(--color-text-1)',
|
color: 'var(--color-text-1)',
|
||||||
opacity: 0.7,
|
opacity: showShortcuts ? 1 : 0.7,
|
||||||
transition: 'opacity 0.15s, background 0.15s',
|
transition: 'opacity 0.15s, background 0.15s',
|
||||||
pointerEvents: 'all',
|
pointerEvents: 'all',
|
||||||
}}
|
}}
|
||||||
|
|
@ -1087,8 +1182,10 @@ function CustomSharePanel() {
|
||||||
e.currentTarget.style.background = 'var(--color-muted-2)'
|
e.currentTarget.style.background = 'var(--color-muted-2)'
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.opacity = '0.7'
|
if (!showShortcuts) {
|
||||||
e.currentTarget.style.background = 'none'
|
e.currentTarget.style.opacity = '0.7'
|
||||||
|
e.currentTarget.style.background = 'none'
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
title="Keyboard shortcuts (?)"
|
title="Keyboard shortcuts (?)"
|
||||||
>
|
>
|
||||||
|
|
@ -1101,6 +1198,117 @@ function CustomSharePanel() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Keyboard shortcuts panel - rendered via portal to break out of parent container */}
|
||||||
|
{showShortcuts && shortcutsDropdownPos && createPortal(
|
||||||
|
<>
|
||||||
|
{/* Backdrop - only uses onClick, not onPointerDown */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
zIndex: 99998,
|
||||||
|
background: 'transparent',
|
||||||
|
}}
|
||||||
|
onClick={() => setShowShortcuts(false)}
|
||||||
|
/>
|
||||||
|
{/* Shortcuts menu */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: shortcutsDropdownPos.top,
|
||||||
|
right: shortcutsDropdownPos.right,
|
||||||
|
width: '320px',
|
||||||
|
maxHeight: '50vh',
|
||||||
|
overflowY: 'auto',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
background: 'var(--color-panel, #ffffff)',
|
||||||
|
backgroundColor: 'var(--color-panel, #ffffff)',
|
||||||
|
border: '1px solid var(--color-panel-contrast)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
boxShadow: '0 4px 20px rgba(0,0,0,0.25)',
|
||||||
|
zIndex: 99999,
|
||||||
|
padding: '10px 0',
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
backdropFilter: 'none',
|
||||||
|
opacity: 1,
|
||||||
|
}}
|
||||||
|
onWheel={(e) => e.stopPropagation()}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
padding: '8px 16px 12px',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--color-text)',
|
||||||
|
borderBottom: '1px solid var(--color-panel-contrast)',
|
||||||
|
marginBottom: '8px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<span>Keyboard Shortcuts</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowShortcuts(false)}
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '4px',
|
||||||
|
color: 'var(--color-text-3)',
|
||||||
|
fontSize: '16px',
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{Object.entries(groupedShortcuts).map(([category, shortcuts]) => (
|
||||||
|
<div key={category} style={{ marginBottom: '12px' }}>
|
||||||
|
<div style={{
|
||||||
|
padding: '4px 16px',
|
||||||
|
fontSize: '10px',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--color-text-3)',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.5px',
|
||||||
|
}}>
|
||||||
|
{category}
|
||||||
|
</div>
|
||||||
|
{shortcuts.map((shortcut, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '6px 16px',
|
||||||
|
fontSize: '13px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ color: 'var(--color-text)' }}>
|
||||||
|
{shortcut.name.replace('tool.', '').replace('action.', '')}
|
||||||
|
</span>
|
||||||
|
<kbd style={{
|
||||||
|
background: 'var(--color-muted-2)',
|
||||||
|
padding: '2px 6px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '11px',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
color: 'var(--color-text-1)',
|
||||||
|
border: '1px solid var(--color-panel-contrast)',
|
||||||
|
}}>
|
||||||
|
{shortcut.kbd.toUpperCase()}
|
||||||
|
</kbd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Version Reversion Panel - Coming Soon */}
|
{/* Version Reversion Panel - Coming Soon */}
|
||||||
{/* TODO: Re-enable when version history backend is fully tested
|
{/* TODO: Re-enable when version history backend is fully tested
|
||||||
{showVersionHistory && createPortal(
|
{showVersionHistory && createPortal(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue