@ossy/package-catalog 1.0.1

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/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@ossy/package-catalog",
3
+ "description": "Capability catalog UI — browse manifest packages, actions, and integration examples",
4
+ "version": "1.0.1",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "module": "./src/index.js",
8
+ "exports": {
9
+ ".": "./src/index.js"
10
+ },
11
+ "author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
12
+ "license": "MIT",
13
+ "ossy": {
14
+ "src": "./src"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://registry.npmjs.org"
19
+ },
20
+ "files": [
21
+ "/src",
22
+ "README.md"
23
+ ],
24
+ "peerDependencies": {
25
+ "@ossy/app": ">=1.0.0",
26
+ "@ossy/design-system": ">=1.0.0",
27
+ "@ossy/router-react": ">=1.0.0",
28
+ "@ossy/sdk-react": ">=1.0.0",
29
+ "@ossy/workspaces": ">=1.0.0",
30
+ "react": ">=19.0.0 <20.0.0"
31
+ },
32
+ "gitHead": "a0d89185a17f8de8ce328c3a648c108ff1d61d8f"
33
+ }
@@ -0,0 +1,87 @@
1
+ import React from 'react'
2
+ import { Text, View } from '@ossy/design-system'
3
+ import { humanizeCapabilityId } from './packageCopy.js'
4
+
5
+ /**
6
+ * Widget-scale action card — clickable list item in the Actions split view.
7
+ *
8
+ * @param {{
9
+ * action: { id: string, access?: string, title?: string, description?: string, mcpTool?: string, inputSchema?: object }
10
+ * selected?: boolean
11
+ * onSelect?: () => void
12
+ * }} props
13
+ */
14
+ export function ActionCapabilityCard ({ action, selected = false, onSelect }) {
15
+ const title = action.title || humanizeCapabilityId(action.id)
16
+
17
+ return (
18
+ <View
19
+ as="button"
20
+ type="button"
21
+ gap="s"
22
+ inset="m"
23
+ roundness="m"
24
+ surface="primary"
25
+ className="pkg-store-action-card"
26
+ onClick={onSelect}
27
+ aria-pressed={selected}
28
+ >
29
+ <View gap="2px">
30
+ <Text className="pkg-store-text-strong">{title}</Text>
31
+ <Text className="pkg-store-mono pkg-store-mono--sm pkg-store-text-subtle">{action.id}</Text>
32
+ </View>
33
+
34
+ {action.description && (
35
+ <Text variant="small" className="pkg-store-text-dim">
36
+ {action.description}
37
+ </Text>
38
+ )}
39
+
40
+ {action.access && (
41
+ <Text variant="small" className="pkg-store-text-subtle">
42
+ access: {action.access}
43
+ </Text>
44
+ )}
45
+ </View>
46
+ )
47
+ }
48
+
49
+ /**
50
+ * Merge manifest action with capability metadata from build-time capabilities.json.
51
+ *
52
+ * @param {{ id: string, access?: string }} action
53
+ * @param {Record<string, object> | undefined} schemaById keyed by action id
54
+ */
55
+ export function enrichActionWithSchema (action, schemaById) {
56
+ const schema = schemaById?.[action.id]
57
+ if (!schema) return action
58
+ return {
59
+ ...action,
60
+ title: schema.title,
61
+ description: schema.description,
62
+ mcpTool: schema.mcpTool,
63
+ inputSchema: schema.inputSchema,
64
+ }
65
+ }
66
+
67
+ /** @param {Array<{ actionId: string, name?: string, title?: string, description?: string, inputSchema?: object }>} tools */
68
+ export function buildCapabilitySchemaMap (tools) {
69
+ /** @type {Record<string, object>} */
70
+ const map = {}
71
+ for (const tool of tools || []) {
72
+ if (!tool?.actionId) continue
73
+ map[tool.actionId] = {
74
+ id: tool.actionId,
75
+ title: tool.title,
76
+ description: tool.description,
77
+ mcpTool: tool.name,
78
+ inputSchema: tool.inputSchema,
79
+ }
80
+ }
81
+ return map
82
+ }
83
+
84
+ /** @param {{ tools?: Array<object> } | undefined} capabilities */
85
+ export function capabilitySchemaMapFromApp (capabilities) {
86
+ return buildCapabilitySchemaMap(capabilities?.tools)
87
+ }
@@ -0,0 +1,47 @@
1
+ import React from 'react'
2
+ import { HttpInvokeExample, SdkInvokeExample, Text, View } from '@ossy/design-system'
3
+ import { useApp, resolveEndpoints } from '@ossy/app/shell'
4
+ import { humanizeCapabilityId } from './packageCopy.js'
5
+
6
+ /**
7
+ * Split-view detail panel for a single action's invoke examples.
8
+ *
9
+ * @param {{ action: { id: string, title?: string, description?: string, mcpTool?: string, access?: string } }} props
10
+ */
11
+ export function ActionExamplesPanel ({ action }) {
12
+ const app = useApp()
13
+ const { actionsUrl } = resolveEndpoints(app)
14
+ const title = action.title || humanizeCapabilityId(action.id)
15
+
16
+ return (
17
+ <View
18
+ gap="s"
19
+ inset="m"
20
+ roundness="m"
21
+ surface="primary"
22
+ className="pkg-store-action-examples"
23
+ >
24
+ <View gap="2px">
25
+ <Text className="pkg-store-text-strong">{title}</Text>
26
+ <Text className="pkg-store-mono pkg-store-mono--sm pkg-store-text-subtle">{action.id}</Text>
27
+ </View>
28
+
29
+ {action.description && (
30
+ <Text variant="small" className="pkg-store-prose--muted">
31
+ {action.description}
32
+ </Text>
33
+ )}
34
+
35
+ {action.mcpTool && (
36
+ <Text variant="small" className="pkg-store-mono pkg-store-mono--xs pkg-store-text-muted">
37
+ MCP: {action.mcpTool}
38
+ </Text>
39
+ )}
40
+
41
+ <View gap="xs">
42
+ <SdkInvokeExample actionId={action.id} />
43
+ <HttpInvokeExample actionId={action.id} actionsUrl={actionsUrl} />
44
+ </View>
45
+ </View>
46
+ )
47
+ }
@@ -0,0 +1,15 @@
1
+ import React from 'react'
2
+ import { CodeExampleStyles } from '@ossy/design-system'
3
+ import { packageStoreStyles } from './packageStoreStyles.js'
4
+
5
+ /** Shared styles for catalog list and detail pages. */
6
+ export function CatalogPageStyles () {
7
+ return (
8
+ <>
9
+ <CodeExampleStyles />
10
+ <style href="@ossy/package-catalog/store" precedence="high">
11
+ {packageStoreStyles}
12
+ </style>
13
+ </>
14
+ )
15
+ }
@@ -0,0 +1,9 @@
1
+ export const Definition = {
2
+ id: 'package-catalog',
3
+ title: 'Capabilities',
4
+ description: 'Browse documented, invokable platform capabilities for your workspace.',
5
+ icon: 'layout-grid',
6
+ statuses: ['beta'],
7
+ entitlementRequired: false,
8
+ navOrder: 0,
9
+ }
@@ -0,0 +1,79 @@
1
+ import React from 'react'
2
+ import { Button, Icon, Text, View } from '@ossy/design-system'
3
+ import { useRouter } from '@ossy/router-react'
4
+ import { scrollToPackageSection } from './packageDetailSections.js'
5
+
6
+ /**
7
+ * @param {{
8
+ * icon: string
9
+ * title: string
10
+ * body: string | React.ReactNode
11
+ * href?: string
12
+ * sectionId?: string
13
+ * bodyParams?: Record<string, unknown>
14
+ * }} props
15
+ */
16
+ export function HowToCard ({ icon, title, body, href, sectionId, bodyParams }) {
17
+ const router = useRouter()
18
+ const isSectionLink = Boolean(sectionId)
19
+
20
+ const handleActivate = () => {
21
+ if (sectionId) scrollToPackageSection(sectionId)
22
+ }
23
+
24
+ const handleKeyDown = (event) => {
25
+ if (!sectionId) return
26
+ if (event.key === 'Enter' || event.key === ' ') {
27
+ event.preventDefault()
28
+ scrollToPackageSection(sectionId)
29
+ }
30
+ }
31
+
32
+ const cardClassName = [
33
+ 'pkg-store-howto-card',
34
+ 'pkg-store-envelope',
35
+ isSectionLink ? 'pkg-store-howto-card--link' : '',
36
+ ].filter(Boolean).join(' ')
37
+
38
+ return (
39
+ <View
40
+ gap="s"
41
+ inset="m"
42
+ roundness="m"
43
+ surface="primary"
44
+ className={cardClassName}
45
+ role={isSectionLink ? 'link' : undefined}
46
+ tabIndex={isSectionLink ? 0 : undefined}
47
+ onClick={isSectionLink ? handleActivate : undefined}
48
+ onKeyDown={isSectionLink ? handleKeyDown : undefined}
49
+ >
50
+ <View justifyContent="center" alignItems="center">
51
+ <Icon name={icon} size="m" />
52
+ </View>
53
+ <Text variant="small" className="pkg-store-text-bold pkg-store-text-center" text={title} />
54
+ {typeof body === 'string'
55
+ ? (
56
+ <Text variant="small" className="pkg-store-card-body pkg-store-text-center pkg-store-prose--muted" text={body} params={bodyParams} />
57
+ )
58
+ : (
59
+ <View className="pkg-store-card-body pkg-store-text-center pkg-store-prose--muted">
60
+ {body}
61
+ </View>
62
+ )}
63
+ {href && (
64
+ <Button
65
+ variant="link"
66
+ href={router.getHref(href)}
67
+ label="package-catalog.home.howTo.http.createLink"
68
+ />
69
+ )}
70
+ {sectionId && (
71
+ <Text
72
+ variant="small"
73
+ text="package-catalog.detail.howTo.sectionLink"
74
+ className="pkg-store-section-link"
75
+ />
76
+ )}
77
+ </View>
78
+ )
79
+ }
@@ -0,0 +1,259 @@
1
+ import React from 'react'
2
+ import { Button, Icon, Text, View, useLocale } from '@ossy/design-system'
3
+ import { useRouter } from '@ossy/router-react'
4
+ import { formatPagePathForLanguage, formatPagePaths, monoStyle } from './formatPaths.js'
5
+ import { getTaskDisplayDescription, getTaskDisplayTitle, humanizeCapabilityId } from './packageCopy.js'
6
+ import { pkgStoreEnvelopedSurfaceProps } from './packageStoreStyles.js'
7
+
8
+ /**
9
+ * @typedef {'pages' | 'actions' | 'entities' | 'secondary' | 'backgroundTasks' | 'list'} ManifestSectionVariant
10
+ */
11
+
12
+ /**
13
+ * @param {{
14
+ * title: string
15
+ * subtitle?: string
16
+ * items: Array<{ id: string, path?: string | Record<string, string>, access?: string, title?: string, credentials?: string[], description?: string }>
17
+ * variant?: ManifestSectionVariant
18
+ * linkPages?: boolean
19
+ * icon?: string
20
+ * openPageLabel?: string
21
+ * accessLabel?: string
22
+ * credentialsLabel?: string
23
+ * titleVariant?: string
24
+ * compact?: boolean
25
+ * enveloped?: boolean
26
+ * sectionId?: string
27
+ * }} props
28
+ */
29
+ export function ManifestSection ({
30
+ title,
31
+ subtitle,
32
+ items = [],
33
+ variant = 'list',
34
+ linkPages = false,
35
+ icon,
36
+ openPageLabel = 'package-catalog.detail.openPage',
37
+ accessLabel = 'design-system.dev.pagesPanel.accessLabel',
38
+ credentialsLabel = 'design-system.dev.pagesPanel.credentialsLabel',
39
+ titleVariant = 'heading-secondary',
40
+ compact = false,
41
+ enveloped = false,
42
+ sectionId,
43
+ }) {
44
+ const router = useRouter()
45
+ const { t } = useLocale()
46
+
47
+ if (!items.length) return null
48
+
49
+ const sectionIcon = icon || variantIcon(variant)
50
+ const resolvedTitleVariant = enveloped ? 'heading-secondary' : titleVariant
51
+ const subtitleOpacity = enveloped ? 0.75 : (compact ? 0.65 : 0.75)
52
+
53
+ const section = (
54
+ <View id={sectionId} gap={enveloped ? 'm' : (compact ? 's' : 'm')} className="pkg-store-section">
55
+ <View gap="xs">
56
+ <View layout="row" gap="s" alignItems="center">
57
+ {sectionIcon && (
58
+ enveloped
59
+ ? <Icon name={sectionIcon} size="m" />
60
+ : (
61
+ <View
62
+ alignItems="center"
63
+ justifyContent="center"
64
+ style={{
65
+ width: compact ? 28 : 36,
66
+ height: compact ? 28 : 36,
67
+ borderRadius: compact ? 8 : 10,
68
+ background: 'color-mix(in srgb, var(--foreground) 6%, transparent)',
69
+ }}
70
+ >
71
+ <Icon name={sectionIcon} size={compact ? 'xs' : 's'} />
72
+ </View>
73
+ )
74
+ )}
75
+ <Text as="h2" variant={resolvedTitleVariant} text={title} />
76
+ </View>
77
+ {subtitle && (
78
+ <Text text={subtitle} style={{ opacity: subtitleOpacity, maxWidth: 560, fontSize: enveloped ? undefined : (compact ? '0.875rem' : undefined) }} />
79
+ )}
80
+ </View>
81
+
82
+ {variant === 'pages' && (
83
+ <View gap="s" className="pkg-store-pages-list">
84
+ {items.map((item) => (
85
+ <View
86
+ key={item.id}
87
+ layout="row"
88
+ gap="m"
89
+ alignItems="center"
90
+ inset="m"
91
+ roundness="m"
92
+ surface="primary"
93
+ className="pkg-store-page-row"
94
+ style={{ border: '1px solid var(--separator-primary)' }}
95
+ >
96
+ <View gap="2px" style={{ flex: 1, minWidth: 0 }}>
97
+ <Text style={{ fontWeight: 600 }}>{item.title || humanizeCapabilityId(item.id)}</Text>
98
+ {item.path != null && (
99
+ <Text style={{ ...monoStyle, opacity: 0.8, fontSize: '0.75rem' }}>
100
+ {formatPagePathForLanguage(item.path, router.language)}
101
+ </Text>
102
+ )}
103
+ </View>
104
+ {linkPages && (
105
+ <Button
106
+ variant="secondary"
107
+ size="s"
108
+ href={router.getHref(item.id)}
109
+ icon="external"
110
+ title={t(openPageLabel)}
111
+ aria-label={t(openPageLabel)}
112
+ style={{ flexShrink: 0 }}
113
+ />
114
+ )}
115
+ </View>
116
+ ))}
117
+ </View>
118
+ )}
119
+
120
+ {variant === 'actions' && (
121
+ <View as="ul" gap="s" className="pkg-store-bullets" style={{ listStyle: 'none', padding: 0, margin: 0 }}>
122
+ {items.map((item) => (
123
+ <View as="li" key={item.id} layout="row" gap="s" alignItems="flex-start">
124
+ <Text style={{ opacity: 0.5, lineHeight: 1.5 }} aria-hidden>
125
+
126
+ </Text>
127
+ <View gap="2px" style={{ flex: 1 }}>
128
+ <Text style={{ fontWeight: 600 }}>{humanizeCapabilityId(item.id)}</Text>
129
+ <Text style={{ ...monoStyle, opacity: 0.65, fontSize: '0.75rem' }}>{item.id}</Text>
130
+ {item.access && (
131
+ <Text variant="small" text={accessLabel} params={{ access: item.access }} style={{ opacity: 0.7 }} />
132
+ )}
133
+ </View>
134
+ </View>
135
+ ))}
136
+ </View>
137
+ )}
138
+
139
+ {variant === 'backgroundTasks' && (
140
+ <View gap="s" className="pkg-store-secondary-grid">
141
+ {items.map((item) => {
142
+ const taskTitle = getTaskDisplayTitle(item)
143
+ const taskDescription = getTaskDisplayDescription(item)
144
+ return (
145
+ <View key={item.id} gap="xs" inset="m" roundness="m" className="pkg-store-secondary-item">
146
+ <Text style={{ fontWeight: 600, fontSize: '0.9375rem' }}>{taskTitle}</Text>
147
+ {taskDescription && (
148
+ <Text variant="small" style={{ opacity: 0.8 }}>
149
+ {taskDescription}
150
+ </Text>
151
+ )}
152
+ <Text style={{ ...monoStyle, opacity: 0.65, fontSize: '0.75rem' }}>{item.id}</Text>
153
+ </View>
154
+ )
155
+ })}
156
+ </View>
157
+ )}
158
+
159
+ {variant === 'entities' && (
160
+ <View gap="s" className="pkg-store-entities-grid">
161
+ {items.map((item) => (
162
+ <View key={item.id} gap="xs" inset="m" roundness="m" surface="primary" className="pkg-store-entity">
163
+ <Text style={{ fontWeight: 600 }}>{item.title || humanizeCapabilityId(item.id)}</Text>
164
+ <Text style={{ ...monoStyle, opacity: 0.7, fontSize: '0.75rem' }}>{item.id}</Text>
165
+ {item.path != null && (
166
+ <Text style={{ ...monoStyle, opacity: 0.75, fontSize: '0.75rem' }}>
167
+ {formatPagePaths(item.path)}
168
+ </Text>
169
+ )}
170
+ {item.credentials?.length > 0 && (
171
+ <Text
172
+ variant="small"
173
+ text={credentialsLabel}
174
+ params={{ credentials: item.credentials.join(', ') }}
175
+ style={{ opacity: 0.7 }}
176
+ />
177
+ )}
178
+ </View>
179
+ ))}
180
+ </View>
181
+ )}
182
+
183
+ {variant === 'secondary' && (
184
+ <View gap="xs" className="pkg-store-secondary-grid">
185
+ {items.map((item) => (
186
+ <View key={item.id} gap="2px" inset="s" roundness="s" className="pkg-store-secondary-item">
187
+ <Text style={{ fontWeight: 600, fontSize: '0.9375rem' }}>
188
+ {item.title || humanizeCapabilityId(item.id)}
189
+ </Text>
190
+ <Text style={{ ...monoStyle, opacity: 0.7, fontSize: '0.75rem' }}>{item.id}</Text>
191
+ {item.path != null && (
192
+ <Text style={{ ...monoStyle, opacity: 0.75, fontSize: '0.75rem' }}>
193
+ {formatPagePaths(item.path)}
194
+ </Text>
195
+ )}
196
+ {item.access && (
197
+ <Text variant="small" style={{ opacity: 0.7 }}>
198
+ {item.access}
199
+ </Text>
200
+ )}
201
+ </View>
202
+ ))}
203
+ </View>
204
+ )}
205
+
206
+ {variant === 'list' && (
207
+ <View gap="xs">
208
+ {items.map((item) => (
209
+ <View key={item.id} gap="2px" inset="s" roundness="s" surface="primary" className="pkg-store-card">
210
+ {linkPages ? (
211
+ <Button
212
+ variant="link"
213
+ href={router.getHref(item.id)}
214
+ style={{ justifyContent: 'flex-start', height: 'auto', padding: 0 }}
215
+ >
216
+ <Text style={{ ...monoStyle, fontWeight: 600 }}>{item.id}</Text>
217
+ </Button>
218
+ ) : (
219
+ <Text style={{ ...monoStyle, fontWeight: 600 }}>{item.id}</Text>
220
+ )}
221
+ {item.path != null && (
222
+ <Text style={{ ...monoStyle, opacity: 0.85 }}>{formatPagePaths(item.path)}</Text>
223
+ )}
224
+ {item.title && <Text style={{ opacity: 0.8 }}>{item.title}</Text>}
225
+ {item.access && <Text style={{ opacity: 0.75 }}>access: {item.access}</Text>}
226
+ {item.credentials?.length > 0 && (
227
+ <Text style={{ opacity: 0.75 }}>credentials: {item.credentials.join(', ')}</Text>
228
+ )}
229
+ </View>
230
+ ))}
231
+ </View>
232
+ )}
233
+ </View>
234
+ )
235
+
236
+ if (enveloped) {
237
+ return <View {...pkgStoreEnvelopedSurfaceProps}>{section}</View>
238
+ }
239
+
240
+ return section
241
+ }
242
+
243
+ /** @param {ManifestSectionVariant} variant */
244
+ function variantIcon (variant) {
245
+ switch (variant) {
246
+ case 'pages':
247
+ return 'file-document'
248
+ case 'actions':
249
+ return 'bolt'
250
+ case 'backgroundTasks':
251
+ return 'timer'
252
+ case 'entities':
253
+ return 'database'
254
+ case 'secondary':
255
+ return 'layout-grid'
256
+ default:
257
+ return 'list'
258
+ }
259
+ }
@@ -0,0 +1,52 @@
1
+ import React, { useState } from 'react'
2
+ import { Icon, Text, View } from '@ossy/design-system'
3
+ import { ActionCapabilityCard } from './ActionCapabilityCard.jsx'
4
+ import { ActionExamplesPanel } from './ActionExamplesPanel.jsx'
5
+ import { pkgStoreEnvelopedSurfaceProps } from './packageStoreStyles.js'
6
+ import { PKG_DETAIL_SECTION_IDS } from './packageDetailSections.js'
7
+
8
+ /**
9
+ * Actions section with enveloped surface and split-view examples panel.
10
+ *
11
+ * @param {{ actions: Array<{ id: string, access?: string, title?: string, description?: string, mcpTool?: string }> }} props
12
+ */
13
+ export function PackageActionsSection ({ actions }) {
14
+ const [selectedId, setSelectedId] = useState(actions[0]?.id)
15
+ const selectedAction = actions.find((a) => a.id === selectedId) ?? actions[0]
16
+
17
+ return (
18
+ <View id={PKG_DETAIL_SECTION_IDS.actions} {...pkgStoreEnvelopedSurfaceProps}>
19
+ <View gap="xs">
20
+ <View layout="row" gap="s" alignItems="center">
21
+ <Icon name="bolt" size="m" />
22
+ <Text as="h2" variant="heading-secondary" text="package-catalog.detail.section.actions.title" />
23
+ </View>
24
+ <Text
25
+ text="package-catalog.detail.section.actions.subtitle"
26
+ className="pkg-store-prose--narrow"
27
+ />
28
+ </View>
29
+
30
+ <View
31
+ gap="m"
32
+ className={selectedAction ? 'pkg-store-actions-split' : undefined}
33
+ layout={selectedAction ? undefined : 'row-wrap'}
34
+ >
35
+ <View gap="m" layout="row-wrap" className="pkg-store-actions-list">
36
+ {actions.map((action) => (
37
+ <ActionCapabilityCard
38
+ key={action.id}
39
+ action={action}
40
+ selected={action.id === selectedId}
41
+ onSelect={() => setSelectedId(action.id)}
42
+ />
43
+ ))}
44
+ </View>
45
+
46
+ {selectedAction && (
47
+ <ActionExamplesPanel action={selectedAction} />
48
+ )}
49
+ </View>
50
+ </View>
51
+ )
52
+ }
@@ -0,0 +1,97 @@
1
+ import React from 'react'
2
+ import { Button, Text, View, PageSection } from '@ossy/design-system'
3
+
4
+ const Cover = ({
5
+ title,
6
+ titleMaxWidth = '1100px',
7
+ text,
8
+ textParams,
9
+ eyebrow,
10
+ meta,
11
+ metaParams,
12
+ actions = [],
13
+ }) => (
14
+ <View
15
+ gap="l"
16
+ placeContent="center"
17
+ layout="column"
18
+ justifyContent="center"
19
+ className="pkg-store-hero-cover"
20
+ >
21
+ <View gap="m" alignItems="center">
22
+ {eyebrow && (
23
+ <Text
24
+ variant="small"
25
+ className="pkg-store-mono pkg-store-hero-eyebrow"
26
+ >
27
+ {eyebrow}
28
+ </Text>
29
+ )}
30
+
31
+ <Text
32
+ as="h1"
33
+ variant="display"
34
+ text={title}
35
+ className="pkg-store-hero-title"
36
+ style={{ maxWidth: titleMaxWidth }}
37
+ />
38
+
39
+ {text && (
40
+ <Text as="h2" variant="heading-tertiary" text={text} params={textParams} className="pkg-store-hero-subtitle" />
41
+ )}
42
+
43
+ {meta && (
44
+ <Text variant="small" text={meta} params={metaParams} className="pkg-store-hero-meta" />
45
+ )}
46
+ </View>
47
+
48
+ {actions.length > 0 && (
49
+ <View gap="m" layout="row" justifyContent="center" className="pkg-store-hero-actions">
50
+ {actions.map(({ label, ...props }) => (
51
+ <Button {...props} key={label} label={label} />
52
+ ))}
53
+ </View>
54
+ )}
55
+ </View>
56
+ )
57
+
58
+ /**
59
+ * Marketing-style hero for package detail — mirrors @ossy/booking HeroCover.
60
+ */
61
+ export const PackageHero = ({
62
+ title,
63
+ titleMaxWidth = '1100px',
64
+ text,
65
+ textParams,
66
+ eyebrow,
67
+ meta,
68
+ metaParams,
69
+ actions = [],
70
+
71
+ id = 'package-hero',
72
+ surfaceAs = 'div',
73
+ maxWidth = 'l',
74
+ surface = 'hero',
75
+ ...props
76
+ }) => (
77
+ <PageSection
78
+ id={id}
79
+ surfaceAs={surfaceAs}
80
+ maxWidth={maxWidth}
81
+ surface={surface}
82
+ {...props}
83
+ data-rounded
84
+ data-component="@ossy/package-catalog/package-hero"
85
+ >
86
+ <Cover
87
+ title={title}
88
+ titleMaxWidth={titleMaxWidth}
89
+ text={text}
90
+ textParams={textParams}
91
+ eyebrow={eyebrow}
92
+ meta={meta}
93
+ metaParams={metaParams}
94
+ actions={actions}
95
+ />
96
+ </PageSection>
97
+ )