@xeplr/ui-account 1.0.0 → 1.0.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/src/pages.jsx CHANGED
@@ -1,3 +1,4 @@
1
+ import { memo } from 'react';
1
2
  import { useLoginController } from './useLoginController.js';
2
3
  import { useRegisterController } from './useRegisterController.js';
3
4
  import { useForgotPasswordController } from './useForgotPasswordController.js';
@@ -8,85 +9,131 @@ import { useProfileController } from './useProfileController.js';
8
9
  import { useUserRolesController } from './useUserRolesController.js';
9
10
  import { useAccessMatrixController } from './useAccessMatrixController.js';
10
11
  import { useMasterSettingsController } from './useMasterSettingsController.js';
11
- import { useTenantController } from './useTenantController.js';
12
- import { useTenantPickerController } from './useTenantPickerController.js';
13
- import { LoginSample, RegisterSample, ForgotPasswordSample, ResetPasswordSample, ActivateSample, NotActivatedSample, ChangePasswordSample, ProfileSample, UserRolesMatrixSample, AccessMatrixSample, MasterSettingsSample, TenantSample, TenantPickerSample } from './designs/index.js';
14
- import { useDesignValidator, LOGIN_RULES, REGISTER_RULES, FORGOT_PASSWORD_RULES, RESET_PASSWORD_RULES, CHANGE_PASSWORD_RULES, PROFILE_RULES, USER_ROLES_MATRIX_RULES, ACCESS_MATRIX_RULES, MASTER_SETTINGS_RULES } from './validateDesign.js';
12
+ import { useNavController } from './useNavController.js';
13
+ import { LoginSample, RegisterSample, ForgotPasswordSample, ResetPasswordSample, ActivateSample, NotActivatedSample, ChangePasswordSample, ProfileSample, UserRolesMatrixSample, AccessMatrixSample, MasterSettingsSample, NavTopSample, NavDrawer } from './designs/index.js';
14
+ import { useDesignValidator, LOGIN_RULES, REGISTER_RULES, FORGOT_PASSWORD_RULES, RESET_PASSWORD_RULES, CHANGE_PASSWORD_RULES, PROFILE_RULES, USER_ROLES_MATRIX_RULES, ACCESS_MATRIX_RULES, MASTER_SETTINGS_RULES, NAV_RULES } from './validateDesign.js';
15
15
 
16
16
  /**
17
- * Ready-made pages using sample designs.
18
- * Use these for quick setup, or build your own with the controllers + your own designs.
17
+ * Ready-made pages: controller (the logic) + sample design (the look), wired together.
18
+ *
19
+ * Every page takes an optional `design` prop — pass your OWN design component to
20
+ * re-skin the page while keeping the framework's controller + validation. The design
21
+ * receives the controller's output as props (the same contract the sample honours), e.g.
22
+ * LoginSample gets { email, setEmail, password, setPassword, error, loading, handleSubmit }.
23
+ *
24
+ * <LoginPage design={MyLogin} onSuccess={...} /> // my look, framework logic
25
+ * <LoginPage onSuccess={...} /> // framework look
26
+ *
27
+ * Any other props (onSuccess, etc.) flow through to the controller.
19
28
  */
20
29
 
