diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c83c02..aba0e39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [Unreleased] +## [0.58.0] - 2026-11-02 + +### Added +- Aufgaben: „Alle Aufgaben“ ist standardmäßig nach Liste gruppiert. Aufgaben ohne Liste stehen als „Ohne Liste“ ganz oben, danach die Listen in der Reihenfolge der Reiter. Unter „Gruppe“ lässt sich wie bisher anders gruppieren; einzelne Listen bleiben ungruppiert. + +### Changed +- Gewählte Aufgabenliste, Listen-/Kanban-Ansicht und Gruppierung sowie die gewählte Einkaufsliste merkt sich MindSift jetzt pro Gerät, auch über einen Neustart der installierten App oder einen neuen Tab hinweg. +- Zähler an Reitern und in der Navigation stehen direkt hinter dem Wort und hängen immer nur 2 px über den letzten Buchstaben, auch bei zwei- und dreistelligen Zahlen. Sie sind überall gefüllt (am Handy schien vorher das Icon durch) und haben keinen dunklen Ring mehr. + +### Fixed +- Handy: In den Aufgaben-Details waren Aufgabenliste, Personen-Suche, Hashtag und Unteraufgabe deutlich kleiner als die übrigen Felder; jetzt sind alle gleich groß (auch beim Teilen und in Gruppen), und iOS zoomt beim Antippen nicht mehr hinein. Der Knopf zur verknüpften Notiz heißt dort kurz „Notiz“, damit der Kopf einzeilig bleibt. + ## [0.57.0] - 2026-11-02 ### Changed @@ -836,7 +848,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V - Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken. - Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung. -[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.57.0...dev +[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.58.0...dev +[0.58.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.57.0...v0.58.0 [0.57.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.56.0...v0.57.0 [0.56.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.55.0...v0.56.0 [0.55.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.54.0...v0.55.0 diff --git a/Doku/ui-patterns.md b/Doku/ui-patterns.md index f02c6e9..6aed6d4 100644 --- a/Doku/ui-patterns.md +++ b/Doku/ui-patterns.md @@ -14,6 +14,7 @@ Eingesetzt beim Teilen (`UnifiedShareModal`), beim Hinzufügen zu Gruppen (`Grou - Bereits gewählte oder freigegebene Personen werden über `excludeUserIds` ausgeblendet. - **Einzelauswahl** (Teilen, Gruppen): `selectedUser` setzen; das Feld zeigt dann die gewählte Person mit ×. - **Mehrfachauswahl** (Aufgaben zuweisen): `selectedUser={null}` lassen und in `onSelectUser` zur Liste hinzufügen. Die Gewählten stehen als Chips (Avatar, Name, ×) über dem Feld, das Feld bleibt für die nächste Suche frei. +- Am Handy sind Feld, gewählte Person und Trefferzeilen 44 px hoch (Schrift 16 px, kein Zoom unter iOS), ab `sm` wie bisher 32 px. ## Profilbilder als Stapel @@ -58,13 +59,14 @@ Komponente: [FormSheet.tsx](../frontend/src/components/FormSheet.tsx), Tastatur- Komponente: [CountBadge.tsx](../frontend/src/components/CountBadge.tsx), CSS-Klassen `count-badge*` in [index.css](../frontend/src/index.css). Es gibt keine anderen Zähler-Stile mehr. -- **Form:** runder Kreis, mindestens 16 px, ab 100 als „99+“ (der Screenreader liest die echte Zahl). Bei 0 ausgeblendet, außer mit `showZero` (z. B. Mitgliederzahl, mobiles Menü). +- **Form:** runder, gefüllter Kreis mit dünnem Rand, ohne Schatten oder Ring, mindestens 16 px, ab 100 als „99+“ (der Screenreader liest die echte Zahl). Bei 0 ausgeblendet, außer mit `showZero` (z. B. Mitgliederzahl, mobiles Menü). - **Farbe (`variant`):** - `accent`: Hier wartet etwas auf dich. Navigationsleiste: Triage (Inbox), Aufgaben (offene Aufgaben), Listen (offene Einträge aller eigenen und geteilten Listen). Workspace hat keinen Zähler. - `muted` (Standard): reine Anzahl. Tabs der Aufgabenlisten und Listen, Organisationen, mobiles Menü. - - Beide kommen aus den Theme-Variablen (`--accent-primary`/`--text-on-accent` bzw. `--text-muted`/`--bg-panel`) und passen sich damit jedem Theme an. + - Beide kommen aus den Theme-Variablen (`--accent-primary`/`--text-on-accent` bzw. `--text-muted`/`--bg-panel`) und passen sich damit jedem Theme an. Der Akzent liegt auf einer deckenden Panel-Fläche, damit am Handy das Icon darunter nicht durchscheint. - **Position (`position`):** - - `corner`: Benachrichtigungs-Badge oben rechts am nächsten `relative` Elternelement, mit Ring in der Seitenfarbe. In der Navigation und an den Tabs sitzt er **auf dem Text**: der Text steht in einem `relative inline-flex`-Span (mit `pr-3`, solange ein Zähler da ist), der Kreis bekommt `-top-0.5 -right-2`, damit er die obere Tab-Linie nicht anschneidet und nicht am Text klebt. In der mobilen Leiste sitzt er am Icon (`-top-1.5 -right-2.5`). + - `text`: an Beschriftungen (Navigation, Reiter der Aufgabenlisten und Listen, Workspace-Kopf). Der Zähler steht im Textfluss direkt hinter dem Wort, leicht angehoben, und hängt immer 2 px über den letzten Buchstaben, egal wie viele Stellen die Zahl hat; breitere Zahlen machen den Reiter breiter. Der Text steht dafür in einem `inline-flex items-center`-Span. + - `corner`: Benachrichtigungs-Badge oben rechts am nächsten `relative` Elternelement; für Icons, z. B. in der mobilen Leiste (`-top-1.5 -right-2.5`). - `inline`: im Textfluss. - Zahlen im mobilen Menü (`MobileActionContext`, `badge: number`) werden automatisch als `CountBadge` gezeigt; Text-Badges wie „2 aktiv“ bleiben Textmarken. - **Wichtig für neue Klassen:** Tailwind entfernt Klassen aus `@layer components`, die nicht wörtlich im Code stehen. Klassennamen deshalb nie zusammensetzen (`count-badge-${variant}`), sondern ausschreiben. diff --git a/README.md b/README.md index 949fdcc..a367a2b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Self-Hosted](https://img.shields.io/badge/100%25-Self--Hosted-2563eb?style=flat-square)](#) [![Docker](https://img.shields.io/badge/Deployment-Single%20Container-2496ed?style=flat-square&logo=docker&logoColor=white)](#-schnellstart) [![SQLite](https://img.shields.io/badge/Daten-SQLite-003b57?style=flat-square&logo=sqlite&logoColor=white)](#-deine-daten-gehören-dir) -[![Version](https://img.shields.io/badge/Version-v0.57.0-16a34a?style=flat-square)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/Version-v0.58.0-16a34a?style=flat-square)](CHANGELOG.md) **MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 75e688a..3b2978f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.57.0", + "version": "0.58.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.57.0", + "version": "0.58.0", "license": "AGPL-3.0-only", "dependencies": { "@fontsource-variable/inter": "5.3.0", diff --git a/frontend/package.json b/frontend/package.json index aecf55a..7588054 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.57.0", + "version": "0.58.0", "license": "AGPL-3.0-only", "type": "module", "scripts": { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5b5416b..36fad85 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1198,9 +1198,9 @@ export const App: React.FC = () => { className={`tab-btn ${activeTab === 'triage' ? 'tab-btn-active' : ''}`} > - 0 ? 'pr-3' : ''}`}> + Triage - + @@ -1223,9 +1223,9 @@ export const App: React.FC = () => { className={`tab-btn ${activeTab === 'tasks' ? 'tab-btn-active' : ''}`} > - 0 ? 'pr-3' : ''}`}> + Aufgaben - + @@ -1237,9 +1237,9 @@ export const App: React.FC = () => { className={`tab-btn ${activeTab === 'lists' ? 'tab-btn-active' : ''}`} > - 0 ? 'pr-3' : ''}`}> + Listen - + diff --git a/frontend/src/components/CountBadge.tsx b/frontend/src/components/CountBadge.tsx index f512dde..b411566 100644 --- a/frontend/src/components/CountBadge.tsx +++ b/frontend/src/components/CountBadge.tsx @@ -4,14 +4,16 @@ interface CountBadgeProps { count: number | undefined | null; // accent: something waits for you (nav bar); muted: plain amount (tabs, lists) variant?: 'accent' | 'muted'; - // corner: notification badge on the top right of the nearest `relative` parent - position?: 'inline' | 'corner'; + // corner: notification badge on the top right of the nearest `relative` parent (icons); + // text: right after a label, raised and hanging a little over its last letter (tabs, nav bar) + position?: 'inline' | 'corner' | 'text'; showZero?: boolean; className?: string; // e.g. to fine-tune the corner offset } // Full class names: Tailwind drops component classes it cannot find literally in the source const VARIANT_CLASS = { accent: 'count-badge-accent', muted: 'count-badge-muted' }; +const POSITION_CLASS = { inline: '', corner: 'count-badge-corner', text: 'count-badge-text' }; // The one counter of the app (Doku/ui-patterns.md); hides itself at 0 unless showZero export const CountBadge: React.FC = ({ @@ -25,7 +27,7 @@ export const CountBadge: React.FC = ({ const label = count > 99 ? '99+' : String(count); return ( {label} diff --git a/frontend/src/features/UserAutocomplete.tsx b/frontend/src/features/UserAutocomplete.tsx index debcde6..def6d8c 100644 --- a/frontend/src/features/UserAutocomplete.tsx +++ b/frontend/src/features/UserAutocomplete.tsx @@ -103,7 +103,7 @@ export const UserAutocomplete: React.FC = ({ return (
{selectedUser ? ( -
+
@@ -148,7 +148,7 @@ export const UserAutocomplete: React.FC = ({ onFocus={() => setIsOpen(true)} onKeyDown={handleKeyDown} placeholder={placeholder} - className="w-full h-8 pl-8 pr-2.5 bg-input text-main border border-border rounded-btn focus:outline-none focus:border-accent text-xs placeholder:text-muted transition-colors disabled:opacity-50" + className="w-full h-11 sm:h-8 pl-8 pr-2.5 bg-input text-main border border-border rounded-btn focus:outline-none focus:border-accent text-base sm:text-xs placeholder:text-muted transition-colors disabled:opacity-50" /> {showResults && ( @@ -166,7 +166,7 @@ export const UserAutocomplete: React.FC = ({ type="button" onClick={() => handleSelect(u)} onMouseEnter={() => setHighlightedIndex(idx)} - className={`w-full px-2.5 py-1.5 flex items-center justify-between gap-2 text-left text-xs transition-colors cursor-pointer ${ + className={`w-full min-h-11 sm:min-h-0 px-2.5 py-1.5 flex items-center justify-between gap-2 text-left text-xs transition-colors cursor-pointer ${ isHighlighted ? 'bg-accent/15 text-main font-medium' : 'hover:bg-panel-elevated text-main' diff --git a/frontend/src/features/lists/SharedListsView.tsx b/frontend/src/features/lists/SharedListsView.tsx index 88752d8..0bd02cb 100644 --- a/frontend/src/features/lists/SharedListsView.tsx +++ b/frontend/src/features/lists/SharedListsView.tsx @@ -11,7 +11,7 @@ import { } from 'lucide-react'; import { ListItem, SharedList, Team, User, WSMessage } from '../../lib/types'; import { api, triggerHaptic, createRealtimeConnection } from '../../lib/api'; -import { readViewSession, writeViewSession } from '../../lib/viewSession'; +import { readViewMemory, writeViewMemory } from '../../lib/viewSession'; import { OFFLINE_ITEM_PREFIX, pendingListItems } from '../../lib/offline'; import { UnifiedShareModal } from '../../components/UnifiedShareModal'; import { AvatarStack, AvatarPerson } from '../../components/AvatarStack'; @@ -91,12 +91,12 @@ export function deduplicateItems(items: ListItem[]): ListItem[] { export const SharedListsView: React.FC = ({ onRefreshGlobal, currentUser, initialListId }) => { const { setGroups, setPrimaryAction } = useMobileActions(); const [lists, setLists] = useState([]); - // The open list survives switching to other tabs (like the workspace's open note) + // The open list is remembered on this device (other tabs of the app, new tab, restart) const [activeListId, setActiveListId] = useState( - () => initialListId || readViewSession<{ listId: string }>('lists').listId || '' + () => initialListId || readViewMemory<{ listId: string }>('lists').listId || '' ); useEffect(() => { - if (activeListId) writeViewSession('lists', { listId: activeListId }); + if (activeListId) writeViewMemory('lists', { listId: activeListId }); }, [activeListId]); useEffect(() => { @@ -1063,9 +1063,9 @@ export const SharedListsView: React.FC = ({ onRefreshGloba > {lst.icon} {/* Counter on the label, like in the nav bar */} - 0 ? 'pr-3' : ''}`}> + {lst.title} - + className="btn-primary min-h-[38px] sm:min-h-0 h-9 sm:h-7 px-2.5 sm:px-2 text-xs flex items-center gap-1.5 font-semibold" > - In Notiz öffnen + {/* Short on phones, so the header buttons stay on one line */} + Notiz + In Notiz öffnen ) : !readOnly && (
@@ -3014,7 +3064,7 @@ export const TasksView: React.FC = ({
) : (
- {groupedTasks ? ( + {groupedTasks && groupedTasks.length > 0 ? ( /* GROUPED LIST */ groupedTasks.map((grp) => (
diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index 9708483..fa478bf 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -1623,9 +1623,9 @@ export const WorkspaceView: React.FC = ({ }`} > - 0 ? 'pr-3' : ''}`}> + To-Dos - + @@ -1642,9 +1642,9 @@ export const WorkspaceView: React.FC = ({ }`} > - 0 ? 'pr-3' : ''}`}> + Hashtags - +
diff --git a/frontend/src/index.css b/frontend/src/index.css index c1c62f2..bd1671e 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -322,7 +322,8 @@ body { pointer-events: none; } .count-badge-accent { - background: var(--accent-dim); + /* The tint on an opaque panel: the badge stays solid on top of icons (phone nav bar) */ + background: linear-gradient(var(--accent-dim), var(--accent-dim)), var(--bg-panel); color: var(--accent-primary); border: 1px solid var(--accent-border); } @@ -336,8 +337,14 @@ body { top: -0.5rem; right: -0.5rem; z-index: 1; - /* ring in the page colour keeps the badge readable on top of borders */ - box-shadow: 0 0 0 2px var(--bg-app); + } + /* In the text flow right after a label: hangs 2 px over the last letter, however many digits; wider + numbers make the tab wider instead of covering more of the word */ + .count-badge-text { + position: relative; + top: -0.375rem; + margin-left: -2px; + z-index: 1; } .badge-ok { diff --git a/frontend/src/lib/viewSession.ts b/frontend/src/lib/viewSession.ts index 51f0f4e..85f0b8c 100644 --- a/frontend/src/lib/viewSession.ts +++ b/frontend/src/lib/viewSession.ts @@ -18,3 +18,27 @@ export function writeViewSession(view: string, state: T) { // storage blocked: the view simply starts fresh } } + +// Remembered per device as well (localStorage): a new tab or a restart of the installed app opens the +// same list again. This tab's own state wins, so two tabs can show different lists. +const DEVICE_PREFIX = 'mindsift_view_device:'; + +export function readViewMemory(view: string): Partial { + let device: Partial = {}; + try { + const raw = JSON.parse(localStorage.getItem(DEVICE_PREFIX + view) || '{}'); + if (raw && typeof raw === 'object') device = raw; + } catch { + // storage blocked or broken: the tab state alone + } + return { ...device, ...readViewSession(view) }; +} + +export function writeViewMemory(view: string, state: T) { + writeViewSession(view, state); + try { + localStorage.setItem(DEVICE_PREFIX + view, JSON.stringify(state)); + } catch { + // only a convenience + } +}