create-ab-app 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/index.js +380 -0
- package/package.json +36 -0
- package/template/.env.example +8 -0
- package/template/.oxlintrc.json +8 -0
- package/template/README.md +196 -0
- package/template/_gitignore +24 -0
- package/template/components.json +25 -0
- package/template/index.html +13 -0
- package/template/package-lock.json +6096 -0
- package/template/package.json +40 -0
- package/template/public/_redirects +1 -0
- package/template/public/favicon.svg +1 -0
- package/template/public/icons.svg +24 -0
- package/template/src/App.tsx +60 -0
- package/template/src/assets/hero.png +0 -0
- package/template/src/assets/vite.svg +1 -0
- package/template/src/components/app-sidebar.tsx +53 -0
- package/template/src/components/credit-balance.tsx +31 -0
- package/template/src/components/docs/doc-primitives.tsx +125 -0
- package/template/src/components/full-page-loader.tsx +14 -0
- package/template/src/components/header-user.tsx +64 -0
- package/template/src/components/layouts/app-layout.tsx +80 -0
- package/template/src/components/missing-env.tsx +24 -0
- package/template/src/components/nav-main.tsx +87 -0
- package/template/src/components/nav-user.tsx +69 -0
- package/template/src/components/theme-toggle.tsx +39 -0
- package/template/src/components/ui/avatar.tsx +106 -0
- package/template/src/components/ui/breadcrumb.tsx +124 -0
- package/template/src/components/ui/button.tsx +57 -0
- package/template/src/components/ui/card.tsx +102 -0
- package/template/src/components/ui/collapsible.tsx +19 -0
- package/template/src/components/ui/dropdown-menu.tsx +267 -0
- package/template/src/components/ui/field.tsx +238 -0
- package/template/src/components/ui/input.tsx +19 -0
- package/template/src/components/ui/label.tsx +19 -0
- package/template/src/components/ui/separator.tsx +22 -0
- package/template/src/components/ui/sheet.tsx +136 -0
- package/template/src/components/ui/sidebar.tsx +721 -0
- package/template/src/components/ui/skeleton.tsx +13 -0
- package/template/src/components/ui/sonner.tsx +47 -0
- package/template/src/components/ui/tooltip.tsx +65 -0
- package/template/src/components/user-menu.tsx +96 -0
- package/template/src/config/env.ts +14 -0
- package/template/src/config/navigation.ts +52 -0
- package/template/src/hooks/use-mobile.ts +19 -0
- package/template/src/hooks/use-user-identity.ts +27 -0
- package/template/src/index.css +134 -0
- package/template/src/lib/http.ts +40 -0
- package/template/src/lib/sso/account-settings.tsx +53 -0
- package/template/src/lib/sso/auth-context.ts +16 -0
- package/template/src/lib/sso/auth-provider.tsx +42 -0
- package/template/src/lib/sso/auth-screens.tsx +152 -0
- package/template/src/lib/sso/config.ts +13 -0
- package/template/src/lib/sso/entitlements.ts +101 -0
- package/template/src/lib/sso/index.ts +33 -0
- package/template/src/lib/sso/protected-route.tsx +72 -0
- package/template/src/lib/sso/queries.ts +55 -0
- package/template/src/lib/sso/require-plan.tsx +19 -0
- package/template/src/lib/sso/token-store.ts +62 -0
- package/template/src/lib/utils.ts +1 -0
- package/template/src/main.tsx +42 -0
- package/template/src/routes/app/example.tsx +125 -0
- package/template/src/routes/auth/forgot-password.tsx +5 -0
- package/template/src/routes/auth/login.tsx +5 -0
- package/template/src/routes/auth/signup.tsx +5 -0
- package/template/src/routes/auth/verify-email.tsx +5 -0
- package/template/src/routes/docs/api.tsx +66 -0
- package/template/src/routes/docs/auth.tsx +145 -0
- package/template/src/routes/docs/billing.tsx +103 -0
- package/template/src/routes/docs/configuration.tsx +113 -0
- package/template/src/routes/docs/overview.tsx +97 -0
- package/template/src/routes/docs/routing.tsx +126 -0
- package/template/src/routes/docs/structure.tsx +150 -0
- package/template/src/routes/docs/theming.tsx +88 -0
- package/template/src/routes/not-found.tsx +12 -0
- package/template/src/styles/aas-theme.css +21 -0
- package/template/src/vite-env.d.ts +12 -0
- package/template/tsconfig.app.json +30 -0
- package/template/tsconfig.json +12 -0
- package/template/tsconfig.node.json +23 -0
- package/template/vercel.json +3 -0
- package/template/vite.config.ts +17 -0
- package/variants/minimal/src/App.tsx +43 -0
- package/variants/minimal/src/components/app-sidebar.tsx +50 -0
- package/variants/minimal/src/config/navigation.ts +19 -0
- package/variants/minimal/src/routes/app/home.tsx +45 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
|
3
|
+
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
|
4
|
+
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
|
5
|
+
</symbol>
|
|
6
|
+
<symbol id="discord-icon" viewBox="0 0 20 19">
|
|
7
|
+
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
|
8
|
+
</symbol>
|
|
9
|
+
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
|
10
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
|
11
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
|
12
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
|
13
|
+
</symbol>
|
|
14
|
+
<symbol id="github-icon" viewBox="0 0 19 19">
|
|
15
|
+
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
|
16
|
+
</symbol>
|
|
17
|
+
<symbol id="social-icon" viewBox="0 0 20 20">
|
|
18
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
|
19
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
|
20
|
+
</symbol>
|
|
21
|
+
<symbol id="x-icon" viewBox="0 0 19 19">
|
|
22
|
+
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
|
23
|
+
</symbol>
|
|
24
|
+
</svg>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Navigate, Route, Routes } from "react-router-dom"
|
|
2
|
+
|
|
3
|
+
import { AppLayout } from "@/components/layouts/app-layout"
|
|
4
|
+
import { ProtectedRoute, PublicOnlyRoute } from "@/lib/sso"
|
|
5
|
+
import ForgotPasswordPage from "@/routes/auth/forgot-password"
|
|
6
|
+
import LoginPage from "@/routes/auth/login"
|
|
7
|
+
import SignupPage from "@/routes/auth/signup"
|
|
8
|
+
import VerifyEmailPage from "@/routes/auth/verify-email"
|
|
9
|
+
import ExamplePage from "@/routes/app/example"
|
|
10
|
+
import ApiPage from "@/routes/docs/api"
|
|
11
|
+
import AuthDocPage from "@/routes/docs/auth"
|
|
12
|
+
import BillingPage from "@/routes/docs/billing"
|
|
13
|
+
import ConfigurationPage from "@/routes/docs/configuration"
|
|
14
|
+
import OverviewPage from "@/routes/docs/overview"
|
|
15
|
+
import RoutingPage from "@/routes/docs/routing"
|
|
16
|
+
import StructurePage from "@/routes/docs/structure"
|
|
17
|
+
import ThemingPage from "@/routes/docs/theming"
|
|
18
|
+
import NotFoundPage from "@/routes/not-found"
|
|
19
|
+
|
|
20
|
+
export default function App() {
|
|
21
|
+
return (
|
|
22
|
+
<Routes>
|
|
23
|
+
{/* Auth — no layout wrapper: each SDK screen renders its own centered page. */}
|
|
24
|
+
<Route element={<PublicOnlyRoute />}>
|
|
25
|
+
<Route path="/login" element={<LoginPage />} />
|
|
26
|
+
<Route path="/signup" element={<SignupPage />} />
|
|
27
|
+
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
|
|
28
|
+
</Route>
|
|
29
|
+
|
|
30
|
+
{/*
|
|
31
|
+
Emailed links. Not behind PublicOnlyRoute: a user who just signed up is
|
|
32
|
+
already signed in, and must still be able to verify or reset.
|
|
33
|
+
*/}
|
|
34
|
+
<Route path="/reset-password/:token" element={<ForgotPasswordPage />} />
|
|
35
|
+
<Route path="/verify/:token" element={<VerifyEmailPage />} />
|
|
36
|
+
{/* Aliases for links that carry the token as ?token= instead. */}
|
|
37
|
+
<Route path="/reset-password" element={<ForgotPasswordPage />} />
|
|
38
|
+
<Route path="/verify-email" element={<VerifyEmailPage />} />
|
|
39
|
+
|
|
40
|
+
{/* Add enforceActiveSubscription for a hard paywall on the whole shell. */}
|
|
41
|
+
<Route element={<ProtectedRoute />}>
|
|
42
|
+
<Route element={<AppLayout />}>
|
|
43
|
+
{/* Replace these docs routes with your product's pages. */}
|
|
44
|
+
<Route path="/" element={<OverviewPage />} />
|
|
45
|
+
<Route path="/docs/structure" element={<StructurePage />} />
|
|
46
|
+
<Route path="/docs/configuration" element={<ConfigurationPage />} />
|
|
47
|
+
<Route path="/docs/auth" element={<AuthDocPage />} />
|
|
48
|
+
<Route path="/docs/billing" element={<BillingPage />} />
|
|
49
|
+
<Route path="/docs/routing" element={<RoutingPage />} />
|
|
50
|
+
<Route path="/docs/theming" element={<ThemingPage />} />
|
|
51
|
+
<Route path="/docs/api" element={<ApiPage />} />
|
|
52
|
+
<Route path="/example" element={<ExamplePage />} />
|
|
53
|
+
</Route>
|
|
54
|
+
</Route>
|
|
55
|
+
|
|
56
|
+
<Route path="/404" element={<NotFoundPage />} />
|
|
57
|
+
<Route path="*" element={<Navigate to="/404" replace />} />
|
|
58
|
+
</Routes>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="77" height="47" fill="none" aria-labelledby="vite-logo-title" viewBox="0 0 77 47"><title id="vite-logo-title">Vite</title><style>.parenthesis{fill:#000}@media (prefers-color-scheme:dark){.parenthesis{fill:#fff}}</style><path fill="#9135ff" d="M40.151 45.71c-.663.844-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.493c-.92 0-1.457-1.04-.92-1.788l7.479-10.471c1.07-1.498 0-3.578-1.842-3.578H15.443c-.92 0-1.456-1.04-.92-1.788l9.696-13.576c.213-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.472c-1.07 1.497 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.087.89 1.83L40.153 45.712z"/><mask id="a" width="48" height="47" x="14" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M40.047 45.71c-.663.843-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.389c-.92 0-1.457-1.04-.92-1.788l7.479-10.472c1.07-1.497 0-3.578-1.842-3.578H15.34c-.92 0-1.456-1.04-.92-1.788l9.696-13.575c.213-.297.556-.474.92-.474H53.93c.92 0 1.456 1.04.92 1.788L47.37 13.03c-1.07 1.498 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.088.89 1.831L40.049 45.712z"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#eee6ff" rx="5.508" ry="14.704" transform="rotate(269.814 20.96 11.29)scale(-1 1)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#eee6ff" rx="10.399" ry="29.851" transform="rotate(89.814 -16.902 -8.275)scale(1 -1)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#8900ff" rx="5.508" ry="30.487" transform="rotate(89.814 -19.197 -7.127)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.928 4.177)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.738 5.52)scale(1 -1)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#eee6ff" rx="14.072" ry="22.078" transform="rotate(93.35 31.245 55.578)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx="14.592" cy="9.743" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(39.51 14.592 9.743)"/></g><g filter="url(#k)"><ellipse cx="61.728" cy="-5.321" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 61.728 -5.32)"/></g><g filter="url(#l)"><ellipse cx="55.618" cy="7.104" fill="#00c2ff" rx="5.971" ry="9.665" transform="rotate(37.892 55.618 7.104)"/></g><g filter="url(#m)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#n)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#o)"><ellipse cx="49.857" cy="30.678" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 49.857 30.678)"/></g><g filter="url(#p)"><ellipse cx="52.623" cy="33.171" fill="#00c2ff" rx="5.971" ry="15.297" transform="rotate(37.892 52.623 33.17)"/></g></g><path d="M6.919 0c-9.198 13.166-9.252 33.575 0 46.789h6.215c-9.25-13.214-9.196-33.623 0-46.789zm62.424 0h-6.215c9.198 13.166 9.252 33.575 0 46.789h6.215c9.25-13.214 9.196-33.623 0-46.789" class="parenthesis"/><defs><filter id="b" width="60.045" height="41.654" x="-5.564" y="16.92" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-40.407" y="-6.762" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-35.435" y="2.801" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-30.84" y="20.8" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-29.307" y="21.949" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="29.961" y="-17.13" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-13.43" y="-22.082" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="34.321" y="-37.644" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="38.847" y="-10.552" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="22.45" y="-1.645" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="32.919" y="11.36" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter></defs></svg>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Link } from "react-router-dom"
|
|
3
|
+
import { GalleryVerticalEndIcon } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
import { NavMain } from "@/components/nav-main"
|
|
6
|
+
import { NavUser } from "@/components/nav-user"
|
|
7
|
+
import { CreditBalance } from "@/components/credit-balance"
|
|
8
|
+
import {
|
|
9
|
+
Sidebar,
|
|
10
|
+
SidebarContent,
|
|
11
|
+
SidebarFooter,
|
|
12
|
+
SidebarHeader,
|
|
13
|
+
SidebarMenu,
|
|
14
|
+
SidebarMenuButton,
|
|
15
|
+
SidebarMenuItem,
|
|
16
|
+
SidebarRail,
|
|
17
|
+
} from "@/components/ui/sidebar"
|
|
18
|
+
import { env } from "@/config/env"
|
|
19
|
+
import { navExamples, navGettingStarted, navGuides } from "@/config/navigation"
|
|
20
|
+
|
|
21
|
+
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
22
|
+
return (
|
|
23
|
+
<Sidebar collapsible="icon" {...props}>
|
|
24
|
+
<SidebarHeader>
|
|
25
|
+
<SidebarMenu>
|
|
26
|
+
<SidebarMenuItem>
|
|
27
|
+
<SidebarMenuButton size="lg" render={<Link to="/" />}>
|
|
28
|
+
<div className="bg-primary text-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
|
29
|
+
<GalleryVerticalEndIcon className="size-4" />
|
|
30
|
+
</div>
|
|
31
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
32
|
+
<span className="truncate font-medium">{env.appName}</span>
|
|
33
|
+
<span className="truncate text-xs">Template docs</span>
|
|
34
|
+
</div>
|
|
35
|
+
</SidebarMenuButton>
|
|
36
|
+
</SidebarMenuItem>
|
|
37
|
+
</SidebarMenu>
|
|
38
|
+
</SidebarHeader>
|
|
39
|
+
|
|
40
|
+
<SidebarContent>
|
|
41
|
+
<NavMain items={navGettingStarted} label="Getting started" />
|
|
42
|
+
<NavMain items={navGuides} label="Guides" />
|
|
43
|
+
<NavMain items={navExamples} label="Examples" />
|
|
44
|
+
</SidebarContent>
|
|
45
|
+
|
|
46
|
+
<SidebarFooter>
|
|
47
|
+
<CreditBalance />
|
|
48
|
+
<NavUser />
|
|
49
|
+
</SidebarFooter>
|
|
50
|
+
<SidebarRail />
|
|
51
|
+
</Sidebar>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CoinsIcon } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
SidebarMenu,
|
|
5
|
+
SidebarMenuButton,
|
|
6
|
+
SidebarMenuItem,
|
|
7
|
+
} from "@/components/ui/sidebar"
|
|
8
|
+
import { useAccountSettings, useCreditWallets } from "@/lib/sso"
|
|
9
|
+
|
|
10
|
+
/** Metered balance in the sidebar footer; click to top up. */
|
|
11
|
+
export function CreditBalance() {
|
|
12
|
+
const { data: wallets } = useCreditWallets()
|
|
13
|
+
const { open } = useAccountSettings()
|
|
14
|
+
|
|
15
|
+
const wallet = wallets?.[0]
|
|
16
|
+
if (!wallet) return null
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<SidebarMenu>
|
|
20
|
+
<SidebarMenuItem>
|
|
21
|
+
<SidebarMenuButton
|
|
22
|
+
tooltip={`${wallet.balanceFormatted} — ${wallet.label}`}
|
|
23
|
+
onClick={() => open("credits")}
|
|
24
|
+
>
|
|
25
|
+
<CoinsIcon />
|
|
26
|
+
<span className="truncate">{wallet.balanceFormatted}</span>
|
|
27
|
+
</SidebarMenuButton>
|
|
28
|
+
</SidebarMenuItem>
|
|
29
|
+
</SidebarMenu>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils"
|
|
2
|
+
|
|
3
|
+
/** Page wrapper — title, one-line summary, and a readable measure. */
|
|
4
|
+
export function DocPage({
|
|
5
|
+
title,
|
|
6
|
+
summary,
|
|
7
|
+
children,
|
|
8
|
+
}: {
|
|
9
|
+
title: string
|
|
10
|
+
summary?: string
|
|
11
|
+
children: React.ReactNode
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<article className="mx-auto flex w-full max-w-3xl flex-col gap-8 pb-16">
|
|
15
|
+
<header className="flex flex-col gap-2">
|
|
16
|
+
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
|
17
|
+
{summary && <p className="text-muted-foreground text-sm">{summary}</p>}
|
|
18
|
+
</header>
|
|
19
|
+
{children}
|
|
20
|
+
</article>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Section({
|
|
25
|
+
title,
|
|
26
|
+
children,
|
|
27
|
+
}: {
|
|
28
|
+
title?: string
|
|
29
|
+
children: React.ReactNode
|
|
30
|
+
}) {
|
|
31
|
+
return (
|
|
32
|
+
<section className="flex flex-col gap-3">
|
|
33
|
+
{title && <h2 className="text-base font-semibold tracking-tight">{title}</h2>}
|
|
34
|
+
{children}
|
|
35
|
+
</section>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function P({ children }: { children: React.ReactNode }) {
|
|
40
|
+
return <p className="text-muted-foreground text-sm leading-6">{children}</p>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function Code({ children }: { children: React.ReactNode }) {
|
|
44
|
+
return (
|
|
45
|
+
<code className="bg-muted text-foreground rounded px-1.5 py-0.5 font-mono text-[0.8em]">
|
|
46
|
+
{children}
|
|
47
|
+
</code>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function CodeBlock({ children }: { children: string }) {
|
|
52
|
+
return (
|
|
53
|
+
<pre className="bg-muted overflow-x-auto rounded-lg border p-4 font-mono text-xs leading-5">
|
|
54
|
+
<code>{children}</code>
|
|
55
|
+
</pre>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Takes <li> children, so callers never hand-key an array. */
|
|
60
|
+
export function List({ children }: { children: React.ReactNode }) {
|
|
61
|
+
return (
|
|
62
|
+
<ul className="text-muted-foreground flex list-disc flex-col gap-2 pl-5 text-sm leading-6">
|
|
63
|
+
{children}
|
|
64
|
+
</ul>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Takes <Row>/<Cell> children, for the same reason. */
|
|
69
|
+
export function Table({
|
|
70
|
+
head,
|
|
71
|
+
children,
|
|
72
|
+
}: {
|
|
73
|
+
head: string[]
|
|
74
|
+
children: React.ReactNode
|
|
75
|
+
}) {
|
|
76
|
+
return (
|
|
77
|
+
<div className="overflow-x-auto rounded-lg border">
|
|
78
|
+
<table className="w-full text-left text-sm">
|
|
79
|
+
<thead className="bg-muted/50">
|
|
80
|
+
<tr>
|
|
81
|
+
{head.map((cell) => (
|
|
82
|
+
<th key={cell} className="px-3 py-2 font-medium whitespace-nowrap">
|
|
83
|
+
{cell}
|
|
84
|
+
</th>
|
|
85
|
+
))}
|
|
86
|
+
</tr>
|
|
87
|
+
</thead>
|
|
88
|
+
<tbody>{children}</tbody>
|
|
89
|
+
</table>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function Row({ children }: { children: React.ReactNode }) {
|
|
95
|
+
return <tr className="border-t">{children}</tr>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function Cell({ children }: { children: React.ReactNode }) {
|
|
99
|
+
return <td className="text-muted-foreground px-3 py-2 align-top">{children}</td>
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Callout for the things that cost people an afternoon. */
|
|
103
|
+
export function Note({
|
|
104
|
+
tone = "info",
|
|
105
|
+
title,
|
|
106
|
+
children,
|
|
107
|
+
}: {
|
|
108
|
+
tone?: "info" | "warn"
|
|
109
|
+
title?: string
|
|
110
|
+
children: React.ReactNode
|
|
111
|
+
}) {
|
|
112
|
+
return (
|
|
113
|
+
<div
|
|
114
|
+
className={cn(
|
|
115
|
+
"rounded-lg border-l-4 px-4 py-3 text-sm leading-6",
|
|
116
|
+
tone === "warn"
|
|
117
|
+
? "border-l-destructive bg-destructive/5"
|
|
118
|
+
: "border-l-primary bg-muted/50",
|
|
119
|
+
)}
|
|
120
|
+
>
|
|
121
|
+
{title && <p className="mb-1 font-medium">{title}</p>}
|
|
122
|
+
<div className="text-muted-foreground">{children}</div>
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Loader2Icon } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Covers the gap where SessionProvider renders null while it validates the
|
|
5
|
+
* token, which would otherwise show a blank page on every protected load.
|
|
6
|
+
*/
|
|
7
|
+
export function FullPageLoader({ label = "Loading…" }: { label?: string }) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="bg-background fixed inset-0 z-50 flex flex-col items-center justify-center gap-3">
|
|
10
|
+
<Loader2Icon className="text-muted-foreground size-6 animate-spin" />
|
|
11
|
+
<p className="text-muted-foreground text-sm">{label}</p>
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ChevronsUpDownIcon } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button"
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuGroup,
|
|
8
|
+
DropdownMenuLabel,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from "@/components/ui/dropdown-menu"
|
|
11
|
+
import { Skeleton } from "@/components/ui/skeleton"
|
|
12
|
+
import { UserAvatar, UserIdentity, UserMenuItems } from "@/components/user-menu"
|
|
13
|
+
import { useUserIdentity } from "@/hooks/use-user-identity"
|
|
14
|
+
|
|
15
|
+
/** Same menu as the sidebar footer, reachable from the header. */
|
|
16
|
+
export function HeaderUser() {
|
|
17
|
+
const { isPending, name, email } = useUserIdentity()
|
|
18
|
+
|
|
19
|
+
if (isPending) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex items-center gap-2">
|
|
22
|
+
<Skeleton className="size-7 rounded-full" />
|
|
23
|
+
<div className="hidden gap-1 sm:grid">
|
|
24
|
+
<Skeleton className="h-3 w-20" />
|
|
25
|
+
<Skeleton className="h-3 w-28" />
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<DropdownMenu>
|
|
33
|
+
<DropdownMenuTrigger
|
|
34
|
+
render={
|
|
35
|
+
<Button
|
|
36
|
+
variant="ghost"
|
|
37
|
+
className="h-auto gap-2 px-2 py-1"
|
|
38
|
+
aria-label={name || "Account"}
|
|
39
|
+
/>
|
|
40
|
+
}
|
|
41
|
+
>
|
|
42
|
+
<UserAvatar className="size-7" />
|
|
43
|
+
{/* Hidden on narrow screens, where the avatar alone is the trigger. */}
|
|
44
|
+
<div className="hidden max-w-44 min-w-0 text-left leading-tight sm:grid">
|
|
45
|
+
<span className="truncate text-sm font-medium">{name}</span>
|
|
46
|
+
<span className="text-muted-foreground truncate text-xs font-normal">
|
|
47
|
+
{email}
|
|
48
|
+
</span>
|
|
49
|
+
</div>
|
|
50
|
+
<ChevronsUpDownIcon className="size-4 opacity-60" />
|
|
51
|
+
</DropdownMenuTrigger>
|
|
52
|
+
<DropdownMenuContent className="w-56" align="end" sideOffset={8}>
|
|
53
|
+
<DropdownMenuGroup>
|
|
54
|
+
<DropdownMenuLabel className="p-0 font-normal">
|
|
55
|
+
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
|
56
|
+
<UserIdentity />
|
|
57
|
+
</div>
|
|
58
|
+
</DropdownMenuLabel>
|
|
59
|
+
</DropdownMenuGroup>
|
|
60
|
+
<UserMenuItems />
|
|
61
|
+
</DropdownMenuContent>
|
|
62
|
+
</DropdownMenu>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Outlet, useLocation } from "react-router-dom"
|
|
2
|
+
|
|
3
|
+
import { AppSidebar } from "@/components/app-sidebar"
|
|
4
|
+
import { HeaderUser } from "@/components/header-user"
|
|
5
|
+
import { ThemeToggle } from "@/components/theme-toggle"
|
|
6
|
+
import {
|
|
7
|
+
Breadcrumb,
|
|
8
|
+
BreadcrumbItem,
|
|
9
|
+
BreadcrumbList,
|
|
10
|
+
BreadcrumbPage,
|
|
11
|
+
BreadcrumbSeparator,
|
|
12
|
+
} from "@/components/ui/breadcrumb"
|
|
13
|
+
import { Separator } from "@/components/ui/separator"
|
|
14
|
+
import {
|
|
15
|
+
SidebarInset,
|
|
16
|
+
SidebarProvider,
|
|
17
|
+
SidebarTrigger,
|
|
18
|
+
} from "@/components/ui/sidebar"
|
|
19
|
+
import { env } from "@/config/env"
|
|
20
|
+
import { allNavItems } from "@/config/navigation"
|
|
21
|
+
import { useRestoreAccountSettings } from "@/lib/sso"
|
|
22
|
+
|
|
23
|
+
/** Prefers the nav item's own title, falling back to the URL segments. */
|
|
24
|
+
function useBreadcrumbs() {
|
|
25
|
+
const { pathname } = useLocation()
|
|
26
|
+
const match = allNavItems.find((item) => item.url === pathname)
|
|
27
|
+
if (match) return match.url === "/" ? [] : [match.title]
|
|
28
|
+
|
|
29
|
+
return pathname
|
|
30
|
+
.split("/")
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.map((segment) =>
|
|
33
|
+
segment.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The shell around every authenticated route. */
|
|
38
|
+
export function AppLayout() {
|
|
39
|
+
const crumbs = useBreadcrumbs()
|
|
40
|
+
useRestoreAccountSettings()
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<SidebarProvider>
|
|
44
|
+
<AppSidebar />
|
|
45
|
+
<SidebarInset>
|
|
46
|
+
<header className="flex h-16 shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
|
|
47
|
+
<div className="flex items-center gap-2 px-4">
|
|
48
|
+
<SidebarTrigger className="-ml-1" />
|
|
49
|
+
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
|
50
|
+
<Breadcrumb>
|
|
51
|
+
<BreadcrumbList>
|
|
52
|
+
{crumbs.length === 0 ? (
|
|
53
|
+
<BreadcrumbItem>
|
|
54
|
+
<BreadcrumbPage>{env.appName}</BreadcrumbPage>
|
|
55
|
+
</BreadcrumbItem>
|
|
56
|
+
) : (
|
|
57
|
+
crumbs.map((crumb, index) => (
|
|
58
|
+
<span key={crumb} className="contents">
|
|
59
|
+
{index > 0 && <BreadcrumbSeparator />}
|
|
60
|
+
<BreadcrumbItem>
|
|
61
|
+
<BreadcrumbPage>{crumb}</BreadcrumbPage>
|
|
62
|
+
</BreadcrumbItem>
|
|
63
|
+
</span>
|
|
64
|
+
))
|
|
65
|
+
)}
|
|
66
|
+
</BreadcrumbList>
|
|
67
|
+
</Breadcrumb>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="ml-auto flex items-center gap-1 px-4">
|
|
70
|
+
<ThemeToggle />
|
|
71
|
+
<HeaderUser />
|
|
72
|
+
</div>
|
|
73
|
+
</header>
|
|
74
|
+
<div className="flex flex-1 flex-col gap-4 p-4">
|
|
75
|
+
<Outlet />
|
|
76
|
+
</div>
|
|
77
|
+
</SidebarInset>
|
|
78
|
+
</SidebarProvider>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Preflight screen — names the missing vars instead of failing on the network. */
|
|
2
|
+
export function MissingEnv({ missing }: { missing: string[] }) {
|
|
3
|
+
return (
|
|
4
|
+
<div className="bg-muted flex min-h-svh items-center justify-center p-6">
|
|
5
|
+
<div className="bg-card w-full max-w-md rounded-lg border p-6 shadow-sm">
|
|
6
|
+
<h1 className="text-lg font-semibold">Configuration required</h1>
|
|
7
|
+
<p className="text-muted-foreground mt-2 text-sm">
|
|
8
|
+
Copy <code className="font-mono">.env.example</code> to{" "}
|
|
9
|
+
<code className="font-mono">.env.local</code> and set:
|
|
10
|
+
</p>
|
|
11
|
+
<ul className="mt-4 space-y-1">
|
|
12
|
+
{missing.map((key) => (
|
|
13
|
+
<li key={key} className="font-mono text-sm">
|
|
14
|
+
{key}
|
|
15
|
+
</li>
|
|
16
|
+
))}
|
|
17
|
+
</ul>
|
|
18
|
+
<p className="text-muted-foreground mt-4 text-xs">
|
|
19
|
+
Restart the dev server after editing the file.
|
|
20
|
+
</p>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Link, useLocation } from "react-router-dom"
|
|
2
|
+
import { ChevronRightIcon } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Collapsible,
|
|
6
|
+
CollapsibleContent,
|
|
7
|
+
CollapsibleTrigger,
|
|
8
|
+
} from "@/components/ui/collapsible"
|
|
9
|
+
import {
|
|
10
|
+
SidebarGroup,
|
|
11
|
+
SidebarGroupLabel,
|
|
12
|
+
SidebarMenu,
|
|
13
|
+
SidebarMenuButton,
|
|
14
|
+
SidebarMenuItem,
|
|
15
|
+
SidebarMenuSub,
|
|
16
|
+
SidebarMenuSubButton,
|
|
17
|
+
SidebarMenuSubItem,
|
|
18
|
+
} from "@/components/ui/sidebar"
|
|
19
|
+
import type { NavItem } from "@/config/navigation"
|
|
20
|
+
import { useEntitlements } from "@/lib/sso"
|
|
21
|
+
|
|
22
|
+
export function NavMain({ items, label = "Platform" }: { items: NavItem[]; label?: string }) {
|
|
23
|
+
const { pathname } = useLocation()
|
|
24
|
+
const { has } = useEntitlements()
|
|
25
|
+
|
|
26
|
+
const visible = items.filter((item) => !item.requiresIds || item.requiresIds.some(has))
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<SidebarGroup>
|
|
30
|
+
<SidebarGroupLabel>{label}</SidebarGroupLabel>
|
|
31
|
+
<SidebarMenu>
|
|
32
|
+
{visible.map((item) => {
|
|
33
|
+
const isActive = pathname === item.url || pathname.startsWith(`${item.url}/`)
|
|
34
|
+
const subItems = (item.items ?? []).filter(
|
|
35
|
+
(sub) => !sub.requiresIds || sub.requiresIds.some(has),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if (subItems.length === 0) {
|
|
39
|
+
return (
|
|
40
|
+
<SidebarMenuItem key={item.title}>
|
|
41
|
+
<SidebarMenuButton
|
|
42
|
+
tooltip={item.title}
|
|
43
|
+
isActive={isActive}
|
|
44
|
+
render={<Link to={item.url} />}
|
|
45
|
+
>
|
|
46
|
+
{item.icon ? <item.icon /> : null}
|
|
47
|
+
<span>{item.title}</span>
|
|
48
|
+
</SidebarMenuButton>
|
|
49
|
+
</SidebarMenuItem>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Collapsible
|
|
55
|
+
key={item.title}
|
|
56
|
+
defaultOpen={isActive}
|
|
57
|
+
className="group/collapsible"
|
|
58
|
+
render={<SidebarMenuItem />}
|
|
59
|
+
>
|
|
60
|
+
<CollapsibleTrigger
|
|
61
|
+
render={<SidebarMenuButton tooltip={item.title} isActive={isActive} />}
|
|
62
|
+
>
|
|
63
|
+
{item.icon ? <item.icon /> : null}
|
|
64
|
+
<span>{item.title}</span>
|
|
65
|
+
<ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-open/collapsible:rotate-90" />
|
|
66
|
+
</CollapsibleTrigger>
|
|
67
|
+
<CollapsibleContent>
|
|
68
|
+
<SidebarMenuSub>
|
|
69
|
+
{subItems.map((subItem) => (
|
|
70
|
+
<SidebarMenuSubItem key={subItem.title}>
|
|
71
|
+
<SidebarMenuSubButton
|
|
72
|
+
isActive={pathname === subItem.url}
|
|
73
|
+
render={<Link to={subItem.url} />}
|
|
74
|
+
>
|
|
75
|
+
<span>{subItem.title}</span>
|
|
76
|
+
</SidebarMenuSubButton>
|
|
77
|
+
</SidebarMenuSubItem>
|
|
78
|
+
))}
|
|
79
|
+
</SidebarMenuSub>
|
|
80
|
+
</CollapsibleContent>
|
|
81
|
+
</Collapsible>
|
|
82
|
+
)
|
|
83
|
+
})}
|
|
84
|
+
</SidebarMenu>
|
|
85
|
+
</SidebarGroup>
|
|
86
|
+
)
|
|
87
|
+
}
|