@sybilion/uilib 1.2.4 → 1.2.5

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.
Files changed (42) hide show
  1. package/dist/esm/components/ui/AppHeader/appChromeAnchors.js +3 -1
  2. package/dist/esm/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.js +62 -0
  3. package/dist/esm/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.styl.js +7 -0
  4. package/dist/esm/components/ui/WorkspaceAppSwitcher/workspaceApp.types.js +4 -0
  5. package/dist/esm/components/ui/WorkspaceAppSwitcher/workspaceAppIcons.js +16 -0
  6. package/dist/esm/components/ui/WorkspaceAppSwitcher/workspaceAppPaths.js +29 -0
  7. package/dist/esm/components/ui/WorkspaceAppSwitcher/workspaceAppsConstants.js +4 -0
  8. package/dist/esm/components/ui/WorkspaceAppSwitcher/workspaceAppsLocalStorage.js +84 -0
  9. package/dist/esm/components/widgets/SybilionAppHeader/SybilionAppHeader.js +16 -0
  10. package/dist/esm/components/widgets/SybilionAppHeader/SybilionAppHeader.styl.js +7 -0
  11. package/dist/esm/index.js +8 -1
  12. package/dist/esm/types/src/components/ui/AppHeader/appChromeAnchors.d.ts +2 -0
  13. package/dist/esm/types/src/components/ui/AppHeader/index.d.ts +1 -1
  14. package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.d.ts +12 -0
  15. package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/index.d.ts +7 -0
  16. package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/workspaceApp.types.d.ts +19 -0
  17. package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/workspaceAppIcons.d.ts +9 -0
  18. package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/workspaceAppPaths.d.ts +3 -0
  19. package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/workspaceAppsConstants.d.ts +2 -0
  20. package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/workspaceAppsLocalStorage.d.ts +6 -0
  21. package/dist/esm/types/src/components/widgets/SybilionAppHeader/SybilionAppHeader.d.ts +8 -0
  22. package/dist/esm/types/src/components/widgets/SybilionAppHeader/index.d.ts +1 -0
  23. package/dist/esm/types/src/index.d.ts +2 -0
  24. package/docs/standalone-apps.md +113 -52
  25. package/package.json +1 -1
  26. package/src/components/ui/AppHeader/appChromeAnchors.ts +3 -0
  27. package/src/components/ui/AppHeader/index.ts +1 -1
  28. package/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.styl +91 -0
  29. package/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.styl.d.ts +15 -0
  30. package/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.tsx +163 -0
  31. package/src/components/ui/WorkspaceAppSwitcher/index.ts +20 -0
  32. package/src/components/ui/WorkspaceAppSwitcher/workspaceApp.types.ts +21 -0
  33. package/src/components/ui/WorkspaceAppSwitcher/workspaceAppIcons.ts +27 -0
  34. package/src/components/ui/WorkspaceAppSwitcher/workspaceAppPaths.ts +34 -0
  35. package/src/components/ui/WorkspaceAppSwitcher/workspaceAppsConstants.ts +2 -0
  36. package/src/components/ui/WorkspaceAppSwitcher/workspaceAppsLocalStorage.ts +95 -0
  37. package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.styl +7 -0
  38. package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.styl.d.ts +7 -0
  39. package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.tsx +51 -0
  40. package/src/components/widgets/SybilionAppHeader/index.ts +4 -0
  41. package/src/docs/pages/StandaloneAppLayoutPage.tsx +102 -34
  42. package/src/index.ts +2 -0
@@ -2,7 +2,7 @@
2
2
 
3
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:** `AppShell` + `AppShellMainContent`; spacing via uilib primitives (e.g. `Gap`), not ad hoc gutters. **Each `<Route>` page:** follow **§4 Route page body** (one canonical pattern below); **Discovering** explains how to find other `@sybilion/uilib` exports.
6
6
 
7
7
  **Local-first:** After scaffolding, the user should run **`yarn dev`** (or `npm run dev`) on **localhost** with **no deploy** (e.g. Vercel) required. Commit **`.env.example`**; the user copies it to **`.env`**. In development, the Sybilion API is reached via a **same-origin `/api` proxy** so the browser avoids CORS; production builds still use `VITE_SYBILION_API_BASE_URL` on the client, and the real API must allow **CORS** for your deployed `Origin` unless you terminate API calls on the same host.
8
8
 
@@ -41,12 +41,12 @@ Add a **`dev`** script so the app is runnable immediately after clone. Typical V
41
41
 
42
42
  Commit **`.env.example`** at the app root (no secrets). Minimum variables:
