@tidecloak/ui-framework 0.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/README.md +377 -0
- package/dist/index.d.mts +2739 -0
- package/dist/index.d.ts +2739 -0
- package/dist/index.js +12869 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +12703 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -0
- package/src/components/common/ActionButton.tsx +234 -0
- package/src/components/common/EmptyState.tsx +140 -0
- package/src/components/common/LoadingSkeleton.tsx +121 -0
- package/src/components/common/RefreshButton.tsx +127 -0
- package/src/components/common/StatusBadge.tsx +177 -0
- package/src/components/common/index.ts +31 -0
- package/src/components/data-table/DataTable.tsx +201 -0
- package/src/components/data-table/PaginatedTable.tsx +247 -0
- package/src/components/data-table/index.ts +2 -0
- package/src/components/dialogs/CollapsibleSection.tsx +184 -0
- package/src/components/dialogs/ConfirmDialog.tsx +264 -0
- package/src/components/dialogs/DetailDialog.tsx +228 -0
- package/src/components/dialogs/index.ts +3 -0
- package/src/components/index.ts +5 -0
- package/src/components/pages/base/ApprovalsPageBase.tsx +680 -0
- package/src/components/pages/base/LogsPageBase.tsx +581 -0
- package/src/components/pages/base/RolesPageBase.tsx +1470 -0
- package/src/components/pages/base/TemplatesPageBase.tsx +761 -0
- package/src/components/pages/base/UsersPageBase.tsx +843 -0
- package/src/components/pages/base/index.ts +58 -0
- package/src/components/pages/connected/ApprovalsPage.tsx +797 -0
- package/src/components/pages/connected/LogsPage.tsx +267 -0
- package/src/components/pages/connected/RolesPage.tsx +525 -0
- package/src/components/pages/connected/TemplatesPage.tsx +181 -0
- package/src/components/pages/connected/UsersPage.tsx +237 -0
- package/src/components/pages/connected/index.ts +36 -0
- package/src/components/pages/index.ts +5 -0
- package/src/components/tabs/TabsView.tsx +300 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/ui/index.tsx +1001 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAutoRefresh.ts +119 -0
- package/src/hooks/usePagination.ts +152 -0
- package/src/hooks/useSelection.ts +81 -0
- package/src/index.ts +256 -0
- package/src/theme.ts +185 -0
- package/src/tide/index.ts +19 -0
- package/src/tide/tidePolicy.ts +270 -0
- package/src/types/index.ts +484 -0
- package/src/utils/index.ts +121 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UsersPage - Pre-made user management page
|
|
3
|
+
*
|
|
4
|
+
* Uses AdminAPI from @tidecloak/js for data fetching.
|
|
5
|
+
* Just drop it in and it works.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { AdminAPI as DefaultAdminAPI } from "@tidecloak/js";
|
|
9
|
+
import { UsersPageBase, type UsersPageBaseProps, type UserItem } from "../base";
|
|
10
|
+
import type { AccessMetadataAPI, AccessMetadataRecord } from "./ApprovalsPage";
|
|
11
|
+
|
|
12
|
+
// Type for the AdminAPI instance
|
|
13
|
+
type AdminAPIInstance = {
|
|
14
|
+
setRealm?: (realm: string) => void;
|
|
15
|
+
getUsers: () => Promise<any>;
|
|
16
|
+
getRoles: () => Promise<any>;
|
|
17
|
+
getRole: (roleName: string) => Promise<any>;
|
|
18
|
+
createUser: (user: any) => Promise<any>;
|
|
19
|
+
updateUser: (userId: string, user: any) => Promise<any>;
|
|
20
|
+
deleteUser: (userId: string) => Promise<any>;
|
|
21
|
+
addUserRoles?: (userId: string, roles: any[]) => Promise<any>;
|
|
22
|
+
removeUserRoles?: (userId: string, roles: any[]) => Promise<any>;
|
|
23
|
+
setUserEnabled?: (userId: string, enabled: boolean) => Promise<any>;
|
|
24
|
+
getTideLinkUrl?: (userId: string, redirectUri: string, lifespan?: number) => Promise<string>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface ChangeSetInfo {
|
|
28
|
+
changeSetId: string;
|
|
29
|
+
changeSetType: string;
|
|
30
|
+
actionType: string;
|
|
31
|
+
username: string;
|
|
32
|
+
userId: string;
|
|
33
|
+
roles: string[];
|
|
34
|
+
timestamp: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface UsersPageProps<T extends UserItem = UserItem>
|
|
38
|
+
extends Omit<
|
|
39
|
+
UsersPageBaseProps<T>,
|
|
40
|
+
| "fetchUsers"
|
|
41
|
+
| "fetchRoles"
|
|
42
|
+
| "onCreate"
|
|
43
|
+
| "onUpdateProfile"
|
|
44
|
+
| "onUpdateRoles"
|
|
45
|
+
| "onDelete"
|
|
46
|
+
| "onSetEnabled"
|
|
47
|
+
| "getTideLinkUrl"
|
|
48
|
+
> {
|
|
49
|
+
/** AdminAPI instance */
|
|
50
|
+
adminAPI?: AdminAPIInstance;
|
|
51
|
+
/** Realm name */
|
|
52
|
+
realm?: string;
|
|
53
|
+
/** Redirect URI for Tide link */
|
|
54
|
+
tideLinkRedirectUri?: string;
|
|
55
|
+
/** Access metadata adapter (saves metadata when role changes create change sets) */
|
|
56
|
+
accessMetadataAPI?: AccessMetadataAPI;
|
|
57
|
+
/** Callback when a change set is created */
|
|
58
|
+
onChangeSetCreated?: (info: ChangeSetInfo) => void | Promise<void>;
|
|
59
|
+
/** Override fetch users */
|
|
60
|
+
fetchUsers?: UsersPageBaseProps<T>["fetchUsers"];
|
|
61
|
+
/** Override create */
|
|
62
|
+
onCreate?: UsersPageBaseProps<T>["onCreate"];
|
|
63
|
+
/** Override delete */
|
|
64
|
+
onDelete?: UsersPageBaseProps<T>["onDelete"];
|
|
65
|
+
/** Override getTideLinkUrl */
|
|
66
|
+
getTideLinkUrl?: UsersPageBaseProps<T>["getTideLinkUrl"];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function UsersPage<T extends UserItem = UserItem>({
|
|
70
|
+
adminAPI: adminAPIProp,
|
|
71
|
+
realm,
|
|
72
|
+
tideLinkRedirectUri,
|
|
73
|
+
accessMetadataAPI,
|
|
74
|
+
onChangeSetCreated,
|
|
75
|
+
fetchUsers: fetchUsersProp,
|
|
76
|
+
onCreate: onCreateProp,
|
|
77
|
+
onDelete: onDeleteProp,
|
|
78
|
+
getTideLinkUrl: getTideLinkUrlProp,
|
|
79
|
+
...props
|
|
80
|
+
}: UsersPageProps<T>) {
|
|
81
|
+
// Use provided AdminAPI instance or fall back to default singleton
|
|
82
|
+
const api = adminAPIProp || DefaultAdminAPI;
|
|
83
|
+
|
|
84
|
+
// Set realm if provided
|
|
85
|
+
if (realm && api.setRealm) {
|
|
86
|
+
api.setRealm(realm);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const fetchUsers =
|
|
90
|
+
fetchUsersProp ||
|
|
91
|
+
(async () => {
|
|
92
|
+
const rawUsers = await api.getUsers();
|
|
93
|
+
// Transform users to include linked status based on vuid attribute
|
|
94
|
+
const users = (rawUsers || []).map((u: any) => ({
|
|
95
|
+
...u,
|
|
96
|
+
// User is linked if they have a vuid attribute (Tide account linked)
|
|
97
|
+
linked: !!(u.attributes?.vuid?.[0]),
|
|
98
|
+
// Ensure role is an array
|
|
99
|
+
role: Array.isArray(u.role) ? u.role : u.role ? [u.role] : [],
|
|
100
|
+
})) as T[];
|
|
101
|
+
return users;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const fetchRoles = async () => {
|
|
105
|
+
const roles = await api.getRoles();
|
|
106
|
+
return { roles: roles || [] };
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const onCreate =
|
|
110
|
+
onCreateProp ||
|
|
111
|
+
(async (data) => {
|
|
112
|
+
await api.createUser(data);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const onUpdateProfile = async (data: {
|
|
116
|
+
id: string;
|
|
117
|
+
firstName?: string;
|
|
118
|
+
lastName?: string;
|
|
119
|
+
email?: string;
|
|
120
|
+
}) => {
|
|
121
|
+
await api.updateUser(data.id, {
|
|
122
|
+
firstName: data.firstName,
|
|
123
|
+
lastName: data.lastName,
|
|
124
|
+
email: data.email,
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Helper to track change set metadata
|
|
129
|
+
const trackChangeSet = async (
|
|
130
|
+
response: any,
|
|
131
|
+
userId: string,
|
|
132
|
+
username: string,
|
|
133
|
+
roles: string[],
|
|
134
|
+
actionType: string
|
|
135
|
+
) => {
|
|
136
|
+
// Extract changeSetId from response (AdminAPI returns this)
|
|
137
|
+
const changeSetId = response?.changeSetId || response?.draftRecordId || response?.id;
|
|
138
|
+
if (!changeSetId) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const timestamp = Date.now();
|
|
143
|
+
const info: ChangeSetInfo = {
|
|
144
|
+
changeSetId,
|
|
145
|
+
changeSetType: response?.changeSetType || "USER",
|
|
146
|
+
actionType,
|
|
147
|
+
username,
|
|
148
|
+
userId,
|
|
149
|
+
roles,
|
|
150
|
+
timestamp,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Call the callback if provided
|
|
154
|
+
if (onChangeSetCreated) {
|
|
155
|
+
await onChangeSetCreated(info);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Save to metadata API if provided
|
|
159
|
+
if (accessMetadataAPI) {
|
|
160
|
+
const record: AccessMetadataRecord = {
|
|
161
|
+
changeSetId,
|
|
162
|
+
username,
|
|
163
|
+
role: roles.join(", "),
|
|
164
|
+
timestamp,
|
|
165
|
+
actionType,
|
|
166
|
+
changeSetType: info.changeSetType,
|
|
167
|
+
};
|
|
168
|
+
await accessMetadataAPI.saveMetadata(record);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const onUpdateRoles = async (data: {
|
|
173
|
+
id: string;
|
|
174
|
+
username?: string;
|
|
175
|
+
rolesToAdd?: string[];
|
|
176
|
+
rolesToRemove?: string[];
|
|
177
|
+
}) => {
|
|
178
|
+
// Get username for metadata tracking
|
|
179
|
+
const username = data.username || data.id;
|
|
180
|
+
|
|
181
|
+
if (data.rolesToAdd?.length && api.addUserRoles) {
|
|
182
|
+
// Fetch role details first
|
|
183
|
+
const roles = await Promise.all(
|
|
184
|
+
data.rolesToAdd.map((name) => api.getRole(name))
|
|
185
|
+
);
|
|
186
|
+
const response = await api.addUserRoles(data.id, roles);
|
|
187
|
+
// Track the change set
|
|
188
|
+
await trackChangeSet(response, data.id, username, data.rolesToAdd, "ADD");
|
|
189
|
+
}
|
|
190
|
+
if (data.rolesToRemove?.length && api.removeUserRoles) {
|
|
191
|
+
const roles = await Promise.all(
|
|
192
|
+
data.rolesToRemove.map((name) => api.getRole(name))
|
|
193
|
+
);
|
|
194
|
+
const response = await api.removeUserRoles(data.id, roles);
|
|
195
|
+
// Track the change set
|
|
196
|
+
await trackChangeSet(response, data.id, username, data.rolesToRemove, "REMOVE");
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const onDelete =
|
|
201
|
+
onDeleteProp ||
|
|
202
|
+
(async (userId: string) => {
|
|
203
|
+
await api.deleteUser(userId);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const onSetEnabled = api.setUserEnabled
|
|
207
|
+
? async (userId: string, enabled: boolean) => {
|
|
208
|
+
await api.setUserEnabled!(userId, enabled);
|
|
209
|
+
}
|
|
210
|
+
: undefined;
|
|
211
|
+
|
|
212
|
+
// Get Tide link URL for user invitation
|
|
213
|
+
const getTideLinkUrl =
|
|
214
|
+
getTideLinkUrlProp ||
|
|
215
|
+
(api.getTideLinkUrl
|
|
216
|
+
? async (userId: string) => {
|
|
217
|
+
// Default redirect to current origin if not specified
|
|
218
|
+
const redirectUri = tideLinkRedirectUri || (typeof window !== "undefined" ? window.location.origin : "");
|
|
219
|
+
const linkUrl = await api.getTideLinkUrl!(userId, redirectUri);
|
|
220
|
+
return { linkUrl };
|
|
221
|
+
}
|
|
222
|
+
: undefined);
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<UsersPageBase<T>
|
|
226
|
+
fetchUsers={fetchUsers}
|
|
227
|
+
fetchRoles={fetchRoles}
|
|
228
|
+
onCreate={onCreate}
|
|
229
|
+
onUpdateProfile={onUpdateProfile}
|
|
230
|
+
onUpdateRoles={onUpdateRoles}
|
|
231
|
+
onDelete={onDelete}
|
|
232
|
+
onSetEnabled={onSetEnabled}
|
|
233
|
+
getTideLinkUrl={getTideLinkUrl}
|
|
234
|
+
{...props}
|
|
235
|
+
/>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Pre-made page components - uses AdminAPI from @tidecloak/js
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
RolesPage,
|
|
5
|
+
createLocalStoragePolicyAPI,
|
|
6
|
+
type RolesPageProps,
|
|
7
|
+
type PolicyAPI,
|
|
8
|
+
type RolePolicy,
|
|
9
|
+
} from "./RolesPage";
|
|
10
|
+
export { UsersPage, type UsersPageProps, type ChangeSetInfo } from "./UsersPage";
|
|
11
|
+
export {
|
|
12
|
+
TemplatesPage,
|
|
13
|
+
createLocalStorageTemplateAPI,
|
|
14
|
+
type TemplatesPageProps,
|
|
15
|
+
type TemplateAPI,
|
|
16
|
+
} from "./TemplatesPage";
|
|
17
|
+
export {
|
|
18
|
+
LogsPage,
|
|
19
|
+
createLocalStoragePolicyLogsAPI,
|
|
20
|
+
type LogsPageProps,
|
|
21
|
+
type PolicyLogsAPI,
|
|
22
|
+
type PolicyLogData,
|
|
23
|
+
} from "./LogsPage";
|
|
24
|
+
export {
|
|
25
|
+
ApprovalsPage,
|
|
26
|
+
createLocalStoragePolicyApprovalsAPI,
|
|
27
|
+
createLocalStorageAccessMetadataAPI,
|
|
28
|
+
type ApprovalsPageProps,
|
|
29
|
+
type PolicyApprovalsAPI,
|
|
30
|
+
type PolicyApprovalData,
|
|
31
|
+
type AccessMetadataAPI,
|
|
32
|
+
type AccessMetadataRecord,
|
|
33
|
+
type TideApprovalContext,
|
|
34
|
+
type TideApprovalResult,
|
|
35
|
+
type EnclaveApprovalTabConfig,
|
|
36
|
+
} from "./ApprovalsPage";
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { TabDef } from "../../types";
|
|
3
|
+
|
|
4
|
+
export interface TabsViewProps {
|
|
5
|
+
/** Tab definitions */
|
|
6
|
+
tabs: TabDef[];
|
|
7
|
+
/** Currently active tab key */
|
|
8
|
+
activeTab: string;
|
|
9
|
+
/** Tab change callback */
|
|
10
|
+
onTabChange: (tabKey: string) => void;
|
|
11
|
+
/** Additional class name */
|
|
12
|
+
className?: string;
|
|
13
|
+
/** Tab list class name */
|
|
14
|
+
tabListClassName?: string;
|
|
15
|
+
/** Custom components */
|
|
16
|
+
components?: {
|
|
17
|
+
Tabs?: React.ComponentType<{
|
|
18
|
+
value: string;
|
|
19
|
+
onValueChange: (value: string) => void;
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
className?: string;
|
|
22
|
+
}>;
|
|
23
|
+
TabsList?: React.ComponentType<{ children: React.ReactNode; className?: string }>;
|
|
24
|
+
TabsTrigger?: React.ComponentType<{
|
|
25
|
+
value: string;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
className?: string;
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
}>;
|
|
30
|
+
TabsContent?: React.ComponentType<{
|
|
31
|
+
value: string;
|
|
32
|
+
className?: string;
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
}>;
|
|
35
|
+
Badge?: React.ComponentType<{
|
|
36
|
+
variant?: string;
|
|
37
|
+
className?: string;
|
|
38
|
+
children: React.ReactNode;
|
|
39
|
+
}>;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* TabsView - Generic tabbed view component
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```tsx
|
|
48
|
+
* <TabsView
|
|
49
|
+
* tabs={[
|
|
50
|
+
* { key: 'access', label: 'Access', badge: 5, content: <AccessTab /> },
|
|
51
|
+
* { key: 'roles', label: 'Roles', badge: 2, content: <RolesTab /> },
|
|
52
|
+
* { key: 'policies', label: 'Policies', content: <PoliciesTab /> },
|
|
53
|
+
* ]}
|
|
54
|
+
* activeTab={activeTab}
|
|
55
|
+
* onTabChange={setActiveTab}
|
|
56
|
+
* />
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function TabsView({
|
|
60
|
+
tabs,
|
|
61
|
+
activeTab,
|
|
62
|
+
onTabChange,
|
|
63
|
+
className,
|
|
64
|
+
tabListClassName,
|
|
65
|
+
components = {},
|
|
66
|
+
}: TabsViewProps) {
|
|
67
|
+
// Use custom components or defaults
|
|
68
|
+
const Tabs = components.Tabs || DefaultTabs;
|
|
69
|
+
const TabsList = components.TabsList || DefaultTabsList;
|
|
70
|
+
const TabsTrigger = components.TabsTrigger || DefaultTabsTrigger;
|
|
71
|
+
const TabsContent = components.TabsContent || DefaultTabsContent;
|
|
72
|
+
const Badge = components.Badge || DefaultBadge;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Tabs value={activeTab} onValueChange={onTabChange} className={className}>
|
|
76
|
+
<TabsList className={tabListClassName}>
|
|
77
|
+
{tabs.map((tab) => (
|
|
78
|
+
<TabsTrigger
|
|
79
|
+
key={tab.key}
|
|
80
|
+
value={tab.key}
|
|
81
|
+
disabled={tab.disabled}
|
|
82
|
+
>
|
|
83
|
+
<span style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
|
84
|
+
{tab.icon}
|
|
85
|
+
{tab.label}
|
|
86
|
+
{tab.badge !== undefined && tab.badge !== null && tab.badge > 0 && (
|
|
87
|
+
<Badge
|
|
88
|
+
variant={tab.badgeVariant || "destructive"}
|
|
89
|
+
>
|
|
90
|
+
{tab.badge > 99 ? "99+" : tab.badge}
|
|
91
|
+
</Badge>
|
|
92
|
+
)}
|
|
93
|
+
</span>
|
|
94
|
+
</TabsTrigger>
|
|
95
|
+
))}
|
|
96
|
+
</TabsList>
|
|
97
|
+
|
|
98
|
+
{tabs.map((tab) => (
|
|
99
|
+
<TabsContent key={tab.key} value={tab.key}>
|
|
100
|
+
{tab.content}
|
|
101
|
+
</TabsContent>
|
|
102
|
+
))}
|
|
103
|
+
</Tabs>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Default component implementations
|
|
108
|
+
function DefaultTabs({
|
|
109
|
+
value,
|
|
110
|
+
onValueChange,
|
|
111
|
+
children,
|
|
112
|
+
className,
|
|
113
|
+
}: {
|
|
114
|
+
value: string;
|
|
115
|
+
onValueChange: (value: string) => void;
|
|
116
|
+
children: React.ReactNode;
|
|
117
|
+
className?: string;
|
|
118
|
+
}) {
|
|
119
|
+
return (
|
|
120
|
+
<div className={className} data-value={value}>
|
|
121
|
+
{React.Children.map(children, (child) => {
|
|
122
|
+
if (React.isValidElement(child)) {
|
|
123
|
+
return React.cloneElement(child as React.ReactElement<any>, {
|
|
124
|
+
activeTab: value,
|
|
125
|
+
onTabChange: onValueChange,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return child;
|
|
129
|
+
})}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function DefaultTabsList({
|
|
135
|
+
children,
|
|
136
|
+
className,
|
|
137
|
+
activeTab,
|
|
138
|
+
onTabChange,
|
|
139
|
+
}: {
|
|
140
|
+
children: React.ReactNode;
|
|
141
|
+
className?: string;
|
|
142
|
+
activeTab?: string;
|
|
143
|
+
onTabChange?: (value: string) => void;
|
|
144
|
+
}) {
|
|
145
|
+
return (
|
|
146
|
+
<div
|
|
147
|
+
style={{
|
|
148
|
+
display: "flex",
|
|
149
|
+
alignItems: "stretch",
|
|
150
|
+
borderBottom: "1px solid #e5e7eb",
|
|
151
|
+
backgroundColor: "#f9fafb",
|
|
152
|
+
padding: "0 1rem",
|
|
153
|
+
gap: "0",
|
|
154
|
+
}}
|
|
155
|
+
className={className}
|
|
156
|
+
>
|
|
157
|
+
{React.Children.map(children, (child) => {
|
|
158
|
+
if (React.isValidElement(child)) {
|
|
159
|
+
return React.cloneElement(child as React.ReactElement<any>, {
|
|
160
|
+
activeTab,
|
|
161
|
+
onTabChange,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return child;
|
|
165
|
+
})}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function DefaultTabsTrigger({
|
|
171
|
+
value,
|
|
172
|
+
disabled,
|
|
173
|
+
className,
|
|
174
|
+
children,
|
|
175
|
+
activeTab,
|
|
176
|
+
onTabChange,
|
|
177
|
+
}: {
|
|
178
|
+
value: string;
|
|
179
|
+
disabled?: boolean;
|
|
180
|
+
className?: string;
|
|
181
|
+
children: React.ReactNode;
|
|
182
|
+
activeTab?: string;
|
|
183
|
+
onTabChange?: (value: string) => void;
|
|
184
|
+
}) {
|
|
185
|
+
const isActive = activeTab === value;
|
|
186
|
+
|
|
187
|
+
const baseStyle: React.CSSProperties = {
|
|
188
|
+
display: "inline-flex",
|
|
189
|
+
alignItems: "center",
|
|
190
|
+
justifyContent: "center",
|
|
191
|
+
whiteSpace: "nowrap",
|
|
192
|
+
padding: "0.75rem 1rem",
|
|
193
|
+
fontSize: "0.875rem",
|
|
194
|
+
fontWeight: 500,
|
|
195
|
+
transition: "all 150ms ease",
|
|
196
|
+
border: "none",
|
|
197
|
+
borderBottom: isActive ? "2px solid #1f2937" : "2px solid transparent",
|
|
198
|
+
marginBottom: "-1px",
|
|
199
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
200
|
+
opacity: disabled ? 0.5 : 1,
|
|
201
|
+
backgroundColor: isActive ? "#ffffff" : "transparent",
|
|
202
|
+
color: isActive ? "#111827" : "#6b7280",
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
onClick={() => onTabChange?.(value)}
|
|
209
|
+
disabled={disabled}
|
|
210
|
+
style={baseStyle}
|
|
211
|
+
className={className}
|
|
212
|
+
>
|
|
213
|
+
{children}
|
|
214
|
+
</button>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function DefaultTabsContent({
|
|
219
|
+
value,
|
|
220
|
+
className,
|
|
221
|
+
children,
|
|
222
|
+
activeTab,
|
|
223
|
+
}: {
|
|
224
|
+
value: string;
|
|
225
|
+
className?: string;
|
|
226
|
+
children: React.ReactNode;
|
|
227
|
+
activeTab?: string;
|
|
228
|
+
}) {
|
|
229
|
+
if (activeTab !== value) return null;
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<div
|
|
233
|
+
style={{ marginTop: 0 }}
|
|
234
|
+
className={className}
|
|
235
|
+
>
|
|
236
|
+
{children}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function DefaultBadge({
|
|
242
|
+
variant,
|
|
243
|
+
className,
|
|
244
|
+
children,
|
|
245
|
+
}: {
|
|
246
|
+
variant?: string;
|
|
247
|
+
className?: string;
|
|
248
|
+
children: React.ReactNode;
|
|
249
|
+
}) {
|
|
250
|
+
const getVariantStyles = (): React.CSSProperties => {
|
|
251
|
+
switch (variant) {
|
|
252
|
+
case "destructive":
|
|
253
|
+
return {
|
|
254
|
+
backgroundColor: "#dc2626",
|
|
255
|
+
color: "#ffffff",
|
|
256
|
+
borderColor: "transparent",
|
|
257
|
+
};
|
|
258
|
+
case "secondary":
|
|
259
|
+
return {
|
|
260
|
+
backgroundColor: "#f3f4f6",
|
|
261
|
+
color: "#374151",
|
|
262
|
+
borderColor: "transparent",
|
|
263
|
+
};
|
|
264
|
+
case "outline":
|
|
265
|
+
return {
|
|
266
|
+
backgroundColor: "transparent",
|
|
267
|
+
color: "#111827",
|
|
268
|
+
borderColor: "#e5e7eb",
|
|
269
|
+
};
|
|
270
|
+
default:
|
|
271
|
+
return {
|
|
272
|
+
backgroundColor: "#3b82f6",
|
|
273
|
+
color: "#ffffff",
|
|
274
|
+
borderColor: "transparent",
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<span
|
|
281
|
+
style={{
|
|
282
|
+
display: "inline-flex",
|
|
283
|
+
alignItems: "center",
|
|
284
|
+
justifyContent: "center",
|
|
285
|
+
borderRadius: "9999px",
|
|
286
|
+
border: "1px solid",
|
|
287
|
+
padding: "0 0.5rem",
|
|
288
|
+
fontSize: "0.75rem",
|
|
289
|
+
fontWeight: 600,
|
|
290
|
+
height: "1.25rem",
|
|
291
|
+
minWidth: "1.25rem",
|
|
292
|
+
marginLeft: "0.25rem",
|
|
293
|
+
...getVariantStyles(),
|
|
294
|
+
}}
|
|
295
|
+
className={className}
|
|
296
|
+
>
|
|
297
|
+
{children}
|
|
298
|
+
</span>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TabsView, type TabsViewProps } from "./TabsView";
|