@schandlergarcia/sf-web-components 2.3.17 → 2.5.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 (94) hide show
  1. package/.a4drules/skills/command-center-builder/SKILL.md +3 -2
  2. package/.a4drules/skills/component-library/SKILL.md +50 -4
  3. package/.a4drules/skills/component-library/card-components.md +88 -0
  4. package/.a4drules/skills/component-library/when-to-use.md +1 -0
  5. package/CHANGELOG.md +40 -0
  6. package/CLAUDE.md +12 -13
  7. package/README.md +0 -15
  8. package/dist/components/library/cards/KanbanBoard.js +313 -0
  9. package/dist/components/library/cards/KanbanBoard.js.map +1 -0
  10. package/dist/components/library/index.js +60 -57
  11. package/dist/components/library/index.js.map +1 -1
  12. package/dist/components/workspace/ComponentRegistry.js +5 -2
  13. package/dist/components/workspace/ComponentRegistry.js.map +1 -1
  14. package/dist/index.js +84 -82
  15. package/dist/index.js.map +1 -1
  16. package/dist/styles/global.css +44 -57
  17. package/package.json +7 -2
  18. package/scripts/apply-brand.mjs +47 -30
  19. package/scripts/postinstall.mjs +1 -11
  20. package/src/components/library/cards/KanbanBoard.jsx +507 -0
  21. package/src/components/library/index.jsx +1 -0
  22. package/src/styles/global.css +44 -57
  23. package/brands/engine/PARTNER_HUB_PRD.md +0 -584
  24. package/brands/engine/agentApiConfig.ts +0 -36
  25. package/brands/engine/app/api/graphql-operations-types.ts +0 -11260
  26. package/brands/engine/app/api/graphqlClient.ts +0 -25
  27. package/brands/engine/app/api/partnerQueries.ts +0 -212
  28. package/brands/engine/app/appLayout.tsx +0 -5
  29. package/brands/engine/app/components/AgentPanel.tsx +0 -541
  30. package/brands/engine/app/components/AgentforceConversationClient.tsx +0 -201
  31. package/brands/engine/app/components/Data360Widget.tsx +0 -301
  32. package/brands/engine/app/components/__inherit_AgentforceConversationClient.tsx +0 -3
  33. package/brands/engine/app/components/alerts/status-alert.tsx +0 -49
  34. package/brands/engine/app/components/layouts/card-layout.tsx +0 -29
  35. package/brands/engine/app/components/workspace/CommandCenter.tsx +0 -16
  36. package/brands/engine/app/config/agentApi.ts +0 -36
  37. package/brands/engine/app/data/partner-hub-sample-data.js +0 -297
  38. package/brands/engine/app/features/object-search/__examples__/api/accountSearchService.ts +0 -46
  39. package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +0 -19
  40. package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +0 -19
  41. package/brands/engine/app/features/object-search/__examples__/api/query/getAccountDetail.graphql +0 -121
  42. package/brands/engine/app/features/object-search/__examples__/api/query/searchAccounts.graphql +0 -51
  43. package/brands/engine/app/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +0 -357
  44. package/brands/engine/app/features/object-search/__examples__/pages/AccountSearch.tsx +0 -312
  45. package/brands/engine/app/features/object-search/__examples__/pages/Home.tsx +0 -34
  46. package/brands/engine/app/features/object-search/api/objectSearchService.ts +0 -84
  47. package/brands/engine/app/features/object-search/components/ActiveFilters.tsx +0 -89
  48. package/brands/engine/app/features/object-search/components/FilterContext.tsx +0 -83
  49. package/brands/engine/app/features/object-search/components/ObjectBreadcrumb.tsx +0 -66
  50. package/brands/engine/app/features/object-search/components/PaginationControls.tsx +0 -109
  51. package/brands/engine/app/features/object-search/components/SearchBar.tsx +0 -41
  52. package/brands/engine/app/features/object-search/components/SortControl.tsx +0 -143
  53. package/brands/engine/app/features/object-search/components/filters/BooleanFilter.tsx +0 -78
  54. package/brands/engine/app/features/object-search/components/filters/DateFilter.tsx +0 -128
  55. package/brands/engine/app/features/object-search/components/filters/DateRangeFilter.tsx +0 -70
  56. package/brands/engine/app/features/object-search/components/filters/FilterFieldWrapper.tsx +0 -33
  57. package/brands/engine/app/features/object-search/components/filters/MultiSelectFilter.tsx +0 -97
  58. package/brands/engine/app/features/object-search/components/filters/NumericRangeFilter.tsx +0 -163
  59. package/brands/engine/app/features/object-search/components/filters/SearchFilter.tsx +0 -50
  60. package/brands/engine/app/features/object-search/components/filters/SelectFilter.tsx +0 -97
  61. package/brands/engine/app/features/object-search/components/filters/TextFilter.tsx +0 -91
  62. package/brands/engine/app/features/object-search/hooks/useAsyncData.ts +0 -54
  63. package/brands/engine/app/features/object-search/hooks/useCachedAsyncData.ts +0 -184
  64. package/brands/engine/app/features/object-search/hooks/useDebouncedCallback.ts +0 -34
  65. package/brands/engine/app/features/object-search/hooks/useObjectSearchParams.ts +0 -252
  66. package/brands/engine/app/features/object-search/utils/debounce.ts +0 -25
  67. package/brands/engine/app/features/object-search/utils/fieldUtils.ts +0 -29
  68. package/brands/engine/app/features/object-search/utils/filterUtils.ts +0 -404
  69. package/brands/engine/app/features/object-search/utils/sortUtils.ts +0 -38
  70. package/brands/engine/app/hooks/useEngineLiveData.ts +0 -49
  71. package/brands/engine/app/hooks/useEvaAgent.ts +0 -288
  72. package/brands/engine/app/hooks/usePartnerDashboardData.ts +0 -141
  73. package/brands/engine/app/navigationMenu.tsx +0 -80
  74. package/brands/engine/app/pages/AccountObjectDetailPage.tsx +0 -361
  75. package/brands/engine/app/pages/AccountSearch.tsx +0 -305
  76. package/brands/engine/app/pages/BlankDashboard.tsx +0 -15
  77. package/brands/engine/app/pages/DataTest.tsx +0 -78
  78. package/brands/engine/app/pages/Home.tsx +0 -5
  79. package/brands/engine/app/pages/NotFound.tsx +0 -19
  80. package/brands/engine/app/pages/PartnerHubDashboard.tsx +0 -2760
  81. package/brands/engine/app/pages/Search.tsx +0 -13
  82. package/brands/engine/app/router-utils.tsx +0 -35
  83. package/brands/engine/app/routes.tsx +0 -39
  84. package/brands/engine/app/styles/global.css +0 -269
  85. package/brands/engine/brand.css +0 -40
  86. package/brands/engine/engine-command-center-prd.md +0 -575
  87. package/brands/engine/engine-live-data.js +0 -135
  88. package/brands/engine/engine-sample-data.js +0 -378
  89. package/brands/engine/engine_logo.png +0 -0
  90. package/brands/engine/global.css +0 -269
  91. package/brands/engine/partner-hub-sample-data.js +0 -281
  92. package/brands/engine/schema.graphql +0 -292
  93. package/brands/engine/useEngineLiveData.ts +0 -49
  94. package/brands/engine/useEvaAgent.ts +0 -288
