@sybilion/uilib 1.0.32 → 1.2.0

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 (72) hide show
  1. package/README.md +8 -8
  2. package/assets/{mini-app-global.css → standalone-global.css} +1 -1
  3. package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.constants.js +11 -0
  4. package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.js +2 -1
  5. package/dist/esm/components/ui/Sidebar/Sidebar.js +1 -8
  6. package/dist/esm/components/ui/Sidebar/Sidebar.styl.js +2 -2
  7. package/dist/esm/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.js +48 -0
  8. package/dist/esm/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.styl.js +7 -0
  9. package/dist/esm/components/widgets/SidebarDatasetsItemsGrouped/groupSidebarDatasets.js +38 -0
  10. package/dist/esm/index.js +6 -6
  11. package/dist/esm/sybilion-auth/SybilionAuthProvider.js +185 -0
  12. package/dist/esm/sybilion-auth/authPaths.js +7 -0
  13. package/dist/esm/sybilion-auth/exchangeSybilionToken.js +40 -0
  14. package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.constants.d.ts +9 -0
  15. package/dist/esm/types/src/components/ui/Input/Input.d.ts +1 -1
  16. package/dist/esm/types/src/components/ui/Sidebar/Sidebar.d.ts +1 -5
  17. package/dist/esm/types/src/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.d.ts +11 -0
  18. package/dist/esm/types/src/components/widgets/SidebarDatasetsItemsGrouped/groupSidebarDatasets.d.ts +8 -0
  19. package/dist/esm/types/src/components/widgets/SidebarDatasetsItemsGrouped/index.d.ts +3 -0
  20. package/dist/esm/types/src/docs/pages/SidebarDatasetsItemsGroupedPage.d.ts +1 -0
  21. package/dist/esm/types/src/docs/pages/SybilionAuthProviderPage.d.ts +1 -0
  22. package/dist/esm/types/src/index.d.ts +3 -1
  23. package/dist/esm/types/src/sybilion-auth/SybilionAuthProvider.d.ts +39 -0
  24. package/dist/esm/types/src/sybilion-auth/authPaths.d.ts +3 -0
  25. package/dist/esm/types/src/sybilion-auth/exchangeSybilionToken.d.ts +2 -0
  26. package/dist/esm/types/src/sybilion-auth/index.d.ts +4 -0
  27. package/dist/esm/types/src/{mini-app/miniAppDataTypes.d.ts → types/sybilionDatasetSnapshots.d.ts} +5 -8
  28. package/docs/standalone-apps.md +65 -0
  29. package/package.json +10 -3
  30. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.constants.ts +9 -0
  31. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.tsx +2 -1
  32. package/src/components/ui/Sidebar/Sidebar.styl +0 -30
  33. package/src/components/ui/Sidebar/Sidebar.styl.d.ts +0 -2
  34. package/src/components/ui/Sidebar/Sidebar.tsx +0 -30
  35. package/src/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.styl +29 -0
  36. package/src/{mini-app/MiniAppRoot.styl.d.ts → components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.styl.d.ts} +4 -2
  37. package/src/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.tsx +128 -0
  38. package/src/components/widgets/SidebarDatasetsItemsGrouped/groupSidebarDatasets.ts +51 -0
  39. package/src/components/widgets/SidebarDatasetsItemsGrouped/index.ts +10 -0
  40. package/src/docs/components/DocsSidebar/DocsSidebar.tsx +47 -50
  41. package/src/docs/pages/SidebarDatasetsItemsGroupedPage.tsx +136 -0
  42. package/src/docs/pages/SidebarPage.tsx +21 -28
  43. package/src/docs/pages/SybilionAuthProviderPage.tsx +37 -0
  44. package/src/docs/registry.ts +9 -3
  45. package/src/index.ts +3 -1
  46. package/src/sybilion-auth/SybilionAuthProvider.tsx +322 -0
  47. package/src/sybilion-auth/authPaths.ts +6 -0
  48. package/src/sybilion-auth/exchangeSybilionToken.ts +47 -0
  49. package/src/sybilion-auth/index.ts +16 -0
  50. package/src/{mini-app/miniAppDataTypes.ts → types/sybilionDatasetSnapshots.ts} +5 -8
  51. package/dist/esm/mini-app/MiniAppRoot.js +0 -82
  52. package/dist/esm/mini-app/MiniAppRoot.styl.js +0 -7
  53. package/dist/esm/mini-app/miniAppChatBridge.js +0 -45
  54. package/dist/esm/mini-app/miniAppDataClient.js +0 -98
  55. package/dist/esm/mini-app/miniAppProtocol.js +0 -153
  56. package/dist/esm/mini-app/miniAppThemeConfig.js +0 -40
  57. package/dist/esm/types/src/docs/pages/MiniAppRootPage.d.ts +0 -1
  58. package/dist/esm/types/src/mini-app/MiniAppRoot.d.ts +0 -18
  59. package/dist/esm/types/src/mini-app/index.d.ts +0 -10
  60. package/dist/esm/types/src/mini-app/miniAppChatBridge.d.ts +0 -6
  61. package/dist/esm/types/src/mini-app/miniAppDataClient.d.ts +0 -16
  62. package/dist/esm/types/src/mini-app/miniAppProtocol.d.ts +0 -89
  63. package/dist/esm/types/src/mini-app/miniAppThemeConfig.d.ts +0 -3
  64. package/docs/workspace-mini-apps.md +0 -51
  65. package/src/docs/pages/MiniAppRootPage.tsx +0 -58
  66. package/src/mini-app/MiniAppRoot.styl +0 -24
  67. package/src/mini-app/MiniAppRoot.tsx +0 -150
  68. package/src/mini-app/index.ts +0 -43
  69. package/src/mini-app/miniAppChatBridge.ts +0 -55
  70. package/src/mini-app/miniAppDataClient.ts +0 -165
  71. package/src/mini-app/miniAppProtocol.ts +0 -247
  72. package/src/mini-app/miniAppThemeConfig.ts +0 -45
