Merge dev: UI improvements and offline fonts (v0.1.4)
This commit is contained in:
10
index.html
10
index.html
@@ -4,13 +4,9 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Tauri + React + Typescript</title>
|
<title>Nextcloud Notes</title>
|
||||||
<!-- Editor fonts (monospace) -->
|
<!-- Local fonts for offline support -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="stylesheet" href="/fonts/fonts.css">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@200..900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet">
|
|
||||||
<!-- Preview fonts (serif) -->
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Average&family=Crimson+Pro:ital,wght@0,200..900;1,200..900&family=Merriweather:ital,opsz,wght@0,18..144,300..900;1,18..144,300..900&family=Roboto+Serif:ital,opsz,wght@0,8..144,100..900;1,8..144,100..900&display=swap" rel="stylesheet">
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "nextcloud-notes-tauri",
|
"name": "nextcloud-notes-tauri",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
BIN
public/fonts/Average-Regular.ttf
Normal file
BIN
public/fonts/Average-Regular.ttf
Normal file
Binary file not shown.
BIN
public/fonts/CrimsonPro-Italic-VariableFont_wght.ttf
Normal file
BIN
public/fonts/CrimsonPro-Italic-VariableFont_wght.ttf
Normal file
Binary file not shown.
BIN
public/fonts/CrimsonPro-VariableFont_wght.ttf
Normal file
BIN
public/fonts/CrimsonPro-VariableFont_wght.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Inconsolata-VariableFont_wdth,wght.ttf
Normal file
BIN
public/fonts/Inconsolata-VariableFont_wdth,wght.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Merriweather-Italic-VariableFont_opsz,wdth,wght.ttf
Normal file
BIN
public/fonts/Merriweather-Italic-VariableFont_opsz,wdth,wght.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Merriweather-VariableFont_opsz,wdth,wght.ttf
Normal file
BIN
public/fonts/Merriweather-VariableFont_opsz,wdth,wght.ttf
Normal file
Binary file not shown.
BIN
public/fonts/RobotoMono-VariableFont_wght.ttf
Normal file
BIN
public/fonts/RobotoMono-VariableFont_wght.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/fonts/RobotoSerif-VariableFont_GRAD,opsz,wdth,wght.ttf
Normal file
BIN
public/fonts/RobotoSerif-VariableFont_GRAD,opsz,wdth,wght.ttf
Normal file
Binary file not shown.
BIN
public/fonts/SourceCodePro-VariableFont_wght.ttf
Normal file
BIN
public/fonts/SourceCodePro-VariableFont_wght.ttf
Normal file
Binary file not shown.
90
public/fonts/fonts.css
Normal file
90
public/fonts/fonts.css
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/* Editor Fonts (Monospace) */
|
||||||
|
|
||||||
|
/* Source Code Pro */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Code Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 200 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./SourceCodePro-VariableFont_wght.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Roboto Mono */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./RobotoMono-VariableFont_wght.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inconsolata */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inconsolata';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 200 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./Inconsolata-VariableFont_wdth,wght.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preview Fonts (Serif) */
|
||||||
|
|
||||||
|
/* Merriweather */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Merriweather';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./Merriweather-VariableFont_opsz,wdth,wght.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Merriweather';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./Merriweather-Italic-VariableFont_opsz,wdth,wght.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Crimson Pro */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Crimson Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 200 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./CrimsonPro-VariableFont_wght.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Crimson Pro';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 200 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./CrimsonPro-Italic-VariableFont_wght.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Roboto Serif */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto Serif';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./RobotoSerif-VariableFont_GRAD,opsz,wdth,wght.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto Serif';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./RobotoSerif-Italic-VariableFont_GRAD,opsz,wdth,wght.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Average */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Average';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./Average-Regular.ttf') format('truetype');
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Nextcloud Notes",
|
"productName": "Nextcloud Notes",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"identifier": "com.davidrelich.nextcloud-notes",
|
"identifier": "com.davidrelich.nextcloud-notes",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
@@ -64,8 +64,16 @@ export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, i
|
|||||||
// Use setTimeout to ensure DOM has updated
|
// Use setTimeout to ensure DOM has updated
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (textareaRef.current) {
|
if (textareaRef.current) {
|
||||||
|
// Save cursor position and scroll position
|
||||||
|
const cursorPosition = textareaRef.current.selectionStart;
|
||||||
|
const scrollTop = textareaRef.current.scrollTop;
|
||||||
|
|
||||||
textareaRef.current.style.height = 'auto';
|
textareaRef.current.style.height = 'auto';
|
||||||
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px';
|
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px';
|
||||||
|
|
||||||
|
// Restore cursor position and scroll position
|
||||||
|
textareaRef.current.setSelectionRange(cursorPosition, cursorPosition);
|
||||||
|
textareaRef.current.scrollTop = scrollTop;
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
@@ -608,42 +616,6 @@ export function NoteEditor({ note, onUpdateNote, onUnsavedChanges, categories, i
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Attachment Upload */}
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
onChange={handleAttachmentUpload}
|
|
||||||
className="hidden"
|
|
||||||
accept="image/*,.pdf,.doc,.docx,.txt,.md"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
disabled={isUploading || isPreviewMode}
|
|
||||||
className={`px-3 py-1.5 rounded-full transition-colors flex items-center gap-1.5 text-sm ${
|
|
||||||
isUploading || isPreviewMode
|
|
||||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
|
|
||||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
|
||||||
}`}
|
|
||||||
title={isPreviewMode ? "Switch to Edit mode to upload" : "Upload Image/Attachment"}
|
|
||||||
>
|
|
||||||
{isUploading ? (
|
|
||||||
<>
|
|
||||||
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
<span>Uploading...</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
<span>Attach</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Preview Toggle */}
|
{/* Preview Toggle */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsPreviewMode(!isPreviewMode)}
|
onClick={() => setIsPreviewMode(!isPreviewMode)}
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ export function NotesList({
|
|||||||
}: NotesListProps) {
|
}: NotesListProps) {
|
||||||
const [isSyncing, setIsSyncing] = React.useState(false);
|
const [isSyncing, setIsSyncing] = React.useState(false);
|
||||||
const [deleteClickedId, setDeleteClickedId] = React.useState<number | null>(null);
|
const [deleteClickedId, setDeleteClickedId] = React.useState<number | null>(null);
|
||||||
const [width, setWidth] = React.useState(320);
|
const [width, setWidth] = React.useState(() => {
|
||||||
|
const saved = localStorage.getItem('notesListWidth');
|
||||||
|
return saved ? parseInt(saved, 10) : 320;
|
||||||
|
});
|
||||||
const [isResizing, setIsResizing] = React.useState(false);
|
const [isResizing, setIsResizing] = React.useState(false);
|
||||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -53,6 +56,7 @@ export function NotesList({
|
|||||||
const newWidth = e.clientX - (containerRef.current?.getBoundingClientRect().left || 0);
|
const newWidth = e.clientX - (containerRef.current?.getBoundingClientRect().left || 0);
|
||||||
if (newWidth >= 240 && newWidth <= 600) {
|
if (newWidth >= 240 && newWidth <= 600) {
|
||||||
setWidth(newWidth);
|
setWidth(newWidth);
|
||||||
|
localStorage.setItem('notesListWidth', newWidth.toString());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -116,10 +120,35 @@ export function NotesList({
|
|||||||
return cleanedPreview;
|
return cleanedPreview;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCategoryColor = (category: string) => {
|
||||||
|
// Generate consistent pastel color based on category name
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < category.length; i++) {
|
||||||
|
hash = category.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pastel color palette (light, subtle tones)
|
||||||
|
const colors = [
|
||||||
|
{ bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-700 dark:text-blue-300' },
|
||||||
|
{ bg: 'bg-green-100 dark:bg-green-900/30', text: 'text-green-700 dark:text-green-300' },
|
||||||
|
{ bg: 'bg-purple-100 dark:bg-purple-900/30', text: 'text-purple-700 dark:text-purple-300' },
|
||||||
|
{ bg: 'bg-pink-100 dark:bg-pink-900/30', text: 'text-pink-700 dark:text-pink-300' },
|
||||||
|
{ bg: 'bg-yellow-100 dark:bg-yellow-900/30', text: 'text-yellow-700 dark:text-yellow-300' },
|
||||||
|
{ bg: 'bg-indigo-100 dark:bg-indigo-900/30', text: 'text-indigo-700 dark:text-indigo-300' },
|
||||||
|
{ bg: 'bg-red-100 dark:bg-red-900/30', text: 'text-red-700 dark:text-red-300' },
|
||||||
|
{ bg: 'bg-teal-100 dark:bg-teal-900/30', text: 'text-teal-700 dark:text-teal-300' },
|
||||||
|
{ bg: 'bg-orange-100 dark:bg-orange-900/30', text: 'text-orange-700 dark:text-orange-300' },
|
||||||
|
{ bg: 'bg-cyan-100 dark:bg-cyan-900/30', text: 'text-cyan-700 dark:text-cyan-300' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const index = Math.abs(hash) % colors.length;
|
||||||
|
return colors[index];
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="bg-gray-50 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700 flex flex-col relative"
|
className="bg-gray-50 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700 flex flex-col relative flex-shrink-0"
|
||||||
style={{ width: `${width}px`, minWidth: '240px', maxWidth: '600px' }}
|
style={{ width: `${width}px`, minWidth: '240px', maxWidth: '600px' }}
|
||||||
>
|
>
|
||||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
@@ -251,8 +280,16 @@ export function NotesList({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center text-xs text-gray-500 dark:text-gray-400 mb-2">
|
<div className="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 mb-2">
|
||||||
<span>{formatDate(note.modified)}</span>
|
<span>{formatDate(note.modified)}</span>
|
||||||
|
{note.category && (() => {
|
||||||
|
const colors = getCategoryColor(note.category);
|
||||||
|
return (
|
||||||
|
<span className={`px-2 py-0.5 ${colors.bg} ${colors.text} rounded-full text-xs font-medium`}>
|
||||||
|
{note.category}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{getPreview(note.content) && (
|
{getPreview(note.content) && (
|
||||||
|
|||||||
Reference in New Issue
Block a user