43
43
 
44
- | Variable | Purpose |
45
- | -------- | ------- |
46
- | `PORT` | Vite **dev** and **preview** server port. **`PORT=3000`** matches the current **Auth0 test tenant** (`http://localhost:3000` callback / web origins). Override if your tenant uses another port. |
47
- | `VITE_SYBILION_API_BASE_URL` | Real Sybilion API origin (no trailing slash). Used as **proxy target** in dev and as SDK `baseUrl` in production builds. |
48
- | `VITE_AUTH0_DOMAIN` | Auth0 domain (§3). |
49
- | `VITE_AUTH0_CLIENT_ID` | Auth0 SPA client id (§3). |
44
+ | Variable | Purpose |
45
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
46
+ | `PORT` | Vite **dev** and **preview** server port. **`PORT=3000`** matches the current **Auth0 test tenant** (`http://localhost:3000` callback / web origins). Override if your tenant uses another port. |
47
+ | `VITE_SYBILION_API_BASE_URL` | Real Sybilion API origin (no trailing slash). Used as **proxy target** in dev and as SDK `baseUrl` in production builds. |
48
+ | `VITE_AUTH0_DOMAIN` | Auth0 domain (§3). |
49
+ | `VITE_AUTH0_CLIENT_ID` | Auth0 SPA client id (§3). |
50
50
 
51
51
  Example **`.env.example`** content for the app root:
52
52
 
