@orsetra/shared-ui 1.3.7 → 1.3.9
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/components/ui/detail-page-header.tsx +73 -106
- package/package.json +1 -1
|
@@ -3,150 +3,117 @@
|
|
|
3
3
|
import { ArrowLeft } from "lucide-react"
|
|
4
4
|
import Link from "next/link"
|
|
5
5
|
import type { ReactNode, ElementType } from "react"
|
|
6
|
-
|
|
7
|
-
|
|
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"
|
|
8
11
|
|
|
9
12
|
export interface DetailPageHeaderTab {
|
|
10
13
|
id: string
|
|
11
14
|
label: string
|
|
12
15
|
icon?: ElementType
|
|
13
|
-
|
|
14
|
-
href?: string
|
|
15
|
-
/** State-based or programmatic navigation */
|
|
16
|
-
action?: () => void
|
|
16
|
+
href: string
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface DetailPageHeaderProps {
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
|
|
20
|
+
/** Back breadcrumb link destination */
|
|
21
|
+
backHref: string
|
|
22
|
+
/** Back breadcrumb label */
|
|
23
|
+
backLabel: string
|
|
24
24
|
/** Optional icon rendered in a blue square container */
|
|
25
25
|
icon?: ReactNode
|
|
26
|
-
/** Page title */
|
|
26
|
+
/** Page title – truncated to one line */
|
|
27
27
|
title: string
|
|
28
|
-
/** Optional subtitle */
|
|
28
|
+
/** Optional subtitle – truncated to one line */
|
|
29
29
|
description?: string
|
|
30
|
-
/** Slot for badges
|
|
31
|
-
statusNode?: ReactNode
|
|
32
|
-
/** Slot for action buttons placed to the right, after the back button */
|
|
30
|
+
/** Slot for badges, status buttons, action buttons placed to the right */
|
|
33
31
|
actions?: ReactNode
|
|
34
|
-
/** Optional tab navigation */
|
|
32
|
+
/** Optional tab navigation rendered at the bottom of the header */
|
|
35
33
|
tabBar?: DetailPageHeaderTab[]
|
|
36
34
|
/** Alignment of the tab bar – defaults to "left" */
|
|
37
35
|
tabBarPosition?: "left" | "right"
|
|
38
36
|
/** Currently active tab id */
|
|
39
37
|
activeTab?: string
|
|
40
|
-
/** Extra classes on the root element – use to adjust spacing/height */
|
|
41
|
-
className?: string
|
|
42
38
|
}
|
|
43
39
|
|
|
44
40
|
export function DetailPageHeader({
|
|
45
|
-
backText,
|
|
46
41
|
backHref,
|
|
42
|
+
backLabel,
|
|
47
43
|
icon,
|
|
48
44
|
title,
|
|
49
45
|
description,
|
|
50
|
-
statusNode,
|
|
51
46
|
actions,
|
|
52
47
|
tabBar,
|
|
53
48
|
tabBarPosition = "left",
|
|
54
49
|
activeTab,
|
|
55
|
-
className,
|
|
56
50
|
}: DetailPageHeaderProps) {
|
|
57
51
|
return (
|
|
58
|
-
<div className=
|
|
52
|
+
<div className="bg-white border-b border-ibm-gray-20">
|
|
53
|
+
<div className="px-4 sm:px-6 pt-4 pb-0">
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<div className="min-w-0">
|
|
69
|
-
<div className="flex items-center gap-3">
|
|
70
|
-
<h1 className="text-xl sm:text-2xl font-semibold text-text-primary truncate">{title}</h1>
|
|
71
|
-
{statusNode && <div className="flex-shrink-0">{statusNode}</div>}
|
|
72
|
-
</div>
|
|
73
|
-
{description && (
|
|
74
|
-
<p className="text-sm text-text-secondary line-clamp-2 mt-0.5">{description}</p>
|
|
75
|
-
)}
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
55
|
+
{/* Breadcrumb */}
|
|
56
|
+
<Link
|
|
57
|
+
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"
|
|
59
|
+
>
|
|
60
|
+
<ArrowLeft className="h-3 w-3" />
|
|
61
|
+
{backLabel}
|
|
62
|
+
</Link>
|
|
78
63
|
|
|
79
|
-
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
className="h-9"
|
|
87
|
-
leftIcon={<ArrowLeft className="h-4 w-4" />}
|
|
88
|
-
>
|
|
89
|
-
{backText}
|
|
90
|
-
</Button>
|
|
91
|
-
</Link>
|
|
92
|
-
) : (
|
|
93
|
-
<Button
|
|
94
|
-
variant="secondary"
|
|
95
|
-
className="h-9"
|
|
96
|
-
leftIcon={<ArrowLeft className="h-4 w-4" />}
|
|
97
|
-
onClick={() => window.history.back()}
|
|
98
|
-
>
|
|
99
|
-
{backText}
|
|
100
|
-
</Button>
|
|
101
|
-
)
|
|
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>
|
|
102
71
|
)}
|
|
103
|
-
|
|
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>
|
|
104
82
|
</div>
|
|
105
|
-
)}
|
|
106
|
-
</div>
|
|
107
83
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const isActive = activeTab === tab.id
|
|
115
|
-
const triggerClass = cn(
|
|
116
|
-
"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",
|
|
117
|
-
isActive
|
|
118
|
-
? "bg-white text-ibm-gray-100 shadow-sm"
|
|
119
|
-
: "hover:text-ibm-gray-100"
|
|
120
|
-
)
|
|
84
|
+
{actions && (
|
|
85
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
86
|
+
{actions}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
121
90
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
91
|
+
{/* Tab bar — always rendered to keep header height consistent */}
|
|
92
|
+
<nav
|
|
93
|
+
className={`flex gap-0 overflow-x-auto -mx-4 sm:-mx-6 px-4 sm:px-6 min-h-10 ${
|
|
94
|
+
tabBarPosition === "right" ? "justify-end" : "justify-start"
|
|
95
|
+
}`}
|
|
96
|
+
aria-label="Tabs"
|
|
97
|
+
>
|
|
98
|
+
{tabBar?.map((tab) => {
|
|
99
|
+
const Icon = tab.icon
|
|
100
|
+
const isActive = activeTab === tab.id
|
|
101
|
+
return (
|
|
102
|
+
<Link
|
|
103
|
+
key={tab.id}
|
|
104
|
+
href={tab.href}
|
|
105
|
+
className={`inline-flex items-center gap-2 px-5 py-2.5 text-sm font-medium transition-colors whitespace-nowrap ${
|
|
106
|
+
isActive ? TAB_ACTIVE : TAB_INACTIVE
|
|
107
|
+
}`}
|
|
108
|
+
>
|
|
109
|
+
{Icon && <Icon className="h-4 w-4" />}
|
|
110
|
+
{tab.label}
|
|
111
|
+
</Link>
|
|
112
|
+
)
|
|
113
|
+
})}
|
|
114
|
+
</nav>
|
|
135
115
|
|
|
136
|
-
|
|
137
|
-
<Link
|
|
138
|
-
key={tab.id}
|
|
139
|
-
href={tab.href ?? "#"}
|
|
140
|
-
className={triggerClass}
|
|
141
|
-
>
|
|
142
|
-
{Icon && <Icon className="h-4 w-4" />}
|
|
143
|
-
{tab.label}
|
|
144
|
-
</Link>
|
|
145
|
-
)
|
|
146
|
-
})}
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
149
|
-
)}
|
|
116
|
+
</div>
|
|
150
117
|
</div>
|
|
151
118
|
)
|
|
152
119
|
}
|