@morphika/andami 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +151 -36
  2. package/app/admin/layout.tsx +145 -152
  3. package/components/blocks/AudioBlockRenderer.tsx +286 -0
  4. package/components/blocks/BeforeAfterBlockRenderer.tsx +274 -0
  5. package/components/builder/BlockCardIcons.tsx +89 -0
  6. package/components/builder/BlockTypePicker.tsx +2 -0
  7. package/components/builder/CoverSectionCanvas.tsx +90 -2
  8. package/components/builder/SectionV2Canvas.tsx +19 -3
  9. package/components/builder/SectionV2Column.tsx +5 -1
  10. package/components/builder/asset-browser/R2BrowserContent.tsx +23 -6
  11. package/components/builder/asset-browser/helpers.ts +4 -0
  12. package/components/builder/asset-browser/types.ts +2 -1
  13. package/components/builder/blockStyles.tsx +12 -0
  14. package/components/builder/editors/AudioBlockEditor.tsx +242 -0
  15. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -0
  16. package/components/builder/editors/shared.tsx +1 -1
  17. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -0
  18. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +176 -0
  19. package/lib/animation/enter-types.ts +2 -0
  20. package/lib/animation/hover-effect-types.ts +2 -0
  21. package/lib/builder/block-registrations.ts +83 -1
  22. package/lib/builder/types.ts +2 -0
  23. package/lib/sanity/types.ts +58 -0
  24. package/lib/version.ts +1 -1
  25. package/package.json +1 -1
  26. package/sanity/schemas/blocks/audioBlock.ts +69 -0
  27. package/sanity/schemas/blocks/beforeAfterBlock.ts +121 -0
  28. package/sanity/schemas/blocks/index.ts +3 -1
  29. package/sanity/schemas/index.ts +7 -1
package/README.md CHANGED
@@ -1,26 +1,23 @@
1
1
  # @morphika/andami
2
2
 
