@olonjs/cli 3.0.92 → 3.0.94
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.
|
@@ -1634,7 +1634,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1634
1634
|
"dev:clean": "vite --force",
|
|
1635
1635
|
"prebuild": "node scripts/sync-pages-to-public.mjs",
|
|
1636
1636
|
"build": "tsc && vite build",
|
|
1637
|
-
"dist": "bash ./src2Code.sh --template alpha src .cursor vercel.json index.html vite.config.ts scripts specs package.json",
|
|
1637
|
+
"dist": "bash ./src2Code.sh --template alpha src .cursor vercel.json index.html tsconfig.json tsconfig.node.json vite.config.ts scripts specs package.json",
|
|
1638
1638
|
"preview": "vite preview",
|
|
1639
1639
|
"bake:email": "tsx scripts/bake-email.tsx",
|
|
1640
1640
|
"bakemail": "npm run bake:email --",
|
|
@@ -1645,7 +1645,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1645
1645
|
"@tiptap/extension-link": "^2.11.5",
|
|
1646
1646
|
"@tiptap/react": "^2.11.5",
|
|
1647
1647
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1648
|
-
"@olonjs/core": "^1.0.
|
|
1648
|
+
"@olonjs/core": "^1.0.82",
|
|
1649
1649
|
"class-variance-authority": "^0.7.1",
|
|
1650
1650
|
"clsx": "^2.1.1",
|
|
1651
1651
|
"lucide-react": "^0.474.0",
|
|
@@ -6519,333 +6519,333 @@ END_OF_FILE_CONTENT
|
|
|
6519
6519
|
mkdir -p "src/components/header"
|
|
6520
6520
|
echo "Creating src/components/header/View.tsx..."
|
|
6521
6521
|
cat << 'END_OF_FILE_CONTENT' > "src/components/header/View.tsx"
|
|
6522
|
-
import { useState, useRef, useEffect } from 'react';
|
|
6523
|
-
import { Menu, X, ChevronDown } from 'lucide-react';
|
|
6524
|
-
import { OlonMark } from '@/components/OlonWordmark';
|
|
6525
|
-
import { Button } from '@/components/ui/button';
|
|
6526
|
-
import { ThemeToggle } from '@/components/ThemeToggle';
|
|
6527
|
-
import { cn } from '@/lib/utils';
|
|
6528
|
-
import type { HeaderData, HeaderSettings } from './types';
|
|
6529
|
-
import type { MenuItem } from '@olonjs/core';
|
|
6530
|
-
|
|
6531
|
-
interface NavChild {
|
|
6532
|
-
label: string;
|
|
6533
|
-
href: string;
|
|
6534
|
-
}
|
|
6535
|
-
|
|
6536
|
-
interface NavItem {
|
|
6537
|
-
label: string;
|
|
6538
|
-
href: string;
|
|
6539
|
-
variant?: string;
|
|
6540
|
-
children?: NavChild[];
|
|
6541
|
-
}
|
|
6542
|
-
|
|
6543
|
-
interface HeaderViewProps {
|
|
6544
|
-
data: HeaderData;
|
|
6545
|
-
settings?: HeaderSettings;
|
|
6546
|
-
menu: MenuItem[];
|
|
6547
|
-
}
|
|
6548
|
-
|
|
6549
|
-
function isMenuRef(value: unknown): value is { $ref: string } {
|
|
6550
|
-
if (!value || typeof value !== 'object') return false;
|
|
6551
|
-
const rec = value as Record<string, unknown>;
|
|
6552
|
-
return typeof rec.$ref === 'string' && rec.$ref.trim().length > 0;
|
|
6553
|
-
}
|
|
6554
|
-
|
|
6555
|
-
function toNavItem(raw: unknown): NavItem | null {
|
|
6556
|
-
if (!raw || typeof raw !== 'object') return null;
|
|
6557
|
-
const rec = raw as Record<string, unknown>;
|
|
6558
|
-
if (typeof rec.label !== 'string' || typeof rec.href !== 'string') return null;
|
|
6559
|
-
const children = Array.isArray(rec.children)
|
|
6560
|
-
? (rec.children as unknown[])
|
|
6561
|
-
.map((c) => toNavItem(c))
|
|
6562
|
-
.filter((c): c is NavChild => c !== null)
|
|
6563
|
-
: undefined;
|
|
6564
|
-
const variant = typeof rec.variant === 'string' ? rec.variant : undefined;
|
|
6565
|
-
return { label: rec.label, href: rec.href, ...(variant ? { variant } : {}), ...(children && children.length > 0 ? { children } : {}) };
|
|
6566
|
-
}
|
|
6567
|
-
|
|
6568
|
-
export function Header({ data, settings, menu }: HeaderViewProps) {
|
|
6569
|
-
const [mobileOpen, setMobileOpen] = useState(false);
|
|
6570
|
-
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
|
6571
|
-
const [mobileExpanded, setMobileExpanded] = useState<string | null>(null);
|
|
6572
|
-
const isSticky = settings?.sticky ?? true;
|
|
6573
|
-
const navRef = useRef<HTMLElement>(null);
|
|
6574
|
-
|
|
6575
|
-
const linksField = data.links as unknown;
|
|
6576
|
-
const rawLinks = Array.isArray(linksField) ? linksField : [];
|
|
6577
|
-
const menuItems = Array.isArray(menu) ? (menu as unknown[]) : [];
|
|
6578
|
-
// If tenant explicitly uses a JSON ref for links, resolve from menu config.
|
|
6579
|
-
const source =
|
|
6580
|
-
isMenuRef(linksField)
|
|
6581
|
-
? menuItems
|
|
6582
|
-
: (rawLinks.length > 0 ? rawLinks : menuItems);
|
|
6583
|
-
const navItems: NavItem[] = source.map(toNavItem).filter((i): i is NavItem => i !== null);
|
|
6584
|
-
|
|
6585
|
-
useEffect(() => {
|
|
6586
|
-
if (!openDropdown) return;
|
|
6587
|
-
function handleClick(e: MouseEvent) {
|
|
6588
|
-
if (navRef.current && !navRef.current.contains(e.target as Node)) {
|
|
6589
|
-
setOpenDropdown(null);
|
|
6590
|
-
}
|
|
6591
|
-
}
|
|
6592
|
-
document.addEventListener('mousedown', handleClick);
|
|
6593
|
-
return () => document.removeEventListener('mousedown', handleClick);
|
|
6594
|
-
}, [openDropdown]);
|
|
6595
|
-
|
|
6596
|
-
return (
|
|
6597
|
-
<header
|
|
6598
|
-
className={cn(
|
|
6599
|
-
'top-0 left-0 right-0 z-50 border-b border-border bg-background/90 backdrop-blur-md',
|
|
6600
|
-
isSticky ? 'fixed' : 'relative'
|
|
6601
|
-
)}
|
|
6602
|
-
>
|
|
6603
|
-
<div className="max-w-6xl mx-auto px-6 h-18 flex items-center gap-8">
|
|
6604
|
-
|
|
6605
|
-
{/* Logo */}
|
|
6606
|
-
<a href="/" className="flex items-center gap-2 shrink-0" aria-label="OlonJS home">
|
|
6607
|
-
<OlonMark size={26} className="mb-0.5" />
|
|
6608
|
-
<span
|
|
6609
|
-
className="text-2xl text-accent leading-none"
|
|
6610
|
-
style={{
|
|
6611
|
-
fontFamily: 'var(--wordmark-font)',
|
|
6612
|
-
letterSpacing: 'var(--wordmark-tracking)',
|
|
6613
|
-
fontWeight: 'var(--wordmark-weight)',
|
|
6614
|
-
fontVariationSettings: '"wdth" var(--wordmark-width)',
|
|
6615
|
-
}}
|
|
6616
|
-
>
|
|
6617
|
-
{data.logoText}
|
|
6618
|
-
</span>
|
|
6619
|
-
{data.badge && (
|
|
6620
|
-
<span className="hidden sm:inline-flex items-center px-1.5 py-0.5 text-[10px] font-medium font-mono-olon bg-primary-900 text-primary-light border border-primary-800 rounded-sm">
|
|
6621
|
-
{data.badge}
|
|
6622
|
-
</span>
|
|
6623
|
-
)}
|
|
6624
|
-
</a>
|
|
6625
|
-
|
|
6626
|
-
{/* Desktop nav */}
|
|
6627
|
-
<nav ref={navRef} className="hidden md:flex items-center gap-0.5 flex-1">
|
|
6628
|
-
{navItems.map((item) => {
|
|
6629
|
-
const hasChildren = item.children && item.children.length > 0;
|
|
6630
|
-
const isOpen = openDropdown === item.label;
|
|
6631
|
-
const isSecondary = item.variant === 'secondary';
|
|
6632
|
-
|
|
6633
|
-
if (isSecondary) {
|
|
6634
|
-
return (
|
|
6635
|
-
<a
|
|
6636
|
-
key={item.label}
|
|
6637
|
-
href={item.href}
|
|
6638
|
-
className="flex items-center gap-1 px-3 py-1.5 text-[13px] text-muted-foreground hover:text-foreground rounded-md border border-border bg-elevated hover:bg-elevated/70 transition-colors duration-150"
|
|
6639
|
-
>
|
|
6640
|
-
{item.label}
|
|
6641
|
-
</a>
|
|
6642
|
-
);
|
|
6643
|
-
}
|
|
6644
|
-
|
|
6645
|
-
if (!hasChildren) {
|
|
6646
|
-
return (
|
|
6647
|
-
<a
|
|
6648
|
-
key={item.label}
|
|
6649
|
-
href={item.href}
|
|
6650
|
-
className="flex items-center gap-1 px-3 py-1.5 text-[13px] text-muted-foreground hover:text-foreground rounded-md transition-colors duration-150 hover:bg-elevated"
|
|
6651
|
-
>
|
|
6652
|
-
{item.label}
|
|
6653
|
-
</a>
|
|
6654
|
-
);
|
|
6655
|
-
}
|
|
6656
|
-
|
|
6657
|
-
return (
|
|
6658
|
-
<div key={item.label} className="relative">
|
|
6659
|
-
<button
|
|
6660
|
-
type="button"
|
|
6661
|
-
onClick={() => setOpenDropdown(isOpen ? null : item.label)}
|
|
6662
|
-
className={cn(
|
|
6663
|
-
'flex items-center gap-1 px-3 py-1.5 text-[13px] rounded-md transition-colors duration-150',
|
|
6664
|
-
isOpen ? 'text-foreground bg-elevated' : 'text-muted-foreground hover:text-foreground hover:bg-elevated'
|
|
6665
|
-
)}
|
|
6666
|
-
aria-expanded={hasChildren ? isOpen : undefined}
|
|
6667
|
-
>
|
|
6668
|
-
{item.label}
|
|
6669
|
-
{hasChildren && (
|
|
6670
|
-
<ChevronDown
|
|
6671
|
-
size={11}
|
|
6672
|
-
className={cn('opacity-40 mt-px transition-transform duration-150', isOpen && 'rotate-180 opacity-70')}
|
|
6673
|
-
/>
|
|
6674
|
-
)}
|
|
6675
|
-
</button>
|
|
6676
|
-
|
|
6677
|
-
{hasChildren && (
|
|
6678
|
-
<div
|
|
6679
|
-
className={cn(
|
|
6680
|
-
'absolute left-0 top-[calc(100%+8px)] min-w-[220px] rounded-lg border border-border bg-card shadow-lg shadow-black/20 overflow-hidden',
|
|
6681
|
-
'transition-all duration-150 origin-top-left',
|
|
6682
|
-
isOpen ? 'opacity-100 scale-100 pointer-events-auto' : 'opacity-0 scale-95 pointer-events-none'
|
|
6683
|
-
)}
|
|
6684
|
-
>
|
|
6685
|
-
<div className="p-1.5">
|
|
6686
|
-
{item.children!.map((child, i) => (
|
|
6687
|
-
<a
|
|
6688
|
-
key={child.label}
|
|
6689
|
-
href={child.href}
|
|
6690
|
-
onClick={() => setOpenDropdown(null)}
|
|
6691
|
-
className={cn(
|
|
6692
|
-
'flex items-center gap-3 px-3 py-2.5 rounded-md text-[13px] text-muted-foreground hover:text-foreground hover:bg-elevated transition-colors duration-100 group',
|
|
6693
|
-
i < item.children!.length - 1 && ''
|
|
6694
|
-
)}
|
|
6695
|
-
>
|
|
6696
|
-
<span className="w-6 h-6 rounded-md bg-primary-900 border border-primary-800 flex items-center justify-center shrink-0 text-[10px] font-medium font-mono-olon text-primary-light group-hover:border-primary transition-colors">
|
|
6697
|
-
{child.label.slice(0, 2).toUpperCase()}
|
|
6698
|
-
</span>
|
|
6699
|
-
<span className="font-medium">{child.label}</span>
|
|
6700
|
-
</a>
|
|
6701
|
-
))}
|
|
6702
|
-
</div>
|
|
6703
|
-
<div className="px-3 py-2 border-t border-border bg-elevated/50">
|
|
6704
|
-
<a
|
|
6705
|
-
href={item.href}
|
|
6706
|
-
onClick={() => setOpenDropdown(null)}
|
|
6707
|
-
className="text-[11px] text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
|
6708
|
-
>
|
|
6709
|
-
View all {item.label.toLowerCase()} →
|
|
6710
|
-
</a>
|
|
6711
|
-
</div>
|
|
6712
|
-
</div>
|
|
6713
|
-
)}
|
|
6714
|
-
</div>
|
|
6715
|
-
);
|
|
6716
|
-
})}
|
|
6717
|
-
</nav>
|
|
6718
|
-
|
|
6719
|
-
{/* Actions */}
|
|
6720
|
-
<div className="hidden md:flex items-center gap-1 ml-auto shrink-0">
|
|
6721
|
-
<ThemeToggle />
|
|
6722
|
-
{data.signinHref && (
|
|
6723
|
-
<a
|
|
6724
|
-
href={data.signinHref}
|
|
6725
|
-
className="text-[13px] text-muted-foreground hover:text-foreground transition-colors duration-150 px-3 py-1.5 rounded-md hover:bg-elevated"
|
|
6726
|
-
>
|
|
6727
|
-
Sign in
|
|
6728
|
-
</a>
|
|
6729
|
-
)}
|
|
6730
|
-
{data.ctaHref && (
|
|
6731
|
-
<Button variant="accent" size="sm" className="h-8 px-4 text-[13px] font-medium" asChild>
|
|
6732
|
-
<a href={data.ctaHref}>{data.ctaLabel ?? 'Get started →'}</a>
|
|
6733
|
-
</Button>
|
|
6734
|
-
)}
|
|
6735
|
-
</div>
|
|
6736
|
-
|
|
6737
|
-
{/* Mobile toggle */}
|
|
6738
|
-
<button
|
|
6739
|
-
className="md:hidden ml-auto p-1.5 text-muted-foreground hover:text-foreground transition-colors"
|
|
6740
|
-
onClick={() => setMobileOpen(!mobileOpen)}
|
|
6741
|
-
aria-label="Toggle menu"
|
|
6742
|
-
>
|
|
6743
|
-
{mobileOpen ? <X size={16} /> : <Menu size={16} />}
|
|
6744
|
-
</button>
|
|
6745
|
-
</div>
|
|
6746
|
-
|
|
6747
|
-
{/* Mobile drawer */}
|
|
6748
|
-
<div className={cn(
|
|
6749
|
-
'md:hidden border-t border-border bg-card overflow-hidden transition-all duration-200',
|
|
6750
|
-
mobileOpen ? 'max-h-[32rem]' : 'max-h-0'
|
|
6751
|
-
)}>
|
|
6752
|
-
<nav className="px-4 py-3 flex flex-col gap-0.5">
|
|
6753
|
-
{navItems.map((item) => {
|
|
6754
|
-
const hasChildren = item.children && item.children.length > 0;
|
|
6755
|
-
const isExpanded = mobileExpanded === item.label;
|
|
6756
|
-
const isSecondary = item.variant === 'secondary';
|
|
6757
|
-
|
|
6758
|
-
if (isSecondary) {
|
|
6759
|
-
return (
|
|
6760
|
-
<a
|
|
6761
|
-
key={item.label}
|
|
6762
|
-
href={item.href}
|
|
6763
|
-
onClick={() => setMobileOpen(false)}
|
|
6764
|
-
className="mt-1 flex items-center px-3 py-2.5 text-[13px] text-muted-foreground hover:text-foreground border border-border bg-elevated hover:bg-elevated/70 rounded-md transition-colors"
|
|
6765
|
-
>
|
|
6766
|
-
{item.label}
|
|
6767
|
-
</a>
|
|
6768
|
-
);
|
|
6769
|
-
}
|
|
6770
|
-
|
|
6771
|
-
if (!hasChildren) {
|
|
6772
|
-
return (
|
|
6773
|
-
<a
|
|
6774
|
-
key={item.label}
|
|
6775
|
-
href={item.href}
|
|
6776
|
-
onClick={() => setMobileOpen(false)}
|
|
6777
|
-
className="flex items-center px-3 py-2.5 text-[13px] text-muted-foreground hover:text-foreground hover:bg-elevated rounded-md transition-colors"
|
|
6778
|
-
>
|
|
6779
|
-
{item.label}
|
|
6780
|
-
</a>
|
|
6781
|
-
);
|
|
6782
|
-
}
|
|
6783
|
-
|
|
6784
|
-
return (
|
|
6785
|
-
<div key={item.label}>
|
|
6786
|
-
<button
|
|
6787
|
-
type="button"
|
|
6788
|
-
onClick={() => {
|
|
6789
|
-
if (hasChildren) {
|
|
6790
|
-
setMobileExpanded(isExpanded ? null : item.label);
|
|
6791
|
-
}
|
|
6792
|
-
}}
|
|
6793
|
-
className="w-full flex items-center justify-between px-3 py-2.5 text-[13px] text-muted-foreground hover:text-foreground hover:bg-elevated rounded-md transition-colors text-left"
|
|
6794
|
-
>
|
|
6795
|
-
<span>{item.label}</span>
|
|
6796
|
-
{hasChildren && (
|
|
6797
|
-
<ChevronDown
|
|
6798
|
-
size={13}
|
|
6799
|
-
className={cn('opacity-40 transition-transform duration-150', isExpanded && 'rotate-180 opacity-70')}
|
|
6800
|
-
/>
|
|
6801
|
-
)}
|
|
6802
|
-
</button>
|
|
6803
|
-
|
|
6804
|
-
{hasChildren && isExpanded && (
|
|
6805
|
-
<div className="ml-3 pl-3 border-l border-border mt-0.5 mb-1 flex flex-col gap-0.5">
|
|
6806
|
-
{item.children!.map((child) => (
|
|
6807
|
-
<a
|
|
6808
|
-
key={child.label}
|
|
6809
|
-
href={child.href}
|
|
6810
|
-
onClick={() => { setMobileOpen(false); setMobileExpanded(null); }}
|
|
6811
|
-
className="flex items-center gap-2.5 px-3 py-2 text-[12px] text-muted-foreground hover:text-foreground hover:bg-elevated rounded-md transition-colors"
|
|
6812
|
-
>
|
|
6813
|
-
<span className="w-5 h-5 rounded bg-primary-900 border border-primary-800 flex items-center justify-center shrink-0 text-[9px] font-medium font-mono-olon text-primary-light">
|
|
6814
|
-
{child.label.slice(0, 2).toUpperCase()}
|
|
6815
|
-
</span>
|
|
6816
|
-
{child.label}
|
|
6817
|
-
</a>
|
|
6818
|
-
))}
|
|
6819
|
-
<a
|
|
6820
|
-
href={item.href}
|
|
6821
|
-
onClick={() => { setMobileOpen(false); setMobileExpanded(null); }}
|
|
6822
|
-
className="px-3 py-1.5 text-[11px] text-muted-foreground hover:text-foreground transition-colors"
|
|
6823
|
-
>
|
|
6824
|
-
View all →
|
|
6825
|
-
</a>
|
|
6826
|
-
</div>
|
|
6827
|
-
)}
|
|
6828
|
-
</div>
|
|
6829
|
-
);
|
|
6830
|
-
})}
|
|
6831
|
-
|
|
6832
|
-
<div className="flex gap-2 pt-3 mt-2 border-t border-border">
|
|
6833
|
-
{data.signinHref && (
|
|
6834
|
-
<Button variant="outline" size="sm" className="flex-1 text-[13px]" asChild>
|
|
6835
|
-
<a href={data.signinHref}>Sign in</a>
|
|
6836
|
-
</Button>
|
|
6837
|
-
)}
|
|
6838
|
-
{data.ctaHref && (
|
|
6839
|
-
<Button variant="accent" size="sm" className="flex-1 text-[13px]" asChild>
|
|
6840
|
-
<a href={data.ctaHref}>{data.ctaLabel ?? 'Get started'}</a>
|
|
6841
|
-
</Button>
|
|
6842
|
-
)}
|
|
6843
|
-
</div>
|
|
6844
|
-
</nav>
|
|
6845
|
-
</div>
|
|
6846
|
-
</header>
|
|
6847
|
-
);
|
|
6848
|
-
}
|
|
6522
|
+
import { useState, useRef, useEffect } from 'react';
|
|
6523
|
+
import { Menu, X, ChevronDown } from 'lucide-react';
|
|
6524
|
+
import { OlonMark } from '@/components/OlonWordmark';
|
|
6525
|
+
import { Button } from '@/components/ui/button';
|
|
6526
|
+
import { ThemeToggle } from '@/components/ThemeToggle';
|
|
6527
|
+
import { cn } from '@/lib/utils';
|
|
6528
|
+
import type { HeaderData, HeaderSettings } from './types';
|
|
6529
|
+
import type { MenuItem } from '@olonjs/core';
|
|
6530
|
+
|
|
6531
|
+
interface NavChild {
|
|
6532
|
+
label: string;
|
|
6533
|
+
href: string;
|
|
6534
|
+
}
|
|
6535
|
+
|
|
6536
|
+
interface NavItem {
|
|
6537
|
+
label: string;
|
|
6538
|
+
href: string;
|
|
6539
|
+
variant?: string;
|
|
6540
|
+
children?: NavChild[];
|
|
6541
|
+
}
|
|
6542
|
+
|
|
6543
|
+
interface HeaderViewProps {
|
|
6544
|
+
data: HeaderData;
|
|
6545
|
+
settings?: HeaderSettings;
|
|
6546
|
+
menu: MenuItem[];
|
|
6547
|
+
}
|
|
6548
|
+
|
|
6549
|
+
function isMenuRef(value: unknown): value is { $ref: string } {
|
|
6550
|
+
if (!value || typeof value !== 'object') return false;
|
|
6551
|
+
const rec = value as Record<string, unknown>;
|
|
6552
|
+
return typeof rec.$ref === 'string' && rec.$ref.trim().length > 0;
|
|
6553
|
+
}
|
|
6554
|
+
|
|
6555
|
+
function toNavItem(raw: unknown): NavItem | null {
|
|
6556
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
6557
|
+
const rec = raw as Record<string, unknown>;
|
|
6558
|
+
if (typeof rec.label !== 'string' || typeof rec.href !== 'string') return null;
|
|
6559
|
+
const children = Array.isArray(rec.children)
|
|
6560
|
+
? (rec.children as unknown[])
|
|
6561
|
+
.map((c) => toNavItem(c))
|
|
6562
|
+
.filter((c): c is NavChild => c !== null)
|
|
6563
|
+
: undefined;
|
|
6564
|
+
const variant = typeof rec.variant === 'string' ? rec.variant : undefined;
|
|
6565
|
+
return { label: rec.label, href: rec.href, ...(variant ? { variant } : {}), ...(children && children.length > 0 ? { children } : {}) };
|
|
6566
|
+
}
|
|
6567
|
+
|
|
6568
|
+
export function Header({ data, settings, menu }: HeaderViewProps) {
|
|
6569
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
6570
|
+
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
|
6571
|
+
const [mobileExpanded, setMobileExpanded] = useState<string | null>(null);
|
|
6572
|
+
const isSticky = settings?.sticky ?? true;
|
|
6573
|
+
const navRef = useRef<HTMLElement>(null);
|
|
6574
|
+
|
|
6575
|
+
const linksField = data.links as unknown;
|
|
6576
|
+
const rawLinks = Array.isArray(linksField) ? linksField : [];
|
|
6577
|
+
const menuItems = Array.isArray(menu) ? (menu as unknown[]) : [];
|
|
6578
|
+
// If tenant explicitly uses a JSON ref for links, resolve from menu config.
|
|
6579
|
+
const source =
|
|
6580
|
+
isMenuRef(linksField)
|
|
6581
|
+
? menuItems
|
|
6582
|
+
: (rawLinks.length > 0 ? rawLinks : menuItems);
|
|
6583
|
+
const navItems: NavItem[] = source.map(toNavItem).filter((i): i is NavItem => i !== null);
|
|
6584
|
+
|
|
6585
|
+
useEffect(() => {
|
|
6586
|
+
if (!openDropdown) return;
|
|
6587
|
+
function handleClick(e: MouseEvent) {
|
|
6588
|
+
if (navRef.current && !navRef.current.contains(e.target as Node)) {
|
|
6589
|
+
setOpenDropdown(null);
|
|
6590
|
+
}
|
|
6591
|
+
}
|
|
6592
|
+
document.addEventListener('mousedown', handleClick);
|
|
6593
|
+
return () => document.removeEventListener('mousedown', handleClick);
|
|
6594
|
+
}, [openDropdown]);
|
|
6595
|
+
|
|
6596
|
+
return (
|
|
6597
|
+
<header
|
|
6598
|
+
className={cn(
|
|
6599
|
+
'top-0 left-0 right-0 z-50 border-b border-border bg-background/90 backdrop-blur-md',
|
|
6600
|
+
isSticky ? 'fixed' : 'relative'
|
|
6601
|
+
)}
|
|
6602
|
+
>
|
|
6603
|
+
<div className="max-w-6xl mx-auto px-6 h-18 flex items-center gap-8">
|
|
6604
|
+
|
|
6605
|
+
{/* Logo */}
|
|
6606
|
+
<a href="/" className="flex items-center gap-2 shrink-0" aria-label="OlonJS home">
|
|
6607
|
+
<OlonMark size={26} className="mb-0.5" />
|
|
6608
|
+
<span
|
|
6609
|
+
className="text-2xl text-accent leading-none"
|
|
6610
|
+
style={{
|
|
6611
|
+
fontFamily: 'var(--wordmark-font)',
|
|
6612
|
+
letterSpacing: 'var(--wordmark-tracking)',
|
|
6613
|
+
fontWeight: 'var(--wordmark-weight)',
|
|
6614
|
+
fontVariationSettings: '"wdth" var(--wordmark-width)',
|
|
6615
|
+
}}
|
|
6616
|
+
>
|
|
6617
|
+
{data.logoText}
|
|
6618
|
+
</span>
|
|
6619
|
+
{data.badge && (
|
|
6620
|
+
<span className="hidden sm:inline-flex items-center px-1.5 py-0.5 text-[10px] font-medium font-mono-olon bg-primary-900 text-primary-light border border-primary-800 rounded-sm">
|
|
6621
|
+
{data.badge}
|
|
6622
|
+
</span>
|
|
6623
|
+
)}
|
|
6624
|
+
</a>
|
|
6625
|
+
|
|
6626
|
+
{/* Desktop nav */}
|
|
6627
|
+
<nav ref={navRef} className="hidden md:flex items-center gap-0.5 flex-1">
|
|
6628
|
+
{navItems.map((item) => {
|
|
6629
|
+
const hasChildren = item.children && item.children.length > 0;
|
|
6630
|
+
const isOpen = openDropdown === item.label;
|
|
6631
|
+
const isSecondary = item.variant === 'secondary';
|
|
6632
|
+
|
|
6633
|
+
if (isSecondary) {
|
|
6634
|
+
return (
|
|
6635
|
+
<a
|
|
6636
|
+
key={item.label}
|
|
6637
|
+
href={item.href}
|
|
6638
|
+
className="flex items-center gap-1 px-3 py-1.5 text-[13px] text-muted-foreground hover:text-foreground rounded-md border border-border bg-elevated hover:bg-elevated/70 transition-colors duration-150"
|
|
6639
|
+
>
|
|
6640
|
+
{item.label}
|
|
6641
|
+
</a>
|
|
6642
|
+
);
|
|
6643
|
+
}
|
|
6644
|
+
|
|
6645
|
+
if (!hasChildren) {
|
|
6646
|
+
return (
|
|
6647
|
+
<a
|
|
6648
|
+
key={item.label}
|
|
6649
|
+
href={item.href}
|
|
6650
|
+
className="flex items-center gap-1 px-3 py-1.5 text-[13px] text-muted-foreground hover:text-foreground rounded-md transition-colors duration-150 hover:bg-elevated"
|
|
6651
|
+
>
|
|
6652
|
+
{item.label}
|
|
6653
|
+
</a>
|
|
6654
|
+
);
|
|
6655
|
+
}
|
|
6656
|
+
|
|
6657
|
+
return (
|
|
6658
|
+
<div key={item.label} className="relative">
|
|
6659
|
+
<button
|
|
6660
|
+
type="button"
|
|
6661
|
+
onClick={() => setOpenDropdown(isOpen ? null : item.label)}
|
|
6662
|
+
className={cn(
|
|
6663
|
+
'flex items-center gap-1 px-3 py-1.5 text-[13px] rounded-md transition-colors duration-150',
|
|
6664
|
+
isOpen ? 'text-foreground bg-elevated' : 'text-muted-foreground hover:text-foreground hover:bg-elevated'
|
|
6665
|
+
)}
|
|
6666
|
+
aria-expanded={hasChildren ? isOpen : undefined}
|
|
6667
|
+
>
|
|
6668
|
+
{item.label}
|
|
6669
|
+
{hasChildren && (
|
|
6670
|
+
<ChevronDown
|
|
6671
|
+
size={11}
|
|
6672
|
+
className={cn('opacity-40 mt-px transition-transform duration-150', isOpen && 'rotate-180 opacity-70')}
|
|
6673
|
+
/>
|
|
6674
|
+
)}
|
|
6675
|
+
</button>
|
|
6676
|
+
|
|
6677
|
+
{hasChildren && (
|
|
6678
|
+
<div
|
|
6679
|
+
className={cn(
|
|
6680
|
+
'absolute left-0 top-[calc(100%+8px)] min-w-[220px] rounded-lg border border-border bg-card shadow-lg shadow-black/20 overflow-hidden',
|
|
6681
|
+
'transition-all duration-150 origin-top-left',
|
|
6682
|
+
isOpen ? 'opacity-100 scale-100 pointer-events-auto' : 'opacity-0 scale-95 pointer-events-none'
|
|
6683
|
+
)}
|
|
6684
|
+
>
|
|
6685
|
+
<div className="p-1.5">
|
|
6686
|
+
{item.children!.map((child, i) => (
|
|
6687
|
+
<a
|
|
6688
|
+
key={child.label}
|
|
6689
|
+
href={child.href}
|
|
6690
|
+
onClick={() => setOpenDropdown(null)}
|
|
6691
|
+
className={cn(
|
|
6692
|
+
'flex items-center gap-3 px-3 py-2.5 rounded-md text-[13px] text-muted-foreground hover:text-foreground hover:bg-elevated transition-colors duration-100 group',
|
|
6693
|
+
i < item.children!.length - 1 && ''
|
|
6694
|
+
)}
|
|
6695
|
+
>
|
|
6696
|
+
<span className="w-6 h-6 rounded-md bg-primary-900 border border-primary-800 flex items-center justify-center shrink-0 text-[10px] font-medium font-mono-olon text-primary-light group-hover:border-primary transition-colors">
|
|
6697
|
+
{child.label.slice(0, 2).toUpperCase()}
|
|
6698
|
+
</span>
|
|
6699
|
+
<span className="font-medium">{child.label}</span>
|
|
6700
|
+
</a>
|
|
6701
|
+
))}
|
|
6702
|
+
</div>
|
|
6703
|
+
<div className="px-3 py-2 border-t border-border bg-elevated/50">
|
|
6704
|
+
<a
|
|
6705
|
+
href={item.href}
|
|
6706
|
+
onClick={() => setOpenDropdown(null)}
|
|
6707
|
+
className="text-[11px] text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
|
6708
|
+
>
|
|
6709
|
+
View all {item.label.toLowerCase()} →
|
|
6710
|
+
</a>
|
|
6711
|
+
</div>
|
|
6712
|
+
</div>
|
|
6713
|
+
)}
|
|
6714
|
+
</div>
|
|
6715
|
+
);
|
|
6716
|
+
})}
|
|
6717
|
+
</nav>
|
|
6718
|
+
|
|
6719
|
+
{/* Actions */}
|
|
6720
|
+
<div className="hidden md:flex items-center gap-1 ml-auto shrink-0">
|
|
6721
|
+
<ThemeToggle />
|
|
6722
|
+
{data.signinHref && (
|
|
6723
|
+
<a
|
|
6724
|
+
href={data.signinHref}
|
|
6725
|
+
className="text-[13px] text-muted-foreground hover:text-foreground transition-colors duration-150 px-3 py-1.5 rounded-md hover:bg-elevated"
|
|
6726
|
+
>
|
|
6727
|
+
Sign in
|
|
6728
|
+
</a>
|
|
6729
|
+
)}
|
|
6730
|
+
{data.ctaHref && (
|
|
6731
|
+
<Button variant="accent" size="sm" className="h-8 px-4 text-[13px] font-medium" asChild>
|
|
6732
|
+
<a href={data.ctaHref}>{data.ctaLabel ?? 'Get started →'}</a>
|
|
6733
|
+
</Button>
|
|
6734
|
+
)}
|
|
6735
|
+
</div>
|
|
6736
|
+
|
|
6737
|
+
{/* Mobile toggle */}
|
|
6738
|
+
<button
|
|
6739
|
+
className="md:hidden ml-auto p-1.5 text-muted-foreground hover:text-foreground transition-colors"
|
|
6740
|
+
onClick={() => setMobileOpen(!mobileOpen)}
|
|
6741
|
+
aria-label="Toggle menu"
|
|
6742
|
+
>
|
|
6743
|
+
{mobileOpen ? <X size={16} /> : <Menu size={16} />}
|
|
6744
|
+
</button>
|
|
6745
|
+
</div>
|
|
6746
|
+
|
|
6747
|
+
{/* Mobile drawer */}
|
|
6748
|
+
<div className={cn(
|
|
6749
|
+
'md:hidden border-t border-border bg-card overflow-hidden transition-all duration-200',
|
|
6750
|
+
mobileOpen ? 'max-h-[32rem]' : 'max-h-0'
|
|
6751
|
+
)}>
|
|
6752
|
+
<nav className="px-4 py-3 flex flex-col gap-0.5">
|
|
6753
|
+
{navItems.map((item) => {
|
|
6754
|
+
const hasChildren = item.children && item.children.length > 0;
|
|
6755
|
+
const isExpanded = mobileExpanded === item.label;
|
|
6756
|
+
const isSecondary = item.variant === 'secondary';
|
|
6757
|
+
|
|
6758
|
+
if (isSecondary) {
|
|
6759
|
+
return (
|
|
6760
|
+
<a
|
|
6761
|
+
key={item.label}
|
|
6762
|
+
href={item.href}
|
|
6763
|
+
onClick={() => setMobileOpen(false)}
|
|
6764
|
+
className="mt-1 flex items-center px-3 py-2.5 text-[13px] text-muted-foreground hover:text-foreground border border-border bg-elevated hover:bg-elevated/70 rounded-md transition-colors"
|
|
6765
|
+
>
|
|
6766
|
+
{item.label}
|
|
6767
|
+
</a>
|
|
6768
|
+
);
|
|
6769
|
+
}
|
|
6770
|
+
|
|
6771
|
+
if (!hasChildren) {
|
|
6772
|
+
return (
|
|
6773
|
+
<a
|
|
6774
|
+
key={item.label}
|
|
6775
|
+
href={item.href}
|
|
6776
|
+
onClick={() => setMobileOpen(false)}
|
|
6777
|
+
className="flex items-center px-3 py-2.5 text-[13px] text-muted-foreground hover:text-foreground hover:bg-elevated rounded-md transition-colors"
|
|
6778
|
+
>
|
|
6779
|
+
{item.label}
|
|
6780
|
+
</a>
|
|
6781
|
+
);
|
|
6782
|
+
}
|
|
6783
|
+
|
|
6784
|
+
return (
|
|
6785
|
+
<div key={item.label}>
|
|
6786
|
+
<button
|
|
6787
|
+
type="button"
|
|
6788
|
+
onClick={() => {
|
|
6789
|
+
if (hasChildren) {
|
|
6790
|
+
setMobileExpanded(isExpanded ? null : item.label);
|
|
6791
|
+
}
|
|
6792
|
+
}}
|
|
6793
|
+
className="w-full flex items-center justify-between px-3 py-2.5 text-[13px] text-muted-foreground hover:text-foreground hover:bg-elevated rounded-md transition-colors text-left"
|
|
6794
|
+
>
|
|
6795
|
+
<span>{item.label}</span>
|
|
6796
|
+
{hasChildren && (
|
|
6797
|
+
<ChevronDown
|
|
6798
|
+
size={13}
|
|
6799
|
+
className={cn('opacity-40 transition-transform duration-150', isExpanded && 'rotate-180 opacity-70')}
|
|
6800
|
+
/>
|
|
6801
|
+
)}
|
|
6802
|
+
</button>
|
|
6803
|
+
|
|
6804
|
+
{hasChildren && isExpanded && (
|
|
6805
|
+
<div className="ml-3 pl-3 border-l border-border mt-0.5 mb-1 flex flex-col gap-0.5">
|
|
6806
|
+
{item.children!.map((child) => (
|
|
6807
|
+
<a
|
|
6808
|
+
key={child.label}
|
|
6809
|
+
href={child.href}
|
|
6810
|
+
onClick={() => { setMobileOpen(false); setMobileExpanded(null); }}
|
|
6811
|
+
className="flex items-center gap-2.5 px-3 py-2 text-[12px] text-muted-foreground hover:text-foreground hover:bg-elevated rounded-md transition-colors"
|
|
6812
|
+
>
|
|
6813
|
+
<span className="w-5 h-5 rounded bg-primary-900 border border-primary-800 flex items-center justify-center shrink-0 text-[9px] font-medium font-mono-olon text-primary-light">
|
|
6814
|
+
{child.label.slice(0, 2).toUpperCase()}
|
|
6815
|
+
</span>
|
|
6816
|
+
{child.label}
|
|
6817
|
+
</a>
|
|
6818
|
+
))}
|
|
6819
|
+
<a
|
|
6820
|
+
href={item.href}
|
|
6821
|
+
onClick={() => { setMobileOpen(false); setMobileExpanded(null); }}
|
|
6822
|
+
className="px-3 py-1.5 text-[11px] text-muted-foreground hover:text-foreground transition-colors"
|
|
6823
|
+
>
|
|
6824
|
+
View all →
|
|
6825
|
+
</a>
|
|
6826
|
+
</div>
|
|
6827
|
+
)}
|
|
6828
|
+
</div>
|
|
6829
|
+
);
|
|
6830
|
+
})}
|
|
6831
|
+
|
|
6832
|
+
<div className="flex gap-2 pt-3 mt-2 border-t border-border">
|
|
6833
|
+
{data.signinHref && (
|
|
6834
|
+
<Button variant="outline" size="sm" className="flex-1 text-[13px]" asChild>
|
|
6835
|
+
<a href={data.signinHref}>Sign in</a>
|
|
6836
|
+
</Button>
|
|
6837
|
+
)}
|
|
6838
|
+
{data.ctaHref && (
|
|
6839
|
+
<Button variant="accent" size="sm" className="flex-1 text-[13px]" asChild>
|
|
6840
|
+
<a href={data.ctaHref}>{data.ctaLabel ?? 'Get started'}</a>
|
|
6841
|
+
</Button>
|
|
6842
|
+
)}
|
|
6843
|
+
</div>
|
|
6844
|
+
</nav>
|
|
6845
|
+
</div>
|
|
6846
|
+
</header>
|
|
6847
|
+
);
|
|
6848
|
+
}
|
|
6849
6849
|
|
|
6850
6850
|
END_OF_FILE_CONTENT
|
|
6851
6851
|
echo "Creating src/components/header/index.ts..."
|
|
@@ -13277,6 +13277,55 @@ declare module '*?inline' {
|
|
|
13277
13277
|
|
|
13278
13278
|
|
|
13279
13279
|
|
|
13280
|
+
END_OF_FILE_CONTENT
|
|
13281
|
+
echo "Creating tsconfig.json..."
|
|
13282
|
+
cat << 'END_OF_FILE_CONTENT' > "tsconfig.json"
|
|
13283
|
+
{
|
|
13284
|
+
"compilerOptions": {
|
|
13285
|
+
"target": "ES2020",
|
|
13286
|
+
"useDefineForClassFields": true,
|
|
13287
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
13288
|
+
"module": "ESNext",
|
|
13289
|
+
"skipLibCheck": true,
|
|
13290
|
+
"moduleResolution": "bundler",
|
|
13291
|
+
"allowImportingTsExtensions": true,
|
|
13292
|
+
"resolveJsonModule": true,
|
|
13293
|
+
"isolatedModules": true,
|
|
13294
|
+
"noEmit": true,
|
|
13295
|
+
"jsx": "react-jsx",
|
|
13296
|
+
"strict": true,
|
|
13297
|
+
"noUnusedLocals": true,
|
|
13298
|
+
"noUnusedParameters": true,
|
|
13299
|
+
"noFallthroughCasesInSwitch": true,
|
|
13300
|
+
"baseUrl": ".",
|
|
13301
|
+
"paths": {
|
|
13302
|
+
"@/*": ["./src/*"],
|
|
13303
|
+
"@olonjs/core": ["../../packages/core/src/index.ts"]
|
|
13304
|
+
}
|
|
13305
|
+
},
|
|
13306
|
+
"include": ["src"],
|
|
13307
|
+
"exclude": ["src/emails"],
|
|
13308
|
+
"references": [{ "path": "./tsconfig.node.json" }]
|
|
13309
|
+
}
|
|
13310
|
+
|
|
13311
|
+
END_OF_FILE_CONTENT
|
|
13312
|
+
echo "Creating tsconfig.node.json..."
|
|
13313
|
+
cat << 'END_OF_FILE_CONTENT' > "tsconfig.node.json"
|
|
13314
|
+
{
|
|
13315
|
+
"compilerOptions": {
|
|
13316
|
+
"composite": true,
|
|
13317
|
+
"skipLibCheck": true,
|
|
13318
|
+
"module": "ESNext",
|
|
13319
|
+
"moduleResolution": "bundler",
|
|
13320
|
+
"allowSyntheticDefaultImports": true,
|
|
13321
|
+
"strict": true
|
|
13322
|
+
},
|
|
13323
|
+
"include": ["vite.config.ts"]
|
|
13324
|
+
}
|
|
13325
|
+
|
|
13326
|
+
|
|
13327
|
+
|
|
13328
|
+
|
|
13280
13329
|
END_OF_FILE_CONTENT
|
|
13281
13330
|
echo "Creating vercel.json..."
|
|
13282
13331
|
cat << 'END_OF_FILE_CONTENT' > "vercel.json"
|
|
@@ -13308,7 +13357,7 @@ END_OF_FILE_CONTENT
|
|
|
13308
13357
|
echo "Creating vite.config.ts..."
|
|
13309
13358
|
cat << 'END_OF_FILE_CONTENT' > "vite.config.ts"
|
|
13310
13359
|
/**
|
|
13311
|
-
* Generated by @
|
|
13360
|
+
* Generated by @jsonpages/cli. Dev server API: /api/save-to-file, /api/upload-asset, /api/list-assets.
|
|
13312
13361
|
*/
|
|
13313
13362
|
import { defineConfig } from 'vite';
|
|
13314
13363
|
import react from '@vitejs/plugin-react';
|
|
@@ -13322,8 +13371,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
13322
13371
|
const ASSETS_IMAGES_DIR = path.resolve(__dirname, 'public', 'assets', 'images');
|
|
13323
13372
|
const DATA_CONFIG_DIR = path.resolve(__dirname, 'src', 'data', 'config');
|
|
13324
13373
|
const DATA_PAGES_DIR = path.resolve(__dirname, 'src', 'data', 'pages');
|
|
13325
|
-
const MONOREPO_ROOT_DIR = path.resolve(__dirname, '../..');
|
|
13326
|
-
const CORE_SRC_INDEX = path.resolve(__dirname, '../../packages/core/src/index.ts');
|
|
13327
13374
|
const IMAGE_EXT = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.avif']);
|
|
13328
13375
|
const IMAGE_MIMES = new Set([
|
|
13329
13376
|
'image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml', 'image/avif',
|
|
@@ -13365,37 +13412,6 @@ function isTenantPageJsonRequest(req, pathname) {
|
|
|
13365
13412
|
const viteOrStaticPrefixes = ['/api/', '/assets/', '/src/', '/node_modules/', '/public/', '/@'];
|
|
13366
13413
|
return !viteOrStaticPrefixes.some((prefix) => pathname.startsWith(prefix));
|
|
13367
13414
|
}
|
|
13368
|
-
|
|
13369
|
-
function sanitizeNestedSlug(rawSlug) {
|
|
13370
|
-
const normalized = String(rawSlug || '')
|
|
13371
|
-
.replace(/\\/g, '/')
|
|
13372
|
-
.replace(/^\/+|\/+$/g, '');
|
|
13373
|
-
const segments = normalized
|
|
13374
|
-
.split('/')
|
|
13375
|
-
.map((segment) => segment.trim())
|
|
13376
|
-
.filter(Boolean)
|
|
13377
|
-
.map((segment) => segment.replace(/[^a-zA-Z0-9-_]/g, '_'))
|
|
13378
|
-
.filter((segment) => segment && segment !== '.' && segment !== '..');
|
|
13379
|
-
return segments.join('/');
|
|
13380
|
-
}
|
|
13381
|
-
|
|
13382
|
-
function writeJsonWithoutWatcher(server, targetPath, value) {
|
|
13383
|
-
const normalized = path.resolve(targetPath);
|
|
13384
|
-
try {
|
|
13385
|
-
server.watcher.unwatch(normalized);
|
|
13386
|
-
} catch {
|
|
13387
|
-
// best-effort
|
|
13388
|
-
}
|
|
13389
|
-
fs.mkdirSync(path.dirname(normalized), { recursive: true });
|
|
13390
|
-
fs.writeFileSync(normalized, JSON.stringify(value, null, 2), 'utf8');
|
|
13391
|
-
setTimeout(() => {
|
|
13392
|
-
try {
|
|
13393
|
-
server.watcher.add(normalized);
|
|
13394
|
-
} catch {
|
|
13395
|
-
// best-effort
|
|
13396
|
-
}
|
|
13397
|
-
}, 250);
|
|
13398
|
-
}
|
|
13399
13415
|
export default defineConfig({
|
|
13400
13416
|
plugins: [
|
|
13401
13417
|
react(),
|
|
@@ -13435,29 +13451,12 @@ export default defineConfig({
|
|
|
13435
13451
|
if (!projectState || typeof slug !== 'string') { sendJson(res, 400, { error: 'Missing projectState or slug' }); return; }
|
|
13436
13452
|
if (!fs.existsSync(DATA_CONFIG_DIR)) fs.mkdirSync(DATA_CONFIG_DIR, { recursive: true });
|
|
13437
13453
|
if (!fs.existsSync(DATA_PAGES_DIR)) fs.mkdirSync(DATA_PAGES_DIR, { recursive: true });
|
|
13438
|
-
|
|
13439
|
-
if (projectState.
|
|
13440
|
-
|
|
13441
|
-
writeJsonWithoutWatcher(server, sitePath, projectState.site);
|
|
13442
|
-
touchedFiles.push(sitePath);
|
|
13443
|
-
}
|
|
13444
|
-
if (projectState.theme != null) {
|
|
13445
|
-
const themePath = path.join(DATA_CONFIG_DIR, 'theme.json');
|
|
13446
|
-
writeJsonWithoutWatcher(server, themePath, projectState.theme);
|
|
13447
|
-
touchedFiles.push(themePath);
|
|
13448
|
-
}
|
|
13449
|
-
if (projectState.menu != null) {
|
|
13450
|
-
const menuPath = path.join(DATA_CONFIG_DIR, 'menu.json');
|
|
13451
|
-
writeJsonWithoutWatcher(server, menuPath, projectState.menu);
|
|
13452
|
-
touchedFiles.push(menuPath);
|
|
13453
|
-
}
|
|
13454
|
+
if (projectState.site != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'site.json'), JSON.stringify(projectState.site, null, 2), 'utf8');
|
|
13455
|
+
if (projectState.theme != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'theme.json'), JSON.stringify(projectState.theme, null, 2), 'utf8');
|
|
13456
|
+
if (projectState.menu != null) fs.writeFileSync(path.join(DATA_CONFIG_DIR, 'menu.json'), JSON.stringify(projectState.menu, null, 2), 'utf8');
|
|
13454
13457
|
if (projectState.page != null) {
|
|
13455
|
-
const safeSlug =
|
|
13456
|
-
|
|
13457
|
-
const isInsidePagesDir = pagePath.startsWith(`${DATA_PAGES_DIR}${path.sep}`) || pagePath === DATA_PAGES_DIR;
|
|
13458
|
-
if (!isInsidePagesDir) { sendJson(res, 400, { error: 'Invalid page slug path' }); return; }
|
|
13459
|
-
writeJsonWithoutWatcher(server, pagePath, projectState.page);
|
|
13460
|
-
touchedFiles.push(pagePath);
|
|
13458
|
+
const safeSlug = (slug.replace(/[^a-zA-Z0-9-_]/g, '_') || 'page');
|
|
13459
|
+
fs.writeFileSync(path.join(DATA_PAGES_DIR, `${safeSlug}.json`), JSON.stringify(projectState.page, null, 2), 'utf8');
|
|
13461
13460
|
}
|
|
13462
13461
|
sendJson(res, 200, { ok: true });
|
|
13463
13462
|
} catch (e) { sendJson(res, 500, { error: e?.message || 'Save to file failed' }); }
|
|
@@ -13487,26 +13486,16 @@ export default defineConfig({
|
|
|
13487
13486
|
},
|
|
13488
13487
|
},
|
|
13489
13488
|
],
|
|
13490
|
-
server: {
|
|
13491
|
-
fs: {
|
|
13492
|
-
allow: [MONOREPO_ROOT_DIR],
|
|
13493
|
-
},
|
|
13494
|
-
},
|
|
13495
|
-
optimizeDeps: {
|
|
13496
|
-
exclude: ['@olonjs/core', '@jsonpages/core'],
|
|
13497
|
-
},
|
|
13498
13489
|
resolve: {
|
|
13499
|
-
dedupe: ['react', 'react-dom'],
|
|
13500
13490
|
alias: {
|
|
13501
13491
|
'@': path.resolve(__dirname, './src'),
|
|
13502
|
-
'
|
|
13503
|
-
'@jsonpages/core': CORE_SRC_INDEX,
|
|
13492
|
+
'next/link': path.resolve(__dirname, './src/shims/next-link.tsx'),
|
|
13504
13493
|
},
|
|
13505
13494
|
},
|
|
13506
13495
|
});
|
|
13507
13496
|
|
|
13508
|
-
|
|
13509
|
-
|
|
13510
|
-
|
|
13497
|
+
|
|
13498
|
+
|
|
13499
|
+
|
|
13511
13500
|
|
|
13512
13501
|
END_OF_FILE_CONTENT
|