@olonjs/cli 3.0.93 → 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.81",
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"