11'use client'
22
3+ import { type ReactNode , useMemo } from 'react'
34import { ArrowLeft , ArrowRight } from 'lucide-react'
45import { useRouter } from 'next/navigation'
56import { ChipLink } from '@/components/emcn'
67import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
78import { cn } from '@/lib/core/utils/cn'
89import { blockTypeToIconMap , type Integration } from '@/lib/integrations'
910import { IntegrationTile } from '@/app/workspace/[workspaceId]/integrations/components/integrations-showcase'
10- import { getTemplatesForBlock , type ScopedBlockTemplate } from '@/blocks/registry'
11+ import { getBlock , getTemplatesForBlock , type ScopedBlockTemplate } from '@/blocks/registry'
1112
1213/** Maximum number of overlapping icon tiles rendered per template row. */
1314const TEMPLATE_CLUSTER_MAX = 3 as const
@@ -26,6 +27,7 @@ interface IntegrationBlockDetailProps {
2627export function IntegrationBlockDetail ( { integration, workspaceId } : IntegrationBlockDetailProps ) {
2728 const Icon = blockTypeToIconMap [ integration . type ]
2829 const matchingTemplates = getTemplatesForBlock ( integration . type )
30+ const operations = useMemo ( ( ) => getBlockOperations ( integration . type ) , [ integration . type ] )
2931
3032 return (
3133 < div className = 'flex h-full flex-col bg-[var(--bg)]' >
@@ -53,6 +55,10 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
5355 </ div >
5456 </ div >
5557
58+ { operations . length > 0 && (
59+ < OperationsSection integration = { integration } operations = { operations } />
60+ ) }
61+
5662 { matchingTemplates . length > 0 && (
5763 < TemplatesSection
5864 integration = { integration }
@@ -66,6 +72,54 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
6672 )
6773}
6874
75+ interface IntegrationRowProps {
76+ leftIcon : ReactNode
77+ title : string
78+ subtitle ?: string
79+ onClick ?: ( ) => void
80+ }
81+
82+ /**
83+ * Shared row primitive for the integration detail page's "Operations" and
84+ * "Templates" sections. Layout is identical in both modes — same `p-2`,
85+ * `gap-2.5`, same title typography (`text-[14px] text-[var(--text-body)]`) so
86+ * row heights line up regardless of whether a subtitle is present (the
87+ * 36px-tall `IntegrationTile` icon sets the minimum height).
88+ *
89+ * Interactive mode (when `onClick` is supplied) renders a `<button>` with a
90+ * `group` class enabling hover-state coordination with child icons, a
91+ * `hover-hover:` background, and a trailing `ArrowRight`. Read-only mode
92+ * renders a plain `<div>` with no hover affordance and no trailing chevron.
93+ */
94+ function IntegrationRow ( { leftIcon, title, subtitle, onClick } : IntegrationRowProps ) {
95+ const content = (
96+ < >
97+ { leftIcon }
98+ < div className = 'flex min-w-0 flex-1 flex-col' >
99+ < span className = 'truncate text-[14px] text-[var(--text-body)]' > { title } </ span >
100+ { subtitle ? (
101+ < span className = 'truncate text-[12px] text-[var(--text-muted)]' > { subtitle } </ span >
102+ ) : null }
103+ </ div >
104+ { onClick ? < ArrowRight className = 'size-4 flex-shrink-0 text-[var(--text-icon)]' /> : null }
105+ </ >
106+ )
107+
108+ if ( onClick ) {
109+ return (
110+ < button
111+ type = 'button'
112+ onClick = { onClick }
113+ className = 'group flex items-center gap-2.5 rounded-lg p-2 text-left transition-colors hover-hover:bg-[var(--surface-active)]'
114+ >
115+ { content }
116+ </ button >
117+ )
118+ }
119+
120+ return < div className = 'flex items-center gap-2.5 rounded-lg p-2' > { content } </ div >
121+ }
122+
69123interface TemplatesSectionProps {
70124 integration : Integration
71125 templates : readonly ScopedBlockTemplate [ ]
@@ -91,12 +145,12 @@ function TemplatesSection({ integration, templates, workspaceId }: TemplatesSect
91145 TEMPLATE_CLUSTER_MAX
92146 )
93147 return (
94- < TemplateRow
148+ < IntegrationRow
95149 key = { template . title }
96- blockTypes = { blockTypes }
150+ leftIcon = { < TemplateIcons blockTypes = { blockTypes } /> }
97151 title = { template . title }
98- prompt = { template . prompt }
99- onSelect = { handleSelect }
152+ subtitle = { template . prompt }
153+ onClick = { ( ) => handleSelect ( template . prompt ) }
100154 />
101155 )
102156 } ) }
@@ -105,36 +159,6 @@ function TemplatesSection({ integration, templates, workspaceId }: TemplatesSect
105159 )
106160}
107161
108- interface TemplateRowProps {
109- blockTypes : string [ ]
110- title : string
111- prompt : string
112- onSelect : ( prompt : string ) => void
113- }
114-
115- /**
116- * Template row that mirrors `IntegrationItem` from the integrations index
117- * byte-for-byte (icon cluster · title · description · trailing `ArrowRight`).
118- * Renders as a `<button>` because click seeds the home page chat with `prompt`
119- * and navigates to the workspace home, matching the `ShowcaseWithExplore` flow.
120- */
121- function TemplateRow ( { blockTypes, title, prompt, onSelect } : TemplateRowProps ) {
122- return (
123- < button
124- type = 'button'
125- onClick = { ( ) => onSelect ( prompt ) }
126- className = 'group flex items-center gap-2.5 rounded-lg p-2 text-left transition-colors hover-hover:bg-[var(--surface-active)]'
127- >
128- < TemplateIcons blockTypes = { blockTypes } />
129- < div className = 'flex min-w-0 flex-1 flex-col' >
130- < span className = 'truncate text-[14px] text-[var(--text-body)]' > { title } </ span >
131- < span className = 'truncate text-[12px] text-[var(--text-muted)]' > { prompt } </ span >
132- </ div >
133- < ArrowRight className = 'size-4 flex-shrink-0 text-[var(--text-icon)]' />
134- </ button >
135- )
136- }
137-
138162interface TemplateIconsProps {
139163 blockTypes : string [ ]
140164}
@@ -177,3 +201,69 @@ function TemplateIcons({ blockTypes }: TemplateIconsProps) {
177201 </ span >
178202 )
179203}
204+
205+ interface OperationOption {
206+ label : string
207+ id : string
208+ }
209+
210+ /**
211+ * Extracts the operation list from a block's `subBlocks`. Looks for a dropdown
212+ * subBlock with `id === 'operation'` and returns its `options` flattened to
213+ * `{ label, id }`. Supports both static array `options` and function `options`,
214+ * filters out entries marked `hidden`. Returns an empty array when the block
215+ * has no operation selector or no visible options.
216+ */
217+ function getBlockOperations ( blockType : string ) : OperationOption [ ] {
218+ const block = getBlock ( blockType )
219+ if ( ! block ) return [ ]
220+ const operationSubBlock = block . subBlocks . find (
221+ ( sb ) => sb . id === 'operation' && sb . type === 'dropdown'
222+ )
223+ if ( ! operationSubBlock ?. options ) return [ ]
224+ const rawOptions =
225+ typeof operationSubBlock . options === 'function'
226+ ? operationSubBlock . options ( )
227+ : operationSubBlock . options
228+ return rawOptions
229+ . filter ( ( option ) => ! option . hidden )
230+ . map ( ( option ) => ( { label : option . label , id : option . id } ) )
231+ }
232+
233+ interface OperationsSectionProps {
234+ integration : Integration
235+ operations : OperationOption [ ]
236+ }
237+
238+ /**
239+ * Lists every operation exposed by the block's `operation` subBlock. Mirrors
240+ * `TemplatesSection`'s heading, divider, and outer spacing so the two sections
241+ * stack uniformly on the detail page. Each row reuses the same
242+ * `IntegrationRow` primitive as templates with no `onClick` and a single
243+ * `IntegrationTile` icon — no cluster, no chevron, no hover state.
244+ */
245+ function OperationsSection ( { integration, operations } : OperationsSectionProps ) {
246+ const Icon = blockTypeToIconMap [ integration . type ]
247+ const leftIcon = Icon ? (
248+ < IntegrationTile blockType = { integration . type } icon = { Icon } />
249+ ) : (
250+ < div
251+ className = 'flex size-9 flex-shrink-0 items-center justify-center rounded-xl border border-[var(--border-1)] text-white'
252+ style = { { background : integration . bgColor } }
253+ >
254+ { integration . name . charAt ( 0 ) }
255+ </ div >
256+ )
257+
258+ return (
259+ < section className = 'flex flex-col' >
260+ < span className = 'pl-0.5 text-[var(--text-muted)] text-small' > Operations</ span >
261+ < div className = 'mt-[9px] mb-3 h-px bg-[var(--border)]' />
262+ < div className = '-mx-2 flex flex-col gap-y-0.5' >
263+ { operations . map ( ( operation ) => (
264+ < IntegrationRow key = { operation . id } leftIcon = { leftIcon } title = { operation . label } />
265+ ) ) }
266+ </ div >
267+ </ section >
268+ )
269+ }
0 commit comments