@@ -81,6 +81,56 @@ function ProviderPreviewIcon({ providerId }: { providerId?: string }) {
8181 )
8282}
8383
84+ interface FeatureToggleItemProps {
85+ feature : PermissionFeature
86+ enabled : boolean
87+ color : string
88+ isInView : boolean
89+ delay : number
90+ textClassName : string
91+ transition : Record < string , unknown >
92+ onToggle : ( ) => void
93+ }
94+
95+ function FeatureToggleItem ( {
96+ feature,
97+ enabled,
98+ color,
99+ isInView,
100+ delay,
101+ textClassName,
102+ transition,
103+ onToggle,
104+ } : FeatureToggleItemProps ) {
105+ return (
106+ < motion . div
107+ key = { feature . key }
108+ role = 'button'
109+ tabIndex = { 0 }
110+ aria-label = { `Toggle ${ feature . name } ` }
111+ aria-pressed = { enabled }
112+ className = 'flex cursor-pointer items-center gap-2 rounded-[4px] py-0.5'
113+ initial = { { opacity : 0 , x : - 6 } }
114+ animate = { isInView ? { opacity : 1 , x : 0 } : { } }
115+ transition = { { ...transition , delay } }
116+ onClick = { onToggle }
117+ onKeyDown = { ( e ) => {
118+ if ( e . key === 'Enter' || e . key === ' ' ) {
119+ e . preventDefault ( )
120+ onToggle ( )
121+ }
122+ } }
123+ whileTap = { { scale : 0.98 } }
124+ >
125+ < CheckboxIcon checked = { enabled } color = { color } />
126+ < ProviderPreviewIcon providerId = { feature . providerId } />
127+ < span className = { textClassName } style = { { color : enabled ? '#F6F6F6AA' : '#F6F6F640' } } >
128+ { feature . name }
129+ </ span >
130+ </ motion . div >
131+ )
132+ }
133+
84134export function AccessControlPanel ( ) {
85135 const ref = useRef ( null )
86136 const isInView = useInView ( ref , { once : true , margin : '-40px' } )
@@ -97,39 +147,25 @@ export function AccessControlPanel() {
97147
98148 return (
99149 < div key = { category . label } className = { catIdx > 0 ? 'mt-4' : '' } >
100- < span className = 'font-[430] font-season text-[#F6F6F6]/30 text-[10px] uppercase leading-none tracking-[0.08em]' >
150+ < span className = 'font-[430] font-season text-[#F6F6F6]/55 text-[10px] uppercase leading-none tracking-[0.08em]' >
101151 { category . label }
102152 </ span >
103153 < div className = 'mt-2 grid grid-cols-2 gap-x-4 gap-y-2' >
104- { category . features . map ( ( feature , featIdx ) => {
105- const enabled = accessState [ feature . key ]
106-
107- return (
108- < motion . div
109- key = { feature . key }
110- className = 'flex cursor-pointer items-center gap-2 rounded-[4px] py-0.5'
111- initial = { { opacity : 0 , x : - 6 } }
112- animate = { isInView ? { opacity : 1 , x : 0 } : { } }
113- transition = { {
114- delay : 0.05 + ( offsetBefore + featIdx ) * 0.04 ,
115- duration : 0.3 ,
116- } }
117- onClick = { ( ) =>
118- setAccessState ( ( prev ) => ( { ...prev , [ feature . key ] : ! prev [ feature . key ] } ) )
119- }
120- whileTap = { { scale : 0.98 } }
121- >
122- < CheckboxIcon checked = { enabled } color = { category . color } />
123- < ProviderPreviewIcon providerId = { feature . providerId } />
124- < span
125- className = 'truncate font-[430] font-season text-[13px] leading-none tracking-[0.02em]'
126- style = { { color : enabled ? '#F6F6F6AA' : '#F6F6F640' } }
127- >
128- { feature . name }
129- </ span >
130- </ motion . div >
131- )
132- } ) }
154+ { category . features . map ( ( feature , featIdx ) => (
155+ < FeatureToggleItem
156+ key = { feature . key }
157+ feature = { feature }
158+ enabled = { accessState [ feature . key ] }
159+ color = { category . color }
160+ isInView = { isInView }
161+ delay = { 0.05 + ( offsetBefore + featIdx ) * 0.04 }
162+ textClassName = 'truncate font-[430] font-season text-[13px] leading-none tracking-[0.02em]'
163+ transition = { { duration : 0.3 } }
164+ onToggle = { ( ) =>
165+ setAccessState ( ( prev ) => ( { ...prev , [ feature . key ] : ! prev [ feature . key ] } ) )
166+ }
167+ />
168+ ) ) }
133169 </ div >
134170 </ div >
135171 )
@@ -140,43 +176,31 @@ export function AccessControlPanel() {
140176 < div className = 'hidden lg:block' >
141177 { PERMISSION_CATEGORIES . map ( ( category , catIdx ) => (
142178 < div key = { category . label } className = { catIdx > 0 ? 'mt-4' : '' } >
143- < span className = 'font-[430] font-season text-[#F6F6F6]/30 text-[10px] uppercase leading-none tracking-[0.08em]' >
179+ < span className = 'font-[430] font-season text-[#F6F6F6]/55 text-[10px] uppercase leading-none tracking-[0.08em]' >
144180 { category . label }
145181 </ span >
146182 < div className = 'mt-2 grid grid-cols-2 gap-x-4 gap-y-2' >
147183 { category . features . map ( ( feature , featIdx ) => {
148- const enabled = accessState [ feature . key ]
149184 const currentIndex =
150185 PERMISSION_CATEGORIES . slice ( 0 , catIdx ) . reduce (
151186 ( sum , c ) => sum + c . features . length ,
152187 0
153188 ) + featIdx
154189
155190 return (
156- < motion . div
191+ < FeatureToggleItem
157192 key = { feature . key }
158- className = 'flex cursor-pointer items-center gap-2 rounded-[4px] py-0.5'
159- initial = { { opacity : 0 , x : - 6 } }
160- animate = { isInView ? { opacity : 1 , x : 0 } : { } }
161- transition = { {
162- delay : 0.1 + currentIndex * 0.04 ,
163- duration : 0.3 ,
164- ease : [ 0.25 , 0.46 , 0.45 , 0.94 ] ,
165- } }
166- onClick = { ( ) =>
193+ feature = { feature }
194+ enabled = { accessState [ feature . key ] }
195+ color = { category . color }
196+ isInView = { isInView }
197+ delay = { 0.1 + currentIndex * 0.04 }
198+ textClassName = 'truncate font-[430] font-season text-[11px] leading-none tracking-[0.02em] transition-opacity duration-200'
199+ transition = { { duration : 0.3 , ease : [ 0.25 , 0.46 , 0.45 , 0.94 ] } }
200+ onToggle = { ( ) =>
167201 setAccessState ( ( prev ) => ( { ...prev , [ feature . key ] : ! prev [ feature . key ] } ) )
168202 }
169- whileTap = { { scale : 0.98 } }
170- >
171- < CheckboxIcon checked = { enabled } color = { category . color } />
172- < ProviderPreviewIcon providerId = { feature . providerId } />
173- < span
174- className = 'truncate font-[430] font-season text-[11px] leading-none tracking-[0.02em] transition-opacity duration-200'
175- style = { { color : enabled ? '#F6F6F6AA' : '#F6F6F640' } }
176- >
177- { feature . name }
178- </ span >
179- </ motion . div >
203+ />
180204 )
181205 } ) }
182206 </ div >
0 commit comments