feat: standardize input styling with tier system, add depth and admin features

- Define 4 input tier CSS classes (input-standard, input-search, input-embedded, input-danger)
  in globals.css, migrating ~50 inputs across ~28 component files to use them
- Add subtle border and inset shadow to solid input tiers for resting-state visibility
- Fix focus ring clipping in settings panel scroll container
- Fix phantom Tailwind tokens (border-border-primary, placeholder-txt-muted)
- Add admin user management panel, storage management, and account deletion utilities
This commit is contained in:
Jannis Braun
2026-03-14 13:49:32 +01:00
parent afbc4b5e31
commit 836f0acef6
45 changed files with 1478 additions and 235 deletions
@@ -83,7 +83,7 @@ export function ExplorePage() {
placeholder="Search spaces..."
value={searchQuery}
onChange={(e) => handleSearchChange(e.target.value)}
className="w-full bg-surface-base text-txt-primary text-sm px-3 py-1.5 rounded-[4px] outline-none placeholder:text-txt-tertiary/50 focus:ring-1 focus:ring-accent-primary transition-all"
className="input-search w-full"
/>
{searchQuery && (
<button
@@ -230,10 +230,10 @@ function SpaceCard({
: null;
const iconUrl = space.icon
? (space.icon.startsWith('http') ? space.icon : `/api/uploads/${space.icon}`)
? (space.icon.startsWith('http') || space.icon.startsWith('/') ? space.icon : `/api/uploads/${space.icon}`)
: null;
const bannerUrl = space.banner
? (space.banner.startsWith('http') ? space.banner : `/api/uploads/${space.banner}`)
? (space.banner.startsWith('http') || space.banner.startsWith('/') ? space.banner : `/api/uploads/${space.banner}`)
: null;
// Extract dominant colors from icon when no banner is set
@@ -412,7 +412,7 @@ function SpaceCard({
onChange={(e) => setRequestMessage(e.target.value.slice(0, 200))}
placeholder="Why do you want to join? (optional)"
rows={2}
className="w-full px-3 py-2 bg-surface-input rounded text-sm text-txt-primary outline-none focus:ring-1 focus:ring-accent-primary resize-none placeholder:text-txt-tertiary"
className="input-standard w-full resize-none"
/>
<div className="flex gap-2">
<button
@@ -268,7 +268,7 @@ function AddFriendTab({
placeholder="You can add a friend with their username"
value={addUsername}
onChange={(e) => setAddUsername(e.target.value)}
className="w-full bg-surface-base text-txt-primary px-4 py-3 rounded-lg border border-transparent focus:border-txt-link outline-none transition-all placeholder:text-txt-tertiary/50"
className="input-search w-full px-4 py-3 rounded-lg"
/>
<button
type="submit"
@@ -301,7 +301,7 @@ function AddFriendTab({
placeholder="Search people..."
value={discoverQuery}
onChange={(e) => handleDiscoverSearch(e.target.value)}
className="w-full bg-surface-base text-txt-primary text-sm px-3 py-1.5 rounded-[4px] outline-none placeholder:text-txt-tertiary/50 focus:ring-1 focus:ring-accent-primary transition-all"
className="input-search w-full"
/>
{discoverQuery && (
<button
@@ -371,10 +371,10 @@ function UserDiscoverCard({
: null;
const avatarUrl = user.avatar
? (user.avatar.startsWith('http') ? user.avatar : `/api/uploads/${user.avatar}`)
? (user.avatar.startsWith('http') || user.avatar.startsWith('/') ? user.avatar : `/api/uploads/${user.avatar}`)
: null;
const bannerUrl = user.banner
? (user.banner.startsWith('http') ? user.banner : `/api/uploads/${user.banner}`)
? (user.banner.startsWith('http') || user.banner.startsWith('/') ? user.banner : `/api/uploads/${user.banner}`)
: null;
const handleSendRequest = async () => {
+3 -3
View File
@@ -229,7 +229,7 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) {
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
onKeyDown={handleEditSubmit}
className="w-full p-3 bg-surface-input rounded-lg text-txt-primary outline-none resize-none text-[15px] leading-[1.5] shadow-inner"
className="input-standard w-full p-3 rounded-lg resize-none text-[15px] leading-[1.5] shadow-inner"
rows={2}
autoFocus
/>
@@ -262,9 +262,9 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) {
<div className="mt-1 grid gap-2">
{message.attachments.map((att) => {
const isImage = att.mimetype.startsWith('image/');
const attUrl = att.filename.startsWith('http') ? att.filename : `/api/uploads/${att.filename}`;
const attUrl = att.filename.startsWith('http') || att.filename.startsWith('/') ? att.filename : `/api/uploads/${att.filename}`;
const thumbUrl = att.thumbnailFilename
? (att.thumbnailFilename.startsWith('http') ? att.thumbnailFilename : `/api/uploads/${att.thumbnailFilename}`)
? (att.thumbnailFilename.startsWith('http') || att.thumbnailFilename.startsWith('/') ? att.thumbnailFilename : `/api/uploads/${att.thumbnailFilename}`)
: null;
if (isImage) {
return (
@@ -349,7 +349,7 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
onKeyDown={handleKeyDown}
onPaste={canAttachFiles ? handlePaste : undefined}
placeholder={`Message ${channelName.startsWith('@') ? channelName : `#${channelName}`}`}
className="flex-1 py-[10px] px-1 bg-transparent text-txt-primary placeholder-txt-tertiary/60 outline-none resize-none text-[15px] leading-[1.375rem] max-h-[50vh] scrollbar-thin"
className="input-embedded flex-1 py-[10px] px-1 resize-none text-[15px] leading-[1.375rem] max-h-[50vh] scrollbar-thin"
rows={1}
/>
@@ -172,7 +172,7 @@ export function SearchPopover({ open, onClose, anchorRef, channelId, isDm, onJum
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search messages..."
className="flex-1 bg-transparent text-txt-primary text-[14px] placeholder-txt-tertiary outline-none"
className="input-embedded flex-1 text-[14px]"
/>
{query && (
<button
@@ -210,7 +210,7 @@ export function SearchPopover({ open, onClose, anchorRef, channelId, isDm, onJum
value={fromFilter}
onChange={(e) => setFromFilter(e.target.value)}
placeholder="username"
className="w-full bg-surface-input rounded px-2 py-1 text-[13px] text-txt-primary placeholder-txt-tertiary outline-none"
className="input-search w-full px-2 py-1 text-[13px]"
/>
</div>
<div>
@@ -218,7 +218,7 @@ export function SearchPopover({ open, onClose, anchorRef, channelId, isDm, onJum
<select
value={hasFilter}
onChange={(e) => setHasFilter(e.target.value)}
className="w-full bg-surface-input rounded px-2 py-1 text-[13px] text-txt-primary outline-none appearance-none cursor-pointer"
className="input-search w-full px-2 py-1 text-[13px] appearance-none cursor-pointer"
>
<option value="">Any</option>
<option value="file">File</option>
@@ -232,7 +232,7 @@ export function SearchPopover({ open, onClose, anchorRef, channelId, isDm, onJum
type="date"
value={beforeFilter}
onChange={(e) => setBeforeFilter(e.target.value)}
className="w-full bg-surface-input rounded px-2 py-1 text-[13px] text-txt-primary outline-none"
className="input-search w-full px-2 py-1 text-[13px]"
/>
</div>
<div>
@@ -241,7 +241,7 @@ export function SearchPopover({ open, onClose, anchorRef, channelId, isDm, onJum
type="date"
value={afterFilter}
onChange={(e) => setAfterFilter(e.target.value)}
className="w-full bg-surface-input rounded px-2 py-1 text-[13px] text-txt-primary outline-none"
className="input-search w-full px-2 py-1 text-[13px]"
/>
</div>
</div>