@sybilion/uilib 1.2.3 → 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.
- package/dist/esm/components/ui/AppHeader/appChromeAnchors.js +3 -1
- package/dist/esm/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.js +62 -0
- package/dist/esm/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.styl.js +7 -0
- package/dist/esm/components/ui/WorkspaceAppSwitcher/workspaceApp.types.js +4 -0
- package/dist/esm/components/ui/WorkspaceAppSwitcher/workspaceAppIcons.js +16 -0
- package/dist/esm/components/ui/WorkspaceAppSwitcher/workspaceAppPaths.js +29 -0
- package/dist/esm/components/ui/WorkspaceAppSwitcher/workspaceAppsConstants.js +4 -0
- package/dist/esm/components/ui/WorkspaceAppSwitcher/workspaceAppsLocalStorage.js +84 -0
- package/dist/esm/components/widgets/SybilionAppHeader/SybilionAppHeader.js +16 -0
- package/dist/esm/components/widgets/SybilionAppHeader/SybilionAppHeader.styl.js +7 -0
- package/dist/esm/index.js +8 -1
- package/dist/esm/types/src/components/ui/AppHeader/appChromeAnchors.d.ts +2 -0
- package/dist/esm/types/src/components/ui/AppHeader/index.d.ts +1 -1
- package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.d.ts +12 -0
- package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/index.d.ts +7 -0
- package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/workspaceApp.types.d.ts +19 -0
- package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/workspaceAppIcons.d.ts +9 -0
- package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/workspaceAppPaths.d.ts +3 -0
- package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/workspaceAppsConstants.d.ts +2 -0
- package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/workspaceAppsLocalStorage.d.ts +6 -0
- package/dist/esm/types/src/components/widgets/SybilionAppHeader/SybilionAppHeader.d.ts +8 -0
- package/dist/esm/types/src/components/widgets/SybilionAppHeader/index.d.ts +1 -0
- package/dist/esm/types/src/index.d.ts +2 -0
- package/dist/standalone/vite-sybilion-standalone-dev.d.ts +13 -0
- package/dist/standalone/vite-sybilion-standalone-dev.js +49 -0
- package/docs/standalone-apps.md +172 -43
- package/package.json +13 -3
- package/src/components/ui/AppHeader/appChromeAnchors.ts +3 -0
- package/src/components/ui/AppHeader/index.ts +1 -1
- package/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.styl +91 -0
- package/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.styl.d.ts +15 -0
- package/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.tsx +163 -0
- package/src/components/ui/WorkspaceAppSwitcher/index.ts +20 -0
- package/src/components/ui/WorkspaceAppSwitcher/workspaceApp.types.ts +21 -0
- package/src/components/ui/WorkspaceAppSwitcher/workspaceAppIcons.ts +27 -0
- package/src/components/ui/WorkspaceAppSwitcher/workspaceAppPaths.ts +34 -0
- package/src/components/ui/WorkspaceAppSwitcher/workspaceAppsConstants.ts +2 -0
- package/src/components/ui/WorkspaceAppSwitcher/workspaceAppsLocalStorage.ts +95 -0
- package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.styl +7 -0
- package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.styl.d.ts +7 -0
- package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.tsx +51 -0
- package/src/components/widgets/SybilionAppHeader/index.ts +4 -0
- package/src/docs/pages/StandaloneAppLayoutPage.tsx +102 -34
- package/src/index.ts +2 -0
- package/src/standalone/vite-sybilion-standalone-dev.ts +65 -0
package/docs/standalone-apps.md
CHANGED
|
@@ -2,12 +2,17 @@
|
|
|
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:**
|
|
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
|
+
|
|
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.
|
|
6
8
|
|
|
7
9
|
## 1. Dependencies and global CSS
|
|
8
10
|
|
|
11
|
+
Vite-based apps (recommended for new standalone SPAs):
|
|
12
|
+
|
|
9
13
|
```bash
|
|
10
14
|
yarn add react react-dom react-router-dom @auth0/auth0-react @sybilion/uilib @sybilion/sdk
|
|
15
|
+
yarn add -D vite @vitejs/plugin-react
|
|
11
16
|
```
|
|
12
17
|
|
|
13
18
|
Import tokens/fonts once (typically `src/main.tsx`):
|
|
@@ -18,6 +23,43 @@ import '@sybilion/uilib/standalone-global.css';
|
|
|
18
23
|
|
|
19
24
|
Mount the tree with `ReactDOM.createRoot` → `App` (wrap with `StrictMode` if you want).
|
|
20
25
|
|
|
26
|
+
### `package.json` scripts (required)
|
|
27
|
+
|
|
28
|
+
Add a **`dev`** script so the app is runnable immediately after clone. Typical Vite setup:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"scripts": {
|
|
33
|
+
"dev": "vite",
|
|
34
|
+
"build": "vite build",
|
|
35
|
+
"preview": "vite preview"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### `.env.example`
|
|
41
|
+
|
|
42
|
+
Commit **`.env.example`** at the app root (no secrets). Minimum variables:
|
|
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). |
|
|
50
|
+
|
|
51
|
+
Example **`.env.example`** content for the app root:
|
|
52
|
+
|
|
53
|
+
```env
|
|
54
|
+
# Copy to `.env` and fill in values. Do not commit `.env`.
|
|
55
|
+
# PORT: Vite dev/preview bind (sybilionStandaloneViteDev reads this). 3000 matches Auth0 test tenant localhost URLs.
|
|
56
|
+
PORT=3000
|
|
57
|
+
|
|
58
|
+
VITE_SYBILION_API_BASE_URL=https://api-dev.sybilion.com
|
|
59
|
+
VITE_AUTH0_DOMAIN=your-tenant.eu.auth0.com
|
|
60
|
+
VITE_AUTH0_CLIENT_ID=your-spa-client-id
|
|
61
|
+
```
|
|
62
|
+
|
|
21
63
|
## 2. SDK (`@sybilion/sdk`)
|
|
22
64
|
|
|
23
65
|
Typed HTTP client for the Sybilion API. Env vars depend on bundler; for Vite, only `import.meta.env.VITE_*` reaches the client.
|
|
@@ -30,7 +72,9 @@ import { createSybilionSDK } from '@sybilion/sdk';
|
|
|
30
72
|
export const sybilionJwtStorageKey = 'sybilion.standalone.jwt';
|
|
31
73
|
|
|
32
74
|
export const sybilionSdk = createSybilionSDK({
|
|
33
|
-
baseUrl: import.meta.env.
|
|
75
|
+
baseUrl: import.meta.env.DEV
|
|
76
|
+
? ''
|
|
77
|
+
: (import.meta.env.VITE_SYBILION_API_BASE_URL as string),
|
|
34
78
|
apiPrefix: '/api',
|
|
35
79
|
getToken: () =>
|
|
36
80
|
typeof localStorage !== 'undefined'
|
|
@@ -39,7 +83,7 @@ export const sybilionSdk = createSybilionSDK({
|
|
|
39
83
|
});
|
|
40
84
|
```
|
|
41
85
|
|
|
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).
|
|
86
|
+
**Options:** `baseUrl` — API origin only (no trailing slash) **in production**; in **development** use `''` so requests stay **same-origin** (`/api/v1/...`) and the Vite dev server **proxies `/api`** to `VITE_SYBILION_API_BASE_URL` (see **Local dev: Vite API proxy** below). `apiPrefix` — default `'/api'` so calls go to `{baseUrl}/api/v1/...`. `getToken` — must read the same key you pass as `sybilionTokenStorageKey` on `SybilionAuthProvider` (§3).
|
|
43
87
|
|
|
44
88
|
```ts
|
|
45
89
|
import { sybilionSdk } from './libs/sybilion-sdk';
|
|
@@ -53,6 +97,35 @@ await sybilionSdk.raw.datasets.getById(datasetId);
|
|
|
53
97
|
|
|
54
98
|
Package README: [`@sybilion/sdk`](https://www.npmjs.com/package/@sybilion/sdk) — monorepo: [`../../sdk/README.md`](../../sdk/README.md).
|
|
55
99
|
|
|
100
|
+
## Local dev: Vite API proxy
|
|
101
|
+
|
|
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
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { sybilionStandaloneViteDev } from '@sybilion/uilib/vite-standalone-dev';
|
|
106
|
+
import react from '@vitejs/plugin-react';
|
|
107
|
+
import { defineConfig } from 'vite';
|
|
108
|
+
|
|
109
|
+
export default defineConfig(({ mode }) => ({
|
|
110
|
+
...sybilionStandaloneViteDev({ mode }),
|
|
111
|
+
plugins: [react()],
|
|
112
|
+
}));
|
|
113
|
+
```
|
|
114
|
+
|
|
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
|
+
|
|
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
|
+
|
|
125
|
+
## Local dev: apps with a Go server
|
|
126
|
+
|
|
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.
|
|
128
|
+
|
|
56
129
|
## 3. Auth (`SybilionAuthProvider`)
|
|
57
130
|
|
|
58
131
|
Use inside `BrowserRouter` if redirects hit a callback route.
|
|
@@ -141,26 +214,32 @@ export function App() {
|
|
|
141
214
|
|
|
142
215
|
### `AppLayout` (sidebar + main + `children`)
|
|
143
216
|
|
|
144
|
-
`
|
|
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`**.
|
|
145
222
|
|
|
146
223
|
```tsx
|
|
147
224
|
import type { ReactNode } from 'react';
|
|
225
|
+
import { useLocation, useNavigate } from 'react-router-dom';
|
|
148
226
|
|
|
149
227
|
import {
|
|
150
228
|
AppHeaderHost,
|
|
151
|
-
AppHeaderPortal,
|
|
152
229
|
AppShell,
|
|
153
230
|
AppShellMainContent,
|
|
154
|
-
|
|
155
|
-
NavUserHeader,
|
|
231
|
+
DropdownMenuItem,
|
|
156
232
|
PageFooter,
|
|
157
233
|
PageScroll,
|
|
158
|
-
|
|
234
|
+
SybilionAppHeader,
|
|
159
235
|
} from '@sybilion/uilib';
|
|
160
236
|
|
|
161
237
|
import { AppSidebar } from './AppSidebar';
|
|
162
238
|
|
|
163
239
|
export function AppLayout({ children }: { children: ReactNode }) {
|
|
240
|
+
const location = useLocation();
|
|
241
|
+
const navigate = useNavigate();
|
|
242
|
+
|
|
164
243
|
return (
|
|
165
244
|
<PageScroll>
|
|
166
245
|
<AppShell>
|
|
@@ -170,16 +249,24 @@ export function AppLayout({ children }: { children: ReactNode }) {
|
|
|
170
249
|
header={<AppHeaderHost />}
|
|
171
250
|
footer={<PageFooter versionLink="/releases" versionLabel="0.0.1" />}
|
|
172
251
|
>
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
/>
|
|
183
270
|
{children}
|
|
184
271
|
</AppShellMainContent>
|
|
185
272
|
</AppShell>
|
|
@@ -188,7 +275,7 @@ export function AppLayout({ children }: { children: ReactNode }) {
|
|
|
188
275
|
}
|
|
189
276
|
```
|
|
190
277
|
|
|
191
|
-
Wire
|
|
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`**).
|
|
192
279
|
|
|
193
280
|
#### Sidebar (`AppSidebar.tsx`)
|
|
194
281
|
|
|
@@ -261,38 +348,80 @@ export function AppSidebar() {
|
|
|
261
348
|
|
|
262
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`.
|
|
263
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
|
+
|
|
264
388
|
### Full pattern
|
|
265
389
|
|
|
266
|
-
Composition: `PageScroll` → `AppShell` → `AppSidebar` → `AppShellMainContent` with
|
|
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.
|
|
267
391
|
|
|
268
392
|
### Greenfield checklist (agents)
|
|
269
393
|
|
|
270
|
-
| Step
|
|
271
|
-
|
|
|
272
|
-
| Env
|
|
273
|
-
|
|
|
274
|
-
|
|
|
275
|
-
|
|
|
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. |
|
|
276
404
|
|
|
277
405
|
### Glossary (high-use pieces)
|
|
278
406
|
|
|
279
|
-
| Component / API
|
|
280
|
-
|
|
|
281
|
-
| `PageScroll`
|
|
282
|
-
| `AppShell`
|
|
283
|
-
| `AppShellMainContent`
|
|
284
|
-
| `
|
|
285
|
-
| `
|
|
286
|
-
| `NavUserHeader`
|
|
287
|
-
| `Sidebar`, `SidebarProvider`
|
|
288
|
-
| `AppSidebar`
|
|
289
|
-
| `SidebarDatasetsItemsGrouped`
|
|
290
|
-
| `SidebarTrigger`
|
|
291
|
-
| `PageFooter`
|
|
292
|
-
| `Gap`
|
|
293
|
-
| `
|
|
294
|
-
| `
|
|
295
|
-
| `
|
|
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. |
|
|
296
425
|
|
|
297
426
|
## 5. Data
|
|
298
427
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sybilion/uilib",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"description": "Sybilion Design System — React UI components (Webpack + Stylus)",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -30,7 +30,12 @@
|
|
|
30
30
|
"default": "./src/index.ts"
|
|
31
31
|
},
|
|
32
32
|
"./src/*": "./src/*",
|
|
33
|
-
"./standalone-global.css": "./assets/standalone-global.css"
|
|
33
|
+
"./standalone-global.css": "./assets/standalone-global.css",
|
|
34
|
+
"./vite-standalone-dev": {
|
|
35
|
+
"types": "./dist/standalone/vite-sybilion-standalone-dev.d.ts",
|
|
36
|
+
"import": "./dist/standalone/vite-sybilion-standalone-dev.js",
|
|
37
|
+
"default": "./dist/standalone/vite-sybilion-standalone-dev.js"
|
|
38
|
+
}
|
|
34
39
|
},
|
|
35
40
|
"files": [
|
|
36
41
|
"assets",
|
|
@@ -109,7 +114,8 @@
|
|
|
109
114
|
"@sybilion/sdk": ">=0.0.1",
|
|
110
115
|
"react": ">=18.0.0",
|
|
111
116
|
"react-dom": ">=18.0.0",
|
|
112
|
-
"react-router-dom": ">=6.0.0"
|
|
117
|
+
"react-router-dom": ">=6.0.0",
|
|
118
|
+
"vite": "^6.0.0"
|
|
113
119
|
},
|
|
114
120
|
"peerDependenciesMeta": {
|
|
115
121
|
"@auth0/auth0-react": {
|
|
@@ -117,6 +123,9 @@
|
|
|
117
123
|
},
|
|
118
124
|
"@sybilion/sdk": {
|
|
119
125
|
"optional": true
|
|
126
|
+
},
|
|
127
|
+
"vite": {
|
|
128
|
+
"optional": true
|
|
120
129
|
}
|
|
121
130
|
},
|
|
122
131
|
"devDependencies": {
|
|
@@ -178,6 +187,7 @@
|
|
|
178
187
|
"ts-jest": "^29.2.5",
|
|
179
188
|
"ts-node": "^10.9.1",
|
|
180
189
|
"typescript": "^5.3.3",
|
|
190
|
+
"vite": "^6.0.0",
|
|
181
191
|
"webpack": "^5.75.0",
|
|
182
192
|
"webpack-cli": "^5.0.1",
|
|
183
193
|
"webpack-dev-server": "^5.2.3"
|
|
@@ -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
|
+
}
|