first commit
This commit is contained in:
6
src/components/Navigation.tsx
Normal file
6
src/components/Navigation.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { navigationHistory, goBack } from '../store'
|
||||
|
||||
export function Navigation() {
|
||||
return null
|
||||
}
|
||||
169
src/components/NavigationConsole.tsx
Normal file
169
src/components/NavigationConsole.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { navigateToSpace } from '../store'
|
||||
import { db } from '../db'
|
||||
|
||||
interface NavigationConsoleProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function NavigationConsole({ isOpen, onClose }: NavigationConsoleProps) {
|
||||
const [input, setInput] = useState('')
|
||||
const [existingSpaces, setExistingSpaces] = useState<string[]>([])
|
||||
const [filteredSpaces, setFilteredSpaces] = useState<string[]>([])
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const loadSpaces = async () => {
|
||||
const spaces = await db.getAllSpaceIds()
|
||||
setExistingSpaces(spaces)
|
||||
}
|
||||
if (isOpen) {
|
||||
loadSpaces()
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (input.trim() === '') {
|
||||
setFilteredSpaces(existingSpaces.slice(0, 8))
|
||||
} else {
|
||||
const filtered = existingSpaces.filter(space =>
|
||||
space.toLowerCase().includes(input.toLowerCase())
|
||||
).slice(0, 8)
|
||||
|
||||
if (filtered.length === 0 || !filtered.includes(input)) {
|
||||
filtered.unshift(input)
|
||||
}
|
||||
|
||||
setFilteredSpaces(filtered)
|
||||
}
|
||||
setSelectedIndex(0)
|
||||
}, [input, existingSpaces])
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose()
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
const targetSpace = filteredSpaces[selectedIndex] || input
|
||||
if (targetSpace.trim()) {
|
||||
navigateToSpace(sanitizeSpaceId(targetSpace.trim()))
|
||||
onClose()
|
||||
}
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
setSelectedIndex(prev => (prev + 1) % filteredSpaces.length)
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
setSelectedIndex(prev => (prev - 1 + filteredSpaces.length) % filteredSpaces.length)
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizeSpaceId = (input: string): string => {
|
||||
if (input.startsWith('http://') || input.startsWith('https://')) {
|
||||
try {
|
||||
const url = new URL(input)
|
||||
const path = url.pathname.slice(1) || url.hostname
|
||||
return path.toLowerCase().replace(/[^a-z0-9]/g, '-')
|
||||
} catch {
|
||||
return input.toLowerCase().replace(/[^a-z0-9]/g, '-')
|
||||
}
|
||||
}
|
||||
return input.toLowerCase().replace(/[^a-z0-9]/g, '-')
|
||||
}
|
||||
|
||||
const handleItemClick = (space: string) => {
|
||||
navigateToSpace(sanitizeSpaceId(space))
|
||||
onClose()
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
zIndex: 2000
|
||||
}}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: '20%',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
background: '#1a1a1a',
|
||||
border: '1px solid #333',
|
||||
borderRadius: '4px',
|
||||
width: '400px',
|
||||
zIndex: 2001,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '8px' }}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Nom de l'espace ou URL..."
|
||||
style={{
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
color: 'white',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'monospace'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{filteredSpaces.length > 0 && (
|
||||
<div style={{ borderTop: '1px solid #333', maxHeight: '200px', overflowY: 'auto' }}>
|
||||
{filteredSpaces.map((space, index) => {
|
||||
const isExisting = existingSpaces.includes(space)
|
||||
const isSelected = index === selectedIndex
|
||||
const isUrl = space.startsWith('http://') || space.startsWith('https://')
|
||||
|
||||
return (
|
||||
<div
|
||||
key={space}
|
||||
onClick={() => handleItemClick(space)}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
cursor: 'pointer',
|
||||
background: isSelected ? '#333' : 'transparent',
|
||||
color: isExisting ? '#4CAF50' : (isUrl ? '#2196F3' : '#FFA726'),
|
||||
borderLeft: isSelected ? '3px solid white' : '3px solid transparent'
|
||||
}}
|
||||
>
|
||||
{space}
|
||||
{!isExisting && (
|
||||
<span style={{ color: '#666', marginLeft: '8px', fontSize: '12px' }}>
|
||||
(nouveau)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
228
src/components/Note.tsx
Normal file
228
src/components/Note.tsx
Normal file
@ -0,0 +1,228 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { updateNote, deleteNote, editingNoteId, navigateToSpace, selectedNoteIds, selectNote, moveSelectedNotesLocal, saveSelectedNotesToDB, existingSpaceIds } from '../store'
|
||||
import { useStore } from '@nanostores/react'
|
||||
|
||||
type NoteType = {
|
||||
id: string
|
||||
x: number
|
||||
y: number
|
||||
content: string
|
||||
}
|
||||
|
||||
interface NoteProps {
|
||||
note: NoteType
|
||||
}
|
||||
|
||||
export function Note({ note }: NoteProps) {
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [content, setContent] = useState(note.content)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
|
||||
const [hasDragged, setHasDragged] = useState(false)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const $editingNoteId = useStore(editingNoteId)
|
||||
const $selectedNoteIds = useStore(selectedNoteIds)
|
||||
const $existingSpaceIds = useStore(existingSpaceIds)
|
||||
|
||||
const isSelected = $selectedNoteIds.includes(note.id)
|
||||
|
||||
// Check if note content is a single word that matches an existing space
|
||||
const isSpaceLink = () => {
|
||||
if (!note.content) return false
|
||||
const words = note.content.trim().split(/\s+/)
|
||||
if (words.length !== 1) return false
|
||||
const spaceId = words[0].toLowerCase().replace(/[^a-z0-9]/g, '-')
|
||||
return $existingSpaceIds.includes(spaceId)
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if ($editingNoteId === note.id) {
|
||||
setIsEditing(true)
|
||||
editingNoteId.set(null)
|
||||
}
|
||||
}, [$editingNoteId, note.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && textareaRef.current) {
|
||||
textareaRef.current.focus()
|
||||
textareaRef.current.setSelectionRange(content.length, content.length)
|
||||
}
|
||||
}, [isEditing, content.length])
|
||||
|
||||
const handleSave = async () => {
|
||||
if (content.trim() === '') {
|
||||
await deleteNote(note.id)
|
||||
} else {
|
||||
await updateNote(note.id, { content: content.trim() })
|
||||
}
|
||||
setIsEditing(false)
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSave()
|
||||
} else if (e.key === 'Escape') {
|
||||
setContent(note.content)
|
||||
setIsEditing(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Selection
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (isEditing) return
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
// If we just finished dragging, don't change selection
|
||||
if (hasDragged) {
|
||||
setHasDragged(false)
|
||||
return
|
||||
}
|
||||
|
||||
// If note is already selected and no shift key, don't change selection
|
||||
// (this preserves multi-selection during drag start)
|
||||
if (isSelected && !e.shiftKey) {
|
||||
return
|
||||
}
|
||||
|
||||
selectNote(note.id, e.shiftKey)
|
||||
}
|
||||
|
||||
// Navigation
|
||||
const handleDoubleClick = (e: React.MouseEvent) => {
|
||||
if (isEditing) return
|
||||
e.stopPropagation()
|
||||
|
||||
if (note.content.trim()) {
|
||||
const words = note.content.trim().split(/\s+/)
|
||||
if (words.length > 0) {
|
||||
const spaceId = words[0].toLowerCase().replace(/[^a-z0-9]/g, '-')
|
||||
navigateToSpace(spaceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drag and drop
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (isEditing) return
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
// If the note isn't selected, add it to selection before starting drag
|
||||
if (!isSelected) {
|
||||
selectNote(note.id, e.shiftKey)
|
||||
}
|
||||
|
||||
setIsDragging(true)
|
||||
setDragStart({ x: e.clientX, y: e.clientY })
|
||||
setHasDragged(false)
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isDragging) return
|
||||
|
||||
const deltaX = e.clientX - dragStart.x
|
||||
const deltaY = e.clientY - dragStart.y
|
||||
|
||||
// Mark that we've dragged (prevents click event from changing selection)
|
||||
if (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2) {
|
||||
setHasDragged(true)
|
||||
}
|
||||
|
||||
// Move all selected notes (or just this note if none are selected)
|
||||
if ($selectedNoteIds.length > 0) {
|
||||
moveSelectedNotesLocal(deltaX, deltaY)
|
||||
} else {
|
||||
// If no notes are selected, just move this one
|
||||
updateNote(note.id, { x: note.x + deltaX, y: note.y + deltaY })
|
||||
}
|
||||
|
||||
setDragStart({ x: e.clientX, y: e.clientY })
|
||||
}
|
||||
|
||||
const handleMouseUp = async () => {
|
||||
if (isDragging && $selectedNoteIds.length > 0) {
|
||||
// Save multi-note moves to DB
|
||||
await saveSelectedNotesToDB()
|
||||
}
|
||||
setIsDragging(false)
|
||||
// Don't reset hasDragged here - let click handler reset it
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
}
|
||||
}, [isDragging, dragStart])
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onBlur={handleSave}
|
||||
onKeyDown={handleKeyDown}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: note.x,
|
||||
top: note.y,
|
||||
background: 'transparent',
|
||||
border: '1px solid #333',
|
||||
color: 'white',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '14px',
|
||||
padding: '4px 8px',
|
||||
resize: 'none',
|
||||
outline: 'none',
|
||||
minWidth: '100px',
|
||||
minHeight: '20px'
|
||||
}}
|
||||
rows={1}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: note.x,
|
||||
top: note.y,
|
||||
color: isSpaceLink() ? '#ff4444' : 'white',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '14px',
|
||||
padding: '4px 8px',
|
||||
cursor: isDragging ? 'grabbing' : (isSelected ? 'grab' : 'pointer'),
|
||||
userSelect: 'none',
|
||||
border: isSelected ? '2px solid white' : '1px solid transparent',
|
||||
zIndex: isDragging ? 1000 : (isSelected ? 10 : 1),
|
||||
background: isSelected ? 'rgba(255, 255, 255, 0.1)' : 'transparent'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isDragging && !isSelected) {
|
||||
e.currentTarget.style.border = '1px solid #333'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isDragging && !isSelected) {
|
||||
e.currentTarget.style.border = '1px solid transparent'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{note.content || '...'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
227
src/components/Space.tsx
Normal file
227
src/components/Space.tsx
Normal file
@ -0,0 +1,227 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { currentSpace, currentSpaceId, createNote, editingNoteId, clearSelection, deleteSelectedNotes, selectNotesInRect, refreshExistingSpaces } from '../store'
|
||||
import { Note } from './Note'
|
||||
import { NavigationConsole } from './NavigationConsole'
|
||||
import { WelcomeModal, shouldShowWelcome } from './WelcomeModal'
|
||||
|
||||
export function Space() {
|
||||
const $currentSpace = useStore(currentSpace)
|
||||
const $currentSpaceId = useStore(currentSpaceId)
|
||||
const $editingNoteId = useStore(editingNoteId)
|
||||
|
||||
const [isSelecting, setIsSelecting] = useState(false)
|
||||
const [selectionStart, setSelectionStart] = useState({ x: 0, y: 0 })
|
||||
const [selectionEnd, setSelectionEnd] = useState({ x: 0, y: 0 })
|
||||
const [justFinishedSelection, setJustFinishedSelection] = useState(false)
|
||||
const [showNavigationConsole, setShowNavigationConsole] = useState(false)
|
||||
const [showWelcomeModal, setShowWelcomeModal] = useState(false)
|
||||
|
||||
// Show welcome modal on first load and refresh existing spaces
|
||||
useEffect(() => {
|
||||
if (shouldShowWelcome()) {
|
||||
setShowWelcomeModal(true)
|
||||
}
|
||||
// Initialize existing spaces list
|
||||
refreshExistingSpaces()
|
||||
}, [])
|
||||
|
||||
// Click in empty space - clear selection (but not if we just finished a rectangle selection)
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (justFinishedSelection) {
|
||||
setJustFinishedSelection(false)
|
||||
return
|
||||
}
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
// Double-click in empty space - create note
|
||||
const handleDoubleClick = async (e: React.MouseEvent) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
|
||||
const noteId = await createNote(x, y)
|
||||
if (noteId) {
|
||||
editingNoteId.set(noteId)
|
||||
}
|
||||
}
|
||||
|
||||
// Drag in empty space - rectangle selection
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (e.detail === 2) return // Ignore double-click
|
||||
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
|
||||
setIsSelecting(true)
|
||||
setSelectionStart({ x, y })
|
||||
setSelectionEnd({ x, y })
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isSelecting) return
|
||||
|
||||
const spaceElement = document.querySelector('[data-space-canvas]') as HTMLElement
|
||||
if (!spaceElement) return
|
||||
|
||||
const rect = spaceElement.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
|
||||
setSelectionEnd({ x, y })
|
||||
}
|
||||
|
||||
const handleMouseUp = (e: MouseEvent) => {
|
||||
if (!isSelecting) return
|
||||
|
||||
setIsSelecting(false)
|
||||
|
||||
// Get final mouse position directly from event
|
||||
const spaceElement = document.querySelector('[data-space-canvas]') as HTMLElement
|
||||
if (!spaceElement) return
|
||||
|
||||
const rect = spaceElement.getBoundingClientRect()
|
||||
const finalX = e.clientX - rect.left
|
||||
const finalY = e.clientY - rect.top
|
||||
|
||||
// Calculate selection rectangle using actual coordinates
|
||||
const minX = Math.min(selectionStart.x, finalX)
|
||||
const maxX = Math.max(selectionStart.x, finalX)
|
||||
const minY = Math.min(selectionStart.y, finalY)
|
||||
const maxY = Math.max(selectionStart.y, finalY)
|
||||
|
||||
const width = Math.abs(maxX - minX)
|
||||
const height = Math.abs(maxY - minY)
|
||||
|
||||
// Only select if rectangle is big enough
|
||||
if (width > 5 || height > 5) {
|
||||
selectNotesInRect(minX, minY, maxX, maxY)
|
||||
setJustFinishedSelection(true)
|
||||
} else {
|
||||
clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelecting) {
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
}
|
||||
}, [isSelecting, selectionStart])
|
||||
|
||||
// Keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Don't handle shortcuts if editing a note, navigation console or welcome modal is open
|
||||
if ($editingNoteId || showNavigationConsole || showWelcomeModal) return
|
||||
|
||||
if (e.key === ':' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
setShowNavigationConsole(true)
|
||||
} else if (e.key === '?' || (e.key === ':' && e.shiftKey)) {
|
||||
e.preventDefault()
|
||||
setShowWelcomeModal(true)
|
||||
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
deleteSelectedNotes()
|
||||
} else if (e.key === 'Escape') {
|
||||
clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
}, [$editingNoteId, showNavigationConsole, showWelcomeModal])
|
||||
|
||||
if (!$currentSpace) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
background: '#000',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const selectionRect = {
|
||||
left: Math.min(selectionStart.x, selectionEnd.x),
|
||||
top: Math.min(selectionStart.y, selectionEnd.y),
|
||||
width: Math.abs(selectionEnd.x - selectionStart.x),
|
||||
height: Math.abs(selectionEnd.y - selectionStart.y)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-space-canvas
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
background: '#000',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: isSelecting ? 'crosshair' : 'default'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '10px',
|
||||
left: '10px',
|
||||
color: '#666',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '12px',
|
||||
userSelect: 'none',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
>
|
||||
Space: {$currentSpaceId}
|
||||
</div>
|
||||
|
||||
{$currentSpace.notes.map((note) => (
|
||||
<Note key={note.id} note={note} />
|
||||
))}
|
||||
|
||||
{isSelecting && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: selectionRect.left,
|
||||
top: selectionRect.top,
|
||||
width: selectionRect.width,
|
||||
height: selectionRect.height,
|
||||
border: '1px dashed #555',
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<NavigationConsole
|
||||
isOpen={showNavigationConsole}
|
||||
onClose={() => setShowNavigationConsole(false)}
|
||||
/>
|
||||
|
||||
<WelcomeModal
|
||||
isOpen={showWelcomeModal}
|
||||
onClose={() => setShowWelcomeModal(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
117
src/components/WelcomeModal.tsx
Normal file
117
src/components/WelcomeModal.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
interface WelcomeModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function WelcomeModal({ isOpen, onClose }: WelcomeModalProps) {
|
||||
const [neverShowAgain, setNeverShowAgain] = useState(false)
|
||||
|
||||
const handleClose = () => {
|
||||
if (neverShowAgain) {
|
||||
localStorage.setItem('palace-welcome-dismissed', 'true')
|
||||
}
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleOverlayClick = (e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
zIndex: 3000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
onClick={handleOverlayClick}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: '#1a1a1a',
|
||||
border: '1px solid #333',
|
||||
width: '500px',
|
||||
maxWidth: '90vw',
|
||||
fontFamily: 'monospace',
|
||||
color: 'white'
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h2 style={{ margin: '0 0 20px 0', fontSize: '18px', color: '#fff' }}>
|
||||
Bienvenue dans Palace
|
||||
</h2>
|
||||
|
||||
<div style={{ marginBottom: '20px', lineHeight: '1.5', color: '#ccc' }}>
|
||||
<p style={{ margin: '0 0 12px 0' }}>
|
||||
<strong>Navigation :</strong> Tapez <code style={{ background: '#333', padding: '2px 4px', borderRadius: '2px' }}>:</code> pour ouvrir la console de navigation
|
||||
</p>
|
||||
<p style={{ margin: '0 0 12px 0' }}>
|
||||
<strong>Création de notes :</strong> Double-cliquez sur l'espace vide pour créer une note
|
||||
</p>
|
||||
<p style={{ margin: '0 0 12px 0' }}>
|
||||
<strong>Sélection multiple :</strong> Shift+clic ou glisser pour sélectionner plusieurs notes
|
||||
</p>
|
||||
<p style={{ margin: '0 0 12px 0' }}>
|
||||
<strong>Navigation entre espaces :</strong> Double-cliquez sur une note pour naviguer vers l'espace correspondant
|
||||
</p>
|
||||
<p style={{ margin: '0' }}>
|
||||
<strong>Raccourcis :</strong> Suppr/Backspace pour effacer, Échap pour désélectionner
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', color: '#ccc' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={neverShowAgain}
|
||||
onChange={(e) => setNeverShowAgain(e.target.checked)}
|
||||
style={{ marginRight: '8px' }}
|
||||
/>
|
||||
Ne plus afficher ce message
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px' }}>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
style={{
|
||||
background: '#333',
|
||||
color: 'white',
|
||||
border: '1px solid #555',
|
||||
padding: '8px 16px',
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#444'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = '#333'
|
||||
}}
|
||||
>
|
||||
Compris !
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function shouldShowWelcome(): boolean {
|
||||
return localStorage.getItem('palace-welcome-dismissed') !== 'true'
|
||||
}
|
||||
Reference in New Issue
Block a user