22
33import { AnimatePresence , motion } from 'framer-motion'
44import { Button , Tooltip } from '@/components/emcn'
5- import { PlayOutline , RefreshCw , Square } from '@/components/emcn/icons'
5+ import { Eye , PlayOutline , RefreshCw , Square } from '@/components/emcn/icons'
66import { cn } from '@/lib/core/utils/cn'
77
88interface TableActionBarProps {
9- /** Number of rows currently selected (checkbox + multi-row range). */
9+ /** Number of rows currently selected (checkbox + multi-row range). 0 in
10+ * single-cell mode (use `singleCell` instead). */
1011 selectedCount : number
1112 /** Total running/queued workflow cells across the selected rows. Drives the
1213 * Stop button's visibility (hidden when 0) and label. */
@@ -24,6 +25,19 @@ interface TableActionBarProps {
2425 onRerun : ( ) => void
2526 /** Cancel running/queued cells across selected rows. */
2627 onStopWorkflows : ( ) => void
28+ /**
29+ * When the user has a single workflow-output cell highlighted (no row
30+ * selection), the bar switches to a per-cell mode showing the cell's
31+ * status + an Eye button to open the execution log. `null` for multi-row
32+ * selections.
33+ */
34+ singleCell ?: {
35+ canViewExecution : boolean
36+ onViewExecution : ( ) => void
37+ isRunning : boolean
38+ onRunCell : ( ) => void
39+ onStopCell : ( ) => void
40+ } | null
2741 /** Disables actions while a bulk mutation is in flight. */
2842 isLoading ?: boolean
2943 /** Additional className for the floating wrapper — used to lift the bar
@@ -32,14 +46,14 @@ interface TableActionBarProps {
3246}
3347
3448/**
35- * Floating action bar shown at the bottom of the viewport when one or more
36- * rows are selected on a table that has workflow columns. Mirrors the shell
37- * + interaction pattern from the knowledge-base `<ActionBar>` so the bulk-
38- * action surface reads consistently across the product.
49+ * Floating action bar shown at the bottom of the table when one or more rows
50+ * are selected, OR when a single workflow-output cell is highlighted. Mirrors
51+ * the shell + interaction pattern from the knowledge-base `<ActionBar>`.
3952 *
40- * Two run actions: **Play** is the smart default (run only on empty / failed
41- * cells); **Refresh** forces a full re-run on every selected row. **Stop**
42- * only appears when ≥1 selected row has a running cell.
53+ * Rendered with `position: absolute` inside the table's container (not
54+ * `fixed`) so it scopes to the table's bounds — important for embedded mode,
55+ * where the table sits inside a panel and a fixed-positioned bar would land
56+ * centered on the whole viewport instead of the panel.
4357 */
4458export function TableActionBar ( {
4559 selectedCount,
@@ -48,10 +62,13 @@ export function TableActionBar({
4862 onRun,
4963 onRerun,
5064 onStopWorkflows,
65+ singleCell,
5166 isLoading = false ,
5267 className,
5368} : TableActionBarProps ) {
54- const visible = hasWorkflowColumns && selectedCount > 0
69+ const isMultiRow = selectedCount > 0
70+ const isSingleCell = ! isMultiRow && Boolean ( singleCell )
71+ const visible = hasWorkflowColumns && ( isMultiRow || isSingleCell )
5572 const stopLabel =
5673 runningCount === 1 ? 'Stop running workflow' : `Stop ${ runningCount } running workflows`
5774 const runLabel = 'Run workflows on empty or failed cells'
@@ -67,60 +84,121 @@ export function TableActionBar({
6784 animate = { { opacity : 1 , y : 0 } }
6885 exit = { { opacity : 0 , y : 10 } }
6986 transition = { { duration : 0.2 } }
70- className = { cn ( '-translate-x-1/2 fixed bottom-6 z-50 transform' , className ) }
71- style = { { left : '50%' } }
87+ className = { cn (
88+ '-translate-x-1/2 pointer-events-none absolute bottom-6 left-1/2 z-50 transform' ,
89+ className
90+ ) }
7291 >
73- < div className = 'flex items-center gap-2 rounded-[10px] border border-[var(--border)] bg-[var(--surface-2)] px-2 py-1.5' >
92+ < div className = 'pointer-events-auto flex items-center gap-2 rounded-[10px] border border-[var(--border)] bg-[var(--surface-2)] px-2 py-1.5' >
7493 < span className = 'px-1 text-[var(--text-secondary)] text-small' >
75- { selectedCount } selected
94+ { isMultiRow ? ` ${ selectedCount } selected` : 'Cell' }
7695 </ span >
7796
7897 < div className = 'flex items-center gap-[5px]' >
79- < Tooltip . Root >
80- < Tooltip . Trigger asChild >
81- < Button
82- variant = 'ghost'
83- onClick = { onRun }
84- disabled = { isLoading }
85- className = 'hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
86- aria-label = { runLabel }
87- >
88- < PlayOutline className = 'h-[12px] w-[12px]' />
89- </ Button >
90- </ Tooltip . Trigger >
91- < Tooltip . Content side = 'top' > { runLabel } </ Tooltip . Content >
92- </ Tooltip . Root >
98+ { isMultiRow && (
99+ < >
100+ < Tooltip . Root >
101+ < Tooltip . Trigger asChild >
102+ < Button
103+ variant = 'ghost'
104+ onClick = { onRun }
105+ disabled = { isLoading }
106+ className = 'hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
107+ aria-label = { runLabel }
108+ >
109+ < PlayOutline className = 'h-[12px] w-[12px]' />
110+ </ Button >
111+ </ Tooltip . Trigger >
112+ < Tooltip . Content side = 'top' > { runLabel } </ Tooltip . Content >
113+ </ Tooltip . Root >
93114
94- < Tooltip . Root >
95- < Tooltip . Trigger asChild >
96- < Button
97- variant = 'ghost'
98- onClick = { onRerun }
99- disabled = { isLoading }
100- className = 'hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
101- aria-label = { rerunLabel }
102- >
103- < RefreshCw className = 'h-[12px] w-[12px]' />
104- </ Button >
105- </ Tooltip . Trigger >
106- < Tooltip . Content side = 'top' > { rerunLabel } </ Tooltip . Content >
107- </ Tooltip . Root >
115+ < Tooltip . Root >
116+ < Tooltip . Trigger asChild >
117+ < Button
118+ variant = 'ghost'
119+ onClick = { onRerun }
120+ disabled = { isLoading }
121+ className = 'hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
122+ aria-label = { rerunLabel }
123+ >
124+ < RefreshCw className = 'h-[12px] w-[12px]' />
125+ </ Button >
126+ </ Tooltip . Trigger >
127+ < Tooltip . Content side = 'top' > { rerunLabel } </ Tooltip . Content >
128+ </ Tooltip . Root >
108129
109- { runningCount > 0 && (
110- < Tooltip . Root >
111- < Tooltip . Trigger asChild >
112- < Button
113- variant = 'ghost'
114- onClick = { onStopWorkflows }
115- disabled = { isLoading }
116- className = 'hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
117- aria-label = { stopLabel }
118- >
119- < Square className = 'h-[12px] w-[12px]' />
120- </ Button >
121- </ Tooltip . Trigger >
122- < Tooltip . Content side = 'top' > { stopLabel } </ Tooltip . Content >
123- </ Tooltip . Root >
130+ { runningCount > 0 && (
131+ < Tooltip . Root >
132+ < Tooltip . Trigger asChild >
133+ < Button
134+ variant = 'ghost'
135+ onClick = { onStopWorkflows }
136+ disabled = { isLoading }
137+ className = 'hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
138+ aria-label = { stopLabel }
139+ >
140+ < Square className = 'h-[12px] w-[12px]' />
141+ </ Button >
142+ </ Tooltip . Trigger >
143+ < Tooltip . Content side = 'top' > { stopLabel } </ Tooltip . Content >
144+ </ Tooltip . Root >
145+ ) }
146+ </ >
147+ ) }
148+
149+ { isSingleCell && singleCell && (
150+ < >
151+ { ! singleCell . isRunning && (
152+ < Tooltip . Root >
153+ < Tooltip . Trigger asChild >
154+ < Button
155+ variant = 'ghost'
156+ onClick = { singleCell . onRunCell }
157+ disabled = { isLoading }
158+ className = 'hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
159+ aria-label = 'Run cell'
160+ >
161+ < PlayOutline className = 'h-[12px] w-[12px]' />
162+ </ Button >
163+ </ Tooltip . Trigger >
164+ < Tooltip . Content side = 'top' > Run cell</ Tooltip . Content >
165+ </ Tooltip . Root >
166+ ) }
167+
168+ { singleCell . isRunning && (
169+ < Tooltip . Root >
170+ < Tooltip . Trigger asChild >
171+ < Button
172+ variant = 'ghost'
173+ onClick = { singleCell . onStopCell }
174+ disabled = { isLoading }
175+ className = 'hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
176+ aria-label = 'Stop cell'
177+ >
178+ < Square className = 'h-[12px] w-[12px]' />
179+ </ Button >
180+ </ Tooltip . Trigger >
181+ < Tooltip . Content side = 'top' > Stop cell</ Tooltip . Content >
182+ </ Tooltip . Root >
183+ ) }
184+
185+ { singleCell . canViewExecution && (
186+ < Tooltip . Root >
187+ < Tooltip . Trigger asChild >
188+ < Button
189+ variant = 'ghost'
190+ onClick = { singleCell . onViewExecution }
191+ disabled = { isLoading }
192+ className = 'hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
193+ aria-label = 'View execution'
194+ >
195+ < Eye className = 'h-[12px] w-[12px]' />
196+ </ Button >
197+ </ Tooltip . Trigger >
198+ < Tooltip . Content side = 'top' > View execution</ Tooltip . Content >
199+ </ Tooltip . Root >
200+ ) }
201+ </ >
124202 ) }
125203 </ div >
126204 </ div >
0 commit comments