@@ -248,6 +248,12 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved,
248248 > ( null ) ;
249249 const [ filter , setFilter ] = React . useState ( '' ) ;
250250 const [ showOnlyEnabled , setShowOnlyEnabled ] = React . useState ( false ) ;
251+ // objectui#2600 B1 — the pillar is called "Permission Matrix", so the matrix
252+ // must reach the first screen. Name/label change rarely and the capability
253+ // picker floods the top with option chips even at zero grants, so both start
254+ // collapsed to a one-line summary and the user opts in to edit them.
255+ const [ basicsOpen , setBasicsOpen ] = React . useState ( false ) ;
256+ const [ capsOpen , setCapsOpen ] = React . useState ( false ) ;
251257 // Embedded-mode History sheet (see `embedded` prop doc).
252258 const [ historyOpen , setHistoryOpen ] = React . useState ( false ) ;
253259 // All permission-set api-names — the admin-scope editor's assignable
@@ -648,58 +654,76 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved,
648654 </ div >
649655 ) }
650656
651- { /* Header strip — name / label / provenance + default badges */ }
652- < div className = "px-6 py-3 border-b bg-muted/30 flex flex-wrap items-end gap-4" >
653- < div className = "space-y-1" >
654- < Label htmlFor = "perm-name" className = "text-xs" > { t ( 'perm.field.name' ) } </ Label >
655- < Input
656- id = "perm-name"
657- value = { draft . name }
658- disabled = { ! writable }
659- onChange = { ( e ) => setDraft ( ( p ) => ( { ...p , name : e . target . value } ) ) }
660- className = "h-8 w-56"
661- />
662- </ div >
663- < div className = "space-y-1" >
664- < Label htmlFor = "perm-label" className = "text-xs" > { t ( 'perm.field.label' ) } </ Label >
665- < Input
666- id = "perm-label"
667- value = { draft . label ?? '' }
668- disabled = { ! writable }
669- onChange = { ( e ) => setDraft ( ( p ) => ( { ...p , label : e . target . value } ) ) }
670- className = "h-8 w-72"
671- />
672- </ div >
673- { /* ADR-0090 D2: the profile toggle is gone. What matters instead is
674- WHO OWNS the set (provenance, ADR-0086 D3) and whether it is the
675- package's suggested default (ADR-0090 D5). */ }
676- < div className = "flex items-center gap-1.5 pb-1" >
657+ { /* Identity summary — collapsible (objectui#2600 B1). Name/label change
658+ rarely, so the strip collapses to a one-line summary and the matrix
659+ reaches the first screen; the row toggles the editable inputs open. */ }
660+ < div className = "border-b bg-muted/30" >
661+ < button
662+ type = "button"
663+ onClick = { ( ) => setBasicsOpen ( ( o ) => ! o ) }
664+ aria-expanded = { basicsOpen }
665+ className = "w-full px-6 py-2.5 flex items-center gap-2 text-left hover:bg-muted/50 transition-colors"
666+ >
667+ { basicsOpen ? (
668+ < ChevronDown className = "h-4 w-4 shrink-0 text-muted-foreground" />
669+ ) : (
670+ < ChevronRight className = "h-4 w-4 shrink-0 text-muted-foreground" />
671+ ) }
672+ { /* The api-name already sits in the PageShell breadcrumb (font-mono),
673+ so the summary carries the human label — no need to repeat it. */ }
674+ < span className = "font-medium truncate" > { draft . label || draft . name } </ span >
677675 { /* [A4 framework#2920] Provenance tri-state — platform / package /
678- admin(custom) — mirrors the unified sys_* `managed_by` vocab so
679- the Studio matrix and the Setup record page read the same. */ }
680- < Badge variant = "outline" className = "text-[10px]" >
676+ admin(custom) — mirrors the unified sys_* `managed_by` vocab. */ }
677+ < Badge variant = "outline" className = "text-[10px] shrink-0" >
681678 { draft . managedBy === 'platform'
682679 ? t ( 'perm.badge.platform' )
683680 : draft . managedBy === 'package' || packageId
684681 ? t ( 'perm.badge.package' )
685682 : t ( 'perm.badge.custom' ) }
686683 </ Badge >
687684 { ! ! draft . isDefault && (
688- < Badge variant = "secondary" className = "text-[10px]" > { t ( 'perm.badge.default' ) } </ Badge >
685+ < Badge variant = "secondary" className = "text-[10px] shrink-0 " > { t ( 'perm.badge.default' ) } </ Badge >
689686 ) }
690- </ div >
691- { ! writable && (
692- // Same badge slot, two distinct reasons: a read-only PACKAGE
693- // (host gate — mirror the top-bar wording so the screen is not
694- // self-contradictory) vs. metadata writes disabled environment-
695- // wide (type gate).
696- < Badge
697- variant = "secondary"
698- className = "ml-auto"
699- title = { readOnly ? t ( 'engine.studio.pkg.readonlyHint' ) : undefined }
700- >
701- { readOnly ? t ( 'engine.studio.pkg.readonly' ) : t ( 'perm.readOnly' ) }
702- </ Badge >
687+ { writable && (
688+ < span className = "text-xs text-muted-foreground shrink-0" > { t ( 'perm.basics.editHint' ) } </ span >
689+ ) }
690+ { ! writable && (
691+ // Same badge slot, two distinct reasons: a read-only PACKAGE
692+ // (host gate — mirror the top-bar wording so the screen is not
693+ // self-contradictory) vs. metadata writes disabled environment-
694+ // wide (type gate).
695+ < Badge
696+ variant = "secondary"
697+ className = "ml-auto shrink-0"
698+ title = { readOnly ? t ( 'engine.studio.pkg.readonlyHint' ) : undefined }
699+ >
700+ { readOnly ? t ( 'engine.studio.pkg.readonly' ) : t ( 'perm.readOnly' ) }
701+ </ Badge >
702+ ) }
703+ </ button >
704+ { basicsOpen && (
705+ < div className = "px-6 pb-3 flex flex-wrap items-end gap-4" >
706+ < div className = "space-y-1" >
707+ < Label htmlFor = "perm-name" className = "text-xs" > { t ( 'perm.field.name' ) } </ Label >
708+ < Input
709+ id = "perm-name"
710+ value = { draft . name }
711+ disabled = { ! writable }
712+ onChange = { ( e ) => setDraft ( ( p ) => ( { ...p , name : e . target . value } ) ) }
713+ className = "h-8 w-56"
714+ />
715+ </ div >
716+ < div className = "space-y-1" >
717+ < Label htmlFor = "perm-label" className = "text-xs" > { t ( 'perm.field.label' ) } </ Label >
718+ < Input
719+ id = "perm-label"
720+ value = { draft . label ?? '' }
721+ disabled = { ! writable }
722+ onChange = { ( e ) => setDraft ( ( p ) => ( { ...p , label : e . target . value } ) ) }
723+ className = "h-8 w-72"
724+ />
725+ </ div >
726+ </ div >
703727 ) }
704728 </ div >
705729
@@ -709,21 +733,40 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved,
709733 as PermissionSetDraft.systemPermissions (string[]); the picker
710734 round-trips via a JSON string, so parse back into the array the
711735 draft model uses. Persisted by the whole-record Save at env scope. */ }
712- < div className = "px-6 py-3 border-b" >
713- < Label className = "text-xs" > { t ( 'perm.field.systemCapabilities' ) } </ Label >
714- < p className = "text-xs text-muted-foreground mt-0.5 mb-2" >
715- { t ( 'perm.field.systemCapabilitiesHelp' ) }
716- </ p >
717- < CapabilityMultiSelectField
718- value = { JSON . stringify ( draft . systemPermissions ?? [ ] ) }
719- onChange = { ( v : unknown ) =>
720- setDraft ( ( p ) => ( { ...p , systemPermissions : parseCapabilityNames ( v ) } ) )
721- }
722- field = { { name : 'system_permissions' } as any }
723- dataSource = { adapter as any }
724- readonly = { ! writable }
725- />
726- </ div >
736+ { writable && ( draft . systemPermissions ?? [ ] ) . length === 0 && ! capsOpen ? (
737+ // objectui#2600 B1 — zero-grant writable sets used to render the full
738+ // picker's option chips, which read as already-owned capabilities.
739+ // Collapse to an explicit "none granted · add" affordance instead.
740+ < div className = "px-6 py-2.5 border-b flex items-center gap-2 text-xs" >
741+ < Label className = "text-xs text-muted-foreground" > { t ( 'perm.field.systemCapabilities' ) } </ Label >
742+ < span className = "text-muted-foreground" > ·</ span >
743+ < span className = "text-muted-foreground" > { t ( 'perm.cap.none' ) } </ span >
744+ < Button
745+ variant = "ghost"
746+ size = "sm"
747+ className = "h-6 px-2 text-xs"
748+ onClick = { ( ) => setCapsOpen ( true ) }
749+ >
750+ + { t ( 'perm.cap.add' ) }
751+ </ Button >
752+ </ div >
753+ ) : (
754+ < div className = "px-6 py-3 border-b" >
755+ < Label className = "text-xs" > { t ( 'perm.field.systemCapabilities' ) } </ Label >
756+ < p className = "text-xs text-muted-foreground mt-0.5 mb-2" >
757+ { t ( 'perm.field.systemCapabilitiesHelp' ) }
758+ </ p >
759+ < CapabilityMultiSelectField
760+ value = { JSON . stringify ( draft . systemPermissions ?? [ ] ) }
761+ onChange = { ( v : unknown ) =>
762+ setDraft ( ( p ) => ( { ...p , systemPermissions : parseCapabilityNames ( v ) } ) )
763+ }
764+ field = { { name : 'system_permissions' } as any }
765+ dataSource = { adapter as any }
766+ readonly = { ! writable }
767+ />
768+ </ div >
769+ ) }
727770
728771 { /* Filter bar */ }
729772 < div className = "px-6 py-3 border-b flex items-center gap-3" >
0 commit comments