feat: Phase 1 Aether Drift — mobile-first grid layout and token migration

Replace Discord flex layout with CSS Grid on desktop (312px sidebar +
1fr main) and overlay drawer on mobile. Migrate all discord-* class
references to Aether Drift design tokens across 15 files (~200 refs).

Layout: ServerSidebar becomes a fixed glass strip (md:glass-strip),
ChannelSidebar gets pl-[72px] offset, bottom bar is a glass-bubble
(z-105), MessageInput floats as absolute glass pill on desktop (z-110).
MemberSidebar/ActivityPanel hidden on mobile via hidden md:block.

Z-index stack: MobileNav backdrop z-35, sidebar z-40, glass strip z-100,
bottom bar z-105, input bubble z-110, hamburger z-120, profile popout
z-145, device panels z-150, modals z-200.
This commit is contained in:
Jannis Braun
2026-03-01 22:46:27 +01:00
parent e219229b63
commit 79252fe54a
15 changed files with 412 additions and 230 deletions
@@ -216,16 +216,16 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
};
return (
<div className="px-4 pb-6 flex-shrink-0">
<div className="px-3 pb-3 flex-shrink-0 md:absolute md:bottom-3 md:left-3 md:right-3 md:z-[110] md:px-0 md:pb-0 md:glass-bubble md:rounded-[14px]">
{replyTo && (
<div className="bg-discord-bg-hover rounded-t-lg px-4 py-2 flex items-center justify-between border-b border-discord-bg-tertiary/50">
<div className="flex items-center gap-1 text-[14px] text-discord-text-normal truncate">
<div className="bg-interactive-hover rounded-t-lg px-4 py-2 flex items-center justify-between border-b border-border-hard/50">
<div className="flex items-center gap-1 text-[14px] text-txt-message truncate">
<span className="opacity-60">Replying to</span>
<span className="font-bold">{replyTo.user.displayName ?? replyTo.user.username}</span>
</div>
<button
onClick={() => setReplyTo(null)}
className="text-discord-text-muted hover:text-discord-text-primary transition-colors"
className="text-txt-tertiary hover:text-txt-primary transition-colors"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z" />
@@ -234,7 +234,7 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
</div>
)}
<div
className={`relative bg-discord-bg-input ${replyTo ? 'rounded-b-lg' : 'rounded-lg'} overflow-visible`}
className={`relative bg-surface-input md:bg-transparent ${replyTo ? 'rounded-b-lg' : 'rounded-lg md:rounded-none'} overflow-visible`}
onDrop={handleDrop}
onDragOver={handleDragOver}
>
@@ -249,9 +249,9 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
{/* File previews */}
{files.length > 0 && (
<div className="p-4 flex flex-wrap gap-4 bg-discord-bg-secondary/30">
<div className="p-4 flex flex-wrap gap-4 bg-surface-channel/30">
{files.map((file, i) => (
<div key={i} className="relative group bg-discord-bg-secondary rounded-lg p-2 max-w-[200px] shadow-elevation-low border border-discord-bg-tertiary">
<div key={i} className="relative group bg-surface-channel rounded-lg p-2 max-w-[200px] shadow-elevation-low border border-border-hard">
{file.type.startsWith('image/') ? (
<img
src={URL.createObjectURL(file)}
@@ -259,7 +259,7 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
className="max-h-[150px] rounded object-cover"
/>
) : (
<div className="flex items-center gap-2 text-sm text-discord-text-secondary py-4 px-2">
<div className="flex items-center gap-2 text-sm text-txt-secondary py-4 px-2">
<svg className="w-8 h-8 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
@@ -268,7 +268,7 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
)}
<button
onClick={() => removeFile(i)}
className="absolute -top-2 -right-2 w-7 h-7 bg-discord-red hover:bg-discord-red-hover shadow-elevation-high rounded-lg flex items-center justify-center text-white transition-colors z-10"
className="absolute -top-2 -right-2 w-7 h-7 bg-accent-rose hover:bg-accent-rose/80 shadow-elevation-high rounded-lg flex items-center justify-center text-white transition-colors z-10"
>
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
<path d="M5 2a1 1 0 011-1h4a1 1 0 011 1v1h3a1 1 0 110 2h-.08L13 14a2 2 0 01-2 2H5a2 2 0 01-2-2L2.08 5H2a1 1 0 110-2h3V2zm2 0v1h2V2H7z" />
@@ -283,7 +283,7 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
{/* File attach button */}
<button
onClick={() => fileInputRef.current?.click()}
className="p-3 text-discord-text-muted hover:text-discord-text-secondary transition-colors sticky top-0"
className="p-3 text-txt-tertiary hover:text-txt-secondary transition-colors sticky top-0"
title="Attach file"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
@@ -312,14 +312,14 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
onKeyDown={handleKeyDown}
onPaste={handlePaste}
placeholder={`Message ${channelName.startsWith('@') ? channelName : `#${channelName}`}`}
className="flex-1 py-[11px] px-1 bg-transparent text-discord-text-primary placeholder-discord-text-muted/60 outline-none resize-none text-[15px] leading-[1.375rem] max-h-[50vh] scrollbar-thin"
className="flex-1 py-[11px] 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"
rows={1}
disabled={isUploading}
/>
{/* Send indicator */}
{isUploading && (
<div className="p-3 text-discord-text-muted">
<div className="p-3 text-txt-tertiary">
<svg className="w-5 h-5 animate-spin" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
@@ -328,21 +328,21 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
)}
{/* GIF button */}
<button className="p-2 text-discord-text-muted hover:text-discord-text-secondary transition-colors" title="GIF">
<button className="p-2 text-txt-tertiary hover:text-txt-secondary transition-colors" title="GIF">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M2 5.5A2.5 2.5 0 0 1 4.5 3h15A2.5 2.5 0 0 1 22 5.5v13a2.5 2.5 0 0 1-2.5 2.5h-15A2.5 2.5 0 0 1 2 18.5v-13ZM5.1 14V10h3.2v1.2H6.5v.6h1.6v1.1H6.5V14H5.1Zm4.5 0V10h1.4v4H9.6Zm2.5 0V10h3.2v1.2h-1.8v.5h1.6v1h-1.6V14h-1.4Z" />
</svg>
</button>
{/* Sticker button */}
<button className="p-2 text-discord-text-muted hover:text-discord-text-secondary transition-colors" title="Stickers">
<button className="p-2 text-txt-tertiary hover:text-txt-secondary transition-colors" title="Stickers">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12.5 2C6.81 2 2 6.81 2 12.5S6.81 23 12.5 23c1.31 0 2.56-.25 3.73-.7l5.07-5.07c.45-1.17.7-2.42.7-3.73C22 7.81 17.19 2 12.5 2Zm0 19c-4.69 0-8.5-3.81-8.5-8.5S7.81 4 12.5 4 21 7.81 21 12.5c0 .89-.14 1.74-.4 2.54l-3.56 3.56c-.8.26-1.65.4-2.54.4ZM8 11.5c.83 0 1.5-.67 1.5-1.5S8.83 8.5 8 8.5 6.5 9.17 6.5 10s.67 1.5 1.5 1.5Zm6 0c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5-1.5.67-1.5 1.5.67 1.5 1.5 1.5Zm-1 3.5c-2.33 0-4.31-1.46-5.11-3.5h10.22c-.8 2.04-2.78 3.5-5.11 3.5Z" />
</svg>
</button>
{/* Emoji button */}
<button className="p-2 text-discord-text-muted hover:text-discord-text-secondary transition-colors" title="Emoji">
<button className="p-2 text-txt-tertiary hover:text-txt-secondary transition-colors" title="Emoji">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5s.67 1.5 1.5 1.5zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z" />
</svg>