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
+65 -29

No files matched your search

+6
View File
@@ -86,6 +86,12 @@ Komponente: [ListSwitcher.tsx](../frontend/src/components/ListSwitcher.tsx)
- In der Kopfleiste am Handy (Aufgaben, Listen) ist der Titel der offenen Liste ein Knopf mit Pfeil. Ein Tipp klappt alle Listen auf, mit Icon, Zähler (`CountBadge`, `muted`) und Haken an der offenen; ein Tipp auf eine Liste wechselt dorthin. - In der Kopfleiste am Handy (Aufgaben, Listen) ist der Titel der offenen Liste ein Knopf mit Pfeil. Ein Tipp klappt alle Listen auf, mit Icon, Zähler (`CountBadge`, `muted`) und Haken an der offenen; ein Tipp auf eine Liste wechselt dorthin.
- Schließt bei Tipp daneben und mit Escape. Am Desktop bleiben die Reiter über der Liste. - Schließt bei Tipp daneben und mit Escape. Am Desktop bleiben die Reiter über der Liste.
## Kopfzeile: Status | Aktionen
Komponente für den Verbindungsstatus: [LivePill.tsx](../frontend/src/components/LivePill.tsx) (Notizen zeigen ihren eigenen Sync-Weg im selben Aussehen).
- Rechts im Kopf von Notizen, Aufgaben und Listen gilt dieselbe Reihenfolge: links die Statusanzeigen (Live-Pille, „Geteilt“), dann ein senkrechter Trenner (`w-px h-5 bg-border`), rechts die Knöpfe (Teilen, Ansicht, Menü). Am Handy genauso, dort steht in der Pille nur „Live“.
## Mikrofon in Eingabezeilen ## Mikrofon in Eingabezeilen
Komponente: [VoiceInputButton.tsx](../frontend/src/components/VoiceInputButton.tsx) (nutzt `useVoiceInput` wie der Quick Dump) Komponente: [VoiceInputButton.tsx](../frontend/src/components/VoiceInputButton.tsx) (nutzt `useVoiceInput` wie der Quick Dump)
+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>
);
};
@@ -10,13 +10,14 @@ import {
CloudOff, CloudOff,
} from 'lucide-react'; } from 'lucide-react';
import { ListItem, SharedList, Team, User, WSMessage } from '../../lib/types'; 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 { readViewSession, writeViewSession } from '../../lib/viewSession';
import { OFFLINE_ITEM_PREFIX, pendingListItems } from '../../lib/offline'; import { OFFLINE_ITEM_PREFIX, pendingListItems } from '../../lib/offline';
import { UnifiedShareModal } from '../../components/UnifiedShareModal'; import { UnifiedShareModal } from '../../components/UnifiedShareModal';
import { AvatarStack, AvatarPerson } from '../../components/AvatarStack'; import { AvatarStack, AvatarPerson } from '../../components/AvatarStack';
import { CountBadge } from '../../components/CountBadge'; import { CountBadge } from '../../components/CountBadge';
import { ListSwitcher } from '../../components/ListSwitcher'; import { ListSwitcher } from '../../components/ListSwitcher';
import { LivePill } from '../../components/LivePill';
import { OriginBadge } from '../../components/OriginBadge'; import { OriginBadge } from '../../components/OriginBadge';
import { VoiceInputButton } from '../../components/VoiceInputButton'; import { VoiceInputButton } from '../../components/VoiceInputButton';
import { FormSheet } from '../../components/FormSheet'; import { FormSheet } from '../../components/FormSheet';
@@ -110,8 +111,6 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
const [editingQtyItemId, setEditingQtyItemId] = useState<string | null>(null); const [editingQtyItemId, setEditingQtyItemId] = useState<string | null>(null);
const [editingQtyValue, setEditingQtyValue] = useState(''); const [editingQtyValue, setEditingQtyValue] = useState('');
const [wsConnected, setWsConnected] = useState(false);
const [realtimeTransport, setRealtimeTransport] = useState<RealtimeTransport>('none');
const [showNewListModal, setShowNewListModal] = useState(false); const [showNewListModal, setShowNewListModal] = useState(false);
const [newListTitle, setNewListTitle] = useState(''); const [newListTitle, setNewListTitle] = useState('');
const [newListIcon, setNewListIcon] = useState('🛒'); const [newListIcon, setNewListIcon] = useState('🛒');
@@ -222,10 +221,6 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
const conn = createRealtimeConnection({ const conn = createRealtimeConnection({
listId: activeList.id, listId: activeList.id,
onStatusChange: (status) => {
setWsConnected(status.connected);
setRealtimeTransport(status.transport);
},
onMessage: (msg: WSMessage) => { onMessage: (msg: WSMessage) => {
const msgListId = msg.listId || (msg as any).list_id; const msgListId = msg.listId || (msg as any).list_id;
if (msg.event === 'ITEM_CREATED' && msgListId) { if (msg.event === 'ITEM_CREATED' && msgListId) {
@@ -1032,22 +1027,6 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
</> </>
) : null; ) : 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 ( return (
<div className="w-full space-y-3"> <div className="w-full space-y-3">
@@ -1117,8 +1096,9 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
</button> </button>
</div> </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"> <div className="flex items-center gap-2">
<LivePill />
{activeList && (() => { {activeList && (() => {
const isOwner = activeList.isOwner !== false && (activeList.userId === currentUser?.id || !activeList.userId || currentUser?.role === 'admin'); const isOwner = activeList.isOwner !== false && (activeList.userId === currentUser?.id || !activeList.userId || currentUser?.role === 'admin');
const sharesCount = activeList.shares?.length || 0; 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 <button
type="button" type="button"
onClick={openShareModal} onClick={openShareModal}
@@ -1162,8 +1143,6 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
</div> </div>
); );
})()} })()}
{livePill}
</div> </div>
</div> </div>
@@ -1193,7 +1172,10 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
)} )}
</div> </div>
<div className="flex items-center gap-1.5 flex-shrink-0"> <div className="flex items-center gap-1.5 flex-shrink-0">
<LivePill />
{activeList && ( {activeList && (
<>
<span className="w-px h-5 bg-border" aria-hidden />
<button <button
type="button" type="button"
onClick={openShareModal} onClick={openShareModal}
@@ -1202,8 +1184,8 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
> >
<Users className="w-4 h-4 text-accent" /> <Users className="w-4 h-4 text-accent" />
</button> </button>
</>
)} )}
{livePill}
</div> </div>
</div> </div>
+12 -2
View File
@@ -41,6 +41,7 @@ import { ManageColumnsModal, DEFAULT_KANBAN_COLUMNS, columnDotClass } from './Ma
import { AvatarStack, AvatarPerson } from '../../components/AvatarStack'; import { AvatarStack, AvatarPerson } from '../../components/AvatarStack';
import { CountBadge } from '../../components/CountBadge'; import { CountBadge } from '../../components/CountBadge';
import { ListSwitcher } from '../../components/ListSwitcher'; import { ListSwitcher } from '../../components/ListSwitcher';
import { LivePill } from '../../components/LivePill';
import { FormSheet } from '../../components/FormSheet'; import { FormSheet } from '../../components/FormSheet';
import { EmojiIconButton } from '../../components/EmojiIconButton'; import { EmojiIconButton } from '../../components/EmojiIconButton';
import { useMobileActions } from '../../context/MobileActionContext'; import { useMobileActions } from '../../context/MobileActionContext';
@@ -1872,8 +1873,9 @@ export const TasksView: React.FC<TasksViewProps> = ({
</button> </button>
</div> </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"> <div className="flex items-center gap-2">
<LivePill />
{currentSelectedList && (() => { {currentSelectedList && (() => {
const listOwnerId = currentSelectedList.ownerId || currentSelectedList.userId; const listOwnerId = currentSelectedList.ownerId || currentSelectedList.userId;
const isOwner = currentSelectedList.isOwner !== false && (!listOwnerId || listOwnerId === currentUser?.id); 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 <button
type="button" type="button"
onClick={() => handleOpenShareModal(currentSelectedList)} onClick={() => handleOpenShareModal(currentSelectedList)}
@@ -1921,6 +1924,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div> </div>
); );
})()} })()}
{!currentSelectedList && <span className="w-px h-5 bg-border mx-0.5" aria-hidden />}
{/* View Switcher: List vs Kanban */} {/* View Switcher: List vs Kanban */}
<div className="flex border border-border rounded-btn bg-canvas overflow-hidden"> <div className="flex border border-border rounded-btn bg-canvas overflow-hidden">
@@ -1973,17 +1977,23 @@ export const TasksView: React.FC<TasksViewProps> = ({
: `Papierkorb (${taskTrash.length})`} : `Papierkorb (${taskTrash.length})`}
</span> </span>
</div> </div>
<div className="flex items-center gap-1.5 flex-shrink-0">
<LivePill />
{currentSelectedList && ( {currentSelectedList && (
<>
<span className="w-px h-5 bg-border" aria-hidden />
<button <button
type="button" type="button"
onClick={() => handleOpenShareModal(currentSelectedList)} onClick={() => handleOpenShareModal(currentSelectedList)}
className="btn-icon min-h-[36px] min-w-[36px] flex items-center justify-center flex-shrink-0" className="btn-icon min-h-[36px] min-w-[36px] flex items-center justify-center"
title="Aufgabenliste teilen" title="Aufgabenliste teilen"
> >
<Users className="w-4 h-4 text-accent" /> <Users className="w-4 h-4 text-accent" />
</button> </button>
</>
)} )}
</div> </div>
</div>
{isSelectedListReadOnly && ( {isSelectedListReadOnly && (
<div className="flex items-center gap-2 p-2.5 rounded-btn bg-panel-elevated border border-border text-xs text-muted"> <div className="flex items-center gap-2 p-2.5 rounded-btn bg-panel-elevated border border-border text-xs text-muted">