@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.
Files changed (45) 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/dist/standalone/vite-sybilion-standalone-dev.d.ts +13 -0
  25. package/dist/standalone/vite-sybilion-standalone-dev.js +49 -0
  26. package/docs/standalone-apps.md +172 -43
  27. package/package.json +13 -3
  28. package/src/components/ui/AppHeader/appChromeAnchors.ts +3 -0
  29. package/src/components/ui/AppHeader/index.ts +1 -1
  30. package/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.styl +91 -0
  31. package/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.styl.d.ts +15 -0
  32. package/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.tsx +163 -0
  33. package/src/components/ui/WorkspaceAppSwitcher/index.ts +20 -0
  34. package/src/components/ui/WorkspaceAppSwitcher/workspaceApp.types.ts +21 -0
  35. package/src/components/ui/WorkspaceAppSwitcher/workspaceAppIcons.ts +27 -0
  36. package/src/components/ui/WorkspaceAppSwitcher/workspaceAppPaths.ts +34 -0
  37. package/src/components/ui/WorkspaceAppSwitcher/workspaceAppsConstants.ts +2 -0
  38. package/src/components/ui/WorkspaceAppSwitcher/workspaceAppsLocalStorage.ts +95 -0
  39. package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.styl +7 -0
  40. package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.styl.d.ts +7 -0
  41. package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.tsx +51 -0
  42. package/src/components/widgets/SybilionAppHeader/index.ts +4 -0
  43. package/src/docs/pages/StandaloneAppLayoutPage.tsx +102 -34
  44. package/src/index.ts +2 -0
  45. package/src/standalone/vite-sybilion-standalone-dev.ts +65 -0