@@ -102,9 +102,9 @@ Package README: [`@sybilion/sdk`](https://www.npmjs.com/package/@sybilion/sdk)
102
102
  Avoid browser CORS in development by serving the SPA from Vite and proxying **`/api`** to the real API. Use **`sybilionStandaloneViteDev`** from `@sybilion/uilib/vite-standalone-dev` in **`vite.config.ts`**: it reads **`PORT`** (defaults to **3000** if unset or invalid) for **`server`** / **`preview`**, and sets **`proxy['/api']`** → **`VITE_SYBILION_API_BASE_URL`** with `changeOrigin` and `secure: true`.
103
103
 
104
104
  ```ts
105
- import { defineConfig } from 'vite';
106
- import react from '@vitejs/plugin-react';
107
105
  import { sybilionStandaloneViteDev } from '@sybilion/uilib/vite-standalone-dev';
106
+ import react from '@vitejs/plugin-react';
107
+ import { defineConfig } from 'vite';
108
108
 
109
109
  export default defineConfig(({ mode }) => ({
110
110
  ...sybilionStandaloneViteDev({ mode }),
@@ -114,6 +114,14 @@ export default defineConfig(({ mode }) => ({
114
114
 
115
115
  Combine with an **empty `baseUrl` in dev** in the SDK module (§2). In **`preview`** builds, keep **`.env`** with **`VITE_SYBILION_API_BASE_URL`** so `vite preview` can still proxy API calls locally.
116
116
 
117
+ ## Discovering `@sybilion/uilib` components (agents)
118
+
119
+ Prefer **`@sybilion/uilib`** over bespoke layout/controls.
120
+
121
+ 1. **Barrel:** uilib repo `src/index.ts`, or **`node_modules/@sybilion/uilib/dist/esm/types/index.d.ts`** after install — scan `export * from './components/ui/...'`.
122
+ 2. **Page cluster:** `PageScroll`, `AppShell`, `PageHeader`, `PageFooter`, tabs/columns, **`SybilionAppHeader`** (workspace switcher + **`NavUserHeader`**), etc. come from the same package; **what to compose and when** lives only in **§4 Route page body** (not repeated here).
123
+ 3. **Examples:** `src/docs/pages/*Page.tsx`; **`PagePage`** (`slug page`) — minimal **`PageContent` + `PageContentSection`**.
124
+
117
125
  ## Local dev: apps with a Go server
118
126
 
119
127
  Some templates include a **Go** server. This repo does **not** provide Go middleware. The contract: whatever serves the **same origin the browser uses for the SPA** must **reverse-proxy** path prefix **`/api`** (or your SDK `apiPrefix`) to the Sybilion API. Use **`baseUrl: ''`** in dev when those requests are same-origin. Name and document server-side env vars (e.g. API upstream URL) in the Go project.
@@ -206,26 +214,32 @@ export function App() {
206
214
 
207
215
  ### `AppLayout` (sidebar + main + `children`)
208
216
 
209
- `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.
217
+ `AppSidebar` is a sibling of `AppShellMainContent` inside `AppShell`. The matched route (`<Routes>` from `App.tsx`) renders as `{children}` in the main column.
218
+
219
+ **Header row:** pass **`header={<AppHeaderHost />}`** to **`AppShellMainContent`**, then **`SybilionAppHeader`** as the **first** child before `{children}`. **`SybilionAppHeader`** portals workspace switcher + embedded **`NavUserHeader`** into that shell header (and **`page-header-actions`** so **`PageHeader`** toolbars line up).
220
+
221
+ Props: all **`WorkspaceAppSwitcher`** props plus all **`NavUserHeader`** props (`user`, **`menuItems`** as **`DropdownMenuItem`** rows, **`theme`**, **`onLogout`**, etc.), and optional **`pageHeaderId`**, **`actionsAnchorId`**, **`actionsAnchorClassName`**.
210
222
 
211
223
  ```tsx
212
224
  import type { ReactNode } from 'react';
225
+ import { useLocation, useNavigate } from 'react-router-dom';
213
226
 
214
227
  import {
215
228
  AppHeaderHost,
216
- AppHeaderPortal,
217
229
  AppShell,
218
230
  AppShellMainContent,
219
- Gap,
220
- NavUserHeader,
231
+ DropdownMenuItem,
221
232
  PageFooter,
222
233
  PageScroll,
223
- SidebarTrigger,
234
+ SybilionAppHeader,
224
235
  } from '@sybilion/uilib';
225
236
 
226
237
  import { AppSidebar } from './AppSidebar';
227
238
 
228
239
  export function AppLayout({ children }: { children: ReactNode }) {
240
+ const location = useLocation();
241
+ const navigate = useNavigate();
242
+
229
243
  return (
230
244
  <PageScroll>
231
245
  <AppShell>
@@ -235,16 +249,24 @@ export function AppLayout({ children }: { children: ReactNode }) {
235
249
  header={<AppHeaderHost />}
236
250
  footer={<PageFooter versionLink="/releases" versionLabel="0.0.1" />}
237
251
  >
238
- <AppHeaderPortal>
239
- <SidebarTrigger />
240
- <Gap />
241
- <NavUserHeader
242
- theme="light"
243
- onThemeToggle={() => undefined}
244
- onLogout={() => undefined}
245
- isAuthenticated={false}
246
- />
247
- </AppHeaderPortal>
252
+ <SybilionAppHeader
253
+ pathname={location.pathname}
254
+ onNavigate={href => navigate(href)}
255
+ authenticated={false}
256
+ appsStorageKey="myapp.workspaceApps"
257
+ defaultApps={[]}
258
+ user={{ name: 'Analyst', email: 'you@example.com', avatar: '' }}
259
+ theme="light"
260
+ onThemeToggle={() => undefined}
261
+ onLogout={() => undefined}
262
+ isAuthenticated={false}
263
+ menuItems={
264
+ <>
265
+ <DropdownMenuItem>Account</DropdownMenuItem>
266
+ <DropdownMenuItem>Settings</DropdownMenuItem>
267
+ </>
268
+ }
269
+ />
248
270
  {children}
249
271
  </AppShellMainContent>
250
272
  </AppShell>
@@ -253,7 +275,7 @@ export function AppLayout({ children }: { children: ReactNode }) {
253
275
  }
254
276
  ```
255
277
 
256
- Wire `NavUserHeader` to real auth (`useSybilionAuth`, theme context, etc.; §3). Demo in repo: `src/docs/pages/NavUserHeaderPage.tsx` (slug `nav-user-header`).
278
+ Wire **`authenticated`**, **`user`** / **`isAuthenticated`**, **`theme`** / **`onLogout`**, **`menuItems`**, and **`defaultApps`** / **`appsStorageKey`** to real auth and workspace config (`useSybilionAuth`, §3). **`NavUserHeader`** behavior reference: `src/docs/pages/NavUserHeaderPage.tsx` (slug `nav-user-header`). Full shell preview: `src/docs/pages/StandaloneAppLayoutPage.tsx` (slug **`standalone-app-layout`**).
257
279
 
258
280
  #### Sidebar (`AppSidebar.tsx`)
259
281
 
@@ -326,41 +348,80 @@ export function AppSidebar() {
326
348
 
327
349
  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`.
328
350
 
351
+ ### Route page body (`PageHeader`, `PageContent`, `PageContentSection`)
352
+
353
+ **Canonical rule for this doc:** the shell (`AppLayout`) owns **global** chrome only. Every **route** (component under `<Routes>`) builds the main column with **`PageHeader` → `PageContent` → `PageContentSection`** (and other uilib primitives inside sections)—not a padded root `<div>`/`<main>` unless nothing fits.
354
+
355
+ Pieces:
356
+
357
+ | Piece | Role |
358
+ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
359
+ | **`PageHeader`** | Route title (`h1`), optional **breadcrumbs**, **subheader**, **actions** (toolbar). Renders the shared header chrome (collapsing title on scroll uses `PageContext` from `PageScroll`). |
360
+ | **`PageContent`** | Outer wrapper for the scrollable main column body. Use **`variant="clean"`** when you need the alternate spacing preset. |
361
+ | **`PageContentSection`** | Vertical **section** blocks inside the page (group related UI; stack multiple sections). Accepts normal `div` props (e.g. `className`, `style`; HTML **`title`** is the native tooltip attribute, not a section heading—pass a real heading element as a child if needed). |
362
+
363
+ **Example — `HomePage.tsx` (fragment inside `AppShellMainContent` / `<Routes>`):**
364
+
365
+ ```tsx
366
+ import { PageContent, PageContentSection, PageHeader } from '@sybilion/uilib';
367
+
368
+ export function HomePage() {
369
+ return (
370
+ <>
371
+ <PageHeader
372
+ breadcrumbs={[{ label: 'Home', href: '/' }]}
373
+ title="Home"
374
+ subheader="Short route description."
375
+ />
376
+ <PageContent>
377
+ <PageContentSection>
378
+ {/* tables, cards, charts — use uilib components where they exist */}
379
+ </PageContentSection>
380
+ </PageContent>
381
+ </>
382
+ );
383
+ }
384
+ ```
385
+
386
+ Set **`breadcrumbSidebarTrigger={false}`** on `PageHeader` only when **no** `SidebarProvider` wraps the tree (rare for standalone apps; default is fine when the sidebar exists).
387
+
329
388
  ### Full pattern
330
389
 
331
- 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.
390
+ Composition: `PageScroll` → `AppShell` → `AppSidebar` → `AppShellMainContent` with **`AppHeaderHost`** + **`SybilionAppHeader`**, `PageFooter`, and the active route as `children`. **`SidebarProvider`** wraps `AppLayout`. **Route main column:** subsection **Route page body** above. Add `Theme` from `@homecode/ui` only when your product uses those primitives alongside uilib.
332
391
 
333
392
  ### Greenfield checklist (agents)
334
393
 
335
- | Step | Deliverable |
336
- | ---- | ----------- |
337
- | Env files | **`.env.example`** (committed) + **`.env`** locally; **`PORT=3000`** recommended for Auth0 test tenant; **`VITE_*`** as in §1 table. |
338
- | Scripts | **`package.json`** includes mandatory **`dev`** (e.g. `"vite"`); optional **`build`**, **`preview`**. |
339
- | Vite proxy | **`vite.config.ts`** spreads **`sybilionStandaloneViteDev({ mode })`**; SDK **`baseUrl`** empty in dev (§2). |
340
- | Files | `src/libs/sybilion-sdk.ts`, `AppProviders.tsx`, `AppLayout.tsx`, `AppSidebar.tsx`, `App.tsx`, `main.tsx`, route pages under e.g. `src/pages/`. |
341
- | Auth0 | SPA callback / logout URLs + allowed web origins **`http://localhost:<PORT>`** for local dev and deploy URLs (and previews). |
342
- | API | **Dev:** proxy handles API traffic (no browser CORS to API). **Prod:** Sybilion backend **CORS** your deploy `Origin`, unless API is same-origin. |
343
- | Go (if applicable) | Server proxies **`/api`** to Sybilion; SPA dev **`baseUrl`** stays `''` when same-origin. |
394
+ | Step | Deliverable |
395
+ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
396
+ | Env files | **`.env.example`** (committed) + **`.env`** locally; **`PORT=3000`** recommended for Auth0 test tenant; **`VITE_*`** as in §1 table. |
397
+ | Scripts | **`package.json`** includes mandatory **`dev`** (e.g. `"vite"`); optional **`build`**, **`preview`**. |
398
+ | Vite proxy | **`vite.config.ts`** spreads **`sybilionStandaloneViteDev({ mode })`**; SDK **`baseUrl`** empty in dev (§2). |
399
+ | Files | `src/libs/sybilion-sdk.ts`, `AppProviders.tsx`, `AppLayout.tsx`, `AppSidebar.tsx`, `App.tsx`, `main.tsx`, route pages under e.g. `src/pages/`. |
400
+ | Pages + UI | **§4 Route page body** (mandatory stack) + **Discovering** (barrel-first; bespoke markup only when no export fits). |
401
+ | Auth0 | SPA callback / logout URLs + allowed web origins **`http://localhost:<PORT>`** for local dev and deploy URLs (and previews). |
402
+ | API | **Dev:** proxy handles API traffic (no browser CORS to API). **Prod:** Sybilion backend **CORS** your deploy `Origin`, unless API is same-origin. |
403
+ | Go (if applicable) | Server proxies **`/api`** to Sybilion; SPA dev **`baseUrl`** stays `''` when same-origin. |
344
404
 
