File size: 4,795 Bytes
34b0226
013ff92
00a4363
aeea10c
00a4363
aeea10c
00a4363
aeea10c
00a4363
 
e1222dc
fa6e4a3
00a4363
 
 
 
 
b74c6ae
 
00a4363
b74c6ae
e1222dc
bdd1640
fa6e4a3
00a4363
 
34b0226
 
 
 
 
 
 
 
 
 
00a4363
1e1198f
013ff92
b74c6ae
34b0226
 
 
 
 
 
 
 
 
 
00a4363
1e1198f
00a4363
 
aeea10c
b74c6ae
aeea10c
 
 
 
 
8e2a6eb
aeea10c
 
 
8e2a6eb
aeea10c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23cef78
aeea10c
 
3c705c5
 
 
 
 
 
 
 
aeea10c
 
 
 
 
 
3c705c5
 
 
 
aeea10c
 
1e1198f
aeea10c
 
 
 
 
 
 
 
 
 
 
 
00a4363
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import { useState } from "react";
import { NavLink, Outlet, Navigate, useLocation } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";
import { UpgradeProvider } from "../hooks/useUpgrade";
import Logo from "./Logo";
import { FuelGauge, NudgeDock } from "./Nudges";
import SkyCanvas from "./SkyCanvas";
import UpgradeModal from "./UpgradeModal";
import {
  LaunchpadIcon,
  LibraryIcon,
  MicIcon,
  OrbitIcon,
  PlanetIcon,
  SatelliteDishIcon,
} from "./icons/Icons";

// Keep the primary navigation focused; related pages remain reachable through these destinations.
const NAV: { to: string; label: string; icon: typeof PlanetIcon; end?: boolean; also?: string[] }[] = [
  { to: "/app/notebooks", label: "Notebooks", icon: PlanetIcon },
  { to: "/app/sources", label: "Sources", icon: SatelliteDishIcon, also: ["/app/map"] },
  { to: "/app/archive", label: "Library", icon: LibraryIcon, also: ["/app/videos"] },
  { to: "/app/launchpad", label: "Launchpad", icon: LaunchpadIcon, also: ["/app/resurface", "/app/launch-kit"] },
  { to: "/app/voices", label: "Manage voices", icon: MicIcon },
];

const COLLAPSED_KEY = "ns_sidebar_collapsed";

function readCollapsed(): boolean {
  try {
    return localStorage.getItem(COLLAPSED_KEY) === "1";
  } catch {
    return false;
  }
}

export default function AppShell() {
  const { user, loading, signingOut, logout } = useAuth();
  const location = useLocation();
  const notebookFocus = /^\/app\/notebooks\/[^/]+$/.test(location.pathname);
  const [collapsed, setCollapsed] = useState(readCollapsed);
  const toggle = () =>
    setCollapsed((c) => {
      try {
        localStorage.setItem(COLLAPSED_KEY, c ? "0" : "1");
      } catch {
        /* private mode: remember for this visit only */
      }
      return !c;
    });
  if (loading) return <div className="boot"><div className="orbit-loader"><span /></div></div>;
  if (!user) return <Navigate to="/" replace />;

  return (
    <UpgradeProvider>
      <div className={`shell${collapsed ? " collapsed" : ""}${notebookFocus ? " notebook-focus" : ""}`}>
        <SkyCanvas intensity={0.35} />
        <aside className="sidebar">
          <div className="sidebar-top">
            {collapsed ? (
              <button type="button" className="sidebar-brand sidebar-brand-btn" onClick={toggle} aria-label="Expand sidebar" title="Expand sidebar">
                <Logo size={28} withWordmark={false} />
              </button>
            ) : (
              <NavLink to="/" className="sidebar-brand">
                <Logo size={28} withWordmark={false} />
              </NavLink>
            )}
            <button
              type="button"
              className="icon-btn sidebar-toggle"
              onClick={toggle}
              aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
              aria-expanded={!collapsed}
              title={collapsed ? "Expand sidebar" : "Collapse sidebar"}
            >
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <rect x="3.5" y="4.5" width="17" height="15" rx="2.5" />
                <path d="M9 4.5v15" />
                <path d={collapsed ? "M13 10l2 2-2 2" : "M15 10l-2 2 2 2"} />
              </svg>
            </button>
          </div>
          <nav aria-label="App">
            {NAV.map(({ to, label, icon: Icon, end, also }) => (
              <NavLink
                key={to}
                to={to}
                end={end}
                title={collapsed ? label : undefined}
                className={({ isActive }) => `side-link${isActive || also?.some((p) => location.pathname.startsWith(p)) ? " active" : ""}`}
              >
                <Icon />
                <span className="side-label">{label}</span>
              </NavLink>
            ))}
          </nav>
          <FuelGauge collapsed={collapsed} />
          <NavLink to="/app/settings" title={collapsed ? "Settings" : undefined} className={({ isActive }) => `side-link side-settings${isActive ? " active" : ""}`}>
            <OrbitIcon />
            <span className="side-label">Settings</span>
          </NavLink>
          <div className="sidebar-user">
            <span className="mono muted side-label">{user.email}</span>
            <button className="link-btn" onClick={logout} disabled={signingOut} title="Sign out">
              Sign out
            </button>
          </div>
        </aside>
        <main className="shell-main">
          <Outlet />
        </main>
      </div>
      {/* Outside .shell: its children get position: relative, and these are fixed overlays. */}
      <NudgeDock />
      <UpgradeModal />
    </UpgradeProvider>
  );
}