@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.
@@ -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
- import { cn } from "../../lib/utils"
7
- import { Button } from "./button"
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
- /** URL-based navigation */
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
- /** If provided, renders a secondary Back button. Clicks navigate to backHref if set, otherwise history.back() */
21
- backText?: string
22
- /** Optional explicit back destination. If absent, the back button uses history.back() */
23
- backHref?: string
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 or status nodes placed next to the title */
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={cn("mb-3", 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
- {/* Title row */}
61
- <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
62
- <div className="flex items-center gap-3 min-w-0">
63
- {icon && (
64
- <div className="h-9 w-9 rounded bg-ibm-blue-10 flex items-center justify-center flex-shrink-0">
65
- {icon}
66
- </div>
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
- {(backText || actions) && (
80
- <div className="flex items-center gap-2 shrink-0">
81
- {backText && (
82
- backHref ? (
83
- <Link href={backHref}>
84
- <Button
85
- variant="secondary"
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
- {actions}
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
- {/* Tab bar */}
109
- {tabBar && tabBar.length > 0 && (
110
- <div className={cn("mt-3 flex", tabBarPosition === "right" ? "justify-end" : "justify-start")}>
111
- <div className="inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground">
112
- {tabBar.map((tab) => {
113
- const Icon = tab.icon
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
- if (tab.action) {
123
- return (
124
- <button
125
- key={tab.id}
126
- type="button"
127
- onClick={tab.action}
128
- className={triggerClass}
129
- >
130
- {Icon && <Icon className="h-4 w-4" />}
131
- {tab.label}
132
- </button>
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
- return (
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",