Compare commits
No commits in common. "8892a9cf3aebaa66781237fb5810c9c46ef75967" and "84c6bf834cbf0b52e13e8b66b51b1755ac813acc" have entirely different histories.
8892a9cf3a
...
84c6bf834c
|
|
@ -1,200 +0,0 @@
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
|
||||||
import { useEditor, TLShapeId } from 'tldraw'
|
|
||||||
import { VisibilityChangeModal, shouldSkipVisibilityPrompt, setSkipVisibilityPrompt } from './VisibilityChangeModal'
|
|
||||||
import { updateItemVisibility, ItemVisibility } from '../shapes/GoogleItemShapeUtil'
|
|
||||||
import { findPrivateWorkspace, isShapeInPrivateWorkspace } from '../shapes/PrivateWorkspaceShapeUtil'
|
|
||||||
|
|
||||||
interface PendingChange {
|
|
||||||
shapeId: TLShapeId
|
|
||||||
currentVisibility: ItemVisibility
|
|
||||||
newVisibility: ItemVisibility
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function VisibilityChangeManager() {
|
|
||||||
const editor = useEditor()
|
|
||||||
const [pendingChange, setPendingChange] = useState<PendingChange | null>(null)
|
|
||||||
const [isDarkMode, setIsDarkMode] = useState(false)
|
|
||||||
|
|
||||||
// Detect dark mode
|
|
||||||
useEffect(() => {
|
|
||||||
const checkDarkMode = () => {
|
|
||||||
setIsDarkMode(document.documentElement.classList.contains('dark'))
|
|
||||||
}
|
|
||||||
checkDarkMode()
|
|
||||||
|
|
||||||
// Watch for class changes
|
|
||||||
const observer = new MutationObserver(checkDarkMode)
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['class']
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => observer.disconnect()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Handle visibility change requests from GoogleItem shapes
|
|
||||||
useEffect(() => {
|
|
||||||
const handleVisibilityChangeRequest = (event: CustomEvent<{
|
|
||||||
shapeId: TLShapeId
|
|
||||||
currentVisibility: ItemVisibility
|
|
||||||
newVisibility: ItemVisibility
|
|
||||||
title: string
|
|
||||||
}>) => {
|
|
||||||
const { shapeId, currentVisibility, newVisibility, title } = event.detail
|
|
||||||
|
|
||||||
// Check if user has opted to skip prompts
|
|
||||||
if (shouldSkipVisibilityPrompt()) {
|
|
||||||
// Apply change immediately
|
|
||||||
updateItemVisibility(editor, shapeId, newVisibility)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show confirmation modal
|
|
||||||
setPendingChange({
|
|
||||||
shapeId,
|
|
||||||
currentVisibility,
|
|
||||||
newVisibility,
|
|
||||||
title,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('request-visibility-change', handleVisibilityChangeRequest as EventListener)
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('request-visibility-change', handleVisibilityChangeRequest as EventListener)
|
|
||||||
}
|
|
||||||
}, [editor])
|
|
||||||
|
|
||||||
// Handle drag detection - check when items leave the Private Workspace
|
|
||||||
// Track GoogleItem positions to detect when they move outside workspace
|
|
||||||
useEffect(() => {
|
|
||||||
if (!editor) return
|
|
||||||
|
|
||||||
// Track which GoogleItems were inside workspace at start of drag
|
|
||||||
const wasInWorkspace = new Map<TLShapeId, boolean>()
|
|
||||||
let isDragging = false
|
|
||||||
|
|
||||||
// Record initial positions when pointer goes down
|
|
||||||
const handlePointerDown = () => {
|
|
||||||
const workspace = findPrivateWorkspace(editor)
|
|
||||||
if (!workspace) return
|
|
||||||
|
|
||||||
const selectedIds = editor.getSelectedShapeIds()
|
|
||||||
wasInWorkspace.clear()
|
|
||||||
|
|
||||||
for (const id of selectedIds) {
|
|
||||||
const shape = editor.getShape(id)
|
|
||||||
if (shape && shape.type === 'GoogleItem') {
|
|
||||||
const inWorkspace = isShapeInPrivateWorkspace(editor, id, workspace.id)
|
|
||||||
wasInWorkspace.set(id, inWorkspace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isDragging = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for visibility changes when pointer goes up
|
|
||||||
const handlePointerUp = () => {
|
|
||||||
if (!isDragging || wasInWorkspace.size === 0) {
|
|
||||||
isDragging = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspace = findPrivateWorkspace(editor)
|
|
||||||
if (!workspace) {
|
|
||||||
wasInWorkspace.clear()
|
|
||||||
isDragging = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check each tracked shape
|
|
||||||
wasInWorkspace.forEach((wasIn, id) => {
|
|
||||||
const shape = editor.getShape(id)
|
|
||||||
if (!shape || shape.type !== 'GoogleItem') return
|
|
||||||
|
|
||||||
const isNowIn = isShapeInPrivateWorkspace(editor, id, workspace.id)
|
|
||||||
|
|
||||||
// If shape was in workspace and is now outside, trigger visibility change
|
|
||||||
if (wasIn && !isNowIn) {
|
|
||||||
const itemShape = shape as any // GoogleItem shape
|
|
||||||
if (itemShape.props.visibility === 'local') {
|
|
||||||
// Trigger visibility change request
|
|
||||||
window.dispatchEvent(new CustomEvent('request-visibility-change', {
|
|
||||||
detail: {
|
|
||||||
shapeId: id,
|
|
||||||
currentVisibility: 'local',
|
|
||||||
newVisibility: 'shared',
|
|
||||||
title: itemShape.props.title || 'Untitled',
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
wasInWorkspace.clear()
|
|
||||||
isDragging = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use DOM events for pointer tracking (more reliable with tldraw)
|
|
||||||
const canvas = document.querySelector('.tl-canvas')
|
|
||||||
if (canvas) {
|
|
||||||
canvas.addEventListener('pointerdown', handlePointerDown)
|
|
||||||
canvas.addEventListener('pointerup', handlePointerUp)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (canvas) {
|
|
||||||
canvas.removeEventListener('pointerdown', handlePointerDown)
|
|
||||||
canvas.removeEventListener('pointerup', handlePointerUp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [editor])
|
|
||||||
|
|
||||||
// Handle modal confirmation
|
|
||||||
const handleConfirm = useCallback((dontAskAgain: boolean) => {
|
|
||||||
if (!pendingChange) return
|
|
||||||
|
|
||||||
// Update the shape visibility
|
|
||||||
updateItemVisibility(editor, pendingChange.shapeId, pendingChange.newVisibility)
|
|
||||||
|
|
||||||
// Save preference if requested
|
|
||||||
if (dontAskAgain) {
|
|
||||||
setSkipVisibilityPrompt(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
setPendingChange(null)
|
|
||||||
}, [editor, pendingChange])
|
|
||||||
|
|
||||||
// Handle modal cancellation
|
|
||||||
const handleCancel = useCallback(() => {
|
|
||||||
if (!pendingChange) return
|
|
||||||
|
|
||||||
// If this was triggered by drag, move the shape back inside the workspace
|
|
||||||
const workspace = findPrivateWorkspace(editor)
|
|
||||||
if (workspace) {
|
|
||||||
const shape = editor.getShape(pendingChange.shapeId)
|
|
||||||
if (shape) {
|
|
||||||
// Move shape back inside workspace bounds
|
|
||||||
editor.updateShape({
|
|
||||||
id: pendingChange.shapeId,
|
|
||||||
type: shape.type,
|
|
||||||
x: workspace.x + 20,
|
|
||||||
y: workspace.y + 60,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setPendingChange(null)
|
|
||||||
}, [editor, pendingChange])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VisibilityChangeModal
|
|
||||||
isOpen={pendingChange !== null}
|
|
||||||
itemTitle={pendingChange?.title || ''}
|
|
||||||
currentVisibility={pendingChange?.currentVisibility || 'local'}
|
|
||||||
newVisibility={pendingChange?.newVisibility || 'shared'}
|
|
||||||
onConfirm={handleConfirm}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
isDarkMode={isDarkMode}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,360 +0,0 @@
|
||||||
import { useState, useEffect, useRef } from 'react'
|
|
||||||
|
|
||||||
interface VisibilityChangeModalProps {
|
|
||||||
isOpen: boolean
|
|
||||||
itemTitle: string
|
|
||||||
currentVisibility: 'local' | 'shared'
|
|
||||||
newVisibility: 'local' | 'shared'
|
|
||||||
onConfirm: (dontAskAgain: boolean) => void
|
|
||||||
onCancel: () => void
|
|
||||||
isDarkMode: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function VisibilityChangeModal({
|
|
||||||
isOpen,
|
|
||||||
itemTitle,
|
|
||||||
currentVisibility,
|
|
||||||
newVisibility,
|
|
||||||
onConfirm,
|
|
||||||
onCancel,
|
|
||||||
isDarkMode,
|
|
||||||
}: VisibilityChangeModalProps) {
|
|
||||||
const [dontAskAgain, setDontAskAgain] = useState(false)
|
|
||||||
const modalRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
// Handle escape key
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) return
|
|
||||||
|
|
||||||
const handleEscape = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
onCancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('keydown', handleEscape)
|
|
||||||
return () => document.removeEventListener('keydown', handleEscape)
|
|
||||||
}, [isOpen, onCancel])
|
|
||||||
|
|
||||||
// Dark mode colors
|
|
||||||
const colors = isDarkMode ? {
|
|
||||||
bg: '#1f2937',
|
|
||||||
cardBg: '#252525',
|
|
||||||
cardBorder: '#404040',
|
|
||||||
text: '#e4e4e7',
|
|
||||||
textMuted: '#a1a1aa',
|
|
||||||
textHeading: '#f4f4f5',
|
|
||||||
warningBg: 'rgba(251, 191, 36, 0.15)',
|
|
||||||
warningBorder: 'rgba(251, 191, 36, 0.3)',
|
|
||||||
warningText: '#fbbf24',
|
|
||||||
btnPrimaryBg: '#6366f1',
|
|
||||||
btnPrimaryText: '#ffffff',
|
|
||||||
btnSecondaryBg: '#333333',
|
|
||||||
btnSecondaryText: '#e4e4e4',
|
|
||||||
checkboxBg: '#333333',
|
|
||||||
checkboxBorder: '#555555',
|
|
||||||
localColor: '#6366f1',
|
|
||||||
sharedColor: '#22c55e',
|
|
||||||
} : {
|
|
||||||
bg: '#ffffff',
|
|
||||||
cardBg: '#f9fafb',
|
|
||||||
cardBorder: '#e5e7eb',
|
|
||||||
text: '#374151',
|
|
||||||
textMuted: '#6b7280',
|
|
||||||
textHeading: '#1f2937',
|
|
||||||
warningBg: 'rgba(251, 191, 36, 0.1)',
|
|
||||||
warningBorder: 'rgba(251, 191, 36, 0.3)',
|
|
||||||
warningText: '#92400e',
|
|
||||||
btnPrimaryBg: '#6366f1',
|
|
||||||
btnPrimaryText: '#ffffff',
|
|
||||||
btnSecondaryBg: '#f3f4f6',
|
|
||||||
btnSecondaryText: '#374151',
|
|
||||||
checkboxBg: '#ffffff',
|
|
||||||
checkboxBorder: '#d1d5db',
|
|
||||||
localColor: '#6366f1',
|
|
||||||
sharedColor: '#22c55e',
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isOpen) return null
|
|
||||||
|
|
||||||
const isSharing = currentVisibility === 'local' && newVisibility === 'shared'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
|
||||||
backdropFilter: 'blur(4px)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
zIndex: 100010,
|
|
||||||
}}
|
|
||||||
onClick={onCancel}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={modalRef}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
style={{
|
|
||||||
backgroundColor: colors.bg,
|
|
||||||
borderRadius: '12px',
|
|
||||||
width: '90%',
|
|
||||||
maxWidth: '420px',
|
|
||||||
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
|
||||||
border: `1px solid ${colors.cardBorder}`,
|
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: '20px 24px 16px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '12px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ fontSize: '24px' }}>
|
|
||||||
{isSharing ? '⚠️' : '🔒'}
|
|
||||||
</span>
|
|
||||||
<h2
|
|
||||||
style={{
|
|
||||||
fontSize: '18px',
|
|
||||||
fontWeight: '600',
|
|
||||||
color: colors.textHeading,
|
|
||||||
margin: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isSharing ? 'Change Visibility?' : 'Make Private?'}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div style={{ padding: '0 24px 20px' }}>
|
|
||||||
<p style={{ fontSize: '14px', color: colors.text, margin: '0 0 16px 0', lineHeight: '1.5' }}>
|
|
||||||
{isSharing
|
|
||||||
? "You're about to make this item visible to others:"
|
|
||||||
: "You're about to make this item private:"}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Item preview */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
backgroundColor: colors.cardBg,
|
|
||||||
padding: '12px 14px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: `1px solid ${colors.cardBorder}`,
|
|
||||||
marginBottom: '16px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '10px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ fontSize: '18px' }}>📄</span>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: '14px',
|
|
||||||
fontWeight: '500',
|
|
||||||
color: colors.text,
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{itemTitle}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Current vs New state */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '12px',
|
|
||||||
fontSize: '13px',
|
|
||||||
marginBottom: '16px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '6px',
|
|
||||||
padding: '6px 10px',
|
|
||||||
backgroundColor: isSharing ? `${colors.localColor}20` : `${colors.sharedColor}20`,
|
|
||||||
borderRadius: '6px',
|
|
||||||
color: isSharing ? colors.localColor : colors.sharedColor,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{isSharing ? '🔒' : '🌐'}</span>
|
|
||||||
<span>{isSharing ? 'Private' : 'Shared'}</span>
|
|
||||||
</div>
|
|
||||||
<span style={{ color: colors.textMuted }}>→</span>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '6px',
|
|
||||||
padding: '6px 10px',
|
|
||||||
backgroundColor: isSharing ? `${colors.sharedColor}20` : `${colors.localColor}20`,
|
|
||||||
borderRadius: '6px',
|
|
||||||
color: isSharing ? colors.sharedColor : colors.localColor,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{isSharing ? '🌐' : '🔒'}</span>
|
|
||||||
<span>{isSharing ? 'Shared' : 'Private'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Warning for sharing */}
|
|
||||||
{isSharing && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
backgroundColor: colors.warningBg,
|
|
||||||
border: `1px solid ${colors.warningBorder}`,
|
|
||||||
borderRadius: '8px',
|
|
||||||
padding: '12px 14px',
|
|
||||||
marginBottom: '16px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
fontSize: '12px',
|
|
||||||
color: colors.warningText,
|
|
||||||
margin: 0,
|
|
||||||
lineHeight: '1.5',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<strong>Note:</strong> Shared items will be visible to all collaborators
|
|
||||||
on this board and may be uploaded to cloud storage.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Info for making private */}
|
|
||||||
{!isSharing && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
backgroundColor: isDarkMode ? 'rgba(99, 102, 241, 0.15)' : 'rgba(99, 102, 241, 0.1)',
|
|
||||||
border: `1px solid ${isDarkMode ? 'rgba(99, 102, 241, 0.3)' : 'rgba(99, 102, 241, 0.3)'}`,
|
|
||||||
borderRadius: '8px',
|
|
||||||
padding: '12px 14px',
|
|
||||||
marginBottom: '16px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
fontSize: '12px',
|
|
||||||
color: isDarkMode ? '#a5b4fc' : '#4f46e5',
|
|
||||||
margin: 0,
|
|
||||||
lineHeight: '1.5',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<strong>Note:</strong> Private items are only visible to you and remain
|
|
||||||
encrypted in your browser. Other collaborators won't be able to see this item.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Don't ask again checkbox */}
|
|
||||||
<label
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '10px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: '13px',
|
|
||||||
color: colors.textMuted,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={dontAskAgain}
|
|
||||||
onChange={(e) => setDontAskAgain(e.target.checked)}
|
|
||||||
style={{
|
|
||||||
width: '16px',
|
|
||||||
height: '16px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
Don't ask again for this session
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
gap: '10px',
|
|
||||||
padding: '16px 24px',
|
|
||||||
borderTop: `1px solid ${colors.cardBorder}`,
|
|
||||||
backgroundColor: colors.cardBg,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={onCancel}
|
|
||||||
style={{
|
|
||||||
padding: '10px 18px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: `1px solid ${colors.cardBorder}`,
|
|
||||||
backgroundColor: colors.btnSecondaryBg,
|
|
||||||
color: colors.btnSecondaryText,
|
|
||||||
fontSize: '14px',
|
|
||||||
fontWeight: '500',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => onConfirm(dontAskAgain)}
|
|
||||||
style={{
|
|
||||||
padding: '10px 18px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: 'none',
|
|
||||||
backgroundColor: isSharing ? colors.sharedColor : colors.localColor,
|
|
||||||
color: '#ffffff',
|
|
||||||
fontSize: '14px',
|
|
||||||
fontWeight: '500',
|
|
||||||
cursor: 'pointer',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '6px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{isSharing ? '🌐' : '🔒'}</span>
|
|
||||||
{isSharing ? 'Make Shared' : 'Make Private'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session storage key for "don't ask again" preference
|
|
||||||
const DONT_ASK_KEY = 'visibility-change-dont-ask'
|
|
||||||
|
|
||||||
export function shouldSkipVisibilityPrompt(): boolean {
|
|
||||||
try {
|
|
||||||
return sessionStorage.getItem(DONT_ASK_KEY) === 'true'
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setSkipVisibilityPrompt(skip: boolean): void {
|
|
||||||
try {
|
|
||||||
if (skip) {
|
|
||||||
sessionStorage.setItem(DONT_ASK_KEY, 'true')
|
|
||||||
} else {
|
|
||||||
sessionStorage.removeItem(DONT_ASK_KEY)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore storage errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -53,7 +53,6 @@ import { MycelialIntelligenceShape } from "@/shapes/MycelialIntelligenceShapeUti
|
||||||
import { PrivateWorkspaceShape } from "@/shapes/PrivateWorkspaceShapeUtil"
|
import { PrivateWorkspaceShape } from "@/shapes/PrivateWorkspaceShapeUtil"
|
||||||
import { PrivateWorkspaceTool } from "@/tools/PrivateWorkspaceTool"
|
import { PrivateWorkspaceTool } from "@/tools/PrivateWorkspaceTool"
|
||||||
import { PrivateWorkspaceManager } from "@/components/PrivateWorkspaceManager"
|
import { PrivateWorkspaceManager } from "@/components/PrivateWorkspaceManager"
|
||||||
import { VisibilityChangeManager } from "@/components/VisibilityChangeManager"
|
|
||||||
import { GoogleItemShape } from "@/shapes/GoogleItemShapeUtil"
|
import { GoogleItemShape } from "@/shapes/GoogleItemShapeUtil"
|
||||||
import { GoogleItemTool } from "@/tools/GoogleItemTool"
|
import { GoogleItemTool } from "@/tools/GoogleItemTool"
|
||||||
import {
|
import {
|
||||||
|
|
@ -1112,7 +1111,6 @@ export function Board() {
|
||||||
>
|
>
|
||||||
<CmdK />
|
<CmdK />
|
||||||
<PrivateWorkspaceManager />
|
<PrivateWorkspaceManager />
|
||||||
<VisibilityChangeManager />
|
|
||||||
</Tldraw>
|
</Tldraw>
|
||||||
</div>
|
</div>
|
||||||
</AutomergeHandleProvider>
|
</AutomergeHandleProvider>
|
||||||
|
|
|
||||||
|
|
@ -107,14 +107,13 @@ export class GoogleItemShape extends BaseBoxShapeUtil<IGoogleItemShape> {
|
||||||
return date.toLocaleDateString()
|
return date.toLocaleDateString()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleToggleVisibility = () => {
|
const handleMakeShared = () => {
|
||||||
const newVisibility = isLocal ? 'shared' : 'local'
|
|
||||||
// Dispatch event for Phase 5 permission flow
|
// Dispatch event for Phase 5 permission flow
|
||||||
window.dispatchEvent(new CustomEvent('request-visibility-change', {
|
window.dispatchEvent(new CustomEvent('request-visibility-change', {
|
||||||
detail: {
|
detail: {
|
||||||
shapeId: shape.id,
|
shapeId: shape.id,
|
||||||
currentVisibility: shape.props.visibility,
|
currentVisibility: shape.props.visibility,
|
||||||
newVisibility,
|
newVisibility: 'shared',
|
||||||
title: shape.props.title,
|
title: shape.props.title,
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
@ -183,11 +182,11 @@ export class GoogleItemShape extends BaseBoxShapeUtil<IGoogleItemShape> {
|
||||||
}}
|
}}
|
||||||
title={isLocal
|
title={isLocal
|
||||||
? 'Private - Only you can see (click to share)'
|
? 'Private - Only you can see (click to share)'
|
||||||
: 'Shared - Visible to collaborators (click to make private)'
|
: 'Shared - Visible to collaborators'
|
||||||
}
|
}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
handleToggleVisibility()
|
if (isLocal) handleMakeShared()
|
||||||
}}
|
}}
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import { CustomContextMenu } from "./CustomContextMenu"
|
||||||
import { FocusLockIndicator } from "./FocusLockIndicator"
|
import { FocusLockIndicator } from "./FocusLockIndicator"
|
||||||
import { MycelialIntelligenceBar } from "./MycelialIntelligenceBar"
|
import { MycelialIntelligenceBar } from "./MycelialIntelligenceBar"
|
||||||
import { CommandPalette } from "./CommandPalette"
|
import { CommandPalette } from "./CommandPalette"
|
||||||
import { UserSettingsModal } from "./UserSettingsModal"
|
|
||||||
import { GoogleExportBrowser } from "../components/GoogleExportBrowser"
|
|
||||||
import {
|
import {
|
||||||
DefaultKeyboardShortcutsDialog,
|
DefaultKeyboardShortcutsDialog,
|
||||||
DefaultKeyboardShortcutsDialogContent,
|
DefaultKeyboardShortcutsDialogContent,
|
||||||
|
|
@ -19,77 +17,15 @@ import {
|
||||||
} from "tldraw"
|
} from "tldraw"
|
||||||
import { SlidesPanel } from "@/slides/SlidesPanel"
|
import { SlidesPanel } from "@/slides/SlidesPanel"
|
||||||
|
|
||||||
// Custom People Menu component for showing connected users and integrations
|
// Custom People Menu component for showing connected users
|
||||||
function CustomPeopleMenu() {
|
function CustomPeopleMenu() {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
const [showDropdown, setShowDropdown] = React.useState(false)
|
const [showDropdown, setShowDropdown] = React.useState(false)
|
||||||
const [showGoogleBrowser, setShowGoogleBrowser] = React.useState(false)
|
|
||||||
const [googleConnected, setGoogleConnected] = React.useState(false)
|
|
||||||
const [googleLoading, setGoogleLoading] = React.useState(false)
|
|
||||||
|
|
||||||
// Detect dark mode
|
|
||||||
const isDarkMode = typeof document !== 'undefined' &&
|
|
||||||
document.documentElement.classList.contains('dark')
|
|
||||||
|
|
||||||
// Get current user info
|
// Get current user info
|
||||||
const myUserColor = useValue('myColor', () => editor.user.getColor(), [editor])
|
const myUserColor = useValue('myColor', () => editor.user.getColor(), [editor])
|
||||||
const myUserName = useValue('myName', () => editor.user.getName() || 'You', [editor])
|
const myUserName = useValue('myName', () => editor.user.getName() || 'You', [editor])
|
||||||
|
|
||||||
// Check Google connection on mount
|
|
||||||
React.useEffect(() => {
|
|
||||||
const checkGoogleStatus = async () => {
|
|
||||||
try {
|
|
||||||
const { GoogleDataService } = await import('../lib/google')
|
|
||||||
const service = GoogleDataService.getInstance()
|
|
||||||
const isAuthed = await service.isAuthenticated()
|
|
||||||
setGoogleConnected(isAuthed)
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to check Google status:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkGoogleStatus()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleGoogleConnect = async () => {
|
|
||||||
setGoogleLoading(true)
|
|
||||||
try {
|
|
||||||
const { GoogleDataService } = await import('../lib/google')
|
|
||||||
const service = GoogleDataService.getInstance()
|
|
||||||
await service.authenticate()
|
|
||||||
setGoogleConnected(true)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Google auth failed:', error)
|
|
||||||
} finally {
|
|
||||||
setGoogleLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOpenGoogleBrowser = () => {
|
|
||||||
setShowDropdown(false)
|
|
||||||
setShowGoogleBrowser(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAddToCanvas = async (items: any[], position: { x: number; y: number }) => {
|
|
||||||
try {
|
|
||||||
const { createGoogleItemProps } = await import('../shapes/GoogleItemShapeUtil')
|
|
||||||
|
|
||||||
// Create shapes for each selected item
|
|
||||||
items.forEach((item, index) => {
|
|
||||||
const props = createGoogleItemProps(item, 'local')
|
|
||||||
editor.createShape({
|
|
||||||
type: 'GoogleItem',
|
|
||||||
x: position.x + (index % 3) * 240,
|
|
||||||
y: position.y + Math.floor(index / 3) * 160,
|
|
||||||
props,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
setShowGoogleBrowser(false)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to add items to canvas:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all collaborators (other users in the session)
|
// Get all collaborators (other users in the session)
|
||||||
const collaborators = useValue('collaborators', () => editor.getCollaborators(), [editor])
|
const collaborators = useValue('collaborators', () => editor.getCollaborators(), [editor])
|
||||||
|
|
||||||
|
|
@ -263,128 +199,9 @@ function CustomPeopleMenu() {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Separator */}
|
|
||||||
<div style={{
|
|
||||||
height: '1px',
|
|
||||||
backgroundColor: 'var(--border-color, #e1e4e8)',
|
|
||||||
margin: '8px 0',
|
|
||||||
}} />
|
|
||||||
|
|
||||||
{/* Google Workspace Section */}
|
|
||||||
<div style={{
|
|
||||||
padding: '6px 12px',
|
|
||||||
fontSize: '11px',
|
|
||||||
fontWeight: 600,
|
|
||||||
color: 'var(--tool-text)',
|
|
||||||
opacity: 0.7,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.5px',
|
|
||||||
}}>
|
|
||||||
Integrations
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '10px',
|
|
||||||
padding: '8px 12px',
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
width: '24px',
|
|
||||||
height: '24px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
background: 'linear-gradient(135deg, #4285F4, #34A853, #FBBC04, #EA4335)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
fontSize: '12px',
|
|
||||||
}}>
|
|
||||||
G
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<div style={{
|
|
||||||
fontSize: '13px',
|
|
||||||
color: 'var(--text-color)',
|
|
||||||
fontWeight: 500,
|
|
||||||
}}>
|
|
||||||
Google Workspace
|
|
||||||
</div>
|
|
||||||
<div style={{
|
|
||||||
fontSize: '11px',
|
|
||||||
color: 'var(--tool-text)',
|
|
||||||
opacity: 0.7,
|
|
||||||
}}>
|
|
||||||
{googleConnected ? 'Connected' : 'Not connected'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{googleConnected ? (
|
|
||||||
<span style={{
|
|
||||||
width: '8px',
|
|
||||||
height: '8px',
|
|
||||||
borderRadius: '50%',
|
|
||||||
backgroundColor: '#22c55e',
|
|
||||||
}} />
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Google action buttons */}
|
|
||||||
<div style={{
|
|
||||||
padding: '4px 12px 8px',
|
|
||||||
display: 'flex',
|
|
||||||
gap: '8px',
|
|
||||||
}}>
|
|
||||||
{!googleConnected ? (
|
|
||||||
<button
|
|
||||||
onClick={handleGoogleConnect}
|
|
||||||
disabled={googleLoading}
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
padding: '6px 10px',
|
|
||||||
fontSize: '12px',
|
|
||||||
fontWeight: 500,
|
|
||||||
borderRadius: '6px',
|
|
||||||
border: '1px solid var(--border-color, #e1e4e8)',
|
|
||||||
backgroundColor: 'var(--bg-color, #fff)',
|
|
||||||
color: 'var(--text-color)',
|
|
||||||
cursor: googleLoading ? 'wait' : 'pointer',
|
|
||||||
opacity: googleLoading ? 0.7 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{googleLoading ? 'Connecting...' : 'Connect'}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={handleOpenGoogleBrowser}
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
padding: '6px 10px',
|
|
||||||
fontSize: '12px',
|
|
||||||
fontWeight: 500,
|
|
||||||
borderRadius: '6px',
|
|
||||||
border: 'none',
|
|
||||||
backgroundColor: '#4285F4',
|
|
||||||
color: 'white',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Browse Data
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Google Export Browser Modal */}
|
|
||||||
{showGoogleBrowser && (
|
|
||||||
<GoogleExportBrowser
|
|
||||||
isOpen={showGoogleBrowser}
|
|
||||||
onClose={() => setShowGoogleBrowser(false)}
|
|
||||||
onAddToCanvas={handleAddToCanvas}
|
|
||||||
isDarkMode={isDarkMode}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Click outside to close */}
|
{/* Click outside to close */}
|
||||||
{showDropdown && (
|
{showDropdown && (
|
||||||
<div
|
<div
|
||||||
|
|
@ -400,222 +217,10 @@ function CustomPeopleMenu() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom SharePanel that shows people menu and help button
|
// Custom SharePanel that shows the people menu
|
||||||
function CustomSharePanel() {
|
function CustomSharePanel() {
|
||||||
const tools = useTools()
|
|
||||||
const actions = useActions()
|
|
||||||
const [showShortcuts, setShowShortcuts] = React.useState(false)
|
|
||||||
|
|
||||||
// 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: tool.label || toolId,
|
|
||||||
kbd: tool.kbd,
|
|
||||||
category: 'Tools'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Custom tools
|
|
||||||
const customToolIds = ['VideoChat', 'ChatBox', 'Embed', 'Slide', 'Markdown', 'MycrozineTemplate', 'Prompt', 'ObsidianNote', 'Transcription', 'Holon', 'FathomMeetings', 'ImageGen', 'VideoGen', 'Multmux']
|
|
||||||
customToolIds.forEach(toolId => {
|
|
||||||
const tool = tools[toolId]
|
|
||||||
if (tool?.kbd) {
|
|
||||||
shortcuts.push({
|
|
||||||
name: 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: 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: 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])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tlui-share-zone" draggable={false} style={{ display: 'flex', alignItems: 'center', gap: '8px', position: 'relative' }}>
|
<div className="tlui-share-zone" draggable={false}>
|
||||||
{/* Help/Keyboard shortcuts button */}
|
|
||||||
<button
|
|
||||||
onClick={() => setShowShortcuts(!showShortcuts)}
|
|
||||||
style={{
|
|
||||||
background: showShortcuts ? 'var(--color-muted-2)' : 'none',
|
|
||||||
border: 'none',
|
|
||||||
padding: '6px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
borderRadius: '6px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
color: 'var(--color-text-1)',
|
|
||||||
opacity: showShortcuts ? 1 : 0.7,
|
|
||||||
transition: 'opacity 0.15s, background 0.15s',
|
|
||||||
pointerEvents: 'all',
|
|
||||||
zIndex: 10,
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.opacity = '1'
|
|
||||||
e.currentTarget.style.background = 'var(--color-muted-2)'
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
if (!showShortcuts) {
|
|
||||||
e.currentTarget.style.opacity = '0.7'
|
|
||||||
e.currentTarget.style.background = 'none'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title="Keyboard shortcuts (?)"
|
|
||||||
>
|
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
|
||||||
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
|
|
||||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Keyboard shortcuts panel */}
|
|
||||||
{showShortcuts && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
inset: 0,
|
|
||||||
zIndex: 99998,
|
|
||||||
}}
|
|
||||||
onClick={() => setShowShortcuts(false)}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 'calc(100% + 8px)',
|
|
||||||
right: 0,
|
|
||||||
width: '320px',
|
|
||||||
maxHeight: '70vh',
|
|
||||||
overflowY: 'auto',
|
|
||||||
background: 'var(--color-panel)',
|
|
||||||
border: '1px solid var(--color-panel-contrast)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
boxShadow: '0 4px 20px rgba(0,0,0,0.2)',
|
|
||||||
zIndex: 99999,
|
|
||||||
padding: '12px 0',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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)' }}>
|
|
||||||
{typeof shortcut.name === 'string' ? shortcut.name.replace('tool.', '').replace('action.', '') : shortcut.name}
|
|
||||||
</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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CustomPeopleMenu />
|
<CustomPeopleMenu />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue