diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 58439813..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1 +0,0 @@ -name: CI diff --git a/.well-known/funding-manifest-urls b/.well-known/funding-manifest-urls new file mode 100644 index 00000000..23781628 --- /dev/null +++ b/.well-known/funding-manifest-urls @@ -0,0 +1 @@ +https://mydevtools.tech/funding.json diff --git a/apps/desktop-ui/messages/af.json b/apps/desktop-ui/messages/af.json index 1223018e..22c4a1ef 100644 --- a/apps/desktop-ui/messages/af.json +++ b/apps/desktop-ui/messages/af.json @@ -195,6 +195,8 @@ "title": "Voorkoms", "description": "Pas aan hoe die toepassing op jou toestel lyk.", "themePreference": "Temavoorkeur", + "accentColor": "Aksentkleur", + "customColor": "Pasgemaakte kleur", "themes": { "light": "Lig", "dark": "Donker", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Oor", + "running": "Tans op MyDevTools", + "runningVersion": "Tans op MyDevTools v{version}", + "checkButton": "Kyk vir opdaterings", + "checking": "Kyk…", + "dialogTitle": "Opdaterings", + "dialogDescription": "MyDevTools dateer homself ter plaatse op — geen herinstallasie nie, en jou aflyn data word nooit geraak nie.", + "onVersion": "Jy is op weergawe {version}.", + "available": "Weergawe {version} is beskikbaar.", + "install": "Laai af en installeer", + "upToDate": "Jy is op datum.", + "checkError": "Kon nie vir opdaterings kyk nie. Is jy aanlyn?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/ar.json b/apps/desktop-ui/messages/ar.json index 38d21da9..05b9549e 100644 --- a/apps/desktop-ui/messages/ar.json +++ b/apps/desktop-ui/messages/ar.json @@ -195,6 +195,8 @@ "title": "المظهر", "description": "خصّص مظهر التطبيق على جهازك.", "themePreference": "تفضيل السمة", + "accentColor": "لون التمييز", + "customColor": "لون مخصص", "themes": { "light": "فاتح", "dark": "داكن", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "حول", + "running": "تشغيل MyDevTools", + "runningVersion": "تشغيل MyDevTools الإصدار {version}", + "checkButton": "التحقق من التحديثات", + "checking": "جارٍ التحقق…", + "dialogTitle": "التحديثات", + "dialogDescription": "يقوم MyDevTools بتحديث نفسه في مكانه — بدون إعادة تثبيت، ولا يتم المساس ببياناتك دون اتصال أبدًا.", + "onVersion": "أنت تستخدم الإصدار {version}.", + "available": "الإصدار {version} متوفر.", + "install": "التنزيل والتثبيت", + "upToDate": "أنت على أحدث إصدار.", + "checkError": "تعذّر التحقق من التحديثات. هل أنت متصل بالإنترنت؟" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/ca.json b/apps/desktop-ui/messages/ca.json index 44bedc74..e3931b1d 100644 --- a/apps/desktop-ui/messages/ca.json +++ b/apps/desktop-ui/messages/ca.json @@ -195,6 +195,8 @@ "title": "Aparença", "description": "Personalitza com es veu l'aplicació al teu dispositiu.", "themePreference": "Preferència de tema", + "accentColor": "Color d'accent", + "customColor": "Color personalitzat", "themes": { "light": "Clar", "dark": "Fosc", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Quant a", + "running": "S'està executant MyDevTools", + "runningVersion": "S'està executant MyDevTools v{version}", + "checkButton": "Comprova si hi ha actualitzacions", + "checking": "S'està comprovant…", + "dialogTitle": "Actualitzacions", + "dialogDescription": "MyDevTools s’actualitza sobre si mateix — sense reinstal·lar res, i les teves dades fora de línia no es toquen mai.", + "onVersion": "Tens la versió {version}.", + "available": "La versió {version} està disponible.", + "install": "Baixa i instal·la", + "upToDate": "Estàs al dia.", + "checkError": "No s'han pogut comprovar les actualitzacions. Estàs connectat?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/cs.json b/apps/desktop-ui/messages/cs.json index 28dc5e22..56840805 100644 --- a/apps/desktop-ui/messages/cs.json +++ b/apps/desktop-ui/messages/cs.json @@ -195,6 +195,8 @@ "title": "Vzhled", "description": "Přizpůsobte vzhled aplikace na vašem zařízení.", "themePreference": "Motiv", + "accentColor": "Barva zvýraznění", + "customColor": "Vlastní barva", "themes": { "light": "Světlý", "dark": "Tmavý", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "O aplikaci", + "running": "Běží MyDevTools", + "runningVersion": "Běží MyDevTools v{version}", + "checkButton": "Zkontrolovat aktualizace", + "checking": "Kontrola…", + "dialogTitle": "Aktualizace", + "dialogDescription": "MyDevTools se aktualizuje na místě — žádná reinstalace a vašich offline dat se to nikdy nedotkne.", + "onVersion": "Máte verzi {version}.", + "available": "Verze {version} je dostupná.", + "install": "Stáhnout a nainstalovat", + "upToDate": "Máte nejnovější verzi.", + "checkError": "Aktualizace se nepodařilo zkontrolovat. Jste online?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/da.json b/apps/desktop-ui/messages/da.json index 5e24bb60..3e896a30 100644 --- a/apps/desktop-ui/messages/da.json +++ b/apps/desktop-ui/messages/da.json @@ -195,6 +195,8 @@ "title": "Udseende", "description": "Tilpas, hvordan applikationen ser ud på din enhed.", "themePreference": "Temapræference", + "accentColor": "Accentfarve", + "customColor": "Tilpasset farve", "themes": { "light": "Lys", "dark": "Mørk", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Om", + "running": "Kører MyDevTools", + "runningVersion": "Kører MyDevTools v{version}", + "checkButton": "Søg efter opdateringer", + "checking": "Søger…", + "dialogTitle": "Opdateringer", + "dialogDescription": "MyDevTools opdaterer sig selv på stedet — ingen geninstallation, og dine offlinedata bliver aldrig berørt.", + "onVersion": "Du bruger version {version}.", + "available": "Version {version} er tilgængelig.", + "install": "Download og installer", + "upToDate": "Du er opdateret.", + "checkError": "Kunne ikke søge efter opdateringer. Er du online?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/de.json b/apps/desktop-ui/messages/de.json index b36a0801..03d4599c 100644 --- a/apps/desktop-ui/messages/de.json +++ b/apps/desktop-ui/messages/de.json @@ -195,6 +195,8 @@ "title": "Erscheinungsbild", "description": "Passen Sie das Aussehen der Anwendung auf Ihrem Gerät an.", "themePreference": "Theme", + "accentColor": "Akzentfarbe", + "customColor": "Eigene Farbe", "themes": { "light": "Hell", "dark": "Dunkel", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Info", + "running": "MyDevTools läuft", + "runningVersion": "MyDevTools v{version} läuft", + "checkButton": "Nach Updates suchen", + "checking": "Suche…", + "dialogTitle": "Updates", + "dialogDescription": "MyDevTools aktualisiert sich direkt an Ort und Stelle — keine Neuinstallation, und deine Offline-Daten werden nie angetastet.", + "onVersion": "Du nutzt Version {version}.", + "available": "Version {version} ist verfügbar.", + "install": "Herunterladen und installieren", + "upToDate": "Du bist auf dem neuesten Stand.", + "checkError": "Suche nach Updates fehlgeschlagen. Bist du online?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/el.json b/apps/desktop-ui/messages/el.json index dfbb9a66..5929babf 100644 --- a/apps/desktop-ui/messages/el.json +++ b/apps/desktop-ui/messages/el.json @@ -195,6 +195,8 @@ "title": "Εμφάνιση", "description": "Προσαρμόστε τον τρόπο εμφάνισης της εφαρμογής στη συσκευή σας.", "themePreference": "Προτίμηση θέματος", + "accentColor": "Χρώμα τόνου", + "customColor": "Προσαρμοσμένο χρώμα", "themes": { "light": "Φως", "dark": "Σκοτάδι", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Σχετικά", + "running": "Εκτελείται το MyDevTools", + "runningVersion": "Εκτελείται το MyDevTools v{version}", + "checkButton": "Έλεγχος για ενημερώσεις", + "checking": "Έλεγχος…", + "dialogTitle": "Ενημερώσεις", + "dialogDescription": "Το MyDevTools ενημερώνεται επί τόπου — χωρίς επανεγκατάσταση, και τα offline δεδομένα σας δεν αγγίζονται ποτέ.", + "onVersion": "Χρησιμοποιείτε την έκδοση {version}.", + "available": "Η έκδοση {version} είναι διαθέσιμη.", + "install": "Λήψη και εγκατάσταση", + "upToDate": "Είστε ενημερωμένοι.", + "checkError": "Δεν ήταν δυνατός ο έλεγχος για ενημερώσεις. Είστε συνδεδεμένοι;" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/en.json b/apps/desktop-ui/messages/en.json index ee10a0be..9e1e77c2 100644 --- a/apps/desktop-ui/messages/en.json +++ b/apps/desktop-ui/messages/en.json @@ -195,6 +195,8 @@ "title": "Appearance", "description": "Customize how the application looks on your device.", "themePreference": "Theme Preference", + "accentColor": "Accent color", + "customColor": "Custom color", "themes": { "light": "Light", "dark": "Dark", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "About", + "running": "Running MyDevTools", + "runningVersion": "Running MyDevTools v{version}", + "checkButton": "Check for updates", + "checking": "Checking…", + "dialogTitle": "Updates", + "dialogDescription": "MyDevTools updates itself in place — no reinstall, and your offline data is never touched.", + "onVersion": "You're on version {version}.", + "available": "Version {version} is available.", + "install": "Download & install", + "upToDate": "You're up to date.", + "checkError": "Couldn't check for updates. Are you online?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/es.json b/apps/desktop-ui/messages/es.json index 247e3ca5..d0a87de8 100644 --- a/apps/desktop-ui/messages/es.json +++ b/apps/desktop-ui/messages/es.json @@ -195,6 +195,8 @@ "title": "Apariencia", "description": "Personaliza cómo se ve la aplicación en tu dispositivo.", "themePreference": "Preferencia de tema", + "accentColor": "Color de acento", + "customColor": "Color personalizado", "themes": { "light": "Claro", "dark": "Oscuro", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Acerca de", + "running": "Ejecutando MyDevTools", + "runningVersion": "Ejecutando MyDevTools v{version}", + "checkButton": "Buscar actualizaciones", + "checking": "Buscando…", + "dialogTitle": "Actualizaciones", + "dialogDescription": "MyDevTools se actualiza en el sitio: sin reinstalar nada y sin tocar nunca tus datos sin conexión.", + "onVersion": "Tienes la versión {version}.", + "available": "La versión {version} está disponible.", + "install": "Descargar e instalar", + "upToDate": "Estás al día.", + "checkError": "No se pudieron buscar actualizaciones. ¿Estás conectado?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/fa.json b/apps/desktop-ui/messages/fa.json index fd960552..7ffb07b0 100644 --- a/apps/desktop-ui/messages/fa.json +++ b/apps/desktop-ui/messages/fa.json @@ -195,6 +195,8 @@ "title": "ظاهر", "description": "نحوه ظاهر برنامه را در دستگاه خود سفارشی کنید.", "themePreference": "اولویت تم", + "accentColor": "رنگ تأکید", + "customColor": "رنگ سفارشی", "themes": { "light": "نور", "dark": "تاریک", @@ -257,6 +259,20 @@ "uk": "Українська", "fa": "فارسی" } + }, + "about": { + "title": "درباره", + "running": "در حال اجرای MyDevTools", + "runningVersion": "در حال اجرای MyDevTools نسخه {version}", + "checkButton": "بررسی به‌روزرسانی‌ها", + "checking": "در حال بررسی…", + "dialogTitle": "به‌روزرسانی‌ها", + "dialogDescription": "MyDevTools خودش را در همان محل به‌روزرسانی می‌کند — بدون نصب مجدد و داده‌های آفلاین شما هرگز دست‌کاری نمی‌شود.", + "onVersion": "شما از نسخه {version} استفاده می‌کنید.", + "available": "نسخه {version} در دسترس است.", + "install": "دانلود و نصب", + "upToDate": "شما به‌روز هستید.", + "checkError": "بررسی به‌روزرسانی‌ها ممکن نشد. آیا به اینترنت متصل هستید؟" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/fr.json b/apps/desktop-ui/messages/fr.json index dd2b7081..6f5d7587 100644 --- a/apps/desktop-ui/messages/fr.json +++ b/apps/desktop-ui/messages/fr.json @@ -195,6 +195,8 @@ "title": "Apparence", "description": "Personnalisez l’apparence de l’application sur votre appareil.", "themePreference": "Thème", + "accentColor": "Couleur d'accentuation", + "customColor": "Couleur personnalisée", "themes": { "light": "Clair", "dark": "Sombre", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "À propos", + "running": "MyDevTools en cours d'exécution", + "runningVersion": "MyDevTools v{version} en cours d'exécution", + "checkButton": "Rechercher des mises à jour", + "checking": "Recherche…", + "dialogTitle": "Mises à jour", + "dialogDescription": "MyDevTools se met à jour sur place — aucune réinstallation, et vos données hors ligne ne sont jamais touchées.", + "onVersion": "Vous utilisez la version {version}.", + "available": "La version {version} est disponible.", + "install": "Télécharger et installer", + "upToDate": "Vous êtes à jour.", + "checkError": "Impossible de rechercher des mises à jour. Êtes-vous en ligne ?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/id.json b/apps/desktop-ui/messages/id.json index f04b3d09..983241a5 100644 --- a/apps/desktop-ui/messages/id.json +++ b/apps/desktop-ui/messages/id.json @@ -195,6 +195,8 @@ "title": "Penampilan", "description": "Sesuaikan tampilan aplikasi di perangkat Anda.", "themePreference": "Preferensi Tema", + "accentColor": "Warna aksen", + "customColor": "Warna khusus", "themes": { "light": "Cahaya", "dark": "Gelap", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Tentang", + "running": "Menjalankan MyDevTools", + "runningVersion": "Menjalankan MyDevTools v{version}", + "checkButton": "Periksa pembaruan", + "checking": "Memeriksa…", + "dialogTitle": "Pembaruan", + "dialogDescription": "MyDevTools memperbarui dirinya di tempat — tanpa instal ulang, dan data offline Anda tidak pernah tersentuh.", + "onVersion": "Anda menggunakan versi {version}.", + "available": "Versi {version} tersedia.", + "install": "Unduh dan pasang", + "upToDate": "Anda sudah menggunakan versi terbaru.", + "checkError": "Tidak dapat memeriksa pembaruan. Apakah Anda online?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/it.json b/apps/desktop-ui/messages/it.json index a5d4201b..0ada945d 100644 --- a/apps/desktop-ui/messages/it.json +++ b/apps/desktop-ui/messages/it.json @@ -195,6 +195,8 @@ "title": "Aspetto", "description": "Personalizza l'aspetto dell'applicazione sul tuo dispositivo.", "themePreference": "Preferenza tema", + "accentColor": "Colore d'accento", + "customColor": "Colore personalizzato", "themes": { "light": "Leggero", "dark": "Buio", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Informazioni", + "running": "MyDevTools in esecuzione", + "runningVersion": "MyDevTools v{version} in esecuzione", + "checkButton": "Controlla aggiornamenti", + "checking": "Controllo…", + "dialogTitle": "Aggiornamenti", + "dialogDescription": "MyDevTools si aggiorna sul posto — nessuna reinstallazione e i tuoi dati offline non vengono mai toccati.", + "onVersion": "Stai usando la versione {version}.", + "available": "La versione {version} è disponibile.", + "install": "Scarica e installa", + "upToDate": "Sei aggiornato.", + "checkError": "Impossibile controllare gli aggiornamenti. Sei online?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/ja.json b/apps/desktop-ui/messages/ja.json index 049c0a75..beaa918c 100644 --- a/apps/desktop-ui/messages/ja.json +++ b/apps/desktop-ui/messages/ja.json @@ -195,6 +195,8 @@ "title": "外観", "description": "デバイスでのアプリケーションの表示をカスタマイズします。", "themePreference": "テーマ設定", + "accentColor": "アクセントカラー", + "customColor": "カスタムカラー", "themes": { "light": "ライト", "dark": "ダーク", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "このアプリについて", + "running": "MyDevTools を実行中", + "runningVersion": "MyDevTools v{version} を実行中", + "checkButton": "更新を確認", + "checking": "確認中…", + "dialogTitle": "更新", + "dialogDescription": "MyDevTools はその場で自動更新されます。再インストールは不要で、オフラインデータには一切触れません。", + "onVersion": "現在のバージョンは {version} です。", + "available": "バージョン {version} が利用可能です。", + "install": "ダウンロードしてインストール", + "upToDate": "最新の状態です。", + "checkError": "更新を確認できませんでした。オンラインですか?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/ko.json b/apps/desktop-ui/messages/ko.json index 57c45243..e8d84aa8 100644 --- a/apps/desktop-ui/messages/ko.json +++ b/apps/desktop-ui/messages/ko.json @@ -195,6 +195,8 @@ "title": "외관", "description": "기기에서 애플리케이션의 모양을 사용자 지정하세요.", "themePreference": "테마 설정", + "accentColor": "강조 색상", + "customColor": "사용자 지정 색상", "themes": { "light": "라이트", "dark": "다크", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "정보", + "running": "MyDevTools 실행 중", + "runningVersion": "MyDevTools v{version} 실행 중", + "checkButton": "업데이트 확인", + "checking": "확인 중…", + "dialogTitle": "업데이트", + "dialogDescription": "MyDevTools는 그 자리에서 스스로 업데이트됩니다. 재설치가 필요 없고 오프라인 데이터는 절대 건드리지 않습니다.", + "onVersion": "현재 버전은 {version}입니다.", + "available": "버전 {version}을(를) 사용할 수 있습니다.", + "install": "다운로드 및 설치", + "upToDate": "최신 버전입니다.", + "checkError": "업데이트를 확인할 수 없습니다. 인터넷에 연결되어 있나요?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/ms.json b/apps/desktop-ui/messages/ms.json index 098a4975..e0078eab 100644 --- a/apps/desktop-ui/messages/ms.json +++ b/apps/desktop-ui/messages/ms.json @@ -195,6 +195,8 @@ "title": "Penampilan", "description": "Sesuaikan rupa aplikasi pada peranti anda.", "themePreference": "Keutamaan Tema", + "accentColor": "Warna aksen", + "customColor": "Warna tersuai", "themes": { "light": "Cerah", "dark": "Gelap", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Perihal", + "running": "Menjalankan MyDevTools", + "runningVersion": "Menjalankan MyDevTools v{version}", + "checkButton": "Periksa kemas kini", + "checking": "Memeriksa…", + "dialogTitle": "Kemas kini", + "dialogDescription": "MyDevTools mengemas kini dirinya di tempat — tanpa pemasangan semula, dan data luar talian anda tidak pernah disentuh.", + "onVersion": "Anda menggunakan versi {version}.", + "available": "Versi {version} tersedia.", + "install": "Muat turun dan pasang", + "upToDate": "Anda sudah terkini.", + "checkError": "Tidak dapat memeriksa kemas kini. Adakah anda dalam talian?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/nb.json b/apps/desktop-ui/messages/nb.json index ea489d08..7bcd27ff 100644 --- a/apps/desktop-ui/messages/nb.json +++ b/apps/desktop-ui/messages/nb.json @@ -195,6 +195,8 @@ "title": "Utseende", "description": "Tilpass hvordan applikasjonen ser ut på enheten din.", "themePreference": "Temapreferanse", + "accentColor": "Aksentfarge", + "customColor": "Egendefinert farge", "themes": { "light": "Lys", "dark": "Mørk", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Om", + "running": "Kjører MyDevTools", + "runningVersion": "Kjører MyDevTools v{version}", + "checkButton": "Se etter oppdateringer", + "checking": "Søker…", + "dialogTitle": "Oppdateringer", + "dialogDescription": "MyDevTools oppdaterer seg selv på stedet — ingen reinstallering, og offline-dataene dine blir aldri berørt.", + "onVersion": "Du bruker versjon {version}.", + "available": "Versjon {version} er tilgjengelig.", + "install": "Last ned og installer", + "upToDate": "Du er oppdatert.", + "checkError": "Kunne ikke se etter oppdateringer. Er du på nett?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/nl.json b/apps/desktop-ui/messages/nl.json index 4c09bf9c..c7fcbfc5 100644 --- a/apps/desktop-ui/messages/nl.json +++ b/apps/desktop-ui/messages/nl.json @@ -195,6 +195,8 @@ "title": "Weergave", "description": "Pas het uiterlijk van de applicatie aan op je apparaat.", "themePreference": "Themavoorkeur", + "accentColor": "Accentkleur", + "customColor": "Aangepaste kleur", "themes": { "light": "Licht", "dark": "Donker", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Over", + "running": "MyDevTools wordt uitgevoerd", + "runningVersion": "MyDevTools v{version} wordt uitgevoerd", + "checkButton": "Controleren op updates", + "checking": "Controleren…", + "dialogTitle": "Updates", + "dialogDescription": "MyDevTools werkt zichzelf ter plekke bij — geen herinstallatie, en je offlinegegevens worden nooit aangeraakt.", + "onVersion": "Je gebruikt versie {version}.", + "available": "Versie {version} is beschikbaar.", + "install": "Downloaden en installeren", + "upToDate": "Je bent up-to-date.", + "checkError": "Kon niet op updates controleren. Ben je online?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/pl.json b/apps/desktop-ui/messages/pl.json index a2e52692..db4d07e2 100644 --- a/apps/desktop-ui/messages/pl.json +++ b/apps/desktop-ui/messages/pl.json @@ -195,6 +195,8 @@ "title": "Appearance", "description": "Customize how the application looks on your device.", "themePreference": "Theme Preference", + "accentColor": "Kolor akcentu", + "customColor": "Kolor własny", "themes": { "light": "Light", "dark": "Dark", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "O aplikacji", + "running": "Działa MyDevTools", + "runningVersion": "Działa MyDevTools v{version}", + "checkButton": "Sprawdź aktualizacje", + "checking": "Sprawdzanie…", + "dialogTitle": "Aktualizacje", + "dialogDescription": "MyDevTools aktualizuje się na miejscu — bez ponownej instalacji, a Twoje dane offline nigdy nie są ruszane.", + "onVersion": "Używasz wersji {version}.", + "available": "Wersja {version} jest dostępna.", + "install": "Pobierz i zainstaluj", + "upToDate": "Masz najnowszą wersję.", + "checkError": "Nie udało się sprawdzić aktualizacji. Czy jesteś online?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/pt-BR.json b/apps/desktop-ui/messages/pt-BR.json index b683c90e..4782bbd3 100644 --- a/apps/desktop-ui/messages/pt-BR.json +++ b/apps/desktop-ui/messages/pt-BR.json @@ -195,6 +195,8 @@ "title": "Aparência", "description": "Personalize a aparência do aplicativo no seu dispositivo.", "themePreference": "Preferência de Tema", + "accentColor": "Cor de destaque", + "customColor": "Cor personalizada", "themes": { "light": "Claro", "dark": "Escuro", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Sobre", + "running": "Executando o MyDevTools", + "runningVersion": "Executando o MyDevTools v{version}", + "checkButton": "Verificar atualizações", + "checking": "Verificando…", + "dialogTitle": "Atualizações", + "dialogDescription": "O MyDevTools se atualiza no lugar — sem reinstalar nada, e seus dados offline nunca são tocados.", + "onVersion": "Você está na versão {version}.", + "available": "A versão {version} está disponível.", + "install": "Baixar e instalar", + "upToDate": "Você está atualizado.", + "checkError": "Não foi possível verificar atualizações. Você está online?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/pt.json b/apps/desktop-ui/messages/pt.json index 0ebe342f..9ee5761b 100644 --- a/apps/desktop-ui/messages/pt.json +++ b/apps/desktop-ui/messages/pt.json @@ -195,6 +195,8 @@ "title": "Aparência", "description": "Personaliza o aspeto da aplicação no teu dispositivo.", "themePreference": "Preferência de Tema", + "accentColor": "Cor de destaque", + "customColor": "Cor personalizada", "themes": { "light": "Claro", "dark": "Escuro", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Acerca de", + "running": "A executar o MyDevTools", + "runningVersion": "A executar o MyDevTools v{version}", + "checkButton": "Procurar atualizações", + "checking": "A procurar…", + "dialogTitle": "Atualizações", + "dialogDescription": "O MyDevTools atualiza-se no local — sem reinstalar nada, e os seus dados offline nunca são tocados.", + "onVersion": "Está na versão {version}.", + "available": "A versão {version} está disponível.", + "install": "Transferir e instalar", + "upToDate": "Está atualizado.", + "checkError": "Não foi possível procurar atualizações. Está online?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/ru.json b/apps/desktop-ui/messages/ru.json index 279dd21f..6291421d 100644 --- a/apps/desktop-ui/messages/ru.json +++ b/apps/desktop-ui/messages/ru.json @@ -195,6 +195,8 @@ "title": "Внешний вид", "description": "Настройте внешний вид приложения на вашем устройстве.", "themePreference": "Тема оформления", + "accentColor": "Акцентный цвет", + "customColor": "Свой цвет", "themes": { "light": "Светлая", "dark": "Тёмная", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "О приложении", + "running": "MyDevTools запущен", + "runningVersion": "MyDevTools v{version} запущен", + "checkButton": "Проверить обновления", + "checking": "Проверка…", + "dialogTitle": "Обновления", + "dialogDescription": "MyDevTools обновляется на месте — без переустановки, и ваши офлайн-данные никогда не затрагиваются.", + "onVersion": "У вас версия {version}.", + "available": "Доступна версия {version}.", + "install": "Скачать и установить", + "upToDate": "У вас последняя версия.", + "checkError": "Не удалось проверить обновления. Вы онлайн?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/sv.json b/apps/desktop-ui/messages/sv.json index f44fa205..f09d7dc8 100644 --- a/apps/desktop-ui/messages/sv.json +++ b/apps/desktop-ui/messages/sv.json @@ -195,6 +195,8 @@ "title": "Utseende", "description": "Anpassa hur applikationen ser ut på din enhet.", "themePreference": "Temainställning", + "accentColor": "Accentfärg", + "customColor": "Anpassad färg", "themes": { "light": "Ljust", "dark": "Mörkt", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Om", + "running": "Kör MyDevTools", + "runningVersion": "Kör MyDevTools v{version}", + "checkButton": "Sök efter uppdateringar", + "checking": "Söker…", + "dialogTitle": "Uppdateringar", + "dialogDescription": "MyDevTools uppdaterar sig själv på plats — ingen ominstallation, och dina offlinedata berörs aldrig.", + "onVersion": "Du använder version {version}.", + "available": "Version {version} är tillgänglig.", + "install": "Ladda ner och installera", + "upToDate": "Du är uppdaterad.", + "checkError": "Kunde inte söka efter uppdateringar. Är du online?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/tr.json b/apps/desktop-ui/messages/tr.json index 796be883..fa6423d4 100644 --- a/apps/desktop-ui/messages/tr.json +++ b/apps/desktop-ui/messages/tr.json @@ -195,6 +195,8 @@ "title": "Görünüm", "description": "Uygulamanın cihazınızda nasıl göründüğünü özelleştirin.", "themePreference": "Tema Tercihi", + "accentColor": "Vurgu rengi", + "customColor": "Özel renk", "themes": { "light": "Açık", "dark": "Koyu", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Hakkında", + "running": "MyDevTools çalışıyor", + "runningVersion": "MyDevTools v{version} çalışıyor", + "checkButton": "Güncellemeleri denetle", + "checking": "Denetleniyor…", + "dialogTitle": "Güncellemeler", + "dialogDescription": "MyDevTools kendini yerinde günceller — yeniden kurulum gerekmez ve çevrimdışı verilerinize asla dokunulmaz.", + "onVersion": "{version} sürümünü kullanıyorsunuz.", + "available": "{version} sürümü kullanılabilir.", + "install": "İndir ve kur", + "upToDate": "En son sürümü kullanıyorsunuz.", + "checkError": "Güncellemeler denetlenemedi. Çevrimiçi misiniz?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/uk.json b/apps/desktop-ui/messages/uk.json index ec3ee262..ad1dca1c 100644 --- a/apps/desktop-ui/messages/uk.json +++ b/apps/desktop-ui/messages/uk.json @@ -195,6 +195,8 @@ "title": "Зовнішній вигляд", "description": "Налаштуйте вигляд програми на вашому пристрої.", "themePreference": "Вибір теми", + "accentColor": "Акцентний колір", + "customColor": "Власний колір", "themes": { "light": "Світла", "dark": "Темна", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Про програму", + "running": "MyDevTools запущено", + "runningVersion": "MyDevTools v{version} запущено", + "checkButton": "Перевірити оновлення", + "checking": "Перевірка…", + "dialogTitle": "Оновлення", + "dialogDescription": "MyDevTools оновлюється на місці — без перевстановлення, і ваші офлайн-дані ніколи не змінюються.", + "onVersion": "У вас версія {version}.", + "available": "Доступна версія {version}.", + "install": "Завантажити й встановити", + "upToDate": "У вас найновіша версія.", + "checkError": "Не вдалося перевірити оновлення. Ви онлайн?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/vi.json b/apps/desktop-ui/messages/vi.json index e1f27695..8167888f 100644 --- a/apps/desktop-ui/messages/vi.json +++ b/apps/desktop-ui/messages/vi.json @@ -195,6 +195,8 @@ "title": "Giao diện", "description": "Tùy chỉnh giao diện ứng dụng trên thiết bị của bạn.", "themePreference": "Tùy chọn giao diện", + "accentColor": "Màu nhấn", + "customColor": "Màu tùy chỉnh", "themes": { "light": "Sáng", "dark": "Tối", @@ -257,6 +259,20 @@ "tr": "Türkçe", "uk": "Українська" } + }, + "about": { + "title": "Giới thiệu", + "running": "Đang chạy MyDevTools", + "runningVersion": "Đang chạy MyDevTools v{version}", + "checkButton": "Kiểm tra bản cập nhật", + "checking": "Đang kiểm tra…", + "dialogTitle": "Bản cập nhật", + "dialogDescription": "MyDevTools tự cập nhật ngay tại chỗ — không cần cài lại và dữ liệu ngoại tuyến của bạn không bao giờ bị ảnh hưởng.", + "onVersion": "Bạn đang dùng phiên bản {version}.", + "available": "Đã có phiên bản {version}.", + "install": "Tải xuống và cài đặt", + "upToDate": "Bạn đang dùng bản mới nhất.", + "checkError": "Không thể kiểm tra bản cập nhật. Bạn có đang trực tuyến không?" } }, "Dashboard": { diff --git a/apps/desktop-ui/messages/zh.json b/apps/desktop-ui/messages/zh.json index 3e9414ec..7f879ef1 100644 --- a/apps/desktop-ui/messages/zh.json +++ b/apps/desktop-ui/messages/zh.json @@ -195,6 +195,8 @@ "title": "外观", "description": "自定义应用在你设备上的显示方式。", "themePreference": "主题偏好", + "accentColor": "强调色", + "customColor": "自定义颜色", "themes": { "light": "浅色", "dark": "深色", @@ -257,6 +259,20 @@ "sv": "Svenska", "tr": "Türkçe" } + }, + "about": { + "title": "关于", + "running": "正在运行 MyDevTools", + "runningVersion": "正在运行 MyDevTools v{version}", + "checkButton": "检查更新", + "checking": "检查中…", + "dialogTitle": "更新", + "dialogDescription": "MyDevTools 会就地自我更新——无需重新安装,且绝不会触碰你的离线数据。", + "onVersion": "你当前使用的版本是 {version}。", + "available": "版本 {version} 可用。", + "install": "下载并安装", + "upToDate": "已是最新版本。", + "checkError": "无法检查更新。你是否已联网?" } }, "Dashboard": { diff --git a/apps/desktop-ui/src/app/app/notes/context/NotesContext.tsx b/apps/desktop-ui/src/app/app/notes/context/NotesContext.tsx index c80c09ec..595fb694 100644 --- a/apps/desktop-ui/src/app/app/notes/context/NotesContext.tsx +++ b/apps/desktop-ui/src/app/app/notes/context/NotesContext.tsx @@ -81,6 +81,9 @@ export function NotesProvider({ children }: { children: React.ReactNode }) { useEffect(() => { warmedKeyRef.current = warmedKey; }, [warmedKey]); const warmInFlight = useRef | null>(null); const contentLoadedIds = useRef>(new Set()); + // Cipher key the loaded bodies were decrypted under — a mismatch invalidates + // them (see the body-loading effect below). + const loadedUnderKey = useRef(null); // Notes whose content is an envelope we couldn't decrypt (vault locked). Their // in-state `content` is a null placeholder — writing it back would clobber the // real body, so content writes are blocked until unlock. @@ -156,7 +159,19 @@ export function NotesProvider({ children }: { children: React.ReactNode }) { setNotes([...allNotes].sort((a, b) => a.createdAt.localeCompare(b.createdAt))); }, [apiRequest]); + // Body of the active note. Runs on cipherKey change too: a note opened while + // the vault was locked holds a null placeholder, and the plain-text cache is + // module state — both must be invalidated and refetched under the new key, or + // the editor shows an empty document and the next keystroke saves that over + // the real body. (Invalidation lives here, not in its own [cipherKey] effect, + // so it can't run *after* this one and leave the refetch skipped.) useEffect(() => { + if (loadedUnderKey.current !== cipherKey) { + loadedUnderKey.current = cipherKey; + contentLoadedIds.current.clear(); + lockedIds.current.clear(); + clearPlainTextCache(); + } if (!activeNoteId || contentLoadedIds.current.has(activeNoteId)) return; let cancelled = false; setIsContentLoading(true); @@ -175,7 +190,7 @@ export function NotesProvider({ children }: { children: React.ReactNode }) { .catch(() => { /* note may have been deleted */ }) .finally(() => { if (!cancelled) setIsContentLoading(false); }); return () => { cancelled = true; }; - }, [activeNoteId, apiRequest, decryptBody, setContent]); + }, [activeNoteId, cipherKey, apiRequest, decryptBody, setContent]); // Flush parked updatedAt bumps from body saves when the user leaves a note — // one re-render per switch instead of one per autosave. @@ -203,16 +218,6 @@ export function NotesProvider({ children }: { children: React.ReactNode }) { // on the vault key. Bodies are handled by the effect below. }, [refreshNotes]); - // Vault lock/unlock: bodies and the search index invalidate themselves (both - // are tagged with their key), but the plain-text cache is module state — - // clear it so decrypted text doesn't outlive the unlocked session. The - // active note then refetches under the new key state. No mass refetch. - useEffect(() => { - contentLoadedIds.current.clear(); - lockedIds.current.clear(); - clearPlainTextCache(); - }, [cipherKey]); - /** * Decrypt every note body once into the plain-text search cache. Only the * extracted text is retained — raw bodies are dropped — so memory doesn't diff --git a/apps/desktop-ui/src/app/globals.css b/apps/desktop-ui/src/app/globals.css index 0ab5234a..99f3e29f 100644 --- a/apps/desktop-ui/src/app/globals.css +++ b/apps/desktop-ui/src/app/globals.css @@ -101,129 +101,9 @@ body { --sidebar-ring: 235 84% 67%; } - /* User-selectable accent overrides (ColorThemeProvider toggles these - classes on ). Light + dark variants; everything accent-colored - flows through --primary / --ring / --sidebar-primary. */ - :root.blue { - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 0 0% 98%; - --ring: 217.2 91.2% 59.8%; - --sidebar-primary: 217.2 91.2% 59.8%; - --sidebar-ring: 217.2 91.2% 59.8%; - } - - :root.purple { - --primary: 258.3 89.5% 66.3%; - --primary-foreground: 0 0% 98%; - --ring: 258.3 89.5% 66.3%; - --sidebar-primary: 258.3 89.5% 66.3%; - --sidebar-ring: 258.3 89.5% 66.3%; - } - - :root.green { - --primary: 142.1 76.2% 36.3%; - --primary-foreground: 0 0% 98%; - --ring: 142.1 76.2% 36.3%; - --sidebar-primary: 142.1 76.2% 36.3%; - --sidebar-ring: 142.1 76.2% 36.3%; - } - - :root.orange { - --primary: 24.6 95% 53.1%; - --primary-foreground: 0 0% 98%; - --ring: 24.6 95% 53.1%; - --sidebar-primary: 24.6 95% 53.1%; - --sidebar-ring: 24.6 95% 53.1%; - } - - :root.red { - --primary: 0 72.2% 50.6%; - --primary-foreground: 0 0% 98%; - --ring: 0 72.2% 50.6%; - --sidebar-primary: 0 72.2% 50.6%; - --sidebar-ring: 0 72.2% 50.6%; - } - - :root.pink { - --primary: 340.7 82.1% 52.7%; - --primary-foreground: 0 0% 98%; - --ring: 340.7 82.1% 52.7%; - --sidebar-primary: 340.7 82.1% 52.7%; - --sidebar-ring: 340.7 82.1% 52.7%; - } - - :root.cyan { - --primary: 199.1 89.1% 48.2%; - --primary-foreground: 0 0% 98%; - --ring: 199.1 89.1% 48.2%; - --sidebar-primary: 199.1 89.1% 48.2%; - --sidebar-ring: 199.1 89.1% 48.2%; - } - - :root.indigo { - --primary: 235 84% 67%; - --primary-foreground: 0 0% 100%; - --ring: 235 84% 67%; - --sidebar-primary: 235 84% 67%; - --sidebar-ring: 235 84% 67%; - } - - /* Dark mode: same hues, tuned lightness where needed */ - .dark.blue { - --primary: 217.2 91.2% 59.8%; - --ring: 217.2 91.2% 59.8%; - --sidebar-primary: 217.2 91.2% 59.8%; - --sidebar-ring: 217.2 91.2% 59.8%; - } - - .dark.purple { - --primary: 258.3 89.5% 66.3%; - --ring: 258.3 89.5% 66.3%; - --sidebar-primary: 258.3 89.5% 66.3%; - --sidebar-ring: 258.3 89.5% 66.3%; - } - - .dark.green { - --primary: 142.1 70.6% 45.3%; - --ring: 142.1 70.6% 45.3%; - --sidebar-primary: 142.1 70.6% 45.3%; - --sidebar-ring: 142.1 70.6% 45.3%; - } - - .dark.orange { - --primary: 24.6 95% 53.1%; - --ring: 24.6 95% 53.1%; - --sidebar-primary: 24.6 95% 53.1%; - --sidebar-ring: 24.6 95% 53.1%; - } - - .dark.red { - --primary: 0 72.2% 50.6%; - --ring: 0 72.2% 50.6%; - --sidebar-primary: 0 72.2% 50.6%; - --sidebar-ring: 0 72.2% 50.6%; - } - - .dark.pink { - --primary: 340.7 82.1% 52.7%; - --ring: 340.7 82.1% 52.7%; - --sidebar-primary: 340.7 82.1% 52.7%; - --sidebar-ring: 340.7 82.1% 52.7%; - } - - .dark.cyan { - --primary: 199.1 89.1% 48.2%; - --ring: 199.1 89.1% 48.2%; - --sidebar-primary: 199.1 89.1% 48.2%; - --sidebar-ring: 199.1 89.1% 48.2%; - } - - .dark.indigo { - --primary: 235 84% 67%; - --ring: 235 84% 67%; - --sidebar-primary: 235 84% 67%; - --sidebar-ring: 235 84% 67%; - } + /* Accent overrides live in JS: ColorThemeProvider writes --primary / --ring / + --sidebar-primary inline on (see lib/accent-color.ts), which also + lets a custom hex from the picker work without a CSS class per color. */ } /* Hidden scrollbar (element still scrolls) — used by the top-nav strip. */ diff --git a/apps/desktop-ui/src/app/settings/page.tsx b/apps/desktop-ui/src/app/settings/page.tsx index ee664f2d..fd193c4b 100644 --- a/apps/desktop-ui/src/app/settings/page.tsx +++ b/apps/desktop-ui/src/app/settings/page.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { Sun, Globe, Palette, Check, Lock } from 'lucide-react' +import { Sun, Globe, Palette, Lock, Pipette } from 'lucide-react' import { IDLE_TIMEOUT_KEY, getIdleTimeoutMinutes } from '@/lib/use-idle-lock' import { Switch } from '@/components/ui/switch' import { getVaultIconsEnabled, setVaultIconsEnabled } from '@/lib/vault-icon-pref' @@ -13,21 +13,12 @@ import { useTheme } from 'next-themes' import { cn } from '@/lib/utils' import { useLocale, useTranslations } from 'next-intl' import { useRouter } from 'next/navigation' -import { COLOR_THEME_OPTIONS, type ColorTheme, useColorTheme } from '@/hooks/use-color-theme' +import { useColorTheme } from '@/hooks/use-color-theme' +import { ACCENT_PRESETS, accentHex, isCustomAccent } from '@/lib/accent-color' import { ProfileCard } from '@/components/settings/profile-card' import { AppVersionLabel } from '@/components/desktop/app-version-label' import { useActiveWorkspace } from '@/store/workspace-store' import { Briefcase } from 'lucide-react' -const colorDisplay: Record = { - cyan: { swatchClass: 'bg-cyan-500', name: 'Teal' }, - blue: { swatchClass: 'bg-blue-500', name: 'Blue' }, - indigo: { swatchClass: 'bg-indigo-500', name: 'Indigo' }, - purple: { swatchClass: 'bg-purple-500', name: 'Purple' }, - green: { swatchClass: 'bg-green-500', name: 'Green' }, - orange: { swatchClass: 'bg-orange-500', name: 'Orange' }, - red: { swatchClass: 'bg-red-500', name: 'Red' }, - pink: { swatchClass: 'bg-pink-500', name: 'Pink' }, -} export default function SettingsPage() { const t = useTranslations('SettingsPage') @@ -57,6 +48,8 @@ export default function SettingsPage() { return null // Avoid hydration mismatch } + const customAccent = isCustomAccent(colorTheme) + const handleLanguageChange = (value: string) => { document.cookie = `NEXT_LOCALE=${value}; path=/; max-age=31536000; SameSite=Lax` router.refresh() @@ -100,7 +93,11 @@ export default function SettingsPage() {
- + {/* Fixed-height label rows keep both dropdowns on the same baseline, + even though only the accent label carries an icon. */} +
+ +
-
-
- - +
+
+ +
-
- {COLOR_THEME_OPTIONS.map((color) => { - const isSelected = colorTheme === color - const { swatchClass, name } = colorDisplay[color] +
+ {/* Trigger is w-full by default: flex-1 keeps it from squeezing the + picker swatch out of the row. */} + - return ( - - ) - })} + {/* Native picker: no dependency, and the OS eyedropper comes free. */} +
diff --git a/apps/desktop-ui/src/components/desktop/app-version-label.tsx b/apps/desktop-ui/src/components/desktop/app-version-label.tsx index a18c6015..adfac56f 100644 --- a/apps/desktop-ui/src/components/desktop/app-version-label.tsx +++ b/apps/desktop-ui/src/components/desktop/app-version-label.tsx @@ -1,21 +1,21 @@ "use client"; import { useEffect, useState } from "react"; -import { toast } from "sonner"; +import { useTranslations } from "next-intl"; -import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Info } from "lucide-react"; import { isDesktop } from "@/lib/desktop/is-desktop"; +import { DesktopUpdateDialog } from "@/components/desktop/desktop-update-dialog"; /** * About card for the settings page: shows the running app version (baked into - * tauri.conf.json) with a Check for updates button. Desktop-only — renders - * nothing on web. + * tauri.conf.json) with the Check for updates button + modal. Desktop-only — + * renders nothing on web. */ export function AppVersionLabel() { + const t = useTranslations("SettingsPage.about"); const [version, setVersion] = useState(""); - const [checking, setChecking] = useState(false); useEffect(() => { if (!isDesktop()) return; @@ -30,24 +30,6 @@ export function AppVersionLabel() { if (!isDesktop()) return null; - const handleCheck = async () => { - setChecking(true); - try { - const { checkForUpdate } = await import("@/lib/desktop/updater"); - const update = await checkForUpdate(); - if (!update) { - toast.success("You're on the latest version"); - return; - } - const { startUpdate } = await import("@/lib/desktop/use-update-install"); - startUpdate(); - } catch { - toast.error("Couldn't check for updates"); - } finally { - setChecking(false); - } - }; - return ( @@ -55,14 +37,14 @@ export function AppVersionLabel() { - About + {t("title")} - Running MyDevTools {version ? `v${version}` : ""} + + {version ? t("runningVersion", { version }) : t("running")} + - + ); diff --git a/apps/desktop-ui/src/components/desktop/desktop-update-dialog.tsx b/apps/desktop-ui/src/components/desktop/desktop-update-dialog.tsx index 48f904a1..4b5ef174 100644 --- a/apps/desktop-ui/src/components/desktop/desktop-update-dialog.tsx +++ b/apps/desktop-ui/src/components/desktop/desktop-update-dialog.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { useTranslations } from "next-intl"; import { CheckCircle2, Download, RefreshCw } from "lucide-react"; import { toast } from "sonner"; @@ -20,6 +21,7 @@ import type { UpdateInfo } from "@/lib/desktop/updater"; * data is never touched). Desktop-only — callers gate on isDesktop(). */ export function DesktopUpdateDialog() { + const t = useTranslations("SettingsPage.about"); const [open, setOpen] = useState(false); const [version, setVersion] = useState(""); const [checking, setChecking] = useState(false); @@ -33,7 +35,7 @@ export function DesktopUpdateDialog() { if (!version) setVersion(await m.currentAppVersion()); setUpdate(await m.checkForUpdate()); } catch { - toast.error("Couldn't check for updates. Are you online?"); + toast.error(t("checkError")); setOpen(false); } finally { setChecking(false); @@ -50,28 +52,27 @@ export function DesktopUpdateDialog() { <> setOpen(o)}> - Updates + {t("dialogTitle")} - MyDevTools updates itself in place — no reinstall, and your - offline data is never touched. - {version ? ` You're on version ${version}.` : ""} + {t("dialogDescription")} + {version ? ` ${t("onVersion", { version })}` : ""} {checking ? (
- Checking… + {t("checking")}
) : update ? (

- Version {update.version} is available. + {t("available", { version: update.version })}

{update.notes ? (

@@ -80,12 +81,12 @@ export function DesktopUpdateDialog() { ) : null}

) : (
- You're up to date. + {t("upToDate")}
)}
diff --git a/apps/desktop-ui/src/components/notes/NotionEditor.tsx b/apps/desktop-ui/src/components/notes/NotionEditor.tsx index 11e43f5e..6a8b2314 100644 --- a/apps/desktop-ui/src/components/notes/NotionEditor.tsx +++ b/apps/desktop-ui/src/components/notes/NotionEditor.tsx @@ -40,6 +40,11 @@ import { readingTimeMinutes, } from "@/app/app/notes/utils/noteContentUtils"; import type { Note } from "@/app/app/notes/types/Note"; +import { + mergePendingUpdate, + requeuePendingUpdates, + type PendingUpdates, +} from "@/components/notes/notes-helpers"; export default function NotionEditor() { const tEditor = useTranslations("Notes.editor"); @@ -100,33 +105,46 @@ export default function NotionEditor() { return () => clearTimeout(timeout); }, [saveState]); - const handleUpdate = useCallback(async (id: string, updates: Partial) => { - if (id) { - setSaveState("saving"); + // Title, tags and body all share one debounced save, so pending edits are + // queued per note and merged — a title keystroke inside the body's debounce + // window must not replace the queued body edit with itself. + const pendingUpdatesRef = useRef(new Map()); + + const flushUpdates = useCallback(async () => { + const batch = [...pendingUpdatesRef.current]; + if (!batch.length) return; + pendingUpdatesRef.current.clear(); + setSaveState("saving"); + try { + for (const [id, updates] of batch) await updateNote(id, updates); isDirtyRef.current = false; - try { - await updateNote(id, updates); - setLastSavedAt(new Date()); - setSaveState("saved"); - } catch (err) { - // Vault locked / no workspace: surface the reason and keep the - // note dirty so the next edit retries instead of silently dropping. - isDirtyRef.current = true; - setSaveState("idle"); - toast.error(err instanceof Error ? err.message : String(err)); - } + setLastSavedAt(new Date()); + setSaveState("saved"); + } catch (err) { + // Vault locked / no workspace: surface the reason and requeue so the + // next edit retries instead of silently dropping. + requeuePendingUpdates(pendingUpdatesRef.current, batch); + isDirtyRef.current = true; + setSaveState("idle"); + toast.error(err instanceof Error ? err.message : String(err)); } }, [updateNote]); - const debouncedUpdate = useDebouncedCallback(handleUpdate, 1000); + // flushOnExit: unmount (navigating away, vault auto-lock swapping in the + // locked placeholder) must not drop an edit that already showed "Saving…". + const debouncedUpdate = useDebouncedCallback(flushUpdates, 1000, { flushOnExit: true }); + + const queueUpdate = useCallback((id: string | null, updates: Partial) => { + if (!id) return; + mergePendingUpdate(pendingUpdatesRef.current, id, updates); + setSaveState("saving"); + debouncedUpdate(); + }, [debouncedUpdate]); const handleTitleChange = (e: React.ChangeEvent) => { const newTitle = e.target.value; setTitle(newTitle); - if (activeNoteId) { - setSaveState("saving"); - debouncedUpdate(activeNoteId, { title: newTitle }); - } + queueUpdate(activeNoteId, { title: newTitle }); }; const handleEditorChange = (markdown: string) => { @@ -134,8 +152,7 @@ export default function NotionEditor() { pushWordCount(markdown); if (activeNoteId) { isDirtyRef.current = true; - setSaveState("saving"); - debouncedUpdate(activeNoteId, { content: markdown }); + queueUpdate(activeNoteId, { content: markdown }); } }; @@ -191,14 +208,14 @@ export default function NotionEditor() { if (!tag || tags.includes(tag) || tags.length >= 20) return; const next = [...tags, tag]; setTags(next); - if (activeNoteId) debouncedUpdate(activeNoteId, { tags: next }); - }, [tags, activeNoteId, debouncedUpdate]); + queueUpdate(activeNoteId, { tags: next }); + }, [tags, activeNoteId, queueUpdate]); const removeTag = useCallback((tag: string) => { const next = tags.filter(t => t !== tag); setTags(next); - if (activeNoteId) debouncedUpdate(activeNoteId, { tags: next }); - }, [tags, activeNoteId, debouncedUpdate]); + queueUpdate(activeNoteId, { tags: next }); + }, [tags, activeNoteId, queueUpdate]); // Defer word-count compute: the heavy extract runs at most every 500ms // instead of every keystroke. @@ -237,8 +254,7 @@ export default function NotionEditor() { const handleApplyTemplate = useCallback((tpl: NoteTemplate) => { if (!activeNoteId) return; - setSaveState("saving"); - debouncedUpdate(activeNoteId, { content: tpl.content, title: tpl.defaultTitle }); + queueUpdate(activeNoteId, { content: tpl.content, title: tpl.defaultTitle }); setTitle(tpl.defaultTitle); latestMarkdownRef.current = tpl.content; setWordCountSource(tpl.content); @@ -248,7 +264,7 @@ export default function NotionEditor() { // clobber the title/content we just set. setPendingTemplateContent({ noteId: activeNoteId, markdown: tpl.content }); setEditorKey(k => k + 1); - }, [activeNoteId, debouncedUpdate]); + }, [activeNoteId, queueUpdate]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { diff --git a/apps/desktop-ui/src/components/notes/__tests__/notes-helpers.test.ts b/apps/desktop-ui/src/components/notes/__tests__/notes-helpers.test.ts index ace8032a..e2bc7616 100644 --- a/apps/desktop-ui/src/components/notes/__tests__/notes-helpers.test.ts +++ b/apps/desktop-ui/src/components/notes/__tests__/notes-helpers.test.ts @@ -4,6 +4,9 @@ import { deleteCachedPlainText, clearPlainTextCache, buildChildrenMap, + mergePendingUpdate, + requeuePendingUpdates, + type PendingUpdates, } from "../notes-helpers"; import type { Note } from "@/app/app/notes/types/Note"; @@ -65,3 +68,27 @@ describe("buildChildrenMap", () => { expect(map.get("root")?.map((n) => n.id)).toEqual(["kid"]); }); }); + +describe("pending update queue", () => { + it("merges fields instead of replacing (body edit survives a title keystroke)", () => { + const queue: PendingUpdates = new Map(); + mergePendingUpdate(queue, "a", { content: "body text" }); + mergePendingUpdate(queue, "a", { title: "Renamed" }); + expect(queue.get("a")).toEqual({ content: "body text", title: "Renamed" }); + }); + + it("keeps notes separate", () => { + const queue: PendingUpdates = new Map(); + mergePendingUpdate(queue, "a", { content: "a body" }); + mergePendingUpdate(queue, "b", { content: "b body" }); + expect(queue.get("a")).toEqual({ content: "a body" }); + expect(queue.get("b")).toEqual({ content: "b body" }); + }); + + it("requeues a failed batch without clobbering newer edits", () => { + const queue: PendingUpdates = new Map(); + mergePendingUpdate(queue, "a", { content: "newer", tags: ["t"] }); + requeuePendingUpdates(queue, [["a", { content: "failed", title: "T" }]]); + expect(queue.get("a")).toEqual({ content: "newer", tags: ["t"], title: "T" }); + }); +}); diff --git a/apps/desktop-ui/src/components/notes/notes-helpers.ts b/apps/desktop-ui/src/components/notes/notes-helpers.ts index bdf84a42..928526f2 100644 --- a/apps/desktop-ui/src/components/notes/notes-helpers.ts +++ b/apps/desktop-ui/src/components/notes/notes-helpers.ts @@ -47,6 +47,30 @@ export function getCachedPlainText(note: Note): string { return v; } +// ── Pending-save queue ──────────────────────────────────────────────────────── +// The editor shares ONE debounced save across title, tags and body, so a title +// keystroke landing inside the body's debounce window would otherwise replace +// the queued body edit with itself. Updates are queued per note and merged. + +export type PendingUpdates = Map>; + +/** Queue one note update; later edits win field-by-field. */ +export function mergePendingUpdate( + queue: PendingUpdates, + id: string, + updates: Partial, +): void { + queue.set(id, { ...queue.get(id), ...updates }); +} + +/** Put a failed batch back in the queue without clobbering newer edits. */ +export function requeuePendingUpdates( + queue: PendingUpdates, + batch: Array<[string, Partial]>, +): void { + for (const [id, updates] of batch) queue.set(id, { ...updates, ...queue.get(id) }); +} + // Pre-computed map from parentId -> children (avoids O(n²) per render) export type ChildrenMap = Map; diff --git a/apps/desktop-ui/src/components/settings/profile-card.tsx b/apps/desktop-ui/src/components/settings/profile-card.tsx index 492a4238..57fe8e38 100644 --- a/apps/desktop-ui/src/components/settings/profile-card.tsx +++ b/apps/desktop-ui/src/components/settings/profile-card.tsx @@ -11,7 +11,6 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { PROFILE_UPDATED_EVENT } from '@/hooks/use-app-user' import { getUserPreferences, patchUserPreferences } from '@/lib/user-preferences-api' -import { DesktopUpdateDialog } from '@/components/desktop/desktop-update-dialog' /** * Local profile: the name and avatar the app shows you. Stored in local @@ -92,12 +91,9 @@ export function ProfileCard() {
-
- - -
+ ) diff --git a/apps/desktop-ui/src/components/shell/top-nav-strip.tsx b/apps/desktop-ui/src/components/shell/top-nav-strip.tsx index 11449bf9..975bd02c 100644 --- a/apps/desktop-ui/src/components/shell/top-nav-strip.tsx +++ b/apps/desktop-ui/src/components/shell/top-nav-strip.tsx @@ -178,7 +178,11 @@ export function TopNavStrip() { const activeWs = useActiveWorkspace() const pinned = buildPinnedNavItems(pinnedTools, activeWs) const togglePin = usePinnedToolsStore((s) => s.togglePin) - const { tabs, closeTab } = useTabStore() + const { tabs, closeTab, moveTab } = useTabStore() + + // Drag-to-reorder (native HTML5 DnD, same as the API-client tab bar). + const [draggedPath, setDraggedPath] = useState(null) + const [dragOverPath, setDragOverPath] = useState(null) const unpin = useCallback( (url: string) => { if (activeWs) togglePin(activeWs.id, url) @@ -234,7 +238,7 @@ export function TopNavStrip() { // Edge-fade affordance: show a fade only on the side that has more to scroll. const scrollRef = useRef(null) - const activeTabRef = useRef(null) + const activeTabRef = useRef(null) const [canScrollLeft, setCanScrollLeft] = useState(false) const [canScrollRight, setCanScrollRight] = useState(false) const updateScroll = useCallback(() => { @@ -341,17 +345,53 @@ export function TopNavStrip() { ) : null} - + {title} diff --git a/apps/desktop-ui/src/components/user-preferences-sync.tsx b/apps/desktop-ui/src/components/user-preferences-sync.tsx index afb5f9e4..cb5a6ae4 100644 --- a/apps/desktop-ui/src/components/user-preferences-sync.tsx +++ b/apps/desktop-ui/src/components/user-preferences-sync.tsx @@ -5,7 +5,8 @@ import { useTheme } from "next-themes"; import { useLocale } from "next-intl"; import { useRouter } from "next/navigation"; import useAuth from "@/utils/useAuth"; -import { COLOR_THEME_OPTIONS, type ColorTheme, useColorTheme } from "@/hooks/use-color-theme"; +import { useColorTheme } from "@/hooks/use-color-theme"; +import { isAccentColor } from "@/lib/accent-color"; import { getUserPreferences, patchUserPreferences, type ThemePreference, type UserPreferencesOut } from "@/lib/user-preferences-api"; const SUPPORTED_LOCALES = [ @@ -50,9 +51,8 @@ function setLocaleCookie(locale: string) { document.cookie = `NEXT_LOCALE=${locale}; path=/; max-age=31536000; SameSite=Lax`; } -function isColorTheme(value: string | undefined): value is ColorTheme { - return !!value && (COLOR_THEME_OPTIONS as readonly string[]).includes(value); -} +/** Accepts preset ids and custom hex picks alike (see lib/accent-color.ts). */ +const isColorTheme = isAccentColor; export function UserPreferencesSync() { const { user, loading: authLoading } = useAuth(false); diff --git a/apps/desktop-ui/src/hooks/use-color-theme.tsx b/apps/desktop-ui/src/hooks/use-color-theme.tsx index db43a224..05513168 100644 --- a/apps/desktop-ui/src/hooks/use-color-theme.tsx +++ b/apps/desktop-ui/src/hooks/use-color-theme.tsx @@ -10,27 +10,17 @@ import { type ReactNode, } from "react" -export type ColorTheme = - | "blue" - | "purple" - | "green" - | "orange" - | "red" - | "pink" - | "cyan" - | "indigo" +import { + DEFAULT_ACCENT, + accentCssVars, + isAccentColor, + type AccentColor, +} from "@/lib/accent-color" + +/** Preset id (see ACCENT_PRESETS) or a custom `#rrggbb` from the color picker. */ +export type ColorTheme = AccentColor const COLOR_THEME_KEY = "app-color-theme" -export const COLOR_THEME_OPTIONS: ColorTheme[] = [ - "cyan", - "blue", - "indigo", - "purple", - "green", - "orange", - "red", - "pink", -] type ColorThemeContextValue = { colorTheme: ColorTheme @@ -40,28 +30,26 @@ type ColorThemeContextValue = { const ColorThemeContext = createContext(null) -function isValidColorTheme(theme: string): theme is ColorTheme { - return COLOR_THEME_OPTIONS.includes(theme as ColorTheme) -} - +/** Accent vars go inline on so custom hex picks work without new CSS. */ function applyColorTheme(theme: ColorTheme) { - const root = document.documentElement - root.classList.remove(...COLOR_THEME_OPTIONS) - root.classList.add(theme) + const { style } = document.documentElement + for (const [name, value] of Object.entries(accentCssVars(theme))) { + style.setProperty(name, value) + } } export function ColorThemeProvider({ children }: { children: ReactNode }) { - const [colorTheme, setColorThemeState] = useState("indigo") + const [colorTheme, setColorThemeState] = useState(DEFAULT_ACCENT) const [mounted, setMounted] = useState(false) useEffect(() => { setMounted(true) - const stored = localStorage.getItem(COLOR_THEME_KEY) as ColorTheme | null - if (stored && isValidColorTheme(stored)) { + const stored = localStorage.getItem(COLOR_THEME_KEY) + if (isAccentColor(stored)) { setColorThemeState(stored) applyColorTheme(stored) } else { - applyColorTheme("indigo") + applyColorTheme(DEFAULT_ACCENT) } }, []) diff --git a/apps/desktop-ui/src/lib/__tests__/accent-color.test.ts b/apps/desktop-ui/src/lib/__tests__/accent-color.test.ts new file mode 100644 index 00000000..f35dbd71 --- /dev/null +++ b/apps/desktop-ui/src/lib/__tests__/accent-color.test.ts @@ -0,0 +1,29 @@ +import { accentCssVars, accentHex, isAccentColor, isCustomAccent } from '@/lib/accent-color' + +describe('accent-color', () => { + it('resolves presets, custom hex, and garbage', () => { + expect(accentHex('indigo')).toBe('#6366f1') + expect(accentHex('#AABBCC')).toBe('#aabbcc') + expect(accentHex('not-a-color')).toBe('#6366f1') + }) + + it('validates stored values', () => { + expect(isAccentColor('blue')).toBe(true) + expect(isAccentColor('#f0f')).toBe(true) + expect(isAccentColor('rebeccapurple')).toBe(false) + expect(isAccentColor(undefined)).toBe(false) + expect(isCustomAccent('blue')).toBe(false) + expect(isCustomAccent('#123456')).toBe(true) + }) + + it('emits an HSL triple for --primary', () => { + expect(accentCssVars('red')['--primary']).toBe('0 84.2% 60.2%') + }) + + it('flips --primary-foreground to dark text on bright accents', () => { + expect(accentCssVars('yellow')['--primary-foreground']).toBe('222 47% 11%') + expect(accentCssVars('#ffff00')['--primary-foreground']).toBe('222 47% 11%') + expect(accentCssVars('indigo')['--primary-foreground']).toBe('0 0% 100%') + expect(accentCssVars('#000000')['--primary-foreground']).toBe('0 0% 100%') + }) +}) diff --git a/apps/desktop-ui/src/lib/accent-color.ts b/apps/desktop-ui/src/lib/accent-color.ts new file mode 100644 index 00000000..813ea703 --- /dev/null +++ b/apps/desktop-ui/src/lib/accent-color.ts @@ -0,0 +1,121 @@ +/** + * Accent palette + the CSS variables it drives. A stored accent is either a + * preset id or a custom `#rrggbb` chosen from the color picker, so everything + * downstream resolves through `accentCssVars`. + */ +export const ACCENT_PRESETS = [ + { id: 'indigo', name: 'Indigo', hex: '#6366f1' }, + { id: 'blue', name: 'Blue', hex: '#3b82f6' }, + { id: 'sky', name: 'Sky', hex: '#0ea5e9' }, + { id: 'cyan', name: 'Teal', hex: '#06b6d4' }, + { id: 'teal', name: 'Aqua', hex: '#14b8a6' }, + { id: 'emerald', name: 'Emerald', hex: '#10b981' }, + { id: 'green', name: 'Green', hex: '#22c55e' }, + { id: 'lime', name: 'Lime', hex: '#84cc16' }, + { id: 'yellow', name: 'Yellow', hex: '#eab308' }, + { id: 'amber', name: 'Amber', hex: '#f59e0b' }, + { id: 'orange', name: 'Orange', hex: '#f97316' }, + { id: 'red', name: 'Red', hex: '#ef4444' }, + { id: 'rose', name: 'Rose', hex: '#f43f5e' }, + { id: 'pink', name: 'Pink', hex: '#ec4899' }, + { id: 'fuchsia', name: 'Fuchsia', hex: '#d946ef' }, + { id: 'purple', name: 'Purple', hex: '#a855f7' }, + { id: 'violet', name: 'Violet', hex: '#8b5cf6' }, + { id: 'slate', name: 'Slate', hex: '#64748b' }, +] as const + +export type AccentPresetId = (typeof ACCENT_PRESETS)[number]['id'] + +/** Preset id, or a custom hex color. */ +export type AccentColor = AccentPresetId | (string & {}) + +export const DEFAULT_ACCENT: AccentPresetId = 'indigo' + +const HEX_RE = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i +const PRESET_HEX = new Map(ACCENT_PRESETS.map((p) => [p.id, p.hex])) + +/** Foreground pair: the HSL triple written to CSS + its hex, for contrast math. */ +const DARK_FG = { hsl: '222 47% 11%', hex: '#0f1729' } +const LIGHT_FG = { hsl: '0 0% 100%', hex: '#ffffff' } + +export function isAccentColor(value: string | undefined | null): value is AccentColor { + return !!value && (PRESET_HEX.has(value) || HEX_RE.test(value)) +} + +export function isCustomAccent(value: string): boolean { + return !PRESET_HEX.has(value) && HEX_RE.test(value) +} + +/** Resolved swatch color for a stored accent; falls back to the default preset. */ +export function accentHex(value: string): string { + const preset = PRESET_HEX.get(value) + if (preset) return preset + return HEX_RE.test(value) ? value.toLowerCase() : PRESET_HEX.get(DEFAULT_ACCENT)! +} + +const round = (n: number) => Math.round(n * 10) / 10 + +/** + * Local color math (rather than the `color` package wcag-contrast.ts uses) so the + * theme provider stays dependency-free and this stays plain-jest testable. + */ +function toRgb(hex: string): [number, number, number] { + const raw = hex.slice(1) + const full = raw.length === 3 ? raw.replace(/./g, (c) => c + c) : raw + const n = parseInt(full, 16) + return [(n >> 16) & 255, (n >> 8) & 255, n & 255] +} + +function toHsl(hex: string): [number, number, number] { + const [r, g, b] = toRgb(hex).map((c) => c / 255) + const max = Math.max(r, g, b) + const min = Math.min(r, g, b) + const d = max - min + const l = (max + min) / 2 + const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1)) + + let h = 0 + if (d !== 0) { + if (max === r) h = ((g - b) / d) % 6 + else if (max === g) h = (b - r) / d + 2 + else h = (r - g) / d + 4 + h = (h * 60 + 360) % 360 + } + + return [h, s * 100, l * 100] +} + +/** WCAG 2.1 relative luminance (0–1). */ +function luminance(hex: string): number { + const [r, g, b] = toRgb(hex).map((c) => { + const s = c / 255 + return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4) + }) + return 0.2126 * r + 0.7152 * g + 0.0722 * b +} + +function contrast(a: string, b: string): number { + const [la, lb] = [luminance(a), luminance(b)] + return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05) +} + +/** + * CSS custom properties for an accent. `--primary-foreground` follows whichever + * of dark/white text contrasts better, so bright accents (yellow, lime) stay + * readable instead of shipping white-on-yellow. + */ +export function accentCssVars(value: string): Record { + const hex = accentHex(value) + const [h, s, l] = toHsl(hex) + const triple = `${round(h)} ${round(s)}% ${round(l)}%` + const fg = contrast(hex, LIGHT_FG.hex) >= contrast(hex, DARK_FG.hex) ? LIGHT_FG : DARK_FG + + return { + '--primary': triple, + '--primary-foreground': fg.hsl, + '--ring': triple, + '--sidebar-primary': triple, + '--sidebar-primary-foreground': fg.hsl, + '--sidebar-ring': triple, + } +} diff --git a/apps/desktop-ui/src/store/__tests__/tab-store.test.ts b/apps/desktop-ui/src/store/__tests__/tab-store.test.ts new file mode 100644 index 00000000..31ce6b1f --- /dev/null +++ b/apps/desktop-ui/src/store/__tests__/tab-store.test.ts @@ -0,0 +1,27 @@ +import { useTabStore } from '../tab-store' + +const paths = ['/app/a', '/app/b', '/app/c'] + +beforeEach(() => { + useTabStore.setState({ tabs: paths.map(path => ({ path })), activeTabPath: '/app/a' }) +}) + +const order = () => useTabStore.getState().tabs.map(t => t.path) + +describe('moveTab', () => { + it('moves a tab forward', () => { + useTabStore.getState().moveTab('/app/a', '/app/c') + expect(order()).toEqual(['/app/b', '/app/c', '/app/a']) + }) + + it('moves a tab backward', () => { + useTabStore.getState().moveTab('/app/c', '/app/a') + expect(order()).toEqual(['/app/c', '/app/a', '/app/b']) + }) + + it('ignores unknown or identical paths', () => { + useTabStore.getState().moveTab('/app/a', '/app/a') + useTabStore.getState().moveTab('/app/a', '/app/nope') + expect(order()).toEqual(paths) + }) +}) diff --git a/apps/desktop-ui/src/store/tab-store.ts b/apps/desktop-ui/src/store/tab-store.ts index f2ea5ef3..0f7bfb3f 100644 --- a/apps/desktop-ui/src/store/tab-store.ts +++ b/apps/desktop-ui/src/store/tab-store.ts @@ -18,6 +18,7 @@ interface TabStore { closeTab: (path: string) => void setActiveTab: (path: string) => void closeAllTabs: () => void + moveTab: (fromPath: string, toPath: string) => void } export const useTabStore = create()( @@ -52,6 +53,18 @@ export const useTabStore = create()( setActiveTab: (path: string) => set({ activeTabPath: path }), closeAllTabs: () => set({ tabs: [], activeTabPath: null }), + + // Drag-reorder: pull the dragged tab out and drop it at the target's slot. + moveTab: (fromPath: string, toPath: string) => { + const { tabs } = get() + const from = tabs.findIndex(t => t.path === fromPath) + const to = tabs.findIndex(t => t.path === toPath) + if (from === -1 || to === -1 || from === to) return + const next = [...tabs] + const [moved] = next.splice(from, 1) + next.splice(to, 0, moved) + set({ tabs: next }) + }, }), { name: 'mdt-app-tabs', diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index d826e6ad..2c90224d 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -20,6 +20,7 @@ "center": true, "titleBarStyle": "Overlay", "hiddenTitle": true, + "dragDropEnabled": false, "trafficLightPosition": { "x": 14, "y": 27 } } ], diff --git a/apps/web/public/funding.json b/apps/web/public/funding.json new file mode 100644 index 00000000..eddd5c9e --- /dev/null +++ b/apps/web/public/funding.json @@ -0,0 +1,102 @@ +{ + "version": "v1.0.0", + "entity": { + "type": "individual", + "role": "owner", + "name": "Akhil Edathadan", + "email": "akhiledathadan007@gmail.com", + "description": "Solo developer building and maintaining MyDevTools, a free and open-source offline desktop toolkit with 80+ developer tools. Funding covers AI coding-agent usage and hosting/signing infrastructure so the app stays free, offline and without paywalls.", + "webpageUrl": { + "url": "https://mydevtools.tech" + } + }, + "projects": [ + { + "guid": "mydevtools", + "name": "MyDevTools", + "description": "An all-in-one, offline-first developer toolkit desktop app with 80+ tools: formatters, converters, encoders, security and crypto utilities, generators, an API client with mock server and SQL/gRPC support, a data explorer, plus notes, bookmarks and tasks. Fully local: no accounts, no sign-in, no cloud sync, no telemetry beyond an opt-in anonymous ping. Data is stored on-device in an encrypted SQLCipher database. Free and open source under AGPL-3.0, shipped for macOS, Windows and Linux.", + "webpageUrl": { + "url": "https://mydevtools.tech" + }, + "repositoryUrl": { + "url": "https://github.com/mydevtools-tech/mydevtools", + "wellKnown": "https://github.com/mydevtools-tech/mydevtools/blob/main/.well-known/funding-manifest-urls" + }, + "licenses": ["spdx:AGPL-3.0-only"], + "tags": [ + "developer-tools", + "desktop-app", + "offline-first", + "privacy", + "productivity", + "api-client", + "tauri", + "rust", + "typescript", + "open-source" + ] + } + ], + "funding": { + "channels": [ + { + "guid": "direct-contact", + "type": "other", + "address": "akhiledathadan007@gmail.com", + "description": "Email to arrange a bank transfer or an alternative payment method. Please state which funding plan you intend to support." + } + ], + "plans": [ + { + "guid": "five-year-runway", + "status": "active", + "name": "5-year runway (AI agents + infrastructure)", + "description": "One-time grant covering five years of running costs: AI coding-agent usage (~USD 250/month), hosting, domains and email (~USD 40/month), and Apple Developer plus Windows code-signing certificates (~USD 320/year). Total burn ~USD 3,800/year, plus a small buffer for price increases and one-off tooling.", + "amount": 20000, + "currency": "USD", + "frequency": "one-time", + "channels": ["direct-contact"] + }, + { + "guid": "yearly-runway", + "status": "active", + "name": "One year of runway", + "description": "Covers a single year of AI coding-agent usage, hosting and code-signing certificates.", + "amount": 4000, + "currency": "USD", + "frequency": "yearly", + "channels": ["direct-contact"] + }, + { + "guid": "monthly-sustainer", + "status": "active", + "name": "Monthly sustainer", + "description": "Covers the full monthly burn: AI coding agents, servers and the amortised cost of signing certificates.", + "amount": 350, + "currency": "USD", + "frequency": "monthly", + "channels": ["direct-contact"] + }, + { + "guid": "monthly-supporter", + "status": "active", + "name": "Monthly supporter", + "description": "A partial contribution towards the monthly running costs.", + "amount": 25, + "currency": "USD", + "frequency": "monthly", + "channels": ["direct-contact"] + }, + { + "guid": "any-amount", + "status": "active", + "name": "Any amount, one time", + "description": "Give whatever you like, whenever you like.", + "amount": 0, + "currency": "USD", + "frequency": "one-time", + "channels": ["direct-contact"] + } + ] + } +} diff --git a/apps/web/src/app/blog/[slug]/page.tsx b/apps/web/src/app/blog/[slug]/page.tsx index f1de8cf5..66091ebc 100644 --- a/apps/web/src/app/blog/[slug]/page.tsx +++ b/apps/web/src/app/blog/[slug]/page.tsx @@ -148,7 +148,7 @@ export default async function BlogPostPage({ type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> -
+
diff --git a/apps/web/src/app/blog/page.tsx b/apps/web/src/app/blog/page.tsx index 9c901408..7d87f6de 100644 --- a/apps/web/src/app/blog/page.tsx +++ b/apps/web/src/app/blog/page.tsx @@ -73,7 +73,7 @@ export default function BlogPage() { type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> -
+
diff --git a/apps/web/src/app/changelog/page.tsx b/apps/web/src/app/changelog/page.tsx index d0a0d341..e980c6b0 100644 --- a/apps/web/src/app/changelog/page.tsx +++ b/apps/web/src/app/changelog/page.tsx @@ -104,7 +104,7 @@ export default function ChangelogPage() { type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> -
+
diff --git a/apps/web/src/app/compare/[slug]/page.tsx b/apps/web/src/app/compare/[slug]/page.tsx index 3495e37f..e47e015e 100644 --- a/apps/web/src/app/compare/[slug]/page.tsx +++ b/apps/web/src/app/compare/[slug]/page.tsx @@ -100,7 +100,7 @@ export default async function ComparisonPage({ type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> -
+
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 204308a3..a225e0cb 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -239,7 +239,7 @@ export default function Page() {
-
+
{/* ── Hero ────────────────────────────────────────────────────────────── */}
diff --git a/apps/web/src/app/tools/[slug]/page.tsx b/apps/web/src/app/tools/[slug]/page.tsx index a2a5a1e5..358167d1 100644 --- a/apps/web/src/app/tools/[slug]/page.tsx +++ b/apps/web/src/app/tools/[slug]/page.tsx @@ -83,7 +83,7 @@ export default async function ToolLandingPage({ /> )} -
+
{/* ── Hero ── */} diff --git a/apps/web/src/app/tools/page.tsx b/apps/web/src/app/tools/page.tsx index c102d2e0..2509c8f0 100644 --- a/apps/web/src/app/tools/page.tsx +++ b/apps/web/src/app/tools/page.tsx @@ -50,7 +50,7 @@ export default function ToolsIndexPage() { -
+
{/* ── Hero ── */} diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index d8c2787b..f251200d 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -1,10 +1,10 @@ "use client"; import { useState, useEffect, type ElementType } from "react"; import Link from "next/link"; -import { Menu, X } from "lucide-react"; +import { Github, Menu, X } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { ModeToggle } from "./modeToggle"; import { Logo } from "./logo"; +import { SOURCE_URL } from "./footer"; import { motion, AnimatePresence } from "framer-motion"; import { cn } from "@/lib/utils"; @@ -41,16 +41,9 @@ const menuItemVariants = { }), }; -type HeaderProps = { - /** When false, theme toggle is hidden (e.g. marketing page fixed to dark). */ - showThemeToggle?: boolean; -}; - -export function Header({ showThemeToggle = true }: HeaderProps) { +export function Header() { const [isMenuOpen, setIsMenuOpen] = useState(false); const [scrolled, setScrolled] = useState(false); - // No accounts — the only action is getting the app. - const cta = { href: "/download", label: "Download" }; useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 8); @@ -71,6 +64,7 @@ export function Header({ showThemeToggle = true }: HeaderProps) { { href: "/developer-tools", label: "Platform" }, { href: "/tools", label: "Tools" }, { href: "/changelog", label: "Changelog" }, + { href: SOURCE_URL, label: "GitHub", icon: Github, external: true }, ]; return ( @@ -133,6 +127,23 @@ export function Header({ showThemeToggle = true }: HeaderProps) { {/* Desktop Actions */}
+ + {/* Mobile Menu Toggle */} - - {/* Desktop Actions */} -
- {showThemeToggle ? : null} - - {cta.label} - -
@@ -230,37 +230,6 @@ export function Header({ showThemeToggle = true }: HeaderProps) { ))} - - - setIsMenuOpen(false)} - className="mdt-btn-grad flex h-12 items-center justify-center rounded-xl px-4 text-base font-medium" - > - {cta.label} - - - - {showThemeToggle ? ( - -
- Theme - -
-
- ) : null} diff --git a/apps/web/src/components/marketing-seo-page.tsx b/apps/web/src/components/marketing-seo-page.tsx index b0145bb2..f080e784 100644 --- a/apps/web/src/components/marketing-seo-page.tsx +++ b/apps/web/src/components/marketing-seo-page.tsx @@ -86,7 +86,7 @@ export function MarketingSeoPage({ dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> ) : null} -
+
diff --git a/apps/web/src/components/modeToggle.tsx b/apps/web/src/components/modeToggle.tsx deleted file mode 100644 index 46f4ba6d..00000000 --- a/apps/web/src/components/modeToggle.tsx +++ /dev/null @@ -1,80 +0,0 @@ -"use client" - -import * as React from "react" -import { Moon, Sun } from "lucide-react" -import { useTheme } from "next-themes" -import { useThemeAnimation } from "@space-man/react-theme-animation" -import * as SwitchPrimitives from "@radix-ui/react-switch" -import { cn } from "@/lib/utils" - -export function ModeToggle() { - const { setTheme } = useTheme() - const { theme, toggleTheme, ref } = useThemeAnimation() - const [mounted, setMounted] = React.useState(false) - - // Avoid hydration mismatch - React.useEffect(() => { - setMounted(true) - }, []) - - // Sync theme from animation hook to next-themes - React.useEffect(() => { - if (mounted && theme) { - setTheme(theme) - } - }, [theme, mounted, setTheme]) - - const isDark = theme === "dark" - - const handleToggle = (checked: boolean) => { - // The toggleTheme from useThemeAnimation handles both theme change and animation - toggleTheme() - } - - if (!mounted) { - return ( - - - - - - ) - } - - return ( - - - {isDark ? ( - - ) : ( - - )} - - - ) -} diff --git a/apps/web/src/lib/seo/platform-pages.ts b/apps/web/src/lib/seo/platform-pages.ts index 38636766..10e2e937 100644 --- a/apps/web/src/lib/seo/platform-pages.ts +++ b/apps/web/src/lib/seo/platform-pages.ts @@ -33,7 +33,6 @@ export const platformSeoPages: PlatformSeoPage[] = [ intro: 'MyDevTools brings the utilities developers reach for every day into one desktop workspace that runs offline: format data, test APIs, generate tokens, inspect encodings, manage secure notes, and stay in flow — with everything processed locally.', primaryCta: { href: '/tools', label: 'Browse all tools' }, - secondaryCta: { href: '/login', label: 'Open dashboard' }, sections: [ { title: 'Built for search-worthy developer workflows',