style(lists): live pill left of a divider, actions right, like the notes

Tasks get the live pill too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-02 11:50:27 +01:00
1 parent 2b7356d043
commit ae1f799d9c
4 files changed
+82 -46

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
import React, { useEffect, useState } from 'react';
import { onRealtimeStatus, RealtimeTransport } from '../lib/api';
// Connection pill in the look of the notes' header (Doku/ui-patterns.md): whether changes of others
// arrive live, and how. Phones show the short label only.
export const LivePill: React.FC = () => {
const [status, setStatus] = useState<{ connected: boolean; transport: RealtimeTransport }>({
connected: false,
transport: 'none',
});
useEffect(() => onRealtimeStatus((connected, transport) => setStatus({ connected, transport })), []);
const { connected, transport } = status;
return (
<span
className="inline-flex items-center gap-1 h-7 px-2 rounded-btn text-[11px] bg-panel text-muted flex-shrink-0"
title={
!connected
? 'Keine Live-Verbindung – Änderungen anderer erscheinen nach dem Neuladen'
: transport === 'sse'
? 'Live-Verbindung über Server-Sent Events (WebSocket wird blockiert)'
: transport === 'poll'
? 'Live über Long-Polling (WebSocket und SSE werden blockiert)'
: 'Live-Verbindung über WebSocket'
}
>
<span className={`w-1.5 h-1.5 rounded-full ${connected ? 'bg-status-ok' : 'bg-status-warn'}`} />
{!connected ? (
'Offline'
) : (
<>
Live
{transport === 'poll' && <span className="hidden sm:inline">&nbsp;(Long-Polling)</span>}
</>
)}
</span>
);
};
+17 -35
View File
@@ -10,13 +10,14 @@ import {
CloudOff,
} from 'lucide-react';
import { ListItem, SharedList, Team, User, WSMessage } from '../../lib/types';
import { api, triggerHaptic, createRealtimeConnection, RealtimeTransport } from '../../lib/api';
import { api, triggerHaptic, createRealtimeConnection } from '../../lib/api';
import { readViewSession, writeViewSession } from '../../lib/viewSession';
import { OFFLINE_ITEM_PREFIX, pendingListItems } from '../../lib/offline';
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
import { AvatarStack, AvatarPerson } from '../../components/AvatarStack';
import { CountBadge } from '../../components/CountBadge';
import { ListSwitcher } from '../../components/ListSwitcher';
import { LivePill } from '../../components/LivePill';
import { OriginBadge } from '../../components/OriginBadge';
import { VoiceInputButton } from '../../components/VoiceInputButton';
import { FormSheet } from '../../components/FormSheet';
@@ -110,8 +111,6 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
const [editingQtyItemId, setEditingQtyItemId] = useState<string | null>(null);
const [editingQtyValue, setEditingQtyValue] = useState('');
const [wsConnected, setWsConnected] = useState(false);
const [realtimeTransport, setRealtimeTransport] = useState<RealtimeTransport>('none');
const [showNewListModal, setShowNewListModal] = useState(false);
const [newListTitle, setNewListTitle] = useState('');
const [newListIcon, setNewListIcon] = useState('🛒');
@@ -222,10 +221,6 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
const conn = createRealtimeConnection({
listId: activeList.id,
onStatusChange: (status) => {
setWsConnected(status.connected);
setRealtimeTransport(status.transport);
},
onMessage: (msg: WSMessage) => {
const msgListId = msg.listId || (msg as any).list_id;
if (msg.event === 'ITEM_CREATED' && msgListId) {
@@ -1032,22 +1027,6 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
</>
) : null;
// Connection pill in the same look as the notes' header
const livePill = (
<span
className="inline-flex items-center gap-1 h-7 px-2 rounded-btn text-[11px] bg-panel text-muted"
title={
!wsConnected
? 'Keine Live-Verbindung – Änderungen anderer erscheinen nach dem Neuladen'
: realtimeTransport === 'sse'
? 'Live-Verbindung über Server-Sent Events (WebSocket wird blockiert)'
: 'Live-Verbindung über WebSocket'
}
>
<span className={`w-1.5 h-1.5 rounded-full ${wsConnected ? 'bg-status-ok' : 'bg-status-warn'}`} />
{wsConnected ? 'Live' : 'Offline'}
</span>
);
return (
<div className="w-full space-y-3">
@@ -1117,8 +1096,9 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
</button>
</div>
{/* Right Header Section: Geteilt Badge, Icon-Only Share Button & Live Sync */}
{/* Right Header Section like the notes: status (live, shared) | actions */}
<div className="flex items-center gap-2">
<LivePill />
{activeList && (() => {
const isOwner = activeList.isOwner !== false && (activeList.userId === currentUser?.id || !activeList.userId || currentUser?.role === 'admin');
const sharesCount = activeList.shares?.length || 0;
@@ -1151,6 +1131,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
)
)}
<span className="w-px h-5 bg-border mx-0.5" aria-hidden />
<button
type="button"
onClick={openShareModal}
@@ -1162,8 +1143,6 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
</div>
);
})()}
{livePill}
</div>
</div>
@@ -1193,17 +1172,20 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
)}
</div>
<div className="flex items-center gap-1.5 flex-shrink-0">
<LivePill />
{activeList && (
<button
type="button"
onClick={openShareModal}
className="btn-icon min-h-[36px] min-w-[36px] flex items-center justify-center"
title="Liste teilen"
>
<Users className="w-4 h-4 text-accent" />
</button>
<>
<span className="w-px h-5 bg-border" aria-hidden />
<button
type="button"
onClick={openShareModal}
className="btn-icon min-h-[36px] min-w-[36px] flex items-center justify-center"
title="Liste teilen"
>
<Users className="w-4 h-4 text-accent" />
</button>
</>
)}
{livePill}
</div>
</div>
+21 -11
View File
@@ -41,6 +41,7 @@ import { ManageColumnsModal, DEFAULT_KANBAN_COLUMNS, columnDotClass } from './Ma
import { AvatarStack, AvatarPerson } from '../../components/AvatarStack';
import { CountBadge } from '../../components/CountBadge';
import { ListSwitcher } from '../../components/ListSwitcher';
import { LivePill } from '../../components/LivePill';
import { FormSheet } from '../../components/FormSheet';
import { EmojiIconButton } from '../../components/EmojiIconButton';
import { useMobileActions } from '../../context/MobileActionContext';
@@ -1872,8 +1873,9 @@ export const TasksView: React.FC<TasksViewProps> = ({
</button>
</div>
{/* Right Header Section: Sharing Badge, Icon-Only Button & View Switcher */}
{/* Right Header Section like the notes: status (live, shared) | actions (share, view switcher) */}
<div className="flex items-center gap-2">
<LivePill />
{currentSelectedList && (() => {
const listOwnerId = currentSelectedList.ownerId || currentSelectedList.userId;
const isOwner = currentSelectedList.isOwner !== false && (!listOwnerId || listOwnerId === currentUser?.id);
@@ -1910,6 +1912,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
)
)}
<span className="w-px h-5 bg-border mx-0.5" aria-hidden />
<button
type="button"
onClick={() => handleOpenShareModal(currentSelectedList)}
@@ -1921,6 +1924,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div>
);
})()}
{!currentSelectedList && <span className="w-px h-5 bg-border mx-0.5" aria-hidden />}
{/* View Switcher: List vs Kanban */}
<div className="flex border border-border rounded-btn bg-canvas overflow-hidden">
@@ -1973,16 +1977,22 @@ export const TasksView: React.FC<TasksViewProps> = ({
: `Papierkorb (${taskTrash.length})`}
</span>
</div>
{currentSelectedList && (
<button
type="button"
onClick={() => handleOpenShareModal(currentSelectedList)}
className="btn-icon min-h-[36px] min-w-[36px] flex items-center justify-center flex-shrink-0"
title="Aufgabenliste teilen"
>
<Users className="w-4 h-4 text-accent" />
</button>
)}
<div className="flex items-center gap-1.5 flex-shrink-0">
<LivePill />
{currentSelectedList && (
<>
<span className="w-px h-5 bg-border" aria-hidden />
<button
type="button"
onClick={() => handleOpenShareModal(currentSelectedList)}
className="btn-icon min-h-[36px] min-w-[36px] flex items-center justify-center"
title="Aufgabenliste teilen"
>
<Users className="w-4 h-4 text-accent" />
</button>
</>
)}
</div>
</div>
{isSelectedListReadOnly && (