@@ -0,0 +1 @@
1
+ export default function SybilionAuthProviderPage(): import("react/jsx-runtime").JSX.Element;
@@ -1,4 +1,5 @@
1
- export * from './mini-app';
1
+ export * from './sybilion-auth';
2
+ export * from './types/sybilionDatasetSnapshots';
2
3
  export * from './contexts/chat-context';
3
4
  export * from './types/chat-api.types';
4
5
  export * from './components/ui/AnalysesSelector';
@@ -53,3 +54,4 @@ export * from './components/ui/Toggle';
53
54
  export * from './components/ui/ToggleGroup';
54
55
  export * from './components/ui/Tooltip';
55
56
  export * from './components/ui/VimeoEmbed';
57
+ export * from './components/widgets/SidebarDatasetsItemsGrouped';
@@ -0,0 +1,39 @@
1
+ import { type RedirectLoginOptions } from '@auth0/auth0-react';
2
+ import type { JSX, ReactNode } from 'react';
3
+ export type SybilionAuthProviderProps = {
4
+ children: ReactNode;
5
+ apiBaseUrl: string;
6
+ auth0Domain: string;
7
+ auth0ClientId: string;
8
+ redirectUri: string;
9
+ /**
10
+ * Defaults match sybilion-client AuthProvider (Management API audience + metadata scope).
11
+ * Override if your Auth0 SPA app uses a Resource Server audience for `/v1/auth/login`.
12
+ */
13
+ authorizationParams?: {
14
+ audience?: string;
15
+ scope?: string;
16
+ redirect_uri?: string;
17
+ };
18
+ sybilionTokenStorageKey?: string;
19
+ logoutReturnTo?: string;
20
+ };
21
+ export type SybilionAuthContextValue = {
22
+ apiBaseUrl: string;
23
+ isLoading: boolean;
24
+ /** Auth0 authenticated and Sybilion JWT present. */
25
+ isAuthenticated: boolean;
26
+ sybilionAccessToken: string | null;
27
+ error: string | null;
28
+ loginWithRedirect: (options?: RedirectLoginOptions) => Promise<void>;
29
+ logout: () => void;
30
+ getAuth0AccessToken: () => Promise<string | undefined>;
31
+ getSybilionAccessToken: () => Promise<string | null>;
32
+ };
33
+ export declare function useSybilionAuth(): SybilionAuthContextValue;
34
+ /** fetch() relative to Sybilion API base with Bearer Sybilion JWT. */
35
+ export declare function sybilionApiFetch(apiBaseUrl: string, bearerToken: string, path: string, init?: RequestInit): Promise<Response>;
36
+ export declare function createSybilionApiFetch(apiBaseUrl: string, getSybilionAccessToken: () => Promise<string | null>): (path: string, init?: RequestInit) => Promise<Response>;
37
+ /** Authenticated fetch using {@link useSybilionAuth} context. */
38
+ export declare function useSybilionApiFetch(): (path: string, init?: RequestInit) => Promise<Response>;
39
+ export declare function SybilionAuthProvider({ children, apiBaseUrl, auth0Domain, auth0ClientId, redirectUri, authorizationParams, sybilionTokenStorageKey, logoutReturnTo, }: SybilionAuthProviderProps): JSX.Element;
@@ -0,0 +1,3 @@
1
+ /** Sybilion platform auth exchange route (joined with trimmed API base URL). */
2
+ export declare const SYBILION_AUTH_LOGIN_PATH = "/v1/auth/login";
3
+ export declare function normalizeApiBaseUrl(url: string): string;
@@ -0,0 +1,2 @@
1
+ /** POST `{ identity: auth0AccessToken, type: 'auth0' }` → Sybilion API JWT string. */
2
+ export declare function exchangeAuth0AccessTokenForSybilionJwt(apiBaseUrl: string, auth0AccessToken: string): Promise<string>;
@@ -0,0 +1,4 @@
1
+ export type { SybilionAuthProviderProps, SybilionAuthContextValue, } from '#uilib/sybilion-auth/SybilionAuthProvider';
2
+ export { SybilionAuthProvider, useSybilionAuth, sybilionApiFetch, createSybilionApiFetch, useSybilionApiFetch, } from '#uilib/sybilion-auth/SybilionAuthProvider';
3
+ export { SYBILION_AUTH_LOGIN_PATH, normalizeApiBaseUrl, } from '#uilib/sybilion-auth/authPaths';
4
+ export { exchangeAuth0AccessTokenForSybilionJwt } from '#uilib/sybilion-auth/exchangeSybilionToken';
@@ -1,8 +1,7 @@
1
1
  /**
2
- * Serializable shapes returned by the shell for mini-app data getters.
3
- * Subset of Sybilion `Dataset` / API types — use `unknown` where payloads are large or evolving.
2
+ * Loose JSON shapes aligned with Sybilion dataset payloads (standalone apps / widgets).
4
3
  */
