@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.
- package/README.md +151 -36
- package/app/admin/layout.tsx +145 -152
- package/components/blocks/AudioBlockRenderer.tsx +286 -0
- package/components/blocks/BeforeAfterBlockRenderer.tsx +274 -0
- package/components/builder/BlockCardIcons.tsx +89 -0
- package/components/builder/BlockTypePicker.tsx +2 -0
- package/components/builder/CoverSectionCanvas.tsx +90 -2
- package/components/builder/SectionV2Canvas.tsx +19 -3
- package/components/builder/SectionV2Column.tsx +5 -1
- package/components/builder/asset-browser/R2BrowserContent.tsx +23 -6
- package/components/builder/asset-browser/helpers.ts +4 -0
- package/components/builder/asset-browser/types.ts +2 -1
- package/components/builder/blockStyles.tsx +12 -0
- package/components/builder/editors/AudioBlockEditor.tsx +242 -0
- package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -0
- package/components/builder/editors/shared.tsx +1 -1
- package/components/builder/live-preview/LiveAudioPreview.tsx +120 -0
- package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +176 -0
- package/lib/animation/enter-types.ts +2 -0
- package/lib/animation/hover-effect-types.ts +2 -0
- package/lib/builder/block-registrations.ts +83 -1
- package/lib/builder/types.ts +2 -0
- package/lib/sanity/types.ts +58 -0
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/audioBlock.ts +69 -0
- package/sanity/schemas/blocks/beforeAfterBlock.ts +121 -0
- package/sanity/schemas/blocks/index.ts +3 -1
- package/sanity/schemas/index.ts +7 -1
package/README.md
CHANGED
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
# @morphika/andami
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
**A visual page builder framework for Next.js — the engine behind [morphika.tv](https://morphika.tv).**
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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 |
|
|
41
|
-
|
|
42
|
-
| [
|
|
43
|
-
| [
|
|
44
|
-
| [
|
|
45
|
-
| [
|
|
46
|
-
| [
|
|
47
|
-
| [
|
|
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
|
-
| [
|
|
50
|
-
| [
|
|
51
|
-
| [
|
|
52
|
-
| [
|
|
53
|
-
| [
|
|
54
|
-
| [
|
|
55
|
-
| [
|
|
56
|
-
|
|
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.
|
package/app/admin/layout.tsx
CHANGED
|
@@ -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
|
|
32
|
-
const size =
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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={
|
|
228
|
+
className={`${tileBase} ${
|
|
174
229
|
isActive
|
|
175
|
-
? "bg-
|
|
176
|
-
: "text-white/
|
|
177
|
-
}
|
|
178
|
-
|
|
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
|
-
{
|
|
181
|
-
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
>
|
|
207
|
-
{/*
|
|
208
|
-
<div
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
<
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
{/*
|
|
244
|
-
<nav className="flex-1
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
{/*
|
|
263
|
-
<div className="
|
|
264
|
-
|
|
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={`${
|
|
276
|
-
|
|
278
|
+
className={`${tileBase} text-white/55 hover:bg-white/[0.06] hover:text-white`}
|
|
279
|
+
aria-label="Setup Wizard"
|
|
277
280
|
>
|
|
278
|
-
<
|
|
279
|
-
|
|
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={`${
|
|
289
|
-
|
|
288
|
+
className={`${tileBase} text-white/55 hover:bg-white/[0.06] hover:text-white`}
|
|
289
|
+
aria-label="View Site"
|
|
290
290
|
>
|
|
291
|
-
<
|
|
292
|
-
|
|
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={`${
|
|
303
|
-
|
|
297
|
+
className={`${tileBase} text-white/55 hover:bg-red-500/[0.1] hover:text-red-300`}
|
|
298
|
+
aria-label="Log out"
|
|
304
299
|
>
|
|
305
|
-
<
|
|
306
|
-
|
|
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
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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>
|