@@ -1,288 +0,0 @@
1
- /**
2
- * useEvaAgent — React hook for the Agentforce Agent REST API
3
- *
4
- * Handles the full lifecycle:
5
- * 1. OAuth client-credentials → access_token
6
- * 2. POST …/sessions → sessionId + message href
7
- * 3. POST …/messages → agent response text
8
- * 4. DELETE …/sessions → cleanup on unmount
9
- *
10
- * All HTTP calls go through the Vite proxy (see vite.config.ts):
11
- * /sf-oauth/* → myDomainUrl
12
- * /sf-agent/* → agentApiBaseUrl
13
- */
14
-
15
- import { useCallback, useEffect, useRef, useState } from "react";
16
- import { AGENT_API_CONFIG } from "@/config/agentApi";
17
-
18
- export interface AgentMessage {
19
- role: "user" | "agent";
20
- text: string;
21
- id: string;
22
- timestamp: string;
23
- }
24
-
25
- interface SessionState {
26
- accessToken: string;
27
- sessionId: string;
28
- messagesHref: string;
29
- endHref: string;
30
- }
31
-
32
- export default function useEvaAgent() {
33
- const [messages, setMessages] = useState<AgentMessage[]>([]);
34
- const [isConnecting, setIsConnecting] = useState(false);
35
- const [isReady, setIsReady] = useState(false);
36
- const [isSending, setIsSending] = useState(false);
37
- const [error, setError] = useState<string | null>(null);
38
-
39
- const sessionRef = useRef<SessionState | null>(null);
40
- const sequenceRef = useRef(1);
41
- const abortRef = useRef<AbortController | null>(null);
42
-
43
- /* ── Step 1: OAuth token ──────────────────────────────── */
44
- async function authenticate(signal: AbortSignal): Promise<string> {
45
- const { clientId, clientSecret } = AGENT_API_CONFIG;
46
-
47
- const body = new URLSearchParams({
48
- grant_type: "client_credentials",
49
- client_id: clientId,
50
- client_secret: clientSecret,
51
- });
52
-
53
- const res = await fetch("/sf-oauth/services/oauth2/token", {
54
- method: "POST",
55
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
56
- body,
57
- signal,
58
- });
59
-
60
- if (!res.ok) {
61
- const text = await res.text();
62
- throw new Error(`OAuth failed (${res.status}): ${text}`);
63
- }
64
-
65
- const data = await res.json();
66
- return data.access_token;
67
- }
68
-
69
- /* ── Step 2: Create session ───────────────────────────── */
70
- async function createSession(
71
- accessToken: string,
72
- signal: AbortSignal
73
- ): Promise<Omit<SessionState, "accessToken">> {
74
- const { agentId, bypassUser, demoTraveler, myDomainUrl } =
75
- AGENT_API_CONFIG;
76
-
77
- const payload = {
78
- externalSessionKey: crypto.randomUUID(),
79
- instanceConfig: { endpoint: myDomainUrl },
80
- streamingCapabilities: { chunkTypes: ["Text"] },
81
- bypassUser,
82
- variables: [
83
- {
84
- name: "$Context.EndUserId",
85
- type: "Text",
86
- value: demoTraveler.contactId,
87
- },
88
- {
89
- name: "$Context.EndUserEmail",
90
- type: "Text",
91
- value: demoTraveler.email,
92
- },
93
- {
94
- name: "$Context.EndUserFirstName",
95
- type: "Text",
96
- value: demoTraveler.firstName,
97
- },
98
- {
99
- name: "$Context.EndUserLastName",
100
- type: "Text",
101
- value: demoTraveler.lastName,
102
- },
103
- ],
104
- };
105
-
106
- const res = await fetch(
107
- `/sf-agent/einstein/ai-agent/v1/agents/${agentId}/sessions`,
108
- {
109
- method: "POST",
110
- headers: {
111
- Authorization: `Bearer ${accessToken}`,
112
- "Content-Type": "application/json",
113
- },
114
- body: JSON.stringify(payload),
115
- signal,
116
- }
117
- );
118
-
119
- if (!res.ok) {
120
- const text = await res.text();
121
- throw new Error(`Session creation failed (${res.status}): ${text}`);
122
- }
123
-
124
- const data = await res.json();
125
- return {
126
- sessionId: data.sessionId,
127
- messagesHref: data._links?.messages?.href ?? "",
128
- endHref: data._links?.end?.href ?? "",
129
- };
130
- }
131
-
132
- /* ── Step 3: Send message ─────────────────────────────── */
133
- const sendMessage = useCallback(async (text: string) => {
134
- const session = sessionRef.current;
135
- if (!session) return;
136
-
137
- const userMsg: AgentMessage = {
138
- role: "user",
139
- text,
140
- id: `user-${Date.now()}`,
141
- timestamp: new Date().toISOString(),
142
- };
143
- setMessages((prev) => [...prev, userMsg]);
144
- setIsSending(true);
145
-
146
- try {
147
- const seqId = sequenceRef.current++;
148
-
149
- const messagesUrl = session.messagesHref.startsWith("http")
150
- ? `/sf-agent${new URL(session.messagesHref).pathname}`
151
- : `/sf-agent${session.messagesHref}`;
152
-
153
- const res = await fetch(messagesUrl, {
154
- method: "POST",
155
- headers: {
156
- Authorization: `Bearer ${session.accessToken}`,
157
- "Content-Type": "application/json",
158
- Accept: "application/json",
159
- },
160
- body: JSON.stringify({
161
- message: { sequenceId: seqId, type: "Text", text },
162
- }),
163
- });
164
-
165
- if (!res.ok) {
166
- const errText = await res.text();
167
- throw new Error(`Agent message failed (${res.status}): ${errText}`);
168
- }
169
-
170
- const data = await res.json();
171
-
172
- const agentTexts: string[] = [];
173
- if (Array.isArray(data.messages)) {
174
- for (const m of data.messages) {
175
- if (m.type === "Inform" && m.message) {
176
- agentTexts.push(m.message);
177
- } else if (m.type === "Text" && m.message) {
178
- agentTexts.push(m.message);
179
- }
180
- }
181
- }
182
-
183
- const responseText =
184
- agentTexts.length > 0
185
- ? agentTexts.join("\n\n")
186
- : data.message ?? "No response from agent.";
187
-
188
- const agentMsg: AgentMessage = {
189
- role: "agent",
190
- text: responseText,
191
- id: `agent-${Date.now()}`,
192
- timestamp: new Date().toISOString(),
193
- };
194
- setMessages((prev) => [...prev, agentMsg]);
195
- } catch (err: unknown) {
196
- const msg = err instanceof Error ? err.message : "Unknown error";
197
- setError(msg);
198
- const errMsg: AgentMessage = {
199
- role: "agent",
200
- text: `Error: ${msg}`,
201
- id: `error-${Date.now()}`,
202
- timestamp: new Date().toISOString(),
203
- };
204
- setMessages((prev) => [...prev, errMsg]);
205
- } finally {
206
- setIsSending(false);
207
- }
208
- }, []);
209
-
210
- /* ── Step 4: End session ──────────────────────────────── */
211
- const endSession = useCallback(async () => {
212
- const session = sessionRef.current;
213
- if (!session) return;
214
-
215
- try {
216
- const endUrl = session.endHref.startsWith("http")
217
- ? `/sf-agent${new URL(session.endHref).pathname}`
218
- : `/sf-agent${session.endHref}`;
219
-
220
- await fetch(endUrl, {
221
- method: "DELETE",
222
- headers: { Authorization: `Bearer ${session.accessToken}` },
223
- });
224
- } catch {
225
- /* best-effort cleanup */
226
- }
227
-
228
- sessionRef.current = null;
229
- setIsReady(false);
230
- }, []);
231
-
232
- /* ── Init: authenticate + create session on mount ─────── */
233
- const connect = useCallback(async () => {
234
- if (sessionRef.current) return;
235
-
236
- abortRef.current = new AbortController();
237
- const { signal } = abortRef.current;
238
-
239
- setIsConnecting(true);
240
- setError(null);
241
-
242
- try {
243
- const accessToken = await authenticate(signal);
244
- const session = await createSession(accessToken, signal);
245
-
246
- sessionRef.current = { accessToken, ...session };
247
- sequenceRef.current = 1;
248
- setIsReady(true);
249
- } catch (err: unknown) {
250
- if ((err as Error).name !== "AbortError") {
251
- const msg = err instanceof Error ? err.message : "Connection failed";
252
- setError(msg);
253
- }
254
- } finally {
255
- setIsConnecting(false);
256
- }
257
- }, []);
258
-
259
- /* ── Cleanup on unmount ───────────────────────────────── */
260
- useEffect(() => {
261
- return () => {
262
- abortRef.current?.abort();
263
- if (sessionRef.current) {
264
- const session = sessionRef.current;
265
- const endUrl = session.endHref.startsWith("http")
266
- ? `/sf-agent${new URL(session.endHref).pathname}`
267
- : `/sf-agent${session.endHref}`;
268
-
269
- fetch(endUrl, {
270
- method: "DELETE",
271
- headers: { Authorization: `Bearer ${session.accessToken}` },
272
- }).catch(() => {});
273
- sessionRef.current = null;
274
- }
275
- };
276
- }, []);
277
-
278
- return {
279
- messages,
280
- isConnecting,
281
- isReady,
282
- isSending,
283
- error,
284
- connect,
285
- sendMessage,
286
- endSession,
287
- };
288
- }
@@ -1,141 +0,0 @@
1
- import { createDataSDK, gql } from "@salesforce/sdk-data";
2
- import { useState, useEffect } from "react";
3
- import {
4
- GET_CURRENT_USER,
5
- GET_PARTNER_ACCOUNT,
6
- FIND_PARTNER_ACCOUNT,
7
- GET_CONTRACTS,
8
- GET_INVOICES,
9
- GET_PENALTIES,
10
- GET_DISPUTES,
11
- GET_PROPERTIES,
12
- } from "@/api/partnerQueries";
13
-
14
- /**
15
- * Hook to fetch live partner dashboard data from Salesforce
16
- *
17
- * Queries:
18
- * - Current partner Account data
19
- * - Partner's invoices, contracts, penalties, disputes
20
- *
21
- * Uses GraphQL via Data SDK for all queries
22
- */
23
-
24
- export function usePartnerDashboardData(partnerId?: string | null) {
25
- const [data, setData] = useState<any>(null);
26
- const [loading, setLoading] = useState(true);
27
- const [error, setError] = useState<Error | null>(null);
28
-
29
- useEffect(() => {
30
- async function fetchData() {
31
- try {
32
- const sdk = await createDataSDK();
33
-
34
- // Get current user
35
- const userResponse = await sdk.graphql?.(GET_CURRENT_USER);
36
- console.log("Current User:", userResponse?.data?.uiapi?.currentUser);
37
-
38
- if (userResponse?.errors?.length) {
39
- console.error("User query errors:", userResponse.errors);
40
- throw new Error(userResponse.errors.map((e: any) => e.message).join("; "));
41
- }
42
-
43
- const user = userResponse?.data?.uiapi?.currentUser;
44
-
45
- // Use partnerId if provided, otherwise query for first partner account
46
- let accountId = partnerId;
47
-
48
- if (!accountId) {
49
- // Query for partner accounts
50
- const partnerAccountResponse = await sdk.graphql?.(FIND_PARTNER_ACCOUNT);
51
- const firstPartner = partnerAccountResponse?.data?.uiapi?.query?.Account?.edges?.[0]?.node;
52
-
53
- if (firstPartner) {
54
- accountId = firstPartner.Id;
55
- console.log("Using first Partner Account:", firstPartner.Name?.value, accountId);
56
- } else {
57
- console.warn("No Partner accounts found. Please create an Account with Type='Partner'");
58
- setLoading(false);
59
- return;
60
- }
61
- }
62
-
63
- console.log("Fetching data for Account:", accountId);
64
-
65
- // Fetch all partner data in parallel
66
- const [
67
- partnerResponse,
68
- contractsResponse,
69
- invoicesResponse,
70
- penaltiesResponse,
71
- disputesResponse,
72
- propertiesResponse,
73
- ] = await Promise.all([
74
- sdk.graphql?.(GET_PARTNER_ACCOUNT, { accountId }),
75
- sdk.graphql?.(GET_CONTRACTS, { accountId }),
76
- sdk.graphql?.(GET_INVOICES, { accountId }),
77
- sdk.graphql?.(GET_PENALTIES, { accountId }),
78
- sdk.graphql?.(GET_DISPUTES, { accountId }),
79
- sdk.graphql?.(GET_PROPERTIES, { accountId }),
80
- ]);
81
-
82
- // Check for errors
83
- if (partnerResponse?.errors?.length) {
84
- console.error("Partner query errors:", partnerResponse.errors);
85
- }
86
- if (contractsResponse?.errors?.length) {
87
- console.error("Contracts query errors:", contractsResponse.errors);
88
- }
89
- if (invoicesResponse?.errors?.length) {
90
- console.error("Invoices query errors:", invoicesResponse.errors);
91
- }
92
- if (penaltiesResponse?.errors?.length) {
93
- console.error("Penalties query errors:", penaltiesResponse.errors);
94
- }
95
- if (disputesResponse?.errors?.length) {
96
- console.error("Disputes query errors:", disputesResponse.errors);
97
- }
98
- if (propertiesResponse?.errors?.length) {
99
- console.error("Properties query errors:", propertiesResponse.errors);
100
- }
101
-
102
- // Extract data
103
- const partner = partnerResponse?.data?.uiapi?.query?.Account?.edges?.[0]?.node || null;
104
- const contracts = contractsResponse?.data?.uiapi?.query?.Contract__c?.edges?.map((e: any) => e.node) ?? [];
105
- const invoices = invoicesResponse?.data?.uiapi?.query?.Invoice__c?.edges?.map((e: any) => e.node) ?? [];
106
- const penalties = penaltiesResponse?.data?.uiapi?.query?.Attrition_Penalty__c?.edges?.map((e: any) => e.node) ?? [];
107
- const disputes = disputesResponse?.data?.uiapi?.query?.Case?.edges?.map((e: any) => e.node) ?? [];
108
- const properties = propertiesResponse?.data?.uiapi?.query?.Property__c?.edges?.map((e: any) => e.node) ?? [];
109
-
110
- console.log("Loaded partner data:", {
111
- partner,
112
- contracts: contracts.length,
113
- invoices: invoices.length,
114
- penalties: penalties.length,
115
- disputes: disputes.length,
116
- properties: properties.length,
117
- });
118
-
119
- setData({
120
- user,
121
- partner,
122
- contracts,
123
- invoices,
124
- penalties,
125
- disputes,
126
- properties,
127
- });
128
-
129
- setLoading(false);
130
- } catch (err) {
131
- console.error("Data fetch error:", err);
132
- setError(err as Error);
133
- setLoading(false);
134
- }
135
- }
136
-
137
- fetchData();
138
- }, [partnerId]);
139
-
140
- return { data, loading, error };
141
- }
@@ -1,80 +0,0 @@
1
- import { Link, useLocation } from 'react-router';
2
- import { getAllRoutes } from './router-utils';
3
- import { useState } from 'react';
4
-
5
- export default function NavigationMenu() {
6
- const [isOpen, setIsOpen] = useState(false);
7
- const location = useLocation();
8
-
9
- const isActive = (path: string) => location.pathname === path;
10
-
11
- const toggleMenu = () => setIsOpen(!isOpen);
12
-
13
- const navigationRoutes: { path: string; label: string }[] = getAllRoutes()
14
- .filter(
15
- route =>
16
- route.handle?.showInNavigation === true &&
17
- route.fullPath !== undefined &&
18
- route.handle?.label !== undefined
19
- )
20
- .map(
21
- route =>
22
- ({
23
- path: route.fullPath,
24
- label: route.handle?.label,
25
- }) as { path: string; label: string }
26
- );
27
-
28
- return (
29
- <nav className="bg-white border-b border-gray-200">
30
- <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
31
- <div className="flex justify-between items-center h-16">
32
- <Link to="/" className="text-xl font-semibold text-gray-900">
33
- React App
34
- </Link>
35
- <button
36
- onClick={toggleMenu}
37
- className="p-2 rounded-md text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
38
- aria-label="Toggle menu"
39
- >
40
- <div className="w-6 h-6 flex flex-col justify-center space-y-1.5">
41
- <span
42
- className={`block h-0.5 w-6 bg-current transition-all ${
43
- isOpen ? 'rotate-45 translate-y-2' : ''
44
- }`}
45
- />
46
- <span
47
- className={`block h-0.5 w-6 bg-current transition-all ${isOpen ? 'opacity-0' : ''}`}
48
- />
49
- <span
50
- className={`block h-0.5 w-6 bg-current transition-all ${
51
- isOpen ? '-rotate-45 -translate-y-2' : ''
52
- }`}
53
- />
54
- </div>
55
- </button>
56
- </div>
57
- {isOpen && (
58
- <div className="pb-4">
59
- <div className="flex flex-col space-y-2">
60
- {navigationRoutes.map(item => (
61
- <Link
62
- key={item.path}
63
- to={item.path}
64
- onClick={() => setIsOpen(false)}
65
- className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
66
- isActive(item.path)
67
- ? 'bg-blue-100 text-blue-700'
68
- : 'text-gray-700 hover:bg-gray-100'
69
- }`}
70
- >
71
- {item.label}
72
- </Link>
73
- ))}
74
- </div>
75
- </div>
76
- )}
77
- </div>
78
- </nav>
79
- );
80
- }