5
- export type MiniAppDataset = {
4
+ export type SybilionDatasetSnapshot = {
6
5
  id: number;
7
6
  user_id?: number;
8
7
  name: string;
@@ -34,14 +33,12 @@ export type MiniAppDataset = {
34
33
  version?: number;
35
34
  };
36
35
  /** `forecastData` slice: analysis id (string) → forecast series blob. */
37
- export type MiniAppForecastMap = Record<string, unknown>;
38
- export type MiniAppPerformanceBundle = {
39
- /** Cached Performance-tab API payload, or null if user never opened Performance / cache empty. */
36
+ export type SybilionForecastMap = Record<string, unknown>;
37
+ export type SybilionPerformanceSnapshot = {
40
38
  table: unknown;
41
- /** In-memory spaghetti lines from shell context, or null. */
42
39
  spaghetti: unknown;
43
40
  };
44
- export type MiniAppDriversComparisonSnapshot = {
41
+ export type SybilionDriversComparisonSnapshot = {
45
42
  byRegion: unknown;
46
43
  byCountry: unknown;
47
44
  byCategory: unknown;
@@ -0,0 +1,65 @@
1
+ # Standalone Sybilion apps (@sybilion/uilib)
2
+
3
+ Greenfield SPA on **your own origin**: `@sybilion/uilib` for layout/UI, `**SybilionAuthProvider`\*\* + Sybilion API for data—no iframe in the main client.
4
+
5
+ **Agents / humans:** Use `AppShell` + `AppShellMainContent`; stick to uilib spacing primitives (no random root horizontal gutters).
6
+
7
+ ## 1. Dependencies and global CSS
8
+
9
+ ```bash
10
+ yarn add react react-dom react-router-dom @auth0/auth0-react @sybilion/uilib
11
+ ```
12
+
13
+ Import tokens/fonts once:
14
+
15
+ ```ts
16
+ import '@sybilion/uilib/standalone-global.css';
17
+ ```
18
+
19
+ ## 2. Layout (AppShell)
20
+
21
+ Pattern: `[src/docs/DocsShell.tsx](https://github.com/Mir-Insight/uilib/blob/main/src/docs/DocsShell.tsx)` — `PageScroll` → `AppShell` → sidebar → `AppShellMainContent` with `AppHeaderHost`, `PageFooter`, main content (`Outlet` / routes). Add `Theme` and optional `SidebarProvider` from `@homecode/ui`.
22
+
23
+ ## 3. Auth (`SybilionAuthProvider`)
24
+
25
+ Use inside `BrowserRouter` if redirects hit a callback route.
26
+
27
+ Env vars depend on bundler—for **Vite** only `import.meta.env.VITE_`\* is exposed client-side:
28
+
29
+ ```tsx
30
+ import { SybilionAuthProvider } from '@sybilion/uilib';
31
+
32
+ const apiBaseUrl = import.meta.env.VITE_SYBILION_API_BASE_URL as string;
33
+ const auth0Domain = import.meta.env.VITE_AUTH0_DOMAIN as string;
34
+ const auth0ClientId = import.meta.env.VITE_AUTH0_CLIENT_ID as string;
35
+
36
+ <SybilionAuthProvider
37
+ apiBaseUrl={apiBaseUrl}
38
+ auth0Domain={auth0Domain}
39
+ auth0ClientId={auth0ClientId}
40
+ redirectUri={window.location.origin}
41
+ >
42
+ <App />
43
+ </SybilionAuthProvider>;
44
+ ```
45
+
46
+ **Flow:** Auth0 SPA → `POST {apiBaseUrl}/v1/auth/login` with `{ identity: "<Auth0 AT>", type: "auth0" }` → Sybilion JWT in response (`data.token` or `token`) → Bearer on API calls.
47
+
48
+ **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).
49
+
50
+ | Layer | Configure |
51
+ | ---------------- | ------------------------------------------------------------------------------------- |
52
+ | **Auth0** | Callback, logout, and web origins → your URLs (+ previews). |
53
+ | **Sybilion API** | CORS → your deploy `Origin`. |
54
+ | **App** | `apiBaseUrl`, Auth0 `domain` / `clientId`, redirect usually `window.location.origin`. |
55
+
56
+ **Hooks:** `useSybilionAuth()`, `useSybilionApiFetch()` (or `createSybilionApiFetch` / `sybilionApiFetch` helpers).
57
+
58
+ ## 4. Data
59
+
60
+ Fetch Sybilion API with the JWT above—no host `postMessage` bridge.
61
+
62
+ ## Related
63
+
64
+ - [auth0-configuration-guide.md](https://github.com/Mir-Insight/sybilion-client/blob/main/docs/auth0-configuration-guide.md)
65
+ - [server-auth-verification.md](https://github.com/Mir-Insight/sybilion-client/blob/main/docs/server-auth-verification.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.0.32",
3
+ "version": "1.2.0",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -30,7 +30,7 @@
30
30
  "default": "./src/index.ts"
31
31
  },
32
32
  "./src/*": "./src/*",
33
- "./mini-app-global.css": "./assets/mini-app-global.css"
33
+ "./standalone-global.css": "./assets/standalone-global.css"
34
34
  },
35
35
  "files": [
36
36
  "assets",
@@ -76,6 +76,7 @@
76
76
  },
77
77
  "homepage": "https://github.com/Mir-Insight/uilib#readme",
78
78
  "dependencies": {
79
+ "@homecode/ui": "^4.17.0",
79
80
  "@phosphor-icons/react": "^2.1.10",
80
81
  "@radix-ui/react-avatar": "^1.1.10",
81
82
  "@radix-ui/react-checkbox": "^1.3.3",
@@ -104,12 +105,18 @@
104
105
  "vaul": "^1.1.2"
105
106
  },
106
107
  "peerDependencies": {
107
- "@homecode/ui": ">=4.17.0",
108
+ "@auth0/auth0-react": "^2.3.1",
108
109
  "react": ">=18.0.0",
109
110
  "react-dom": ">=18.0.0",
110
111
  "react-router-dom": ">=6.0.0"
111
112
  },
113
+ "peerDependenciesMeta": {
114
+ "@auth0/auth0-react": {
115
+ "optional": true
116
+ }
117
+ },
112
118
  "devDependencies": {
119
+ "@auth0/auth0-react": "^2.3.1",
113
120
  "@babel/core": "^7.20.12",
114
121
  "@babel/preset-typescript": "^7.21.0",
115
122
  "@homecode/ui": "^4.30.6",
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Default chart margins
3
+ */
4
+ export const CHART_MARGINS = {
5
+ top: 20,
6
+ right: 30,
7
+ bottom: 20,
8
+ left: -10,
9
+ } as const;
@@ -10,6 +10,7 @@ import { InteractionOverlay } from '#uilib/components/ui/InteractionOverlay/Inte
10
10
  import { TimeRangeControls } from '#uilib/components/ui/TimeRangeControls/TimeRangeControls';
11
11
  import { ensureChartForecastBridge } from '#uilib/utils/chartConnectionPoint';
12
12
 
13
+ import { CHART_MARGINS } from './ChartAreaInteractive.constants';
13
14
  import {
14
15
  filterDataForTimeRange,
15
16
  longDateFormatter,
@@ -151,7 +152,7 @@ export function ChartAreaInteractive({
151
152
  formatDate: shortDateFormatter,
152
153
  labelFormatter: (v: unknown) => longDateFormatter(v as string),
153
154
  chartType: 'composed' as const,
154
- margin: { left: -10, right: 30, top: 20, bottom: 20 },
155
+ margin: { ...CHART_MARGINS },
155
156
  hiddenSeries: seriesHidden,
156
157
  onLegendClick: handleLegendClick,
157
158
  onAnalysisSelect: onAnalysisSelect,
@@ -219,31 +219,6 @@
219
219
  padding 0
220
220
  padding-top 40px
221
221
 
222
- // Sidebar group label
223
- .sidebarGroupLabel
224
- color var(--sidebar-foreground)
225
- opacity 0.7
226
- display flex
227
- height 2rem
228
- flex-shrink 0
229
- align-items center
230
- border-radius 0.375rem
231
- padding-left 0.5rem
232
- padding-right 0.5rem
233
- font-size 0.75rem
234
- font-weight 500
235
- outline none
236
- transition margin 200ms ease-linear, opacity 200ms ease-linear
237
- &:focus-visible
238
- box-shadow 0 0 0 2px var(--sidebar-ring)
239
- & > svg
240
- width 1rem
241
- height 1rem
242
- flex-shrink 0
243
- &[data-collapsible="icon"]
244
- margin-top -2rem
245
- opacity 0
246
-
247
222
  // Sidebar group action
248
223
  .sidebarGroupAction
249
224
  color var(--sidebar-foreground)
@@ -278,11 +253,6 @@
278
253
  &[data-collapsible="icon"]
279
254
  display none
280
255
 
281
- // Sidebar group content
282
- .sidebarGroupContent
283
- width 100%
284
- font-size 0.875rem
285
-
286
256
  // Sidebar menu
287
257
  .sidebarMenu
288
258
  display flex
@@ -11,8 +11,6 @@ interface CssExports {
11
11
  'sidebarFooter': string;
12
12
  'sidebarGroup': string;
13
13
  'sidebarGroupAction': string;
14
- 'sidebarGroupContent': string;
15
- 'sidebarGroupLabel': string;
16
14
  'sidebarHeader': string;
17
15
  'sidebarInput': string;
18
16
  'sidebarMainShell': string;
@@ -738,23 +738,6 @@ function SidebarGroup({ className, ...props }: ComponentProps<'div'>) {
738
738
  );
739
739
  }
740
740
 
741
- function SidebarGroupLabel({
742
- className,
743
- asChild = false,
744
- ...props
745
- }: ComponentProps<'div'> & { asChild?: boolean }) {
746
- const Comp = asChild ? Slot : 'div';
747
-
748
- return (
749
- <Comp
750
- data-slot="sidebar-group-label"
751
- data-sidebar="group-label"
752
- className={cn(S.sidebarGroupLabel, className)}
753
- {...props}
754
- />
755
- );
756
- }
757
-
758
741
  function SidebarGroupAction({
759
742
  className,
760
743
  asChild = false,
@@ -772,17 +755,6 @@ function SidebarGroupAction({
772
755
  );
773
756
  }
774
757
 
775
- function SidebarGroupContent({ className, ...props }: ComponentProps<'div'>) {
776
- return (
777
- <div
778
- data-slot="sidebar-group-content"
779
- data-sidebar="group-content"
780
- className={cn(S.sidebarGroupContent, className)}
781
- {...props}
782
- />
783
- );
784
- }
785
-
786
758
  function SidebarMenu({ className, ...props }: ComponentProps<'ul'>) {
787
759
  return (
788
760
  <ul
@@ -952,8 +924,6 @@ export {
952
924
  SidebarFooter,
953
925
  SidebarGroup,
954
926
  SidebarGroupAction,
955
- SidebarGroupContent,
956
- SidebarGroupLabel,
957
927
  SidebarMenu,
958
928
  SidebarMenuAction,
959
929
  SidebarMenuBadge,
@@ -0,0 +1,29 @@
1
+ .chevronContainer
2
+ margin-left auto
3
+
4
+ .subMenuContainer
5
+ position relative
6
+
7
+ .subMenuBorder
8
+ position absolute
9
+ left var(--p-5)
10
+ top 0
11
+ bottom 26px
12
+ width 1px
13
+ background var(--sidebar-border)
14
+
15
+ .subMenuItem
16
+ position relative
17
+ margin-left var(--p-6)
18
+
19
+ &::before
20
+ content ''
21
+ position absolute
22
+ left calc(var(--p-1) * -1)
23
+ top var(--p-2)
24
+ bottom 0
25
+ width var(--p-3)
26
+ height var(--p-3)
27
+ border-left 1px solid var(--sidebar-border)
28
+ border-bottom 1px solid var(--sidebar-border)
29
+ border-bottom-left-radius var(--p-2)
@@ -1,8 +1,10 @@
1
1
  // This file is automatically generated.
2
2
  // Please do not change this file!
3
3
  interface CssExports {
4
- 'inner': string;
5
- 'root': string;
4
+ 'chevronContainer': string;
5
+ 'subMenuBorder': string;
6
+ 'subMenuContainer': string;
7
+ 'subMenuItem': string;
6
8
  }
7
9
  export const cssExports: CssExports;
8
10
  export default cssExports;
@@ -0,0 +1,128 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+
3
+ import {
4
+ SidebarGroup,
5
+ SidebarMenu,
6
+ SidebarMenuButton,
7
+ SidebarMenuItem,
8
+ SidebarMenuSub,
9
+ SidebarMenuSubButton,
10
+ SidebarMenuSubItem,
11
+ } from '#uilib/components/ui/Sidebar/Sidebar';
12
+ import { SmartTextTruncate } from '#uilib/components/ui/SmartTextTruncate';
13
+ import { ChevronDown, ChevronRight, PackageOpen } from 'lucide-react';
14
+
15
+ import S from './SidebarDatasetsItemsGrouped.styl';
16
+ import {
17
+ type SidebarDatasetsItemsGroupBy,
18
+ type SidebarDatasetsItemsGroupedDataset,
19
+ groupSidebarDatasets,
20
+ } from './groupSidebarDatasets';
21
+
22
+ export type SidebarDatasetsItemsGroupedProps = {
23
+ groupBy: SidebarDatasetsItemsGroupBy;
24
+ datasets: SidebarDatasetsItemsGroupedDataset[];
25
+ selectedDatasetId?: number;
26
+ onDatasetClick?: (datasetId: number) => void;
27
+ /** When omitted, all groups start expanded. */
28
+ defaultExpandedGroupNames?: string[];
29
+ className?: string;
30
+ };
31
+
32
+ export function SidebarDatasetsItemsGrouped({
33
+ groupBy,
34
+ datasets,
35
+ selectedDatasetId,
36
+ onDatasetClick,
37
+ defaultExpandedGroupNames,
38
+ className,
39
+ }: SidebarDatasetsItemsGroupedProps) {
40
+ const grouped = useMemo(
41
+ () => groupSidebarDatasets(datasets, groupBy),
42
+ [datasets, groupBy],
43
+ );
44
+
45
+ const [expanded, setExpanded] = useState<Set<string>>(new Set());
46
+
47
+ useEffect(() => {
48
+ if (defaultExpandedGroupNames !== undefined) {
49
+ setExpanded(new Set(defaultExpandedGroupNames));
50
+ } else {
51
+ setExpanded(new Set(grouped.map(([name]) => name)));
52
+ }
53
+ }, [grouped, defaultExpandedGroupNames]);
54
+
55
+ useEffect(() => {
56
+ if (selectedDatasetId == null) return;
57
+ const groupName = grouped.find(([, ds]) =>
58
+ ds.some(d => d.id === selectedDatasetId),
59
+ )?.[0];
60
+ if (groupName) {
61
+ setExpanded(prev => new Set(prev).add(groupName));
62
+ }
63
+ }, [selectedDatasetId, grouped]);
64
+
65
+ const toggleGroup = (name: string) => {
66
+ setExpanded(prev => {
67
+ const next = new Set(prev);
68
+ if (next.has(name)) next.delete(name);
69
+ else next.add(name);
70
+ return next;
71
+ });
72
+ };
73
+
74
+ return (
75
+ <SidebarGroup className={className}>
76
+ <SidebarMenu>
77
+ {grouped.map(([groupName, groupDatasets]) => {
78
+ const isExpanded = expanded.has(groupName);
79
+ const parentActive = groupDatasets.some(
80
+ d => d.id === selectedDatasetId,
81
+ );
82
+
83
+ return (
84
+ <SidebarMenuItem key={groupName}>
85
+ <SidebarMenuButton
86
+ type="button"
87
+ isActive={parentActive}
88
+ onClick={() => toggleGroup(groupName)}
89
+ >
90
+ <PackageOpen strokeWidth={1.5} size={16} />
91
+ <SmartTextTruncate>{groupName}</SmartTextTruncate>
92
+ <div className={S.chevronContainer}>
93
+ {isExpanded ? (
94
+ <ChevronDown size={12} />
95
+ ) : (
96
+ <ChevronRight size={12} />
97
+ )}
98
+ </div>
99
+ </SidebarMenuButton>
100
+ {isExpanded && (
101
+ <SidebarMenuSub className={S.subMenuContainer}>
102
+ <div className={S.subMenuBorder} />
103
+ {groupDatasets.map(dataset => (
104
+ <SidebarMenuSubItem
105
+ key={dataset.id}
106
+ className={S.subMenuItem}
107
+ >
108
+ <SidebarMenuSubButton
109
+ href={`#dataset-${dataset.id}`}
110
+ isActive={dataset.id === selectedDatasetId}
111
+ onClick={e => {
112
+ e.preventDefault();
113
+ onDatasetClick?.(dataset.id);
114
+ }}
115
+ >
116
+ <SmartTextTruncate>{dataset.name}</SmartTextTruncate>
117
+ </SidebarMenuSubButton>
118
+ </SidebarMenuSubItem>
119
+ ))}
120
+ </SidebarMenuSub>
121
+ )}
122
+ </SidebarMenuItem>
123
+ );
124
+ })}
125
+ </SidebarMenu>
126
+ </SidebarGroup>
127
+ );
128
+ }
@@ -0,0 +1,51 @@
1
+ import type { SybilionDatasetSnapshot } from '#uilib/types/sybilionDatasetSnapshots';
2
+
3
+ export type SidebarDatasetsItemsGroupedDataset = SybilionDatasetSnapshot;
4
+
5
+ export type SidebarDatasetsItemsGroupBy =
6
+ | 'target_type'
7
+ | 'regions'
8
+ | 'categories';
9
+
10
+ export type GroupedSidebarDatasetsEntry = readonly [
11
+ groupName: string,
12
+ datasets: SidebarDatasetsItemsGroupedDataset[],
13
+ ];
14
+
15
+ export function groupSidebarDatasets(
16
+ datasets: SidebarDatasetsItemsGroupedDataset[],
17
+ groupBy: SidebarDatasetsItemsGroupBy,
18
+ ): GroupedSidebarDatasetsEntry[] {
19
+ const grouped: Record<string, SidebarDatasetsItemsGroupedDataset[]> = {};
20
+
21
+ datasets.forEach(dataset => {
22
+ if (groupBy === 'target_type') {
23
+ const key = dataset.target_type?.name || 'Unknown';
24
+ if (!grouped[key]) grouped[key] = [];
25
+ grouped[key].push(dataset);
26
+ } else if (groupBy === 'regions') {
27
+ if (dataset.regions && dataset.regions.length > 0) {
28
+ const mostSpecificRegion = dataset.regions[dataset.regions.length - 1];
29
+ const key = mostSpecificRegion.name || 'Unknown';
30
+ if (!grouped[key]) grouped[key] = [];
31
+ grouped[key].push(dataset);
32
+ } else {
33
+ const key = 'No Region';
34
+ if (!grouped[key]) grouped[key] = [];
35
+ grouped[key].push(dataset);
36
+ }
37
+ } else {
38
+ const key = dataset.category?.name || 'Unknown';
39
+ if (!grouped[key]) grouped[key] = [];
40
+ grouped[key].push(dataset);
41
+ }
42
+ });
43
+
44
+ return Object.entries(grouped)
45
+ .map(
46
+ ([name, ds]) =>
47
+ [name, [...ds].sort((a, b) => a.name.localeCompare(b.name))] as const,
48
+ )
49
+ .filter(([, ds]) => ds.length > 0)
50
+ .sort((a, b) => a[0].localeCompare(b[0]));
51
+ }
@@ -0,0 +1,10 @@
1
+ export {
2
+ SidebarDatasetsItemsGrouped,
3
+ type SidebarDatasetsItemsGroupedProps,
4
+ } from './SidebarDatasetsItemsGrouped';
5
+ export type {
6
+ GroupedSidebarDatasetsEntry,
7
+ SidebarDatasetsItemsGroupBy,
8
+ SidebarDatasetsItemsGroupedDataset,
9
+ } from './groupSidebarDatasets';
10
+ export { groupSidebarDatasets } from './groupSidebarDatasets';