Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 172 additions & 18 deletions packages/desktop/src/components/files/ProjectFilesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ import {
Home,
Image,
Loader2,
Paperclip,
Plus,
Search,
Trash2,
Type,
Upload,
X,
} from 'lucide-react'
Expand Down Expand Up @@ -151,6 +154,12 @@ export function ProjectFilesView() {
const [newFolderOpen, setNewFolderOpen] = useState(false)
const [newFolderName, setNewFolderName] = useState('')

// Add menu + text-content modal
const [addMenuOpen, setAddMenuOpen] = useState(false)
const [textModalOpen, setTextModalOpen] = useState(false)
const [textTitle, setTextTitle] = useState('')
const [textBody, setTextBody] = useState('')

// Preview pane state
const [selected, setSelected] = useState<FileEntry | null>(null)
const [preview, setPreview] = useState<string | null>(null)
Expand All @@ -160,6 +169,8 @@ export function ProjectFilesView() {

const fileInputRef = useRef<HTMLInputElement>(null)
const newFolderInputRef = useRef<HTMLInputElement>(null)
const addMenuRef = useRef<HTMLDivElement>(null)
const textTitleInputRef = useRef<HTMLInputElement>(null)

// Breadcrumbs
const breadcrumbs = useMemo(() => {
Expand Down Expand Up @@ -285,6 +296,23 @@ export function ProjectFilesView() {
return unsub
}, [refresh])

useEffect(() => {
if (!addMenuOpen) return
const onDown = (e: MouseEvent) => {
if (!addMenuRef.current) return
if (!addMenuRef.current.contains(e.target as Node)) setAddMenuOpen(false)
}
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setAddMenuOpen(false)
}
window.addEventListener('mousedown', onDown)
window.addEventListener('keydown', onKey)
return () => {
window.removeEventListener('mousedown', onDown)
window.removeEventListener('keydown', onKey)
}
}, [addMenuOpen])

useEffect(() => {
if (!loading) return
const timer = setTimeout(() => {
Expand Down Expand Up @@ -386,6 +414,23 @@ export function ProjectFilesView() {
setNewFolderName('')
}

const cancelTextModal = () => {
setTextModalOpen(false)
setTextTitle('')
setTextBody('')
}

const canSubmitText = textTitle.trim().length > 0 && textBody.trim().length > 0

const submitTextContent = () => {
if (!canSubmitText) return
const raw = textTitle.trim()
const hasExt = /\.[A-Za-z0-9]{1,8}$/.test(raw)
const filename = hasExt ? raw : `${raw}.md`
connection.sendFilesystemWrite(resolvePath(filename), textBody, 'utf-8')
cancelTextModal()
}

const handleDelete = () => {
if (!selected) return
setDeleteTarget({ name: selected.name, path: resolvePath(selected.name) })
Expand Down Expand Up @@ -478,10 +523,49 @@ export function ProjectFilesView() {
</div>

<div className="fl-toolbar__actions">
<button type="button" className="fl-btn" onClick={() => fileInputRef.current?.click()}>
<Upload size={12} strokeWidth={1.5} />
Upload
</button>
<div className="fl-addmenu" ref={addMenuRef}>
<button
type="button"
className="fl-btn"
onClick={() => setAddMenuOpen((v) => !v)}
aria-haspopup="menu"
aria-expanded={addMenuOpen}
>
<Plus size={13} strokeWidth={1.5} />
Upload
</button>
{addMenuOpen && (
<div className="fl-addmenu__panel" role="menu">
<button
type="button"
className="fl-addmenu__item"
role="menuitem"
onClick={() => {
setAddMenuOpen(false)
fileInputRef.current?.click()
}}
>
<Paperclip size={14} strokeWidth={1.5} />
Upload from device
</button>
<button
type="button"
className="fl-addmenu__item"
role="menuitem"
onClick={() => {
setAddMenuOpen(false)
setTextModalOpen(true)
setTextTitle('')
setTextBody('')
setTimeout(() => textTitleInputRef.current?.focus(), 30)
}}
>
<Type size={14} strokeWidth={1.5} />
Add text content
</button>
</div>
)}
</div>
<input
ref={fileInputRef}
type="file"
Expand All @@ -500,7 +584,7 @@ export function ProjectFilesView() {
</div>

{/* Split: list + preview */}
<div className="fl-split">
<div className={`fl-split${selected ? '' : ' fl-split--nopreview'}`}>
<div className="fl-list-wrap">
<div className="fl-colhead">
<button
Expand Down Expand Up @@ -655,16 +739,10 @@ export function ProjectFilesView() {
</div>
</div>

{/* Preview pane */}
<aside className="fl-preview">
{!selected ? (
<div className="fl-preview__empty">
<Eye size={20} strokeWidth={1.2} />
<div style={{ marginTop: 10 }}>Select a file to preview.</div>
</div>
) : (
<>
<div className="fl-preview__thumb">
{/* Preview pane — only render once a file is selected */}
{selected && (
<aside className="fl-preview">
<div className="fl-preview__thumb">
{previewLoading ? (
<Loader2 size={20} strokeWidth={1.5} className="spin" />
) : previewError ? (
Expand Down Expand Up @@ -744,9 +822,8 @@ export function ProjectFilesView() {
</button>
</div>
</div>
</>
)}
</aside>
</aside>
)}
</div>

{dragging && (
Expand Down Expand Up @@ -804,6 +881,83 @@ export function ProjectFilesView() {
</div>
</div>
)}

{textModalOpen && (
<div
className="modal-overlay"
onClick={cancelTextModal}
onKeyDown={(e) => {
if (e.key === 'Escape') cancelTextModal()
}}
>
<div
className="modal-card"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<div className="fl-textmodal__head">
<h3>Add text content</h3>
<button
type="button"
className="fl-iconbtn"
onClick={cancelTextModal}
aria-label="Close"
>
<X size={14} strokeWidth={1.5} />
</button>
</div>
<div className="modal-card__body">
<div className="form-field">
<label className="form-field__label" htmlFor="fl-text-title">
Title <span style={{ color: 'var(--danger)' }}>*</span>
</label>
<input
id="fl-text-title"
ref={textTitleInputRef}
type="text"
className="form-field__input"
placeholder="Name your content"
value={textTitle}
onChange={(e) => setTextTitle(e.target.value)}
onKeyDown={(e) => {
const mod = e.metaKey || e.ctrlKey
if (mod && e.key === 'Enter') submitTextContent()
}}
/>
</div>
<div className="form-field">
<label className="form-field__label" htmlFor="fl-text-body">
Content <span style={{ color: 'var(--danger)' }}>*</span>
</label>
<textarea
id="fl-text-body"
className="form-field__input fl-textmodal__textarea"
placeholder="Type or paste in content…"
value={textBody}
onChange={(e) => setTextBody(e.target.value)}
onKeyDown={(e) => {
const mod = e.metaKey || e.ctrlKey
if (mod && e.key === 'Enter') submitTextContent()
}}
/>
</div>
</div>
<div className="modal-card__footer">
<button type="button" className="button button--ghost" onClick={cancelTextModal}>
Cancel
</button>
<button
type="button"
className="button button--primary"
onClick={submitTextContent}
disabled={!canSubmitText}
>
Add Content
</button>
</div>
</div>
</div>
)}
</div>
)
}
47 changes: 9 additions & 38 deletions packages/desktop/src/components/projects/NewProjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import {
FolderOpen,
MapPin,
Search,
Sparkles,
Upload,
} from 'lucide-react'
import { useEffect, useState } from 'react'
import { projectStore } from '../../lib/store/projectStore.js'
Expand Down Expand Up @@ -97,8 +95,6 @@ export function NewProjectView() {
const [instructions, setInstructions] = useState('')
const [creating, setCreating] = useState(false)

const template = TEMPLATES.find((t) => t.id === templateId) ?? TEMPLATES[0]

const pickTemplate = (id: TemplateId) => {
const next = TEMPLATES.find((t) => t.id === id) ?? TEMPLATES[0]
setTemplateId(id)
Expand All @@ -110,11 +106,6 @@ export function NewProjectView() {
}
}

const suggestName = () => {
if (template.id === 'blank') return
setName(template.name)
}

const goBack = () => uiStore.getState().setActiveView('projects')

const canCreate = name.trim().length > 0 && !creating
Expand Down Expand Up @@ -213,21 +204,15 @@ export function NewProjectView() {

<div className="np-section">
<div className="np-section__label">Name</div>
<div className="np-name">
<input
type="text"
className="np-input"
placeholder="e.g. Essay, Telegram Bots, Home ops"
value={name}
onChange={(e) => setName(e.target.value)}
// biome-ignore lint/a11y/noAutofocus: new-project view expects the name field focused
autoFocus
/>
<button type="button" className="np-suggest" onClick={suggestName}>
<Sparkles size={11} strokeWidth={1.5} />
Suggest
</button>
</div>
<input
type="text"
className="np-input"
placeholder="e.g. Essay, Telegram Bots, Home ops"
value={name}
onChange={(e) => setName(e.target.value)}
// biome-ignore lint/a11y/noAutofocus: new-project view expects the name field focused
autoFocus
/>
</div>

<div className="np-section">
Expand All @@ -242,20 +227,6 @@ export function NewProjectView() {
onChange={(e) => setInstructions(e.target.value)}
/>
</div>

<div className="np-section">
<div className="np-section__label">
Files & context
<span className="np-section__hint">Anything Anton should always have on hand</span>
</div>
<div className="np-dropzone">
<Upload size={16} strokeWidth={1.5} />
<span>
Drop files here, or{' '}
<span className="np-dropzone__link">pick from your computer</span>
</span>
</div>
</div>
</div>

<aside className="np-preview">
Expand Down
Loading