@@ -0,0 +1,20 @@
1
+ export {
2
+ WorkspaceAppSwitcher,
3
+ type WorkspaceAppSwitcherProps,
4
+ } from './WorkspaceAppSwitcher';
5
+ export type { WorkspaceAppEntry } from './workspaceApp.types';
6
+ export { WORKSPACE_APP_SLUG_BASE_PATH } from './workspaceApp.types';
7
+ export { WORKSPACE_APPS_LS_KEY } from './workspaceAppsConstants';
8
+ export {
9
+ readWorkspaceAppsFromLocalStorage,
10
+ writeWorkspaceAppsToLocalStorage,
11
+ } from './workspaceAppsLocalStorage';
12
+ export {
13
+ WORKSPACE_APP_ICONS,
14
+ type WorkspaceAppIconKey,
15
+ isWorkspaceAppIconKey,
16
+ } from './workspaceAppIcons';
17
+ export {
18
+ findWorkspaceAppByPathname,
19
+ workspaceAppSlugPath,
20
+ } from './workspaceAppPaths';
@@ -0,0 +1,21 @@
1
+ import type { WorkspaceAppIconKey } from './workspaceAppIcons';
2
+
3
+ /** Path segment for slug apps: pathname matches `/apps/{id}` */
4
+ export const WORKSPACE_APP_SLUG_BASE_PATH = '/apps';
5
+
6
+ /** One surface in the workspace app switcher (serializable for localStorage). */
7
+ export type WorkspaceAppEntry = {
8
+ /** Slug (e.g. `my-custom-app` → `https://sybilion.io/apps/my-custom-app` via `href`). */
9
+ id: string;
10
+ displayName: string;
11
+ subtitle: string;
12
+ iconKey: WorkspaceAppIconKey;
13
+ accent: string;
14
+ accentMuted: string;
15
+ href: string;
16
+ /**
17
+ * Optional. Native / multi-route shells: match current app by pathname prefix.
18
+ * Omit for custom apps deployed under a single slug (`/apps/{id}` only).
19
+ */
20
+ matchPathPrefixes?: readonly string[];
21
+ };
@@ -0,0 +1,27 @@
1
+ import type { ComponentType } from 'react';
2
+
3
+ import { Boat, Package, TreeStructure } from '@phosphor-icons/react';
4
+ import { ClockAlert, LayoutDashboard } from 'lucide-react';
5
+
6
+ type IconLike = ComponentType<{ className?: string; 'aria-hidden'?: boolean }>;
7
+
8
+ export type WorkspaceAppIconKey =
9
+ | 'grid-four'
10
+ | 'clock'
11
+ | 'boat'
12
+ | 'package'
13
+ | 'tree-structure';
14
+
15
+ export const WORKSPACE_APP_ICONS: Record<WorkspaceAppIconKey, IconLike> = {
16
+ 'grid-four': LayoutDashboard as IconLike,
17
+ clock: ClockAlert as IconLike,
18
+ boat: Boat as IconLike,
19
+ package: Package as IconLike,
20
+ 'tree-structure': TreeStructure as IconLike,
21
+ };
22
+
23
+ const ICON_KEYS = Object.keys(WORKSPACE_APP_ICONS) as WorkspaceAppIconKey[];
24
+
25
+ export function isWorkspaceAppIconKey(v: string): v is WorkspaceAppIconKey {
26
+ return (ICON_KEYS as string[]).includes(v);
27
+ }
@@ -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';
@@ -0,0 +1,65 @@
1
+ import type { UserConfig } from 'vite';
2
+ import { loadEnv } from 'vite';
3
+
4
+ const DEFAULT_PORT = 3000;
5
+ const SYBILION_API_ENV = 'VITE_SYBILION_API_BASE_URL';
6
+
7
+ export type SybilionStandaloneViteDevOptions = {
8
+ mode: string;
9
+ /** Directory containing `.env*` files. @default process.cwd() */
10
+ envDir?: string;
11
+ /** Prefix proxied to Sybilion API (SDK `apiPrefix`). @default `/api` */
12
+ apiPrefix?: string;
13
+ };
14
+
15
+ let warnedMissingApiUrl = false;
16
+
17
+ function parsePort(raw: string | undefined): number {
18
+ if (raw == null || raw === '') return DEFAULT_PORT;
19
+ const n = Number.parseInt(raw, 10);
20
+ if (!Number.isFinite(n) || n <= 0 || n > 65_535) return DEFAULT_PORT;
21
+ return n;
22
+ }
23
+
24
+ function normalizeApiPrefix(apiPrefix: string): string {
25
+ return apiPrefix.startsWith('/') ? apiPrefix : `/${apiPrefix}`;
26
+ }
27
+
28
+ /**
29
+ * Vite `server` + `preview` fragment for standalone Sybilion SPAs: same-origin `/api` in dev,
30
+ * proxied to `VITE_SYBILION_API_BASE_URL`. Uses `PORT` from env (default `3000`).
31
+ */
32
+ export function sybilionStandaloneViteDev(
33
+ options: SybilionStandaloneViteDevOptions,
34
+ ): Pick<UserConfig, 'server' | 'preview'> {
35
+ const envDir = options.envDir ?? process.cwd();
36
+ const apiPrefix = normalizeApiPrefix(options.apiPrefix ?? '/api');
37
+ const env = loadEnv(options.mode, envDir, '');
38
+ const port = parsePort(env.PORT);
39
+ const target = (env[SYBILION_API_ENV] ?? '').replace(/\/$/, '');
40
+
41
+ const proxy: NonNullable<UserConfig['server']>['proxy'] = {};
42
+
43
+ if (target) {
44
+ proxy[apiPrefix] = {
45
+ target,
46
+ changeOrigin: true,
47
+ secure: true,
48
+ };
49
+ } else if (options.mode === 'development' && !warnedMissingApiUrl) {
50
+ warnedMissingApiUrl = true;
51
+ console.warn(
52
+ `[@sybilion/uilib] ${SYBILION_API_ENV} is not set; API dev proxy disabled.`,
53
+ );
54
+ }
55
+
56
+ const serverPreview = {
57
+ port,
58
+ proxy,
59
+ };
60
+
61
+ return {
62
+ server: serverPreview,
63
+ preview: serverPreview,
64
+ };
65
+ }