@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.80",
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 @olonjs/cli. Dev server API: /api/save-to-file, /api/upload-asset, /api/list-assets.
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
- const touchedFiles = [];
13439
- if (projectState.site != null) {
13440
- const sitePath = path.join(DATA_CONFIG_DIR, 'site.json');
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 = sanitizeNestedSlug(slug) || 'page';
13456
- const pagePath = path.resolve(DATA_PAGES_DIR, `${safeSlug}.json`);
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
- '@olonjs/core': CORE_SRC_INDEX,
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