feat: architectural grid editor fullPage + slash commands + doc info panel + AI title

This commit is contained in:
Antigravity
2026-05-07 22:29:02 +00:00
parent 0d8252aec0
commit e458b63115
126 changed files with 7652 additions and 1110 deletions

View File

@@ -0,0 +1,579 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState, useMemo } from 'react';
import {
Plus,
Search,
Share2,
Archive,
Settings,
Lock,
ChevronRight,
MoreVertical,
ArrowLeft
} from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
// --- Types ---
interface Note {
id: string;
carnetId: string;
title: string;
content: string;
imageUrl: string;
date: string;
}
interface Carnet {
id: string;
name: string;
initial: string;
type: 'Private' | 'Project' | 'Shared';
isPrivate?: boolean;
}
// --- Mock Data ---
const CARNETS: Carnet[] = [
{ id: '1', name: 'Daily Notes', initial: 'D', type: 'Private', isPrivate: true },
{ id: '2', name: 'Project: Neo', initial: 'P', type: 'Project' },
{ id: '3', name: 'Shared Docs', initial: 'S', type: 'Shared' },
{ id: '4', name: 'Architecture Research', initial: 'A', type: 'Project' },
];
const ALL_NOTES: Note[] = [
{
id: 'n1',
carnetId: '4',
title: 'Grid Systems',
date: 'Oct 26, 2024',
content: 'Grid Systems is streathen in ognitiacs clesign and simulhere desipmalt: complded structurer and manamateriai-s: ci arevenuatingly used, asiller straterty of insaee to the tmn and usaes of disrension, architecture of emiornabious tracious structures.',
imageUrl: 'https://images.unsplash.com/photo-1503387762-592dea58ef23?auto=format&fit=crop&q=80&w=800&h=600'
},
{
id: 'n2',
carnetId: '4',
title: 'Materiality',
date: 'Oct 24, 2024',
content: 'Materiality is combinated by relliaitic structureirs measure of plastics, natural, materials and priotical structures. Materialed coasts erabiocera alann light spaces and octicm employed design on thodolen of materiality, and tohlite tersev/ used in the gridin structures en obain materials, coms pathetic structure.',
imageUrl: 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&q=80&w=800&h=600'
},
{
id: 'n3',
carnetId: '4',
title: 'Light & Space',
date: 'Oct 22, 2024',
content: 'Light & Space is a creaivity of light & Space inralicated in sizazant or dark crotrcning and netrescenations of avant trurme sivonpaltures for in inncr-en allimativefiting is cerriadating and sityle.',
imageUrl: 'https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&q=80&w=800&h=600'
},
{
id: 'n4',
carnetId: '2',
title: 'Neo-Brutalism study',
date: 'Sep 12, 2024',
content: 'Exploring the raw aesthetic of neo-brutalism in urban environments. Focus on concrete textures and massive forms.',
imageUrl: 'https://images.unsplash.com/photo-1518005020951-eccb494ad742?auto=format&fit=crop&q=80&w=800&h=600'
}
];
// --- Components ---
interface NoteLinkProps {
note: Note;
isActive: boolean;
onClick: () => void;
}
const NoteLink: React.FC<NoteLinkProps> = ({ note, isActive, onClick }) => (
<motion.button
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
onClick={onClick}
className={`w-full flex items-center gap-2 pl-12 pr-4 py-2 text-[12px] transition-colors rounded-lg
${isActive ? 'bg-white/50 text-ink font-medium' : 'text-muted-ink hover:text-ink hover:bg-white/30'}`}
>
<div className={`w-1.5 h-1.5 rounded-full ${isActive ? 'bg-ink' : 'bg-transparent border border-muted-ink/30'}`} />
<span className="truncate">{note.title}</span>
</motion.button>
);
interface SidebarItemProps {
carnet: Carnet;
isActive: boolean;
notes: Note[];
activeNoteId: string | null;
onCarnetClick: () => void;
onNoteClick: (noteId: string) => void;
}
const SidebarItem: React.FC<SidebarItemProps> = ({
carnet,
isActive,
notes,
activeNoteId,
onCarnetClick,
onNoteClick
}) => {
return (
<div className="space-y-1">
<motion.button
whileHover={{ x: 4 }}
onClick={() => {
onCarnetClick();
}}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300 group
${isActive ? 'active-nav-item' : 'hover:bg-white/40'}`}
>
<motion.div
animate={{ rotate: isActive ? 90 : 0 }}
className="text-muted-ink"
>
<ChevronRight size={14} />
</motion.div>
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium border
${isActive ? 'bg-ink text-paper border-ink' : 'bg-white/60 text-ink border-border'}`}>
{carnet.initial}
</div>
<div className="flex-1 text-left">
<div className="flex items-center gap-2">
<span className={`text-[13px] font-medium transition-colors ${isActive ? 'text-ink' : 'text-muted-ink'}`}>
{carnet.name}
</span>
{carnet.isPrivate && <Lock size={10} className="text-muted-ink" />}
</div>
</div>
</motion.button>
<AnimatePresence>
{isActive && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.4, ease: [0.23, 1, 0.32, 1] }}
className="overflow-hidden space-y-0.5"
>
{notes.map(note => (
<NoteLink
key={note.id}
note={note}
isActive={activeNoteId === note.id}
onClick={() => onNoteClick(note.id)}
/>
))}
{notes.length === 0 && (
<p className="pl-12 text-[11px] text-muted-ink/50 py-2 italic font-light">No notes yet</p>
)}
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default function App() {
const [carnets, setCarnets] = useState<Carnet[]>(CARNETS);
const [notes, setNotes] = useState<Note[]>(ALL_NOTES);
const [activeCarnetId, setActiveCarnetId] = useState('4');
const [activeNoteId, setActiveNoteId] = useState<string | null>(null);
// Modal States
const [showNewCarnetModal, setShowNewCarnetModal] = useState(false);
const [showNewNoteModal, setShowNewNoteModal] = useState(false);
// Form States
const [newCarnetName, setNewCarnetName] = useState('');
const [newNoteTitle, setNewNoteTitle] = useState('');
const [newNoteContent, setNewNoteContent] = useState('');
const filteredNotes = useMemo(() =>
notes.filter(n => n.carnetId === activeCarnetId),
[activeCarnetId, notes]);
const activeNote = useMemo(() =>
notes.find(n => n.id === activeNoteId),
[activeNoteId, notes]);
const activeCarnet = useMemo(() =>
carnets.find(c => c.id === activeCarnetId),
[activeCarnetId, carnets]);
const handleAddCarnet = (e: React.FormEvent) => {
e.preventDefault();
if (!newCarnetName.trim()) return;
const newCarnet: Carnet = {
id: Date.now().toString(),
name: newCarnetName,
initial: newCarnetName.charAt(0).toUpperCase(),
type: 'Project'
};
setCarnets([...carnets, newCarnet]);
setNewCarnetName('');
setShowNewCarnetModal(false);
setActiveCarnetId(newCarnet.id);
};
const handleAddNote = (e: React.FormEvent) => {
e.preventDefault();
if (!newNoteTitle.trim() || !newNoteContent.trim()) return;
const newNote: Note = {
id: `n-${Date.now()}`,
carnetId: activeCarnetId,
title: newNoteTitle,
date: new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(new Date()),
content: newNoteContent,
imageUrl: 'https://images.unsplash.com/photo-1487958449943-2429e8be8625?auto=format&fit=crop&q=80&w=800&h=600'
};
setNotes([newNote, ...notes]);
setNewNoteTitle('');
setNewNoteContent('');
setShowNewNoteModal(false);
setActiveNoteId(newNote.id);
};
return (
<div className="min-h-screen flex items-center justify-center p-4 md:p-8 lg:p-12 overflow-hidden">
<div className="absolute inset-0 z-0 bg-[#DEDEDE]" />
<div className="relative w-full max-w-7xl h-[85vh] flex rounded-2xl overflow-hidden shadow-2xl sidebar-shadow bg-paper">
<div className="absolute -right-4 -bottom-4 w-full h-full bg-white/40 rounded-2xl -z-10 translate-x-2 translate-y-2 opacity-50" />
<div className="absolute -right-2 -bottom-2 w-full h-full bg-white/60 rounded-2xl -z-10 translate-x-1 translate-y-1 opacity-70" />
<aside className="w-80 bg-white/30 backdrop-blur-md border-right border-border p-6 flex flex-col z-20 shrink-0">
<div className="mb-10">
<div className="w-10 h-10 rounded-full bg-slate-200 border border-border flex items-center justify-center text-ink font-serif text-lg shadow-sm">
A
</div>
</div>
<div className="flex-1 overflow-y-auto space-y-6 -mx-2 px-2 custom-scrollbar">
<div>
<p className="text-[10px] font-bold text-muted-ink tracking-widest uppercase mb-4 px-4">
Architecture Grid
</p>
<div className="space-y-1">
{carnets.map(carnet => (
<SidebarItem
key={carnet.id}
carnet={carnet}
isActive={activeCarnetId === carnet.id}
notes={notes.filter(n => n.carnetId === carnet.id)}
activeNoteId={activeNoteId}
onCarnetClick={() => {
setActiveCarnetId(carnet.id);
setActiveNoteId(null);
}}
onNoteClick={(id) => {
setActiveCarnetId(carnet.id);
setActiveNoteId(id);
}}
/>
))}
</div>
<button
onClick={() => setShowNewCarnetModal(true)}
className="w-full mt-4 flex items-center gap-3 px-4 py-2 text-[13px] text-muted-ink hover:text-ink transition-colors font-medium rounded-lg hover:bg-white/40"
>
<Plus size={16} />
<span>New Carnet</span>
</button>
</div>
</div>
<div className="pt-6 border-t border-border space-y-4">
<button className="flex items-center gap-3 px-4 text-[13px] text-muted-ink hover:text-ink transition-colors font-medium">
<Archive size={16} />
<span>Archive</span>
</button>
<button className="flex items-center gap-3 px-4 text-[13px] text-muted-ink hover:text-ink transition-colors font-medium">
<Settings size={16} />
<span>Settings</span>
</button>
</div>
</aside>
<main className="flex-1 paper-texture relative overflow-hidden z-10 flex flex-col h-full">
<AnimatePresence mode="wait">
{!activeNoteId ? (
<motion.div
key="notebook"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="h-full flex flex-col overflow-y-auto"
>
<header className="px-12 pt-12 pb-8 flex flex-col gap-6 sticky top-0 bg-paper/80 backdrop-blur-md z-30">
<div className="flex justify-between items-start">
<h1 className="text-4xl font-serif font-medium tracking-tight text-ink leading-tight pr-12">
{activeCarnet?.name} {filteredNotes[0]?.date || 'Oct 26'}
</h1>
</div>
<div className="flex items-center justify-between border-b border-ink/5 pb-4">
<div className="flex items-center gap-6">
<button
onClick={() => setShowNewNoteModal(true)}
className="flex items-center gap-2 text-[13px] text-ink font-medium hover:opacity-70 transition-opacity"
>
<Plus size={16} />
<span>Add Note</span>
</button>
<button className="flex items-center gap-2 text-[13px] text-ink font-medium hover:opacity-70 transition-opacity">
<Search size={16} />
<span>Search</span>
</button>
</div>
<button className="flex items-center gap-2 text-[13px] text-ink font-medium hover:opacity-70 transition-opacity">
<Share2 size={16} />
<span>Share</span>
</button>
</div>
</header>
<div className="px-12 flex-1 pb-20">
<div className="max-w-3xl space-y-16">
{filteredNotes.map((note, index) => (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 * index, duration: 0.8 }}
key={note.id}
className="space-y-4 group cursor-pointer"
onClick={() => setActiveNoteId(note.id)}
>
<h2 className="text-2xl font-serif font-medium text-ink flex items-center justify-between">
{note.title}
<button className="opacity-0 group-hover:opacity-40 transition-opacity">
<ChevronRight size={20} />
</button>
</h2>
<div className="flex flex-col md:flex-row gap-8 items-start">
<div className="w-full md:w-56 aspect-[4/3] bg-white/50 border border-border overflow-hidden rounded shadow-sm flex-shrink-0">
<img
src={note.imageUrl}
alt={note.title}
className="w-full h-full object-cover mix-blend-multiply opacity-80 grayscale contrast-125 hover:grayscale-0 hover:opacity-100 transition-all duration-500"
referrerPolicy="no-referrer"
/>
</div>
<div className="space-y-3">
<p className="text-[14px] leading-relaxed text-ink/80 font-light max-w-lg line-clamp-4">
{note.content}
</p>
<span className="text-[11px] text-muted-ink uppercase tracking-widest font-medium">Read more</span>
</div>
</div>
</motion.div>
))}
{filteredNotes.length === 0 && (
<div className="h-64 flex flex-col items-center justify-center text-center space-y-4">
<p className="font-serif text-xl italic text-muted-ink">This notebook is waiting for its first vision.</p>
<button
onClick={() => setShowNewNoteModal(true)}
className="px-6 py-2 border border-ink text-[13px] uppercase tracking-[0.2em] hover:bg-ink hover:text-paper transition-all"
>
Begin Drawing
</button>
</div>
)}
</div>
</div>
<footer className="px-12 py-6 border-t border-ink/5 text-center mt-auto">
<p className="text-[11px] text-muted-ink uppercase tracking-[0.2em] font-medium">
&copy; 2024 Architectural Grid. All rights reserved.
</p>
</footer>
</motion.div>
) : (
<motion.div
key="focused-note"
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.02 }}
className="h-full flex flex-col overflow-y-auto bg-white"
>
<div className="flex-1 flex flex-col">
<div className="px-12 py-8 flex items-center justify-between sticky top-0 bg-white/90 backdrop-blur-sm z-40 border-b border-border">
<button
onClick={() => setActiveNoteId(null)}
className="flex items-center gap-2 text-ink hover:opacity-60 transition-opacity"
>
<ArrowLeft size={18} />
<span className="text-sm font-medium">Back to collection</span>
</button>
<div className="flex items-center gap-4">
<button className="p-2 text-muted-ink hover:text-ink transition-colors">
<Share2 size={18} />
</button>
<button className="p-2 text-muted-ink hover:text-ink transition-colors">
<MoreVertical size={18} />
</button>
</div>
</div>
<div className="max-w-4xl mx-auto w-full px-12 py-16 space-y-12">
<div className="space-y-4">
<div className="flex items-center gap-3 text-[12px] text-muted-ink uppercase tracking-[.25em] font-bold">
<span>{activeCarnet?.name}</span>
<ChevronRight size={10} />
<span>{activeNote?.date}</span>
</div>
<h1 className="text-5xl md:text-6xl font-serif font-bold text-ink leading-tight">
{activeNote?.title}
</h1>
</div>
<div className="aspect-[16/9] w-full bg-slate-100 rounded-xl overflow-hidden shadow-xl">
<img
src={activeNote?.imageUrl}
alt={activeNote?.title}
className="w-full h-full object-cover grayscale contrast-110"
referrerPolicy="no-referrer"
/>
</div>
<div className="max-w-2xl mx-auto space-y-8 pb-32">
<p className="text-xl md:text-2xl font-serif leading-relaxed text-ink italic">
{activeNote?.content.split('.')[0]}.
</p>
<div className="h-px bg-border w-32" />
<p className="text-lg leading-relaxed text-ink/80 font-light space-y-4 text-justify whitespace-pre-line">
{activeNote?.content}
{activeNote?.id.startsWith('n-') && (
<>
<br /><br />
Architectural grids serve as the invisible scaffolding upon which spatial experiences are constructed. Beyond mere structural repetition, they facilitate a rhythmic dialogue between materiality and void. In this exploration, we examine how light fractures these rigid boundaries, creating a dynamic interplay that evolves with the passage of time.
</>
)}
</p>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</main>
</div>
{/* Modals */}
<AnimatePresence>
{showNewCarnetModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowNewCarnetModal(false)}
className="absolute inset-0 bg-ink/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-md bg-paper border border-border shadow-2xl rounded-2xl p-8"
>
<h3 className="text-2xl font-serif font-medium text-ink mb-6">Create New Carnet</h3>
<form onSubmit={handleAddCarnet} className="space-y-6">
<div>
<label className="block text-[11px] uppercase tracking-widest font-bold text-muted-ink mb-2">Notebook Name</label>
<input
autoFocus
type="text"
value={newCarnetName}
onChange={(e) => setNewCarnetName(e.target.value)}
placeholder="E.g., Sustainable Patterns"
className="w-full bg-white border border-border rounded-lg px-4 py-3 outline-none focus:border-ink transition-colors font-serif italic text-lg"
/>
</div>
<div className="flex gap-4 pt-4">
<button
type="button"
onClick={() => setShowNewCarnetModal(false)}
className="flex-1 py-3 border border-border rounded-lg text-sm font-medium hover:bg-slate-50 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="flex-1 py-3 bg-ink text-paper rounded-lg text-sm font-medium hover:opacity-90 transition-opacity"
>
Create Notebook
</button>
</div>
</form>
</motion.div>
</div>
)}
{showNewNoteModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowNewNoteModal(false)}
className="absolute inset-0 bg-ink/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-2xl bg-paper border border-border shadow-2xl rounded-2xl p-10"
>
<h3 className="text-3xl font-serif font-medium text-ink mb-8">Add Architectural Note</h3>
<form onSubmit={handleAddNote} className="space-y-8">
<div>
<label className="block text-[11px] uppercase tracking-widest font-bold text-muted-ink mb-2">Concept Title</label>
<input
autoFocus
type="text"
value={newNoteTitle}
onChange={(e) => setNewNoteTitle(e.target.value)}
placeholder="Enter the title of your study..."
className="w-full bg-white border border-border rounded-lg px-5 py-4 outline-none focus:border-ink transition-colors font-serif text-2xl"
/>
</div>
<div>
<label className="block text-[11px] uppercase tracking-widest font-bold text-muted-ink mb-2">Observations & Analysis</label>
<textarea
value={newNoteContent}
onChange={(e) => setNewNoteContent(e.target.value)}
placeholder="Describe the spatial logic, materiality, and light interactions..."
rows={6}
className="w-full bg-white border border-border rounded-lg px-5 py-4 outline-none focus:border-ink transition-colors font-light leading-relaxed resize-none"
/>
</div>
<div className="flex gap-4 pt-4">
<button
type="button"
onClick={() => setShowNewNoteModal(false)}
className="flex-1 py-4 border border-border rounded-lg text-sm font-medium hover:bg-slate-50 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="flex-1 py-4 bg-ink text-paper rounded-lg text-sm font-medium hover:opacity-90 transition-opacity"
>
Save Note
</button>
</div>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
}

View File

@@ -0,0 +1,52 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=JetBrains+Mono:wght@400;500&display=swap');
@import "tailwindcss";
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-serif: "Playfair Display", serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
--color-paper: #F2F0E9;
--color-ink: #1C1C1C;
--color-muted-ink: rgba(28, 28, 28, 0.6);
--color-border: rgba(28, 28, 28, 0.1);
}
@layer base {
body {
@apply bg-[#E5E2D9] text-ink font-sans antialiased;
}
}
.paper-texture {
background-color: var(--color-paper);
background-image: url("https://www.transparenttextures.com/patterns/natural-paper.png");
}
/* Custom Scrollbar - Architectural Minimalist */
.custom-scrollbar::-webkit-scrollbar {
width: 3px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(28, 28, 28, 0.05);
border-radius: 10px;
}
.custom-scrollbar:hover::-webkit-scrollbar-thumb {
background: rgba(28, 28, 28, 0.15);
}
.sidebar-shadow {
box-shadow: 1px 0 10px rgba(0, 0, 0, 0.05);
}
.active-nav-item {
background: linear-gradient(to right, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.4));
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(255, 255, 255, 0.5);
}

View File

@@ -0,0 +1,10 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);