@sybilion/uilib 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/assets/{mini-app-global.css → standalone-global.css} +1 -1
- package/dist/esm/components/ui/Image/Image.styl.js +1 -1
- package/dist/esm/components/ui/NavUserHeader/NavUserHeader.js +28 -0
- package/dist/esm/components/ui/NavUserHeader/NavUserHeader.styl.js +7 -0
- package/dist/esm/components/ui/Sidebar/Sidebar.styl.js +2 -2
- package/dist/esm/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.js +48 -0
- package/dist/esm/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.styl.js +7 -0
- package/dist/esm/components/widgets/SidebarDatasetsItemsGrouped/groupSidebarDatasets.js +38 -0
- package/dist/esm/index.js +6 -5
- package/dist/esm/sybilion-auth/SybilionAuthProvider.js +208 -0
- package/dist/esm/sybilion-auth/authPaths.js +7 -0
- package/dist/esm/sybilion-auth/exchangeSybilionToken.js +44 -0
- package/dist/esm/types/src/components/ui/Input/Input.d.ts +1 -1
- package/dist/esm/types/src/components/ui/NavUserHeader/NavUserHeader.d.ts +2 -0
- package/dist/esm/types/src/components/ui/NavUserHeader/NavUserHeader.types.d.ts +25 -0
- package/dist/esm/types/src/components/ui/NavUserHeader/index.d.ts +2 -0
- package/dist/esm/types/src/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.d.ts +11 -0
- package/dist/esm/types/src/components/widgets/SidebarDatasetsItemsGrouped/groupSidebarDatasets.d.ts +8 -0
- package/dist/esm/types/src/components/widgets/SidebarDatasetsItemsGrouped/index.d.ts +3 -0
- package/dist/esm/types/src/docs/pages/NavUserHeaderPage.d.ts +1 -0
- package/dist/esm/types/src/docs/pages/SidebarDatasetsItemsGroupedPage.d.ts +1 -0
- package/dist/esm/types/src/docs/pages/SybilionAuthProviderPage.d.ts +1 -0
- package/dist/esm/types/src/index.d.ts +4 -1
- package/dist/esm/types/src/sybilion-auth/SybilionAuthProvider.d.ts +42 -0
- package/dist/esm/types/src/sybilion-auth/authPaths.d.ts +3 -0
- package/dist/esm/types/src/sybilion-auth/exchangeSybilionToken.d.ts +4 -0
- package/dist/esm/types/src/sybilion-auth/index.d.ts +4 -0
- package/dist/esm/types/src/{mini-app/miniAppDataTypes.d.ts → types/sybilionDatasetSnapshots.d.ts} +5 -8
- package/docs/standalone-apps.md +181 -0
- package/package.json +15 -3
- package/src/components/ui/Image/Image.styl +1 -0
- package/src/components/ui/NavUserHeader/NavUserHeader.styl +125 -0
- package/src/components/ui/NavUserHeader/NavUserHeader.styl.d.ts +28 -0
- package/src/components/ui/NavUserHeader/NavUserHeader.tsx +148 -0
- package/src/components/ui/NavUserHeader/NavUserHeader.types.ts +27 -0
- package/src/components/ui/NavUserHeader/avatar.svg +4 -0
- package/src/components/ui/NavUserHeader/index.ts +5 -0
- package/src/components/ui/Sidebar/Sidebar.styl +0 -25
- package/src/components/ui/Sidebar/Sidebar.styl.d.ts +0 -1
- package/src/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.styl +29 -0
- package/src/{mini-app/MiniAppRoot.styl.d.ts → components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.styl.d.ts} +4 -2
- package/src/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.tsx +128 -0
- package/src/components/widgets/SidebarDatasetsItemsGrouped/groupSidebarDatasets.ts +51 -0
- package/src/components/widgets/SidebarDatasetsItemsGrouped/index.ts +10 -0
- package/src/docs/pages/NavUserHeaderPage.tsx +89 -0
- package/src/docs/pages/SidebarDatasetsItemsGroupedPage.tsx +136 -0
- package/src/docs/pages/SybilionAuthProviderPage.tsx +40 -0
- package/src/docs/registry.ts +15 -3
- package/src/index.ts +4 -1
- package/src/sybilion-auth/SybilionAuthProvider.tsx +344 -0
- package/src/sybilion-auth/authPaths.ts +6 -0
- package/src/sybilion-auth/exchangeSybilionToken.ts +51 -0
- package/src/sybilion-auth/index.ts +17 -0
- package/src/{mini-app/miniAppDataTypes.ts → types/sybilionDatasetSnapshots.ts} +5 -8
- package/dist/esm/mini-app/MiniAppRoot.js +0 -82
- package/dist/esm/mini-app/MiniAppRoot.styl.js +0 -7
- package/dist/esm/mini-app/miniAppChatBridge.js +0 -45
- package/dist/esm/mini-app/miniAppDataClient.js +0 -98
- package/dist/esm/mini-app/miniAppProtocol.js +0 -153
- package/dist/esm/mini-app/miniAppThemeConfig.js +0 -40
- package/dist/esm/types/src/docs/pages/MiniAppRootPage.d.ts +0 -1
- package/dist/esm/types/src/mini-app/MiniAppRoot.d.ts +0 -18
- package/dist/esm/types/src/mini-app/index.d.ts +0 -10
- package/dist/esm/types/src/mini-app/miniAppChatBridge.d.ts +0 -6
- package/dist/esm/types/src/mini-app/miniAppDataClient.d.ts +0 -16
- package/dist/esm/types/src/mini-app/miniAppProtocol.d.ts +0 -89
- package/dist/esm/types/src/mini-app/miniAppThemeConfig.d.ts +0 -3
- package/docs/workspace-mini-apps.md +0 -51
- package/src/docs/pages/MiniAppRootPage.tsx +0 -58
- package/src/mini-app/MiniAppRoot.styl +0 -24
- package/src/mini-app/MiniAppRoot.tsx +0 -150
- package/src/mini-app/index.ts +0 -43
- package/src/mini-app/miniAppChatBridge.ts +0 -55
- package/src/mini-app/miniAppDataClient.ts +0 -165
- package/src/mini-app/miniAppProtocol.ts +0 -247
- package/src/mini-app/miniAppThemeConfig.ts +0 -45
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import cn from 'classnames';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
MoonIcon,
|
|
5
|
+
SignOutIcon,
|
|
6
|
+
SunIcon,
|
|
7
|
+
UserCircleIcon,
|
|
8
|
+
} from '@phosphor-icons/react';
|
|
9
|
+
import { ChevronDownIcon } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
import { Avatar } from '../Avatar';
|
|
12
|
+
import { Button } from '../Button';
|
|
13
|
+
import {
|
|
14
|
+
DropdownMenu,
|
|
15
|
+
DropdownMenuContent,
|
|
16
|
+
DropdownMenuGroup,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
DropdownMenuLabel,
|
|
19
|
+
DropdownMenuSeparator,
|
|
20
|
+
DropdownMenuTrigger,
|
|
21
|
+
} from '../DropdownMenu';
|
|
22
|
+
import { Image } from '../Image';
|
|
23
|
+
import S from './NavUserHeader.styl';
|
|
24
|
+
import type { NavUserHeaderProps } from './NavUserHeader.types';
|
|
25
|
+
|
|
26
|
+
export function NavUserHeader({
|
|
27
|
+
variant = 'default',
|
|
28
|
+
isLoading = false,
|
|
29
|
+
isAuthenticated,
|
|
30
|
+
user = null,
|
|
31
|
+
menuItems,
|
|
32
|
+
theme,
|
|
33
|
+
onThemeToggle,
|
|
34
|
+
onLogout,
|
|
35
|
+
signInSlot,
|
|
36
|
+
onSignInClick,
|
|
37
|
+
}: NavUserHeaderProps) {
|
|
38
|
+
const authenticated = isAuthenticated ?? true;
|
|
39
|
+
|
|
40
|
+
const avatarUrl = user?.avatar ?? '';
|
|
41
|
+
const userName = user?.name ?? '';
|
|
42
|
+
const userEmail = user?.email ?? '';
|
|
43
|
+
|
|
44
|
+
if (isLoading) {
|
|
45
|
+
return (
|
|
46
|
+
<Button variant="ghost" size="sm" disabled className={S.loadingButton}>
|
|
47
|
+
<div className={S.avatarSkeleton} />
|
|
48
|
+
<div className={S.textSkeleton} />
|
|
49
|
+
</Button>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!authenticated) {
|
|
54
|
+
if (signInSlot) {
|
|
55
|
+
return signInSlot;
|
|
56
|
+
}
|
|
57
|
+
return (
|
|
58
|
+
<Button
|
|
59
|
+
variant="ghost"
|
|
60
|
+
size="sm"
|
|
61
|
+
className={S.loginButton}
|
|
62
|
+
type="button"
|
|
63
|
+
onClick={onSignInClick}
|
|
64
|
+
>
|
|
65
|
+
<UserCircleIcon className={S.iconLg} />
|
|
66
|
+
<span>Log in</span>
|
|
67
|
+
</Button>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<DropdownMenu>
|
|
73
|
+
<DropdownMenuTrigger asChild>
|
|
74
|
+
<Button
|
|
75
|
+
variant="ghost"
|
|
76
|
+
size="sm"
|
|
77
|
+
className={cn(S.userButton, variant === 'compact' && S.compact)}
|
|
78
|
+
>
|
|
79
|
+
<Avatar className={S.avatar}>
|
|
80
|
+
<Image
|
|
81
|
+
url={avatarUrl}
|
|
82
|
+
alt={userName}
|
|
83
|
+
fallback={<div className={S.avatarFallback} />}
|
|
84
|
+
/>
|
|
85
|
+
</Avatar>
|
|
86
|
+
{variant === 'default' && (
|
|
87
|
+
<>
|
|
88
|
+
<div className={S.userInfo}>
|
|
89
|
+
<span className={`${S.userName} ph-no-capture`}>
|
|
90
|
+
{userName}
|
|
91
|
+
</span>
|
|
92
|
+
<span className={S.userEmail}>{userEmail}</span>
|
|
93
|
+
</div>
|
|
94
|
+
<ChevronDownIcon className={S.iconSm} />
|
|
95
|
+
</>
|
|
96
|
+
)}
|
|
97
|
+
</Button>
|
|
98
|
+
</DropdownMenuTrigger>
|
|
99
|
+
<DropdownMenuContent
|
|
100
|
+
className={S.dropdownContent}
|
|
101
|
+
align="end"
|
|
102
|
+
elevation="md"
|
|
103
|
+
>
|
|
104
|
+
<DropdownMenuLabel className={S.userLabel}>
|
|
105
|
+
<div className={S.userLabelContent}>
|
|
106
|
+
<Avatar className={S.avatar}>
|
|
107
|
+
<Image
|
|
108
|
+
url={avatarUrl}
|
|
109
|
+
alt={userName}
|
|
110
|
+
fallback={<div className={S.avatarFallback} />}
|
|
111
|
+
/>
|
|
112
|
+
</Avatar>
|
|
113
|
+
<div className={S.userDetails}>
|
|
114
|
+
<span className={`${S.userDetailName} ph-no-capture`}>
|
|
115
|
+
{userName}
|
|
116
|
+
</span>
|
|
117
|
+
<span className={S.userDetailEmail}>{userEmail}</span>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</DropdownMenuLabel>
|
|
121
|
+
<DropdownMenuSeparator />
|
|
122
|
+
<DropdownMenuGroup>
|
|
123
|
+
{menuItems}
|
|
124
|
+
{onThemeToggle ? (
|
|
125
|
+
<DropdownMenuItem onSelect={() => onThemeToggle()}>
|
|
126
|
+
{theme === 'dark' ? (
|
|
127
|
+
<>
|
|
128
|
+
<SunIcon />
|
|
129
|
+
Light theme
|
|
130
|
+
</>
|
|
131
|
+
) : (
|
|
132
|
+
<>
|
|
133
|
+
<MoonIcon />
|
|
134
|
+
Dark theme
|
|
135
|
+
</>
|
|
136
|
+
)}
|
|
137
|
+
</DropdownMenuItem>
|
|
138
|
+
) : null}
|
|
139
|
+
</DropdownMenuGroup>
|
|
140
|
+
<DropdownMenuSeparator />
|
|
141
|
+
<DropdownMenuItem variant="destructive" onSelect={() => onLogout()}>
|
|
142
|
+
<SignOutIcon />
|
|
143
|
+
Log out
|
|
144
|
+
</DropdownMenuItem>
|
|
145
|
+
</DropdownMenuContent>
|
|
146
|
+
</DropdownMenu>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type NavUserHeaderUser = {
|
|
4
|
+
name: string;
|
|
5
|
+
email: string;
|
|
6
|
+
/** Passed to avatar `Image` as `src`. */
|
|
7
|
+
avatar?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type NavUserHeaderProps = {
|
|
11
|
+
variant?: 'default' | 'compact';
|
|
12
|
+
isLoading?: boolean;
|
|
13
|
+
/** When false, signed-out branch is shown. Defaults to true when omitted. */
|
|
14
|
+
isAuthenticated?: boolean;
|
|
15
|
+
/** Present when authenticated: shown in trigger and dropdown label. */
|
|
16
|
+
user?: NavUserHeaderUser | null;
|
|
17
|
+
/** Rows inside the menu above theme toggle and logout. Use `DropdownMenuItem` nodes. */
|
|
18
|
+
menuItems?: ReactNode;
|
|
19
|
+
/** Current theme drives the toggle row label/icons. */
|
|
20
|
+
theme: 'light' | 'dark';
|
|
21
|
+
/** When set, renders the light/dark theme menu row. */
|
|
22
|
+
onThemeToggle?: () => void;
|
|
23
|
+
onLogout: () => void;
|
|
24
|
+
/** Replaces default “Log in” control when signed out. */
|
|
25
|
+
signInSlot?: ReactNode;
|
|
26
|
+
onSignInClick?: () => void;
|
|
27
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none">
|
|
2
|
+
<path fill="#ECFEFF" d="M0 16C0 7.163 7.163 0 16 0s16 7.163 16 16-7.163 16-16 16S0 24.837 0 16" />
|
|
3
|
+
<path fill="#22D3EE" fill-rule="evenodd" d="M17.4 9.454h-2.8V8h2.8zm1.4 1.455V9.454h-1.4v1.455zm0 2.91v-2.91h1.4v2.91zm-1.4 1.454v-1.455h1.4v1.455zm-2.8 0h2.8v1.454h-2.8zm-1.4-1.455v1.455h1.4v-1.455zm0-2.91v2.91h-1.4v-2.91zm0 0h1.4V9.455h-1.4zm6.3 11.638h-7V21.09h-2.1v-1.455h1.4v-1.454h2.1v-1.454h-2.1v1.454h-1.4v1.454H9v1.455h1.4v1.455h2.1V24h7zm2.1-1.455v1.455h-2.1V21.09zm0-1.455H23v1.455h-1.4zm-1.4-1.454v1.454h1.4v-1.454zm0 0h-2.1v-1.454h2.1z" clip-rule="evenodd" />
|
|
4
|
+
</svg>
|
|
@@ -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)
|
|
@@ -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
|
-
'
|
|
5
|
-
'
|
|
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';
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { DropdownMenuItem } from '#uilib/components/ui/DropdownMenu';
|
|
4
|
+
import { NavUserHeader } from '#uilib/components/ui/NavUserHeader';
|
|
5
|
+
import { PageContentSection } from '#uilib/components/ui/Page';
|
|
6
|
+
import { GearSixIcon, UserCircleIcon } from '@phosphor-icons/react';
|
|
7
|
+
|
|
8
|
+
import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
|
|
9
|
+
import { DocsHeaderActions } from '../docsHeaderActions';
|
|
10
|
+
|
|
11
|
+
export default function NavUserHeaderPage() {
|
|
12
|
+
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
document.documentElement.dataset.theme = theme;
|
|
16
|
+
return () => {
|
|
17
|
+
delete document.documentElement.dataset.theme;
|
|
18
|
+
};
|
|
19
|
+
}, [theme]);
|
|
20
|
+
|
|
21
|
+
const onThemeToggle = useCallback(() => {
|
|
22
|
+
setTheme(t => (t === 'dark' ? 'light' : 'dark'));
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const customMenuItems = (
|
|
26
|
+
<>
|
|
27
|
+
<DropdownMenuItem>
|
|
28
|
+
<UserCircleIcon />
|
|
29
|
+
Account (custom)
|
|
30
|
+
</DropdownMenuItem>
|
|
31
|
+
<DropdownMenuItem>
|
|
32
|
+
<GearSixIcon />
|
|
33
|
+
Settings (custom)
|
|
34
|
+
</DropdownMenuItem>
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<AppPageHeader
|
|
41
|
+
breadcrumbs={[{ label: 'NavUserHeader' }]}
|
|
42
|
+
title="NavUserHeader"
|
|
43
|
+
subheader="User menu with label, custom rows, theme toggle, and logout."
|
|
44
|
+
actions={<DocsHeaderActions />}
|
|
45
|
+
/>
|
|
46
|
+
<PageContentSection
|
|
47
|
+
style={{ display: 'flex', flexWrap: 'wrap', gap: '2rem' }}
|
|
48
|
+
>
|
|
49
|
+
<div>
|
|
50
|
+
<p style={{ marginBottom: 8, fontSize: 12 }}>
|
|
51
|
+
Signed in · default variant
|
|
52
|
+
</p>
|
|
53
|
+
<NavUserHeader
|
|
54
|
+
user={{
|
|
55
|
+
name: 'Demo Analyst',
|
|
56
|
+
email: 'demo@sybilion.io',
|
|
57
|
+
avatar: '',
|
|
58
|
+
}}
|
|
59
|
+
theme={theme}
|
|
60
|
+
onThemeToggle={onThemeToggle}
|
|
61
|
+
onLogout={() => {
|
|
62
|
+
console.info('[docs] logout');
|
|
63
|
+
}}
|
|
64
|
+
menuItems={customMenuItems}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
<div>
|
|
68
|
+
<p style={{ marginBottom: 8, fontSize: 12 }}>
|
|
69
|
+
Signed in · compact variant
|
|
70
|
+
</p>
|
|
71
|
+
<NavUserHeader
|
|
72
|
+
variant="compact"
|
|
73
|
+
user={{
|
|
74
|
+
name: 'Compact',
|
|
75
|
+
email: 'compact@sybilion.io',
|
|
76
|
+
avatar: '',
|
|
77
|
+
}}
|
|
78
|
+
theme={theme}
|
|
79
|
+
onThemeToggle={onThemeToggle}
|
|
80
|
+
onLogout={() => {
|
|
81
|
+
console.info('[docs] logout compact');
|
|
82
|
+
}}
|
|
83
|
+
menuItems={customMenuItems}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
</PageContentSection>
|
|
87
|
+
</>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { PageContentSection } from '#uilib/components/ui/Page';
|
|
4
|
+
import { ToggleGroup, ToggleGroupItem } from '#uilib/components/ui/ToggleGroup';
|
|
5
|
+
import {
|
|
6
|
+
type SidebarDatasetsItemsGroupBy,
|
|
7
|
+
SidebarDatasetsItemsGrouped,
|
|
8
|
+
type SidebarDatasetsItemsGroupedDataset,
|
|
9
|
+
} from '#uilib/components/widgets/SidebarDatasetsItemsGrouped';
|
|
10
|
+
|
|
11
|
+
import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
|
|
12
|
+
import { DocsHeaderActions } from '../docsHeaderActions';
|
|
13
|
+
|
|
14
|
+
const MOCK_DATASETS: SidebarDatasetsItemsGroupedDataset[] = [
|
|
15
|
+
{
|
|
16
|
+
id: 1,
|
|
17
|
+
name: 'Acetic Acid Price - China - Dollar/MT',
|
|
18
|
+
status: 'active',
|
|
19
|
+
created_at: '2024-01-01',
|
|
20
|
+
updated_at: '2024-01-02',
|
|
21
|
+
keywords: '',
|
|
22
|
+
category: { id: 10, name: 'Chemicals' },
|
|
23
|
+
target_type_id: 1,
|
|
24
|
+
target_type: { id: 1, name: 'Commodity price' },
|
|
25
|
+
trend: 0,
|
|
26
|
+
regular_price: '0',
|
|
27
|
+
sale_price: '0',
|
|
28
|
+
regions: [
|
|
29
|
+
{ id: 1, name: 'Asia' },
|
|
30
|
+
{ id: 2, name: 'China' },
|
|
31
|
+
],
|
|
32
|
+
unit: { id: 1, name: 'USD/MT' },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 2,
|
|
36
|
+
name: 'Freight index — spot volume (China)',
|
|
37
|
+
status: 'active',
|
|
38
|
+
created_at: '2024-01-01',
|
|
39
|
+
updated_at: '2024-01-02',
|
|
40
|
+
keywords: '',
|
|
41
|
+
category: { id: 10, name: 'Chemicals' },
|
|
42
|
+
target_type_id: 2,
|
|
43
|
+
target_type: { id: 2, name: 'Freight index' },
|
|
44
|
+
trend: 0,
|
|
45
|
+
regular_price: '0',
|
|
46
|
+
sale_price: '0',
|
|
47
|
+
regions: [
|
|
48
|
+
{ id: 1, name: 'Asia' },
|
|
49
|
+
{ id: 2, name: 'China' },
|
|
50
|
+
],
|
|
51
|
+
unit: { id: 1, name: 'Index' },
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 3,
|
|
55
|
+
name: 'Generic industrial chemical — spot volume (Germany)',
|
|
56
|
+
status: 'active',
|
|
57
|
+
created_at: '2024-01-01',
|
|
58
|
+
updated_at: '2024-01-02',
|
|
59
|
+
keywords: '',
|
|
60
|
+
category: { id: 10, name: 'Chemicals' },
|
|
61
|
+
target_type_id: 1,
|
|
62
|
+
target_type: { id: 1, name: 'Commodity price' },
|
|
63
|
+
trend: 0,
|
|
64
|
+
regular_price: '0',
|
|
65
|
+
sale_price: '0',
|
|
66
|
+
regions: [
|
|
67
|
+
{ id: 3, name: 'Europe' },
|
|
68
|
+
{ id: 4, name: 'Germany' },
|
|
69
|
+
],
|
|
70
|
+
unit: { id: 1, name: 'MT' },
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 4,
|
|
74
|
+
name: 'Ethanol Price Europe Per Kg In USD',
|
|
75
|
+
status: 'active',
|
|
76
|
+
created_at: '2024-01-01',
|
|
77
|
+
updated_at: '2024-01-02',
|
|
78
|
+
keywords: '',
|
|
79
|
+
category: { id: 11, name: 'Energy' },
|
|
80
|
+
target_type_id: 1,
|
|
81
|
+
target_type: { id: 1, name: 'Commodity price' },
|
|
82
|
+
trend: 0,
|
|
83
|
+
regular_price: '0',
|
|
84
|
+
sale_price: '0',
|
|
85
|
+
regions: [{ id: 5, name: 'Europe' }],
|
|
86
|
+
unit: { id: 1, name: 'USD/kg' },
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
export default function SidebarDatasetsItemsGroupedPage() {
|
|
91
|
+
const [groupBy, setGroupBy] =
|
|
92
|
+
useState<SidebarDatasetsItemsGroupBy>('regions');
|
|
93
|
+
const [selectedDatasetId, setSelectedDatasetId] = useState<number>(1);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<>
|
|
97
|
+
<AppPageHeader
|
|
98
|
+
breadcrumbs={[{ label: 'SidebarDatasetsItemsGrouped' }]}
|
|
99
|
+
title="SidebarDatasetsItemsGrouped"
|
|
100
|
+
subheader="Datasets sidebar: collapsible groups and nested rows."
|
|
101
|
+
actions={<DocsHeaderActions />}
|
|
102
|
+
/>
|
|
103
|
+
<PageContentSection>
|
|
104
|
+
<ToggleGroup
|
|
105
|
+
type="single"
|
|
106
|
+
value={groupBy}
|
|
107
|
+
onValueChange={val => {
|
|
108
|
+
if (val) setGroupBy(val as SidebarDatasetsItemsGroupBy);
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<ToggleGroupItem value="target_type">Target type</ToggleGroupItem>
|
|
112
|
+
<ToggleGroupItem value="regions">Regions</ToggleGroupItem>
|
|
113
|
+
<ToggleGroupItem value="categories">Categories</ToggleGroupItem>
|
|
114
|
+
</ToggleGroup>
|
|
115
|
+
<div
|
|
116
|
+
style={{
|
|
117
|
+
maxWidth: 420,
|
|
118
|
+
marginTop: 16,
|
|
119
|
+
padding: 12,
|
|
120
|
+
background: 'var(--sidebar)',
|
|
121
|
+
color: 'var(--sidebar-foreground)',
|
|
122
|
+
border: '1px solid var(--border)',
|
|
123
|
+
borderRadius: 'var(--p-4)',
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
<SidebarDatasetsItemsGrouped
|
|
127
|
+
groupBy={groupBy}
|
|
128
|
+
datasets={MOCK_DATASETS}
|
|
129
|
+
selectedDatasetId={selectedDatasetId}
|
|
130
|
+
onDatasetClick={setSelectedDatasetId}
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
</PageContentSection>
|
|
134
|
+
</>
|
|
135
|
+
);
|
|
136
|
+
}
|