@orsetra/shared-ui 1.3.1 → 1.3.3

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.
@@ -3,38 +3,41 @@
3
3
  import { ArrowLeft } from "lucide-react"
4
4
  import Link from "next/link"
5
5
  import type { ReactNode, ElementType } from "react"
6
-
7
- const TAB_ACTIVE =
8
- "border-b-2 border-ibm-blue-60 text-ibm-blue-60 bg-ibm-blue-10/60"
9
- const TAB_INACTIVE =
10
- "border-b-2 border-transparent text-ibm-gray-60 hover:text-ibm-blue-60 hover:bg-ibm-blue-10/40"
6
+ import { cn } from "../../lib/utils"
11
7
 
12
8
  export interface DetailPageHeaderTab {
13
9
  id: string
14
10
  label: string
15
11
  icon?: ElementType
16
- href: string
12
+ /** Required when onTabChange is not provided (URL-based navigation) */
13
+ href?: string
17
14
  }
18
15
 
19
16
  export interface DetailPageHeaderProps {
20
17
  /** Back breadcrumb link destination */
21
- backHref: string
18
+ backHref?: string
22
19
  /** Back breadcrumb label */
23
- backLabel: string
20
+ backLabel?: string
24
21
  /** Optional icon rendered in a blue square container */
25
22
  icon?: ReactNode
26
- /** Page title – truncated to one line */
23
+ /** Page title */
27
24
  title: string
28
- /** Optional subtitle – truncated to one line */
25
+ /** Optional subtitle */
29
26
  description?: string
30
- /** Slot for badges, status buttons, action buttons placed to the right */
27
+ /** Slot for badges or status nodes placed next to the title */
28
+ statusNode?: ReactNode
29
+ /** Slot for action buttons placed to the right */
31
30
  actions?: ReactNode
32
- /** Optional tab navigation rendered at the bottom of the header */
31
+ /** Optional tab navigation */
33
32
  tabBar?: DetailPageHeaderTab[]
34
33
  /** Alignment of the tab bar – defaults to "left" */
35
34
  tabBarPosition?: "left" | "right"
36
35
  /** Currently active tab id */
37
36
  activeTab?: string
37
+ /** State-based tab change handler. When provided, tabs render as buttons instead of Links */
38
+ onTabChange?: (id: string) => void
39
+ /** Extra classes on the root element – use to adjust spacing/height */
40
+ className?: string
38
41
  }
39
42
 
