feat: dynamic PiP collision avoidance, reaction pill redesign, channel dedup fix

PiP positioning:
- Replace hardcoded layout constants with dynamic DOM measurement system
- Obstacle elements declare themselves via data-pip-obstacle="left|bottom"
- getPipBounds() queries actual element rects at runtime via getBoundingClientRect
- MutationObserver + ResizeObserver re-clamp PiP when obstacles appear/resize
- PiP reappears after close when navigating away from voice channel

Reactions:
- Align reaction pills with Aether Drift prototype
- Own-reaction accent: purple → mint (bg, border, count color)
- Default pills: subtle white overlay bg + border-soft border
- Count text: 12px/semibold/txt-secondary per prototype spec

Bug fix:
- Deduplicate channel insertion in createChannel store action
- Prevents double-add race between REST response and WS broadcast
This commit is contained in:
Jannis Braun
2026-03-02 02:09:52 +01:00
parent f79575ffa7
commit ddeb72101e
6 changed files with 124 additions and 29 deletions
+4 -3
View File
@@ -172,9 +172,10 @@ export const useServerStore = create<ServerState>((set, get) => ({
createChannel: async (serverId: string, name: string, type: 'text' | 'voice' | 'video', topic?: string) => {
const channel = await api.channels.create(serverId, { name, type, topic });
set((state) => ({
channels: [...state.channels, channel].sort((a, b) => a.position - b.position),
}));
set((state) => {
if (state.channels.some(c => c.id === channel.id)) return state;
return { channels: [...state.channels, channel].sort((a, b) => a.position - b.position) };
});
return channel;
},