feat: floating glass navigation and action bars in modals

Replace inline Save/Discard/Reset buttons with sticky glass-bubble pills
that float at the bottom of scrollable modal content. Make SpaceSettings
tab sidebar sticky with glass material. Convert RoleEditView "Back to
roles" into a sticky glass pill at the top. UserSettings Log Out + Save
always visible in a glass pill with separator. Also includes floating
position hook and popover/tooltip improvements from prior work.
This commit is contained in:
Jannis Braun
2026-03-09 01:31:35 +01:00
parent 31db1a7bd3
commit 37f199e544
15 changed files with 389 additions and 145 deletions
@@ -196,20 +196,24 @@ function StreamingLimitsPanel() {
<div className="p-2 bg-status-online/10 border border-status-online/30 rounded text-status-online text-sm">Settings saved</div>
)}
{hasChanges && (
<div className="flex items-center gap-2">
<button
onClick={handleSave}
disabled={saving}
className="px-4 py-1.5 bg-accent-primary hover:bg-accent-primary/80 text-white text-sm font-medium rounded transition-colors disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save'}
</button>
<button
onClick={handleReset}
className="px-4 py-1.5 text-sm text-txt-tertiary hover:text-txt-secondary transition-colors"
>
Reset
</button>
<div className="sticky bottom-0 z-10 pointer-events-none">
<div className="flex justify-center pt-3 pb-1">
<div className="glass-bubble rounded-full px-4 py-2 flex items-center gap-2 animate-slide-up pointer-events-auto">
<button
onClick={handleReset}
className="px-3 py-1 text-sm text-txt-tertiary hover:text-txt-secondary transition-colors"
>
Reset
</button>
<button
onClick={handleSave}
disabled={saving}
className="px-3 py-1.5 bg-accent-primary hover:bg-accent-primary/80 text-white text-sm font-medium rounded-full transition-colors disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save'}
</button>
</div>
</div>
</div>
)}
</div>
@@ -331,20 +335,24 @@ function DiscoveryPanel({ spaceId }: { spaceId: string }) {
<div className="p-2 bg-status-online/10 border border-status-online/30 rounded text-status-online text-sm">Settings saved</div>
)}
{hasChanges && (
<div className="flex items-center gap-2">
<button
onClick={handleSave}
disabled={saving}
className="px-4 py-1.5 bg-accent-primary hover:bg-accent-primary/80 text-white text-sm font-medium rounded transition-colors disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save'}
</button>
<button
onClick={handleReset}
className="px-4 py-1.5 text-sm text-txt-tertiary hover:text-txt-secondary transition-colors"
>
Reset
</button>
<div className="sticky bottom-0 z-10 pointer-events-none">
<div className="flex justify-center pt-3 pb-1">
<div className="glass-bubble rounded-full px-4 py-2 flex items-center gap-2 animate-slide-up pointer-events-auto">
<button
onClick={handleReset}
className="px-3 py-1 text-sm text-txt-tertiary hover:text-txt-secondary transition-colors"
>
Reset
</button>
<button
onClick={handleSave}
disabled={saving}
className="px-3 py-1.5 bg-accent-primary hover:bg-accent-primary/80 text-white text-sm font-medium rounded-full transition-colors disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save'}
</button>
</div>
</div>
</div>
)}
@@ -479,7 +487,7 @@ export function SpaceSettingsModal() {
if (!space || !currentSpaceId) return null;
const tabClass = (t: typeof tab) =>
`w-full text-left px-3 py-1.5 rounded text-sm transition-colors ${
`w-full text-left px-2.5 py-1.5 rounded text-sm transition-colors ${
tab === t ? 'bg-interactive-selected text-txt-primary' : 'text-txt-tertiary hover:text-txt-secondary hover:bg-interactive-hover'
}`;
@@ -487,28 +495,30 @@ export function SpaceSettingsModal() {
<Modal isOpen={isOpen} onClose={closeModal} title="Space Settings" maxWidth="max-w-xl">
<div className="flex gap-4">
{/* Tabs */}
<div className="w-32 flex-shrink-0 space-y-1">
<button onClick={() => setTab('overview')} className={tabClass('overview')}>
Overview
</button>
{canManageSpace && (
<button onClick={() => setTab('discovery')} className={tabClass('discovery')}>
Discovery
<div className="w-32 flex-shrink-0 sticky top-0 self-start z-10">
<div className="glass-bubble rounded-lg p-1.5 space-y-0.5">
<button onClick={() => setTab('overview')} className={tabClass('overview')}>
Overview
</button>
)}
<button onClick={() => setTab('members')} className={tabClass('members')}>
Members
</button>
{canManageRoles && (
<button onClick={() => setTab('roles')} className={tabClass('roles')}>
Roles
{canManageSpace && (
<button onClick={() => setTab('discovery')} className={tabClass('discovery')}>
Discovery
</button>
)}
<button onClick={() => setTab('members')} className={tabClass('members')}>
Members
</button>
)}
{isAdmin && (
<button onClick={() => setTab('streaming')} className={tabClass('streaming')}>
Streaming
</button>
)}
{canManageRoles && (
<button onClick={() => setTab('roles')} className={tabClass('roles')}>
Roles
</button>
)}
{isAdmin && (
<button onClick={() => setTab('streaming')} className={tabClass('streaming')}>
Streaming
</button>
)}
</div>
</div>
{/* Content */}
@@ -172,20 +172,25 @@ export function UserSettingsModal() {
{/* Connected Instances */}
<ConnectedInstances />
<div className="flex items-center justify-between pt-2">
<button
onClick={handleLogout}
className="px-4 py-2 text-sm text-txt-danger hover:bg-accent-rose/10 rounded transition-colors"
>
Log Out
</button>
<button
onClick={handleSave}
disabled={isLoading}
className="px-4 py-2 bg-accent-primary hover:bg-accent-primary/80 text-white text-sm font-medium rounded transition-colors disabled:opacity-50"
>
{isLoading ? 'Saving...' : 'Save Changes'}
</button>
<div className="sticky bottom-0 z-10 pointer-events-none">
<div className="flex justify-center pt-3 pb-1">
<div className="glass-bubble rounded-full px-3 py-2 flex items-center gap-3 pointer-events-auto">
<button
onClick={handleLogout}
className="px-3 py-1 text-sm text-txt-danger hover:bg-accent-rose/10 rounded-full transition-colors"
>
Log Out
</button>
<div className="w-px h-5 bg-white/10" />
<button
onClick={handleSave}
disabled={isLoading}
className="px-3 py-1.5 bg-accent-primary hover:bg-accent-primary/80 text-white text-sm font-medium rounded-full transition-colors disabled:opacity-50"
>
{isLoading ? 'Saving...' : 'Save Changes'}
</button>
</div>
</div>
</div>
</div>
</Modal>
@@ -229,20 +229,24 @@ export function OverviewPanel({ spaceId }: OverviewPanelProps) {
<div className="p-2 bg-status-online/10 border border-status-online/30 rounded text-status-online text-sm">Settings saved</div>
)}
{canManageSpace && hasChanges && (
<div className="flex items-center gap-2">
<button
onClick={handleSave}
disabled={saving || uploadingIcon || !spaceName.trim()}
className="px-4 py-1.5 bg-accent-primary hover:bg-accent-primary/80 text-white text-sm font-medium rounded transition-colors disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save'}
</button>
<button
onClick={handleDiscard}
className="px-4 py-1.5 text-sm text-txt-tertiary hover:text-txt-secondary transition-colors"
>
Discard
</button>
<div className="sticky bottom-0 z-10 pointer-events-none">
<div className="flex justify-center pt-3 pb-1">
<div className="glass-bubble rounded-full px-4 py-2 flex items-center gap-2 animate-slide-up pointer-events-auto">
<button
onClick={handleDiscard}
className="px-3 py-1 text-sm text-txt-tertiary hover:text-txt-secondary transition-colors"
>
Discard
</button>
<button
onClick={handleSave}
disabled={saving || uploadingIcon || !spaceName.trim()}
className="px-3 py-1.5 bg-accent-primary hover:bg-accent-primary/80 text-white text-sm font-medium rounded-full transition-colors disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save'}
</button>
</div>
</div>
</div>
)}
@@ -242,15 +242,17 @@ function RoleEditView({ role, spaceId, onBack, onDeleted }: RoleEditViewProps) {
return (
<div className="space-y-4">
{/* Back button */}
<button
onClick={onBack}
className="flex items-center gap-1 text-sm text-txt-tertiary hover:text-txt-secondary transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
Back to roles
</button>
<div className="sticky top-0 z-10 pointer-events-none pb-3">
<button
onClick={onBack}
className="glass-bubble rounded-full px-3 py-1.5 flex items-center gap-1 text-sm text-txt-tertiary hover:text-txt-secondary transition-colors pointer-events-auto"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
Back to roles
</button>
</div>
{/* Role Name (not editable for @everyone) */}
{!isEveryone && (
@@ -362,20 +364,24 @@ function RoleEditView({ role, spaceId, onBack, onDeleted }: RoleEditViewProps) {
<div className="p-2 bg-status-online/10 border border-status-online/30 rounded text-status-online text-sm">Role saved</div>
)}
{hasChanges && (
<div className="flex items-center gap-2">
<button
onClick={handleSave}
disabled={saving || (!isEveryone && !draftName.trim())}
className="px-4 py-1.5 bg-accent-primary hover:bg-accent-primary/80 text-white text-sm font-medium rounded transition-colors disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save'}
</button>
<button
onClick={handleDiscard}
className="px-4 py-1.5 text-sm text-txt-tertiary hover:text-txt-secondary transition-colors"
>
Discard
</button>
<div className="sticky bottom-0 z-10 pointer-events-none">
<div className="flex justify-center pt-3 pb-1">
<div className="glass-bubble rounded-full px-4 py-2 flex items-center gap-2 animate-slide-up pointer-events-auto">
<button
onClick={handleDiscard}
className="px-3 py-1 text-sm text-txt-tertiary hover:text-txt-secondary transition-colors"
>
Discard
</button>
<button
onClick={handleSave}
disabled={saving || (!isEveryone && !draftName.trim())}
className="px-3 py-1.5 bg-accent-primary hover:bg-accent-primary/80 text-white text-sm font-medium rounded-full transition-colors disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save'}
</button>
</div>
</div>
</div>
)}