40
43
  export function DetailPageHeader({
@@ -43,79 +46,94 @@ export function DetailPageHeader({
43
46
  icon,
44
47
  title,
45
48
  description,
49
+ statusNode,
46
50
  actions,
47
51
  tabBar,
48
52
  tabBarPosition = "left",
49
53
  activeTab,
54
+ onTabChange,
55
+ className,
50
56
  }: DetailPageHeaderProps) {
51
57
  return (
52
- <div className="bg-white border-b border-ibm-gray-20">
53
- <div className="px-4 sm:px-6 pt-4 pb-0">
58
+ <div className={cn("mb-4 sm:mb-6", className)}>
54
59
 
55
- {/* Breadcrumb */}
60
+ {/* Back link */}
61
+ {backHref && backLabel && (
56
62
  <Link
57
63
  href={backHref}
58
- className="inline-flex items-center gap-1.5 text-xs text-ibm-gray-50 hover:text-ibm-blue-60 transition-colors mb-3"
64
+ className="inline-flex items-center gap-1.5 text-xs text-text-secondary hover:text-ibm-blue-60 transition-colors mb-3"
59
65
  >
60
- <ArrowLeft className="h-3 w-3" />
66
+ <ArrowLeft className="h-3.5 w-3.5" />
61
67
  {backLabel}
62
68
  </Link>
69
+ )}
63
70
 
64
- {/* Title row */}
65
- <div className="flex items-center justify-between gap-4 pb-4">
66
- <div className="flex items-center gap-3 min-w-0">
67
- {icon && (
68
- <div className="h-9 w-9 rounded bg-ibm-blue-10 flex items-center justify-center flex-shrink-0">
69
- {icon}
70
- </div>
71
- )}
72
- <div className="min-w-0">
73
- <h1 className="text-lg font-semibold text-ibm-gray-100 leading-tight truncate">
74
- {title}
75
- </h1>
76
- {description && (
77
- <p className="text-xs text-ibm-gray-50 mt-0.5 truncate">
78
- {description}
79
- </p>
80
- )}
81
- </div>
82
- </div>
83
-
84
- {actions && (
85
- <div className="flex items-center gap-2 shrink-0">
86
- {actions}
71
+ {/* Title row */}
72
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
73
+ <div className="flex items-center gap-3 min-w-0">
74
+ {icon && (
75
+ <div className="h-9 w-9 rounded bg-ibm-blue-10 flex items-center justify-center flex-shrink-0">
76
+ {icon}
87
77
  </div>
88
78
  )}
79
+ <div className="min-w-0">
80
+ <div className="flex items-center gap-3">
81
+ <h1 className="text-xl sm:text-2xl font-semibold text-text-primary truncate">{title}</h1>
82
+ {statusNode && <div className="flex-shrink-0">{statusNode}</div>}
83
+ </div>
84
+ {description && (
85
+ <p className="text-sm text-text-secondary line-clamp-2 mt-0.5">{description}</p>
86
+ )}
87
+ </div>
89
88
  </div>
90
89
 
91
- {/* Tab bar */}
92
- {tabBar && tabBar.length > 0 && (
93
- <nav
94
- className={`flex gap-0 overflow-x-auto -mx-4 sm:-mx-6 px-4 sm:px-6 ${
95
- tabBarPosition === "right" ? "justify-end" : "justify-start"
96
- }`}
97
- aria-label="Tabs"
98
- >
90
+ {actions && (
91
+ <div className="flex items-center gap-2 shrink-0">{actions}</div>
92
+ )}
93
+ </div>
94
+
95
+ {/* Tab bar */}
96
+ {tabBar && tabBar.length > 0 && (
97
+ <div className={cn("mt-4 flex", tabBarPosition === "right" ? "justify-end" : "justify-start")}>
98
+ <div className="inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground">
99
99
  {tabBar.map((tab) => {
100
100
  const Icon = tab.icon
101
101
  const isActive = activeTab === tab.id
102
+ const triggerClass = cn(
103
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
104
+ isActive
105
+ ? "bg-white text-ibm-gray-100 shadow-sm"
106
+ : "hover:text-ibm-gray-100"
107
+ )
108
+
109
+ if (onTabChange) {
110
+ return (
111
+ <button
112
+ key={tab.id}
113
+ type="button"
114
+ onClick={() => onTabChange(tab.id)}
115
+ className={triggerClass}
116
+ >
117
+ {Icon && <Icon className="h-4 w-4" />}
118
+ {tab.label}
119
+ </button>
120
+ )
121
+ }
122
+
102
123
  return (
103
124
  <Link
104
125
  key={tab.id}
105
- href={tab.href}
106
- className={`inline-flex items-center gap-2 px-5 py-2.5 text-sm font-medium transition-colors whitespace-nowrap ${
107
- isActive ? TAB_ACTIVE : TAB_INACTIVE
108
- }`}
126
+ href={tab.href ?? "#"}
127
+ className={triggerClass}
109
128
  >
110
129
  {Icon && <Icon className="h-4 w-4" />}
111
130
  {tab.label}
112
131
  </Link>
113
132
  )
114
133
  })}
115
- </nav>
116
- )}
117
-
118
- </div>
134
+ </div>
135
+ </div>
136
+ )}
119
137
  </div>
120
138
  )
121
139
  }
@@ -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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",