3
- A reusable Visual Page Builder framework for Next.js. Build custom websites with a drag-and-drop visual editor, Sanity CMS backend, and zero-config deployment.
3
+ **A visual page builder framework for Next.js the engine behind [morphika.tv](https://morphika.tv).**
4
4
 
5
- **Designed for Vercel Hobby tier** the framework is engineered to run comfortably within Vercel's free/Hobby plan limits (4h Fluid Active CPU, 10 GB Fast Origin Transfer per month). Aggressive ISR caching, Sanity CDN routing, bot mitigation, and Edge-first middleware keep serverless CPU usage minimal even under crawler traffic.
5
+ Andami is a self-hosted, code-first alternative to Webflow / Framer / Semplice. You scaffold a new site in one command, write your branding in a single config file, and get a Sanity-backed CMS plus a drag-and-drop visual editor all running on Vercel's free Hobby tier.
6
6
 
7
- ## Features
7
+ > **Status — actively maintained, not actively promoted.**
8
+ > Built and used in production by [Morphika Studio](https://morphika.tv). Released as MIT for transparency and as a reference implementation. Issues and PRs are welcome but not guaranteed a response — this is a personal/studio tool first, an open-source project second.
8
9
 
9
- - **Visual Page Builder** — Infinite canvas editor with device previews (desktop, tablet, phone)
10
- - **6 Content Blocks** — Text, Image, Image Grid, Video, Spacer, Button (added via "+ Add Block" inside columns)
11
- - **2 Section-level Blocks** — Project Grid (masonry) and Project Carousel (horizontal "keep browsing" at end of project pages). Added via "+ Add Section"
12
- - **Cover Sections** — Full-viewport hero sections with proportional rows, background media, and drag-to-resize
13
- - **V2 Grid System** — 12-column CSS grid with push cascade engine and responsive overrides
14
- - **Custom Sections** — Create reusable sections with per-instance setting overrides
15
- - **Navigation Builder** — Visual 12-column grid editor with drag & drop
16
- - **Animation System** — Enter animations, hover effects (CSS + WebGL shaders), page transitions
17
- - **Asset Management** — Cloudflare R2 storage with browser, thumbnails, and CDN serving
18
- - **Full Site Backups** — Client-side ZIP export/restore. Browser talks directly to R2 (public CDN + presigned PUT URLs); Vercel only sees lightweight JSON, so backups never hit the 4.5 MB / 10s serverless limits
19
- - **Setup Wizard** — Guided onboarding for new sites
20
- - **Sanity v3 CMS** — Structured content with custom schemas, GROQ queries, and ISR
21
- - **Vercel-Optimized** — Sanity CDN for public queries, 24h ISR, R2 direct CDN shortcut, bot guard middleware, aggressive robots.txt with crawl-delay — all tuned to minimize serverless CPU on Hobby tier
10
+ ---
22
11
 
23
- ## Quick Start
12
+ ## Demo
13
+
14
+ 🔗 **Live site:** [morphika.tv](https://morphika.tv)
15
+ 🔗 **Admin preview:** _(add screenshot/GIF here — `docs/media/admin-preview.png`)_
16
+ 🔗 **Builder canvas:** _(add screenshot here — `docs/media/builder-canvas.png`)_
17
+
18
+ ---
19
+
20
+ ## Quick start
24
21
 
25
22
  ```bash
26
23
  npx create-andami-site my-site
@@ -29,32 +26,150 @@ npm install
29
26
  npm run dev
30
27
  ```
31
28
 
32
- Visit `http://localhost:3000/admin` to start building.
29
+ Open `http://localhost:3000/admin` and follow the setup wizard. You'll need:
30
+
31
+ - A free [Sanity](https://www.sanity.io/) account (CMS backend)
32
+ - A [Cloudflare R2](https://www.cloudflare.com/developer-platform/products/r2/) bucket (asset storage — also free up to 10 GB)
33
+ - A Vercel account for deployment (optional — works anywhere Next.js runs)
34
+
35
+ Full guide → [docs/QUICK-START.md](docs/QUICK-START.md)
36
+
37
+ ---
38
+
39
+ ## Who this is for
40
+
41
+ ✅ **Designers / studios** who want a polished portfolio site they fully own — no monthly subscription, no platform lock-in.
42
+ ✅ **Developers** building bespoke sites for clients in the design / architecture / fashion / photography space.
43
+ ✅ **Anyone** who has outgrown Webflow's pricing or wants a Next.js codebase they can extend.
44
+
45
+ ## Who this is *not* for
46
+
47
+ ❌ Non-technical users expecting a SaaS experience. You need to deploy it yourself.
48
+ ❌ E-commerce, blogs at scale, or content-heavy sites. Andami is optimized for portfolio / brand / case-study sites.
49
+ ❌ Teams looking for guaranteed support or SLAs. This is single-maintainer software.
50
+
51
+ ---
52
+
53
+ ## What's inside
54
+
55
+ ### Visual page builder
56
+ - Infinite canvas with zoom, pan, minimap and device frames (desktop / tablet / phone)
57
+ - Cross-section drag-and-drop between V2 sections, Cover sections, and Parallax groups
58
+ - 12-column responsive grid with push-cascade overlap resolution
59
+ - Per-viewport overrides (desktop / tablet / mobile) for every block
60
+
61
+ ### Content blocks (10)
62
+ - Text (Tiptap rich text with inline color, links, BubbleMenu)
63
+ - Image, Image Grid, Video, Audio, Spacer, Button, Before/After
64
+ - Project Grid (masonry with multi-aspect-ratio cards)
65
+ - Project Carousel (horizontal "keep browsing" at end of project pages)
66
+
67
+ ### Section types
68
+ - **V2 Section** — flat columns, content-driven height
69
+ - **Cover Section** — full-viewport, proportional rows, background media, drag-to-resize
70
+ - **Parallax Group** — multi-slide parallax with crossfade / reveal transitions
71
+ - **Custom Section** — reusable sections with per-instance overrides
72
+
73
+ ### Animation system
74
+ - 7 enter-animation presets + per-character typewriter
75
+ - 4-level cascade (block → column → section → page)
76
+ - Hover effects: 7 CSS presets + 3 WebGL shaders (ripple, RGB shift, pixelate)
77
+ - Page exit animations that replay enter animations in reverse
78
+
79
+ ### CMS & assets
80
+ - Sanity v3 schemas (5 doc types, 10+ object types)
81
+ - Cloudflare R2 storage with presigned uploads, asset registry, thumbnail CLI
82
+ - Full-site backup & restore (client-side ZIP, bypasses serverless body limits)
83
+ - Custom font upload with magic-byte validation
84
+
85
+ ### Vercel-Hobby-tier optimized
86
+ - Sanity CDN routing for public queries (~2ms vs ~200ms)
87
+ - 24h ISR with on-demand revalidation
88
+ - Edge-middleware bot guard (blocks 20+ aggressive crawlers before they hit serverless)
89
+ - Aggressive `robots.txt` with crawl-delay and AI-scraper blocks
90
+ - React `cache()` deduplication on SSR fetches
91
+
92
+ → Designed to run a low-traffic portfolio site comfortably within 4h Fluid Active CPU/month.
93
+
94
+ ---
33
95
 
34
96
  ## Stack
35
97
 
36
- Next.js 16 (App Router) · Sanity v3 · Tailwind CSS v4 · Zustand · @dnd-kit · TypeScript
98
+ | Layer | Choice |
99
+ |---|---|
100
+ | Framework | Next.js 16 (App Router) |
101
+ | CMS backend | Sanity v3 |
102
+ | State | Zustand 5 |
103
+ | Drag & drop | @dnd-kit |
104
+ | Styling | Tailwind CSS v4 |
105
+ | Rich text | Tiptap 2 |
106
+ | Shaders | OGL (WebGL) |
107
+ | Storage | Cloudflare R2 (S3-compatible) |
108
+ | Tests | Vitest + React Testing Library (980+ assertions) |
109
+
110
+ ### Why this stack
111
+
112
+ - **Next.js + Sanity** — most boring, well-documented stack for content-driven sites. ISR + GROQ scales to any portfolio.
113
+ - **Zustand instead of Redux** — the page-builder state is complex (drag, undo/redo, multi-viewport overrides, snapshots) but a single feature; Zustand's slice pattern keeps it readable.
114
+ - **Sanity for data, not for editing** — Sanity is excellent as a structured-content backend but its built-in studio doesn't fit a visual layout tool. Andami uses Sanity purely as a typed JSON store.
115
+ - **Cloudflare R2 over S3** — zero egress fees. For a portfolio site that's the difference between $0/mo and "depends on the month".
116
+
117
+ ---
118
+
119
+ ## Architecture
120
+
121
+ Andami ships as an npm package (`@morphika/andami`). Each deployed site is an **instance** — a thin Next.js app that imports framework code and provides its own `site.config.ts`:
122
+
123
+ ```
124
+ my-site/ # Instance (your repo)
125
+ ├── site.config.ts # Branding, fonts, palette, features
126
+ ├── app/
127
+ │ ├── (site)/page.tsx # → re-exports @morphika/andami/site/page
128
+ │ ├── admin/ # → re-exports admin pages
129
+ │ └── api/ # → re-exports API routes
130
+ └── lib/config-init.ts # Registers config with the framework
131
+
132
+ @morphika/andami/ # Framework (this repo)
133
+ ├── components/builder/ # Visual editor
134
+ ├── components/blocks/ # Public-site renderers
135
+ ├── lib/sanity/ # Schemas, queries, types
136
+ └── app/api/ # All API routes (auth, pages, assets, R2, backups)
137
+ ```
138
+
139
+ This split lets you upgrade the framework with `npm update` without touching the instance, and lets one studio run many sites from a single shared codebase.
140
+
141
+ → Full breakdown in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
142
+
143
+ ---
37
144
 
38
145
  ## Documentation
39
146
 
40
- | Doc | Description |
41
- |-----|-------------|
42
- | [Quick Start](docs/QUICK-START.md) | 5-minute quickstart |
43
- | [Configuration](docs/CONFIGURATION.md) | `site.config.ts` reference |
44
- | [Architecture](docs/ARCHITECTURE.md) | Technical architecture and data flow |
45
- | [Blocks](docs/BLOCKS.md) | All block types with fields and options |
46
- | [Builder](docs/BUILDER.md) | Page builder UI, interactions, store |
47
- | [Canvas](docs/CANVAS.md) | Infinite canvas, zoom/pan, device frames |
147
+ | Doc | What's in it |
148
+ |---|---|
149
+ | [QUICK-START](docs/QUICK-START.md) | 5-minute setup for a new instance |
150
+ | [CONFIGURATION](docs/CONFIGURATION.md) | Every field of `site.config.ts` |
151
+ | [ARCHITECTURE](docs/ARCHITECTURE.md) | Data flow, module boundaries, design decisions |
152
+ | [BUILDER](docs/BUILDER.md) | Visual editor interactions, store, undo/redo |
153
+ | [CANVAS](docs/CANVAS.md) | Infinite canvas, zoom, pan, device frames |
154
+ | [BLOCKS](docs/BLOCKS.md) | All 10 block types with fields and options |
48
155
  | [CMS](docs/CMS.md) | Sanity schemas and GROQ queries |
49
- | [Navigation](docs/NAVIGATION.md) | Navigation builder, grid editor, styling |
50
- | [Assets](docs/ASSETS.md) | Storage, providers, thumbnails |
51
- | [Backups](docs/BACKUPS.md) | Full site backup & restore (V2 client-side architecture) |
52
- | [Thumbnails](docs/THUMBNAILS.md) | Thumbnail system and CLI tool |
53
- | [Customization](docs/CUSTOMIZATION.md) | Custom blocks, fonts, styles |
54
- | [Security](docs/SECURITY.md) | Auth, CSRF, input validation, encryption |
55
- | [Deployment](docs/DEPLOYMENT.md) | Vercel, domains, R2 setup |
56
- | [Package Dev](docs/PACKAGE-DEV.md) | Dev workflow, npm link, publishing |
156
+ | [NAVIGATION](docs/NAVIGATION.md) | Navigation builder spec |
157
+ | [ASSETS](docs/ASSETS.md) | Storage, providers, thumbnails |
158
+ | [BACKUPS](docs/BACKUPS.md) | Full-site backup & restore (V2 client-side) |
159
+ | [CUSTOMIZATION](docs/CUSTOMIZATION.md) | Custom blocks, fonts, schemas |
160
+ | [SECURITY](docs/SECURITY.md) | Auth, CSRF, encryption, sanitization |
161
+ | [DEPLOYMENT](docs/DEPLOYMENT.md) | Vercel, domains, R2 setup |
162
+ | [PACKAGE-DEV](docs/PACKAGE-DEV.md) | Local dev, npm link, publishing |
163
+
164
+ ---
57
165
 
58
166
  ## License
59
167
 
60
- MIT
168
+ [MIT](LICENSE) — use it, fork it, ship clients with it. Attribution appreciated but not required.
169
+
170
+ ---
171
+
172
+ ## Credits
173
+
174
+ Built by [Daniel Planas](https://morphika.tv) at Morphika Studio.
175
+ The name *Andami* (Catalan for "scaffold") refers to the temporary structures used to build something more permanent.
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { usePathname, useRouter } from "next/navigation";
4
4
  import Link from "next/link";
5
- import { useState, useEffect, useRef } from "react";
6
5
  import { getSiteConfig } from "../../lib/config";
7
6
  import { ANDAMI_VERSION } from "../../lib/version";
8
7
 
@@ -24,16 +23,79 @@ const systemLinks = [
24
23
  { href: "/admin/backups", label: "Backups", icon: "cloud-download" },
25
24
  ];
26
25
 
26
+ // Shared tile shape — logo square, active item, hover bg all share this
27
+ // so every piece lines up pixel-perfect.
28
+ const TILE_SIZE = "w-10 h-10";
29
+ const TILE_RADIUS = "rounded-lg";
30
+
31
+ // ============================================
32
+ // Tooltip — CSS-only (group-hover), no JS state, 120ms fade + slide-in
33
+ // ============================================
34
+ // Rendered to the right of its parent tile. The parent must have
35
+ // `group relative` classes (already baked into `tileBase`). Uses
36
+ // `pointer-events-none` so it never blocks clicks or creates its own
37
+ // hover state.
38
+
39
+ function Tooltip({ children }: { children: React.ReactNode }) {
40
+ return (
41
+ <span
42
+ role="tooltip"
43
+ className="pointer-events-none absolute left-full top-1/2 ml-1.5 z-50 whitespace-nowrap rounded-md border border-white/10 bg-[#2a2d33] px-2.5 py-1.5 text-[11px] font-medium text-white shadow-lg opacity-0 transition-[opacity,margin] duration-150 ease-out group-hover:opacity-100 group-hover:ml-3"
44
+ style={{ transform: "translateY(-50%)" }}
45
+ >
46
+ {children}
47
+ </span>
48
+ );
49
+ }
50
+
51
+ // ============================================
52
+ // Andami Mark — inlined from docs/andami_mark.svg
53
+ // ============================================
54
+
55
+ function AndamiMark({ size = 30 }: { size?: number }) {
56
+ // viewBox cropped to the actual content bbox (the original 0 0 500 500
57
+ // had ~36% empty padding that made the mark render tiny). Centered on the
58
+ // shape's true centroid (249.75, 240.2) with a 280px padded square.
59
+ return (
60
+ <svg
61
+ width={size}
62
+ height={size}
63
+ viewBox="110 100 280 280"
64
+ xmlns="http://www.w3.org/2000/svg"
65
+ aria-hidden
66
+ >
67
+ <path
68
+ fill="#076BFF"
69
+ d="M228.8,166.3h39.6c3.1,0,5.7-2.5,5.7-5.6V121c0-3.1-2.6-5.7-5.7-5.7h-39.6c-3.1,0-5.7,2.6-5.7,5.7v39.6 C223.1,163.8,225.7,166.3,228.8,166.3z"
70
+ />
71
+ <path
72
+ fill="#1E2025"
73
+ d="M227.2,185.8h-39.6c-3.1,0-5.7,2.6-5.7,5.7v39.6c0,3.1,2.6,5.7,5.7,5.7h39.6c3.1,0,5.7-2.6,5.7-5.7v-39.6 C232.9,188.4,230.3,185.8,227.2,185.8z"
74
+ />
75
+ <path
76
+ fill="#1E2025"
77
+ d="M311.4,231.1v-39.6c0-3.1-2.6-5.7-5.7-5.7h-39.6c-3.1,0-5.7,2.6-5.7,5.7v39.6c0,3.1,2.6,5.7,5.7,5.7h39.6 C308.8,236.8,311.4,234.3,311.4,231.1z"
78
+ />
79
+ <path
80
+ fill="#1E2025"
81
+ d="M332.7,258.4h-41.3c-3.3,0-6,2.7-6,6v94.7c0,3.3,2.7,6,6,6h41.3c3.3,0,6-2.7,6-6v-94.7 C338.7,261.1,336,258.4,332.7,258.4z"
82
+ />
83
+ <path
84
+ fill="#1E2025"
85
+ d="M208.1,258.4h-41.3c-3.3,0-6,2.7-6,6v94.7c0,3.3,2.7,6,6,6h41.3c3.3,0,6-2.7,6-6v-94.7 C214.2,261.1,211.5,258.4,208.1,258.4z"
86
+ />
87
+ </svg>
88
+ );
89
+ }
90
+
27
91
  // ============================================
28
92
  // Icon Component — Mockup A set (Storage kept from original)
29
93
  // ============================================
30
94
 
31
- function NavIcon({ icon, active }: { icon: string; active?: boolean }) {
32
- const size = 18;
33
- void active; // active state handled by parent text color
95
+ function NavIcon({ icon }: { icon: string }) {
96
+ const size = 20;
34
97
  switch (icon) {
35
98
  case "file":
36
- // Mockup A — document with folded corner
37
99
  return (
38
100
  <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
39
101
  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
@@ -41,7 +103,6 @@ function NavIcon({ icon, active }: { icon: string; active?: boolean }) {
41
103
  </svg>
42
104
  );
43
105
  case "film":
44
- // Mockup A — 2x2 rounded grid (Projects)
45
106
  return (
46
107
  <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
47
108
  <rect x="3" y="3" width="7" height="7" rx="1.5" />
@@ -51,7 +112,6 @@ function NavIcon({ icon, active }: { icon: string; active?: boolean }) {
51
112
  </svg>
52
113
  );
53
114
  case "palette":
54
- // Mockup A — minimal palette face (Customize)
55
115
  return (
56
116
  <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
57
117
  <circle cx="12" cy="12" r="9" />
@@ -61,7 +121,6 @@ function NavIcon({ icon, active }: { icon: string; active?: boolean }) {
61
121
  </svg>
62
122
  );
63
123
  case "nav":
64
- // Mockup A — 3 horizontal lines (Navigation)
65
124
  return (
66
125
  <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
67
126
  <line x1="4" y1="7" x2="20" y2="7" />
@@ -70,7 +129,6 @@ function NavIcon({ icon, active }: { icon: string; active?: boolean }) {
70
129
  </svg>
71
130
  );
72
131
  case "database":
73
- // Mockup A — 3-tier cylinder (Database)
74
132
  return (
75
133
  <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
76
134
  <ellipse cx="12" cy="6" rx="8" ry="3" />
@@ -79,7 +137,6 @@ function NavIcon({ icon, active }: { icon: string; active?: boolean }) {
79
137
  </svg>
80
138
  );
81
139
  case "harddisk":
82
- // Kept as-is per user request (Storage)
83
140
  return (
84
141
  <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
85
142
  <path d="M22 12H2" />
@@ -89,7 +146,6 @@ function NavIcon({ icon, active }: { icon: string; active?: boolean }) {
89
146
  </svg>
90
147
  );
91
148
  case "code":
92
- // Mockup A — < > brackets (Metadata)
93
149
  return (
94
150
  <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
95
151
  <polyline points="8 6 3 12 8 18" />
@@ -97,7 +153,6 @@ function NavIcon({ icon, active }: { icon: string; active?: boolean }) {
97
153
  </svg>
98
154
  );
99
155
  case "cloud-download":
100
- // Mockup A — cloud with down arrow (Backups)
101
156
  return (
102
157
  <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
103
158
  <path d="M20 16.5A4.5 4.5 0 0 0 17 8.5 7 7 0 0 0 4 9a5 5 0 0 0 1 9.9" />
@@ -105,13 +160,35 @@ function NavIcon({ icon, active }: { icon: string; active?: boolean }) {
105
160
  <path d="M8.5 16.5L12 20l3.5-3.5" />
106
161
  </svg>
107
162
  );
163
+ case "setup":
164
+ return (
165
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
166
+ <path d="M14.7 6.3a4 4 0 0 0-5.6 5.6l-6 6a1.5 1.5 0 0 0 2.1 2.1l6-6a4 4 0 0 0 5.6-5.6l-2.2 2.2-2.1-2.1z" />
167
+ </svg>
168
+ );
169
+ case "view":
170
+ return (
171
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
172
+ <path d="M15 3h6v6" />
173
+ <path d="M10 14L21 3" />
174
+ <path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5" />
175
+ </svg>
176
+ );
177
+ case "logout":
178
+ return (
179
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
180
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
181
+ <polyline points="16 17 21 12 16 7" />
182
+ <line x1="21" y1="12" x2="9" y2="12" />
183
+ </svg>
184
+ );
108
185
  default:
109
186
  return null;
110
187
  }
111
188
  }
112
189
 
113
190
  // ============================================
114
- // Layout Component
191
+ // Layout Component — collapsed-only sidebar
115
192
  // ============================================
116
193
 
117
194
  export default function AdminLayout({
@@ -122,31 +199,10 @@ export default function AdminLayout({
122
199
  const pathname = usePathname();
123
200
  const router = useRouter();
124
201
 
125
- // Detect if we're inside the page builder (editing a specific page or project)
126
202
  const isPageBuilder =
127
203
  /^\/admin\/pages\/[^/]+$/.test(pathname) ||
128
204
  /^\/admin\/projects\/[^/]+$/.test(pathname);
129
205
 
130
- // Start collapsed if loading directly into the builder
131
- const [sidebarOpen, setSidebarOpen] = useState(!isPageBuilder);
132
- const prevPathname = useRef(pathname);
133
-
134
- // Auto-collapse sidebar when entering the page builder,
135
- // auto-expand when leaving it
136
- useEffect(() => {
137
- if (pathname === prevPathname.current) return;
138
- const wasInBuilder =
139
- /^\/admin\/pages\/[^/]+$/.test(prevPathname.current) ||
140
- /^\/admin\/projects\/[^/]+$/.test(prevPathname.current);
141
- prevPathname.current = pathname;
142
-
143
- if (isPageBuilder && !wasInBuilder) {
144
- setSidebarOpen(false);
145
- } else if (!isPageBuilder && wasInBuilder) {
146
- setSidebarOpen(true);
147
- }
148
- }, [pathname, isPageBuilder]);
149
-
150
206
  // Don't show admin shell on login or setup pages
151
207
  if (pathname === "/admin/login" || pathname === "/admin/setup") {
152
208
  return <>{children}</>;
@@ -158,167 +214,104 @@ export default function AdminLayout({
158
214
  router.refresh();
159
215
  };
160
216
 
161
- // Check if a link is active
162
- const isLinkActive = (href: string) => {
163
- return pathname === href || pathname.startsWith(href + "/");
164
- };
217
+ const isLinkActive = (href: string) =>
218
+ pathname === href || pathname.startsWith(href + "/");
219
+
220
+ const tileBase = `group relative flex items-center justify-center ${TILE_SIZE} ${TILE_RADIUS} transition-colors`;
165
221
 
166
- // Reusable nav link renderer for workspace/system sections
167
222
  const renderNavLink = (link: { href: string; label: string; icon: string }) => {
168
223
  const isActive = isLinkActive(link.href);
169
224
  return (
170
225
  <Link
171
226
  key={link.href}
172
227
  href={link.href}
173
- className={`relative flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors mb-0.5 ${
228
+ className={`${tileBase} ${
174
229
  isActive
175
- ? "bg-gradient-to-r from-[rgba(7,107,255,0.14)] to-[rgba(7,107,255,0.02)] text-white shadow-[inset_0_0_0_1px_rgba(7,107,255,0.18)]"
176
- : "text-white/65 hover:bg-white/[0.035] hover:text-white"
177
- } ${!sidebarOpen ? "justify-center px-0" : ""}`}
178
- title={!sidebarOpen ? link.label : undefined}
230
+ ? "bg-[#076bff] text-white"
231
+ : "text-white/70 hover:bg-white/[0.06] hover:text-white"
232
+ }`}
233
+ aria-label={link.label}
179
234
  >
180
- {isActive && (
181
- <span
182
- aria-hidden
183
- className="pointer-events-none absolute left-[-8px] top-2 bottom-2 w-[3px] rounded-r-full bg-[#076bff] shadow-[0_0_12px_rgba(7,107,255,0.45)]"
184
- />
185
- )}
186
- <span className={`shrink-0 transition-colors ${isActive ? "text-[#076bff]" : ""}`}>
187
- <NavIcon icon={link.icon} active={isActive} />
188
- </span>
189
- {sidebarOpen && <span className="text-[13px]">{link.label}</span>}
235
+ <NavIcon icon={link.icon} />
236
+ <Tooltip>{link.label}</Tooltip>
190
237
  </Link>
191
238
  );
192
239
  };
193
240
 
194
- // Shared class for non-primary utility links (Setup Wizard, View Site, Log out)
195
- const utilityItemBase = `flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors mb-0.5 ${
196
- !sidebarOpen ? "justify-center px-0" : ""
197
- }`;
198
-
199
241
  return (
200
- <div data-admin className="flex h-screen bg-[#f8f8f8]" style={{ fontFamily: "Inter, system-ui, sans-serif" }}>
201
- {/* Sidebar — Dark (refined) */}
202
- <aside
203
- className={`flex flex-col bg-gradient-to-b from-[#1e2025] to-[#1a1c20] border-r border-white/[0.07] transition-all duration-200 ${
204
- sidebarOpen ? "w-56" : "w-16"
205
- }`}
206
- >
207
- {/* Workspace header */}
208
- <div
209
- className={`flex h-14 items-center border-b border-white/[0.06] ${
210
- sidebarOpen ? "justify-between px-4" : "justify-center px-0"
211
- }`}
212
- >
213
- {sidebarOpen && (
214
- <div className="flex min-w-0 flex-col leading-tight">
215
- <div className="flex items-center gap-1.5">
216
- <span className="whitespace-nowrap text-[11.5px] font-semibold tracking-wide text-white">
217
- Morphika Andami
218
- </span>
219
- <span className="rounded-full border border-[rgba(7,107,255,0.2)] bg-[rgba(7,107,255,0.12)] px-1.5 text-[9px] font-semibold leading-[15px] tracking-[0.08em] text-[#9cc2ff]">
220
- v{ANDAMI_VERSION}
221
- </span>
222
- </div>
223
- <span className="mt-0.5 truncate text-[10.5px] text-white/45">
224
- {getSiteConfig().name}
225
- </span>
226
- </div>
227
- )}
228
- <button
229
- onClick={() => setSidebarOpen(!sidebarOpen)}
230
- className="shrink-0 rounded-md p-1 text-white/40 transition-colors hover:bg-white/[0.05] hover:text-white/75"
231
- aria-label={sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}
242
+ <div
243
+ data-admin
244
+ className="flex h-screen bg-[#f8f8f8]"
245
+ style={{ fontFamily: "Inter, system-ui, sans-serif" }}
246
+ >
247
+ {/* Sidebar — collapsed-only (64px) */}
248
+ <aside className="flex w-16 flex-col items-center bg-gradient-to-b from-[#1e2025] to-[#1a1c20] border-r border-white/[0.07]">
249
+ {/* Logo white tile with the Andami mark, same square as nav items */}
250
+ <div className="pt-3 pb-2">
251
+ <div
252
+ className={`${tileBase} bg-white`}
253
+ aria-label={`Morphika Andami v${ANDAMI_VERSION}`}
232
254
  >
233
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
234
- {sidebarOpen ? (
235
- <polyline points="11 17 6 12 11 7" />
236
- ) : (
237
- <polyline points="13 7 18 12 13 17" />
238
- )}
239
- </svg>
240
- </button>
255
+ <AndamiMark size={30} />
256
+ <Tooltip>
257
+ Morphika Andami
258
+ <span className="ml-1.5 text-white/55">v{ANDAMI_VERSION}</span>
259
+ </Tooltip>
260
+ </div>
241
261
  </div>
242
262
 
243
- {/* Nav — grouped into Workspace + System */}
244
- <nav className="flex-1 overflow-y-auto px-2 py-3">
245
- {sidebarOpen && (
246
- <div className="px-3 pt-1 pb-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-white/35">
247
- Workspace
248
- </div>
249
- )}
263
+ {/* Workspace nav */}
264
+ <nav className="flex flex-1 flex-col items-center gap-1.5 pt-2 pb-2">
250
265
  {workspaceLinks.map(renderNavLink)}
251
266
 
252
- {sidebarOpen ? (
253
- <div className="px-3 pt-4 pb-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-white/35">
254
- System
255
- </div>
256
- ) : (
257
- <div className="mx-3 my-2 h-px bg-white/[0.06]" aria-hidden />
258
- )}
267
+ <div className="my-2 h-px w-6 bg-white/10" aria-hidden />
268
+
259
269
  {systemLinks.map(renderNavLink)}
260
270
  </nav>
261
271
 
262
- {/* Footer — Utility group */}
263
- <div className="px-2 pb-2">
264
- {sidebarOpen ? (
265
- <div className="px-3 pt-1 pb-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-white/35">
266
- Utility
267
- </div>
268
- ) : (
269
- <div className="mx-3 my-2 h-px bg-white/[0.06]" aria-hidden />
270
- )}
272
+ {/* Utility footer */}
273
+ <div className="flex flex-col items-center gap-1.5 pb-3">
274
+ <div className="mb-2 h-px w-6 bg-white/10" aria-hidden />
271
275
 
272
- {/* Setup Wizard */}
273
276
  <Link
274
277
  href="/admin/setup"
275
- className={`${utilityItemBase} text-white/50 hover:bg-white/[0.035] hover:text-white`}
276
- title="Setup Wizard"
278
+ className={`${tileBase} text-white/55 hover:bg-white/[0.06] hover:text-white`}
279
+ aria-label="Setup Wizard"
277
280
  >
278
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="shrink-0">
279
- <path d="M14.7 6.3a4 4 0 0 0-5.6 5.6l-6 6a1.5 1.5 0 0 0 2.1 2.1l6-6a4 4 0 0 0 5.6-5.6l-2.2 2.2-2.1-2.1z" />
280
- </svg>
281
- {sidebarOpen && <span className="text-[13px]">Setup Wizard</span>}
281
+ <NavIcon icon="setup" />
282
+ <Tooltip>Setup Wizard</Tooltip>
282
283
  </Link>
283
284
 
284
- {/* View Site */}
285
285
  <Link
286
286
  href="/"
287
287
  target="_blank"
288
- className={`${utilityItemBase} text-white/50 hover:bg-white/[0.035] hover:text-white`}
289
- title="View Site"
288
+ className={`${tileBase} text-white/55 hover:bg-white/[0.06] hover:text-white`}
289
+ aria-label="View Site"
290
290
  >
291
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="shrink-0">
292
- <path d="M15 3h6v6" />
293
- <path d="M10 14L21 3" />
294
- <path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5" />
295
- </svg>
296
- {sidebarOpen && <span className="text-[13px]">View Site</span>}
291
+ <NavIcon icon="view" />
292
+ <Tooltip>View Site</Tooltip>
297
293
  </Link>
298
294
 
299
- {/* Log out */}
300
295
  <button
301
296
  onClick={handleLogout}
302
- className={`${utilityItemBase} w-full text-white/50 hover:bg-red-500/[0.08] hover:text-red-300`}
303
- title="Log out"
297
+ className={`${tileBase} text-white/55 hover:bg-red-500/[0.1] hover:text-red-300`}
298
+ aria-label="Log out"
304
299
  >
305
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="shrink-0">
306
- <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
307
- <polyline points="16 17 21 12 16 7" />
308
- <line x1="21" y1="12" x2="9" y2="12" />
309
- </svg>
310
- {sidebarOpen && <span className="text-[13px]">Log out</span>}
300
+ <NavIcon icon="logout" />
301
+ <Tooltip>Log out</Tooltip>
311
302
  </button>
312
303
  </div>
313
304
  </aside>
314
305
 
315
306
  {/* Main content area — no top header bar, pages have their own titles */}
316
307
  <div className="flex flex-1 flex-col overflow-hidden">
317
- <main className={`flex-1 ${
318
- isPageBuilder
319
- ? "overflow-hidden"
320
- : "overflow-y-auto p-8 bg-[#f8f8f8]"
321
- }`}>
308
+ <main
309
+ className={`flex-1 ${
310
+ isPageBuilder
311
+ ? "overflow-hidden"
312
+ : "overflow-y-auto p-8 bg-[#f8f8f8]"
313
+ }`}
314
+ >
322
315
  {children}
323
316
  </main>
324
317
  </div>