feat(mobile): list items and new notes via a sheet opened by "+"
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m34s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m34s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
19f98ae92f
commit
c43c99dc0c
6 files changed
+197
-95
No files matched your search
+8
-1
@@ -6,6 +6,12 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.25.0] - 2026-10-31
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Einkaufslisten am Handy: Das Eingabefeld steht nicht mehr dauerhaft über der Liste. „+“ öffnet es als Tafel von unten (wie bei Aufgaben und Triage); nach „Hinzufügen“ bleibt sie offen und leer für den nächsten Eintrag. Am Desktop bleibt das Feld wie bisher.
|
||||||
|
- Notizen am Handy: „+“ (und „Neue Notiz“ im Menü) fragt zuerst Name und Emoji ab; erst „Erstellen“ legt die Notiz an. Ein versehentlicher Tipp hinterlässt so keine leere Notiz mehr. Am Desktop entsteht die Notiz wie bisher sofort.
|
||||||
|
|
||||||
## [0.24.2] - 2026-10-31
|
## [0.24.2] - 2026-10-31
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -508,7 +514,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.
|
- 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.
|
- 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.24.2...dev
|
[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.25.0...dev
|
||||||
|
[0.25.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.24.2...v0.25.0
|
||||||
[0.24.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.24.1...v0.24.2
|
[0.24.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.24.1...v0.24.2
|
||||||
[0.24.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.24.0...v0.24.1
|
[0.24.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.24.0...v0.24.1
|
||||||
[0.24.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.23.0...v0.24.0
|
[0.24.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.23.0...v0.24.0
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
[](#)
|
[](#)
|
||||||
[](#-schnellstart)
|
[](#-schnellstart)
|
||||||
[](#-deine-daten-gehören-dir)
|
[](#-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.
|
**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",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.24.2",
|
"version": "0.25.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.24.2",
|
"version": "0.25.0",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "5.3.0",
|
"@fontsource-variable/inter": "5.3.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.24.2",
|
"version": "0.25.0",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -121,6 +121,15 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
const [workspaceTags, setWorkspaceTags] = useState<string[]>([]);
|
const [workspaceTags, setWorkspaceTags] = useState<string[]>([]);
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
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 [showAddSheet, setShowAddSheet] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const onResize = () => setIsPhone(window.innerWidth < 640);
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
return () => window.removeEventListener('resize', onResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [draggingItemId, setDraggingItemId] = useState<string | null>(null);
|
const [draggingItemId, setDraggingItemId] = useState<string | null>(null);
|
||||||
const [dragOverItemTarget, setDragOverItemTarget] = useState<{ itemId: string; position: 'before' | 'after' } | null>(null);
|
const [dragOverItemTarget, setDragOverItemTarget] = useState<{ itemId: string; position: 'before' | 'after' } | null>(null);
|
||||||
const [dragOverCategoryTarget, setDragOverCategoryTarget] = useState<string | null>(null);
|
const [dragOverCategoryTarget, setDragOverCategoryTarget] = useState<string | null>(null);
|
||||||
@@ -312,6 +321,8 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
|
|
||||||
const handleAddItem = async (e: React.FormEvent) => {
|
const handleAddItem = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
// Keep the field (and the phone keyboard) for the next item: refocus inside the tap
|
||||||
|
inputRef.current?.focus({ preventScroll: true });
|
||||||
if (!activeList || !newItemText.trim() || isSubmittingItem) return;
|
if (!activeList || !newItemText.trim() || isSubmittingItem) return;
|
||||||
setIsSubmittingItem(true);
|
setIsSubmittingItem(true);
|
||||||
triggerHaptic(15);
|
triggerHaptic(15);
|
||||||
@@ -827,6 +838,10 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
}, [openItems]);
|
}, [openItems]);
|
||||||
|
|
||||||
const focusNewItemInput = () => {
|
const focusNewItemInput = () => {
|
||||||
|
if (window.innerWidth < 640) {
|
||||||
|
setShowAddSheet(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
inputRef.current?.focus({ preventScroll: true });
|
inputRef.current?.focus({ preventScroll: true });
|
||||||
inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
};
|
};
|
||||||
@@ -909,6 +924,99 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
]);
|
]);
|
||||||
}, [lists, activeList, completedItems.length, setGroups, setPrimaryAction]);
|
}, [lists, activeList, completedItems.length, setGroups, setPrimaryAction]);
|
||||||
|
|
||||||
|
// The add form's fields: inline on the desktop, on phones in a sheet that "+" opens (no fixed field
|
||||||
|
// taking room above the list); the sheet brings its own buttons
|
||||||
|
const renderAddItemFields = (withButton: boolean) =>
|
||||||
|
activeList ? (
|
||||||
|
<>
|
||||||
|
{/* The item is the main field: a real input like the quantity next to it, with the + inside */}
|
||||||
|
<div className="relative flex-1 w-full min-w-0">
|
||||||
|
<Plus className="w-4 h-4 text-accent absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none" />
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
autoFocus={!withButton}
|
||||||
|
value={newItemText}
|
||||||
|
onChange={(e) => {
|
||||||
|
setNewItemText(e.target.value);
|
||||||
|
setCursorPosition(e.target.selectionStart);
|
||||||
|
setShowSuggestions(true);
|
||||||
|
setSuggestionIndex(0);
|
||||||
|
}}
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
onSelect={(e) => {
|
||||||
|
setCursorPosition(e.currentTarget.selectionStart);
|
||||||
|
}}
|
||||||
|
onFocus={(e) => {
|
||||||
|
setCursorPosition(e.target.selectionStart);
|
||||||
|
setShowSuggestions(true);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setTimeout(() => setShowSuggestions(false), 200);
|
||||||
|
}}
|
||||||
|
placeholder={`Was soll auf „${activeList.title}“? (z. B. Hafermilch #Kühlregal)`}
|
||||||
|
// 16px on phones: iOS zooms into smaller fields and the layout jumps under the keyboard
|
||||||
|
className="w-full h-11 sm:h-9 pl-9 pr-3 input-field text-base sm:text-sm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Hashtag Autocomplete Suggestions Dropdown */}
|
||||||
|
{showSuggestions && activeHashtagQuery && matchingSuggestions.length > 0 && (
|
||||||
|
<div className="absolute left-6 top-full mt-2 w-64 max-h-56 overflow-y-auto bg-panel-elevated border border-border rounded-btn shadow-xl py-1 z-50 animate-in fade-in zoom-in-95 duration-100">
|
||||||
|
<div className="px-2.5 py-1 text-[10px] uppercase font-semibold text-faint tracking-wider border-b border-border/50">
|
||||||
|
Hashtags
|
||||||
|
</div>
|
||||||
|
{matchingSuggestions.map((tag, idx) => {
|
||||||
|
const isSelected = idx === suggestionIndex;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tag}
|
||||||
|
type="button"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
applySuggestion(tag);
|
||||||
|
}}
|
||||||
|
className={`w-full text-left px-3 py-2 sm:py-1.5 text-xs flex items-center justify-between transition-colors ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-accent/20 text-accent font-medium'
|
||||||
|
: 'text-main hover:bg-panel-hover'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<span className="text-accent font-mono font-semibold">#</span>
|
||||||
|
<span>{tag}</span>
|
||||||
|
</span>
|
||||||
|
{isSelected && (
|
||||||
|
<span className="text-[10px] text-faint font-mono">↵</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 w-full sm:w-auto">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
|
value={newItemQty}
|
||||||
|
onChange={(e) => setNewItemQty(e.target.value.replace(/[^0-9.,xX]/g, ''))}
|
||||||
|
placeholder="Menge"
|
||||||
|
aria-label="Menge (optional)"
|
||||||
|
className="w-24 sm:w-20 h-11 sm:h-9 px-2.5 input-field text-base sm:text-sm"
|
||||||
|
/>
|
||||||
|
{withButton && (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmittingItem || !newItemText.trim()}
|
||||||
|
className="btn-primary flex-1 sm:flex-none h-11 sm:h-9 px-4 text-sm sm:text-xs disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center gap-1 flex-shrink-0"
|
||||||
|
>
|
||||||
|
{isSubmittingItem ? 'Wird hinzugefügt…' : 'Hinzufügen'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null;
|
||||||
|
|
||||||
// Connection pill in the same look as the notes' header
|
// Connection pill in the same look as the notes' header
|
||||||
const livePill = (
|
const livePill = (
|
||||||
<span
|
<span
|
||||||
@@ -1107,94 +1215,29 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
{/* Active Shared List View */}
|
{/* Active Shared List View */}
|
||||||
{activeList ? (
|
{activeList ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Quick-Add Form */}
|
{/* Phones: "+" opens the add form as a sheet; it stays open for the next item */}
|
||||||
|
{isPhone && showAddSheet && (
|
||||||
|
<FormSheet
|
||||||
|
title={`Auf „${activeList.title}“`}
|
||||||
|
icon={<ShoppingCart className="w-4 h-4 text-accent" />}
|
||||||
|
onClose={() => setShowAddSheet(false)}
|
||||||
|
onSubmit={handleAddItem}
|
||||||
|
submitLabel="Hinzufügen"
|
||||||
|
busy={isSubmittingItem}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2 relative">{renderAddItemFields(false)}</div>
|
||||||
|
</FormSheet>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Quick-Add Form (desktop; on phones it is the sheet above) */}
|
||||||
|
{!isPhone && (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleAddItem}
|
onSubmit={handleAddItem}
|
||||||
className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 panel-surface p-2 relative"
|
className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 panel-surface p-2 relative"
|
||||||
>
|
>
|
||||||
{/* The item is the main field: a real input like the quantity next to it, with the + inside */}
|
{renderAddItemFields(true)}
|
||||||
<div className="relative flex-1 w-full min-w-0">
|
|
||||||
<Plus className="w-4 h-4 text-accent absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none" />
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
type="text"
|
|
||||||
value={newItemText}
|
|
||||||
onChange={(e) => {
|
|
||||||
setNewItemText(e.target.value);
|
|
||||||
setCursorPosition(e.target.selectionStart);
|
|
||||||
setShowSuggestions(true);
|
|
||||||
setSuggestionIndex(0);
|
|
||||||
}}
|
|
||||||
onKeyDown={handleInputKeyDown}
|
|
||||||
onSelect={(e) => {
|
|
||||||
setCursorPosition(e.currentTarget.selectionStart);
|
|
||||||
}}
|
|
||||||
onFocus={(e) => {
|
|
||||||
setCursorPosition(e.target.selectionStart);
|
|
||||||
setShowSuggestions(true);
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
setTimeout(() => setShowSuggestions(false), 200);
|
|
||||||
}}
|
|
||||||
placeholder={`Was soll auf „${activeList.title}“? (z. B. Hafermilch #Kühlregal)`}
|
|
||||||
// 16px on phones: iOS zooms into smaller fields and the layout jumps under the keyboard
|
|
||||||
className="w-full h-11 sm:h-9 pl-9 pr-3 input-field text-base sm:text-sm"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Hashtag Autocomplete Suggestions Dropdown */}
|
|
||||||
{showSuggestions && activeHashtagQuery && matchingSuggestions.length > 0 && (
|
|
||||||
<div className="absolute left-6 top-full mt-2 w-64 max-h-56 overflow-y-auto bg-panel-elevated border border-border rounded-btn shadow-xl py-1 z-50 animate-in fade-in zoom-in-95 duration-100">
|
|
||||||
<div className="px-2.5 py-1 text-[10px] uppercase font-semibold text-faint tracking-wider border-b border-border/50">
|
|
||||||
Hashtags
|
|
||||||
</div>
|
|
||||||
{matchingSuggestions.map((tag, idx) => {
|
|
||||||
const isSelected = idx === suggestionIndex;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={tag}
|
|
||||||
type="button"
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
applySuggestion(tag);
|
|
||||||
}}
|
|
||||||
className={`w-full text-left px-3 py-2 sm:py-1.5 text-xs flex items-center justify-between transition-colors ${
|
|
||||||
isSelected
|
|
||||||
? 'bg-accent/20 text-accent font-medium'
|
|
||||||
: 'text-main hover:bg-panel-hover'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="flex items-center gap-1.5">
|
|
||||||
<span className="text-accent font-mono font-semibold">#</span>
|
|
||||||
<span>{tag}</span>
|
|
||||||
</span>
|
|
||||||
{isSelected && (
|
|
||||||
<span className="text-[10px] text-faint font-mono">↵</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 w-full sm:w-auto">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
inputMode="decimal"
|
|
||||||
value={newItemQty}
|
|
||||||
onChange={(e) => setNewItemQty(e.target.value.replace(/[^0-9.,xX]/g, ''))}
|
|
||||||
placeholder="Menge"
|
|
||||||
aria-label="Menge (optional)"
|
|
||||||
className="w-24 sm:w-20 h-11 sm:h-9 px-2.5 input-field text-base sm:text-sm"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isSubmittingItem || !newItemText.trim()}
|
|
||||||
className="btn-primary flex-1 sm:flex-none h-11 sm:h-9 px-4 text-sm sm:text-xs disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center gap-1 flex-shrink-0"
|
|
||||||
>
|
|
||||||
{isSubmittingItem ? 'Wird hinzugefügt…' : 'Hinzufügen'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Open Items Grouped by Hashtags / Categories */}
|
{/* Open Items Grouped by Hashtags / Categories */}
|
||||||
{openItems.length === 0 ? (
|
{openItems.length === 0 ? (
|
||||||
|
|||||||
@@ -562,14 +562,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
}, [currentUser, reloadTrash]);
|
}, [currentUser, reloadTrash]);
|
||||||
|
|
||||||
const handleCreateDocument = useCallback(
|
const handleCreateDocument = useCallback(
|
||||||
async (folderPath = '') => {
|
async (folderPath = '', title = 'Neue Notiz', icon = '📝') => {
|
||||||
if (isCreatingRef.current) return;
|
if (isCreatingRef.current) return;
|
||||||
isCreatingRef.current = true;
|
isCreatingRef.current = true;
|
||||||
triggerHaptic(10);
|
triggerHaptic(10);
|
||||||
try {
|
try {
|
||||||
const created = await api.createDocument({
|
const created = await api.createDocument({
|
||||||
title: 'Neue Notiz',
|
title: title.trim() || 'Neue Notiz',
|
||||||
icon: '📝',
|
icon: icon.trim() || '📝',
|
||||||
folderPath,
|
folderPath,
|
||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
@@ -602,6 +602,24 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
[onStatsChange]
|
[onStatsChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Phones: "+" asks for name and emoji first, so a stray tap does not leave an empty note behind.
|
||||||
|
// The desktop keeps creating at once.
|
||||||
|
const [newNoteSheet, setNewNoteSheet] = useState<{ folderPath: string } | null>(null);
|
||||||
|
const [newNoteTitle, setNewNoteTitle] = useState('');
|
||||||
|
const [newNoteIcon, setNewNoteIcon] = useState('📝');
|
||||||
|
const openNewNote = useCallback(
|
||||||
|
(folderPath = '') => {
|
||||||
|
if (window.innerWidth >= 640) {
|
||||||
|
void handleCreateDocument(folderPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setNewNoteTitle('');
|
||||||
|
setNewNoteIcon('📝');
|
||||||
|
setNewNoteSheet({ folderPath });
|
||||||
|
},
|
||||||
|
[handleCreateDocument]
|
||||||
|
);
|
||||||
|
|
||||||
const handleJumpToDocBlock = useCallback((docId: string, blockId?: string) => {
|
const handleJumpToDocBlock = useCallback((docId: string, blockId?: string) => {
|
||||||
setSelectedDocId(docId);
|
setSelectedDocId(docId);
|
||||||
setTargetBlockId(blockId || null);
|
setTargetBlockId(blockId || null);
|
||||||
@@ -1142,7 +1160,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
// Register mobile FAB actions and the menu at the top; on phones the menu also switches between
|
// Register mobile FAB actions and the menu at the top; on phones the menu also switches between
|
||||||
// notes, to-dos and hashtags (like the lists in Tasks and Shopping lists), the tab row is hidden there
|
// notes, to-dos and hashtags (like the lists in Tasks and Shopping lists), the tab row is hidden there
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPrimaryAction(() => handleCreateDocument(''));
|
setPrimaryAction(() => openNewNote(''));
|
||||||
setGroups([
|
setGroups([
|
||||||
{
|
{
|
||||||
id: 'workspace-views',
|
id: 'workspace-views',
|
||||||
@@ -1191,7 +1209,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
label: 'Neue Notiz',
|
label: 'Neue Notiz',
|
||||||
icon: <FilePlus className="w-4 h-4" />,
|
icon: <FilePlus className="w-4 h-4" />,
|
||||||
variant: 'accent',
|
variant: 'accent',
|
||||||
onClick: () => handleCreateDocument(''),
|
onClick: () => openNewNote(''),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'new-folder',
|
id: 'new-folder',
|
||||||
@@ -1213,7 +1231,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
setGroups([]);
|
setGroups([]);
|
||||||
setPrimaryAction(undefined);
|
setPrimaryAction(undefined);
|
||||||
};
|
};
|
||||||
}, [handleCreateDocument, setGroups, setPrimaryAction, trash.length, openTrash, subView, totalOpenTodos, tagsSummary.length]);
|
}, [handleCreateDocument, openNewNote, setGroups, setPrimaryAction, trash.length, openTrash, subView, totalOpenTodos, tagsSummary.length]);
|
||||||
|
|
||||||
// Real-time matched blocks and all nested children for the selected hashtag
|
// Real-time matched blocks and all nested children for the selected hashtag
|
||||||
const activeTagGroups = useMemo(() => {
|
const activeTagGroups = useMemo(() => {
|
||||||
@@ -2735,7 +2753,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleCreateDocument('')}
|
onClick={() => openNewNote('')}
|
||||||
className="btn-primary inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold"
|
className="btn-primary inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold"
|
||||||
>
|
>
|
||||||
<Plus className="w-3.5 h-3.5" />
|
<Plus className="w-3.5 h-3.5" />
|
||||||
@@ -2766,6 +2784,40 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
</FormSheet>
|
</FormSheet>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Phones: name and emoji of a new note before it is created */}
|
||||||
|
{newNoteSheet && (
|
||||||
|
<FormSheet
|
||||||
|
title="Neue Notiz"
|
||||||
|
icon={<FilePlus className="w-4 h-4 text-accent" />}
|
||||||
|
onClose={() => setNewNoteSheet(null)}
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const { folderPath } = newNoteSheet;
|
||||||
|
setNewNoteSheet(null);
|
||||||
|
void handleCreateDocument(folderPath, newNoteTitle, newNoteIcon);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newNoteIcon}
|
||||||
|
onChange={(e) => setNewNoteIcon(e.target.value)}
|
||||||
|
aria-label="Emoji"
|
||||||
|
title="Icon (Emoji)"
|
||||||
|
className="w-12 sm:w-10 h-11 sm:h-8 text-center input-field text-base sm:text-sm"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
value={newNoteTitle}
|
||||||
|
onChange={(e) => setNewNoteTitle(e.target.value)}
|
||||||
|
placeholder="Name der Notiz"
|
||||||
|
className="input-field flex-1 min-w-0 h-11 sm:h-8 px-2.5 text-base sm:text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormSheet>
|
||||||
|
)}
|
||||||
|
|
||||||
{mergeDialog && (
|
{mergeDialog && (
|
||||||
<MergeDialog
|
<MergeDialog
|
||||||
docs={docs}
|
docs={docs}
|
||||||
|
|||||||
Reference in new issue
Block a user