345
405
  ### Glossary (high-use pieces)
346
406
 
347
- | Component / API | What it is for |
348
- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
349
- | `PageScroll` | Page-level vertical scroll wrapper; usual outer shell for the app body. |
350
- | `AppShell` | Layout grid container; `Sidebar` + `AppShellMainContent` as siblings inside it. |
351
- | `AppShellMainContent` | Main column: `header`, scrollable body (`children`), `footer`. |
352
- | `AppHeaderHost` | Top header anchor (DOM id `page-header`); stays empty until `AppHeaderPortal` portals into it. |
353
- | `AppHeaderPortal` | Portals into `AppHeaderHost` `SidebarTrigger`, `NavUserHeader`, theme toggle. |
354
- | `NavUserHeader` | Header user menu (avatar, account, theme, logout). |
355
- | `Sidebar`, `SidebarProvider` | Collapsible rail + context (`@sybilion/uilib`). Wrap `SidebarProvider` above `AppLayout`; render `Sidebar` inside `AppShell` (usually via `AppSidebar`). |
356
- | `AppSidebar` | App-specific component (`src/AppSidebar.tsx`) composing `Sidebar` + nav links + product widgets. Keeps `AppLayout` generic. |
357
- | `SidebarDatasetsItemsGrouped` | Dataset list widget for the sidebar: collapsible groups (`regions` / `target_type` / `categories`) with nested rows + selection callback. |
358
- | `SidebarTrigger` | Toggle sidebar visibility (especially mobile / `offcanvas`). |
359
- | `PageFooter` | Standard footer; requires `versionLink` + `versionLabel`. |
360
- | `Gap` | Spacing primitive between flex children. |
361
- | `SybilionAuthProvider` | Auth0 + Sybilion JWT (§3). |
362
- | `useSybilionAuth` | User session + login/logout under provider. |
363
- | `useSybilionApiFetch` | Authenticated `fetch` using stored JWT. |
407
+ | Component / API | What it is for |
408
+ | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
409
+ | `PageScroll` | Page-level vertical scroll wrapper; usual outer shell for the app body. |
410
+ | `AppShell` | Layout grid container; `Sidebar` + `AppShellMainContent` as siblings inside it. |
411
+ | `AppShellMainContent` | Main column: `header`, scrollable body (`children`), `footer`. |
412
+ | `SybilionAppHeader` | **Default** standalone top header: **`AppShellMainContent`** `header` = **`AppHeaderHost`**, first body child = **`SybilionAppHeader`** — workspace switcher + embedded **`NavUserHeader`** (props incl. **`menuItems`**); aligns **`PageHeader`** via **`page-header-actions`**. |
413
+ | `WorkspaceAppSwitcher` | Dropdown of workspace apps (`WorkspaceAppEntry[]`); composed inside **`SybilionAppHeader`**. |
414
+ | `NavUserHeader` | Header user menu (avatar, account, theme, logout). |
415
+ | `Sidebar`, `SidebarProvider` | Collapsible rail + context (`@sybilion/uilib`). Wrap `SidebarProvider` above `AppLayout`; render `Sidebar` inside `AppShell` (usually via `AppSidebar`). |
416
+ | `AppSidebar` | App-specific component (`src/AppSidebar.tsx`) composing `Sidebar` + nav links + product widgets. Keeps `AppLayout` generic. |
417
+ | `SidebarDatasetsItemsGrouped` | Dataset list widget for the sidebar: collapsible groups (`regions` / `target_type` / `categories`) with nested rows + selection callback. |
418
+ | `SidebarTrigger` | Toggle sidebar visibility (especially mobile / `offcanvas`). |
419
+ | `PageFooter` | Standard footer; requires `versionLink` + `versionLabel`. |
420
+ | `Gap` | Spacing primitive between flex children. |
421
+ | `PageHeader`, `PageContent`, `PageContentSection`, … | **§4 Route page body** (roles, `breadcrumbSidebarTrigger`, example). Same barrel also has `PageTabs`, `PageColumns`, `SectionHeader`, `PageEmptyCanvas`, … |
422
+ | `SybilionAuthProvider` | Auth0 + Sybilion JWT (§3). |
423
+ | `useSybilionAuth` | User session + login/logout under provider. |
424
+ | `useSybilionApiFetch` | Authenticated `fetch` using stored JWT. |
364
425
 
