@orsetra/shared-ui 1.3.2 → 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.
|
@@ -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
|
-
|
|
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
|
|
18
|
+
backHref?: string
|
|
22
19
|
/** Back breadcrumb label */
|
|
23
|
-
backLabel
|
|
20
|
+
backLabel?: string
|
|
24
21
|
/** Optional icon rendered in a blue square container */
|
|
25
22
|
icon?: ReactNode
|
|
26
|
-
/** Page title
|
|
23
|
+
/** Page title */
|
|
27
24
|
title: string
|
|
28
|
-
/** Optional subtitle
|
|
25
|
+
/** Optional subtitle */
|
|
29
26
|
description?: string
|
|
30
|
-
/** Slot for badges
|
|
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
|
|
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="
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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={
|
|
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
|
-
</
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
119
137
|
</div>
|
|
120
138
|
)
|
|
121
139
|
}
|