@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.
- 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/docs/standalone-apps.md +113 -52
- package/package.json +1 -1
- 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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { WorkspaceAppEntry } from './workspaceApp.types';
|
|
2
|
+
import { WORKSPACE_APP_SLUG_BASE_PATH } from './workspaceApp.types';
|
|
3
|
+
|
|
4
|
+
export function workspaceAppSlugPath(id: string): string {
|
|
5
|
+
return `${WORKSPACE_APP_SLUG_BASE_PATH}/${id}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function pathnameMatchesPrefix(pathname: string, prefix: string): boolean {
|
|
9
|
+
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function pathnameMatchesSlugApp(pathname: string, id: string): boolean {
|
|
13
|
+
const base = workspaceAppSlugPath(id);
|
|
14
|
+
return pathname === base || pathname.startsWith(`${base}/`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function findWorkspaceAppByPathname(
|
|
18
|
+
pathname: string,
|
|
19
|
+
apps: readonly WorkspaceAppEntry[],
|
|
20
|
+
): WorkspaceAppEntry | null {
|
|
21
|
+
for (const app of apps) {
|
|
22
|
+
const prefixes = app.matchPathPrefixes;
|
|
23
|
+
if (prefixes && prefixes.length > 0) {
|
|
24
|
+
if (prefixes.some(prefix => pathnameMatchesPrefix(pathname, prefix))) {
|
|
25
|
+
return app;
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (pathnameMatchesSlugApp(pathname, app.id)) {
|
|
30
|
+
return app;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { WorkspaceAppEntry } from './workspaceApp.types';
|
|
2
|
+
import { isWorkspaceAppIconKey } from './workspaceAppIcons';
|
|
3
|
+
|
|
4
|
+
function parseEntry(raw: unknown): WorkspaceAppEntry | null {
|
|
5
|
+
if (!raw || typeof raw !== 'object') {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const o = raw as Record<string, unknown>;
|
|
9
|
+
const idRaw = o.id ?? o.nativeId;
|
|
10
|
+
const displayName = o.displayName;
|
|
11
|
+
const subtitle = o.subtitle;
|
|
12
|
+
const iconKey = o.iconKey;
|
|
13
|
+
const accent = o.accent;
|
|
14
|
+
const accentMuted = o.accentMuted;
|
|
15
|
+
const href = o.href;
|
|
16
|
+
const prefixesRaw = o.matchPathPrefixes;
|
|
17
|
+
|
|
18
|
+
if (
|
|
19
|
+
typeof idRaw !== 'string' ||
|
|
20
|
+
!idRaw ||
|
|
21
|
+
typeof displayName !== 'string' ||
|
|
22
|
+
typeof subtitle !== 'string' ||
|
|
23
|
+
typeof iconKey !== 'string' ||
|
|
24
|
+
!isWorkspaceAppIconKey(iconKey) ||
|
|
25
|
+
typeof accent !== 'string' ||
|
|
26
|
+
typeof accentMuted !== 'string' ||
|
|
27
|
+
typeof href !== 'string' ||
|
|
28
|
+
!href
|
|
29
|
+
) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let matchPathPrefixes: WorkspaceAppEntry['matchPathPrefixes'];
|
|
34
|
+
if (prefixesRaw != null) {
|
|
35
|
+
if (!Array.isArray(prefixesRaw)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const prefixes = prefixesRaw.filter(
|
|
39
|
+
(p): p is string => typeof p === 'string',
|
|
40
|
+
);
|
|
41
|
+
if (prefixes.length > 0) {
|
|
42
|
+
matchPathPrefixes = prefixes;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
id: idRaw,
|
|
48
|
+
displayName,
|
|
49
|
+
subtitle,
|
|
50
|
+
iconKey,
|
|
51
|
+
accent,
|
|
52
|
+
accentMuted,
|
|
53
|
+
href,
|
|
54
|
+
matchPathPrefixes,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Read validated workspace apps JSON from localStorage; returns null if missing or invalid.
|
|
60
|
+
*/
|
|
61
|
+
export function readWorkspaceAppsFromLocalStorage(
|
|
62
|
+
key: string,
|
|
63
|
+
): WorkspaceAppEntry[] | null {
|
|
64
|
+
try {
|
|
65
|
+
const raw = localStorage.getItem(key);
|
|
66
|
+
if (raw == null || raw === '') {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const parsed: unknown = JSON.parse(raw);
|
|
70
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const apps: WorkspaceAppEntry[] = [];
|
|
74
|
+
for (const item of parsed) {
|
|
75
|
+
const entry = parseEntry(item);
|
|
76
|
+
if (entry) {
|
|
77
|
+
apps.push(entry);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return apps.length > 0 ? apps : null;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function writeWorkspaceAppsToLocalStorage(
|
|
87
|
+
key: string,
|
|
88
|
+
apps: WorkspaceAppEntry[],
|
|
89
|
+
): void {
|
|
90
|
+
try {
|
|
91
|
+
localStorage.setItem(key, JSON.stringify(apps));
|
|
92
|
+
} catch {
|
|
93
|
+
// ignore quota / private mode
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import cn from 'classnames';
|
|
2
|
+
|
|
3
|
+
import { AppHeaderPortal } from '#uilib/components/ui/AppHeader/AppHeader';
|
|
4
|
+
import { PAGE_HEADER_ACTIONS_ID } from '#uilib/components/ui/AppHeader/appChromeAnchors';
|
|
5
|
+
import { Gap } from '#uilib/components/ui/Gap/Gap';
|
|
6
|
+
import { NavUserHeader } from '#uilib/components/ui/NavUserHeader';
|
|
7
|
+
import type { NavUserHeaderProps } from '#uilib/components/ui/NavUserHeader';
|
|
8
|
+
import {
|
|
9
|
+
WorkspaceAppSwitcher,
|
|
10
|
+
type WorkspaceAppSwitcherProps,
|
|
11
|
+
} from '#uilib/components/ui/WorkspaceAppSwitcher';
|
|
12
|
+
|
|
13
|
+
import S from './SybilionAppHeader.styl';
|
|
14
|
+
|
|
15
|
+
export type SybilionAppHeaderProps = WorkspaceAppSwitcherProps &
|
|
16
|
+
NavUserHeaderProps & {
|
|
17
|
+
pageHeaderId?: string;
|
|
18
|
+
actionsAnchorId?: string;
|
|
19
|
+
actionsAnchorClassName?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function SybilionAppHeader({
|
|
23
|
+
pageHeaderId,
|
|
24
|
+
actionsAnchorId = PAGE_HEADER_ACTIONS_ID,
|
|
25
|
+
actionsAnchorClassName,
|
|
26
|
+
pathname,
|
|
27
|
+
onNavigate,
|
|
28
|
+
authenticated,
|
|
29
|
+
defaultApps,
|
|
30
|
+
appsStorageKey,
|
|
31
|
+
...navUserHeaderProps
|
|
32
|
+
}: SybilionAppHeaderProps) {
|
|
33
|
+
return (
|
|
34
|
+
<AppHeaderPortal pageHeaderId={pageHeaderId}>
|
|
35
|
+
<WorkspaceAppSwitcher
|
|
36
|
+
pathname={pathname}
|
|
37
|
+
onNavigate={onNavigate}
|
|
38
|
+
authenticated={authenticated}
|
|
39
|
+
defaultApps={defaultApps}
|
|
40
|
+
appsStorageKey={appsStorageKey}
|
|
41
|
+
/>
|
|
42
|
+
<Gap />
|
|
43
|
+
<div
|
|
44
|
+
id={actionsAnchorId}
|
|
45
|
+
className={cn(S.actionsAnchor, actionsAnchorClassName)}
|
|
46
|
+
>
|
|
47
|
+
<NavUserHeader {...navUserHeaderProps} />
|
|
48
|
+
</div>
|
|
49
|
+
</AppHeaderPortal>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
import { useCallback, useState } from 'react';
|
|
2
2
|
|
|
3
|
-
import { AppHeaderHost
|
|
4
|
-
import {
|
|
5
|
-
import { NavUserHeader } from '#uilib/components/ui/NavUserHeader/NavUserHeader';
|
|
3
|
+
import { AppHeaderHost } from '#uilib/components/ui/AppHeader';
|
|
4
|
+
import { DropdownMenuItem } from '#uilib/components/ui/DropdownMenu';
|
|
6
5
|
import {
|
|
7
6
|
AppShell,
|
|
8
7
|
AppShellMainContent,
|
|
8
|
+
PageContent,
|
|
9
9
|
PageContentSection,
|
|
10
|
+
PageHeader,
|
|
10
11
|
} from '#uilib/components/ui/Page';
|
|
11
12
|
import { PageFooter } from '#uilib/components/ui/Page/PageFooter/PageFooter';
|
|
12
13
|
import { PageScroll } from '#uilib/components/ui/Page/PageScroll/PageScroll';
|
|
13
14
|
import {
|
|
14
15
|
Sidebar,
|
|
15
16
|
SidebarContent,
|
|
16
|
-
SidebarGroup,
|
|
17
17
|
SidebarMenu,
|
|
18
18
|
SidebarMenuButton,
|
|
19
19
|
SidebarMenuItem,
|
|
20
20
|
SidebarProvider,
|
|
21
|
-
SidebarTrigger,
|
|
22
21
|
} from '#uilib/components/ui/Sidebar/Sidebar';
|
|
22
|
+
import type { WorkspaceAppEntry } from '#uilib/components/ui/WorkspaceAppSwitcher';
|
|
23
23
|
import {
|
|
24
24
|
SidebarDatasetsItemsGrouped,
|
|
25
25
|
type SidebarDatasetsItemsGroupedDataset,
|
|
26
26
|
} from '#uilib/components/widgets/SidebarDatasetsItemsGrouped';
|
|
27
|
+
import { SybilionAppHeader } from '#uilib/components/widgets/SybilionAppHeader';
|
|
28
|
+
import { GearSixIcon, UserCircleIcon } from '@phosphor-icons/react';
|
|
27
29
|
import { House } from 'lucide-react';
|
|
28
30
|
|
|
29
31
|
import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
|
|
@@ -91,6 +93,40 @@ type PreviewPanel = 'home' | 'datasets';
|
|
|
91
93
|
|
|
92
94
|
const TEST_HEADER_ID = 'test-header-id';
|
|
93
95
|
|
|
96
|
+
const DOCS_WORKSPACE_APPS_LS_KEY = 'uilib.docs.workspaceApps';
|
|
97
|
+
|
|
98
|
+
const DOCS_PREVIEW_APPS: WorkspaceAppEntry[] = [
|
|
99
|
+
{
|
|
100
|
+
id: 'docs-home',
|
|
101
|
+
displayName: 'Datasets Dashboard',
|
|
102
|
+
subtitle: 'Data analysis tools',
|
|
103
|
+
iconKey: 'grid-four',
|
|
104
|
+
accent: '#0d9488',
|
|
105
|
+
accentMuted: 'rgba(13, 148, 136, 0.12)',
|
|
106
|
+
href: '/docs-preview/home',
|
|
107
|
+
matchPathPrefixes: ['/docs-preview/home'],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'docs-datasets',
|
|
111
|
+
displayName: 'Datasets',
|
|
112
|
+
subtitle: 'Dataset library',
|
|
113
|
+
iconKey: 'package',
|
|
114
|
+
accent: '#6366f1',
|
|
115
|
+
accentMuted: 'rgba(99, 102, 241, 0.12)',
|
|
116
|
+
href: '/docs-preview/datasets',
|
|
117
|
+
matchPathPrefixes: ['/docs-preview/datasets'],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'my-custom-app',
|
|
121
|
+
displayName: 'Custom app',
|
|
122
|
+
subtitle: 'Slug-only app (sybilion.io/apps/…)',
|
|
123
|
+
iconKey: 'boat',
|
|
124
|
+
accent: '#0ea5e9',
|
|
125
|
+
accentMuted: 'rgba(14, 165, 233, 0.12)',
|
|
126
|
+
href: '/apps/my-custom-app',
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
|
|
94
130
|
function DemoAppSidebar({
|
|
95
131
|
panel,
|
|
96
132
|
onSelectPanel,
|
|
@@ -138,31 +174,49 @@ function DemoAppSidebar({
|
|
|
138
174
|
function DemoMainBody({ panel }: { panel: PreviewPanel }) {
|
|
139
175
|
if (panel === 'home') {
|
|
140
176
|
return (
|
|
141
|
-
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
177
|
+
<>
|
|
178
|
+
<PageHeader
|
|
179
|
+
breadcrumbs={[{ label: 'Home' }]}
|
|
180
|
+
title="Home"
|
|
181
|
+
subheader="Greenfield shell preview (no SDK or auth in this embed)."
|
|
182
|
+
/>
|
|
183
|
+
<PageContent>
|
|
184
|
+
<PageContentSection>
|
|
185
|
+
<p style={{ margin: 0, color: 'var(--muted-foreground)' }}>
|
|
186
|
+
Preview: greenfield shell layout only (no SDK or auth). Real SPA
|
|
187
|
+
uses one top-level Router; this embed cannot nest MemoryRouter
|
|
188
|
+
under docs BrowserRouter — sidebar uses local state instead of{' '}
|
|
189
|
+
<code>NavLink</code> / <code>Routes</code>.
|
|
190
|
+
</p>
|
|
191
|
+
</PageContentSection>
|
|
192
|
+
</PageContent>
|
|
193
|
+
</>
|
|
150
194
|
);
|
|
151
195
|
}
|
|
152
196
|
|
|
153
197
|
return (
|
|
154
|
-
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
198
|
+
<>
|
|
199
|
+
<PageHeader
|
|
200
|
+
breadcrumbs={[{ label: 'Datasets' }]}
|
|
201
|
+
title="Datasets"
|
|
202
|
+
subheader="Sidebar + dataset list use local state (not Router)."
|
|
203
|
+
/>
|
|
204
|
+
<PageContent>
|
|
205
|
+
<PageContentSection>
|
|
206
|
+
<p style={{ margin: 0, color: 'var(--muted-foreground)' }}>
|
|
207
|
+
Use sidebar links and dataset rows; panel state stays inside this
|
|
208
|
+
box.
|
|
209
|
+
</p>
|
|
210
|
+
</PageContentSection>
|
|
211
|
+
</PageContent>
|
|
212
|
+
</>
|
|
160
213
|
);
|
|
161
214
|
}
|
|
162
215
|
|
|
163
216
|
function StandaloneLayoutPreview() {
|
|
164
217
|
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
|
165
218
|
const [panel, setPanel] = useState<PreviewPanel>('home');
|
|
219
|
+
const [previewPath, setPreviewPath] = useState('/docs-preview/home');
|
|
166
220
|
const [selectedDatasetId, setSelectedDatasetId] = useState<
|
|
167
221
|
number | undefined
|
|
168
222
|
>();
|
|
@@ -191,20 +245,34 @@ function StandaloneLayoutPreview() {
|
|
|
191
245
|
/>
|
|
192
246
|
}
|
|
193
247
|
>
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
248
|
+
<SybilionAppHeader
|
|
249
|
+
pageHeaderId={TEST_HEADER_ID}
|
|
250
|
+
pathname={previewPath}
|
|
251
|
+
onNavigate={setPreviewPath}
|
|
252
|
+
authenticated
|
|
253
|
+
appsStorageKey={DOCS_WORKSPACE_APPS_LS_KEY}
|
|
254
|
+
defaultApps={DOCS_PREVIEW_APPS}
|
|
255
|
+
user={{
|
|
256
|
+
name: 'Preview User',
|
|
257
|
+
email: 'preview@example.com',
|
|
258
|
+
avatar: '',
|
|
259
|
+
}}
|
|
260
|
+
theme={theme}
|
|
261
|
+
onThemeToggle={onThemeToggle}
|
|
262
|
+
onLogout={() => undefined}
|
|
263
|
+
menuItems={
|
|
264
|
+
<>
|
|
265
|
+
<DropdownMenuItem>
|
|
266
|
+
<UserCircleIcon />
|
|
267
|
+
Account (preview)
|
|
268
|
+
</DropdownMenuItem>
|
|
269
|
+
<DropdownMenuItem>
|
|
270
|
+
<GearSixIcon />
|
|
271
|
+
Settings (preview)
|
|
272
|
+
</DropdownMenuItem>
|
|
273
|
+
</>
|
|
274
|
+
}
|
|
275
|
+
/>
|
|
208
276
|
<DemoMainBody panel={panel} />
|
|
209
277
|
</AppShellMainContent>
|
|
210
278
|
</AppShell>
|
package/src/index.ts
CHANGED
|
@@ -55,4 +55,6 @@ export * from './components/ui/Toggle';
|
|
|
55
55
|
export * from './components/ui/ToggleGroup';
|
|
56
56
|
export * from './components/ui/Tooltip';
|
|
57
57
|
export * from './components/ui/VimeoEmbed';
|
|
58
|
+
export * from './components/ui/WorkspaceAppSwitcher';
|
|
58
59
|
export * from './components/widgets/SidebarDatasetsItemsGrouped';
|
|
60
|
+
export * from './components/widgets/SybilionAppHeader';
|