365
426
  ## 5. Data
366
427
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,2 +1,5 @@
1
1
  /** DOM anchor id for portaling page-level header actions into `AppHeader`. */
2
2
  export const PAGE_HEADER_ID = 'page-header';
3
+
4
+ /** DOM id for `#page-header-actions`; `AppHeaderActions` portaling target. */
5
+ export const PAGE_HEADER_ACTIONS_ID = 'page-header-actions';
@@ -1,3 +1,3 @@
1
1
  export type { AppHeaderProps, AppHeaderPortalProps } from './AppHeader';
2
2
  export { AppHeaderHost, AppHeaderPortal } from './AppHeader';
3
- export { PAGE_HEADER_ID } from './appChromeAnchors';
3
+ export { PAGE_HEADER_ACTIONS_ID, PAGE_HEADER_ID } from './appChromeAnchors';
@@ -0,0 +1,91 @@
1
+ @import '../../../lib/theme.styl'
2
+
3
+ .trigger
4
+ display flex
5
+ align-items center
6
+ gap var(--p-2)
7
+ max-width 320px
8
+ height auto
9
+ padding var(--p-1) !important
10
+ padding-right var(--p-3) !important
11
+ margin-left var(--p-3) !important
12
+
13
+ border-radius 12px
14
+ border none
15
+ background transparent
16
+ cursor pointer
17
+ color inherit
18
+ font inherit
19
+ text-align left
20
+
21
+ &:hover
22
+ background-color var(--muted)
23
+
24
+ .iconTile
25
+ position relative
26
+ display flex
27
+ align-items center
28
+ justify-content center
29
+ flex-shrink 0
30
+ width 40px
31
+ height 40px
32
+ border-radius 10px
33
+ color var(--fg-color)
34
+
35
+ &::before
36
+ &::after
37
+ position absolute
38
+ content ''
39
+ display block
40
+ width 100%
41
+ height 100%
42
+ border-radius inherit
43
+ &::before
44
+ background-color var(--background)
45
+ &::after
46
+ background-color var(--bg-color)
47
+
48
+ .icon
49
+ z-index 1
50
+ width 22px !important
51
+ height 22px !important
52
+ color var(--fg-color) !important
53
+
54
+ .textCol
55
+ display flex
56
+ flex-direction column
57
+ min-width 0
58
+ flex 1
59
+ gap 2px
60
+
61
+ .name
62
+ font-weight 600
63
+ font-size var(--text-sm)
64
+ line-height 1.2
65
+ color var(--foreground)
66
+ white-space nowrap
67
+ overflow hidden
68
+ text-overflow ellipsis
69
+
70
+ .sub
71
+ font-size var(--text-xs)
72
+ line-height 1.2
73
+ color var(--muted-foreground)
74
+ white-space nowrap
75
+ overflow hidden
76
+ text-overflow ellipsis
77
+
78
+ .menuContent
79
+ min-width 280px
80
+ max-width 360px
81
+
82
+ .item
83
+ display flex
84
+ align-items center
85
+ gap var(--p-3)
86
+ padding var(--p-3)
87
+ cursor pointer
88
+ outline none
89
+
90
+ .itemActive
91
+ background-color var(--muted)
@@ -0,0 +1,15 @@
1
+ // This file is automatically generated.
2
+ // Please do not change this file!
3
+ interface CssExports {
4
+ 'icon': string;
5
+ 'iconTile': string;
6
+ 'item': string;
7
+ 'itemActive': string;
8
+ 'menuContent': string;
9
+ 'name': string;
10
+ 'sub': string;
11
+ 'textCol': string;
12
+ 'trigger': string;
13
+ }
14
+ export const cssExports: CssExports;
15
+ export default cssExports;
@@ -0,0 +1,163 @@
1
+ import cn from 'classnames';
2
+ import type { CSSProperties } from 'react';
3
+ import { useEffect, useState } from 'react';
4
+
5
+ import { Button } from '#uilib/components/ui/Button/Button';
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuTrigger,
11
+ } from '#uilib/components/ui/DropdownMenu';
12
+ import type { WorkspaceAppEntry } from '#uilib/components/ui/WorkspaceAppSwitcher/workspaceApp.types';
13
+ import { WORKSPACE_APP_ICONS } from '#uilib/components/ui/WorkspaceAppSwitcher/workspaceAppIcons';
14
+ import { findWorkspaceAppByPathname } from '#uilib/components/ui/WorkspaceAppSwitcher/workspaceAppPaths';
15
+ import { readWorkspaceAppsFromLocalStorage } from '#uilib/components/ui/WorkspaceAppSwitcher/workspaceAppsLocalStorage';
16
+ import { ChevronDown } from 'lucide-react';
17
+
18
+ import S from './WorkspaceAppSwitcher.styl';
19
+
20
+ export type WorkspaceAppSwitcherProps = {
21
+ pathname: string;
22
+ onNavigate: (href: string) => void;
23
+ /** When false, renders nothing (host maps from auth Loading + signed-in). */
24
+ authenticated?: boolean;
25
+ /** Fallback when localStorage missing/invalid/unset — keep referentially stable across renders where possible */
26
+ defaultApps?: WorkspaceAppEntry[];
27
+ /** When set, read apps from localStorage (`readWorkspaceAppsFromLocalStorage`). */
28
+ appsStorageKey?: string;
29
+ };
30
+
31
+ function entryKey(entry: WorkspaceAppEntry): string {
32
+ return entry.id;
33
+ }
34
+
35
+ function IconTile({
36
+ iconKey,
37
+ accentMuted,
38
+ accent,
39
+ }: {
40
+ iconKey: WorkspaceAppEntry['iconKey'];
41
+ accentMuted: string;
42
+ accent: string;
43
+ }) {
44
+ const IconComponent = WORKSPACE_APP_ICONS[iconKey];
45
+ return (
46
+ <span
47
+ className={S.iconTile}
48
+ style={
49
+ {
50
+ '--bg-color': accentMuted,
51
+ '--fg-color': accent,
52
+ } as CSSProperties
53
+ }
54
+ >
55
+ <IconComponent className={S.icon} aria-hidden />
56
+ </span>
57
+ );
58
+ }
59
+
60
+ function useResolvedApps(
61
+ appsStorageKey: string | undefined,
62
+ defaultApps: WorkspaceAppEntry[] | undefined,
63
+ ): WorkspaceAppEntry[] {
64
+ const [apps, setApps] = useState<WorkspaceAppEntry[]>(() => {
65
+ if (
66
+ typeof localStorage !== 'undefined' &&
67
+ appsStorageKey != null &&
68
+ appsStorageKey !== ''
69
+ ) {
70
+ const fromLs = readWorkspaceAppsFromLocalStorage(appsStorageKey);
71
+ if (fromLs != null && fromLs.length > 0) {
72
+ return fromLs;
73
+ }
74
+ }
75
+ return defaultApps ?? [];
76
+ });
77
+
78
+ useEffect(() => {
79
+ if (!appsStorageKey || typeof localStorage === 'undefined') {
80
+ setApps(defaultApps ?? []);
81
+ return;
82
+ }
83
+ const fromLs = readWorkspaceAppsFromLocalStorage(appsStorageKey);
84
+ setApps(fromLs != null && fromLs.length > 0 ? fromLs : (defaultApps ?? []));
85
+ }, [appsStorageKey, defaultApps]);
86
+
87
+ return apps;
88
+ }
89
+
90
+ export function WorkspaceAppSwitcher({
91
+ pathname,
92
+ onNavigate,
93
+ authenticated = true,
94
+ defaultApps,
95
+ appsStorageKey,
96
+ }: WorkspaceAppSwitcherProps) {
97
+ const apps = useResolvedApps(appsStorageKey, defaultApps);
98
+
99
+ const current = findWorkspaceAppByPathname(pathname, apps);
100
+
101
+ if (!authenticated || apps.length === 0) {
102
+ return null;
103
+ }
104
+
105
+ const displayApp = current ?? apps[0];
106
+ if (!displayApp) {
107
+ return null;
108
+ }
109
+
110
+ return (
111
+ <DropdownMenu>
112
+ <DropdownMenuTrigger asChild>
113
+ <Button
114
+ variant="ghost"
115
+ className={S.trigger}
116
+ aria-label="Select workspace app"
117
+ >
118
+ <IconTile
119
+ iconKey={displayApp.iconKey}
120
+ accentMuted={displayApp.accentMuted}
121
+ accent={displayApp.accent}
122
+ />
123
+ <span className={S.textCol}>
124
+ <span className={S.name}>{displayApp.displayName}</span>
125
+ <span className={S.sub}>{displayApp.subtitle}</span>
126
+ </span>
127
+ <ChevronDown size={12} />
128
+ </Button>
129
+ </DropdownMenuTrigger>
130
+ <DropdownMenuContent
131
+ className={S.menuContent}
132
+ align="start"
133
+ sideOffset={8}
134
+ elevation="md"
135
+ >
136
+ {apps.map(entry => {
137
+ const active =
138
+ current != null ? entryKey(entry) === entryKey(current) : false;
139
+
140
+ return (
141
+ <DropdownMenuItem
142
+ key={entry.id}
143
+ className={cn(S.item, active && S.itemActive)}
144
+ onSelect={() => {
145
+ onNavigate(entry.href);
146
+ }}
147
+ >
148
+ <IconTile
149
+ iconKey={entry.iconKey}
150
+ accentMuted={entry.accentMuted}
151
+ accent={entry.accent}
152
+ />
153
+ <span className={S.textCol}>
154
+ <span className={S.name}>{entry.displayName}</span>
155
+ <span className={S.sub}>{entry.subtitle}</span>
156
+ </span>
157
+ </DropdownMenuItem>
158
+ );
159
+ })}
160
+ </DropdownMenuContent>
161
+ </DropdownMenu>
162
+ );
163
+ }
@@ -0,0 +1,20 @@
1
+ export {
2
+ WorkspaceAppSwitcher,
3
+ type WorkspaceAppSwitcherProps,
4
+ } from './WorkspaceAppSwitcher';
5
+ export type { WorkspaceAppEntry } from './workspaceApp.types';
6
+ export { WORKSPACE_APP_SLUG_BASE_PATH } from './workspaceApp.types';
7
+ export { WORKSPACE_APPS_LS_KEY } from './workspaceAppsConstants';
8
+ export {
9
+ readWorkspaceAppsFromLocalStorage,
10
+ writeWorkspaceAppsToLocalStorage,
11
+ } from './workspaceAppsLocalStorage';
12
+ export {
13
+ WORKSPACE_APP_ICONS,
14
+ type WorkspaceAppIconKey,
15
+ isWorkspaceAppIconKey,
16
+ } from './workspaceAppIcons';
17
+ export {
18
+ findWorkspaceAppByPathname,
19
+ workspaceAppSlugPath,
20
+ } from './workspaceAppPaths';
@@ -0,0 +1,21 @@
1
+ import type { WorkspaceAppIconKey } from './workspaceAppIcons';
2
+
3
+ /** Path segment for slug apps: pathname matches `/apps/{id}` */
4
+ export const WORKSPACE_APP_SLUG_BASE_PATH = '/apps';
5
+
6
+ /** One surface in the workspace app switcher (serializable for localStorage). */
7
+ export type WorkspaceAppEntry = {
8
+ /** Slug (e.g. `my-custom-app` → `https://sybilion.io/apps/my-custom-app` via `href`). */
9
+ id: string;
10
+ displayName: string;
11
+ subtitle: string;
12
+ iconKey: WorkspaceAppIconKey;
13
+ accent: string;
14
+ accentMuted: string;
15
+ href: string;
16
+ /**
17
+ * Optional. Native / multi-route shells: match current app by pathname prefix.
18
+ * Omit for custom apps deployed under a single slug (`/apps/{id}` only).
19
+ */
20
+ matchPathPrefixes?: readonly string[];
21
+ };
@@ -0,0 +1,27 @@
1
+ import type { ComponentType } from 'react';
2
+
3
+ import { Boat, Package, TreeStructure } from '@phosphor-icons/react';
4
+ import { ClockAlert, LayoutDashboard } from 'lucide-react';
5
+
6
+ type IconLike = ComponentType<{ className?: string; 'aria-hidden'?: boolean }>;
7
+
8
+ export type WorkspaceAppIconKey =
9
+ | 'grid-four'
10
+ | 'clock'
11
+ | 'boat'
12
+ | 'package'
13
+ | 'tree-structure';
14
+
15
+ export const WORKSPACE_APP_ICONS: Record<WorkspaceAppIconKey, IconLike> = {
16
+ 'grid-four': LayoutDashboard as IconLike,
17
+ clock: ClockAlert as IconLike,
18
+ boat: Boat as IconLike,
19
+ package: Package as IconLike,
20
+ 'tree-structure': TreeStructure as IconLike,
21
+ };
22
+
23
+ const ICON_KEYS = Object.keys(WORKSPACE_APP_ICONS) as WorkspaceAppIconKey[];
24
+
25
+ export function isWorkspaceAppIconKey(v: string): v is WorkspaceAppIconKey {
26
+ return (ICON_KEYS as string[]).includes(v);
27
+ }