refactor: move studio to frontends workspace

- Move studio from root to frontends/studio/
- Add owner-tools frontend for live blog admin UI
- Add shared ui component library
- Set up npm workspaces for frontends
- Add enhanced code block extension for editor

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Josh 2026-01-12 01:59:56 +02:00
parent c662e41b97
commit bef5dd4437
108 changed files with 8650 additions and 441 deletions

View file

@ -0,0 +1,76 @@
import { useState } from 'react'
import { useStore } from '@nanostores/react'
import { $router } from '../../stores/router'
import { Icons, type IconComponent } from '../shared/Icons'
interface NavItem {
route: string
label: string
Icon: IconComponent
}
const navItems: NavItem[] = [
{ route: 'posts', label: 'Posts', Icon: Icons.Posts },
{ route: 'analytics', label: 'Analytics', Icon: Icons.Analytics },
{ route: 'general', label: 'General', Icon: Icons.Settings },
{ route: 'design', label: 'Design', Icon: Icons.Design },
{ route: 'domain', label: 'Domain', Icon: Icons.Domain },
{ route: 'engagement', label: 'Engagement', Icon: Icons.Engagement },
{ route: 'monetization', label: 'Monetization', Icon: Icons.Monetization },
{ route: 'api', label: 'API Keys', Icon: Icons.ApiKeys },
{ route: 'data', label: 'Data', Icon: Icons.Data },
{ route: 'billing', label: 'Billing', Icon: Icons.Billing },
]
interface HeaderProps {
className?: string
}
export function Header({ className = '' }: HeaderProps) {
const [menuOpen, setMenuOpen] = useState(false)
const page = useStore($router)
const currentRoute = page?.route ?? 'posts'
return (
<header className={`lg:hidden bg-surface border-b border-border sticky top-0 z-50 ${className}`}>
<div className="h-14 flex items-center justify-between px-4">
<a href="/" className="block">
<div className="text-[15px] font-bold tracking-tight text-text">WriteKit</div>
<div className="text-[11px] font-medium text-muted tracking-wide">Studio</div>
</a>
<button
onClick={() => setMenuOpen(!menuOpen)}
className="w-9 h-9 flex items-center justify-center hover:bg-border transition-colors"
>
{menuOpen ? <Icons.Close className="text-lg" /> : <Icons.Menu className="text-lg" />}
</button>
</div>
{menuOpen && (
<nav className="p-2 border-t border-border bg-surface">
{navItems.map(item => (
<a
key={item.route}
href={`/studio/${item.route}`}
onClick={() => setMenuOpen(false)}
className={currentRoute === item.route ? 'nav-item-active' : 'nav-item'}
>
<item.Icon className={`text-sm ${currentRoute === item.route ? 'text-accent opacity-100' : 'opacity-50'}`} />
<span>{item.label}</span>
</a>
))}
<div className="border-t border-border mt-2 pt-2">
<a
href="/"
target="_blank"
className="nav-item"
>
<Icons.ExternalLink className="text-sm opacity-50" />
<span>View Site</span>
</a>
</div>
</nav>
)}
</header>
)
}

View file

@ -0,0 +1,102 @@
import { useStore } from '@nanostores/react'
import { $router } from '../../stores/router'
import { Icons, type IconComponent } from '../shared/Icons'
interface NavItem {
route: string
label: string
Icon: IconComponent
}
interface NavSection {
title: string
items: NavItem[]
}
const navigation: NavSection[] = [
{
title: '',
items: [
{ route: 'home', label: 'Home', Icon: Icons.Home },
],
},
{
title: 'Content',
items: [
{ route: 'posts', label: 'Posts', Icon: Icons.Posts },
{ route: 'analytics', label: 'Analytics', Icon: Icons.Analytics },
],
},
{
title: 'Site',
items: [
{ route: 'general', label: 'General', Icon: Icons.Settings },
{ route: 'design', label: 'Design', Icon: Icons.Design },
{ route: 'domain', label: 'Domain', Icon: Icons.Domain },
],
},
{
title: 'Readers',
items: [
{ route: 'engagement', label: 'Engagement', Icon: Icons.Engagement },
{ route: 'monetization', label: 'Monetization', Icon: Icons.Monetization },
],
},
{
title: 'Developer',
items: [
{ route: 'plugins', label: 'Plugins', Icon: Icons.Code },
{ route: 'api', label: 'API Keys', Icon: Icons.ApiKeys },
{ route: 'data', label: 'Data', Icon: Icons.Data },
],
},
{
title: 'Account',
items: [
{ route: 'billing', label: 'Billing', Icon: Icons.Billing },
],
},
]
export function Sidebar() {
const page = useStore($router)
const currentRoute = page?.route ?? 'posts'
return (
<aside className="w-56 h-screen bg-bg border-r border-border flex flex-col">
<div className="px-4 py-6">
<a href="/" className="block group">
<div className="text-[15px] font-bold tracking-tight text-text">
WriteKit
</div>
<div className="text-[11px] font-medium text-muted tracking-wide">
Studio
</div>
</a>
</div>
<nav className="flex-1 overflow-y-auto px-3">
{navigation.map((section, idx) => (
<div key={section.title || idx} className="mb-1">
{section.title && <div className="nav-section">{section.title}</div>}
<div className="space-y-0.5">
{section.items.map(item => {
const href = item.route === 'home' ? '/studio' : `/studio/${item.route}`
return (
<a
key={item.route}
href={href}
className={currentRoute === item.route ? 'nav-item-active' : 'nav-item'}
>
<item.Icon className={`text-sm ${currentRoute === item.route ? 'text-accent opacity-100' : 'opacity-50'}`} />
<span>{item.label}</span>
</a>
)
})}
</div>
</div>
))}
</nav>
</aside>
)
}

View file

@ -0,0 +1,2 @@
export { Sidebar } from './Sidebar'
export { Header } from './Header'