@@ -40,8 +40,19 @@ import { useIsMobile } from '../../hooks/use-mobile';
4040
4141export interface ActionBarSchema {
4242 type : 'action:bar' ;
43- /** Actions to render */
43+ /** Business actions to render — subject to inline/overflow split via { @link maxVisible} */
4444 actions ?: ActionSchema [ ] ;
45+ /**
46+ * System/chrome actions (Duplicate, Export, View History, Delete, etc.) that
47+ * are *always* placed in the overflow menu — never inline — regardless of
48+ * {@link maxVisible}. They share a single overflow button with any business
49+ * actions that spilled past {@link maxVisible}, guaranteeing at most one
50+ * "More" menu per bar.
51+ *
52+ * The first system action is automatically separated from business-overflow
53+ * entries by a menu separator.
54+ */
55+ systemActions ?: ActionSchema [ ] ;
4556 /** Filter actions by this location */
4657 location ?: ActionLocation ;
4758 /** Maximum visible inline actions before overflow into "More" menu (default: 3) */
@@ -70,13 +81,29 @@ const ActionBarRenderer = forwardRef<HTMLDivElement, { schema: ActionBarSchema;
7081 'data-obj-type' : dataObjType ,
7182 style,
7283 data,
84+ // Strip schema metadata props that are consumed via `schema.*` and
85+ // must NOT be spread onto the underlying DOM element (avoids React
86+ // "unknown DOM attribute" warnings — especially for camelCase keys
87+ // like `systemActions`, `mobileMaxVisible`).
88+ /* eslint-disable @typescript-eslint/no-unused-vars */
89+ actions : _schemaActions ,
90+ systemActions : _schemaSystemActions ,
91+ location : _schemaLocation ,
92+ maxVisible : _schemaMaxVisible ,
93+ mobileMaxVisible : _schemaMobileMaxVisible ,
94+ direction : _schemaDirection ,
95+ gap : _schemaGap ,
96+ variant : _schemaVariant ,
97+ size : _schemaSize ,
98+ visible : _schemaVisible ,
99+ /* eslint-enable @typescript-eslint/no-unused-vars */
73100 ...rest
74101 } = props ;
75102
76103 const isVisible = useCondition ( schema . visible ? `\${${ schema . visible } }` : undefined ) ;
77104 const isMobile = useIsMobile ( ) ;
78105
79- // Filter actions by location and deduplicate by name
106+ // Filter business actions by location and deduplicate by name
80107 const filteredActions = useMemo ( ( ) => {
81108 const actions = schema . actions || [ ] ;
82109 const located = ! schema . location
@@ -94,8 +121,21 @@ const ActionBarRenderer = forwardRef<HTMLDivElement, { schema: ActionBarSchema;
94121 } ) ;
95122 } , [ schema . actions , schema . location ] ) ;
96123
97- // Split into visible inline actions and overflow
98- // On mobile, show fewer actions inline (default: 1)
124+ // System actions: always go into the overflow menu, deduped by name,
125+ // never filtered by location (they're chrome, not business logic).
126+ const systemActions = useMemo ( ( ) => {
127+ const actions = schema . systemActions || [ ] ;
128+ const seen = new Set < string > ( ) ;
129+ return actions . filter ( a => {
130+ if ( ! a . name ) return true ;
131+ if ( seen . has ( a . name ) ) return false ;
132+ seen . add ( a . name ) ;
133+ return true ;
134+ } ) ;
135+ } , [ schema . systemActions ] ) ;
136+
137+ // Split business actions into visible inline and overflow.
138+ // On mobile, show fewer actions inline (default: 1).
99139 const maxVisible = isMobile
100140 ? ( schema . mobileMaxVisible ?? 1 )
101141 : ( schema . maxVisible ?? 3 ) ;
@@ -109,19 +149,34 @@ const ActionBarRenderer = forwardRef<HTMLDivElement, { schema: ActionBarSchema;
109149 } ;
110150 } , [ filteredActions , maxVisible ] ) ;
111151
152+ // Merge business overflow with system actions into a single overflow list.
153+ // Insert a visual separator before the first system action when both
154+ // groups coexist, so users can distinguish domain vs. chrome actions.
155+ const combinedOverflow = useMemo < ActionSchema [ ] > ( ( ) => {
156+ if ( systemActions . length === 0 ) return overflowActions ;
157+ if ( overflowActions . length === 0 ) return systemActions ;
158+ const [ firstSys , ...restSys ] = systemActions ;
159+ const firstWithSeparator : ActionSchema = {
160+ ...firstSys ,
161+ tags : [ ...( firstSys . tags || [ ] ) , 'separator-before' ] ,
162+ } ;
163+ return [ ...overflowActions , firstWithSeparator , ...restSys ] ;
164+ } , [ overflowActions , systemActions ] ) ;
165+
112166 if ( schema . visible && ! isVisible ) return null ;
113- if ( filteredActions . length === 0 ) return null ;
167+ if ( filteredActions . length === 0 && systemActions . length === 0 ) return null ;
114168
115169 const direction = schema . direction || 'horizontal' ;
116170 const gap = schema . gap || 'gap-2' ;
117171
118- // Render overflow menu for excess actions
119- const MenuRenderer = overflowActions . length > 0 ? ComponentRegistry . get ( 'action:menu' ) : null ;
172+ // Render a single overflow menu for any combination of business-overflow
173+ // + system actions. This guarantees at most ONE "More" button per bar.
174+ const MenuRenderer = combinedOverflow . length > 0 ? ComponentRegistry . get ( 'action:menu' ) : null ;
120175 const overflowMenu = MenuRenderer ? (
121176 < MenuRenderer
122177 schema = { {
123178 type : 'action:menu' as const ,
124- actions : overflowActions ,
179+ actions : combinedOverflow ,
125180 variant : schema . variant || 'ghost' ,
126181 size : schema . size || 'sm' ,
127182 } }
@@ -163,7 +218,7 @@ const ActionBarRenderer = forwardRef<HTMLDivElement, { schema: ActionBarSchema;
163218 ) ;
164219 } ) }
165220
166- { overflowActions . length > 0 && overflowMenu }
221+ { combinedOverflow . length > 0 && overflowMenu }
167222 </ div >
168223 ) ;
169224 } ,
@@ -176,6 +231,7 @@ ComponentRegistry.register('action:bar', ActionBarRenderer, {
176231 label : 'Action Bar' ,
177232 inputs : [
178233 { name : 'actions' , type : 'object' , label : 'Actions' } ,
234+ { name : 'systemActions' , type : 'object' , label : 'System Actions (always in overflow)' } ,
179235 {
180236 name : 'location' ,
181237 type : 'enum' ,
0 commit comments