21
- export function LoginPage(props) {
30
+ export function LoginPage({ design, ...props }) {
22
31
  var controller = useLoginController(props);
23
32
  var ref = useDesignValidator('LoginPage', LOGIN_RULES);
24
- return <div ref={ref}><LoginSample {...controller} /></div>;
33
+ var View = design || LoginSample;
34
+ return <div ref={ref}><View {...controller} /></div>;
25
35
  }
26
36
 
27
- export function RegisterPage(props) {
37
+ export function RegisterPage({ design, ...props }) {
28
38
  var controller = useRegisterController(props);
29
39
  var ref = useDesignValidator('RegisterPage', REGISTER_RULES);
30
- return <div ref={ref}><RegisterSample {...controller} /></div>;
40
+ var View = design || RegisterSample;
41
+ return <div ref={ref}><View {...controller} /></div>;
31
42
  }
32
43
 
33
- export function ForgotPasswordPage() {
34
- var controller = useForgotPasswordController();
44
+ export function ForgotPasswordPage({ design, ...props }) {
45
+ var controller = useForgotPasswordController(props);
35
46
  var ref = useDesignValidator('ForgotPasswordPage', FORGOT_PASSWORD_RULES);
36
- return <div ref={ref}><ForgotPasswordSample {...controller} /></div>;
47
+ var View = design || ForgotPasswordSample;
48
+ return <div ref={ref}><View {...controller} /></div>;
37
49
  }
38
50
 
39
- export function ResetPasswordPage() {
40
- var controller = useResetPasswordController();
51
+ export function ResetPasswordPage({ design, ...props }) {
52
+ var controller = useResetPasswordController(props);
41
53
  var ref = useDesignValidator('ResetPasswordPage', RESET_PASSWORD_RULES);
42
- return <div ref={ref}><ResetPasswordSample {...controller} /></div>;
54
+ var View = design || ResetPasswordSample;
55
+ return <div ref={ref}><View {...controller} /></div>;
43
56
  }
44
57
 
45
- export function ActivatePage() {
46
- var controller = useActivateController();
47
- return <ActivateSample {...controller} />;
58
+ export function ActivatePage({ design, ...props }) {
59
+ var controller = useActivateController(props);
60
+ var View = design || ActivateSample;
61
+ return <View {...controller} />;
48
62
  }
49
63
 
50
- export function NotActivatedPage() {
51
- return <NotActivatedSample />;
64
+ export function NotActivatedPage({ design }) {
65
+ var View = design || NotActivatedSample;
66
+ return <View />;
52
67
  }
53
68
 
54
- export function ChangePasswordPage(props) {
69
+ export function ChangePasswordPage({ design, ...props }) {
55
70
  var controller = useChangePasswordController(props);
56
71
  var ref = useDesignValidator('ChangePasswordPage', CHANGE_PASSWORD_RULES);
57
- return <div ref={ref}><ChangePasswordSample {...controller} /></div>;
72
+ var View = design || ChangePasswordSample;
73
+ return <div ref={ref}><View {...controller} /></div>;
58
74
  }
59
75
 
60
- export function ProfilePage(props) {
76
+ export function ProfilePage({ design, ...props }) {
61
77
  var controller = useProfileController(props);
62
78
  var ref = useDesignValidator('ProfilePage', PROFILE_RULES);
63
- return <div ref={ref}><ProfileSample {...controller} /></div>;
79
+ var View = design || ProfileSample;
80
+ return <div ref={ref}><View {...controller} /></div>;
64
81
  }
65
82
 
66
- export function UserRolesPage(props) {
83
+ export function UserRolesPage({ design, ...props }) {
67
84
  var controller = useUserRolesController(props);
68
85
  var ref = useDesignValidator('UserRolesPage', USER_ROLES_MATRIX_RULES);
69
- return <div ref={ref}><UserRolesMatrixSample {...controller} /></div>;
86
+ var View = design || UserRolesMatrixSample;
87
+ return <div ref={ref}><View {...controller} /></div>;
70
88
  }
71
89
 
72
- export function AccessMatrixPage(props) {
90
+ export function AccessMatrixPage({ design, ...props }) {
73
91
  var controller = useAccessMatrixController(props);
74
92
  var ref = useDesignValidator('AccessMatrixPage', ACCESS_MATRIX_RULES);
75
- return <div ref={ref}><AccessMatrixSample {...controller} /></div>;
93
+ var View = design || AccessMatrixSample;
94
+ return <div ref={ref}><View {...controller} /></div>;
76
95
  }
77
96
 
78
- export function MasterSettingsPage(props) {
97
+ export function MasterSettingsPage({ design, ...props }) {
79
98
  var controller = useMasterSettingsController(props);
80
99
  var ref = useDesignValidator('MasterSettingsPage', MASTER_SETTINGS_RULES);
81
- return <div ref={ref}><MasterSettingsSample {...controller} /></div>;
100
+ var View = design || MasterSettingsSample;
101
+ return <div ref={ref}><View {...controller} /></div>;
82
102
  }
83
103
 
84
- export function TenantPage(props) {
85
- var controller = useTenantController(props);
86
- return <TenantSample {...controller} />;
104
+ // NavPage is memoized — unlike the other pages, it lives in your app's layout
105
+ // route alongside <Outlet/>, so it must not re-render when the routed page
106
+ // changes. Exactly ONE of two things renders, never both: if `drawerItems` is
107
+ // non-empty, the drawer rail (NavDrawer) IS the nav — it hosts settings,
108
+ // notifications, and everything else that would otherwise be in the top bar.
109
+ // Otherwise the top bar (`design`, defaults to NavTopSample) renders alone.
110
+ // `navMiddle` has no equivalent in the drawer (there's no middle slot in a
111
+ // vertical rail) — it's simply unused whenever a drawer is present.
112
+ // See useNavController.js for the drawerItems/settingsOverrides/notifications
113
+ // props this accepts.
114
+ function NavPageImpl({ design, logo, expandedLogo, navMiddle, drawerPromo, ...props }) {
115
+ var controller = useNavController(props);
116
+ var ref = useDesignValidator('NavPage', NAV_RULES);
117
+ var TopBar = design || NavTopSample;
118
+ var hasDrawer = controller.drawerItems.length > 0;
119
+ return (
120
+ <div ref={ref}>
121
+ {hasDrawer ? (
122
+ <NavDrawer
123
+ {...controller}
124
+ logo={logo}
125
+ expandedLogo={expandedLogo}
126
+ drawerPromo={drawerPromo}
127
+ />
128
+ ) : (
129
+ <TopBar
130
+ {...controller}
131
+ logo={logo}
132
+ navMiddle={navMiddle}
133
+ />
134
+ )}
135
+ </div>
136
+ );
87
137
  }
88
138
 
89
- export function TenantPickerPage(props) {
90
- var controller = useTenantPickerController(props);
91
- return <TenantPickerSample {...controller} />;
92
- }
139
+ export var NavPage = memo(NavPageImpl);
@@ -1,5 +1,6 @@
1
1
  import { useState, useEffect, useRef } from 'react';
2
2
  import { useSearchParams } from 'react-router-dom';
3
+ import { raiseSnackbar } from '@xeplr/ui-utils';
3
4
  import { activateAccount } from './api.js';
4
5
 
5
6
  export function useActivateController() {
@@ -11,17 +12,27 @@ export function useActivateController() {
11
12
  var called = useRef(false);
12
13
 
13
14
  useEffect(function() {
14
- if (!token || called.current) return;
15
+ if (!token) {
16
+ if (!called.current) {
17
+ called.current = true;
18
+ raiseSnackbar('Invalid activation link', { design: 'error' });
19
+ }
20
+ return;
21
+ }
22
+ if (called.current) return;
15
23
  called.current = true;
16
24
  setLoading(true);
17
25
  setError('');
18
26
  setSuccess('');
19
27
  activateAccount(token)
20
28
  .then(function(result) {
21
- setSuccess(result.message || 'Account activated successfully');
29
+ var message = result.message || 'Account activated successfully';
30
+ setSuccess(message);
31
+ raiseSnackbar(message, { design: 'success' });
22
32
  })
23
33
  .catch(function(err) {
24
34
  setError(err.message);
35
+ raiseSnackbar(err.message, { design: 'error' });
25
36
  })
26
37
  .finally(function() {
27
38
  setLoading(false);
@@ -1,4 +1,5 @@
1
1
  import { useState } from 'react';
2
+ import { raiseSnackbar } from '@xeplr/ui-utils';
2
3
  import { changePassword } from './api.js';
3
4
 
4
5
  export function useChangePasswordController(options = {}) {
@@ -17,20 +18,25 @@ export function useChangePasswordController(options = {}) {
17
18
  setSuccess('');
18
19
 
19
20
  if (newPassword !== confirmPassword) {
20
- setError('Passwords do not match');
21
+ var mismatchMessage = 'Passwords do not match';
22
+ setError(mismatchMessage);
23
+ raiseSnackbar(mismatchMessage, { design: 'error' });
21
24
  return;
22
25
  }
23
26
 
24
27
  setLoading(true);
25
28
  try {
26
29
  var result = await changePassword({ currentPassword, newPassword });
27
- setSuccess('Password changed successfully');
30
+ var message = 'Password changed successfully';
31
+ setSuccess(message);
32
+ raiseSnackbar(message, { design: 'success' });
28
33
  setCurrentPassword('');
29
34
  setNewPassword('');
30
35
  setConfirmPassword('');
31
36
  if (onSuccess) onSuccess(result);
32
37
  } catch (err) {
33
38
  setError(err.message);
39
+ raiseSnackbar(err.message, { design: 'error' });
34
40
  } finally {
35
41
  setLoading(false);
36
42
  }
@@ -1,4 +1,5 @@
1
1
  import { useState } from 'react';
2
+ import { raiseSnackbar } from '@xeplr/ui-utils';
2
3
  import { forgotPassword } from './api.js';
3
4
 
4
5
  export function useForgotPasswordController() {
@@ -15,8 +16,10 @@ export function useForgotPasswordController() {
15
16
  try {
16
17
  const result = await forgotPassword({ email });
17
18
  setSuccess(result.message);
19
+ raiseSnackbar(result.message, { design: 'success' });
18
20
  } catch (err) {
19
21
  setError(err.message);
22
+ raiseSnackbar(err.message, { design: 'error' });
20
23
  } finally {
21
24
  setLoading(false);
22
25
  }
@@ -1,5 +1,6 @@
1
1
  import { useState } from 'react';
2
2
  import { useNavigate } from 'react-router-dom';
3
+ import { raiseSnackbar } from '@xeplr/ui-utils';
3
4
  import { loginUser } from './api.js';
4
5
  import { setToken, setRefreshToken } from './token.js';
5
6
  import { useAccess } from './AccessContext.jsx';
@@ -37,6 +38,7 @@ export function useLoginController(options = {}) {
37
38
  navigate(notActivatedPath);
38
39
  } else {
39
40
  setError(err.message);
41
+ raiseSnackbar(err.message, { design: 'error' });
40
42
  }
41
43
  } finally {
42
44
  setLoading(false);
@@ -0,0 +1,102 @@
1
+ import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
2
+ import { useAccessStrict } from './AccessContext.jsx';
3
+ import { authPath } from './authRoutes.jsx';
4
+
5
+ // The built-in settings-dropdown items — seeded via auth's migrations (menus
6
+ // table, see migrations/0006_seed_catalog.sql), not hardcoded here. Maps a
7
+ // seeded menu NAME to the authRoutes key it links to. "Admin" is deliberately
8
+ // NOT here — the framework's builtin section is Profile/Change Password only;
9
+ // an app that wants an Admin link adds it to its own settingsOverrides. Only
10
+ // names present in access.menus (role-filtered, computed at login) render.
11
+ var BUILTIN_SETTINGS_ITEMS = [
12
+ { menuName: 'Profile', authKey: 'profile' },
13
+ { menuName: 'Change Password', authKey: 'changePassword' }
14
+ ];
15
+ var NOTIFICATIONS_MENU_NAME = 'Notifications';
16
+
17
+ /**
18
+ * Nav is self-contained by design: it reads access/user from context (which only
19
+ * changes on login/logout/access updates) and owns its OWN open/closed UI state.
20
+ * It never depends on — and never forces a re-render of — whatever page is
21
+ * currently mounted in the app's <Outlet/>. Keep it that way when extending:
22
+ * don't lift this state into the app's layout component.
23
+ *
24
+ * @param {object} props
25
+ * @param {Array<{name:string, icon?:string, clickHandler?:Function, group?:string}>} [props.drawerItems] —
26
+ * the app's own drawer catalog (each `name` must match a `menus.name` row in
27
+ * the auth DB). Passing a non-empty array auto-activates the drawer rail —
28
+ * there's no separate flag. `group` is optional; ungrouped items render
29
+ * first, grouped ones under their section header (see NavDrawer.jsx).
30
+ * Role-filtered via access.menus, same mechanism as the settings items.
31
+ * @param {Array<{name:string, path:string}>} [props.settingsOverrides] — the app's
32
+ * OWN items rendered ABOVE the builtin (Profile/Change Password) section in
33
+ * the settings dropdown — this is where an "Admin" link belongs now. Each
34
+ * `name` must match a `menus.name` row you've seeded (in YOUR app's
35
+ * migrations-auth, not auth's own) — role-filtered via access.menus.
36
+ * @param {{count?:number, onClick?:Function}} [props.notifications] — the app's feed
37
+ * config. Only takes effect if the user's role grants the "Notifications" menu
38
+ * (seeded basic-tier, visible to everyone by default) — otherwise the bell is
39
+ * hidden regardless of what's passed here.
40
+ */
41
+ export function useNavController(props) {
42
+ props = props || {};
43
+ var accessCtx = useAccessStrict();
44
+ var access = accessCtx.access || {};
45
+ var allowedMenus = access.menus || [];
46
+
47
+ var [accountOpen, setAccountOpen] = useState(false);
48
+ var [drawerOpen, setDrawerOpen] = useState(false);
49
+
50
+ var accountRef = useRef(null);
51
+
52
+ var toggleAccount = useCallback(function() { setAccountOpen(function(v) { return !v; }); }, []);
53
+ var closeAccount = useCallback(function() { setAccountOpen(false); }, []);
54
+ var toggleDrawer = useCallback(function() { setDrawerOpen(function(v) { return !v; }); }, []);
55
+ var closeDrawer = useCallback(function() { setDrawerOpen(false); }, []);
56
+
57
+ // Click-outside for the settings menu only — the drawer closes via its own overlay.
58
+ useEffect(function() {
59
+ if (!accountOpen) return;
60
+ function onDocClick(e) {
61
+ if (accountRef.current && !accountRef.current.contains(e.target)) closeAccount();
62
+ }
63
+ document.addEventListener('mousedown', onDocClick);
64
+ return function() { document.removeEventListener('mousedown', onDocClick); };
65
+ }, [accountOpen, closeAccount]);
66
+
67
+ // Settings dropdown, in render order: app overrides first, then the fixed
68
+ // builtin section — role-filtered the same way for both.
69
+ var accountItems = useMemo(function() {
70
+ var overrides = (props.settingsOverrides || [])
71
+ .filter(function(item) { return allowedMenus.indexOf(item.name) !== -1; });
72
+ var builtin = BUILTIN_SETTINGS_ITEMS
73
+ .filter(function(item) { return allowedMenus.indexOf(item.menuName) !== -1; })
74
+ .map(function(item) { return { name: item.menuName, path: authPath(item.authKey) }; });
75
+ return overrides.concat(builtin);
76
+ }, [allowedMenus, props.settingsOverrides]);
77
+
78
+ var notificationsAllowed = allowedMenus.indexOf(NOTIFICATIONS_MENU_NAME) !== -1;
79
+
80
+ // Role-filter the app's own drawer catalog down to what this user can actually see.
81
+ var drawerItems = useMemo(function() {
82
+ var catalog = props.drawerItems || [];
83
+ return catalog.filter(function(m) { return allowedMenus.indexOf(m.name) !== -1; });
84
+ }, [props.drawerItems, allowedMenus]);
85
+
86
+ return {
87
+ user: accessCtx.user,
88
+ accountItems: accountItems,
89
+ notifications: notificationsAllowed ? (props.notifications || null) : null,
90
+ logout: accessCtx.logout,
91
+
92
+ accountOpen: accountOpen,
93
+ toggleAccount: toggleAccount,
94
+ closeAccount: closeAccount,
95
+ accountRef: accountRef,
96
+
97
+ drawerOpen: drawerOpen,
98
+ toggleDrawer: toggleDrawer,
99
+ closeDrawer: closeDrawer,
100
+ drawerItems: drawerItems
101
+ };
102
+ }
@@ -1,4 +1,5 @@
1
1
  import { useState, useEffect } from 'react';
2
+ import { raiseSnackbar } from '@xeplr/ui-utils';
2
3
  import { getProfile, updateProfile } from './api.js';
3
4
 
4
5
  export function useProfileController(options = {}) {
@@ -25,6 +26,7 @@ export function useProfileController(options = {}) {
25
26
  });
26
27
  } catch (err) {
27
28
  setError(err.message);
29
+ raiseSnackbar(err.message, { design: 'error' });
28
30
  } finally {
29
31
  setFetching(false);
30
32
  }
@@ -41,10 +43,13 @@ export function useProfileController(options = {}) {
41
43
  setLoading(true);
42
44
  try {
43
45
  var result = await updateProfile(form);
44
- setSuccess('Profile updated successfully');
46
+ var message = 'Profile updated successfully';
47
+ setSuccess(message);
48
+ raiseSnackbar(message, { design: 'success' });
45
49
  if (onSuccess) onSuccess(result);
46
50
  } catch (err) {
47
51
  setError(err.message);
52
+ raiseSnackbar(err.message, { design: 'error' });
48
53
  } finally {
49
54
  setLoading(false);
50
55
  }
@@ -1,4 +1,5 @@
1
1
  import { useState } from 'react';
2
+ import { raiseSnackbar } from '@xeplr/ui-utils';
2
3
  import { registerUser } from './api.js';
3
4
 
4
5
  export function useRegisterController(options = {}) {
@@ -20,11 +21,14 @@ export function useRegisterController(options = {}) {
20
21
  setLoading(true);
21
22
  try {
22
23
  const result = await registerUser(form);
23
- setSuccess('Registration successful. Please wait, someone will activate you.');
24
+ const message = 'Registration successful. Please wait, someone will activate you.';
25
+ setSuccess(message);
26
+ raiseSnackbar(message, { design: 'success' });
24
27
  setForm({ name: '', email: '', phoneNumber: '', password: '' });
25
28
  if (onSuccess) onSuccess(result);
26
29
  } catch (err) {
27
30
  setError(err.message);
31
+ raiseSnackbar(err.message, { design: 'error' });
28
32
  } finally {
29
33
  setLoading(false);
30
34
  }
@@ -1,5 +1,6 @@
1
- import { useState } from 'react';
1
+ import { useState, useEffect, useRef } from 'react';
2
2
  import { useSearchParams } from 'react-router-dom';
3
+ import { raiseSnackbar } from '@xeplr/ui-utils';
3
4
  import { resetPassword } from './api.js';
4
5
 
5
6
  export function useResetPasswordController() {
@@ -9,6 +10,14 @@ export function useResetPasswordController() {
9
10
  const [error, setError] = useState('');
10
11
  const [success, setSuccess] = useState('');
11
12
  const [loading, setLoading] = useState(false);
13
+ const warnedNoToken = useRef(false);
14
+
15
+ useEffect(function() {
16
+ if (!token && !warnedNoToken.current) {
17
+ warnedNoToken.current = true;
18
+ raiseSnackbar('Invalid or expired reset link', { design: 'error' });
19
+ }
20
+ }, [token]);
12
21
 
13
22
  async function handleSubmit(e) {
14
23
  e.preventDefault();
@@ -18,8 +27,10 @@ export function useResetPasswordController() {
18
27
  try {
19
28
  const result = await resetPassword({ token, newPassword: password });
20
29
  setSuccess(result.message);
30
+ raiseSnackbar(result.message, { design: 'success' });
21
31
  } catch (err) {
22
32
  setError(err.message);
33
+ raiseSnackbar(err.message, { design: 'error' });
23
34
  } finally {
24
35
  setLoading(false);
25
36
  }
@@ -99,3 +99,9 @@ export var MASTER_SETTINGS_RULES = [
99
99
  { id: 'xeplr-admin-master-search', label: 'Master search input' },
100
100
  { role: 'tablist', label: 'Tab list for master types' }
101
101
  ];
102
+
103
+ // Only the structure common to every Nav design (both Design 1 and Design 2
104
+ // have an account menu; only Design 2 has a drawer, so that isn't required here).
105
+ export var NAV_RULES = [
106
+ { selector: '[aria-haspopup="menu"]', label: 'Account menu trigger' }
107
+ ];
@@ -1,39 +0,0 @@
1
- import './auth.css';
2
-
3
- export default function TenantPickerSample({ tenants, loading, error, label, handleSelect, onLogout }) {
4
- return (
5
- <div className="xeplr-auth-container">
6
- <h1>Select {label}</h1>
7
- {error && <div className="xeplr-auth-alert xeplr-auth-alert-error">{error}</div>}
8
- {loading ? (
9
- <p>Loading...</p>
10
- ) : tenants.length === 0 ? (
11
- <div className="xeplr-auth-alert xeplr-auth-alert-error">
12
- No {label.toLowerCase()}s available. Contact your administrator.
13
- </div>
14
- ) : (
15
- <div className="xeplr-tenant-picker">
16
- {tenants.map(function(t) {
17
- return (
18
- <button
19
- key={t.id}
20
- type="button"
21
- className="xeplr-tenant-option"
22
- onClick={function() { handleSelect(t); }}
23
- >
24
- <span className="xeplr-tenant-name">{t.name}</span>
25
- {t.code && <span className="xeplr-tenant-code">{t.code}</span>}
26
- {t.description && <span className="xeplr-tenant-desc">{t.description}</span>}
27
- </button>
28
- );
29
- })}
30
- </div>
31
- )}
32
- {onLogout && (
33
- <div className="xeplr-auth-links">
34
- <p><button type="button" onClick={onLogout} style={{ background: 'none', border: 'none', color: 'var(--xeplr-text-muted, #777)', cursor: 'pointer', fontSize: '14px' }}>Sign out</button></p>
35
- </div>
36
- )}
37
- </div>
38
- );
39
- }