@apptimate/ui 6.2.0 → 6.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apptimate/ui",
3
- "version": "6.2.0",
3
+ "version": "6.3.0",
4
4
  "main": "src/index.tsx",
5
5
  "types": "src/index.tsx",
6
6
  "dependencies": {
@@ -102,7 +102,7 @@ export const Modal = ({ isOpen, onClose, title, children, className, footer, bac
102
102
  {/* Body */}
103
103
  <div className={cn(
104
104
  "flex-1",
105
- size === 'full' ? "overflow-hidden p-0" : "overflow-y-auto px-5 sm:px-8 py-5 sm:py-7"
105
+ size === 'full' ? "overflow-hidden p-0" : "overflow-auto px-5 sm:px-8 py-5 sm:py-7"
106
106
  )}>
107
107
  {children}
108
108
  </div>
@@ -10,7 +10,7 @@ export interface TableProps {
10
10
 
11
11
  export const Table = ({ children, className }: TableProps) => {
12
12
  return (
13
- <div className={cn("w-full", className)}>
13
+ <div className={cn("w-full overflow-x-auto", className)}>
14
14
  <table className="w-full text-left lg:min-w-[600px] border-separate border-spacing-0 block lg:table">
15
15
  {children}
16
16
  </table>
@@ -6,7 +6,7 @@ import { Dropdown, DropdownItem } from '../base-components/Dropdown';
6
6
 
7
7
  import Link from 'next/link';
8
8
  import { usePathname, useRouter } from 'next/navigation';
9
- import { cn } from '@apptimate/core-lib';
9
+ import { cn, getProjectConstructionGroups } from '@apptimate/core-lib';
10
10
 
11
11
  export type DashboardMenuConfig = {
12
12
  id: string;
@@ -75,7 +75,7 @@ export function DashboardLayout({
75
75
  // Helper to build hierarchy
76
76
  const flattenedOrganizations = React.useMemo(() => {
77
77
  if (!organizations || organizations.length === 0) return [];
78
-
78
+
79
79
  const orgMap = new Map<number, any>();
80
80
  const roots: any[] = [];
81
81
 
@@ -103,14 +103,42 @@ export function DashboardLayout({
103
103
  return flattened;
104
104
  }, [organizations]);
105
105
 
106
+
107
+ // Override menus if it's a project organization
108
+ const effectiveMenus = React.useMemo(() => {
109
+ if (selectedOrganization?.organization_type !== 'project') return menus;
110
+
111
+ const projectMatch = pathname.match(/^(?:\/construction)?\/projects\/(\d+)(?:\/|$)/);
112
+ const projectId = projectMatch ? projectMatch[1] : 'current';
113
+
114
+ return menus.map(menu => {
115
+ if (menu.id === 'construction') {
116
+ return {
117
+ ...menu,
118
+ groups: getProjectConstructionGroups(projectId)
119
+ };
120
+ }
121
+ return menu;
122
+ });
123
+ }, [menus, selectedOrganization, pathname]);
124
+
106
125
  // Find which main menu should be active based on current path.
107
126
  // When basePath is set, prefer the menu whose items are mostly within basePath.
108
127
  const fullPath = basePath + (pathname === "/" && basePath ? "" : pathname);
109
128
  const currentMainMenu = (() => {
110
- let bestMenu = menus[0];
129
+ let bestMenu = effectiveMenus[0];
130
+
131
+ // Default to construction if organization is a project
132
+ if (selectedOrganization?.organization_type === 'project') {
133
+ const constructionMenu = effectiveMenus.find(m => m.id === 'construction');
134
+ if (constructionMenu) {
135
+ bestMenu = constructionMenu;
136
+ }
137
+ }
138
+
111
139
  let bestScore = -1;
112
140
  let bestInternalCount = -1;
113
- for (const menu of menus) {
141
+ for (const menu of effectiveMenus) {
114
142
  for (const group of menu.groups) {
115
143
  for (const item of group.items) {
116
144
  if (fullPath === item.path || fullPath.startsWith(item.path + "/")) {
@@ -138,7 +166,7 @@ export function DashboardLayout({
138
166
  }
139
167
  }, [currentMainMenu?.id]);
140
168
 
141
- const activeMenu = menus.find(m => m.id === activeMenuId) || menus[0];
169
+ const activeMenu = effectiveMenus.find(m => m.id === activeMenuId) || effectiveMenus[0];
142
170
 
143
171
  const handleMainMenuSelect = (menu: DashboardMenuConfig) => {
144
172
  setActiveMenuId(menu.id);
@@ -218,7 +246,7 @@ export function DashboardLayout({
218
246
  }
219
247
  `}} />
220
248
  <nav className="flex flex-col gap-6 flex-1 w-full sidebar-scroll pb-4">
221
- {menus.map((menu) => {
249
+ {effectiveMenus.map((menu) => {
222
250
  const isActive = activeMenuId === menu.id;
223
251
  return (
224
252
  <div
@@ -300,79 +328,79 @@ export function DashboardLayout({
300
328
  <PanelLeftClose size={18} />
301
329
  </button>
302
330
  </div>
303
- <div className="flex-1 overflow-y-auto px-4 space-y-6">
304
- {activeMenu.groups.map((group) => (
305
- <div key={group.id}>
306
- {group.label && (
307
- <h3 className="px-2 text-xs font-bold text-gray-400 uppercase tracking-wider mb-2 empty:hidden">{group.label}</h3>
308
- )}
309
- <nav className="flex flex-col gap-1">
310
- {group.items.map((item) => {
311
- // Find the longest matching path in this group to avoid parent paths being active
312
- const allGroupItems = activeMenu.groups.flatMap(g => g.items);
313
- const matchingItems = allGroupItems.filter(i => fullPath === i.path || fullPath.startsWith(`${i.path}/`));
314
- const longestMatch = matchingItems.sort((a, b) => b.path.length - a.path.length)[0];
315
- const isItemActive = longestMatch?.path === item.path;
316
- const isInternal = basePath
317
- ? item.path.startsWith(basePath)
318
- : !externalPaths.some(ext => item.path.startsWith(ext));
319
- const href = basePath && isInternal ? item.path.replace(basePath, "") || "/" : item.path;
320
-
321
- const className = `px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between ${isItemActive
322
- ? "bg-[#F4F5F7] text-[#2D3142] font-semibold"
323
- : "text-gray-500 font-medium hover:bg-gray-50"
324
- }`;
325
-
326
- const content = (
327
- <>
328
- <div className="flex items-center gap-3">
329
- {item.icon && <span className={`${isItemActive ? 'text-[#2D3142]' : 'text-gray-400'}`}>{item.icon}</span>}
330
- <span>{item.label}</span>
331
- </div>
332
- {item.badge && <div>{item.badge}</div>}
333
- </>
334
- );
335
-
336
- return isInternal ? (
337
- <Link key={item.id} href={href} className={className}>
338
- {content}
339
- </Link>
340
- ) : (
341
- <a key={item.id} href={href} className={className}>
342
- {content}
343
- </a>
344
- );
345
- })}
346
- </nav>
347
- </div>
348
- ))}
349
- </div>
350
-
351
- {/* Organization Selector - Bottom of Secondary Sidebar */}
352
- {organizations.length > 0 && (
353
- <div className="px-4 pt-4 mt-2 border-t border-gray-100">
354
- <button
355
- id="org-selector-desktop"
356
- onClick={() => setIsOrgModalOpen(true)}
357
- className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl bg-[#F4F5F7] hover:bg-gray-200/70 transition-all duration-200 group cursor-pointer"
358
- >
359
- <div className={cn(
360
- "w-8 h-8 rounded-lg flex items-center justify-center shrink-0 shadow-sm",
361
- selectedOrganization?.organization_type === 'project'
362
- ? "bg-gradient-to-br from-teal-400 to-teal-600"
363
- : "bg-gradient-to-br from-indigo-500 to-purple-600"
364
- )}>
365
- <Building2 size={14} className="text-white" />
331
+ <div className="flex-1 overflow-y-auto px-4 space-y-6">
332
+ {activeMenu.groups.map((group) => (
333
+ <div key={group.id}>
334
+ {group.label && (
335
+ <h3 className="px-2 text-xs font-bold text-gray-400 uppercase tracking-wider mb-2 empty:hidden">{group.label}</h3>
336
+ )}
337
+ <nav className="flex flex-col gap-1">
338
+ {group.items.map((item) => {
339
+ // Find the longest matching path in this group to avoid parent paths being active
340
+ const allGroupItems = activeMenu.groups.flatMap(g => g.items);
341
+ const matchingItems = allGroupItems.filter(i => fullPath === i.path || fullPath.startsWith(`${i.path}/`));
342
+ const longestMatch = matchingItems.sort((a, b) => b.path.length - a.path.length)[0];
343
+ const isItemActive = longestMatch?.path === item.path;
344
+ const isInternal = basePath
345
+ ? item.path.startsWith(basePath)
346
+ : !externalPaths.some(ext => item.path.startsWith(ext));
347
+ const href = basePath && isInternal ? item.path.replace(basePath, "") || "/" : item.path;
348
+
349
+ const className = `px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between ${isItemActive
350
+ ? "bg-[#F4F5F7] text-[#2D3142] font-semibold"
351
+ : "text-gray-500 font-medium hover:bg-gray-50"
352
+ }`;
353
+
354
+ const content = (
355
+ <>
356
+ <div className="flex items-center gap-3">
357
+ {item.icon && <span className={`${isItemActive ? 'text-[#2D3142]' : 'text-gray-400'}`}>{item.icon}</span>}
358
+ <span>{item.label}</span>
359
+ </div>
360
+ {item.badge && <div>{item.badge}</div>}
361
+ </>
362
+ );
363
+
364
+ return isInternal ? (
365
+ <Link key={item.id} href={href} className={className}>
366
+ {content}
367
+ </Link>
368
+ ) : (
369
+ <a key={item.id} href={href} className={className}>
370
+ {content}
371
+ </a>
372
+ );
373
+ })}
374
+ </nav>
366
375
  </div>
367
- <div className="flex-1 min-w-0 text-left">
368
- <p className="text-[11px] font-semibold text-gray-400 uppercase tracking-wider leading-none mb-0.5">Organization</p>
369
- <p className="text-[13px] font-bold text-[#2D3142] truncate leading-tight">{orgDisplayName}</p>
370
- </div>
371
- <ChevronRight size={14} className="text-gray-400 group-hover:text-[#2D3142] transition-colors shrink-0" />
372
- </button>
376
+ ))}
373
377
  </div>
374
- )}
375
- </aside>
378
+
379
+ {/* Organization Selector - Bottom of Secondary Sidebar */}
380
+ {organizations.length > 0 && (
381
+ <div className="px-4 pt-4 mt-2 border-t border-gray-100">
382
+ <button
383
+ id="org-selector-desktop"
384
+ onClick={() => setIsOrgModalOpen(true)}
385
+ className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl bg-[#F4F5F7] hover:bg-gray-200/70 transition-all duration-200 group cursor-pointer"
386
+ >
387
+ <div className={cn(
388
+ "w-8 h-8 rounded-lg flex items-center justify-center shrink-0 shadow-sm",
389
+ selectedOrganization?.organization_type === 'project'
390
+ ? "bg-gradient-to-br from-teal-400 to-teal-600"
391
+ : "bg-gradient-to-br from-indigo-500 to-purple-600"
392
+ )}>
393
+ <Building2 size={14} className="text-white" />
394
+ </div>
395
+ <div className="flex-1 min-w-0 text-left">
396
+ <p className="text-[11px] font-semibold text-gray-400 uppercase tracking-wider leading-none mb-0.5">Organization</p>
397
+ <p className="text-[13px] font-bold text-[#2D3142] truncate leading-tight">{orgDisplayName}</p>
398
+ </div>
399
+ <ChevronRight size={14} className="text-gray-400 group-hover:text-[#2D3142] transition-colors shrink-0" />
400
+ </button>
401
+ </div>
402
+ )}
403
+ </aside>
376
404
 
377
405
  </>
378
406
  )}
@@ -451,7 +479,7 @@ export function DashboardLayout({
451
479
 
452
480
  <div className="flex-1 overflow-y-auto py-6 px-4">
453
481
  <nav className="flex flex-col gap-8">
454
- {menus.map((menu) => (
482
+ {effectiveMenus.map((menu) => (
455
483
  <div key={menu.id}>
456
484
  <div className="flex items-center gap-3 text-[#2D3142] font-bold mb-3 px-2">
457
485
  {React.cloneElement(menu.icon as React.ReactElement<any>, {
@@ -567,7 +595,7 @@ export function DashboardLayout({
567
595
  const isSelected = selectedOrganization?.id === org.id;
568
596
  const pl = org._level > 0 ? org._level * 24 : 0;
569
597
  const isProject = org.organization_type === 'project';
570
-
598
+
571
599
  return (
572
600
  <button
573
601
  key={org.id}