@sybilion/uilib 1.2.1 → 1.2.3

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.
@@ -8,11 +8,11 @@ import { PAGE_HEADER_ID } from './appChromeAnchors.js';
8
8
  function AppHeaderHost({ className, anchorId = PAGE_HEADER_ID, }) {
9
9
  return jsx("header", { className: cn(S.root, className), id: anchorId });
10
10
  }
11
- function AppHeaderPortal({ children }) {
11
+ function AppHeaderPortal({ children, pageHeaderId = PAGE_HEADER_ID, }) {
12
12
  const [container, setContainer] = useState(null);
13
13
  useLayoutEffect(() => {
14
- setContainer(document.getElementById(PAGE_HEADER_ID));
15
- }, []);
14
+ setContainer(document.getElementById(pageHeaderId));
15
+ }, [pageHeaderId]);
16
16
  if (!container) {
17
17
  return null;
18
18
  }
@@ -1,6 +1,6 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.AppShell_root__ONlNK{display:grid;flex:1;grid-template-areas:\"main\";grid-template-columns:1fr;min-height:0;width:100%}@media (min-width:768px){.AppShell_root__ONlNK{grid-template-areas:\"sidebar main\";grid-template-columns:var(--sidebar-width) 1fr}[data-slot=sidebar-wrapper][data-state=collapsed] .AppShell_root__ONlNK{grid-template-columns:0 1fr}}.AppShell_mainColumn__Emn1p{display:flex;flex:1;flex-direction:column;grid-area:main;min-height:0;min-width:0}[data-slot=sidebar-wrapper][data-state=collapsed] .AppShell_mainColumn__Emn1p{margin-left:var(--p-3)}.AppShell_mainBody__IoVuy{background-color:var(--page-color);border-radius:var(--p-4);flex:1;min-width:0;padding-bottom:var(--page-y-padding)}";
3
+ var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.AppShell_root__ONlNK{display:grid;flex:1;grid-template-areas:\"main\";grid-template-columns:1fr;min-height:0;min-height:100%;width:100%}@media (min-width:768px){.AppShell_root__ONlNK{grid-template-areas:\"sidebar main\";grid-template-columns:var(--sidebar-width) 1fr}[data-slot=sidebar-wrapper][data-state=collapsed] .AppShell_root__ONlNK{grid-template-columns:0 1fr}}.AppShell_mainColumn__Emn1p{display:flex;flex:1;flex-direction:column;grid-area:main;min-height:0;min-width:0}[data-slot=sidebar-wrapper][data-state=collapsed] .AppShell_mainColumn__Emn1p{margin-left:var(--p-3)}.AppShell_mainBody__IoVuy{background-color:var(--page-color);border-radius:var(--p-4);flex:1;min-width:0;padding-bottom:var(--page-y-padding)}";
4
4
  var S = {"root":"AppShell_root__ONlNK","mainColumn":"AppShell_mainColumn__Emn1p","mainBody":"AppShell_mainBody__IoVuy"};
5
5
  styleInject(css_248z);
6
6
 
@@ -6,7 +6,7 @@ import { PageContext } from '../pageContext.js';
6
6
  import { Scroll } from '@homecode/ui';
7
7
  import S from './PageScroll.styl.js';
8
8
 
9
- function PageScrollInner({ className, children, }) {
9
+ function PageScrollInner({ className, rootClassName, children, }) {
10
10
  // const { setIsScrolled } = useContext(PageContext);
11
11
  // const prevScrollTop = useRef(0);
12
12
  const location = useLocation();
@@ -25,13 +25,13 @@ function PageScrollInner({ className, children, }) {
25
25
  // };
26
26
  return (jsx(Scroll, { y: true,
27
27
  // fadeSize="l"
28
- offset: { y: { before: 120, after: 130 } }, className: S.root, autoHide: true, innerClassName: cn(S.inner, className), yScrollbarClassName: S.scrollbar, onInnerRef: el => {
28
+ offset: { y: { before: 120, after: 130 } }, className: cn(S.root, rootClassName), autoHide: true, innerClassName: cn(S.inner, className), yScrollbarClassName: S.scrollbar, onInnerRef: el => {
29
29
  scrollInnerRef.current = el;
30
30
  }, children: children }));
31
31
  }
32
- const PageScroll = ({ children, className, }) => {
32
+ const PageScroll = ({ children, className, rootClassName, }) => {
33
33
  const [isScrolled, setIsScrolled] = useState(false);
34
- return (jsx(PageContext.Provider, { value: { isScrolled, setIsScrolled }, children: jsx(PageScrollInner, { className: className, children: children }) }));
34
+ return (jsx(PageContext.Provider, { value: { isScrolled, setIsScrolled }, children: jsx(PageScrollInner, { className: className, rootClassName: rootClassName, children: children }) }));
35
35
  };
36
36
 
37
37
  export { PageScroll };
@@ -6,7 +6,7 @@ import { PackageOpen, ChevronDown, ChevronRight } from 'lucide-react';
6
6
  import S from './SidebarDatasetsItemsGrouped.styl.js';
7
7
  import { groupSidebarDatasets } from './groupSidebarDatasets.js';
8
8
 
9
- function SidebarDatasetsItemsGrouped({ groupBy, datasets, selectedDatasetId, onDatasetClick, defaultExpandedGroupNames, className, }) {
9
+ function SidebarDatasetsItemsGrouped({ groupBy, datasets, preItems, postItems, selectedDatasetId, onDatasetClick, defaultExpandedGroupNames, className, }) {
10
10
  const grouped = useMemo(() => groupSidebarDatasets(datasets, groupBy), [datasets, groupBy]);
11
11
  const [expanded, setExpanded] = useState(new Set());
12
12
  useEffect(() => {
@@ -35,14 +35,14 @@ function SidebarDatasetsItemsGrouped({ groupBy, datasets, selectedDatasetId, onD
35
35
  return next;
36
36
  });
37
37
  };
38
- return (jsx(SidebarGroup, { className: className, children: jsx(SidebarMenu, { children: grouped.map(([groupName, groupDatasets]) => {
39
- const isExpanded = expanded.has(groupName);
40
- const parentActive = groupDatasets.some(d => d.id === selectedDatasetId);
41
- return (jsxs(SidebarMenuItem, { children: [jsxs(SidebarMenuButton, { type: "button", isActive: parentActive, onClick: () => toggleGroup(groupName), children: [jsx(PackageOpen, { strokeWidth: 1.5, size: 16 }), jsx(SmartTextTruncate, { children: groupName }), jsx("div", { className: S.chevronContainer, children: isExpanded ? (jsx(ChevronDown, { size: 12 })) : (jsx(ChevronRight, { size: 12 })) })] }), isExpanded && (jsxs(SidebarMenuSub, { className: S.subMenuContainer, children: [jsx("div", { className: S.subMenuBorder }), groupDatasets.map(dataset => (jsx(SidebarMenuSubItem, { className: S.subMenuItem, children: jsx(SidebarMenuSubButton, { href: `#dataset-${dataset.id}`, isActive: dataset.id === selectedDatasetId, onClick: e => {
42
- e.preventDefault();
43
- onDatasetClick?.(dataset.id);
44
- }, children: jsx(SmartTextTruncate, { children: dataset.name }) }) }, dataset.id)))] }))] }, groupName));
45
- }) }) }));
38
+ return (jsx(SidebarGroup, { className: className, children: jsxs(SidebarMenu, { children: [preItems, grouped.map(([groupName, groupDatasets]) => {
39
+ const isExpanded = expanded.has(groupName);
40
+ const parentActive = groupDatasets.some(d => d.id === selectedDatasetId);
41
+ return (jsxs(SidebarMenuItem, { children: [jsxs(SidebarMenuButton, { type: "button", isActive: parentActive, onClick: () => toggleGroup(groupName), children: [jsx(PackageOpen, { strokeWidth: 1.5, size: 16 }), jsx(SmartTextTruncate, { children: groupName }), jsx("div", { className: S.chevronContainer, children: isExpanded ? (jsx(ChevronDown, { size: 12 })) : (jsx(ChevronRight, { size: 12 })) })] }), isExpanded && (jsxs(SidebarMenuSub, { className: S.subMenuContainer, children: [jsx("div", { className: S.subMenuBorder }), groupDatasets.map(dataset => (jsx(SidebarMenuSubItem, { className: S.subMenuItem, children: jsx(SidebarMenuSubButton, { href: `#dataset-${dataset.id}`, isActive: dataset.id === selectedDatasetId, onClick: e => {
42
+ e.preventDefault();
43
+ onDatasetClick?.(dataset.id);
44
+ }, children: jsx(SmartTextTruncate, { children: dataset.name }) }) }, dataset.id)))] }))] }, groupName));
45
+ }), postItems] }) }));
46
46
  }
47
47
 
48
48
  export { SidebarDatasetsItemsGrouped };
@@ -7,5 +7,6 @@ export type AppHeaderProps = {
7
7
  export declare function AppHeaderHost({ className, anchorId, }: AppHeaderProps): import("react/jsx-runtime").JSX.Element;
8
8
  export type AppHeaderPortalProps = {
9
9
  children: ReactNode;
10
+ pageHeaderId?: string;
10
11
  };
11
- export declare function AppHeaderPortal({ children }: AppHeaderPortalProps): import("react").ReactPortal;
12
+ export declare function AppHeaderPortal({ children, pageHeaderId, }: AppHeaderPortalProps): import("react").ReactPortal;
@@ -1,4 +1,5 @@
1
- export declare const PageScroll: ({ children, className, }: {
1
+ export declare const PageScroll: ({ children, className, rootClassName, }: {
2
2
  children: React.ReactNode;
3
3
  className?: string;
4
+ rootClassName?: string;
4
5
  }) => import("react/jsx-runtime").JSX.Element;
@@ -2,10 +2,12 @@ import { type SidebarDatasetsItemsGroupBy, type SidebarDatasetsItemsGroupedDatas
2
2
  export type SidebarDatasetsItemsGroupedProps = {
3
3
  groupBy: SidebarDatasetsItemsGroupBy;
4
4
  datasets: SidebarDatasetsItemsGroupedDataset[];
5
+ preItems?: React.ReactNode;
6
+ postItems?: React.ReactNode;
5
7
  selectedDatasetId?: number;
6
8
  onDatasetClick?: (datasetId: number) => void;
7
9
  /** When omitted, all groups start expanded. */
8
10
  defaultExpandedGroupNames?: string[];
9
11
  className?: string;
10
12
  };
11
- export declare function SidebarDatasetsItemsGrouped({ groupBy, datasets, selectedDatasetId, onDatasetClick, defaultExpandedGroupNames, className, }: SidebarDatasetsItemsGroupedProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function SidebarDatasetsItemsGrouped({ groupBy, datasets, preItems, postItems, selectedDatasetId, onDatasetClick, defaultExpandedGroupNames, className, }: SidebarDatasetsItemsGroupedProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ export default function StandaloneAppLayoutPage(): import("react/jsx-runtime").JSX.Element;
@@ -1,8 +1,8 @@
1
1
  # Standalone Sybilion apps (@sybilion/uilib)
2
2
 
3
- Greenfield SPA on **your own origin**: `@sybilion/uilib` for layout/UI, **SybilionAuthProvider** + Sybilion API for data—no iframe in the main client.
3
+ Greenfield SPA on **your own origin**: `@sybilion/uilib` for layout/UI, `SybilionAuthProvider` + Sybilion API for data—no iframe in the main client.
4
4
 
5
- **Agents / humans:** Use `AppShell` + `AppShellMainContent`; stick to uilib spacing primitives (e.g. `Gap`) instead of ad hoc root horizontal gutters.
5
+ **Agents / humans:** use `AppShell` + `AppShellMainContent`; stick to uilib spacing primitives (e.g. `Gap`) instead of ad hoc root horizontal gutters.
6
6
 
7
7
  ## 1. Dependencies and global CSS
8
8
 
@@ -10,17 +10,138 @@ Greenfield SPA on **your own origin**: `@sybilion/uilib` for layout/UI, **Sybili
10
10
  yarn add react react-dom react-router-dom @auth0/auth0-react @sybilion/uilib @sybilion/sdk
11
11
  ```
12
12
 
13
- Import tokens/fonts once:
13
+ Import tokens/fonts once (typically `src/main.tsx`):
14
14
 
15
15
  ```ts
16
16
  import '@sybilion/uilib/standalone-global.css';
17
17
  ```
18
18
 
19
- ## 2. Layout (AppShell)
19
+ Mount the tree with `ReactDOM.createRoot` `App` (wrap with `StrictMode` if you want).
20
20
 
21
- ### Minimal example
21
+ ## 2. SDK (`@sybilion/sdk`)
22
22
 
23
- `AppHeaderHost` is the empty header bar; put triggers and `NavUserHeader` (a.k.a. the user / account menu) inside `AppHeaderPortal` so they render in that bar.
23
+ Typed HTTP client for the Sybilion API. Env vars depend on bundler; for Vite, only `import.meta.env.VITE_*` reaches the client.
24
+
25
+ Create **one** instance at module scope (e.g. `src/libs/sybilion-sdk.ts`) so React never reconstructs the client (`useMemo` not needed), and reuse it for auth + data:
26
+
27
+ ```ts
28
+ import { createSybilionSDK } from '@sybilion/sdk';
29
+
30
+ export const sybilionJwtStorageKey = 'sybilion.standalone.jwt';
31
+
32
+ export const sybilionSdk = createSybilionSDK({
33
+ baseUrl: import.meta.env.VITE_SYBILION_API_BASE_URL as string,
34
+ apiPrefix: '/api',
35
+ getToken: () =>
36
+ typeof localStorage !== 'undefined'
37
+ ? (localStorage.getItem(sybilionJwtStorageKey) ?? undefined)
38
+ : undefined,
39
+ });
40
+ ```
41
+
42
+ **Options:** `baseUrl` — API origin only (no trailing slash). `apiPrefix` — default `'/api'` so calls go to `{baseUrl}/api/v1/...`. `getToken` — must read the same key you pass as `sybilionTokenStorageKey` on `SybilionAuthProvider` (§3).
43
+
44
+ ```ts
45
+ import { sybilionSdk } from './libs/sybilion-sdk';
46
+
47
+ await sybilionSdk.raw.datasets.getById(datasetId);
48
+ ```
49
+
50
+ - `sybilionSdk.auth` — `loginWithAuth0Identity`, `getMe`, `updateMe` (Auth0 bootstrap + user profile).
51
+ - `sybilionSdk.raw` — thin `GET`/`POST`/… wrappers for `/v1/...` paths (e.g. `raw.analyses.driversMapOnce`; parsed JSON, no app-specific shaping).
52
+ - `sybilionSdk.resources` — higher-level helpers (datasets, drivers, subscriptions) on top of `raw`.
53
+
54
+ Package README: [`@sybilion/sdk`](https://www.npmjs.com/package/@sybilion/sdk) — monorepo: [`../../sdk/README.md`](../../sdk/README.md).
55
+
56
+ ## 3. Auth (`SybilionAuthProvider`)
57
+
58
+ Use inside `BrowserRouter` if redirects hit a callback route.
59
+
60
+ Wire the SDK module from §2 — no second `createSybilionSDK` here:
61
+
62
+ ```tsx
63
+ import type { ReactNode } from 'react';
64
+
65
+ import { SybilionAuthProvider } from '@sybilion/uilib';
66
+
67
+ import { sybilionJwtStorageKey, sybilionSdk } from './libs/sybilion-sdk';
68
+
69
+ export function AppProviders({ children }: { children: ReactNode }) {
70
+ const auth0Domain = import.meta.env.VITE_AUTH0_DOMAIN as string;
71
+ const auth0ClientId = import.meta.env.VITE_AUTH0_CLIENT_ID as string;
72
+
73
+ return (
74
+ <SybilionAuthProvider
75
+ sdk={sybilionSdk}
76
+ sybilionTokenStorageKey={sybilionJwtStorageKey}
77
+ auth0Domain={auth0Domain}
78
+ auth0ClientId={auth0ClientId}
79
+ redirectUri={window.location.origin}
80
+ >
81
+ {children}
82
+ </SybilionAuthProvider>
83
+ );
84
+ }
85
+ ```
86
+
87
+ **Flow:** Auth0 SPA → `sybilionSdk.auth.loginWithAuth0Identity(<Auth0 AT>)` → Sybilion JWT (`data.token` / `token`) persisted → same `sybilionSdk` + `getToken` attach Bearer on requests; `useSybilionApiFetch()` uses `getSybilionApiOriginFromSdk(sdk)` for URLs (paths like `/api/v1/...`).
88
+
89
+ **Defaults** for `authorizationParams` match sybilion-client (Management audience + `openid profile email offline_access …`); override if your Auth0 SPA needs a Resource Server audience (backend confirms).
90
+
91
+ | Layer | Configure |
92
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
93
+ | **Auth0** | Callback, logout, and web origins → your URLs (+ previews). |
94
+ | **Sybilion API** | CORS → your deploy `Origin`. |
95
+ | **App** | §2 SDK module (`baseUrl`, `apiPrefix`, `getToken`); pass `sdk` + matching `sybilionTokenStorageKey`; Auth0 `domain` / `clientId`; redirect usually `window.location.origin`. |
96
+
97
+ **Hooks:** `useSybilionAuth()`, `useSybilionApiFetch()` (or `createSybilionApiFetch` / `sybilionApiFetch` helpers).
98
+
99
+ ## 4. Layout (AppShell)
100
+
101
+ With §2 `sybilionSdk` and §3 `AppProviders` / `SybilionAuthProvider` defined, compose routing + shell so Auth0 callbacks and JWT-backed hooks wrap the whole UI.
102
+
103
+ ### Root wiring (`App.tsx`)
104
+
105
+ Order outside → in: `BrowserRouter` (callbacks) → `AppProviders` (§3) → `SidebarProvider` (sidebar width / open state for `Sidebar` primitives) → `AppLayout` → `Routes`.
106
+
107
+ `AppLayout` renders the persistent chrome (sidebar, header, footer) and the active route renders inside its main column via `children`. Add `HomePage` / `DatasetsPage` as your own page components; register an Auth0 callback route here if `SybilionAuthProvider` uses a dedicated path.
108
+
109
+ ```tsx
110
+ import { BrowserRouter, Route, Routes } from 'react-router-dom';
111
+
112
+ import { SidebarProvider } from '@sybilion/uilib';
113
+
114
+ import { AppLayout } from './AppLayout';
115
+ import { AppProviders } from './AppProviders';
116
+ import { DatasetsPage } from './pages/DatasetsPage';
117
+ import { HomePage } from './pages/HomePage';
118
+
119
+ export function App() {
120
+ return (
121
+ <BrowserRouter>
122
+ <AppProviders>
123
+ <SidebarProvider
124
+ sidebarWidthStorageKey="myapp.sidebarWidthPx"
125
+ persistSidebarWidthWithoutConsent
126
+ >
127
+ <AppLayout>
128
+ <Routes>
129
+ <Route path="/" element={<HomePage />} />
130
+ <Route path="/datasets" element={<DatasetsPage />} />
131
+ </Routes>
132
+ </AppLayout>
133
+ </SidebarProvider>
134
+ </AppProviders>
135
+ </BrowserRouter>
136
+ );
137
+ }
138
+ ```
139
+
140
+ `SidebarProvider`: pass only the props you need. `sidebarWidthStorageKey` namespaces width in localStorage. For production apps with cookie-based consent, drop `persistSidebarWidthWithoutConsent` and pass `userId` so width persistence follows your consent rules.
141
+
142
+ ### `AppLayout` (sidebar + main + `children`)
143
+
144
+ `AppHeaderHost` is the header anchor; put `SidebarTrigger` (collapses / opens rail), `NavUserHeader`, etc. inside `AppHeaderPortal`. `AppSidebar` (next subsection) is a sibling of `AppShellMainContent` inside `AppShell`. The matched route (the `<Routes>` subtree from `App.tsx`) arrives as `children` and renders inside the main column.
24
145
 
25
146
  ```tsx
26
147
  import type { ReactNode } from 'react';
@@ -34,18 +155,23 @@ import {
34
155
  NavUserHeader,
35
156
  PageFooter,
36
157
  PageScroll,
158
+ SidebarTrigger,
37
159
  } from '@sybilion/uilib';
38
160
 
161
+ import { AppSidebar } from './AppSidebar';
162
+
39
163
  export function AppLayout({ children }: { children: ReactNode }) {
40
164
  return (
41
165
  <PageScroll>
42
166
  <AppShell>
43
- {/* Optional: sidebar as a sibling before main, e.g. <Sidebar /> */}
167
+ <AppSidebar />
168
+
44
169
  <AppShellMainContent
45
170
  header={<AppHeaderHost />}
46
- footer={<PageFooter versionLink="/releases" versionLabel="0.0.0" />}
171
+ footer={<PageFooter versionLink="/releases" versionLabel="0.0.1" />}
47
172
  >
48
173
  <AppHeaderPortal>
174
+ <SidebarTrigger />
49
175
  <Gap />
50
176
  <NavUserHeader
51
177
  theme="light"
@@ -62,118 +188,115 @@ export function AppLayout({ children }: { children: ReactNode }) {
62
188
  }
63
189
  ```
64
190
 
65
- Wire `NavUserHeader` to real auth (`useSybilionAuth`, theme context, etc.). Demo page in repo: `src/docs/pages/NavUserHeaderPage.tsx` (slug `nav-user-header`).
66
-
67
- ### Full pattern
68
-
69
- Reference: `[src/docs/DocsShell.tsx](https://github.com/Mir-Insight/uilib/blob/main/src/docs/DocsShell.tsx)` — `PageScroll` → `AppShell` → sidebar → `AppShellMainContent` with `AppHeaderHost`, `PageFooter`, and routed body (`Outlet`). Add `Theme` and optional `SidebarProvider` from `@homecode/ui`.
70
-
71
- ### Glossary (high-use pieces)
72
-
73
- | Component / API | What it is for |
74
- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
75
- | `**PageScroll**` | Page-level vertical scroll wrapper; usual outer shell for the app body. |
76
- | `**AppShell**` | Layout grid container; typically holds a sidebar (optional) + main column. |
77
- | `**AppShellMainContent**` | Main column with slots: `header`, scrollable body (`children`), `footer`. |
78
- | `**AppHeaderHost**` | Renders the top header anchor (DOM id `page-header`); stays empty on purpose. |
79
- | `**AppHeaderPortal**` | Portals children into `AppHeaderHost` — put header actions, `NavUserHeader`, theme toggle here. |
80
- | `**NavUserHeader**` | Header user menu: avatar, account dropdown, theme row, logout; **not** a bare `DropdownMenu`. |
81
- | `**Sidebar`\*\* | Collapsible app sidebar / nav rail (pair with `SidebarProvider` from `@homecode/ui` when needed). |
82
- | `**PageFooter**` | Standard app footer (logo, links, version badge). Requires `versionLink` + `versionLabel`. |
83
- | `**Gap**` | Horizontal or vertical spacing primitive between flex children (prefer over raw margins). |
84
- | `**SybilionAuthProvider**` | Auth0 + Sybilion JWT bootstrap for the whole app (see §3). |
85
- | `**useSybilionAuth**` | Current user, tokens, login/logout inside the provider tree. |
86
- | `**useSybilionApiFetch**` | Authenticated `fetch` to the Sybilion API using the stored JWT. |
87
- | `**Breadcrumb**`, `**BreadcrumbList`, …\*\* | Accessible breadcrumb trail for the page header (compose `BreadcrumbItem` / `BreadcrumbLink` / `BreadcrumbPage`). |
88
- | `**Button`**, `**Card\*\*` | Primary actions and grouped content blocks—default building blocks for screens inside the layout body. |
89
-
90
- ## 3. Auth (`SybilionAuthProvider`)
191
+ Wire `NavUserHeader` to real auth (`useSybilionAuth`, theme context, etc.; §3). Demo in repo: `src/docs/pages/NavUserHeaderPage.tsx` (slug `nav-user-header`).
91
192
 
92
- Use inside `BrowserRouter` if redirects hit a callback route.
193
+ #### Sidebar (`AppSidebar.tsx`)
93
194
 
94
- Env vars depend on bundlerfor **Vite** only `import.meta.env.VITE_` is exposed client-side:
195
+ App-specific sidebar componentkeeps the navigation surface out of `AppLayout` so the shell stays generic. Compose your nav from `@sybilion/uilib` primitives (`Sidebar` + `SidebarContent` + `SidebarGroup` + `SidebarMenu*`) and product widgets like `SidebarDatasetsItemsGrouped` (collapsible groups + nested rows for datasets — see demo `src/docs/pages/SidebarDatasetsItemsGroupedPage.tsx`, slug `sidebar-datasets-items-grouped`).
95
196
 
96
197
  ```tsx
97
- import { useMemo, type ReactNode } from 'react';
198
+ import { useEffect, useState } from 'react';
199
+ import { NavLink, useNavigate } from 'react-router-dom';
98
200
 
99
- import { SybilionAuthProvider } from '@sybilion/uilib';
100
- import { createSybilionSDK } from '@sybilion/sdk';
201
+ import {
202
+ Sidebar,
203
+ SidebarContent,
204
+ SidebarDatasetsItemsGrouped,
205
+ type SidebarDatasetsItemsGroupedDataset,
206
+ SidebarGroup,
207
+ SidebarMenu,
208
+ SidebarMenuButton,
209
+ SidebarMenuItem,
210
+ } from '@sybilion/uilib';
101
211
 
102
- const sybilionJwtKey = 'sybilion.standalone.jwt';
212
+ import { sybilionSdk } from './libs/sybilion-sdk';
103
213
 
104
- export function AppProviders({ children }: { children: ReactNode }) {
105
- const apiBaseUrl = import.meta.env.VITE_SYBILION_API_BASE_URL as string;
106
- const auth0Domain = import.meta.env.VITE_AUTH0_DOMAIN as string;
107
- const auth0ClientId = import.meta.env.VITE_AUTH0_CLIENT_ID as string;
214
+ export function AppSidebar() {
215
+ const navigate = useNavigate();
216
+ const [datasets, setDatasets] = useState<
217
+ SidebarDatasetsItemsGroupedDataset[]
218
+ >([]);
219
+ const [selectedDatasetId, setSelectedDatasetId] = useState<number>();
108
220
 
109
- const sdk = useMemo(
110
- () =>
111
- createSybilionSDK({
112
- baseUrl: apiBaseUrl,
113
- apiPrefix: '/api',
114
- getToken: () =>
115
- typeof localStorage !== 'undefined'
116
- ? localStorage.getItem(sybilionJwtKey) ?? undefined
117
- : undefined,
118
- }),
119
- [apiBaseUrl],
120
- );
221
+ useEffect(() => {
222
+ sybilionSdk.raw.datasetsIndex(1, 50).then(res => {
223
+ setDatasets(res?.data?.datasets ?? []);
224
+ });
225
+ }, []);
121
226
 
122
227
  return (
123
- <SybilionAuthProvider
124
- sdk={sdk}
125
- sybilionTokenStorageKey={sybilionJwtKey}
126
- auth0Domain={auth0Domain}
127
- auth0ClientId={auth0ClientId}
128
- redirectUri={window.location.origin}
129
- >
130
- {children}
131
- </SybilionAuthProvider>
228
+ <Sidebar variant="inset" collapsible="offcanvas">
229
+ <SidebarContent>
230
+ <SidebarGroup>
231
+ <SidebarMenu>
232
+ <SidebarMenuItem>
233
+ <SidebarMenuButton asChild>
234
+ <NavLink to="/" end>
235
+ Home
236
+ </NavLink>
237
+ </SidebarMenuButton>
238
+ </SidebarMenuItem>
239
+ <SidebarMenuItem>
240
+ <SidebarMenuButton asChild>
241
+ <NavLink to="/datasets">Datasets</NavLink>
242
+ </SidebarMenuButton>
243
+ </SidebarMenuItem>
244
+ </SidebarMenu>
245
+ </SidebarGroup>
246
+
247
+ <SidebarDatasetsItemsGrouped
248
+ groupBy="regions"
249
+ datasets={datasets}
250
+ selectedDatasetId={selectedDatasetId}
251
+ onDatasetClick={id => {
252
+ setSelectedDatasetId(id);
253
+ navigate(`/datasets/${id}`);
254
+ }}
255
+ />
256
+ </SidebarContent>
257
+ </Sidebar>
132
258
  );
133
259
  }
134
260
  ```
135
261
 
136
- **Flow:** Auth0 SPA `sdk.auth.loginWithAuth0Identity(<Auth0 AT>)` Sybilion JWT (`data.token` / `token`) persisted same `sdk` + `getToken` supply Bearer on API calls; `useSybilionApiFetch()` uses `getSybilionApiOriginFromSdk(sdk)` for URLs (paths like `/api/v1/...`).
137
-
138
- **Defaults** for `authorizationParams` match sybilion-client (Management audience + `openid profile email offline_access …`); override if your Auth0 SPA needs a Resource Server audience (backend confirms).
139
-
140
- | Layer | Configure |
141
- | ---------------- | ------------------------------------------------------------------------------------- |
142
- | **Auth0** | Callback, logout, and web origins → your URLs (+ previews). |
143
- | **Sybilion API** | CORS → your deploy `Origin`. |
144
- | **App** | `createSybilionSDK` (`baseUrl`, `apiPrefix`), pass **`sdk`** to `SybilionAuthProvider`; align `sybilionTokenStorageKey` with `getToken`; Auth0 `domain` / `clientId`; redirect usually `window.location.origin`. |
145
-
146
- **Hooks:** `useSybilionAuth()`, `useSybilionApiFetch()` (or `createSybilionApiFetch` / `sybilionApiFetch` helpers).
262
+ Data loading uses §2 `sybilionSdk` directly for production swap the inline `useEffect` for your data layer (React Query, SWR, context, etc.). `groupBy` accepts `'regions' | 'target_type' | 'categories'`; the widget owns its expand state and notifies on selection via `onDatasetClick`.
147
263
 
148
- ## 4. Data
149
-
150
- Fetch the Sybilion API with the JWT above (e.g. via `useSybilionApiFetch`).
264
+ ### Full pattern
151
265
 
152
- ### `@sybilion/sdk`
266
+ Composition: `PageScroll` → `AppShell` → `AppSidebar` → `AppShellMainContent` with `AppHeaderHost`, `PageFooter`, and the active route as `children`. `SidebarProvider` wraps `AppLayout`. Add `Theme` from `@homecode/ui` only when your product uses those primitives alongside uilib.
153
267
 
154
- Install the typed client alongside uilib:
268
+ ### Greenfield checklist (agents)
155
269
 
156
- ```bash
157
- yarn add @sybilion/sdk
158
- ```
270
+ | Step | Deliverable |
271
+ | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
272
+ | Env | `VITE_SYBILION_API_BASE_URL`, `VITE_AUTH0_DOMAIN`, `VITE_AUTH0_CLIENT_ID` (names mirror §2–§3). |
273
+ | Files | `src/libs/sybilion-sdk.ts`, `AppProviders.tsx`, `AppLayout.tsx`, `AppSidebar.tsx`, `App.tsx`, `main.tsx`, route `element` pages under e.g. `src/pages/`. |
274
+ | Auth0 | SPA callback / logout URLs + allowed web origins → your deploy URLs (and previews). |
275
+ | API | Sybilion backend CORS → same `Origin` values. |
159
276
 
160
- Configure **`createSybilionSDK`** with the same instance you pass to **`SybilionAuthProvider`** (`sdk` prop). Use **`baseUrl`**: API origin only (no trailing slash), default **`apiPrefix: '/api'`** so requests hit `{baseUrl}/api/v1/...`. **`getToken`** should read the same storage key as **`sybilionTokenStorageKey`** on the provider (default `sybilion.standalone.jwt`).
161
-
162
- ```ts
163
- import { createSybilionSDK } from '@sybilion/sdk';
164
-
165
- const sdk = createSybilionSDK({
166
- baseUrl: apiBaseUrl,
167
- apiPrefix: '/api',
168
- getToken: () => localStorage.getItem('sybilion.standalone.jwt') ?? undefined,
169
- });
170
- ```
171
-
172
- - **`sdk.auth`** — `loginWithAuth0Identity`, `getMe`, `updateMe` (Auth0 bootstrap + user profile).
173
- - **`sdk.raw`** — thin `GET`/`POST`/… wrappers for `/v1/...` paths (e.g. `raw.analyses.driversMapOnce`; returns parsed JSON, no app-specific shaping).
174
- - **`sdk.resources`** — higher-level helpers (datasets, drivers, subscriptions) that compose `raw` calls with parsing where applicable.
277
+ ### Glossary (high-use pieces)
175
278
 
176
- See the package README: [`@sybilion/sdk`](https://www.npmjs.com/package/@sybilion/sdk) in this monorepo, [`../../sdk/README.md`](../../sdk/README.md).
279
+ | Component / API | What it is for |
280
+ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
281
+ | `PageScroll` | Page-level vertical scroll wrapper; usual outer shell for the app body. |
282
+ | `AppShell` | Layout grid container; `Sidebar` + `AppShellMainContent` as siblings inside it. |
283
+ | `AppShellMainContent` | Main column: `header`, scrollable body (`children`), `footer`. |
284
+ | `AppHeaderHost` | Top header anchor (DOM id `page-header`); stays empty until `AppHeaderPortal` portals into it. |
285
+ | `AppHeaderPortal` | Portals into `AppHeaderHost` — `SidebarTrigger`, `NavUserHeader`, theme toggle. |
286
+ | `NavUserHeader` | Header user menu (avatar, account, theme, logout). |
287
+ | `Sidebar`, `SidebarProvider` | Collapsible rail + context (`@sybilion/uilib`). Wrap `SidebarProvider` above `AppLayout`; render `Sidebar` inside `AppShell` (usually via `AppSidebar`). |
288
+ | `AppSidebar` | App-specific component (`src/AppSidebar.tsx`) composing `Sidebar` + nav links + product widgets. Keeps `AppLayout` generic. |
289
+ | `SidebarDatasetsItemsGrouped` | Dataset list widget for the sidebar: collapsible groups (`regions` / `target_type` / `categories`) with nested rows + selection callback. |
290
+ | `SidebarTrigger` | Toggle sidebar visibility (especially mobile / `offcanvas`). |
291
+ | `PageFooter` | Standard footer; requires `versionLink` + `versionLabel`. |
292
+ | `Gap` | Spacing primitive between flex children. |
293
+ | `SybilionAuthProvider` | Auth0 + Sybilion JWT (§3). |
294
+ | `useSybilionAuth` | User session + login/logout under provider. |
295
+ | `useSybilionApiFetch` | Authenticated `fetch` using stored JWT. |
296
+
297
+ ## 5. Data
298
+
299
+ Inside `SybilionAuthProvider`, use `useSybilionApiFetch()` for authenticated `fetch`, or import `sybilionSdk` from §2 for `raw` / `resources` — same JWT storage and API origin either way.
177
300
 
178
301
  ## Related
179
302
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -20,14 +20,18 @@ export function AppHeaderHost({
20
20
 
21
21
  export type AppHeaderPortalProps = {
22
22
  children: ReactNode;
23
+ pageHeaderId?: string;
23
24
  };
24
25
 
25
- export function AppHeaderPortal({ children }: AppHeaderPortalProps) {
26
+ export function AppHeaderPortal({
27
+ children,
28
+ pageHeaderId = PAGE_HEADER_ID,
29
+ }: AppHeaderPortalProps) {
26
30
  const [container, setContainer] = useState<HTMLElement | null>(null);
27
31
 
28
32
  useLayoutEffect(() => {
29
- setContainer(document.getElementById(PAGE_HEADER_ID));
30
- }, []);
33
+ setContainer(document.getElementById(pageHeaderId));
34
+ }, [pageHeaderId]);
31
35
 
32
36
  if (!container) {
33
37
  return null;
@@ -5,6 +5,7 @@
5
5
  flex 1
6
6
  min-height 0
7
7
  width 100%
8
+ min-height 100%
8
9
  grid-template-columns 1fr
9
10
  grid-template-areas "main"
10
11
 
@@ -9,9 +9,12 @@ import S from './PageScroll.styl';
9
9
 
10
10
  function PageScrollInner({
11
11
  className,
12
+ rootClassName,
12
13
  children,
13
14
  }: {
14
15
  className?: string;
16
+ /** Merged onto the outer Scroll root (e.g. embed previews that must not use `100vh`). */
17
+ rootClassName?: string;
15
18
  children: React.ReactNode;
16
19
  }) {
17
20
  // const { setIsScrolled } = useContext(PageContext);
@@ -38,7 +41,7 @@ function PageScrollInner({
38
41
  y
39
42
  // fadeSize="l"
40
43
  offset={{ y: { before: 120, after: 130 } }}
41
- className={S.root}
44
+ className={cn(S.root, rootClassName)}
42
45
  autoHide
43
46
  innerClassName={cn(S.inner, className)}
44
47
  yScrollbarClassName={S.scrollbar}
@@ -55,15 +58,19 @@ function PageScrollInner({
55
58
  export const PageScroll = ({
56
59
  children,
57
60
  className,
61
+ rootClassName,
58
62
  }: {
59
63
  children: React.ReactNode;
60
64
  className?: string;
65
+ rootClassName?: string;
61
66
  }) => {
62
67
  const [isScrolled, setIsScrolled] = useState(false);
63
68
 
64
69
  return (
65
70
  <PageContext.Provider value={{ isScrolled, setIsScrolled }}>
66
- <PageScrollInner className={className}>{children}</PageScrollInner>
71
+ <PageScrollInner className={className} rootClassName={rootClassName}>
72
+ {children}
73
+ </PageScrollInner>
67
74
  </PageContext.Provider>
68
75
  );
69
76
  };
@@ -22,6 +22,8 @@ import {
22
22
  export type SidebarDatasetsItemsGroupedProps = {
23
23
  groupBy: SidebarDatasetsItemsGroupBy;
24
24
  datasets: SidebarDatasetsItemsGroupedDataset[];
25
+ preItems?: React.ReactNode;
26
+ postItems?: React.ReactNode;
25
27
  selectedDatasetId?: number;
26
28
  onDatasetClick?: (datasetId: number) => void;
27
29
  /** When omitted, all groups start expanded. */
@@ -32,6 +34,8 @@ export type SidebarDatasetsItemsGroupedProps = {
32
34
  export function SidebarDatasetsItemsGrouped({
33
35
  groupBy,
34
36
  datasets,
37
+ preItems,
38
+ postItems,
35
39
  selectedDatasetId,
36
40
  onDatasetClick,
37
41
  defaultExpandedGroupNames,
@@ -74,6 +78,8 @@ export function SidebarDatasetsItemsGrouped({
74
78
  return (
75
79
  <SidebarGroup className={className}>
76
80
  <SidebarMenu>
81
+ {preItems}
82
+
77
83
  {grouped.map(([groupName, groupDatasets]) => {
78
84
  const isExpanded = expanded.has(groupName);
79
85
  const parentActive = groupDatasets.some(
@@ -97,6 +103,7 @@ export function SidebarDatasetsItemsGrouped({
97
103
  )}
98
104
  </div>
99
105
  </SidebarMenuButton>
106
+
100
107
  {isExpanded && (
101
108
  <SidebarMenuSub className={S.subMenuContainer}>
102
109
  <div className={S.subMenuBorder} />
@@ -122,6 +129,8 @@ export function SidebarDatasetsItemsGrouped({
122
129
  </SidebarMenuItem>
123
130
  );
124
131
  })}
132
+
133
+ {postItems}
125
134
  </SidebarMenu>
126
135
  </SidebarGroup>
127
136
  );
@@ -0,0 +1,46 @@
1
+ @import '../../lib/theme.styl'
2
+
3
+ // Embed preview: Sidebar uses position:fixed + viewport top/height; PageScroll uses 100vh.
4
+ // Transform creates a containing block for `fixed` children; overrides stop bleed into docs chrome.
5
+ .preview
6
+ position relative
7
+ overflow hidden
8
+ transform translateZ(0)
9
+ border-radius var(--p-4)
10
+ border 1px solid var(--border)
11
+ background-color var(--background)
12
+ height min(720px, 80vh)
13
+ min-height 0
14
+ display flex
15
+ flex-direction column
16
+
17
+ .previewScrollRoot
18
+ flex 1
19
+ min-height 0
20
+ height 100% !important
21
+ max-height 100% !important
22
+
23
+ @media (max-width MOBILE)
24
+ height auto !important
25
+ flex 1
26
+
27
+ .preview aside[data-side="left"]
28
+ top 0 !important
29
+ left 0 !important
30
+ height 100% !important
31
+ min-height 0 !important
32
+
33
+ @media (min-width MOBILE)
34
+ height 100% !important
35
+
36
+ .preview aside[data-side="left"] > div:first-child
37
+ height 100% !important
38
+ max-height 100% !important
39
+
40
+ @media (min-width MOBILE)
41
+ height 100% !important
42
+
43
+ .preview [data-slot="sidebar-resize-handle"]
44
+ top 0 !important
45
+ height 100% !important
46
+ max-height 100%
@@ -0,0 +1,8 @@
1
+ // This file is automatically generated.
2
+ // Please do not change this file!
3
+ interface CssExports {
4
+ 'preview': string;
5
+ 'previewScrollRoot': string;
6
+ }
7
+ export const cssExports: CssExports;
8
+ export default cssExports;
@@ -0,0 +1,242 @@
1
+ import { useCallback, useState } from 'react';
2
+
3
+ import { AppHeaderHost, AppHeaderPortal } from '#uilib/components/ui/AppHeader';
4
+ import { Gap } from '#uilib/components/ui/Gap/Gap';
5
+ import { NavUserHeader } from '#uilib/components/ui/NavUserHeader/NavUserHeader';
6
+ import {
7
+ AppShell,
8
+ AppShellMainContent,
9
+ PageContentSection,
10
+ } from '#uilib/components/ui/Page';
11
+ import { PageFooter } from '#uilib/components/ui/Page/PageFooter/PageFooter';
12
+ import { PageScroll } from '#uilib/components/ui/Page/PageScroll/PageScroll';
13
+ import {
14
+ Sidebar,
15
+ SidebarContent,
16
+ SidebarGroup,
17
+ SidebarMenu,
18
+ SidebarMenuButton,
19
+ SidebarMenuItem,
20
+ SidebarProvider,
21
+ SidebarTrigger,
22
+ } from '#uilib/components/ui/Sidebar/Sidebar';
23
+ import {
24
+ SidebarDatasetsItemsGrouped,
25
+ type SidebarDatasetsItemsGroupedDataset,
26
+ } from '#uilib/components/widgets/SidebarDatasetsItemsGrouped';
27
+ import { House } from 'lucide-react';
28
+
29
+ import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
30
+ import { DocsHeaderActions } from '../docsHeaderActions';
31
+ import S from './StandaloneAppLayoutPage.styl';
32
+
33
+ const MOCK_DATASETS: SidebarDatasetsItemsGroupedDataset[] = [
34
+ {
35
+ id: 1,
36
+ name: 'Acetic Acid Price - China - Dollar/MT',
37
+ status: 'active',
38
+ created_at: '2024-01-01',
39
+ updated_at: '2024-01-02',
40
+ keywords: '',
41
+ category: { id: 10, name: 'Chemicals' },
42
+ target_type_id: 1,
43
+ target_type: { id: 1, name: 'Commodity price' },
44
+ trend: 0,
45
+ regular_price: '0',
46
+ sale_price: '0',
47
+ regions: [
48
+ { id: 1, name: 'Asia' },
49
+ { id: 2, name: 'China' },
50
+ ],
51
+ unit: { id: 1, name: 'USD/MT' },
52
+ },
53
+ {
54
+ id: 2,
55
+ name: 'Freight index — spot volume (China)',
56
+ status: 'active',
57
+ created_at: '2024-01-01',
58
+ updated_at: '2024-01-02',
59
+ keywords: '',
60
+ category: { id: 10, name: 'Chemicals' },
61
+ target_type_id: 2,
62
+ target_type: { id: 2, name: 'Freight index' },
63
+ trend: 0,
64
+ regular_price: '0',
65
+ sale_price: '0',
66
+ regions: [
67
+ { id: 1, name: 'Asia' },
68
+ { id: 2, name: 'China' },
69
+ ],
70
+ unit: { id: 1, name: 'Index' },
71
+ },
72
+ {
73
+ id: 3,
74
+ name: 'Ethanol Price Europe Per Kg In USD',
75
+ status: 'active',
76
+ created_at: '2024-01-01',
77
+ updated_at: '2024-01-02',
78
+ keywords: '',
79
+ category: { id: 11, name: 'Energy' },
80
+ target_type_id: 1,
81
+ target_type: { id: 1, name: 'Commodity price' },
82
+ trend: 0,
83
+ regular_price: '0',
84
+ sale_price: '0',
85
+ regions: [{ id: 5, name: 'Europe' }],
86
+ unit: { id: 1, name: 'USD/kg' },
87
+ },
88
+ ];
89
+
90
+ type PreviewPanel = 'home' | 'datasets';
91
+
92
+ const TEST_HEADER_ID = 'test-header-id';
93
+
94
+ function DemoAppSidebar({
95
+ panel,
96
+ onSelectPanel,
97
+ selectedDatasetId,
98
+ onSelectDatasetId,
99
+ }: {
100
+ panel: PreviewPanel;
101
+ onSelectPanel: (p: PreviewPanel) => void;
102
+ selectedDatasetId: number | undefined;
103
+ onSelectDatasetId: (id: number | undefined) => void;
104
+ }) {
105
+ return (
106
+ <Sidebar
107
+ variant="inset"
108
+ collapsible="offcanvas"
109
+ style={{ maxHeight: '100%', height: '100%' }}
110
+ >
111
+ <SidebarContent>
112
+ <SidebarDatasetsItemsGrouped
113
+ preItems={
114
+ <SidebarMenuItem>
115
+ <SidebarMenuButton
116
+ type="button"
117
+ isActive={panel === 'home'}
118
+ onClick={() => onSelectPanel('home')}
119
+ >
120
+ <House size={16} strokeWidth={1.75} aria-hidden />
121
+ Home
122
+ </SidebarMenuButton>
123
+ </SidebarMenuItem>
124
+ }
125
+ groupBy="regions"
126
+ datasets={MOCK_DATASETS}
127
+ selectedDatasetId={selectedDatasetId}
128
+ onDatasetClick={id => {
129
+ onSelectDatasetId(id);
130
+ onSelectPanel('datasets');
131
+ }}
132
+ />
133
+ </SidebarContent>
134
+ </Sidebar>
135
+ );
136
+ }
137
+
138
+ function DemoMainBody({ panel }: { panel: PreviewPanel }) {
139
+ if (panel === 'home') {
140
+ return (
141
+ <div style={{ padding: 'var(--p-6)' }}>
142
+ <h2 style={{ margin: '0 0 0.5rem', fontSize: '1.125rem' }}>Home</h2>
143
+ <p style={{ margin: 0, color: 'var(--muted-foreground)' }}>
144
+ Preview: greenfield shell layout only (no SDK or auth). Real SPA uses
145
+ one top-level Router; this embed cannot nest MemoryRouter under docs
146
+ BrowserRouter — sidebar uses local state instead of{' '}
147
+ <code>NavLink</code> / <code>Routes</code>.
148
+ </p>
149
+ </div>
150
+ );
151
+ }
152
+
153
+ return (
154
+ <div style={{ padding: 'var(--p-6)' }}>
155
+ <h2 style={{ margin: '0 0 0.5rem', fontSize: '1.125rem' }}>Datasets</h2>
156
+ <p style={{ margin: 0, color: 'var(--muted-foreground)' }}>
157
+ Use sidebar links and dataset rows; panel state stays inside this box.
158
+ </p>
159
+ </div>
160
+ );
161
+ }
162
+
163
+ function StandaloneLayoutPreview() {
164
+ const [theme, setTheme] = useState<'light' | 'dark'>('light');
165
+ const [panel, setPanel] = useState<PreviewPanel>('home');
166
+ const [selectedDatasetId, setSelectedDatasetId] = useState<
167
+ number | undefined
168
+ >();
169
+
170
+ const onThemeToggle = useCallback(() => {
171
+ setTheme(t => (t === 'dark' ? 'light' : 'dark'));
172
+ }, []);
173
+
174
+ return (
175
+ <div className={S.preview}>
176
+ <PageScroll rootClassName={S.previewScrollRoot}>
177
+ <AppShell>
178
+ <DemoAppSidebar
179
+ panel={panel}
180
+ onSelectPanel={setPanel}
181
+ selectedDatasetId={selectedDatasetId}
182
+ onSelectDatasetId={setSelectedDatasetId}
183
+ />
184
+
185
+ <AppShellMainContent
186
+ header={<AppHeaderHost anchorId={TEST_HEADER_ID} />}
187
+ footer={
188
+ <PageFooter
189
+ versionLink="#standalone-layout-preview"
190
+ versionLabel="0.0.0-preview"
191
+ />
192
+ }
193
+ >
194
+ <AppHeaderPortal pageHeaderId={TEST_HEADER_ID}>
195
+ <SidebarTrigger />
196
+ <Gap />
197
+ <NavUserHeader
198
+ user={{
199
+ name: 'Preview User',
200
+ email: 'preview@example.com',
201
+ avatar: '',
202
+ }}
203
+ theme={theme}
204
+ onThemeToggle={onThemeToggle}
205
+ onLogout={() => undefined}
206
+ />
207
+ </AppHeaderPortal>
208
+ <DemoMainBody panel={panel} />
209
+ </AppShellMainContent>
210
+ </AppShell>
211
+ </PageScroll>
212
+ </div>
213
+ );
214
+ }
215
+
216
+ export default function StandaloneAppLayoutPage() {
217
+ return (
218
+ <>
219
+ <AppPageHeader
220
+ breadcrumbs={[{ label: 'Standalone app layout' }]}
221
+ title="Standalone app layout"
222
+ subheader={
223
+ <>
224
+ Live preview of AppShell + Sidebar + main column from
225
+ docs/standalone-apps.md §4. <br />
226
+ Full greenfield setup (deps, global CSS, SybilionAuthProvider, SDK)
227
+ lives in that doc — this page is layout only.
228
+ </>
229
+ }
230
+ actions={<DocsHeaderActions />}
231
+ />
232
+ <PageContentSection title="Embedded mini-app (layout preview)">
233
+ <SidebarProvider
234
+ sidebarWidthStorageKey="uilib.docs.standaloneLayout.sidebarWidthPx"
235
+ persistSidebarWidthWithoutConsent
236
+ >
237
+ <StandaloneLayoutPreview />
238
+ </SidebarProvider>
239
+ </PageContentSection>
240
+ </>
241
+ );
242
+ }
@@ -216,6 +216,12 @@ export const DOC_REGISTRY: DocEntry[] = [
216
216
  section: 'Layout',
217
217
  load: () => import('./pages/PagePage'),
218
218
  },
219
+ {
220
+ slug: 'standalone-app-layout',
221
+ title: 'Standalone app layout',
222
+ section: 'Layout',
223
+ load: () => import('./pages/StandaloneAppLayoutPage'),
224
+ },
219
225
  {
220
226
  slug: 'progress',
221
227
  title: 'Progress',