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:
1 parent
2b7356d043
commit
ae1f799d9c
4 files changed
+82
-46
No files matched your search
@@ -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"> (Long-Polling)</span>}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in new issue
Block a user