@tumbaland/frontend-core 1.14.1 → 1.15.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 (77) hide show
  1. package/dist/authService.d.ts +10 -0
  2. package/dist/authService.js +18 -3
  3. package/dist/components/AppErrorBoundary/AppErrorBoundary.d.ts +26 -0
  4. package/dist/components/AppErrorBoundary/AppErrorBoundary.js +25 -0
  5. package/dist/components/AppErrorBoundary/index.d.ts +2 -0
  6. package/dist/components/AppErrorBoundary/index.js +1 -0
  7. package/dist/components/AppLayout/AppLayout.d.ts +27 -0
  8. package/dist/components/AppLayout/AppLayout.js +45 -0
  9. package/dist/components/AppLayout/index.d.ts +2 -0
  10. package/dist/components/AppLayout/index.js +1 -0
  11. package/dist/components/AuthHeader/AuthHeader.d.ts +57 -0
  12. package/dist/components/AuthHeader/AuthHeader.js +135 -0
  13. package/dist/components/AuthHeader/index.d.ts +2 -0
  14. package/dist/components/AuthHeader/index.js +1 -0
  15. package/dist/components/ErrorPage/ErrorPage.d.ts +17 -0
  16. package/dist/components/ErrorPage/ErrorPage.js +19 -0
  17. package/dist/components/ErrorPage/index.d.ts +2 -0
  18. package/dist/components/ErrorPage/index.js +1 -0
  19. package/dist/components/Loader/Loader.d.ts +12 -0
  20. package/dist/components/Loader/Loader.js +15 -0
  21. package/dist/components/Loader/index.d.ts +2 -0
  22. package/dist/components/Loader/index.js +1 -0
  23. package/dist/components/LoginButton/LoginButton.d.ts +5 -0
  24. package/dist/components/LoginButton/LoginButton.js +15 -0
  25. package/dist/components/LoginButton/index.d.ts +2 -0
  26. package/dist/components/LoginButton/index.js +1 -0
  27. package/dist/components/NotificationsMenu/NotificationsMenu.d.ts +9 -0
  28. package/dist/components/NotificationsMenu/NotificationsMenu.js +29 -0
  29. package/dist/components/ProtectedRoute/ProtectedRoute.d.ts +33 -0
  30. package/dist/components/ProtectedRoute/ProtectedRoute.js +50 -0
  31. package/dist/components/ProtectedRoute/index.d.ts +2 -0
  32. package/dist/components/ProtectedRoute/index.js +1 -0
  33. package/dist/components/UnauthorizedPage/UnauthorizedPage.d.ts +5 -0
  34. package/dist/components/UnauthorizedPage/UnauthorizedPage.js +9 -0
  35. package/dist/components/UnauthorizedPage/index.d.ts +2 -0
  36. package/dist/components/UnauthorizedPage/index.js +1 -0
  37. package/dist/components/UsageMeter/UsageMeter.d.ts +35 -0
  38. package/dist/components/UsageMeter/UsageMeter.js +50 -0
  39. package/dist/components/UsageMeter/index.d.ts +4 -0
  40. package/dist/components/UsageMeter/index.js +2 -0
  41. package/dist/components/UsageMeter/useEntitlements.d.ts +27 -0
  42. package/dist/components/UsageMeter/useEntitlements.js +56 -0
  43. package/dist/components/tenant/TenantProvider.d.ts +11 -0
  44. package/dist/components/tenant/TenantProvider.js +53 -0
  45. package/dist/components/tenant/TenantSelector.d.ts +24 -0
  46. package/dist/components/tenant/TenantSelector.js +77 -0
  47. package/dist/components/tenant/index.d.ts +5 -0
  48. package/dist/components/tenant/index.js +3 -0
  49. package/dist/components/tenant/useTenant.d.ts +15 -0
  50. package/dist/components/tenant/useTenant.js +222 -0
  51. package/dist/components/tenant/useTenantDataRefresh.d.ts +6 -0
  52. package/dist/components/tenant/useTenantDataRefresh.js +28 -0
  53. package/dist/config/createConfigProvider.d.ts +44 -0
  54. package/dist/config/createConfigProvider.js +92 -0
  55. package/dist/federation.d.ts +46 -0
  56. package/dist/federation.js +37 -0
  57. package/dist/index.d.ts +26 -0
  58. package/dist/index.js +34 -1
  59. package/dist/monitoring/batch.js +3 -3
  60. package/dist/monitoring/config.js +1 -4
  61. package/dist/monitoring/console.js +1 -1
  62. package/dist/plansService.js +1 -1
  63. package/dist/routing/embedContext.d.ts +31 -0
  64. package/dist/routing/embedContext.js +63 -0
  65. package/dist/routing/index.d.ts +12 -0
  66. package/dist/routing/index.js +12 -0
  67. package/dist/sessionStorage.d.ts +1 -1
  68. package/dist/sessionStorage.js +1 -1
  69. package/dist/theme/createTumbalandTheme.d.ts +38 -0
  70. package/dist/theme/createTumbalandTheme.js +245 -0
  71. package/dist/theme/index.d.ts +2 -0
  72. package/dist/theme/index.js +2 -0
  73. package/dist/theme/tokens.d.ts +169 -0
  74. package/dist/theme/tokens.js +162 -0
  75. package/package.json +28 -2
  76. package/dist/test/setup.d.ts +0 -9
  77. package/dist/test/setup.js +0 -26
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { logger } from '../../monitoring';
4
+ import { Loader } from '../Loader';
5
+ import { UnauthorizedPage } from '../UnauthorizedPage';
6
+ /**
7
+ * Gates its children behind an authenticated session: renders a Loader while
8
+ * the check is in flight, UnauthorizedPage (whose login action calls the auth
9
+ * service's redirectToLogin) when unauthenticated or the check fails, and the
10
+ * children once authenticated.
11
+ *
12
+ * The first state comes from the auth service's cache when it has one, so a
13
+ * remount does not flash "Checking authentication..." over an answer already
14
+ * held in memory. The shell mounts and unmounts this every time the user
15
+ * crosses between its own pages and a framed module — which, on a phone, is
16
+ * every other tap on the tab bar — and each of those was a spinner for an
17
+ * answer that never changed. The check below still runs either way; the cache
18
+ * only decides what is painted while it does.
19
+ *
20
+ * Consolidated from seven per-front copies that had drifted into two
21
+ * behaviourally different variants (one polling `checkAuth`, one
22
+ * `isAuthenticated`, with different initial/loading semantics).
23
+ */
24
+ export function ProtectedRoute({ children, authService, loadingMessage = 'Checking authentication...' }) {
25
+ const [isAuthenticated, setIsAuthenticated] = useState(() => authService.getCachedAuth?.()?.authenticated ?? null);
26
+ useEffect(() => {
27
+ let active = true;
28
+ authService
29
+ .isAuthenticated()
30
+ .then((authenticated) => {
31
+ if (active)
32
+ setIsAuthenticated(authenticated);
33
+ })
34
+ .catch((error) => {
35
+ logger.error('Auth check failed', error);
36
+ if (active)
37
+ setIsAuthenticated(false);
38
+ });
39
+ return () => {
40
+ active = false;
41
+ };
42
+ }, [authService]);
43
+ if (isAuthenticated === null) {
44
+ return _jsx(Loader, { message: loadingMessage });
45
+ }
46
+ if (!isAuthenticated) {
47
+ return _jsx(UnauthorizedPage, { onLogin: () => authService.redirectToLogin() });
48
+ }
49
+ return _jsx(_Fragment, { children: children });
50
+ }
@@ -0,0 +1,2 @@
1
+ export { ProtectedRoute } from './ProtectedRoute';
2
+ export type { ProtectedRouteProps, ProtectedRouteAuthService } from './ProtectedRoute';
@@ -0,0 +1 @@
1
+ export { ProtectedRoute } from './ProtectedRoute';
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+ export interface UnauthorizedPageProps {
3
+ onLogin: () => void;
4
+ }
5
+ export declare const UnauthorizedPage: React.FC<UnauthorizedPageProps>;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Typography, Button, Container, Paper } from '@mui/material';
3
+ import { Lock, Login } from '@mui/icons-material';
4
+ export const UnauthorizedPage = ({ onLogin }) => {
5
+ return (_jsx(Container, { maxWidth: "sm", sx: { mt: 8 }, children: _jsxs(Paper, { elevation: 3, sx: { p: 4, textAlign: 'center' }, children: [_jsx(Lock, { sx: { fontSize: 64, color: 'primary.main', mb: 2 } }), _jsx(Typography, { variant: "h4", component: "h1", gutterBottom: true, color: "primary", children: "Access Restricted" }), _jsx(Typography, { variant: "body1", sx: {
6
+ color: 'text.secondary',
7
+ mb: 2
8
+ }, children: "You need to be logged in to access the Tumbaland." }), _jsx(Box, { sx: { mt: 4 }, children: _jsx(Button, { variant: "contained", size: "large", startIcon: _jsx(Login, {}), onClick: onLogin, sx: { px: 4, py: 1.5 }, children: "Sign In to Continue" }) })] }) }));
9
+ };
@@ -0,0 +1,2 @@
1
+ export { UnauthorizedPage } from './UnauthorizedPage';
2
+ export type { UnauthorizedPageProps } from './UnauthorizedPage';
@@ -0,0 +1 @@
1
+ export { UnauthorizedPage } from './UnauthorizedPage';
@@ -0,0 +1,35 @@
1
+ export interface UsageMeterProps {
2
+ /** What is being metered, e.g. "Storage" or "Tracked tickers". */
3
+ label: string;
4
+ used: number;
5
+ /** `-1` (or any negative) means no ceiling. */
6
+ limit: number;
7
+ /** Bytes are humanized; counts are printed as-is. */
8
+ unit?: 'bytes' | 'count';
9
+ /** Shown once the meter is at or past the warning threshold. */
10
+ onUpgrade?: () => void;
11
+ upgradeLabel?: string;
12
+ /** Tighter spacing, for a meter sitting inside a list row rather than a card. */
13
+ dense?: boolean;
14
+ }
15
+ /**
16
+ * Humanizes to the largest unit that keeps the number readable.
17
+ *
18
+ * Deliberately binary (1 KB = 1024 B) to match the limits, which are set in
19
+ * powers of two — `1 * GB` in `DEFAULT_PLAN_LIMITS` is 1073741824, and
20
+ * rendering that as "1.07 GB" against a "1 GB" plan would look like an error.
21
+ */
22
+ export declare function formatBytes(bytes: number): string;
23
+ /**
24
+ * A labelled usage bar: what you have used, out of what your plan allows.
25
+ *
26
+ * The single highest-leverage piece of the paywall — people upgrade when they
27
+ * can see the ceiling coming, not when they hit it. It is also what keeps
28
+ * enforcement from reading as breakage: a 402 with no meter beside it is
29
+ * indistinguishable from a bug.
30
+ *
31
+ * Purely presentational. It takes numbers, never fetches them, so the same
32
+ * component serves the album dashboard, the ticker list, the group member list
33
+ * and the pricing table without any of them agreeing on a data layer.
34
+ */
35
+ export declare function UsageMeter({ label, used, limit, unit, onUpgrade, upgradeLabel, dense }: UsageMeterProps): import("react").JSX.Element;
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Button, LinearProgress, Tooltip, Typography } from '@mui/material';
3
+ import { isUnlimited, usageRatio } from '../../entitlementsService';
4
+ /** Fraction of a meter above which the bar turns amber. */
5
+ const WARN_AT = 0.8;
6
+ /**
7
+ * Humanizes to the largest unit that keeps the number readable.
8
+ *
9
+ * Deliberately binary (1 KB = 1024 B) to match the limits, which are set in
10
+ * powers of two — `1 * GB` in `DEFAULT_PLAN_LIMITS` is 1073741824, and
11
+ * rendering that as "1.07 GB" against a "1 GB" plan would look like an error.
12
+ */
13
+ export function formatBytes(bytes) {
14
+ if (!Number.isFinite(bytes) || bytes <= 0)
15
+ return '0 B';
16
+ const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
17
+ const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
18
+ const value = bytes / Math.pow(1024, exponent);
19
+ // Whole units read better without a trailing ".00"; fractions need a digit.
20
+ const decimals = exponent === 0 || Number.isInteger(value) ? 0 : value < 10 ? 2 : 1;
21
+ return `${value.toFixed(decimals)} ${units[exponent]}`;
22
+ }
23
+ function formatValue(value, unit) {
24
+ return unit === 'bytes' ? formatBytes(value) : value.toLocaleString();
25
+ }
26
+ /**
27
+ * A labelled usage bar: what you have used, out of what your plan allows.
28
+ *
29
+ * The single highest-leverage piece of the paywall — people upgrade when they
30
+ * can see the ceiling coming, not when they hit it. It is also what keeps
31
+ * enforcement from reading as breakage: a 402 with no meter beside it is
32
+ * indistinguishable from a bug.
33
+ *
34
+ * Purely presentational. It takes numbers, never fetches them, so the same
35
+ * component serves the album dashboard, the ticker list, the group member list
36
+ * and the pricing table without any of them agreeing on a data layer.
37
+ */
38
+ export function UsageMeter({ label, used, limit, unit = 'count', onUpgrade, upgradeLabel = 'Upgrade', dense = false }) {
39
+ const unlimited = isUnlimited(limit);
40
+ const ratio = usageRatio(used, limit);
41
+ const atLimit = !unlimited && used >= limit;
42
+ const warning = !unlimited && ratio >= WARN_AT;
43
+ const color = atLimit ? 'error' : warning ? 'warning' : 'primary';
44
+ const summary = unlimited
45
+ ? `${formatValue(used, unit)} used`
46
+ : `${formatValue(used, unit)} of ${formatValue(limit, unit)}`;
47
+ return (_jsxs(Box, { sx: { width: '100%', mb: dense ? 1 : 2 }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 1, mb: 0.5 }, children: [_jsx(Typography, { variant: dense ? 'caption' : 'body2', sx: { color: 'text.secondary', fontWeight: 500 }, children: label }), _jsx(Typography, { variant: dense ? 'caption' : 'body2', sx: { fontWeight: 600, color: atLimit ? 'error.main' : 'text.primary' }, children: summary })] }), unlimited ? (_jsx(Typography, { variant: "caption", sx: { color: 'text.secondary' }, children: "Unlimited on your plan" })) : (_jsx(Tooltip, { title: `${Math.round(ratio * 100)}% of ${label.toLowerCase()} used`, children: _jsx(LinearProgress, { variant: "determinate", value: ratio * 100, color: color, "aria-label": label, "aria-valuenow": used, "aria-valuemin": 0, "aria-valuemax": limit, sx: { height: dense ? 4 : 8, borderRadius: 4 } }) })), onUpgrade && warning && (_jsxs(Box, { sx: { mt: 1, display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Typography, { variant: "caption", sx: { color: atLimit ? 'error.main' : 'warning.main' }, children: atLimit
48
+ ? `You have reached your ${label.toLowerCase()} limit.`
49
+ : `You are close to your ${label.toLowerCase()} limit.` }), _jsx(Button, { size: "small", variant: "text", onClick: onUpgrade, children: upgradeLabel })] }))] }));
50
+ }
@@ -0,0 +1,4 @@
1
+ export { UsageMeter, formatBytes } from './UsageMeter';
2
+ export type { UsageMeterProps } from './UsageMeter';
3
+ export { useEntitlements } from './useEntitlements';
4
+ export type { UseEntitlementsResult } from './useEntitlements';
@@ -0,0 +1,2 @@
1
+ export { UsageMeter, formatBytes } from './UsageMeter';
2
+ export { useEntitlements } from './useEntitlements';
@@ -0,0 +1,27 @@
1
+ import type { Entitlements } from '../../entitlementsService';
2
+ export interface UseEntitlementsResult {
3
+ entitlements: Entitlements | null;
4
+ loading: boolean;
5
+ /** Set when the lookup failed. The UI should hide the meter, not show an error. */
6
+ error: Error | null;
7
+ reload: () => void;
8
+ }
9
+ /**
10
+ * Loads the caller's plan, limits and live usage once per mount.
11
+ *
12
+ * Takes the fetcher rather than building it, because each front resolves
13
+ * `PAYMENT_API_URL` through its own `ConfigProvider` and this library has no
14
+ * access to any of them.
15
+ *
16
+ * **For the five fronts without `@tanstack/react-query`** — account, group,
17
+ * payment, relationship and shell. album-front and finance-front already have
18
+ * react-query and should use `useQuery` instead, which gives them caching and
19
+ * revalidation this deliberately does not try to reimplement.
20
+ *
21
+ * **Failure is not surfaced as an error state by design.** A meter is
22
+ * decoration on top of a page that works without it — if payment-service is
23
+ * unreachable, the album dashboard should still render albums. Callers check
24
+ * `entitlements` for null and omit the meter; nobody gets a red box because a
25
+ * usage bar could not load.
26
+ */
27
+ export declare function useEntitlements(fetcher: () => Promise<Entitlements>): UseEntitlementsResult;
@@ -0,0 +1,56 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ /**
3
+ * Loads the caller's plan, limits and live usage once per mount.
4
+ *
5
+ * Takes the fetcher rather than building it, because each front resolves
6
+ * `PAYMENT_API_URL` through its own `ConfigProvider` and this library has no
7
+ * access to any of them.
8
+ *
9
+ * **For the five fronts without `@tanstack/react-query`** — account, group,
10
+ * payment, relationship and shell. album-front and finance-front already have
11
+ * react-query and should use `useQuery` instead, which gives them caching and
12
+ * revalidation this deliberately does not try to reimplement.
13
+ *
14
+ * **Failure is not surfaced as an error state by design.** A meter is
15
+ * decoration on top of a page that works without it — if payment-service is
16
+ * unreachable, the album dashboard should still render albums. Callers check
17
+ * `entitlements` for null and omit the meter; nobody gets a red box because a
18
+ * usage bar could not load.
19
+ */
20
+ export function useEntitlements(fetcher) {
21
+ const [entitlements, setEntitlements] = useState(null);
22
+ const [loading, setLoading] = useState(true);
23
+ const [error, setError] = useState(null);
24
+ const [reloadToken, setReloadToken] = useState(0);
25
+ const reload = useCallback(() => setReloadToken((token) => token + 1), []);
26
+ useEffect(() => {
27
+ // Guards against a state update after unmount, and against a slow first
28
+ // response overwriting a fresher one after `reload`.
29
+ let active = true;
30
+ setLoading(true);
31
+ fetcher()
32
+ .then((result) => {
33
+ if (!active)
34
+ return;
35
+ setEntitlements(result);
36
+ setError(null);
37
+ })
38
+ .catch((err) => {
39
+ if (!active)
40
+ return;
41
+ setEntitlements(null);
42
+ setError(err);
43
+ })
44
+ .finally(() => {
45
+ if (active)
46
+ setLoading(false);
47
+ });
48
+ return () => {
49
+ active = false;
50
+ };
51
+ // `fetcher` is intentionally not a dependency: callers commonly pass an
52
+ // inline arrow, and depending on it would refetch on every render.
53
+ // eslint-disable-next-line react-hooks/exhaustive-deps
54
+ }, [reloadToken]);
55
+ return { entitlements, loading, error, reload };
56
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ export interface TenantOption {
3
+ id: string;
4
+ name: string;
5
+ type: 'personal' | 'group';
6
+ }
7
+ export declare const TenantProvider: React.FC<{
8
+ children: React.ReactNode;
9
+ }>;
10
+ export declare const useTenantListener: (callback: (tenant: TenantOption) => void) => void;
11
+ export declare const useTenantContext: () => TenantOption | null;
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState, useCallback, createContext, useContext, useEffect } from 'react';
3
+ import { logger } from '../../monitoring';
4
+ const TenantContext = createContext(null);
5
+ // Global listeners for tenant changes
6
+ const tenantChangeListeners = [];
7
+ export const TenantProvider = ({ children }) => {
8
+ const [globalTenant, setGlobalTenant] = useState(null);
9
+ const handleTenantChange = useCallback((tenant) => {
10
+ setGlobalTenant(tenant);
11
+ // Notify all registered listeners
12
+ tenantChangeListeners.forEach((listener) => {
13
+ try {
14
+ listener(tenant);
15
+ }
16
+ catch (error) {
17
+ logger.error('Error in tenant change listener', error);
18
+ }
19
+ });
20
+ }, []);
21
+ const registerTenantChangeListener = useCallback((callback) => {
22
+ tenantChangeListeners.push(callback);
23
+ // Return cleanup function
24
+ return () => {
25
+ const index = tenantChangeListeners.indexOf(callback);
26
+ if (index > -1) {
27
+ tenantChangeListeners.splice(index, 1);
28
+ }
29
+ };
30
+ }, []);
31
+ const contextValue = {
32
+ selectedTenant: globalTenant,
33
+ onTenantChange: handleTenantChange,
34
+ registerTenantChangeListener
35
+ };
36
+ return _jsx(TenantContext.Provider, { value: contextValue, children: children });
37
+ };
38
+ // Hook to listen for tenant changes in components
39
+ export const useTenantListener = (callback) => {
40
+ const context = useContext(TenantContext);
41
+ useEffect(() => {
42
+ if (!context) {
43
+ logger.warn('useTenantListener used outside TenantProvider');
44
+ return;
45
+ }
46
+ return context.registerTenantChangeListener(callback);
47
+ }, [context, callback]);
48
+ };
49
+ // Hook to get current tenant context
50
+ export const useTenantContext = () => {
51
+ const context = useContext(TenantContext);
52
+ return context?.selectedTenant || null;
53
+ };
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import type { SxProps, Theme } from '@mui/material';
3
+ export interface TenantOption {
4
+ id: string;
5
+ name: string;
6
+ type: 'personal' | 'group';
7
+ isSelected?: boolean;
8
+ }
9
+ export interface TenantSelectorProps {
10
+ /** Current selected tenant */
11
+ selectedTenant: TenantOption | null;
12
+ /** Available tenant options */
13
+ tenants: TenantOption[];
14
+ /** Callback when tenant is selected */
15
+ onTenantChange: (tenant: TenantOption) => void;
16
+ /** Loading state for fetching tenants */
17
+ loading?: boolean;
18
+ /** Error state */
19
+ error?: string | null;
20
+ /** Custom styling */
21
+ sx?: SxProps<Theme>;
22
+ }
23
+ declare const TenantSelector: React.FC<TenantSelectorProps>;
24
+ export default TenantSelector;
@@ -0,0 +1,77 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Button, Menu, MenuItem, ListItemIcon, ListItemText, Divider, Typography, Chip } from '@mui/material';
4
+ import { PersonOutlined, Groups, ExpandMore, Check } from '@mui/icons-material';
5
+ const TenantSelector = ({ selectedTenant, tenants = [], onTenantChange, loading = false, error = null, sx = {} }) => {
6
+ const [anchorEl, setAnchorEl] = useState(null);
7
+ const isOpen = Boolean(anchorEl);
8
+ const handleClick = (event) => {
9
+ setAnchorEl(event.currentTarget);
10
+ };
11
+ const handleClose = () => {
12
+ setAnchorEl(null);
13
+ };
14
+ const handleTenantSelect = (tenant) => {
15
+ onTenantChange(tenant);
16
+ handleClose();
17
+ };
18
+ const getDisplayName = (tenant) => {
19
+ if (!tenant)
20
+ return 'Select Context';
21
+ return tenant.type === 'personal' ? 'Personal' : tenant.name;
22
+ };
23
+ const getIcon = (type) => {
24
+ return type === 'personal' ? _jsx(PersonOutlined, { fontSize: "small" }) : _jsx(Groups, { fontSize: "small" });
25
+ };
26
+ if (error) {
27
+ return _jsx(Chip, { label: "Error loading contexts", color: "error", size: "small", sx: { ...sx } });
28
+ }
29
+ return (_jsxs(Box, { sx: { ...sx }, children: [_jsx(Button, { onClick: handleClick, disabled: loading || tenants.length === 0, endIcon: _jsx(ExpandMore, {}), startIcon: selectedTenant ? getIcon(selectedTenant.type) : _jsx(PersonOutlined, { fontSize: "small" }), sx: {
30
+ textTransform: 'none',
31
+ fontWeight: 500,
32
+ borderRadius: 2,
33
+ px: 2,
34
+ py: 0.5,
35
+ minWidth: 140,
36
+ justifyContent: 'flex-start',
37
+ color: 'text.primary',
38
+ border: '1px solid',
39
+ borderColor: 'divider',
40
+ '&:hover': {
41
+ borderColor: 'primary.main',
42
+ backgroundColor: 'action.hover'
43
+ }
44
+ }, children: _jsxs(Box, { sx: { flexGrow: 1, textAlign: 'left', overflow: 'hidden' }, children: [_jsx(Typography, { variant: "caption", sx: {
45
+ color: 'text.secondary',
46
+ display: 'block',
47
+ fontSize: 10
48
+ }, children: "Context" }), _jsx(Typography, { variant: "body2", sx: {
49
+ display: 'block',
50
+ whiteSpace: 'nowrap',
51
+ overflow: 'hidden',
52
+ textOverflow: 'ellipsis',
53
+ maxWidth: 100
54
+ }, children: loading ? 'Loading...' : getDisplayName(selectedTenant) })] }) }), _jsxs(Menu, { anchorEl: anchorEl, open: isOpen, onClose: handleClose, anchorOrigin: {
55
+ vertical: 'bottom',
56
+ horizontal: 'left'
57
+ }, transformOrigin: {
58
+ vertical: 'top',
59
+ horizontal: 'left'
60
+ }, children: [_jsx(MenuItem, { disabled: true, children: _jsx(Typography, { variant: "caption", sx: {
61
+ color: 'text.secondary',
62
+ fontWeight: 600
63
+ }, children: "SELECT CONTEXT" }) }), _jsx(Divider, {}), tenants.map((tenant) => {
64
+ const isSelected = selectedTenant?.id === tenant.id;
65
+ return (_jsxs(MenuItem, { onClick: () => handleTenantSelect(tenant), selected: isSelected, sx: {
66
+ py: 1.5,
67
+ '&.Mui-selected': {
68
+ backgroundColor: 'primary.50'
69
+ }
70
+ }, children: [_jsx(ListItemIcon, { children: getIcon(tenant.type) }), _jsx(ListItemText, { primary: _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsx(Typography, { variant: "body2", sx: { fontWeight: 500 }, children: tenant.type === 'personal' ? 'Personal' : tenant.name }), isSelected && _jsx(Check, { fontSize: "small", color: "primary" })] }), secondary: _jsx(Typography, { variant: "caption", sx: {
71
+ color: 'text.secondary'
72
+ }, children: tenant.type === 'personal' ? 'Your personal data' : 'Group workspace' }) })] }, tenant.id));
73
+ }), tenants.length === 0 && !loading && (_jsx(MenuItem, { disabled: true, children: _jsx(ListItemText, { primary: _jsx(Typography, { variant: "body2", sx: {
74
+ color: 'text.secondary'
75
+ }, children: "No contexts available" }) }) }))] })] }));
76
+ };
77
+ export default TenantSelector;
@@ -0,0 +1,5 @@
1
+ export { default as TenantSelector } from './TenantSelector';
2
+ export type { TenantOption, TenantSelectorProps } from './TenantSelector';
3
+ export { useTenant, TenantProvider, useTenantListener, useTenantContext } from './useTenant';
4
+ export type { UseTenantResult } from './useTenant';
5
+ export { useTenantFilter } from './useTenantDataRefresh';
@@ -0,0 +1,3 @@
1
+ export { default as TenantSelector } from './TenantSelector';
2
+ export { useTenant, TenantProvider, useTenantListener, useTenantContext } from './useTenant';
3
+ export { useTenantFilter } from './useTenantDataRefresh';
@@ -0,0 +1,15 @@
1
+ import { TenantOption, TenantProvider, useTenantListener, useTenantContext } from './TenantProvider';
2
+ export { TenantProvider, useTenantListener, useTenantContext, type TenantOption };
3
+ export interface UseTenantResult {
4
+ selectedTenant: TenantOption | null;
5
+ availableTenants: TenantOption[];
6
+ loading: boolean;
7
+ error: string | null;
8
+ setSelectedTenant: (tenant: TenantOption) => void;
9
+ refreshTenants: () => Promise<void>;
10
+ }
11
+ /**
12
+ * Custom hook for managing tenant selection
13
+ * Handles both personal account and group contexts
14
+ */
15
+ export declare const useTenant: (groupApiUrl: string, userEmail?: string) => UseTenantResult;