@sybilion/uilib 1.2.4 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/esm/components/ui/AppHeader/appChromeAnchors.js +3 -1
  2. package/dist/esm/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.js +62 -0
  3. package/dist/esm/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.styl.js +7 -0
  4. package/dist/esm/components/ui/WorkspaceAppSwitcher/workspaceApp.types.js +4 -0
  5. package/dist/esm/components/ui/WorkspaceAppSwitcher/workspaceAppIcons.js +16 -0
  6. package/dist/esm/components/ui/WorkspaceAppSwitcher/workspaceAppPaths.js +29 -0
  7. package/dist/esm/components/ui/WorkspaceAppSwitcher/workspaceAppsConstants.js +4 -0
  8. package/dist/esm/components/ui/WorkspaceAppSwitcher/workspaceAppsLocalStorage.js +84 -0
  9. package/dist/esm/components/widgets/SybilionAppHeader/SybilionAppHeader.js +16 -0
  10. package/dist/esm/components/widgets/SybilionAppHeader/SybilionAppHeader.styl.js +7 -0
  11. package/dist/esm/index.js +8 -1
  12. package/dist/esm/types/src/components/ui/AppHeader/appChromeAnchors.d.ts +2 -0
  13. package/dist/esm/types/src/components/ui/AppHeader/index.d.ts +1 -1
  14. package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.d.ts +12 -0
  15. package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/index.d.ts +7 -0
  16. package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/workspaceApp.types.d.ts +19 -0
  17. package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/workspaceAppIcons.d.ts +9 -0
  18. package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/workspaceAppPaths.d.ts +3 -0
  19. package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/workspaceAppsConstants.d.ts +2 -0
  20. package/dist/esm/types/src/components/ui/WorkspaceAppSwitcher/workspaceAppsLocalStorage.d.ts +6 -0
  21. package/dist/esm/types/src/components/widgets/SybilionAppHeader/SybilionAppHeader.d.ts +8 -0
  22. package/dist/esm/types/src/components/widgets/SybilionAppHeader/index.d.ts +1 -0
  23. package/dist/esm/types/src/index.d.ts +2 -0
  24. package/docs/standalone-apps.md +113 -52
  25. package/package.json +1 -1
  26. package/src/components/ui/AppHeader/appChromeAnchors.ts +3 -0
  27. package/src/components/ui/AppHeader/index.ts +1 -1
  28. package/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.styl +91 -0
  29. package/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.styl.d.ts +15 -0
  30. package/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.tsx +163 -0
  31. package/src/components/ui/WorkspaceAppSwitcher/index.ts +20 -0
  32. package/src/components/ui/WorkspaceAppSwitcher/workspaceApp.types.ts +21 -0
  33. package/src/components/ui/WorkspaceAppSwitcher/workspaceAppIcons.ts +27 -0
  34. package/src/components/ui/WorkspaceAppSwitcher/workspaceAppPaths.ts +34 -0
  35. package/src/components/ui/WorkspaceAppSwitcher/workspaceAppsConstants.ts +2 -0
  36. package/src/components/ui/WorkspaceAppSwitcher/workspaceAppsLocalStorage.ts +95 -0
  37. package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.styl +7 -0
  38. package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.styl.d.ts +7 -0
  39. package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.tsx +51 -0
  40. package/src/components/widgets/SybilionAppHeader/index.ts +4 -0
  41. package/src/docs/pages/StandaloneAppLayoutPage.tsx +102 -34
  42. package/src/index.ts +2 -0
@@ -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,2 @@
1
+ /** localStorage key for workspace app list JSON (shared by host app + demos). */
2
+ export const WORKSPACE_APPS_LS_KEY = 'sybilion.workspaceApps.v1';
@@ -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,7 @@
1
+ @import '../../../lib/theme.styl'
2
+
3
+ .actionsAnchor
4
+ display flex
5
+ align-items center
6
+ gap var(--p-4)
7
+ flex-shrink 0
@@ -0,0 +1,7 @@
1
+ // This file is automatically generated.
2
+ // Please do not change this file!
3
+ interface CssExports {
4
+ 'actionsAnchor': string;
5
+ }
6
+ export const cssExports: CssExports;
7
+ export default cssExports;
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export {
2
+ SybilionAppHeader,
3
+ type SybilionAppHeaderProps,
4
+ } from './SybilionAppHeader';
@@ -1,29 +1,31 @@
1
1
  import { useCallback, useState } from 'react';
2
2
 
3
- import { AppHeaderHost, AppHeaderPortal } from '#uilib/components/ui/AppHeader';
4
- import { Gap } from '#uilib/components/ui/Gap/Gap';
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
- <div style={{ padding: 'var(--p-6)' }}>
142
- <h2 style={{ margin: '0 0 0.5rem', fontSize: '1.125rem' }}>Home</h2>
143
- <p style={{ margin: 0, color: 'var(--muted-foreground)' }}>
144
- Preview: greenfield shell layout only (no SDK or auth). Real SPA uses
145
- one top-level Router; this embed cannot nest MemoryRouter under docs
146
- BrowserRouter — sidebar uses local state instead of{' '}
147
- <code>NavLink</code> / <code>Routes</code>.
148
- </p>
149
- </div>
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
- <div style={{ padding: 'var(--p-6)' }}>
155
- <h2 style={{ margin: '0 0 0.5rem', fontSize: '1.125rem' }}>Datasets</h2>
156
- <p style={{ margin: 0, color: 'var(--muted-foreground)' }}>
157
- Use sidebar links and dataset rows; panel state stays inside this box.
158
- </p>
159
- </div>
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
- <AppHeaderPortal pageHeaderId={TEST_HEADER_ID}>
195
- <SidebarTrigger />
196
- <Gap />
197
- <NavUserHeader
198
- user={{
199
- name: 'Preview User',
200
- email: 'preview@example.com',
201
- avatar: '',
202
- }}
203
- theme={theme}
204
- onThemeToggle={onThemeToggle}
205
- onLogout={() => undefined}
206
- />
207
- </AppHeaderPortal>
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';