feat(17-06): add success toasts to admin create-member and reset-password

- Add toast state + 3000ms auto-dismiss useEffect to AdminPage
- Set toast 'Member added.' in createMemberMutation.onSuccess
- Propagate 'Password reset.' up from ResetPasswordSheet via onSuccess callback
- Render role=status/aria-live=polite toast with CheckCircle icon
- Phone toast offset uses calc(var(--bottom-chrome-h) + var(--space-4)) to clear BottomTabBar
This commit is contained in:
Lucas Berger
2026-06-18 12:53:12 -04:00
parent 9e080fb22a
commit 620d64138a
+394 -220
View File
@@ -55,6 +55,22 @@ const sectionLabelStyle: React.CSSProperties = {
export function AdminPage() { export function AdminPage() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
// Phone detection for toast bottom offset
const phone = typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches;
// Success toast state (D-08)
const [toast, setToast] = useState<string | null>(null);
// Auto-dismiss toast after 3000ms — mirrors SyncStateToast lines 72-79
useEffect(() => {
if (!toast) return;
const timer = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(timer);
}, [toast]);
// Two-tab navigation state (D-10)
const [activeTab, setActiveTab] = useState<'members' | 'settings'>('members');
// Credential sheet state // Credential sheet state
const [sheetOpen, setSheetOpen] = useState(false); const [sheetOpen, setSheetOpen] = useState(false);
const [sheetMode, setSheetMode] = useState<CredentialSheetMode>('admin-add'); const [sheetMode, setSheetMode] = useState<CredentialSheetMode>('admin-add');
@@ -185,6 +201,29 @@ export function AdminPage() {
}, },
}); });
// Roving tabindex keyboard handler for the two-tab strip (D-10)
function handleTabKeyDown(e: React.KeyboardEvent, current: 'members' | 'settings') {
if (e.key === 'ArrowRight') {
e.preventDefault();
const next = current === 'members' ? 'settings' : 'members';
setActiveTab(next);
(
e.currentTarget.parentElement?.querySelector(
`[id="admin-tab-${next}"]`,
) as HTMLElement | null
)?.focus();
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
const prev = current === 'settings' ? 'members' : 'settings';
setActiveTab(prev);
(
e.currentTarget.parentElement?.querySelector(
`[id="admin-tab-${prev}"]`,
) as HTMLElement | null
)?.focus();
}
}
// Open credential sheet for a member // Open credential sheet for a member
function openSheet(member: AdminMember, buttonRef: React.RefObject<HTMLButtonElement | null>) { function openSheet(member: AdminMember, buttonRef: React.RefObject<HTMLButtonElement | null>) {
// Capture the button so focus can return on close // Capture the button so focus can return on close
@@ -219,6 +258,7 @@ export function AdminPage() {
setCreateError(null); setCreateError(null);
void queryClient.invalidateQueries({ queryKey: ['admin', 'members'] }); void queryClient.invalidateQueries({ queryKey: ['admin', 'members'] });
void queryClient.invalidateQueries({ queryKey: ['me'] }); void queryClient.invalidateQueries({ queryKey: ['me'] });
setToast('Member added.');
}, },
onError: (err) => { onError: (err) => {
const msg = err instanceof Error ? err.message : 'server'; const msg = err instanceof Error ? err.message : 'server';
@@ -266,7 +306,7 @@ export function AdminPage() {
{/* Page heading */} {/* Page heading */}
<h1 <h1
style={{ style={{
margin: '0 0 var(--space-8, 32px) 0', margin: '0 0 var(--space-6, 24px) 0',
fontSize: 'var(--text-heading-size, 18px)', fontSize: 'var(--text-heading-size, 18px)',
fontWeight: 600, fontWeight: 600,
lineHeight: 'var(--text-heading-line-height, 1.25)', lineHeight: 'var(--text-heading-line-height, 1.25)',
@@ -276,7 +316,60 @@ export function AdminPage() {
Admin Settings Admin Settings
</h1> </h1>
{/* ── MEMBERS section ─────────────────────────────────────────────── */} {/* ── Two-tab strip (D-10) ──────────────────────────────────────────── */}
<div
role="tablist"
style={{
display: 'flex',
borderBottom: '1px solid var(--color-border-subtle, var(--color-border))',
marginBottom: 'var(--space-6, 24px)',
}}
>
{(['members', 'settings'] as const).map((id) => (
<button
key={id}
role="tab"
id={`admin-tab-${id}`}
aria-selected={activeTab === id}
aria-controls={`admin-panel-${id}`}
tabIndex={activeTab === id ? 0 : -1}
onClick={() => setActiveTab(id)}
onKeyDown={(e) => handleTabKeyDown(e, id)}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 'var(--space-3, 12px) var(--space-4, 16px)',
minHeight: '44px',
fontSize: 'var(--text-label-size, 13px)',
fontWeight: activeTab === id ? 600 : 400,
color:
activeTab === id
? 'var(--color-text-primary)'
: 'var(--color-text-secondary)',
borderBottom:
activeTab === id
? '2px solid var(--color-member-0)'
: '2px solid transparent',
transition: 'color 0.1s ease, border-color 0.1s ease',
fontFamily: 'var(--font-family-base)',
}}
>
{id === 'members' ? 'Members & Accounts' : 'Settings'}
</button>
))}
</div>
{/* ── Tab panel: Members & Accounts ────────────────────────────────── */}
<div
role="tabpanel"
id="admin-panel-members"
aria-labelledby="admin-tab-members"
tabIndex={0}
hidden={activeTab !== 'members'}
>
{/* ── MEMBERS section ───────────────────────────────────────────── */}
<section aria-label="Members" style={{ marginBottom: 'var(--space-8, 32px)' }}> <section aria-label="Members" style={{ marginBottom: 'var(--space-8, 32px)' }}>
<div style={sectionLabelStyle}>Members</div> <div style={sectionLabelStyle}>Members</div>
@@ -324,6 +417,242 @@ export function AdminPage() {
)} )}
</section> </section>
{/* ── LOCAL ACCOUNTS section ──────────────────────────────────────── */}
<section aria-label="Local Accounts" style={{ marginBottom: 'var(--space-8, 32px)' }}>
<div style={sectionLabelStyle}>Local Accounts</div>
{/* Surface 11A — Add member inline form */}
<div
style={{
border: '1px solid var(--color-border-subtle, var(--color-border))',
borderRadius: '8px',
padding: 'var(--space-4, 16px)',
marginBottom: 'var(--space-6, 24px)',
}}
>
<div
style={{
fontSize: 'var(--text-body-size, 15px)',
fontWeight: 600,
color: 'var(--color-text-primary)',
marginBottom: 'var(--space-4, 16px)',
}}
>
Add member
</div>
{/* Display name */}
<div style={{ marginBottom: 'var(--space-3, 12px)' }}>
<label
htmlFor="admin-create-display-name"
style={{
display: 'block',
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 600,
color: 'var(--color-text-primary)',
marginBottom: 'var(--space-1, 4px)',
}}
>
Display name
</label>
<input
id="admin-create-display-name"
type="text"
value={createDisplayName}
onChange={(e) => setCreateDisplayName(e.target.value)}
style={{
width: '100%',
boxSizing: 'border-box',
padding: 'var(--space-3, 12px) var(--space-4, 16px)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--space-1, 4px)',
fontSize: 'var(--text-body-size, 15px)',
color: 'var(--color-text-primary)',
background: 'var(--color-surface)',
fontFamily: 'var(--font-family-base)',
outline: 'none',
minHeight: '44px',
}}
/>
</div>
{/* Username */}
<div style={{ marginBottom: 'var(--space-3, 12px)' }}>
<label
htmlFor="admin-create-username"
style={{
display: 'block',
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 600,
color: 'var(--color-text-primary)',
marginBottom: 'var(--space-1, 4px)',
}}
>
Username
</label>
<input
id="admin-create-username"
type="text"
autoComplete="off"
spellCheck={false}
autoCapitalize="none"
value={createUsername}
onChange={(e) => setCreateUsername(e.target.value)}
style={{
width: '100%',
boxSizing: 'border-box',
padding: 'var(--space-3, 12px) var(--space-4, 16px)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--space-1, 4px)',
fontSize: 'var(--text-body-size, 15px)',
color: 'var(--color-text-primary)',
background: 'var(--color-surface)',
fontFamily: 'var(--font-family-base)',
outline: 'none',
minHeight: '44px',
}}
/>
</div>
{/* Initial password */}
<div style={{ marginBottom: 'var(--space-3, 12px)' }}>
<label
htmlFor="admin-create-password"
style={{
display: 'block',
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 600,
color: 'var(--color-text-primary)',
marginBottom: 'var(--space-1, 4px)',
}}
>
Initial password
</label>
<input
id="admin-create-password"
type="password"
autoComplete="new-password"
value={createPassword}
onChange={(e) => setCreatePassword(e.target.value)}
style={{
width: '100%',
boxSizing: 'border-box',
padding: 'var(--space-3, 12px) var(--space-4, 16px)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--space-1, 4px)',
fontSize: 'var(--text-body-size, 15px)',
color: 'var(--color-text-primary)',
background: 'var(--color-surface)',
fontFamily: 'var(--font-family-base)',
outline: 'none',
minHeight: '44px',
}}
/>
</div>
{/* Confirm password */}
<div style={{ marginBottom: 'var(--space-4, 16px)' }}>
<label
htmlFor="admin-create-confirm-password"
style={{
display: 'block',
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 600,
color: 'var(--color-text-primary)',
marginBottom: 'var(--space-1, 4px)',
}}
>
Confirm password
</label>
<input
id="admin-create-confirm-password"
type="password"
autoComplete="new-password"
value={createConfirmPassword}
onChange={(e) => setCreateConfirmPassword(e.target.value)}
style={{
width: '100%',
boxSizing: 'border-box',
padding: 'var(--space-3, 12px) var(--space-4, 16px)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--space-1, 4px)',
fontSize: 'var(--text-body-size, 15px)',
color: 'var(--color-text-primary)',
background: 'var(--color-surface)',
fontFamily: 'var(--font-family-base)',
outline: 'none',
minHeight: '44px',
}}
/>
</div>
{/* Inline error */}
{createError && (
<div
style={{
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 400,
color: 'var(--color-destructive)',
marginBottom: 'var(--space-3, 12px)',
}}
>
{createError}
</div>
)}
{/* Action row */}
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<button
type="button"
disabled={createSubmitDisabled}
onClick={() => {
setCreateError(null);
createMemberMutation.mutate();
}}
style={{
background: createSubmitDisabled
? 'var(--color-border, #e2e4e9)'
: 'var(--color-member-0, #4a90d9)',
color: '#ffffff',
border: 'none',
cursor: createSubmitDisabled ? 'default' : 'pointer',
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 600,
minHeight: '44px',
minWidth: '44px',
padding: '0 var(--space-6, 24px)',
borderRadius: 'var(--space-1, 4px)',
fontFamily: 'var(--font-family-base)',
transition: 'background 0.15s ease',
display: 'flex',
alignItems: 'center',
gap: 'var(--space-2, 8px)',
}}
>
{createMemberMutation.isPending && (
<Loader2
size={14}
aria-hidden="true"
style={{ animation: 'spin 1s linear infinite', flexShrink: 0 }}
/>
)}
Add member
</button>
</div>
</div>
</section>
</div>{/* end admin-panel-members */}
{/* ── Tab panel: Settings ───────────────────────────────────────────── */}
<div
role="tabpanel"
id="admin-panel-settings"
aria-labelledby="admin-tab-settings"
tabIndex={0}
hidden={activeTab !== 'settings'}
>
{/* ── SHARED CALENDAR section ──────────────────────────────────────── */} {/* ── SHARED CALENDAR section ──────────────────────────────────────── */}
<section aria-label="Shared Calendar" style={{ marginBottom: 'var(--space-8, 32px)' }}> <section aria-label="Shared Calendar" style={{ marginBottom: 'var(--space-8, 32px)' }}>
<div style={sectionLabelStyle}>Shared Calendar</div> <div style={sectionLabelStyle}>Shared Calendar</div>
@@ -605,7 +934,9 @@ export function AdminPage() {
color: 'var(--color-text-primary)', color: 'var(--color-text-primary)',
borderRadius: 'var(--space-1, 4px)', borderRadius: 'var(--space-1, 4px)',
cursor: 'pointer', cursor: 'pointer',
background: active ? 'var(--color-member-0, #4A90D9)' : 'transparent', background: active
? 'var(--color-member-0, #4A90D9)'
: 'transparent',
...(active ? { color: '#ffffff' } : null), ...(active ? { color: '#ffffff' } : null),
minHeight: '44px', minHeight: '44px',
display: 'flex', display: 'flex',
@@ -688,231 +1019,50 @@ export function AdminPage() {
</> </>
)} )}
</section> </section>
{/* ── LOCAL ACCOUNTS section ──────────────────────────────────────── */}
<section aria-label="Local Accounts" style={{ marginBottom: 'var(--space-8, 32px)' }}>
<div style={sectionLabelStyle}>Local Accounts</div>
{/* Surface 11A — Add member inline form */} </div>{/* end admin-panel-settings */}
</div>{/* end centered content column */}
{/* ── Success toast (D-08) ──────────────────────────────────────────────── */}
{toast && (
<div <div
role="status"
aria-live="polite"
aria-atomic="true"
style={{ style={{
border: '1px solid var(--color-border-subtle, var(--color-border))', position: 'fixed',
borderRadius: '8px', bottom: phone
padding: 'var(--space-4, 16px)', ? 'calc(var(--bottom-chrome-h) + var(--space-4, 16px))'
marginBottom: 'var(--space-6, 24px)', : 'var(--space-6, 24px)',
}} left: '50%',
> transform: 'translateX(-50%)',
<div zIndex: 300,
style={{ background: 'var(--color-surface-raised, #ffffff)',
fontSize: 'var(--text-body-size, 15px)',
fontWeight: 600,
color: 'var(--color-text-primary)',
marginBottom: 'var(--space-4, 16px)',
}}
>
Add member
</div>
{/* Display name */}
<div style={{ marginBottom: 'var(--space-3, 12px)' }}>
<label
htmlFor="admin-create-display-name"
style={{
display: 'block',
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 600,
color: 'var(--color-text-primary)',
marginBottom: 'var(--space-1, 4px)',
}}
>
Display name
</label>
<input
id="admin-create-display-name"
type="text"
value={createDisplayName}
onChange={(e) => setCreateDisplayName(e.target.value)}
style={{
width: '100%',
boxSizing: 'border-box',
padding: 'var(--space-3, 12px) var(--space-4, 16px)',
border: '1px solid var(--color-border)', border: '1px solid var(--color-border)',
borderRadius: 'var(--space-1, 4px)', borderRadius: 'var(--space-2, 8px)',
fontSize: 'var(--text-body-size, 15px)', boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
color: 'var(--color-text-primary)',
background: 'var(--color-surface)',
fontFamily: 'var(--font-family-base)',
outline: 'none',
minHeight: '44px',
}}
/>
</div>
{/* Username */}
<div style={{ marginBottom: 'var(--space-3, 12px)' }}>
<label
htmlFor="admin-create-username"
style={{
display: 'block',
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 600,
color: 'var(--color-text-primary)',
marginBottom: 'var(--space-1, 4px)',
}}
>
Username
</label>
<input
id="admin-create-username"
type="text"
autoComplete="off"
spellCheck={false}
autoCapitalize="none"
value={createUsername}
onChange={(e) => setCreateUsername(e.target.value)}
style={{
width: '100%',
boxSizing: 'border-box',
padding: 'var(--space-3, 12px) var(--space-4, 16px)', padding: 'var(--space-3, 12px) var(--space-4, 16px)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--space-1, 4px)',
fontSize: 'var(--text-body-size, 15px)',
color: 'var(--color-text-primary)',
background: 'var(--color-surface)',
fontFamily: 'var(--font-family-base)',
outline: 'none',
minHeight: '44px',
}}
/>
</div>
{/* Initial password */}
<div style={{ marginBottom: 'var(--space-3, 12px)' }}>
<label
htmlFor="admin-create-password"
style={{
display: 'block',
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 600,
color: 'var(--color-text-primary)',
marginBottom: 'var(--space-1, 4px)',
}}
>
Initial password
</label>
<input
id="admin-create-password"
type="password"
autoComplete="new-password"
value={createPassword}
onChange={(e) => setCreatePassword(e.target.value)}
style={{
width: '100%',
boxSizing: 'border-box',
padding: 'var(--space-3, 12px) var(--space-4, 16px)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--space-1, 4px)',
fontSize: 'var(--text-body-size, 15px)',
color: 'var(--color-text-primary)',
background: 'var(--color-surface)',
fontFamily: 'var(--font-family-base)',
outline: 'none',
minHeight: '44px',
}}
/>
</div>
{/* Confirm password */}
<div style={{ marginBottom: 'var(--space-4, 16px)' }}>
<label
htmlFor="admin-create-confirm-password"
style={{
display: 'block',
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 600,
color: 'var(--color-text-primary)',
marginBottom: 'var(--space-1, 4px)',
}}
>
Confirm password
</label>
<input
id="admin-create-confirm-password"
type="password"
autoComplete="new-password"
value={createConfirmPassword}
onChange={(e) => setCreateConfirmPassword(e.target.value)}
style={{
width: '100%',
boxSizing: 'border-box',
padding: 'var(--space-3, 12px) var(--space-4, 16px)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--space-1, 4px)',
fontSize: 'var(--text-body-size, 15px)',
color: 'var(--color-text-primary)',
background: 'var(--color-surface)',
fontFamily: 'var(--font-family-base)',
outline: 'none',
minHeight: '44px',
}}
/>
</div>
{/* Inline error */}
{createError && (
<div
style={{
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 400,
color: 'var(--color-destructive)',
marginBottom: 'var(--space-3, 12px)',
}}
>
{createError}
</div>
)}
{/* Action row */}
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<button
type="button"
disabled={createSubmitDisabled}
onClick={() => {
setCreateError(null);
createMemberMutation.mutate();
}}
style={{
background: createSubmitDisabled
? 'var(--color-border, #e2e4e9)'
: 'var(--color-member-0, #4a90d9)',
color: '#ffffff',
border: 'none',
cursor: createSubmitDisabled ? 'default' : 'pointer',
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 600,
minHeight: '44px',
minWidth: '44px',
padding: '0 var(--space-6, 24px)',
borderRadius: 'var(--space-1, 4px)',
fontFamily: 'var(--font-family-base)',
transition: 'background 0.15s ease',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 'var(--space-2, 8px)', gap: 'var(--space-2, 8px)',
fontSize: 'var(--text-label-size, 13px)',
fontWeight: 'var(--text-label-weight, 400)' as React.CSSProperties['fontWeight'],
lineHeight: 'var(--text-label-line-height, 1.4)',
fontFamily: 'var(--font-family-base)',
color: 'var(--color-text-primary)',
whiteSpace: 'nowrap' as React.CSSProperties['whiteSpace'],
maxWidth: '90vw',
}} }}
> >
{createMemberMutation.isPending && ( <CheckCircle
<Loader2 size={16}
size={14}
aria-hidden="true" aria-hidden="true"
style={{ animation: 'spin 1s linear infinite', flexShrink: 0 }} style={{ color: 'var(--color-member-0)', flexShrink: 0 }}
/> />
<span>{toast}</span>
</div>
)} )}
Add member
</button>
</div>
</div>
</section>
</div>
{/* Credential sheet — admin-rotate or admin-add */} {/* Credential sheet — admin-rotate or admin-add */}
{sheetMember && ( {sheetMember && (
@@ -937,6 +1087,7 @@ export function AdminPage() {
resetTriggerRef.current.focus(); resetTriggerRef.current.focus();
} }
}} }}
onSuccess={() => setToast('Password reset.')}
member={resetTargetMember} member={resetTargetMember}
/> />
)} )}
@@ -1189,10 +1340,11 @@ function CalendarRadioRow({ calendar, isSelected, onSelect }: CalendarRadioRowPr
interface ResetPasswordSheetProps { interface ResetPasswordSheetProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
onSuccess?: () => void;
member: AdminMember; member: AdminMember;
} }
function ResetPasswordSheet({ isOpen, onClose, member }: ResetPasswordSheetProps) { function ResetPasswordSheet({ isOpen, onClose, onSuccess, member }: ResetPasswordSheetProps) {
const [newPassword, setNewPassword] = useState(''); const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -1222,6 +1374,10 @@ function ResetPasswordSheet({ isOpen, onClose, member }: ResetPasswordSheetProps
onClose(); onClose();
} }
// Phone detection for desktop centering
const sheetPhone =
typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches;
const resetMutation = useMutation({ const resetMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
if (newPassword !== confirmPassword) throw new Error('mismatch'); if (newPassword !== confirmPassword) throw new Error('mismatch');
@@ -1229,6 +1385,7 @@ function ResetPasswordSheet({ isOpen, onClose, member }: ResetPasswordSheetProps
}, },
onSuccess: () => { onSuccess: () => {
handleClose(); handleClose();
onSuccess?.();
}, },
onError: (err) => { onError: (err) => {
const msg = err instanceof Error ? err.message : 'server'; const msg = err instanceof Error ? err.message : 'server';
@@ -1259,12 +1416,14 @@ function ResetPasswordSheet({ isOpen, onClose, member }: ResetPasswordSheetProps
}} }}
/> />
{/* Sheet */} {/* Sheet — phone: bottom-sheet / desktop: centered modal (D-09) */}
<div <div
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="Reset password" aria-label="Reset password"
style={{ style={
sheetPhone
? {
position: 'fixed', position: 'fixed',
bottom: 0, bottom: 0,
left: 0, left: 0,
@@ -1275,9 +1434,24 @@ function ResetPasswordSheet({ isOpen, onClose, member }: ResetPasswordSheetProps
padding: 'var(--space-6, 24px)', padding: 'var(--space-6, 24px)',
zIndex: 301, zIndex: 301,
fontFamily: 'var(--font-family-base)', fontFamily: 'var(--font-family-base)',
}
: {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
maxWidth: '480px', maxWidth: '480px',
margin: '0 auto', width: 'calc(100% - var(--space-8, 32px))',
}} maxHeight: 'calc(100dvh - var(--space-8, 32px))',
overflowY: 'auto',
background: 'var(--color-surface, #ffffff)',
borderRadius: '12px',
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
padding: 'var(--space-6, 24px)',
zIndex: 301,
fontFamily: 'var(--font-family-base)',
}
}
> >
<h2 <h2
ref={headingRef} ref={headingRef}