Merge dev into main (v0.57.0)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 30s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 30s
This commit is contained in:
commit
6799a8506f
12 files changed
+55
-25
No files matched your search
@@ -85,7 +85,7 @@ Fremde Branches (z. B. von anderen Agenten) werden vor dem Mergen geprüft.
|
||||
## Oberfläche
|
||||
|
||||
- **Einheitliche Bedienelemente:** Personen-Suche, Profilbild-Stapel, Zähler und weitere Muster stehen in [Doku/ui-patterns.md](Doku/ui-patterns.md). Vorhandene Komponenten verwenden statt neuer Varianten; neue gemeinsame Muster dort ergänzen.
|
||||
- **Handy und Desktop:** Unter `sm` ist Handy-Layout, ab `lg` die volle Desktop-Ansicht (z. B. Seitenleiste neben der Notiz). Wird nur das Handy geändert, bleiben die Werte ab `sm`/`lg` exakt wie vorher.
|
||||
- **Handy und Desktop:** Unter `sm` ist Handy-Layout, ab `lg` die volle Desktop-Ansicht (z. B. Seitenleiste neben der Notiz). `sm` liegt bei **768 px**, nicht bei Tailwinds 640 px (Handys quer und kleine Tablets bekommen das Handy-Layout); `md` ist gleich `sm`. Im Code dafür `isPhoneWidth()` aus `lib/breakpoints.ts` verwenden, keine feste Pixelzahl. Wird nur das Handy geändert, bleiben die Werte ab `sm`/`lg` exakt wie vorher.
|
||||
- **Antippen am Handy:** Ziele mindestens 44 px hoch (`h-11 sm:h-…`). Auf reinen Touch-Geräten gibt es kein HTML5-Drag-&-Drop; Kanban nutzt dafür `useKanbanTouchDrag`, die Notizen die Leiste über der Tastatur (`MobileOutlineBar`).
|
||||
- **Themes:** Farben nur über die Theme-Variablen bzw. Klassen (`bg-panel`, `bg-canvas`, `text-main`, `text-muted`, `border-border`, `bg-accent` …), keine festen Farben. Rahmen mit `bg-panel`/`bg-canvas` bekommen die Ecken des Themes automatisch; liegt darin ein Kopfbereich mit eigenem Hintergrund, braucht der Rahmen `overflow-hidden`, sonst ragen bei runden Ecken die Kanten heraus.
|
||||
- **Installierte App (PWA):** Manifest-Änderungen (Farben, Verknüpfungen) übernimmt Android erst, wenn es die App aktualisiert (bis etwa einen Tag) oder nach Neuinstallation; normaler App-Code kommt beim nächsten Start. Ändert sich der Cache-Inhalt des Service Workers, `VERSION` in `frontend/public/sw.js` erhöhen.
|
||||
+10
-1
@@ -6,6 +6,14 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.57.0] - 2026-11-02
|
||||
|
||||
### Changed
|
||||
- Die Handy-Ansicht gilt jetzt bis 767 px Breite (bisher bis 639 px). Handys im Querformat, kleine Tablets im Hochformat (z. B. iPad mini) und schmale Fenster bekommen damit die Ansicht mit großen Tippzielen und unterer Navigation statt einer gedrängten Desktop-Ansicht.
|
||||
|
||||
### Fixed
|
||||
- Desktop: Die Quick-Dump-Leiste steht genau mittig im Kopf und bleibt stehen, wenn der Verbindungsstatus wechselt (z. B. von „online“ zu „eingeschränkt“) oder der Name lang ist.
|
||||
|
||||
## [0.56.0] - 2026-11-02
|
||||
|
||||
### Added
|
||||
@@ -828,7 +836,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.56.0...dev
|
||||
[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.57.0...dev
|
||||
[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
|
||||
[0.54.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.53.0...v0.54.0
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[](#)
|
||||
[](#-schnellstart)
|
||||
[](#-deine-daten-gehören-dir)
|
||||
[](CHANGELOG.md)
|
||||
[](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.
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mindsift-frontend",
|
||||
"version": "0.56.0",
|
||||
"version": "0.57.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mindsift-frontend",
|
||||
"version": "0.56.0",
|
||||
"version": "0.57.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "5.3.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mindsift-frontend",
|
||||
"private": true,
|
||||
"version": "0.56.0",
|
||||
"version": "0.57.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+12
-9
@@ -82,6 +82,7 @@ import { consumePendingShares, formatSharedThought } from './lib/shareTarget';
|
||||
import { APP_VERSION } from './version';
|
||||
import { readViewSession, writeViewSession } from './lib/viewSession';
|
||||
import { setVoicePreference } from './lib/voice';
|
||||
import { isPhoneWidth } from './lib/breakpoints';
|
||||
|
||||
// Connection status at the profile (useConnectionState); full class names for Tailwind
|
||||
const CONNECTION_STATUS = {
|
||||
@@ -302,7 +303,7 @@ export const App: React.FC = () => {
|
||||
// pendingShortcut above re-runs this effect and must not cancel the timer)
|
||||
window.setTimeout(() => {
|
||||
if (target === 'triage') handleTriggerAddButton();
|
||||
else if (window.innerWidth < 640 || target === 'lists') primaryActionRef.current?.();
|
||||
else if (isPhoneWidth() || target === 'lists') primaryActionRef.current?.();
|
||||
}, 400);
|
||||
}, [pendingShortcut, user?.id, activeTab]);
|
||||
|
||||
@@ -465,7 +466,7 @@ export const App: React.FC = () => {
|
||||
// Immediately offer Web-App installation on mobile if not yet running as installed standalone app
|
||||
useEffect(() => {
|
||||
if (!user || isStandaloneApp) return;
|
||||
if (window.innerWidth < 640 && !sessionStorage.getItem('mindsift_pwa_dismissed')) {
|
||||
if (isPhoneWidth() && !sessionStorage.getItem('mindsift_pwa_dismissed')) {
|
||||
setShowPwaInstallModal(true);
|
||||
}
|
||||
}, [user?.id, isStandaloneApp]);
|
||||
@@ -828,7 +829,7 @@ export const App: React.FC = () => {
|
||||
|
||||
const handleTriggerAddButton = () => {
|
||||
triggerHaptic(20);
|
||||
if (window.innerWidth < 640) {
|
||||
if (isPhoneWidth()) {
|
||||
openMobileTriageCapture();
|
||||
} else {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
@@ -1008,15 +1009,16 @@ export const App: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Header: 3-column Layout (Logo, InstantCaptureBar, Profile Pill) */}
|
||||
<div className="hidden sm:flex max-w-6xl mx-auto px-4 py-2 items-start justify-between gap-6">
|
||||
{/* Desktop Header: 3-column grid (Logo, InstantCaptureBar, Profile Pill) with equal outer columns, so
|
||||
the capture bar stays exactly centred whatever name or status stands on the right */}
|
||||
<div className="hidden sm:grid grid-cols-[minmax(12rem,1fr)_minmax(0,36rem)_minmax(12rem,1fr)] max-w-6xl mx-auto px-4 py-2 items-start gap-6">
|
||||
{/* Logo & Headline */}
|
||||
<div
|
||||
onClick={() => {
|
||||
setActiveTab('triage');
|
||||
handleTriggerAddButton();
|
||||
}}
|
||||
className="h-9 flex items-center gap-2.5 cursor-pointer group select-none flex-shrink-0"
|
||||
className="h-9 flex items-center gap-2.5 cursor-pointer group select-none flex-shrink-0 justify-self-start"
|
||||
>
|
||||
<div className="w-7 h-7 bg-accent text-on-accent rounded-btn flex items-center justify-center">
|
||||
<Brain className="w-4 h-4" />
|
||||
@@ -1039,7 +1041,7 @@ export const App: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Quick-Capture Input Bubble directly in Header (above Tabs) */}
|
||||
<div className="flex-1 max-w-xl mx-auto flex items-center gap-2">
|
||||
<div className="w-full max-w-xl mx-auto flex items-center gap-2">
|
||||
<div className="flex-1">
|
||||
<InstantCaptureBar
|
||||
onCaptured={handleCaptured}
|
||||
@@ -1059,7 +1061,7 @@ export const App: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* User Profile Pill & Dropdown Menu */}
|
||||
<div className="relative h-9 flex items-center gap-2 flex-shrink-0" ref={dropdownRef}>
|
||||
<div className="relative h-9 flex items-center gap-2 flex-shrink-0 justify-self-end" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => {
|
||||
triggerHaptic(10);
|
||||
@@ -1069,7 +1071,8 @@ export const App: React.FC = () => {
|
||||
title="Benutzermenü öffnen"
|
||||
>
|
||||
<UserAvatar user={user} size="sm" />
|
||||
<span className="flex flex-col items-start leading-tight">
|
||||
{/* Wide enough for the longest status ("eingeschränkt"), so switching status moves nothing */}
|
||||
<span className="flex flex-col items-start leading-tight min-w-[5.5rem]">
|
||||
<span className="max-w-[130px] truncate hidden sm:inline text-main font-medium">
|
||||
{user.displayName}
|
||||
</span>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { AvatarStack, AvatarPerson } from '../../components/AvatarStack';
|
||||
import { CountBadge } from '../../components/CountBadge';
|
||||
import { ListSwitcher } from '../../components/ListSwitcher';
|
||||
import { LivePill } from '../../components/LivePill';
|
||||
import { isPhoneWidth } from '../../lib/breakpoints';
|
||||
import { OriginBadge } from '../../components/OriginBadge';
|
||||
import { VoiceInputButton } from '../../components/VoiceInputButton';
|
||||
import { FormSheet } from '../../components/FormSheet';
|
||||
@@ -126,10 +127,10 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
// Phones: the add form lives in a sheet opened by "+" instead of taking room above the list
|
||||
const [isPhone, setIsPhone] = useState(() => typeof window !== 'undefined' && window.innerWidth < 640);
|
||||
const [isPhone, setIsPhone] = useState(isPhoneWidth);
|
||||
const [showAddSheet, setShowAddSheet] = useState(false);
|
||||
useEffect(() => {
|
||||
const onResize = () => setIsPhone(window.innerWidth < 640);
|
||||
const onResize = () => setIsPhone(isPhoneWidth());
|
||||
window.addEventListener('resize', onResize);
|
||||
return () => window.removeEventListener('resize', onResize);
|
||||
}, []);
|
||||
@@ -838,7 +839,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
||||
}, [openItems]);
|
||||
|
||||
const focusNewItemInput = () => {
|
||||
if (window.innerWidth < 640) {
|
||||
if (isPhoneWidth()) {
|
||||
setShowAddSheet(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { X, Trash2, Loader2 } from 'lucide-react';
|
||||
import type { EmojiClickData } from 'emoji-picker-react';
|
||||
import { Categories, EmojiStyle, Theme, SuggestionMode } from 'emoji-picker-react';
|
||||
import { useKeyboardInset } from '../../lib/useKeyboardInset';
|
||||
import { isPhoneWidth } from '../../lib/breakpoints';
|
||||
|
||||
// Lazy-load emoji-picker-react so the main bundle size stays small
|
||||
const EmojiPicker = React.lazy(() => import('emoji-picker-react'));
|
||||
@@ -38,7 +39,7 @@ export const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
|
||||
anchor,
|
||||
}) => {
|
||||
const { inset, visibleHeight } = useKeyboardInset();
|
||||
const isPhone = typeof window !== 'undefined' && window.innerWidth < 640;
|
||||
const isPhone = isPhoneWidth();
|
||||
// On phones the picker shrinks with the keyboard so search field and results stay visible
|
||||
const pickerHeight = isPhone ? Math.max(200, Math.min(350, visibleHeight - 110)) : 300;
|
||||
const [isDark, setIsDark] = useState<boolean>(() =>
|
||||
@@ -105,14 +106,14 @@ export const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
|
||||
|
||||
const picker = (
|
||||
<>
|
||||
{/* Mobile Backdrop (< 640px) */}
|
||||
{/* Mobile Backdrop (below sm) */}
|
||||
<div
|
||||
className={`fixed inset-0 bg-black/60 sm:hidden backdrop-blur-xs ${anchor ? 'z-[60]' : 'z-40'}`}
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Main Container: Mobile Bottom-Sheet (<640px) & Desktop Popover (>=640px) */}
|
||||
{/* Main Container: Mobile Bottom-Sheet (below sm) & Desktop Popover (from sm) */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`
|
||||
|
||||
@@ -60,6 +60,7 @@ import { CountBadge } from '../../components/CountBadge';
|
||||
import { EmojiIconButton } from '../../components/EmojiIconButton';
|
||||
import { LazyMount } from '../../components/LazyMount';
|
||||
import { extractHashtags } from '../../lib/hashtags';
|
||||
import { isPhoneWidth } from '../../lib/breakpoints';
|
||||
|
||||
interface WorkspaceViewProps {
|
||||
currentUser?: User | null;
|
||||
@@ -624,7 +625,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
const [newNoteIcon, setNewNoteIcon] = useState('🗒️');
|
||||
const openNewNote = useCallback(
|
||||
(folderPath = '') => {
|
||||
if (window.innerWidth >= 640) {
|
||||
if (!isPhoneWidth()) {
|
||||
void handleCreateDocument(folderPath);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -563,7 +563,7 @@ body {
|
||||
--epr-dark-category-icon-active-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
@media (max-width: 767px) {
|
||||
/* Prevent iOS auto-zoom on input focus */
|
||||
.mindsift-emoji-picker .EmojiPickerReact input.epr-search-input {
|
||||
font-size: 1rem !important;
|
||||
@@ -584,7 +584,7 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
@media (min-width: 768px) {
|
||||
/* Desktop: compact like a context menu, in step with the app's small type (the sprite of the category
|
||||
icons scales with its size variable) */
|
||||
.mindsift-emoji-picker .EmojiPickerReact {
|
||||
@@ -630,7 +630,7 @@ fieldset:disabled :is(button, input, select, textarea) {
|
||||
--outline-indent: 14px;
|
||||
--outline-min-text: 14rem;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
@media (min-width: 768px) {
|
||||
:root {
|
||||
--outline-indent: 22px;
|
||||
--outline-min-text: 0px;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// Width below which the app uses its phone layout; the same value as Tailwind's sm (tailwind.config.js)
|
||||
// and the media queries in index.css.
|
||||
export const PHONE_MAX_WIDTH = 768;
|
||||
|
||||
export const isPhoneWidth = (): boolean => typeof window !== 'undefined' && window.innerWidth < PHONE_MAX_WIDTH;
|
||||
@@ -12,6 +12,16 @@ export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
// Phone layout below sm, desktop from sm (see AGENTS.md). sm is 768 px, not Tailwind's 640, so phones in
|
||||
// landscape and small tablets (iPad mini upright) get the touch layout too; keep PHONE_MAX_WIDTH in
|
||||
// src/lib/breakpoints.ts and the media queries in index.css in step.
|
||||
screens: {
|
||||
sm: '768px',
|
||||
md: '768px',
|
||||
lg: '1024px',
|
||||
xl: '1280px',
|
||||
'2xl': '1536px',
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
canvas: v('--bg-app'),
|
||||
|
||||
Reference in new issue
Block a user