Deep Dive: The Datapad Explorer
When you combine a standard static blog with a private, encrypted Notion-like workspace, the hardest part isn’t the cryptography—it’s the User Experience.
If you just dump all your private decrypted files onto the screen at once, you no longer have a “second brain”, you just have a messy drawer. To solve this, I designed the Datapad. It is the central reading interface that mounts once you pass the Ghost Terminal authentication.
I had three main goals for the Datapad:
- It needed to handle complex hierarchical relationships (grouping specific logs and resources under their parent Projects).
- It needed a Dynamic Table of Contents that wasn’t hardcoded.
- It needed to feel perfectly native on both a widescreen desktop and a mobile phone.
1. Dynamic Markdown AST Parsing (The Index)
Whenever you read a long technical document, a Table of Contents is critical. However, my markdown files are entirely raw strings when they are decrypted from the vault. I didn’t want to manually write an [Index] inside every single markdown file.
Instead, I built a system that actively scans the raw markdown string the moment it loads into volatile memory. By hooking into React’s useMemo and utilizing a Regex scanner, the application automatically finds every ## and ### heading:
const toc = useMemo(() => {
if (!activeLog || !activeLog.content) return [];
const headings = [];
const regex = /^(#{2,3})\s+(.+)$/gm; // capture H2 and H3
let match;
while ((match = regex.exec(activeLog.content)) !== null) {
headings.push({
level: match[1].length,
text: match[2].replace(/[*_~`]/g, ''),
id: slugify(match[2].replace(/[*_~`]/g, ''))
});
}
return headings;
}, [activeLog]);
But scanning it is only half the battle. How do I actually make those links clickable so they scroll to the right section?
I overrode the Abstract Syntax Tree (AST) renderer of ReactMarkdown. By supplying custom components for the h2 and h3 tags, I forced the renderer to dynamically inject a mathematically identical HTML id into the headers as they render!
h2: ({node, ...props}) => (
<h2
id={slugify(extractText(props.children))}
className="text-3xl font-bold text-glow-turquoise"
{...props}
/>
)
Now, the floating [ INDEX ] button in the top right corner is fully functional. It drops down, maps perfectly to the headers, and gracefully scrolls the user through the document.
2. The Mobile Explorer (Framer Motion)
The hierarchical left sidebar (the Explorer) looks incredibly sleek on a desktop. But on a mobile device, a 400px wide sidebar is a death sentence for readability.
To handle this, I created a responsive split-architecture. On Desktop, the Explorer is a sticky CSS block. But the moment the screen shrinks below the lg breakpoint, the Explorer completely disappears, granting the reading pane 100% of the screen.
To navigate on mobile, I implemented a Slide-Over Drawer using framer-motion:
<AnimatePresence>
{isMobileExplorerOpen && (
<>
{/* The Blurred Backdrop */}
<motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
onClick={() => setIsMobileExplorerOpen(false)}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] lg:hidden"
/>
{/* The Physics Drawer */}
<motion.div
initial={{ x: '-100%' }} animate={{ x: 0 }} exit={{ x: '-100%' }}
transition={{ type: "spring", bounce: 0, duration: 0.3 }}
className="fixed top-0 left-0 bottom-0 w-[80%] max-w-[320px] bg-base-bg/95 border-r border-secondary-accent/30 z-[110] lg:hidden flex flex-col"
>
{renderExplorerTree()}
</motion.div>
</>
)}
</AnimatePresence>
By leveraging Framer Motion’s spring physics, the sidebar slides over the content with a buttery smooth, native-app feel. The moment you click a new file inside the drawer, setIsMobileExplorerOpen(false) is triggered, and the drawer elegantly slides away, instantly returning the full screen to your new content.
3. Vault State Hardening
Working with volatile sessionStorage introduces unique edge cases. What happens if a user is reading a decrypted file, and they hit the browser “Back” button, but the RAM has been purged?
If you feed a raw React component a null string, the entire application will throw a fatal error, resulting in a terrifying white screen.
To prevent this, I hardened the Datapad architecture using early returns inside the useMemo hooks:
const renderedMarkdown = useMemo(() => {
// If the activeLog state is ever corrupted or desynced,
// gracefully drop the render instead of crashing.
if (!activeLog || typeof activeLog.content !== 'string') return null;
return <ReactMarkdown>{activeLog.content}</ReactMarkdown>;
}, [activeLog]);
Conclusion
By combining custom AST overrides with sleek physics-based animations, the Datapad feels completely disconnected from the standard “static blog” experience. It behaves entirely like a premium, native SaaS product while still operating securely inside a sandboxed browser environment!