@orsetra/shared-ui 1.3.0 → 1.3.2

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.
@@ -16,6 +16,7 @@ interface PageWithSidePanelProps {
16
16
  showBorder?: boolean
17
17
  onClose?: () => void
18
18
  onOpen?: () => void
19
+ className?: string
19
20
  contentHeaderClassName?: string
20
21
  sidePanelClassName?: string
21
22
  sidePanelHeaderClassName?: string
@@ -38,6 +39,7 @@ export function PageWithSidePanel({
38
39
  showBorder = true,
39
40
  onClose,
40
41
  onOpen,
42
+ className,
41
43
  contentHeaderClassName,
42
44
  sidePanelClassName,
43
45
  sidePanelHeaderClassName,
@@ -56,7 +58,7 @@ export function PageWithSidePanel({
56
58
  }
57
59
 
58
60
  return (
59
- <div className="flex items-start">
61
+ <div className={cn("flex items-start", className)}>
60
62
  {/* Main content column — grows to fill space left by the panel */}
61
63
  <div className="flex-1 min-w-0 flex flex-col">
62
64
 
@@ -71,9 +73,7 @@ export function PageWithSidePanel({
71
73
  )}
72
74
 
73
75
  {/* Content body */}
74
- <div>
75
76
  {children}
76
- </div>
77
77
  </div>
78
78
 
79
79
  {/* Side panel — sticky in the flex row, stays anchored while content scrolls.
@@ -18,17 +18,10 @@ import {
18
18
  TooltipProvider,
19
19
  TooltipTrigger,
20
20
  } from "../../ui/tooltip"
21
- import {
22
- Settings,
23
- Menu,
24
- User,
25
- ChevronUp,
26
- ChevronRight,
27
- } from "lucide-react"
21
+ import { Menu } from "lucide-react"
28
22
  import { Logo } from "../../ui/logo"
29
23
  import { type SidebarMenus, type SubMenuItem } from "./data"
30
24
  import { Skeleton } from "../skeleton"
31
- import { useUserContext } from "../../../context/user-context"
32
25
 
33
26
  const SIDEBAR_COOKIE_NAME = "sidebar:state"
34
27
  const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
@@ -182,22 +175,6 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
182
175
  const pathname = usePathname()
183
176
  const searchParams = useSearchParams()
184
177
  const { state } = useSidebar()
185
- const [settingsOpen, setSettingsOpen] = React.useState(false)
186
- const settingsRef = React.useRef<HTMLDivElement>(null)
187
- const { organization: orgData } = useUserContext()
188
-
189
- // Close dropdown on click outside
190
- React.useEffect(() => {
191
- function handleClickOutside(event: MouseEvent) {
192
- if (settingsRef.current && !settingsRef.current.contains(event.target as Node)) {
193
- setSettingsOpen(false)
194
- }
195
- }
196
- if (settingsOpen) {
197
- document.addEventListener("mousedown", handleClickOutside)
198
- return () => document.removeEventListener("mousedown", handleClickOutside)
199
- }
200
- }, [settingsOpen])
201
178
 
202
179
  // Micro apps: utilise 'items' comme clé standard
203
180
  // Main app: utilise currentMenu pour sélectionner le bon groupe
@@ -267,64 +244,6 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
267
244
  })}
268
245
  </nav>
269
246
 
270
- {/* Footer - Menu Paramètres */}
271
- <div className="relative border-t border-ui-border px-3 py-3" ref={settingsRef}>
272
- {/* Dropdown vers le haut */}
273
- {settingsOpen && (
274
- <div className="absolute bottom-full left-0 mb-3 bg-white border border-ui-border shadow-2xl z-50 min-w-[380px]">
275
- {/* Flèche pointant vers le bas en direction du bouton Paramètres */}
276
- <div className="absolute -bottom-[5px] left-6 w-[10px] h-[10px] bg-white border-r border-b border-ui-border rotate-45" />
277
-
278
- {/* Organisation */}
279
- <Link
280
- href={`${main_base_url}/organization`}
281
- onClick={() => setSettingsOpen(false)}
282
- className="flex items-center gap-4 px-5 py-4 hover:bg-ui-background transition-colors border-b border-ui-border no-underline"
283
- >
284
- {orgData?.logo ? (
285
- <img src={orgData.logo} alt={orgData.name } className="h-14 w-14 object-cover flex-shrink-0" />
286
- ) : (
287
- <div className="h-14 w-14 bg-interactive flex items-center justify-center text-white text-2xl font-bold flex-shrink-0">
288
- {orgData?.name?.charAt(0)?.toUpperCase() ?? 'O'}
289
- </div>
290
- )}
291
- <div className="flex flex-col min-w-0 flex-1">
292
- <span className="text-xs text-text-secondary uppercase tracking-wide mb-1">Organisation</span>
293
- <span className="text-base font-semibold text-text-primary truncate">
294
- {orgData?.name ?? 'Organisation'}
295
- </span>
296
- </div>
297
- <ChevronRight className="h-4 w-4 text-text-secondary flex-shrink-0 opacity-40" />
298
- </Link>
299
-
300
- {/* Paramètres du compte */}
301
- <Link
302
- href={`${main_base_url}/profile`}
303
- onClick={() => setSettingsOpen(false)}
304
- className="flex items-center gap-3 px-5 py-3 text-sm text-text-secondary hover:bg-ui-background hover:text-text-primary transition-colors no-underline"
305
- >
306
- <User className="h-5 w-5 flex-shrink-0" />
307
- <span className="flex-1">Paramètres du compte</span>
308
- <ChevronRight className="h-4 w-4 flex-shrink-0 opacity-40" />
309
- </Link>
310
- </div>
311
- )}
312
- <button
313
- onClick={() => setSettingsOpen(!settingsOpen)}
314
- className={cn(
315
- "flex items-center justify-between w-full px-3 py-2 text-sm rounded-none gap-x-3 transition-colors",
316
- settingsOpen
317
- ? "bg-interactive/10 text-interactive font-medium"
318
- : "text-text-secondary hover:bg-ui-background hover:text-text-primary"
319
- )}
320
- >
321
- <span className="flex items-center gap-x-3">
322
- <Settings className="h-4 w-4" />
323
- {state === "expanded" && "Paramètres"}
324
- </span>
325
- {state === "expanded" && <ChevronUp className={cn("h-4 w-4 transition-transform", settingsOpen ? "rotate-0" : "rotate-180")} />}
326
- </button>
327
- </div>
328
247
  </div>
329
248
  )
330
249
  }
@@ -1,17 +1,18 @@
1
1
  "use client"
2
2
 
3
3
  import { useState } from "react"
4
- import { ChevronDown, Settings, LogOut } from "lucide-react"
4
+ import { ChevronRight, LogOut } from "lucide-react"
5
5
  import ReactAvatar from "react-avatar"
6
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
6
7
  import { useUserContext } from "../../context/user-context"
7
8
  import {
8
- Sheet,
9
- SheetContent,
10
- SheetTitle,
11
- SheetTrigger,
12
- } from "../ui/sheet"
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuTrigger,
12
+ } from "../ui/dropdown-menu"
13
13
  import { Button } from "../ui"
14
- import { cn } from "../../lib/utils"
14
+
15
+ const Divider = () => <div className="h-px bg-gray-200 w-full" />
15
16
 
16
17
  interface UserPanelProps {
17
18
  main_base_url: string
@@ -19,18 +20,20 @@ interface UserPanelProps {
19
20
 
20
21
  export function UserPanel({ main_base_url }: UserPanelProps) {
21
22
  const [open, setOpen] = useState(false)
22
- const { profile, currentProject, organization, logout } = useUserContext()
23
+ const { profile, currentProject, organization, logout, openBusinessUnitSwitcher } = useUserContext()
23
24
 
24
25
  const displayName =
25
26
  profile?.name || profile?.preferred_username || profile?.email || "User"
26
27
 
28
+ const close = () => setOpen(false)
29
+
27
30
  return (
28
- <Sheet open={open} onOpenChange={setOpen}>
29
- <SheetTrigger asChild>
31
+ <DropdownMenu open={open} onOpenChange={setOpen}>
32
+ <DropdownMenuTrigger asChild>
30
33
  <Button
31
34
  variant="ghost"
32
35
  size="sm"
33
- className="flex items-center gap-1 h-9 px-2"
36
+ className="flex items-center h-9 px-2"
34
37
  style={{ borderRadius: 0 }}
35
38
  >
36
39
  <ReactAvatar
@@ -41,84 +44,102 @@ export function UserPanel({ main_base_url }: UserPanelProps) {
41
44
  color="#0f62fe"
42
45
  textSizeRatio={2}
43
46
  />
44
- <ChevronDown
45
- className={cn(
46
- "h-4 w-4 text-gray-500 transition-transform duration-150",
47
- open && "rotate-180"
48
- )}
49
- />
50
47
  </Button>
51
- </SheetTrigger>
48
+ </DropdownMenuTrigger>
52
49
 
53
- <SheetContent side="right" className="w-72 p-0 flex flex-col">
54
- <SheetTitle className="sr-only">Menu utilisateur</SheetTitle>
50
+ <DropdownMenuContent
51
+ align="end"
52
+ sideOffset={8}
53
+ className="w-80 bg-white shadow-xl border border-gray-200 p-0 rounded-none"
54
+ >
55
+ <DropdownMenuPrimitive.Arrow width={16} height={8} style={{ fill: 'white', stroke: '#e5e7eb', strokeWidth: 1 }} />
56
+
57
+ {/* Organisation | Business Unit */}
58
+ <div className="flex items-stretch">
59
+
60
+ {/* Gauche — Organisation */}
61
+ <a
62
+ href={`${main_base_url}/organization`}
63
+ onClick={close}
64
+ className="flex items-center gap-2 flex-1 min-w-0 px-4 py-4 hover:bg-gray-50 transition-colors no-underline group"
65
+ >
66
+ <div className="flex flex-col min-w-0 flex-1">
67
+ <span className="text-[11px] text-gray-400 uppercase tracking-widest mb-1">Organisation</span>
68
+ <span className="text-sm font-semibold text-gray-900 truncate">
69
+ {organization?.name ?? '—'}
70
+ </span>
71
+ </div>
72
+ <ChevronRight className="h-4 w-4 text-gray-300 flex-shrink-0 group-hover:text-gray-500 transition-colors" />
73
+ </a>
74
+
75
+ {/* Séparateur vertical */}
76
+ <div className="flex items-center py-3">
77
+ <div className="w-px h-full bg-gray-200" />
78
+ </div>
79
+
80
+ {/* Droite — Business Unit */}
81
+ <div className="flex flex-col justify-center flex-1 min-w-0 px-4 py-4">
82
+ <span className="text-[11px] text-gray-400 uppercase tracking-widest mb-1">Business Unit</span>
83
+ <span className="text-sm font-semibold text-gray-900 truncate">
84
+ {currentProject?.name ?? '—'}
85
+ </span>
86
+ <div className="flex items-center gap-3 mt-1.5">
87
+ <button
88
+ onClick={() => { close(); openBusinessUnitSwitcher() }}
89
+ className="text-xs text-[#0f62fe] hover:underline"
90
+ >
91
+ Switch
92
+ </button>
93
+ {currentProject && organization && (
94
+ <a
95
+ href={`${main_base_url}/${organization.id}/projects/${currentProject.id}`}
96
+ onClick={close}
97
+ className="text-xs text-[#0f62fe] hover:underline"
98
+ >
99
+ Settings
100
+ </a>
101
+ )}
102
+ </div>
103
+ </div>
55
104
 
56
- {/* Header — Business Unit */}
57
- <div className="flex items-center justify-between px-4 py-3 border-b border-ui-border">
58
- <span className="font-semibold text-sm text-text-primary truncate">
59
- {currentProject?.name ?? "—"}
60
- </span>
61
- {currentProject && organization && (
62
- <a
63
- href={`${main_base_url}/${organization.id}/projects/${currentProject.id}`}
64
- className="text-text-secondary hover:text-text-primary ml-2 flex-shrink-0"
65
- onClick={() => setOpen(false)}
66
- title="Paramètres du projet"
67
- >
68
- <Settings className="h-4 w-4" />
69
- </a>
70
- )}
71
105
  </div>
72
106
 
73
- {/* User info */}
74
- <div className="flex items-start gap-3 px-4 py-5">
107
+ <Divider />
108
+
109
+ {/* User info + Logout */}
110
+ <div className="flex items-center gap-3 px-4 py-3">
75
111
  <ReactAvatar
76
112
  name={displayName}
77
113
  src={profile?.avatar}
78
- size="48"
114
+ size="40"
79
115
  round
80
116
  color="#0f62fe"
81
117
  textSizeRatio={2}
82
118
  />
83
- <div className="flex flex-col min-w-0 justify-center">
84
- <span className="font-bold text-sm text-text-primary truncate">
85
- {displayName}
86
- </span>
119
+ <div className="flex flex-col min-w-0 flex-1">
120
+ <span className="font-bold text-sm text-gray-900 truncate">{displayName}</span>
87
121
  {profile?.email && (
88
- <span className="text-xs text-text-secondary truncate">
89
- {profile.email}
90
- </span>
122
+ <span className="text-xs text-gray-500 truncate">{profile.email}</span>
91
123
  )}
124
+ <a
125
+ href={`${main_base_url}/profile`}
126
+ onClick={close}
127
+ className="text-xs text-[#0f62fe] hover:underline mt-0.5 w-fit"
128
+ >
129
+ Settings
130
+ </a>
92
131
  </div>
93
- </div>
94
-
95
- <div className="border-t border-ui-border" />
96
-
97
- {/* Account settings */}
98
- <div className="px-4 py-3">
99
- <a
100
- href={`${main_base_url}/profile`}
101
- className="text-sm text-text-primary hover:underline"
102
- onClick={() => setOpen(false)}
103
- >
104
- Paramètres du compte
105
- </a>
106
- </div>
107
-
108
- {/* Spacer */}
109
- <div className="flex-1" />
110
-
111
- {/* Footer — Logout */}
112
- <div className="border-t border-ui-border px-4 py-3">
113
132
  <button
114
- onClick={() => { setOpen(false); logout() }}
115
- className="flex items-center gap-2 text-sm text-red-600 hover:text-red-700 w-full"
133
+ onClick={() => { close(); logout() }}
134
+ className="flex items-center gap-1.5 text-xs text-red-600 hover:text-red-700 flex-shrink-0 ml-2"
135
+ title="Logout"
116
136
  >
117
- <LogOut className="h-4 w-4" />
118
- Logout
137
+ <LogOut className="h-3.5 w-3.5" />
138
+ <span>Logout</span>
119
139
  </button>
120
140
  </div>
121
- </SheetContent>
122
- </Sheet>
141
+
142
+ </DropdownMenuContent>
143
+ </DropdownMenu>
123
144
  )
124
145
  }
@@ -21,23 +21,21 @@ import {
21
21
  import { Loader2, FolderKanban } from "lucide-react"
22
22
 
23
23
  export interface Project {
24
- id: string,
24
+ id: string
25
25
  name: string
26
26
  alias?: string
27
27
  description?: string
28
28
  }
29
29
 
30
- export interface ProjectSelectorModalProps {
31
- open: boolean
32
- onOpenChange?: (open: boolean) => void
30
+ export type ProjectSelectorProps = {
33
31
  getProjects: () => Promise<Project[]>
34
32
  createProject: (name: string) => Promise<Project>
35
- storage: {
36
- getItem: (key: string) => string | null
37
- setItem: (key: string, value: string) => void
38
- }
39
- storageKey: string
40
- doInit: () => void | Promise<void>
33
+ doInit: (id: string) => void | Promise<void>
34
+ }
35
+
36
+ export interface ProjectSelectorModalProps extends ProjectSelectorProps {
37
+ open: boolean
38
+ onOpenChange?: (open: boolean) => void
41
39
  title?: string
42
40
  description?: string
43
41
  }
@@ -45,13 +43,11 @@ export interface ProjectSelectorModalProps {
45
43
  export function ProjectSelectorModal({
46
44
  open,
47
45
  onOpenChange,
46
+ title = "Select Business Unit",
47
+ description = "Choose a business unit to continue. This will be used for all operations in this application.",
48
48
  getProjects,
49
49
  createProject,
50
- storage,
51
- storageKey,
52
50
  doInit,
53
- title = "Select Business Unit",
54
- description = "Choose a business unit to continue. This will be used for all operations in this application.",
55
51
  }: ProjectSelectorModalProps) {
56
52
  const [projects, setProjects] = React.useState<Project[]>([])
57
53
  const [loading, setLoading] = React.useState(true)
@@ -63,13 +59,8 @@ export function ProjectSelectorModal({
63
59
  React.useEffect(() => {
64
60
  if (open) {
65
61
  loadProjects()
66
- // Try to load current project from storage
67
- const currentProject = storage.getItem(storageKey)
68
- if (currentProject) {
69
- setSelectedProject(currentProject)
70
- }
71
62
  }
72
- }, [open, storage, storageKey])
63
+ }, [open])
73
64
 
74
65
  const loadProjects = async () => {
75
66
  setLoading(true)
@@ -107,11 +98,8 @@ export function ProjectSelectorModal({
107
98
  // Create the new project
108
99
  const newProject = await createProject(newProjectName.trim())
109
100
 
110
- // Save to storage
111
- storage.setItem(storageKey, newProject.name)
112
-
113
101
  // Call doInit
114
- await Promise.resolve(doInit())
102
+ await Promise.resolve(doInit(newProject.id))
115
103
 
116
104
  // Close modal
117
105
  onOpenChange?.(false)
@@ -132,11 +120,8 @@ export function ProjectSelectorModal({
132
120
  setError(null)
133
121
 
134
122
  try {
135
- // Save to storage
136
- storage.setItem(storageKey, selectedProject)
137
-
138
123
  // Call doInit
139
- await Promise.resolve(doInit())
124
+ await Promise.resolve(doInit(selectedProject))
140
125
 
141
126
  // Close modal
142
127
  onOpenChange?.(false)
@@ -54,7 +54,7 @@ export function StringsInput({
54
54
  <div className="space-y-2">
55
55
  {items.map((item, index) => (
56
56
  <div key={index} className="flex items-center gap-2">
57
- <span className="flex-1 text-sm px-3 py-2 border border-ibm-gray-20 bg-ibm-gray-10 text-ibm-gray-100 truncate">
57
+ <span className="flex-1 text-sm px-3 h-7 flex items-center border border-ibm-gray-20 bg-ibm-gray-10 text-ibm-gray-100 truncate">
58
58
  {item}
59
59
  </span>
60
60
  <Button
@@ -62,9 +62,9 @@ export function StringsInput({
62
62
  variant="secondary"
63
63
  onClick={() => handleRemove(index)}
64
64
  disabled={disabled}
65
- className="rounded-none h-9 w-9 p-0 shrink-0"
65
+ className="rounded-none h-7 w-7 p-0 shrink-0"
66
66
  >
67
- <X className="h-4 w-4" />
67
+ <X className="h-3.5 w-3.5" />
68
68
  </Button>
69
69
  </div>
70
70
  ))}
@@ -75,18 +75,16 @@ export function StringsInput({
75
75
  onKeyDown={handleKeyDown}
76
76
  disabled={disabled}
77
77
  placeholder={placeholder}
78
- className="rounded-none flex-1"
78
+ className="rounded-none flex-1 h-7 text-sm"
79
79
  />
80
80
  <Button
81
81
  type="button"
82
82
  variant="secondary"
83
- leftIcon={<Plus className="h-4 w-4 mr-1" />}
84
83
  onClick={handleAdd}
85
84
  disabled={disabled || !inputValue.trim()}
86
- className="rounded-none shrink-0"
85
+ className="rounded-none h-7 w-7 p-0 shrink-0"
87
86
  >
88
-
89
- Add
87
+ <Plus className="h-3.5 w-3.5" />
90
88
  </Button>
91
89
  </div>
92
90
  </div>
@@ -0,0 +1,24 @@
1
+ import { createContext, useContext } from "react"
2
+
3
+ export interface Project {
4
+ id: string
5
+ name: string
6
+ alias?: string
7
+ description?: string
8
+ }
9
+
10
+ export interface ProjectSelectorContextType {
11
+ getProjects: () => Promise<Project[]>
12
+ createProject: (name: string) => Promise<Project>
13
+ doInit: () => void | Promise<void>
14
+ }
15
+
16
+ export const ProjectSelectorContext = createContext<ProjectSelectorContextType | undefined>(undefined)
17
+
18
+ export const useProjectSelector = () => {
19
+ const context = useContext(ProjectSelectorContext)
20
+ if (!context) {
21
+ throw new Error('useProjectSelector must be used within a ProjectSelectorProvider')
22
+ }
23
+ return context
24
+ }
@@ -16,6 +16,7 @@ interface UserContextValue {
16
16
  organization: OrgInfo | null
17
17
  logout: () => void
18
18
  switchBusinessUnit: (id: string) => void
19
+ openBusinessUnitSwitcher: () => void
19
20
  }
20
21
 
21
22
  const UserContext = createContext<UserContextValue | null>(null)
@@ -33,6 +34,7 @@ interface UserContextProviderProps {
33
34
  organization?: OrgInfo | null
34
35
  onSignOut?: () => void
35
36
  onProjectChange?: (id: string) => void
37
+ onOpenBusinessUnitSwitcher?: () => void
36
38
  }
37
39
 
38
40
  export function UserContextProvider({
@@ -42,6 +44,7 @@ export function UserContextProvider({
42
44
  organization = null,
43
45
  onSignOut,
44
46
  onProjectChange,
47
+ onOpenBusinessUnitSwitcher,
45
48
  }: UserContextProviderProps) {
46
49
  return (
47
50
  <UserContext.Provider value={{
@@ -50,6 +53,7 @@ export function UserContextProvider({
50
53
  organization,
51
54
  logout: onSignOut ?? (() => {}),
52
55
  switchBusinessUnit: onProjectChange ?? (() => {}),
56
+ openBusinessUnitSwitcher: onOpenBusinessUnitSwitcher ?? (() => {}),
53
57
  }}>
54
58
  {children}
55
59
  </UserContext.Provider>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",