@@ -3,18 +3,17 @@ import {
33 Badge ,
44 Button ,
55 Code ,
6+ CollapsibleCard ,
67 Combobox ,
78 type ComboboxOption ,
89 calculateGutterWidth ,
910 cn ,
10- Expandable ,
11- ExpandableContent ,
1211 getCodeEditorProps ,
13- handleKeyboardActivation ,
1412 highlight ,
1513 Input ,
1614 Label ,
1715 languages ,
16+ OverflowText ,
1817 Tooltip ,
1918} from '@sim/emcn'
2019import { ArrowLeftRight , Plus , Trash } from '@sim/emcn/icons'
@@ -388,52 +387,6 @@ export function FieldFormat({
388387 )
389388 }
390389
391- /**
392- * Renders the field header with name, type badge, and action buttons
393- */
394- const renderFieldHeader = ( field : Field , index : number ) => (
395- < div
396- role = 'group'
397- aria-label = { `${ title } ${ index + 1 } ` }
398- className = 'flex cursor-pointer items-center justify-between rounded-t-[3px] bg-[var(--surface-4)] px-2.5 py-[5px]'
399- onClick = { ( ) => toggleCollapse ( field . id ) }
400- onKeyDown = { ( event ) => {
401- if ( event . target !== event . currentTarget ) return
402- handleKeyboardActivation ( event , ( ) => toggleCollapse ( field . id ) )
403- } }
404- >
405- < div className = 'flex min-w-0 flex-1 items-center gap-2' >
406- < span className = 'block truncate text-[var(--text-tertiary)] text-sm' >
407- { field . name || `${ title } ${ index + 1 } ` }
408- </ span >
409- { field . name && showType && (
410- < Badge variant = 'type' size = 'sm' >
411- { field . type }
412- </ Badge >
413- ) }
414- </ div >
415- < div
416- role = 'presentation'
417- className = 'flex items-center gap-2 pl-2'
418- onClick = { ( e ) => e . stopPropagation ( ) }
419- >
420- < Button variant = 'ghost' onClick = { addField } disabled = { isReadOnly } className = 'h-auto p-0' >
421- < Plus className = 'size-[14px]' />
422- < span className = 'sr-only' > Add { title } </ span >
423- </ Button >
424- < Button
425- variant = 'ghost-destructive'
426- onClick = { ( ) => removeField ( field . id ) }
427- disabled = { isReadOnly }
428- className = 'h-auto p-0 hover-hover:opacity-90'
429- >
430- < Trash className = 'size-[14px]' />
431- < span className = 'sr-only' > Delete Field</ span >
432- </ Button >
433- </ div >
434- </ div >
435- )
436-
437390 /**
438391 * Renders the value input field based on the field type
439392 */
@@ -670,113 +623,145 @@ export function FieldFormat({
670623 return (
671624 < div className = 'space-y-2' >
672625 { fields . map ( ( field , index ) => (
673- < div
626+ < CollapsibleCard
674627 key = { field . id }
675628 data-field-id = { field . id }
676- className = 'overflow-hidden rounded-sm border border-[var(--border-1)]'
629+ role = 'group'
630+ aria-label = { `${ title } ${ index + 1 } ` }
631+ className = 'overflow-hidden'
632+ title = {
633+ < span className = 'flex min-w-0 items-center gap-2' >
634+ < OverflowText
635+ label = { field . name || `${ title } ${ index + 1 } ` }
636+ focusTarget = 'nearest-interactive'
637+ />
638+ { field . name && showType && (
639+ < Badge variant = 'type' size = 'sm' >
640+ { field . type }
641+ </ Badge >
642+ ) }
643+ </ span >
644+ }
645+ collapsed = { ! ! field . collapsed }
646+ onToggleCollapse = { ( ) => toggleCollapse ( field . id ) }
647+ animated
648+ actions = {
649+ < >
650+ < Button
651+ variant = 'ghost'
652+ onClick = { addField }
653+ disabled = { isReadOnly }
654+ className = 'h-auto p-0'
655+ >
656+ < Plus className = 'size-[14px]' />
657+ < span className = 'sr-only' > Add { title } </ span >
658+ </ Button >
659+ < Button
660+ variant = 'ghost-destructive'
661+ onClick = { ( ) => removeField ( field . id ) }
662+ disabled = { isReadOnly }
663+ className = 'h-auto p-0 hover-hover:opacity-90'
664+ >
665+ < Trash className = 'size-[14px]' />
666+ < span className = 'sr-only' > Delete Field</ span >
667+ </ Button >
668+ </ >
669+ }
677670 >
678- { renderFieldHeader ( field , index ) }
679-
680- < Expandable expanded = { ! field . collapsed } >
681- < ExpandableContent >
682- < div className = 'flex flex-col gap-2 rounded-b-[4px] border-[var(--border-1)] border-t bg-[var(--surface-2)] px-2.5 pt-1.5 pb-2.5' >
683- < div className = 'flex flex-col gap-1.5' >
684- { renderFieldLabel ( 'Name' ) }
685- < div className = 'relative' > { renderNameInput ( field ) } </ div >
686- </ div >
671+ < div className = 'flex flex-col gap-1.5' >
672+ { renderFieldLabel ( 'Name' ) }
673+ < div className = 'relative' > { renderNameInput ( field ) } </ div >
674+ </ div >
675+
676+ { showType && (
677+ < div className = 'flex flex-col gap-1.5' >
678+ { renderFieldLabel ( 'Type' ) }
679+ < Combobox
680+ options = { TYPE_OPTIONS }
681+ value = { field . type }
682+ onChange = { ( value ) => updateField ( field . id , 'type' , value ) }
683+ disabled = { isReadOnly }
684+ />
685+ </ div >
686+ ) }
687687
688- { showType && (
689- < div className = 'flex flex-col gap-1.5' >
690- { renderFieldLabel ( 'Type' ) }
691- < Combobox
692- options = { TYPE_OPTIONS }
693- value = { field . type }
694- onChange = { ( value ) => updateField ( field . id , 'type' , value ) }
695- disabled = { isReadOnly }
696- />
697- </ div >
698- ) }
699-
700- { showDescription && (
701- < div className = 'flex flex-col gap-1.5' >
702- { renderFieldLabel ( 'Description' ) }
703- < div className = 'relative' >
704- < Input
705- ref = { ( el ) => {
706- if ( el ) descriptionInputRefs . current [ field . id ] = el
707- } }
708- value = { field . description ?? '' }
709- onChange = { ( e ) => updateField ( field . id , 'description' , e . target . value ) }
710- onScroll = { ( e ) =>
711- syncDescriptionOverlayScroll ( field . id , e . currentTarget . scrollLeft )
712- }
713- onPaste = { ( ) =>
714- setTimeout ( ( ) => {
715- const input = descriptionInputRefs . current [ field . id ]
716- input && syncDescriptionOverlayScroll ( field . id , input . scrollLeft )
717- } , 0 )
718- }
719- placeholder = { descriptionPlaceholder }
720- disabled = { isReadOnly }
721- autoComplete = 'off'
722- className = 'allow-scroll w-full overflow-x-auto overflow-y-hidden text-transparent caret-foreground [letter-spacing:inherit] placeholder:text-muted-foreground/50'
723- />
724- < div
725- ref = { ( el ) => {
726- if ( el ) descriptionOverlayRefs . current [ field . id ] = el
727- } }
728- style = { { scrollbarWidth : 'none' } }
729- className = { cn (
730- 'pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-2 py-1.5 font-sans text-sm' ,
731- isReadOnly && 'opacity-50'
732- ) }
733- >
734- < span className = 'w-full whitespace-pre' style = { { minWidth : 'fit-content' } } >
735- { formatDisplayText (
736- field . description ?? '' ,
737- accessiblePrefixes
738- ? {
739- accessiblePrefixes,
740- workflowSearchHighlight : getActiveWorkflowSearchHighlight ( {
741- activeSearchTarget,
742- blockId,
743- subBlockId,
744- valuePath : [ index , 'description' ] ,
745- } ) ,
746- }
747- : {
748- highlightAll : true ,
749- workflowSearchHighlight : getActiveWorkflowSearchHighlight ( {
750- activeSearchTarget,
751- blockId,
752- subBlockId,
753- valuePath : [ index , 'description' ] ,
754- } ) ,
755- }
756- ) }
757- </ span >
758- </ div >
759- </ div >
760- </ div >
761- ) }
762-
763- { showValue && (
764- < div className = 'flex flex-col gap-1.5' >
765- { isFileFieldType ( field . type ) ? (
766- < div className = 'flex items-center justify-between' >
767- { renderFieldLabel ( 'Value' ) }
768- { renderFileModeToggle ( field ) }
769- </ div >
770- ) : (
771- renderFieldLabel ( 'Value' )
688+ { showDescription && (
689+ < div className = 'flex flex-col gap-1.5' >
690+ { renderFieldLabel ( 'Description' ) }
691+ < div className = 'relative' >
692+ < Input
693+ ref = { ( el ) => {
694+ if ( el ) descriptionInputRefs . current [ field . id ] = el
695+ } }
696+ value = { field . description ?? '' }
697+ onChange = { ( e ) => updateField ( field . id , 'description' , e . target . value ) }
698+ onScroll = { ( e ) =>
699+ syncDescriptionOverlayScroll ( field . id , e . currentTarget . scrollLeft )
700+ }
701+ onPaste = { ( ) =>
702+ setTimeout ( ( ) => {
703+ const input = descriptionInputRefs . current [ field . id ]
704+ input && syncDescriptionOverlayScroll ( field . id , input . scrollLeft )
705+ } , 0 )
706+ }
707+ placeholder = { descriptionPlaceholder }
708+ disabled = { isReadOnly }
709+ autoComplete = 'off'
710+ className = 'allow-scroll w-full overflow-x-auto overflow-y-hidden text-transparent caret-foreground [letter-spacing:inherit] placeholder:text-muted-foreground/50'
711+ />
712+ < div
713+ ref = { ( el ) => {
714+ if ( el ) descriptionOverlayRefs . current [ field . id ] = el
715+ } }
716+ style = { { scrollbarWidth : 'none' } }
717+ className = { cn (
718+ 'pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-2 py-1.5 font-sans text-sm' ,
719+ isReadOnly && 'opacity-50'
720+ ) }
721+ >
722+ < span className = 'w-full whitespace-pre' style = { { minWidth : 'fit-content' } } >
723+ { formatDisplayText (
724+ field . description ?? '' ,
725+ accessiblePrefixes
726+ ? {
727+ accessiblePrefixes,
728+ workflowSearchHighlight : getActiveWorkflowSearchHighlight ( {
729+ activeSearchTarget,
730+ blockId,
731+ subBlockId,
732+ valuePath : [ index , 'description' ] ,
733+ } ) ,
734+ }
735+ : {
736+ highlightAll : true ,
737+ workflowSearchHighlight : getActiveWorkflowSearchHighlight ( {
738+ activeSearchTarget,
739+ blockId,
740+ subBlockId,
741+ valuePath : [ index , 'description' ] ,
742+ } ) ,
743+ }
772744 ) }
773- < div className = 'relative' > { renderValueInput ( field ) } </ div >
774- </ div >
775- ) }
745+ </ span >
746+ </ div >
776747 </ div >
777- </ ExpandableContent >
778- </ Expandable >
779- </ div >
748+ </ div >
749+ ) }
750+
751+ { showValue && (
752+ < div className = 'flex flex-col gap-1.5' >
753+ { isFileFieldType ( field . type ) ? (
754+ < div className = 'flex items-center justify-between' >
755+ { renderFieldLabel ( 'Value' ) }
756+ { renderFileModeToggle ( field ) }
757+ </ div >
758+ ) : (
759+ renderFieldLabel ( 'Value' )
760+ ) }
761+ < div className = 'relative' > { renderValueInput ( field ) } </ div >
762+ </ div >
763+ ) }
764+ </ CollapsibleCard >
780765 ) ) }
781766 </ div >
782767 )
0 commit comments