@orsetra/shared-ui 1.3.2 → 1.3.4
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,119 +3,135 @@
|
|
|
3
3
|
import { ArrowLeft } from "lucide-react"
|
|
4
4
|
import Link from "next/link"
|
|
5
5
|
import type { ReactNode, ElementType } from "react"
|
|
6
|
-
|
|
7
|
-
|
|
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"
|
|
7
|
+
import { Button } from "./button"
|
|
11
8
|
|
|
12
9
|
export interface DetailPageHeaderTab {
|
|
13
10
|
id: string
|
|
14
11
|
label: string
|
|
15
12
|
icon?: ElementType
|
|
16
|
-
|
|
13
|
+
/** URL-based navigation */
|
|
14
|
+
href?: string
|
|
15
|
+
/** State-based or programmatic navigation */
|
|
16
|
+
action?: () => void
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface DetailPageHeaderProps {
|
|
20
|
-
/** Back
|
|
21
|
-
|
|
22
|
-
/** Back breadcrumb label */
|
|
23
|
-
backLabel: string
|
|
20
|
+
/** If provided, renders a secondary Back button on the right that calls history.back() */
|
|
21
|
+
backText?: string
|
|
24
22
|
/** Optional icon rendered in a blue square container */
|
|
25
23
|
icon?: ReactNode
|
|
26
|
-
/** Page title
|
|
24
|
+
/** Page title */
|
|
27
25
|
title: string
|
|
28
|
-
/** Optional subtitle
|
|
26
|
+
/** Optional subtitle */
|
|
29
27
|
description?: string
|
|
30
|
-
/** Slot for badges
|
|
28
|
+
/** Slot for badges or status nodes placed next to the title */
|
|
29
|
+
statusNode?: ReactNode
|
|
30
|
+
/** Slot for action buttons placed to the right, after the back button */
|
|
31
31
|
actions?: ReactNode
|
|
32
|
-
/** Optional tab navigation
|
|
32
|
+
/** Optional tab navigation */
|
|
33
33
|
tabBar?: DetailPageHeaderTab[]
|
|
34
34
|
/** Alignment of the tab bar – defaults to "left" */
|
|
35
35
|
tabBarPosition?: "left" | "right"
|
|
36
36
|
/** Currently active tab id */
|
|
37
37
|
activeTab?: string
|
|
38
|
+
/** Extra classes on the root element – use to adjust spacing/height */
|
|
39
|
+
className?: string
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export function DetailPageHeader({
|
|
41
|
-
|
|
42
|
-
backLabel,
|
|
43
|
+
backText,
|
|
43
44
|
icon,
|
|
44
45
|
title,
|
|
45
46
|
description,
|
|
47
|
+
statusNode,
|
|
46
48
|
actions,
|
|
47
49
|
tabBar,
|
|
48
50
|
tabBarPosition = "left",
|
|
49
51
|
activeTab,
|
|
52
|
+
className,
|
|
50
53
|
}: DetailPageHeaderProps) {
|
|
51
54
|
return (
|
|
52
|
-
<div className="
|
|
53
|
-
<div className="px-4 sm:px-6 pt-4 pb-0">
|
|
54
|
-
|
|
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>
|
|
63
|
-
|
|
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>
|
|
55
|
+
<div className={cn("mb-3", className)}>
|
|
83
56
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
57
|
+
{/* Title row */}
|
|
58
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
|
59
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
60
|
+
{icon && (
|
|
61
|
+
<div className="h-9 w-9 rounded bg-ibm-blue-10 flex items-center justify-center flex-shrink-0">
|
|
62
|
+
{icon}
|
|
87
63
|
</div>
|
|
88
64
|
)}
|
|
65
|
+
<div className="min-w-0">
|
|
66
|
+
<div className="flex items-center gap-3">
|
|
67
|
+
<h1 className="text-xl sm:text-2xl font-semibold text-text-primary truncate">{title}</h1>
|
|
68
|
+
{statusNode && <div className="flex-shrink-0">{statusNode}</div>}
|
|
69
|
+
</div>
|
|
70
|
+
{description && (
|
|
71
|
+
<p className="text-sm text-text-secondary line-clamp-2 mt-0.5">{description}</p>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
89
74
|
</div>
|
|
90
75
|
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
76
|
+
{(backText || actions) && (
|
|
77
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
78
|
+
{backText && (
|
|
79
|
+
<Button
|
|
80
|
+
variant="secondary"
|
|
81
|
+
size="sm"
|
|
82
|
+
onClick={() => window.history.back()}
|
|
83
|
+
>
|
|
84
|
+
<ArrowLeft className="h-4 w-4 mr-1.5" />
|
|
85
|
+
{backText}
|
|
86
|
+
</Button>
|
|
87
|
+
)}
|
|
88
|
+
{actions}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Tab bar */}
|
|
94
|
+
{tabBar && tabBar.length > 0 && (
|
|
95
|
+
<div className={cn("mt-3 flex", tabBarPosition === "right" ? "justify-end" : "justify-start")}>
|
|
96
|
+
<div className="inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground">
|
|
99
97
|
{tabBar.map((tab) => {
|
|
100
98
|
const Icon = tab.icon
|
|
101
99
|
const isActive = activeTab === tab.id
|
|
100
|
+
const triggerClass = cn(
|
|
101
|
+
"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",
|
|
102
|
+
isActive
|
|
103
|
+
? "bg-white text-ibm-gray-100 shadow-sm"
|
|
104
|
+
: "hover:text-ibm-gray-100"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if (tab.action) {
|
|
108
|
+
return (
|
|
109
|
+
<button
|
|
110
|
+
key={tab.id}
|
|
111
|
+
type="button"
|
|
112
|
+
onClick={tab.action}
|
|
113
|
+
className={triggerClass}
|
|
114
|
+
>
|
|
115
|
+
{Icon && <Icon className="h-4 w-4" />}
|
|
116
|
+
{tab.label}
|
|
117
|
+
</button>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
102
121
|
return (
|
|
103
122
|
<Link
|
|
104
123
|
key={tab.id}
|
|
105
|
-
href={tab.href}
|
|
106
|
-
className={
|
|
107
|
-
isActive ? TAB_ACTIVE : TAB_INACTIVE
|
|
108
|
-
}`}
|
|
124
|
+
href={tab.href ?? "#"}
|
|
125
|
+
className={triggerClass}
|
|
109
126
|
>
|
|
110
127
|
{Icon && <Icon className="h-4 w-4" />}
|
|
111
128
|
{tab.label}
|
|
112
129
|
</Link>
|
|
113
130
|
)
|
|
114
131
|
})}
|
|
115
|
-
</
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
119
135
|
</div>
|
|
120
136
|
)
|
|
121
137
|
}
|