From 47ad8479cb6aca8845d41398b522d65645431a12 Mon Sep 17 00:00:00 2001 From: AKHIL Date: Sat, 15 Aug 2026 20:22:01 +0530 Subject: [PATCH 01/11] Delete ci.yml --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .github/workflows/ci.yml 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 From 774db357494a74f31f092a822a58091475c519f4 Mon Sep 17 00:00:00 2001 From: AKHIL Date: Sat, 15 Aug 2026 21:38:53 +0530 Subject: [PATCH 02/11] Update platform-pages.ts --- apps/web/src/lib/seo/platform-pages.ts | 1 - 1 file changed, 1 deletion(-) 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', From 40b7dcdf40b82a3c79cc01ba063c8b8baca1afe4 Mon Sep 17 00:00:00 2001 From: AKHIL Date: Sat, 15 Aug 2026 21:47:01 +0530 Subject: [PATCH 03/11] Update header.tsx --- apps/web/src/components/header.tsx | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index d8c2787b..b4bdd551 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -49,8 +49,6 @@ type HeaderProps = { export function Header({ showThemeToggle = true }: HeaderProps) { 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); @@ -170,12 +168,6 @@ export function Header({ showThemeToggle = true }: HeaderProps) { {/* Desktop Actions */}
{showThemeToggle ? : null} - - {cta.label} -
@@ -231,25 +223,9 @@ 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 ? ( Date: Sat, 15 Aug 2026 21:52:51 +0530 Subject: [PATCH 04/11] Update header.tsx --- apps/web/src/components/header.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/apps/web/src/components/header.tsx b/apps/web/src/components/header.tsx index b4bdd551..732ca398 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -3,7 +3,6 @@ import { useState, useEffect, type ElementType } from "react"; import Link from "next/link"; import { Menu, X } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { ModeToggle } from "./modeToggle"; import { Logo } from "./logo"; import { motion, AnimatePresence } from "framer-motion"; import { cn } from "@/lib/utils"; @@ -41,12 +40,7 @@ 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); @@ -164,11 +158,6 @@ export function Header({ showThemeToggle = true }: HeaderProps) { )} - - {/* Desktop Actions */} -
- {showThemeToggle ? : null} -
From 37f9986e48dff518415f4341df8c0a02c68bf54f Mon Sep 17 00:00:00 2001 From: AKHIL Date: Sat, 15 Aug 2026 22:00:25 +0530 Subject: [PATCH 05/11] theme toggle removal --- apps/web/src/app/blog/[slug]/page.tsx | 2 +- apps/web/src/app/blog/page.tsx | 2 +- apps/web/src/app/changelog/page.tsx | 2 +- apps/web/src/app/compare/[slug]/page.tsx | 2 +- apps/web/src/app/page.tsx | 2 +- apps/web/src/app/tools/[slug]/page.tsx | 2 +- apps/web/src/app/tools/page.tsx | 2 +- apps/web/src/components/header.tsx | 15 ---- .../web/src/components/marketing-seo-page.tsx | 2 +- apps/web/src/components/modeToggle.tsx | 80 ------------------- 10 files changed, 8 insertions(+), 103 deletions(-) delete mode 100644 apps/web/src/components/modeToggle.tsx 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 732ca398..2ecccd58 100644 --- a/apps/web/src/components/header.tsx +++ b/apps/web/src/components/header.tsx @@ -211,21 +211,6 @@ export function Header() { ))} - - {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 ? ( - - ) : ( - - )} - - - ) -} From a08bef27c1fd51270b7b9b8bc9666e223f6fd711 Mon Sep 17 00:00:00 2001 From: AKHIL Date: Sat, 15 Aug 2026 22:21:46 +0530 Subject: [PATCH 06/11] added Sidebar --- apps/desktop-ui/messages/af.json | 17 + apps/desktop-ui/messages/ar.json | 17 + apps/desktop-ui/messages/ca.json | 17 + apps/desktop-ui/messages/cs.json | 17 + apps/desktop-ui/messages/da.json | 17 + apps/desktop-ui/messages/de.json | 17 + apps/desktop-ui/messages/el.json | 17 + apps/desktop-ui/messages/en.json | 17 + apps/desktop-ui/messages/es.json | 17 + apps/desktop-ui/messages/fa.json | 17 + apps/desktop-ui/messages/fr.json | 17 + apps/desktop-ui/messages/id.json | 17 + apps/desktop-ui/messages/it.json | 17 + apps/desktop-ui/messages/ja.json | 17 + apps/desktop-ui/messages/ko.json | 17 + apps/desktop-ui/messages/ms.json | 17 + apps/desktop-ui/messages/nb.json | 17 + apps/desktop-ui/messages/nl.json | 17 + apps/desktop-ui/messages/pl.json | 17 + apps/desktop-ui/messages/pt-BR.json | 17 + apps/desktop-ui/messages/pt.json | 17 + apps/desktop-ui/messages/ru.json | 17 + apps/desktop-ui/messages/sv.json | 17 + apps/desktop-ui/messages/tr.json | 17 + apps/desktop-ui/messages/uk.json | 17 + apps/desktop-ui/messages/vi.json | 17 + apps/desktop-ui/messages/zh.json | 17 + apps/desktop-ui/src/app/app/api-keys/page.tsx | 23 +- .../src/app/app/data-explorer/page.tsx | 56 +- .../src/app/app/environment-manager/page.tsx | 20 +- apps/desktop-ui/src/app/app/layout.tsx | 27 +- .../app/app/notes/context/NotesContext.tsx | 7 +- .../src/app/app/notes/notes-client-layout.tsx | 76 +- .../src/app/app/password-manager/page.tsx | 32 +- apps/desktop-ui/src/app/app/s3-drive/page.tsx | 17 +- .../src/app/app/to-do/TaskContainer.tsx | 66 +- .../to-do/utils/__tests__/statusOrder.test.ts | 51 ++ .../src/app/app/to-do/utils/statusOrder.ts | 36 + .../src/components/api-client/api-client.tsx | 63 +- .../collections/collections-sidebar.tsx | 84 +- .../components/api-key-vault/api-key-list.tsx | 46 +- .../bookmarks/bookmarks-manager.tsx | 193 ++--- .../data-explorer/unified-sidebar.tsx | 36 +- .../environment-set-list.tsx | 51 +- apps/desktop-ui/src/components/mobile-nav.tsx | 28 +- .../src/components/notes/NotesSidebar.tsx | 63 +- .../password-manager/password-list.tsx | 86 +- .../components/s3-drive/bucket-sidebar.tsx | 57 +- .../src/components/sidebar/app-sidebar.tsx | 82 -- .../src/components/sidebar/client-layout.tsx | 18 +- .../src/components/sidebar/nav-group.tsx | 408 ---------- .../snippet-manager/snippet-manager-tool.tsx | 93 +-- .../src/components/tools/tool-sidebar.tsx | 262 ++++++ apps/desktop-ui/src/components/ui/sidebar.tsx | 768 ------------------ apps/desktop-ui/src/lib/tool-categories.ts | 1 + .../__tests__/tool-sidebar-store.test.ts | 36 + .../src/store/tool-sidebar-store.ts | 25 + 57 files changed, 1334 insertions(+), 1936 deletions(-) create mode 100644 apps/desktop-ui/src/app/app/to-do/utils/__tests__/statusOrder.test.ts create mode 100644 apps/desktop-ui/src/app/app/to-do/utils/statusOrder.ts delete mode 100644 apps/desktop-ui/src/components/sidebar/app-sidebar.tsx delete mode 100644 apps/desktop-ui/src/components/sidebar/nav-group.tsx create mode 100644 apps/desktop-ui/src/components/tools/tool-sidebar.tsx delete mode 100644 apps/desktop-ui/src/components/ui/sidebar.tsx create mode 100644 apps/desktop-ui/src/store/__tests__/tool-sidebar-store.test.ts create mode 100644 apps/desktop-ui/src/store/tool-sidebar-store.ts diff --git a/apps/desktop-ui/messages/af.json b/apps/desktop-ui/messages/af.json index 1223018e..aa448f90 100644 --- a/apps/desktop-ui/messages/af.json +++ b/apps/desktop-ui/messages/af.json @@ -709,6 +709,7 @@ "all": "Alles", "total": "Totaal", "allProjects": "Alle projekte", + "projectsHeading": "Projekte", "export": "Uitvoer", "kanban": "Kanban", "list": "Lys", @@ -966,6 +967,16 @@ "emptyHintMobile": "Tik die +-knoppie om jou eerste wagwoord by te voeg", "emptyHintDesktop": "Jy het nog geen wagwoorde gestoor nie. Klik die \"Voeg wagwoord by\"-knoppie hierbo om te begin.", "addPassword": "Voeg wagwoord by", + "title": "Wagwoorde", + "filters": { + "heading": "Kluisgesondheid", + "all": "Alle wagwoorde", + "weak": "Swak", + "reused": "Hergebruik", + "noUrl": "Geen URL", + "tagsHeading": "Etikette", + "allTags": "Alle etikette" + }, "searchPlaceholderMobile": "Soek in jou kluis...", "searchPlaceholderDesktop": "Soek kluis...", "importExport": "Invoer / uitvoer", @@ -1161,6 +1172,8 @@ "emptyHint": "Skep 'n stel vir elke projek en omgewing (bv. produksie API-sleutels). Alles word in jou blaaier geënkripteer voor oplaai.", "searchPlaceholder": "Soek projek, omgewing, etikette, notas…", "tagFilterPlaceholder": "Filter volgens etiket", + "allProjects": "Alle projekte", + "projectsHeading": "Projekte", "variableCount": "{count, plural, one {# veranderlike} other {# veranderlikes}}", "updatedAgo": "Bygewerk {time}", "copyDotEnv": "Kopieer .env", @@ -5106,6 +5119,10 @@ "ToolPageHeader": { "offline": "100% aflyn" }, + "ToolSidebar": { + "show": "Wys sybalk", + "hide": "Versteek sybalk" + }, "DesktopUpdate": { "title": "MyDevTools word bygewerk", "downloading": "Opdatering word afgelaai…", diff --git a/apps/desktop-ui/messages/ar.json b/apps/desktop-ui/messages/ar.json index 38d21da9..3721af91 100644 --- a/apps/desktop-ui/messages/ar.json +++ b/apps/desktop-ui/messages/ar.json @@ -709,6 +709,7 @@ "all": "الكل", "total": "الإجمالي", "allProjects": "كل المشاريع", + "projectsHeading": "المشاريع", "export": "تصدير", "kanban": "كانبان", "list": "قائمة", @@ -966,6 +967,16 @@ "emptyHintMobile": "اضغط زر + لإضافة أول كلمة مرور", "emptyHintDesktop": "لم تخزّن كلمات مرور بعد. انقر \"إضافة كلمة مرور\" أعلاه للبدء.", "addPassword": "إضافة كلمة مرور", + "title": "كلمات المرور", + "filters": { + "heading": "سلامة الخزنة", + "all": "كل كلمات المرور", + "weak": "ضعيفة", + "reused": "مُعاد استخدامها", + "noUrl": "بدون رابط", + "tagsHeading": "الوسوم", + "allTags": "كل الوسوم" + }, "searchPlaceholderMobile": "ابحث في خزنتك...", "searchPlaceholderDesktop": "ابحث في الخزنة...", "importExport": "استيراد / تصدير", @@ -1161,6 +1172,8 @@ "emptyHint": "أنشئ مجموعة لكل مشروع وبيئة (مثل مفاتيح واجهة الإنتاج). يُشفّر كل شيء في متصفحك قبل الرفع.", "searchPlaceholder": "ابحث في المشروع والبيئة والوسوم والملاحظات…", "tagFilterPlaceholder": "تصفية حسب الوسم", + "allProjects": "كل المشاريع", + "projectsHeading": "المشاريع", "variableCount": "{count, plural, one {# متغير} other {# متغيرات}}", "updatedAgo": "عُدّل {time}", "copyDotEnv": "نسخ .env", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "100% دون اتصال" }, + "ToolSidebar": { + "show": "إظهار الشريط الجانبي", + "hide": "إخفاء الشريط الجانبي" + }, "DesktopUpdate": { "title": "جارٍ تحديث MyDevTools", "downloading": "جارٍ تنزيل التحديث…", diff --git a/apps/desktop-ui/messages/ca.json b/apps/desktop-ui/messages/ca.json index 44bedc74..31509047 100644 --- a/apps/desktop-ui/messages/ca.json +++ b/apps/desktop-ui/messages/ca.json @@ -709,6 +709,7 @@ "all": "Totes", "total": "Total", "allProjects": "Tots els projectes", + "projectsHeading": "Projectes", "export": "Exporta", "kanban": "Kanban", "list": "Llista", @@ -966,6 +967,16 @@ "emptyHintMobile": "Toca el botó + per afegir la primera contrasenya", "emptyHintDesktop": "Encara no has desat cap contrasenya. Fes clic a «Afegeix contrasenya» a dalt per començar.", "addPassword": "Afegeix contrasenya", + "title": "Contrasenyes", + "filters": { + "heading": "Salut de la caixa forta", + "all": "Totes les contrasenyes", + "weak": "Febles", + "reused": "Reutilitzades", + "noUrl": "Sense URL", + "tagsHeading": "Etiquetes", + "allTags": "Totes les etiquetes" + }, "searchPlaceholderMobile": "Cerca al coffre...", "searchPlaceholderDesktop": "Cerca al coffre...", "importExport": "Importa / exporta", @@ -1161,6 +1172,8 @@ "emptyHint": "Crea un conjunt per a cada projecte i entorn (p. ex. claus d'API de producció). Tot es xifra al navegador abans de pujar-lo.", "searchPlaceholder": "Cerca projecte, entorn, etiquetes, notes…", "tagFilterPlaceholder": "Filtra per etiqueta", + "allProjects": "Tots els projectes", + "projectsHeading": "Projectes", "variableCount": "{count, plural, one {# variable} other {# variables}}", "updatedAgo": "Actualitzat {time}", "copyDotEnv": "Copia .env", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "100% fora de línia" }, + "ToolSidebar": { + "show": "Mostra la barra lateral", + "hide": "Amaga la barra lateral" + }, "DesktopUpdate": { "title": "S'està actualitzant MyDevTools", "downloading": "Baixant l'actualització…", diff --git a/apps/desktop-ui/messages/cs.json b/apps/desktop-ui/messages/cs.json index 28dc5e22..3794e777 100644 --- a/apps/desktop-ui/messages/cs.json +++ b/apps/desktop-ui/messages/cs.json @@ -709,6 +709,7 @@ "all": "Vše", "total": "Celkem", "allProjects": "Všechny projekty", + "projectsHeading": "Projekty", "export": "Export", "kanban": "Kanban", "list": "Seznam", @@ -966,6 +967,16 @@ "emptyHintMobile": "Klepněte na + a přidejte první heslo", "emptyHintDesktop": "Zatím nemáte uložená žádná hesla. Začněte tlačítkem „Přidat heslo“ výše.", "addPassword": "Přidat heslo", + "title": "Hesla", + "filters": { + "heading": "Stav trezoru", + "all": "Všechna hesla", + "weak": "Slabá", + "reused": "Opakovaná", + "noUrl": "Bez URL", + "tagsHeading": "Štítky", + "allTags": "Všechny štítky" + }, "searchPlaceholderMobile": "Hledat v trezoru...", "searchPlaceholderDesktop": "Hledat v trezoru...", "importExport": "Import / export", @@ -1161,6 +1172,8 @@ "emptyHint": "Vytvořte sadu pro každý projekt a prostředí (např. produkční API klíče). Vše se před nahráním šifruje v prohlížeči.", "searchPlaceholder": "Hledat projekt, prostředí, značky, poznámky…", "tagFilterPlaceholder": "Filtrovat podle značky", + "allProjects": "Všechny projekty", + "projectsHeading": "Projekty", "variableCount": "{count, plural, one {# proměnná} few {# proměnné} other {# proměnných}}", "updatedAgo": "Aktualizováno {time}", "copyDotEnv": "Kopírovat .env", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "100% offline" }, + "ToolSidebar": { + "show": "Zobrazit postranní panel", + "hide": "Skrýt postranní panel" + }, "DesktopUpdate": { "title": "Aktualizace MyDevTools", "downloading": "Stahování aktualizace…", diff --git a/apps/desktop-ui/messages/da.json b/apps/desktop-ui/messages/da.json index 5e24bb60..cce90a07 100644 --- a/apps/desktop-ui/messages/da.json +++ b/apps/desktop-ui/messages/da.json @@ -709,6 +709,7 @@ "all": "Alle", "total": "Total", "allProjects": "Alle projekter", + "projectsHeading": "Projekter", "export": "Eksportere", "kanban": "Kanban", "list": "Liste", @@ -966,6 +967,16 @@ "emptyHintMobile": "Tryk på knappen + for at tilføje din første adgangskode", "emptyHintDesktop": "Du har ikke gemt nogen adgangskoder endnu. Klik på knappen \"Tilføj adgangskode\" ovenfor for at komme i gang.", "addPassword": "Tilføj adgangskode", + "title": "Adgangskoder", + "filters": { + "heading": "Boksens tilstand", + "all": "Alle adgangskoder", + "weak": "Svage", + "reused": "Genbrugte", + "noUrl": "Ingen URL", + "tagsHeading": "Tags", + "allTags": "Alle tags" + }, "searchPlaceholderMobile": "Søg i din boks...", "searchPlaceholderDesktop": "Søg i boksen...", "importExport": "Import/eksport", @@ -1161,6 +1172,8 @@ "emptyHint": "Opret et sæt for hvert projekt og miljø (f.eks. produktions-API-nøgler). Alt krypteres i din browser før upload.", "searchPlaceholder": "Søg projekt, miljø, tags, noter…", "tagFilterPlaceholder": "Filtrér efter tag", + "allProjects": "Alle projekter", + "projectsHeading": "Projekter", "variableCount": "{count, plural, one {# variabel} other {# variabler}}", "updatedAgo": "Opdateret {time}", "copyDotEnv": "Kopiér .env", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "100 % offline" }, + "ToolSidebar": { + "show": "Vis sidepanel", + "hide": "Skjul sidepanel" + }, "DesktopUpdate": { "title": "Opdaterer MyDevTools", "downloading": "Downloader opdatering…", diff --git a/apps/desktop-ui/messages/de.json b/apps/desktop-ui/messages/de.json index b36a0801..2f5636be 100644 --- a/apps/desktop-ui/messages/de.json +++ b/apps/desktop-ui/messages/de.json @@ -709,6 +709,7 @@ "all": "Alle", "total": "Gesamt", "allProjects": "Alle Projekte", + "projectsHeading": "Projekte", "export": "Exportieren", "kanban": "Kanban", "list": "Liste", @@ -966,6 +967,16 @@ "emptyHintMobile": "Tippen Sie auf +, um Ihr erstes Passwort hinzuzufügen", "emptyHintDesktop": "Noch keine Passwörter. Klicken Sie oben auf „Passwort hinzufügen“.", "addPassword": "Passwort hinzufügen", + "title": "Passwörter", + "filters": { + "heading": "Tresor-Status", + "all": "Alle Passwörter", + "weak": "Schwach", + "reused": "Mehrfach genutzt", + "noUrl": "Ohne URL", + "tagsHeading": "Tags", + "allTags": "Alle Tags" + }, "searchPlaceholderMobile": "Tresor durchsuchen...", "searchPlaceholderDesktop": "Tresor durchsuchen...", "importExport": "Import / Export", @@ -1161,6 +1172,8 @@ "emptyHint": "Legen Sie ein Set pro Projekt und Umgebung an (z. B. Produktions-API-Schlüssel). Alles wird vor dem Upload im Browser verschlüsselt.", "searchPlaceholder": "Projekt, Umgebung, Tags, Notizen durchsuchen…", "tagFilterPlaceholder": "Nach Tag filtern", + "allProjects": "Alle Projekte", + "projectsHeading": "Projekte", "variableCount": "{count, plural, one {# Variable} other {# Variablen}}", "updatedAgo": "Aktualisiert {time}", "copyDotEnv": ".env kopieren", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "100 % offline" }, + "ToolSidebar": { + "show": "Seitenleiste einblenden", + "hide": "Seitenleiste ausblenden" + }, "DesktopUpdate": { "title": "MyDevTools wird aktualisiert", "downloading": "Update wird heruntergeladen…", diff --git a/apps/desktop-ui/messages/el.json b/apps/desktop-ui/messages/el.json index dfbb9a66..980da36a 100644 --- a/apps/desktop-ui/messages/el.json +++ b/apps/desktop-ui/messages/el.json @@ -709,6 +709,7 @@ "all": "Ολοι", "total": "Σύνολο", "allProjects": "Όλα τα Έργα", + "projectsHeading": "Έργα", "export": "Εξαγωγή", "kanban": "Kanban", "list": "Λίστα", @@ -966,6 +967,16 @@ "emptyHintMobile": "Πατήστε το κουμπί + για να προσθέσετε τον πρώτο σας κωδικό πρόσβασης", "emptyHintDesktop": "Δεν έχετε αποθηκεύσει ακόμα κανέναν κωδικό πρόσβασης. Κάντε κλικ στο κουμπί \"Προσθήκη κωδικού πρόσβασης\" παραπάνω για να ξεκινήσετε.", "addPassword": "Προσθήκη κωδικού πρόσβασης", + "title": "Κωδικοί", + "filters": { + "heading": "Υγεία θησαυροφυλακίου", + "all": "Όλοι οι κωδικοί", + "weak": "Αδύναμοι", + "reused": "Επαναχρησιμοποιημένοι", + "noUrl": "Χωρίς URL", + "tagsHeading": "Ετικέτες", + "allTags": "Όλες οι ετικέτες" + }, "searchPlaceholderMobile": "Αναζητήστε το θησαυροφυλάκιό σας...", "searchPlaceholderDesktop": "Θησαυροφυλάκιο αναζήτησης...", "importExport": "Εισαγωγή / εξαγωγή", @@ -1161,6 +1172,8 @@ "emptyHint": "Δημιουργήστε ένα σύνολο για κάθε έργο και περιβάλλον (π.χ. κλειδιά API παραγωγής). Όλα κρυπτογραφούνται στο πρόγραμμα περιήγησης πριν την αποστολή.", "searchPlaceholder": "Αναζήτηση έργου, περιβάλλοντος, ετικετών, σημειώσεων…", "tagFilterPlaceholder": "Φιλτράρισμα κατά ετικέτα", + "allProjects": "Όλα τα έργα", + "projectsHeading": "Έργα", "variableCount": "{count, plural, one {# μεταβλητή} other {# μεταβλητές}}", "updatedAgo": "Ενημερώθηκε {time}", "copyDotEnv": "Αντιγραφή .env", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "100% εκτός σύνδεσης" }, + "ToolSidebar": { + "show": "Εμφάνιση πλαϊνής μπάρας", + "hide": "Απόκρυψη πλαϊνής μπάρας" + }, "DesktopUpdate": { "title": "Ενημέρωση MyDevTools", "downloading": "Λήψη ενημέρωσης…", diff --git a/apps/desktop-ui/messages/en.json b/apps/desktop-ui/messages/en.json index ee10a0be..19f82f1e 100644 --- a/apps/desktop-ui/messages/en.json +++ b/apps/desktop-ui/messages/en.json @@ -709,6 +709,7 @@ "all": "All", "total": "Total", "allProjects": "All Projects", + "projectsHeading": "Projects", "export": "Export", "kanban": "Kanban", "list": "List", @@ -966,6 +967,16 @@ "emptyHintMobile": "Tap the + button to add your first password", "emptyHintDesktop": "You have not stored any passwords yet. Click the \"Add password\" button above to get started.", "addPassword": "Add password", + "title": "Passwords", + "filters": { + "heading": "Vault health", + "all": "All passwords", + "weak": "Weak", + "reused": "Reused", + "noUrl": "No URL", + "tagsHeading": "Tags", + "allTags": "All tags" + }, "searchPlaceholderMobile": "Search your vault...", "searchPlaceholderDesktop": "Search vault...", "importExport": "Import / export", @@ -1161,6 +1172,8 @@ "emptyHint": "Create a set for each project and environment (e.g. production API keys). Everything is encrypted in your browser before upload.", "searchPlaceholder": "Search project, environment, tags, notes...", "tagFilterPlaceholder": "Filter by tag", + "allProjects": "All projects", + "projectsHeading": "Projects", "variableCount": "{count, plural, one {# variable} other {# variables}}", "updatedAgo": "Updated {time}", "copyDotEnv": "Copy .env", @@ -5116,6 +5129,10 @@ "ToolPageHeader": { "offline": "100% offline" }, + "ToolSidebar": { + "show": "Show sidebar", + "hide": "Hide sidebar" + }, "DesktopUpdate": { "title": "Updating MyDevTools", "downloading": "Downloading update…", diff --git a/apps/desktop-ui/messages/es.json b/apps/desktop-ui/messages/es.json index 247e3ca5..decea828 100644 --- a/apps/desktop-ui/messages/es.json +++ b/apps/desktop-ui/messages/es.json @@ -709,6 +709,7 @@ "all": "Todas", "total": "Total", "allProjects": "Todos los proyectos", + "projectsHeading": "Proyectos", "export": "Exportar", "kanban": "Kanban", "list": "Lista", @@ -966,6 +967,16 @@ "emptyHintMobile": "Toca el botón + para añadir tu primera contraseña", "emptyHintDesktop": "Aún no has guardado contraseñas. Haz clic en \"Añadir contraseña\" arriba para empezar.", "addPassword": "Añadir contraseña", + "title": "Contraseñas", + "filters": { + "heading": "Estado de la bóveda", + "all": "Todas las contraseñas", + "weak": "Débiles", + "reused": "Reutilizadas", + "noUrl": "Sin URL", + "tagsHeading": "Etiquetas", + "allTags": "Todas las etiquetas" + }, "searchPlaceholderMobile": "Buscar en tu bóveda...", "searchPlaceholderDesktop": "Buscar en la bóveda...", "importExport": "Importar / exportar", @@ -1161,6 +1172,8 @@ "emptyHint": "Crea un conjunto por proyecto y entorno (p. ej. claves de API de producción). Todo se cifra en tu navegador antes de subirlo.", "searchPlaceholder": "Buscar proyecto, entorno, etiquetas, notas…", "tagFilterPlaceholder": "Filtrar por etiqueta", + "allProjects": "Todos los proyectos", + "projectsHeading": "Proyectos", "variableCount": "{count, plural, one {# variable} other {# variables}}", "updatedAgo": "Actualizado {time}", "copyDotEnv": "Copiar .env", @@ -5106,6 +5119,10 @@ "ToolPageHeader": { "offline": "100% sin conexión" }, + "ToolSidebar": { + "show": "Mostrar barra lateral", + "hide": "Ocultar barra lateral" + }, "DesktopUpdate": { "title": "Actualizando MyDevTools", "downloading": "Descargando actualización…", diff --git a/apps/desktop-ui/messages/fa.json b/apps/desktop-ui/messages/fa.json index fd960552..d2cdc59a 100644 --- a/apps/desktop-ui/messages/fa.json +++ b/apps/desktop-ui/messages/fa.json @@ -709,6 +709,7 @@ "all": "همه", "total": "مجموع", "allProjects": "همه پروژه ها", + "projectsHeading": "پروژه‌ها", "export": "صادرات", "kanban": "کانبان", "list": "فهرست کنید", @@ -966,6 +967,16 @@ "emptyHintMobile": "روی دکمه + ضربه بزنید تا اولین رمز عبور خود را اضافه کنید", "emptyHintDesktop": "شما هنوز هیچ رمز عبوری را ذخیره نکرده اید. برای شروع روی دکمه «افزودن رمز عبور» در بالا کلیک کنید.", "addPassword": "اضافه کردن رمز عبور", + "title": "گذرواژه‌ها", + "filters": { + "heading": "سلامت گاوصندوق", + "all": "همه گذرواژه‌ها", + "weak": "ضعیف", + "reused": "تکراری", + "noUrl": "بدون نشانی", + "tagsHeading": "برچسب‌ها", + "allTags": "همه برچسب‌ها" + }, "searchPlaceholderMobile": "خزانه خود را جستجو کنید ...", "searchPlaceholderDesktop": "صندوق جستجو...", "importExport": "واردات / صادرات", @@ -1161,6 +1172,8 @@ "emptyHint": "برای هر پروژه و محیط یک مجموعه بسازید (مثلاً کلیدهای API تولید). همه قبل از آپلود در مرورگر شما رمزگذاری می‌شوند.", "searchPlaceholder": "جستجوی پروژه، محیط، برچسب، یادداشت…", "tagFilterPlaceholder": "فیلتر با برچسب", + "allProjects": "همه پروژه‌ها", + "projectsHeading": "پروژه‌ها", "variableCount": "{count, plural, one {# متغیر} other {# متغیر}}", "updatedAgo": "به‌روزرسانی {time}", "copyDotEnv": "کپی .env", @@ -5106,6 +5119,10 @@ "ToolPageHeader": { "offline": "۱۰۰٪ آفلاین" }, + "ToolSidebar": { + "show": "نمایش نوار کناری", + "hide": "پنهان کردن نوار کناری" + }, "DesktopUpdate": { "title": "در حال به‌روزرسانی MyDevTools", "downloading": "در حال دانلود به‌روزرسانی…", diff --git a/apps/desktop-ui/messages/fr.json b/apps/desktop-ui/messages/fr.json index dd2b7081..39f13772 100644 --- a/apps/desktop-ui/messages/fr.json +++ b/apps/desktop-ui/messages/fr.json @@ -709,6 +709,7 @@ "all": "Toutes", "total": "Total", "allProjects": "Tous les projets", + "projectsHeading": "Projets", "export": "Exporter", "kanban": "Kanban", "list": "Liste", @@ -966,6 +967,16 @@ "emptyHintMobile": "Appuyez sur + pour ajouter votre premier mot de passe", "emptyHintDesktop": "Aucun mot de passe enregistré. Cliquez sur « Ajouter un mot de passe » ci-dessus.", "addPassword": "Ajouter un mot de passe", + "title": "Mots de passe", + "filters": { + "heading": "État du coffre", + "all": "Tous les mots de passe", + "weak": "Faibles", + "reused": "Réutilisés", + "noUrl": "Sans URL", + "tagsHeading": "Étiquettes", + "allTags": "Toutes les étiquettes" + }, "searchPlaceholderMobile": "Rechercher dans le coffre…", "searchPlaceholderDesktop": "Rechercher…", "importExport": "Importer / exporter", @@ -1161,6 +1172,8 @@ "emptyHint": "Créez un jeu pour chaque projet et environnement (ex. clés API de production). Tout est chiffré dans votre navigateur avant l'envoi.", "searchPlaceholder": "Rechercher projet, environnement, étiquettes, notes…", "tagFilterPlaceholder": "Filtrer par étiquette", + "allProjects": "Tous les projets", + "projectsHeading": "Projets", "variableCount": "{count, plural, one {# variable} other {# variables}}", "updatedAgo": "Mis à jour {time}", "copyDotEnv": "Copier .env", @@ -5106,6 +5119,10 @@ "ToolPageHeader": { "offline": "100 % hors ligne" }, + "ToolSidebar": { + "show": "Afficher la barre latérale", + "hide": "Masquer la barre latérale" + }, "DesktopUpdate": { "title": "Mise à jour de MyDevTools", "downloading": "Téléchargement de la mise à jour…", diff --git a/apps/desktop-ui/messages/id.json b/apps/desktop-ui/messages/id.json index f04b3d09..4bc081a7 100644 --- a/apps/desktop-ui/messages/id.json +++ b/apps/desktop-ui/messages/id.json @@ -709,6 +709,7 @@ "all": "Semua", "total": "Total", "allProjects": "Semua Projek", + "projectsHeading": "Proyek", "export": "Ekspor", "kanban": "Kanban", "list": "Dunia", @@ -966,6 +967,16 @@ "emptyHintMobile": "Ketuk tombol + untuk menambahkan kata sandi pertama Anda", "emptyHintDesktop": "Anda belum menyimpan kata sandi apa pun. Klik tombol \"Tambahkan kata sandi\" di atas untuk memulai.", "addPassword": "Tambah Kata Sandi", + "title": "Kata sandi", + "filters": { + "heading": "Kesehatan brankas", + "all": "Semua kata sandi", + "weak": "Lemah", + "reused": "Dipakai ulang", + "noUrl": "Tanpa URL", + "tagsHeading": "Tag", + "allTags": "Semua tag" + }, "searchPlaceholderMobile": "Cari brankas Anda...", "searchPlaceholderDesktop": "Cari brankas...", "importExport": "Impor/Ekspor", @@ -1161,6 +1172,8 @@ "emptyHint": "Buat set untuk setiap proyek dan lingkungan (mis. kunci API produksi). Semua dienkripsi di browser sebelum diunggah.", "searchPlaceholder": "Cari proyek, lingkungan, tag, catatan…", "tagFilterPlaceholder": "Saring menurut tag", + "allProjects": "Semua proyek", + "projectsHeading": "Proyek", "variableCount": "{count, plural, other {# variabel}}", "updatedAgo": "Diperbarui {time}", "copyDotEnv": "Salin .env", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "100% offline" }, + "ToolSidebar": { + "show": "Tampilkan bilah sisi", + "hide": "Sembunyikan bilah sisi" + }, "DesktopUpdate": { "title": "Memperbarui MyDevTools", "downloading": "Mengunduh pembaruan…", diff --git a/apps/desktop-ui/messages/it.json b/apps/desktop-ui/messages/it.json index a5d4201b..7b28eb2d 100644 --- a/apps/desktop-ui/messages/it.json +++ b/apps/desktop-ui/messages/it.json @@ -709,6 +709,7 @@ "all": "Tutto", "total": "Totale", "allProjects": "Tutti i progetti", + "projectsHeading": "Progetti", "export": "Esportare", "kanban": "Kanban", "list": "Lista", @@ -966,6 +967,16 @@ "emptyHintMobile": "Tocca il pulsante + per aggiungere la tua prima password", "emptyHintDesktop": "Non hai ancora memorizzato alcuna password. Fai clic sul pulsante \"Aggiungi password\" qui sopra per iniziare.", "addPassword": "Aggiungi password", + "title": "Password", + "filters": { + "heading": "Stato della cassaforte", + "all": "Tutte le password", + "weak": "Deboli", + "reused": "Riutilizzate", + "noUrl": "Senza URL", + "tagsHeading": "Tag", + "allTags": "Tutti i tag" + }, "searchPlaceholderMobile": "Cerca nel tuo caveau...", "searchPlaceholderDesktop": "Cerca nel caveau...", "importExport": "Importa/esporta", @@ -1161,6 +1172,8 @@ "emptyHint": "Crea un set per ogni progetto e ambiente (es. chiavi API di produzione). Tutto viene crittografato nel browser prima del caricamento.", "searchPlaceholder": "Cerca progetto, ambiente, tag, note…", "tagFilterPlaceholder": "Filtra per tag", + "allProjects": "Tutti i progetti", + "projectsHeading": "Progetti", "variableCount": "{count, plural, one {# variabile} other {# variabili}}", "updatedAgo": "Aggiornato {time}", "copyDotEnv": "Copia .env", @@ -5106,6 +5119,10 @@ "ToolPageHeader": { "offline": "100% offline" }, + "ToolSidebar": { + "show": "Mostra barra laterale", + "hide": "Nascondi barra laterale" + }, "DesktopUpdate": { "title": "Aggiornamento di MyDevTools", "downloading": "Download dell'aggiornamento…", diff --git a/apps/desktop-ui/messages/ja.json b/apps/desktop-ui/messages/ja.json index 049c0a75..67b09e7d 100644 --- a/apps/desktop-ui/messages/ja.json +++ b/apps/desktop-ui/messages/ja.json @@ -709,6 +709,7 @@ "all": "すべて", "total": "合計", "allProjects": "すべてのプロジェクト", + "projectsHeading": "プロジェクト", "export": "エクスポート", "kanban": "カンバン", "list": "リスト", @@ -966,6 +967,16 @@ "emptyHintMobile": "+ボタンをタップして最初のパスワードを追加してください", "emptyHintDesktop": "パスワードはまだ保存されていません。上の「パスワードを追加」ボタンをクリックして始めましょう。", "addPassword": "パスワードを追加", + "title": "パスワード", + "filters": { + "heading": "保管庫の状態", + "all": "すべてのパスワード", + "weak": "脆弱", + "reused": "使い回し", + "noUrl": "URLなし", + "tagsHeading": "タグ", + "allTags": "すべてのタグ" + }, "searchPlaceholderMobile": "ボールトを検索...", "searchPlaceholderDesktop": "ボールトを検索...", "importExport": "インポート / エクスポート", @@ -1161,6 +1172,8 @@ "emptyHint": "プロジェクトと環境ごとにセットを作成します(例: 本番 API キー)。アップロード前にブラウザで暗号化されます。", "searchPlaceholder": "プロジェクト・環境・タグ・メモを検索…", "tagFilterPlaceholder": "タグで絞り込み", + "allProjects": "すべてのプロジェクト", + "projectsHeading": "プロジェクト", "variableCount": "{count, plural, other {# 件の変数}}", "updatedAgo": "更新 {time}", "copyDotEnv": ".env をコピー", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "完全オフライン" }, + "ToolSidebar": { + "show": "サイドバーを表示", + "hide": "サイドバーを非表示" + }, "DesktopUpdate": { "title": "MyDevTools を更新しています", "downloading": "更新をダウンロードしています…", diff --git a/apps/desktop-ui/messages/ko.json b/apps/desktop-ui/messages/ko.json index 57c45243..55eebce7 100644 --- a/apps/desktop-ui/messages/ko.json +++ b/apps/desktop-ui/messages/ko.json @@ -709,6 +709,7 @@ "all": "전체", "total": "합계", "allProjects": "모든 프로젝트", + "projectsHeading": "프로젝트", "export": "내보내기", "kanban": "칸반", "list": "목록", @@ -966,6 +967,16 @@ "emptyHintMobile": "+ 버튼을 눌러 첫 번째 비밀번호를 추가하세요", "emptyHintDesktop": "아직 저장된 비밀번호가 없습니다. 위의 \"비밀번호 추가\" 버튼을 클릭하여 시작하세요.", "addPassword": "비밀번호 추가", + "title": "비밀번호", + "filters": { + "heading": "보관함 상태", + "all": "모든 비밀번호", + "weak": "취약", + "reused": "재사용", + "noUrl": "URL 없음", + "tagsHeading": "태그", + "allTags": "모든 태그" + }, "searchPlaceholderMobile": "볼트 검색...", "searchPlaceholderDesktop": "볼트 검색...", "importExport": "가져오기 / 내보내기", @@ -1161,6 +1172,8 @@ "emptyHint": "프로젝트와 환경마다 세트를 만드세요(예: 프로덕션 API 키). 업로드 전 브라우저에서 암호화됩니다.", "searchPlaceholder": "프로젝트, 환경, 태그, 메모 검색…", "tagFilterPlaceholder": "태그로 필터", + "allProjects": "모든 프로젝트", + "projectsHeading": "프로젝트", "variableCount": "{count, plural, other {#개 변수}}", "updatedAgo": "업데이트 {time}", "copyDotEnv": ".env 복사", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "100% 오프라인" }, + "ToolSidebar": { + "show": "사이드바 표시", + "hide": "사이드바 숨기기" + }, "DesktopUpdate": { "title": "MyDevTools 업데이트 중", "downloading": "업데이트 다운로드 중…", diff --git a/apps/desktop-ui/messages/ms.json b/apps/desktop-ui/messages/ms.json index 098a4975..64617f74 100644 --- a/apps/desktop-ui/messages/ms.json +++ b/apps/desktop-ui/messages/ms.json @@ -709,6 +709,7 @@ "all": "Semua", "total": "Jumlah", "allProjects": "Semua Projek", + "projectsHeading": "Projek", "export": "Eksport", "kanban": "Kanban", "list": "Senarai", @@ -966,6 +967,16 @@ "emptyHintMobile": "Ketik butang + untuk menambah kata laluan pertama anda", "emptyHintDesktop": "Anda belum menyimpan sebarang kata laluan. Klik butang \"Tambah kata laluan\" di atas untuk bermula.", "addPassword": "Tambah kata laluan", + "title": "Kata laluan", + "filters": { + "heading": "Kesihatan peti besi", + "all": "Semua kata laluan", + "weak": "Lemah", + "reused": "Diguna semula", + "noUrl": "Tiada URL", + "tagsHeading": "Tag", + "allTags": "Semua tag" + }, "searchPlaceholderMobile": "Cari peti besi anda...", "searchPlaceholderDesktop": "Cari peti besi...", "importExport": "Import / eksport", @@ -1161,6 +1172,8 @@ "emptyHint": "Cipta set untuk setiap projek dan persekitaran (cth. kunci API pengeluaran). Semua disulitkan dalam pelayar sebelum dimuat naik.", "searchPlaceholder": "Cari projek, persekitaran, teg, nota…", "tagFilterPlaceholder": "Tapis mengikut teg", + "allProjects": "Semua projek", + "projectsHeading": "Projek", "variableCount": "{count, plural, other {# pemboleh ubah}}", "updatedAgo": "Dikemas kini {time}", "copyDotEnv": "Salin .env", @@ -5106,6 +5119,10 @@ "ToolPageHeader": { "offline": "100% luar talian" }, + "ToolSidebar": { + "show": "Tunjukkan bar sisi", + "hide": "Sembunyikan bar sisi" + }, "DesktopUpdate": { "title": "Mengemas kini MyDevTools", "downloading": "Memuat turun kemas kini…", diff --git a/apps/desktop-ui/messages/nb.json b/apps/desktop-ui/messages/nb.json index ea489d08..1f6ae854 100644 --- a/apps/desktop-ui/messages/nb.json +++ b/apps/desktop-ui/messages/nb.json @@ -709,6 +709,7 @@ "all": "Alle", "total": "Totalt", "allProjects": "Alle prosjekter", + "projectsHeading": "Prosjekter", "export": "Eksporter", "kanban": "Kanban", "list": "Liste", @@ -966,6 +967,16 @@ "emptyHintMobile": "Trykk på +-knappen for å legge til ditt første passord", "emptyHintDesktop": "Du har ikke lagret noen passord ennå. Klikk på «Legg til passord»-knappen ovenfor for å komme i gang.", "addPassword": "Legg til passord", + "title": "Passord", + "filters": { + "heading": "Hvelvets tilstand", + "all": "Alle passord", + "weak": "Svake", + "reused": "Gjenbrukte", + "noUrl": "Ingen URL", + "tagsHeading": "Merkelapper", + "allTags": "Alle merkelapper" + }, "searchPlaceholderMobile": "Søk i hvelvet ditt...", "searchPlaceholderDesktop": "Søk i hvelv...", "importExport": "Importer / eksporter", @@ -1161,6 +1172,8 @@ "emptyHint": "Opprett et sett for hvert prosjekt og miljø (f.eks. produksjons-API-nøkler). Alt krypteres i nettleseren før opplasting.", "searchPlaceholder": "Søk prosjekt, miljø, tagger, notater…", "tagFilterPlaceholder": "Filtrer på tagg", + "allProjects": "Alle prosjekter", + "projectsHeading": "Prosjekter", "variableCount": "{count, plural, one {# variabel} other {# variabler}}", "updatedAgo": "Oppdatert {time}", "copyDotEnv": "Kopier .env", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "100 % offline" }, + "ToolSidebar": { + "show": "Vis sidepanel", + "hide": "Skjul sidepanel" + }, "DesktopUpdate": { "title": "Oppdaterer MyDevTools", "downloading": "Laster ned oppdatering…", diff --git a/apps/desktop-ui/messages/nl.json b/apps/desktop-ui/messages/nl.json index 4c09bf9c..557b7429 100644 --- a/apps/desktop-ui/messages/nl.json +++ b/apps/desktop-ui/messages/nl.json @@ -709,6 +709,7 @@ "all": "Alles", "total": "Totaal", "allProjects": "Alle projecten", + "projectsHeading": "Projecten", "export": "Exporteren", "kanban": "Kanban", "list": "Lijst", @@ -966,6 +967,16 @@ "emptyHintMobile": "Tik op de + knop om je eerste wachtwoord toe te voegen", "emptyHintDesktop": "Je hebt nog geen wachtwoorden opgeslagen. Klik op de knop \"Wachtwoord toevoegen\" hierboven om te beginnen.", "addPassword": "Wachtwoord toevoegen", + "title": "Wachtwoorden", + "filters": { + "heading": "Kluisstatus", + "all": "Alle wachtwoorden", + "weak": "Zwak", + "reused": "Hergebruikt", + "noUrl": "Geen URL", + "tagsHeading": "Tags", + "allTags": "Alle tags" + }, "searchPlaceholderMobile": "Doorzoek je kluis...", "searchPlaceholderDesktop": "Kluis doorzoeken...", "importExport": "Importeren / exporteren", @@ -1161,6 +1172,8 @@ "emptyHint": "Maak een set per project en omgeving (bijv. productie-API-sleutels). Alles wordt in uw browser versleuteld vóór upload.", "searchPlaceholder": "Zoek project, omgeving, tags, notities…", "tagFilterPlaceholder": "Filter op tag", + "allProjects": "Alle projecten", + "projectsHeading": "Projecten", "variableCount": "{count, plural, one {# variabele} other {# variabelen}}", "updatedAgo": "Bijgewerkt {time}", "copyDotEnv": ".env kopiëren", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "100% offline" }, + "ToolSidebar": { + "show": "Zijbalk tonen", + "hide": "Zijbalk verbergen" + }, "DesktopUpdate": { "title": "MyDevTools wordt bijgewerkt", "downloading": "Update wordt gedownload…", diff --git a/apps/desktop-ui/messages/pl.json b/apps/desktop-ui/messages/pl.json index a2e52692..8d11d849 100644 --- a/apps/desktop-ui/messages/pl.json +++ b/apps/desktop-ui/messages/pl.json @@ -709,6 +709,7 @@ "all": "All", "total": "Total", "allProjects": "All Projects", + "projectsHeading": "Projekty", "export": "Export", "kanban": "Kanban", "list": "List", @@ -966,6 +967,16 @@ "emptyHintMobile": "Tap the + button to add your first password", "emptyHintDesktop": "You have not stored any passwords yet. Click the \"Add password\" button above to get started.", "addPassword": "Add password", + "title": "Hasła", + "filters": { + "heading": "Stan sejfu", + "all": "Wszystkie hasła", + "weak": "Słabe", + "reused": "Powtórzone", + "noUrl": "Bez adresu URL", + "tagsHeading": "Tagi", + "allTags": "Wszystkie tagi" + }, "searchPlaceholderMobile": "Search your vault...", "searchPlaceholderDesktop": "Search vault...", "importExport": "Import / export", @@ -1161,6 +1172,8 @@ "emptyHint": "Utwórz zestaw dla każdego projektu i środowiska (np. klucze API produkcji). Wszystko jest szyfrowane w przeglądarce przed wysłaniem.", "searchPlaceholder": "Szukaj projektu, środowiska, tagów, notatek…", "tagFilterPlaceholder": "Filtruj według tagu", + "allProjects": "Wszystkie projekty", + "projectsHeading": "Projekty", "variableCount": "{count, plural, one {# zmienna} few {# zmienne} other {# zmiennych}}", "updatedAgo": "Zaktualizowano {time}", "copyDotEnv": "Kopiuj .env", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "100% offline" }, + "ToolSidebar": { + "show": "Pokaż pasek boczny", + "hide": "Ukryj pasek boczny" + }, "DesktopUpdate": { "title": "Aktualizowanie MyDevTools", "downloading": "Pobieranie aktualizacji…", diff --git a/apps/desktop-ui/messages/pt-BR.json b/apps/desktop-ui/messages/pt-BR.json index b683c90e..55a8c1bb 100644 --- a/apps/desktop-ui/messages/pt-BR.json +++ b/apps/desktop-ui/messages/pt-BR.json @@ -709,6 +709,7 @@ "all": "Todas", "total": "Total", "allProjects": "Todos os Projetos", + "projectsHeading": "Projetos", "export": "Exportar", "kanban": "Kanban", "list": "Lista", @@ -966,6 +967,16 @@ "emptyHintMobile": "Toque no botão + para adicionar sua primeira senha", "emptyHintDesktop": "Você ainda não armazenou nenhuma senha. Clique no botão \"Adicionar senha\" acima para começar.", "addPassword": "Adicionar senha", + "title": "Senhas", + "filters": { + "heading": "Saúde do cofre", + "all": "Todas as senhas", + "weak": "Fracas", + "reused": "Reutilizadas", + "noUrl": "Sem URL", + "tagsHeading": "Tags", + "allTags": "Todas as tags" + }, "searchPlaceholderMobile": "Buscar no cofre...", "searchPlaceholderDesktop": "Buscar no cofre...", "importExport": "Importar / exportar", @@ -1161,6 +1172,8 @@ "emptyHint": "Crie um conjunto para cada projeto e ambiente (ex.: chaves de API de produção). Tudo é criptografado no navegador antes do envio.", "searchPlaceholder": "Buscar projeto, ambiente, tags, notas…", "tagFilterPlaceholder": "Filtrar por tag", + "allProjects": "Todos os projetos", + "projectsHeading": "Projetos", "variableCount": "{count, plural, one {# variável} other {# variáveis}}", "updatedAgo": "Atualizado {time}", "copyDotEnv": "Copiar .env", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "100% offline" }, + "ToolSidebar": { + "show": "Mostrar barra lateral", + "hide": "Ocultar barra lateral" + }, "DesktopUpdate": { "title": "Atualizando o MyDevTools", "downloading": "Baixando atualização…", diff --git a/apps/desktop-ui/messages/pt.json b/apps/desktop-ui/messages/pt.json index 0ebe342f..85d6e1d9 100644 --- a/apps/desktop-ui/messages/pt.json +++ b/apps/desktop-ui/messages/pt.json @@ -709,6 +709,7 @@ "all": "Todas", "total": "Total", "allProjects": "Todos os Projetos", + "projectsHeading": "Projetos", "export": "Exportar", "kanban": "Kanban", "list": "Lista", @@ -966,6 +967,16 @@ "emptyHintMobile": "Toca no botão + para adicionar a tua primeira palavra-passe", "emptyHintDesktop": "Ainda não armazenaste nenhuma palavra-passe. Clica no botão \"Adicionar palavra-passe\" acima para começar.", "addPassword": "Adicionar palavra-passe", + "title": "Palavras-passe", + "filters": { + "heading": "Estado do cofre", + "all": "Todas as palavras-passe", + "weak": "Fracas", + "reused": "Reutilizadas", + "noUrl": "Sem URL", + "tagsHeading": "Etiquetas", + "allTags": "Todas as etiquetas" + }, "searchPlaceholderMobile": "Pesquisar no cofre...", "searchPlaceholderDesktop": "Pesquisar no cofre...", "importExport": "Importar / exportar", @@ -1161,6 +1172,8 @@ "emptyHint": "Crie um conjunto por projeto e ambiente (ex.: chaves API de produção). Tudo é encriptado no navegador antes do envio.", "searchPlaceholder": "Pesquisar projeto, ambiente, etiquetas, notas…", "tagFilterPlaceholder": "Filtrar por etiqueta", + "allProjects": "Todos os projetos", + "projectsHeading": "Projetos", "variableCount": "{count, plural, one {# variável} other {# variáveis}}", "updatedAgo": "Atualizado {time}", "copyDotEnv": "Copiar .env", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "100% offline" }, + "ToolSidebar": { + "show": "Mostrar barra lateral", + "hide": "Ocultar barra lateral" + }, "DesktopUpdate": { "title": "A atualizar o MyDevTools", "downloading": "A transferir a atualização…", diff --git a/apps/desktop-ui/messages/ru.json b/apps/desktop-ui/messages/ru.json index 279dd21f..1b4d844c 100644 --- a/apps/desktop-ui/messages/ru.json +++ b/apps/desktop-ui/messages/ru.json @@ -709,6 +709,7 @@ "all": "Все", "total": "Итого", "allProjects": "Все проекты", + "projectsHeading": "Проекты", "export": "Экспорт", "kanban": "Канбан", "list": "Список", @@ -966,6 +967,16 @@ "emptyHintMobile": "Нажмите кнопку +, чтобы добавить первый пароль", "emptyHintDesktop": "У вас пока нет сохранённых паролей. Нажмите «Добавить пароль», чтобы начать.", "addPassword": "Добавить пароль", + "title": "Пароли", + "filters": { + "heading": "Состояние хранилища", + "all": "Все пароли", + "weak": "Слабые", + "reused": "Повторяющиеся", + "noUrl": "Без URL", + "tagsHeading": "Теги", + "allTags": "Все теги" + }, "searchPlaceholderMobile": "Поиск по хранилищу...", "searchPlaceholderDesktop": "Поиск по хранилищу...", "importExport": "Импорт / экспорт", @@ -1161,6 +1172,8 @@ "emptyHint": "Создайте набор для каждого проекта и среды (например, ключи API продакшена). Всё шифруется в браузере перед загрузкой.", "searchPlaceholder": "Поиск по проекту, среде, тегам, заметкам…", "tagFilterPlaceholder": "Фильтр по тегу", + "allProjects": "Все проекты", + "projectsHeading": "Проекты", "variableCount": "{count, plural, one {# переменная} few {# переменные} other {# переменных}}", "updatedAgo": "Обновлено {time}", "copyDotEnv": "Копировать .env", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "100% офлайн" }, + "ToolSidebar": { + "show": "Показать боковую панель", + "hide": "Скрыть боковую панель" + }, "DesktopUpdate": { "title": "Обновление MyDevTools", "downloading": "Загрузка обновления…", diff --git a/apps/desktop-ui/messages/sv.json b/apps/desktop-ui/messages/sv.json index f44fa205..25a741b4 100644 --- a/apps/desktop-ui/messages/sv.json +++ b/apps/desktop-ui/messages/sv.json @@ -709,6 +709,7 @@ "all": "Alla", "total": "Totalt", "allProjects": "Alla projekt", + "projectsHeading": "Projekt", "export": "Exportera", "kanban": "Kanban", "list": "Lista", @@ -966,6 +967,16 @@ "emptyHintMobile": "Tryck på +-knappen för att lägga till ditt första lösenord", "emptyHintDesktop": "Du har inte lagrat några lösenord ännu. Klicka på knappen \"Lägg till lösenord\" ovan för att komma igång.", "addPassword": "Lägg till lösenord", + "title": "Lösenord", + "filters": { + "heading": "Valvets status", + "all": "Alla lösenord", + "weak": "Svaga", + "reused": "Återanvända", + "noUrl": "Ingen URL", + "tagsHeading": "Taggar", + "allTags": "Alla taggar" + }, "searchPlaceholderMobile": "Sök i ditt valv...", "searchPlaceholderDesktop": "Sök i valvet...", "importExport": "Importera / exportera", @@ -1161,6 +1172,8 @@ "emptyHint": "Skapa en uppsättning för varje projekt och miljö (t.ex. produktions-API-nycklar). Allt krypteras i webbläsaren före uppladdning.", "searchPlaceholder": "Sök projekt, miljö, taggar, anteckningar…", "tagFilterPlaceholder": "Filtrera på tagg", + "allProjects": "Alla projekt", + "projectsHeading": "Projekt", "variableCount": "{count, plural, one {# variabel} other {# variabler}}", "updatedAgo": "Uppdaterad {time}", "copyDotEnv": "Kopiera .env", @@ -5106,6 +5119,10 @@ "ToolPageHeader": { "offline": "100 % offline" }, + "ToolSidebar": { + "show": "Visa sidopanel", + "hide": "Dölj sidopanel" + }, "DesktopUpdate": { "title": "Uppdaterar MyDevTools", "downloading": "Laddar ner uppdatering…", diff --git a/apps/desktop-ui/messages/tr.json b/apps/desktop-ui/messages/tr.json index 796be883..7ce7d861 100644 --- a/apps/desktop-ui/messages/tr.json +++ b/apps/desktop-ui/messages/tr.json @@ -709,6 +709,7 @@ "all": "Tümü", "total": "Toplam", "allProjects": "Tüm Projeler", + "projectsHeading": "Projeler", "export": "Dışa Aktar", "kanban": "Kanban", "list": "Liste", @@ -966,6 +967,16 @@ "emptyHintMobile": "İlk şifrenizi eklemek için + düğmesine dokunun", "emptyHintDesktop": "Henüz kayıtlı şifreniz yok. Başlamak için yukarıdaki \"Şifre ekle\" düğmesine tıklayın.", "addPassword": "Şifre ekle", + "title": "Parolalar", + "filters": { + "heading": "Kasa durumu", + "all": "Tüm parolalar", + "weak": "Zayıf", + "reused": "Tekrar kullanılan", + "noUrl": "URL yok", + "tagsHeading": "Etiketler", + "allTags": "Tüm etiketler" + }, "searchPlaceholderMobile": "Kasanızda arayın...", "searchPlaceholderDesktop": "Kasada ara...", "importExport": "İçe / dışa aktar", @@ -1161,6 +1172,8 @@ "emptyHint": "Her proje ve ortam için bir küme oluşturun (ör. üretim API anahtarları). Yüklemeden önce her şey tarayıcınızda şifrelenir.", "searchPlaceholder": "Proje, ortam, etiket, not ara…", "tagFilterPlaceholder": "Etikete göre filtrele", + "allProjects": "Tüm projeler", + "projectsHeading": "Projeler", "variableCount": "{count, plural, one {# değişken} other {# değişken}}", "updatedAgo": "Güncellendi {time}", "copyDotEnv": ".env kopyala", @@ -5106,6 +5119,10 @@ "ToolPageHeader": { "offline": "%100 çevrimdışı" }, + "ToolSidebar": { + "show": "Kenar çubuğunu göster", + "hide": "Kenar çubuğunu gizle" + }, "DesktopUpdate": { "title": "MyDevTools güncelleniyor", "downloading": "Güncelleme indiriliyor…", diff --git a/apps/desktop-ui/messages/uk.json b/apps/desktop-ui/messages/uk.json index ec3ee262..962a3608 100644 --- a/apps/desktop-ui/messages/uk.json +++ b/apps/desktop-ui/messages/uk.json @@ -709,6 +709,7 @@ "all": "Усі", "total": "Всього", "allProjects": "Усі проєкти", + "projectsHeading": "Проєкти", "export": "Експорт", "kanban": "Канбан", "list": "Список", @@ -966,6 +967,16 @@ "emptyHintMobile": "Натисніть кнопку +, щоб додати перший пароль", "emptyHintDesktop": "Ви ще не зберегли жодного паролю. Натисніть кнопку «Додати пароль» вище, щоб почати.", "addPassword": "Додати пароль", + "title": "Паролі", + "filters": { + "heading": "Стан сховища", + "all": "Усі паролі", + "weak": "Слабкі", + "reused": "Повторювані", + "noUrl": "Без URL", + "tagsHeading": "Теги", + "allTags": "Усі теги" + }, "searchPlaceholderMobile": "Пошук у сховищі...", "searchPlaceholderDesktop": "Пошук у сховищі...", "importExport": "Імпорт / експорт", @@ -1161,6 +1172,8 @@ "emptyHint": "Створіть набір для кожного проєкту й середовища (наприклад, ключі API продакшену). Усе шифрується в браузері перед завантаженням.", "searchPlaceholder": "Пошук за проєктом, середовищем, тегами, нотатками…", "tagFilterPlaceholder": "Фільтр за тегом", + "allProjects": "Усі проєкти", + "projectsHeading": "Проєкти", "variableCount": "{count, plural, one {# змінна} few {# змінні} other {# змінних}}", "updatedAgo": "Оновлено {time}", "copyDotEnv": "Копіювати .env", @@ -5106,6 +5119,10 @@ "ToolPageHeader": { "offline": "100% офлайн" }, + "ToolSidebar": { + "show": "Показати бічну панель", + "hide": "Сховати бічну панель" + }, "DesktopUpdate": { "title": "Оновлення MyDevTools", "downloading": "Завантаження оновлення…", diff --git a/apps/desktop-ui/messages/vi.json b/apps/desktop-ui/messages/vi.json index e1f27695..85b51fda 100644 --- a/apps/desktop-ui/messages/vi.json +++ b/apps/desktop-ui/messages/vi.json @@ -709,6 +709,7 @@ "all": "Tất cả", "total": "Tổng", "allProjects": "Tất cả dự án", + "projectsHeading": "Dự án", "export": "Xuất", "kanban": "Kanban", "list": "Danh sách", @@ -966,6 +967,16 @@ "emptyHintMobile": "Nhấn nút + để thêm mật khẩu đầu tiên", "emptyHintDesktop": "Bạn chưa lưu mật khẩu nào. Nhấn nút \"Thêm mật khẩu\" ở trên để bắt đầu.", "addPassword": "Thêm mật khẩu", + "title": "Mật khẩu", + "filters": { + "heading": "Tình trạng kho", + "all": "Tất cả mật khẩu", + "weak": "Yếu", + "reused": "Dùng lại", + "noUrl": "Không có URL", + "tagsHeading": "Thẻ", + "allTags": "Tất cả thẻ" + }, "searchPlaceholderMobile": "Tìm trong kho...", "searchPlaceholderDesktop": "Tìm trong kho...", "importExport": "Nhập / xuất", @@ -1161,6 +1172,8 @@ "emptyHint": "Tạo một bộ cho mỗi dự án và môi trường (ví dụ khóa API production). Mọi thứ được mã hóa trong trình duyệt trước khi tải lên.", "searchPlaceholder": "Tìm dự án, môi trường, thẻ, ghi chú…", "tagFilterPlaceholder": "Lọc theo thẻ", + "allProjects": "Tất cả dự án", + "projectsHeading": "Dự án", "variableCount": "{count, plural, other {# biến}}", "updatedAgo": "Cập nhật {time}", "copyDotEnv": "Sao chép .env", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "100% ngoại tuyến" }, + "ToolSidebar": { + "show": "Hiện thanh bên", + "hide": "Ẩn thanh bên" + }, "DesktopUpdate": { "title": "Đang cập nhật MyDevTools", "downloading": "Đang tải bản cập nhật…", diff --git a/apps/desktop-ui/messages/zh.json b/apps/desktop-ui/messages/zh.json index 3e9414ec..f4d0c425 100644 --- a/apps/desktop-ui/messages/zh.json +++ b/apps/desktop-ui/messages/zh.json @@ -709,6 +709,7 @@ "all": "全部", "total": "总计", "allProjects": "所有项目", + "projectsHeading": "项目", "export": "导出", "kanban": "看板", "list": "列表", @@ -966,6 +967,16 @@ "emptyHintMobile": "点击 + 添加第一条密码", "emptyHintDesktop": "尚未保存任何密码。点击上方「添加密码」开始。", "addPassword": "添加密码", + "title": "密码", + "filters": { + "heading": "保险库健康度", + "all": "所有密码", + "weak": "弱密码", + "reused": "重复使用", + "noUrl": "无网址", + "tagsHeading": "标签", + "allTags": "所有标签" + }, "searchPlaceholderMobile": "搜索保险库…", "searchPlaceholderDesktop": "搜索保险库…", "importExport": "导入 / 导出", @@ -1161,6 +1172,8 @@ "emptyHint": "为每个项目与环境创建集合(例如生产环境 API 密钥)。上传前均在浏览器中加密。", "searchPlaceholder": "搜索项目、环境、标签、备注…", "tagFilterPlaceholder": "按标签筛选", + "allProjects": "所有项目", + "projectsHeading": "项目", "variableCount": "{count, plural, other {# 个变量}}", "updatedAgo": "更新于 {time}", "copyDotEnv": "复制 .env", @@ -5095,6 +5108,10 @@ "ToolPageHeader": { "offline": "100% 离线" }, + "ToolSidebar": { + "show": "显示侧边栏", + "hide": "隐藏侧边栏" + }, "DesktopUpdate": { "title": "正在更新 MyDevTools", "downloading": "正在下载更新…", diff --git a/apps/desktop-ui/src/app/app/api-keys/page.tsx b/apps/desktop-ui/src/app/app/api-keys/page.tsx index 2e644108..a4d39fae 100644 --- a/apps/desktop-ui/src/app/app/api-keys/page.tsx +++ b/apps/desktop-ui/src/app/app/api-keys/page.tsx @@ -4,7 +4,6 @@ import { useEffect, useRef } from "react" import { AddApiKeyDialog } from "@/components/api-key-vault/add-api-key-dialog" import { ApiKeyList } from "@/components/api-key-vault/api-key-list" import { useApiKeyVaultStore, type ApiKeyEntry } from "@/store/api-key-vault-store" -import { ToolHeader } from "@/components/tools/tool-header" import { useVaultGuard } from "@/hooks/use-vault-guard" import { VaultLockedPlaceholder } from "@/components/vault-locked-placeholder" import { VaultRestoringSkeleton } from "@/components/vault-restoring-skeleton" @@ -104,25 +103,11 @@ export default function ApiKeyVaultPage() { if (!user) return null + // Tool identity and the add action live in the ToolSidebarLayout header + // that ApiKeyList renders — no separate page header row. return ( -
- - -
- -
- +
+ {isMobile && }
) diff --git a/apps/desktop-ui/src/app/app/data-explorer/page.tsx b/apps/desktop-ui/src/app/app/data-explorer/page.tsx index 0ff02bd3..9da9816e 100644 --- a/apps/desktop-ui/src/app/app/data-explorer/page.tsx +++ b/apps/desktop-ui/src/app/app/data-explorer/page.tsx @@ -3,14 +3,12 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslations } from "next-intl"; import { toast } from "sonner"; -import { IconAlertTriangle, IconMenu2 } from "@tabler/icons-react"; +import { IconAlertTriangle, IconDatabase, IconPlus } from "@tabler/icons-react"; import { Button } from "@/components/ui/button"; -import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"; -import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import useAuth from "@/utils/useAuth"; import { useMasterKeyStore } from "@/store/master-key-store"; import { useVaultGuard } from "@/hooks/use-vault-guard"; -import { useMediaQuery } from "@/hooks/use-media-query"; +import { ToolSidebarLayout } from "@/components/tools/tool-sidebar"; import { VaultLockedPlaceholder } from "@/components/vault-locked-placeholder"; import { VaultRestoringSkeleton } from "@/components/vault-restoring-skeleton"; import { UnifiedTabBar } from "@/components/data-explorer/unified-tab-bar"; @@ -31,12 +29,10 @@ export default function DataExplorerPage() { const { user } = useAuth(); const { encryptionKey } = useMasterKeyStore(); const { isUnlocked, isRestoring } = useVaultGuard(); - const isWide = useMediaQuery("(min-width: 768px)"); const [connections, setConnections] = useState([]); const [tabs, setTabs] = useState([]); const [activeTabId, setActiveTabId] = useState(null); - const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [isInitialized, setIsInitialized] = useState(false); const [connectionDialogOpen, setConnectionDialogOpen] = useState(false); const [editingConnection, setEditingConnection] = useState(null); @@ -108,7 +104,6 @@ export default function DataExplorerPage() { const openTab = useCallback( (connection: UnifiedConnection, req: OpenTabRequest) => { const id = `${connection.id}:${req.key}`; - setMobileSidebarOpen(false); setTabs((prev) => { if (prev.some((tab) => tab.id === id)) return prev; return [ @@ -253,35 +248,24 @@ export default function DataExplorerPage() { })(); return ( -
- {isWide ? ( -
{sidebar}
- ) : ( - - - - {t("title")} - - {sidebar} - - - )} + + + + } + sidebar={sidebar} + >
- {/* Narrow viewports keep the sidebar in the Sheet; without this - trigger there is no way to add a connection or open a tab. */} - {!isWide && ( -
- -
- )} void reloadConnections()} /> -
+
); } diff --git a/apps/desktop-ui/src/app/app/environment-manager/page.tsx b/apps/desktop-ui/src/app/app/environment-manager/page.tsx index 450d7fa4..3d8a95ad 100644 --- a/apps/desktop-ui/src/app/app/environment-manager/page.tsx +++ b/apps/desktop-ui/src/app/app/environment-manager/page.tsx @@ -3,7 +3,6 @@ import { useEffect, useRef } from "react" import { AddEnvironmentSetDialog } from "@/components/environment-manager/add-environment-set-dialog" import { EnvironmentSetList } from "@/components/environment-manager/environment-set-list" -import { ToolHeader } from "@/components/tools/tool-header" import { useEnvironmentManagerStore, type EnvSetEntry } from "@/store/environment-manager-store" import { useVaultGuard } from "@/hooks/use-vault-guard" import { VaultLockedPlaceholder } from "@/components/vault-locked-placeholder" @@ -110,22 +109,11 @@ export default function EnvironmentManagerPage() { if (!user) return null + // Tool identity lives in the ToolSidebarLayout header EnvironmentSetList + // renders — no separate page header row. return ( -
- {!isMobile && ( - - )} - -
- -
- +
+ {isMobile && }
) diff --git a/apps/desktop-ui/src/app/app/layout.tsx b/apps/desktop-ui/src/app/app/layout.tsx index 6d19ddd7..12b60cbd 100644 --- a/apps/desktop-ui/src/app/app/layout.tsx +++ b/apps/desktop-ui/src/app/app/layout.tsx @@ -1,26 +1,13 @@ import { Suspense } from "react"; -import { cookies } from "next/headers"; -import { SidebarProvider } from "@/components/ui/sidebar"; import { AppContent } from "./app-content"; -// Static export (Tauri desktop) cannot use request APIs; fall back to defaults there. -const isTauriBuild = process.env.TAURI_BUILD === "1"; - -export default async function Layout({ children }: { children: React.ReactNode }) { - let defaultOpen = true; - if (!isTauriBuild) { - const cookieStore = await cookies(); - defaultOpen = cookieStore.get("sidebar:state")?.value !== "false"; - } - +export default function Layout({ children }: { children: React.ReactNode }) { return ( - - {/* Suspense boundary so tool pages that read useSearchParams() (base64, - jwt-decoder, hash-generator, url-encode, json-formatter, qr-code) pass - the static export prerender instead of erroring on a CSR bailout. */} - - {children} - - + // Suspense boundary so tool pages that read useSearchParams() (base64, + // jwt-decoder, hash-generator, url-encode, json-formatter, qr-code) pass + // the static export prerender instead of erroring on a CSR bailout. + + {children} + ); } 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..b9add677 100644 --- a/apps/desktop-ui/src/app/app/notes/context/NotesContext.tsx +++ b/apps/desktop-ui/src/app/app/notes/context/NotesContext.tsx @@ -37,8 +37,6 @@ interface NotesUI { setActiveNoteId: (id: string | null) => void; focusMode: boolean; setFocusMode: (v: boolean) => void; - sidebarOpen: boolean; - setSidebarOpen: (v: boolean) => void; } interface NotesActions { @@ -65,7 +63,6 @@ export function NotesProvider({ children }: { children: React.ReactNode }) { const [isContentLoading, setIsContentLoading] = useState(false); const [activeNoteId, setActiveNoteId] = useState(null); const [focusMode, setFocusMode] = useState(false); - const [sidebarOpen, setSidebarOpen] = useState(true); // Decrypted bodies, tagged with the key they were decrypted under: a vault // lock/unlock invalidates the whole map on read, with no reset effect. const [contentStore, setContentStore] = useState({ key: null, map: EMPTY_CONTENT }); @@ -402,8 +399,8 @@ export function NotesProvider({ children }: { children: React.ReactNode }) { ); const uiValue = useMemo( - () => ({ activeNoteId, setActiveNoteId, focusMode, setFocusMode, sidebarOpen, setSidebarOpen }), - [activeNoteId, focusMode, sidebarOpen] + () => ({ activeNoteId, setActiveNoteId, focusMode, setFocusMode }), + [activeNoteId, focusMode] ); const actionsValue = useMemo( diff --git a/apps/desktop-ui/src/app/app/notes/notes-client-layout.tsx b/apps/desktop-ui/src/app/app/notes/notes-client-layout.tsx index 615ce2c7..94cc182a 100644 --- a/apps/desktop-ui/src/app/app/notes/notes-client-layout.tsx +++ b/apps/desktop-ui/src/app/app/notes/notes-client-layout.tsx @@ -2,69 +2,33 @@ import { NotesProvider, useNotesUI } from "./context/NotesContext"; import NotesSidebar from "@/components/notes/NotesSidebar"; -import { useMediaQuery } from "@/hooks/use-media-query"; -import { Sheet, SheetContent, SheetTrigger, SheetHeader, SheetTitle } from "@/components/ui/sheet"; -import { Menu, PanelLeftOpen, NotebookPen } from "lucide-react"; -import { Button } from "@/components/ui/button"; +import { ToolSidebarLayout } from "@/components/tools/tool-sidebar"; +import { NotebookPen } from "lucide-react"; import { useTranslations } from "next-intl"; function NotesLayout({ children }: { children: React.ReactNode }) { - const t = useTranslations("Notes.layout"); const tSidebar = useTranslations("Notes.sidebar"); - const isDesktop = useMediaQuery("(min-width: 768px)"); - const { focusMode, sidebarOpen, setSidebarOpen } = useNotesUI(); - const sidebarCollapsed = isDesktop && !focusMode && !sidebarOpen; + const { focusMode } = useNotesUI(); - return ( -
- {isDesktop && !focusMode && sidebarOpen && } - -
- {sidebarCollapsed && ( -
- -
- - - - {tSidebar("title")} -
-
- )} - {!isDesktop && !focusMode && ( -
- - - - - - - {t("mobileNavTitle")} - - - - -
- )} + // Focus mode drops the panel entirely — no column, no re-open affordance. + if (focusMode) { + return ( +
{children}
-
+ ); + } + + return ( + } + className="bg-background mobile-nav-offset" + > + {children} + ); } diff --git a/apps/desktop-ui/src/app/app/password-manager/page.tsx b/apps/desktop-ui/src/app/app/password-manager/page.tsx index 7a34f0ff..fd29359a 100644 --- a/apps/desktop-ui/src/app/app/password-manager/page.tsx +++ b/apps/desktop-ui/src/app/app/password-manager/page.tsx @@ -20,9 +20,6 @@ import { fetchAllPages } from "@/lib/fetch-all-pages" import { EncryptedToolPlaceholder } from "@/components/encrypted-tool-placeholder" import { useActiveWorkspace } from "@/store/workspace-store" import { hasWorkspaceEncryption } from "@/lib/workspace-rbac" -import { IconShieldLock } from "@tabler/icons-react" -import { CATEGORY_ACCENT } from "@/components/dashboard/types" -import { cn } from "@/lib/utils" const PASSWORDS_PAGE_SIZE = 500 @@ -122,34 +119,11 @@ export default function PasswordManagerPage() { if (!user) return null + // Tool identity and the add action live in the ToolSidebarLayout header + // PasswordList renders — no separate page header row. return ( -
- {!isMobile && ( -
-
- - - -

{t("title")}

-
- -
- )} - +
- {isMobile && }
) diff --git a/apps/desktop-ui/src/app/app/s3-drive/page.tsx b/apps/desktop-ui/src/app/app/s3-drive/page.tsx index f41dca8f..1baab488 100644 --- a/apps/desktop-ui/src/app/app/s3-drive/page.tsx +++ b/apps/desktop-ui/src/app/app/s3-drive/page.tsx @@ -12,9 +12,9 @@ import { listConnections } from "@/lib/s3-drive-api" import { decryptData } from "@/lib/encryption" import { BucketSidebar } from "@/components/s3-drive/bucket-sidebar" import { FileBrowser } from "@/components/s3-drive/file-browser" +import { ToolSidebarLayout } from "@/components/tools/tool-sidebar" import { IconBucket, IconCloud, IconArrowRight } from "@tabler/icons-react" import { Skeleton } from "@/components/ui/skeleton" -import { cn } from "@/lib/utils" import { DesktopOnlineGate } from "@/components/desktop/desktop-online-gate" export default function S3DrivePage() { @@ -67,7 +67,7 @@ function S3DriveInner() { return (
{/* Sidebar skeleton */} -
+
@@ -120,11 +120,12 @@ function S3DriveInner() { const activeConn = connections.find((c) => c.id === activeConnectionId) ?? null return ( -
-
- -
- + } + >
{activeConn ? ( )}
-
+ ) } diff --git a/apps/desktop-ui/src/app/app/to-do/TaskContainer.tsx b/apps/desktop-ui/src/app/app/to-do/TaskContainer.tsx index 99be9f73..8f5f43ee 100644 --- a/apps/desktop-ui/src/app/app/to-do/TaskContainer.tsx +++ b/apps/desktop-ui/src/app/app/to-do/TaskContainer.tsx @@ -19,6 +19,11 @@ import { useTaskContext } from "@/app/app/to-do/context/TaskContext"; import { useWorkspaceMembers, memberLabel } from "@/app/app/to-do/hooks/useWorkspaceMembers"; import { useProjectContext } from "@/app/app/to-do/context/ProjectContext"; import { ListTodo, Circle, LayoutGrid, List, Search, X, Plus, Folder, Archive, ArchiveRestore, UserRound } from "lucide-react"; +import { + ToolSidebarFilterList, + ToolSidebarLayout, + type ToolSidebarFilterItem, +} from "@/components/tools/tool-sidebar"; import { Button } from "@/components/ui/button"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { STATUS_CONFIG } from "./config/constants"; @@ -35,7 +40,6 @@ import { DrawerClose, DrawerTrigger, } from "@/components/ui/drawer"; -import { SidebarTrigger } from "@/components/ui/sidebar"; import { Badge } from "@/components/ui/badge"; import { Select, @@ -200,7 +204,25 @@ export const TaskContainer = () => { return () => window.removeEventListener("keydown", handleKeyDown); }, [hasSearchFilter, isMobile]); + const projectFilters: ToolSidebarFilterItem[] = [ + { id: "all", label: tFilters("allProjects") }, + ...projects.map((project) => ({ id: project.id, label: project.name, dot: project.color })), + ]; + return ( + + } + >
{liveStatsMessage} @@ -212,8 +234,6 @@ export const TaskContainer = () => { style={{ paddingTop: "calc(env(safe-area-inset-top, 0px) + 0.5rem)" }} >
- -
{
-
- - - -
-

- {tPage("myTasksTitle")} -

-

- {tPage("statsLineDesktop", { total: allTaskStats.total, percent: completionRate })} -

-
-
+ {/* Tool name + icon live in the ToolSidebarLayout panel header. */} +

+ {tPage("statsLineDesktop", { total: allTaskStats.total, percent: completionRate })} +

{/* Enhanced Stats with Progress Bars - Hidden on mobile to save space */}
@@ -397,27 +408,7 @@ export const TaskContainer = () => { )}
- {/* Project Filter */} - + {/* Project filter lives in the ToolSidebarLayout panel. */} {/* Assignee Filter — only meaningful in a shared workspace */} {isShared && ( @@ -739,5 +730,6 @@ export const TaskContainer = () => { )}
+ ); }; diff --git a/apps/desktop-ui/src/app/app/to-do/utils/__tests__/statusOrder.test.ts b/apps/desktop-ui/src/app/app/to-do/utils/__tests__/statusOrder.test.ts new file mode 100644 index 00000000..83e81bec --- /dev/null +++ b/apps/desktop-ui/src/app/app/to-do/utils/__tests__/statusOrder.test.ts @@ -0,0 +1,51 @@ +import { insertOrder, needsRespread, ORDER_STEP } from "../statusOrder"; + +describe("insertOrder", () => { + it("starts at 0 in an empty column", () => { + expect(insertOrder([], 0)).toBe(0); + }); + + it("appends a step past the last card", () => { + expect(insertOrder([0, 1000], 2)).toBe(2000); + }); + + it("prepends a step before the first card", () => { + expect(insertOrder([0, 1000], 0)).toBe(-ORDER_STEP); + }); + + it("lands between two neighbours", () => { + expect(insertOrder([0, 1000], 1)).toBe(500); + }); + + it("always returns an integer — Rust reads statusOrder with as_i64", () => { + // An odd gap must not produce a fraction, or the row is skipped server-side. + expect(Number.isInteger(insertOrder([0, 1], 1))).toBe(true); + expect(Number.isInteger(insertOrder([0, 7], 1))).toBe(true); + }); + + it("clamps an out-of-range index instead of returning NaN", () => { + expect(insertOrder([0, 1000], 99)).toBe(2000); + expect(insertOrder([0, 1000], -5)).toBe(-ORDER_STEP); + }); + + it("keeps legacy 1/2/3 orders sortable without a migration", () => { + // Rows written before statusOrder meant "position" are tightly packed. + const order = insertOrder([1, 2, 3], 3); + expect(order).toBeGreaterThan(3); + }); +}); + +describe("needsRespread", () => { + it("is false when the insert found room", () => { + expect(needsRespread([0, 1000], 1, 500)).toBe(false); + }); + + it("is true when the gap between neighbours is exhausted", () => { + // No integer exists strictly between 5 and 6. + expect(needsRespread([5, 6], 1, insertOrder([5, 6], 1))).toBe(true); + }); + + it("is false at the head of a column, where there is always room", () => { + expect(needsRespread([0, 1000], 0, -ORDER_STEP)).toBe(false); + }); +}); diff --git a/apps/desktop-ui/src/app/app/to-do/utils/statusOrder.ts b/apps/desktop-ui/src/app/app/to-do/utils/statusOrder.ts new file mode 100644 index 00000000..c86f5df5 --- /dev/null +++ b/apps/desktop-ui/src/app/app/to-do/utils/statusOrder.ts @@ -0,0 +1,36 @@ +/** + * Position of a card within its board column. + * + * Cards are spaced `ORDER_STEP` apart so that dropping one between two neighbours is a + * single PATCH of the moved card instead of a renumber of the whole column. When the gap + * between two neighbours is used up, the column is respread — see `needsRespread`. + * + * Integers only: the Rust handler reads `statusOrder` with `as_i64` inside a `filter_map`, + * so a fractional value is silently skipped when it computes the next order on create. + */ +export const ORDER_STEP = 1000; + +/** + * The `statusOrder` for a card inserted at `index` of a column whose existing cards have + * `orders` (ascending, excluding the card being moved). + */ +export function insertOrder(orders: number[], index: number): number { + const at = Math.max(0, Math.min(index, orders.length)); + const prev = orders[at - 1]; + const next = orders[at]; + + if (prev === undefined && next === undefined) return 0; + if (prev === undefined) return next - ORDER_STEP; + if (next === undefined) return prev + ORDER_STEP; + return Math.floor((prev + next) / 2); +} + +/** + * True when `insertOrder` could not find room between the neighbours, so the column has to + * be respread at even intervals before the position is meaningful. + */ +export function needsRespread(orders: number[], index: number, order: number): boolean { + const at = Math.max(0, Math.min(index, orders.length)); + const prev = orders[at - 1]; + return prev !== undefined && order <= prev; +} diff --git a/apps/desktop-ui/src/components/api-client/api-client.tsx b/apps/desktop-ui/src/components/api-client/api-client.tsx index 3a62396a..943e9519 100644 --- a/apps/desktop-ui/src/components/api-client/api-client.tsx +++ b/apps/desktop-ui/src/components/api-client/api-client.tsx @@ -29,6 +29,7 @@ import { parseCurlCommand } from "@/utils/curl-parser" import { generateCode } from "./generate-code" import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard" import { CollectionsSidebar } from "./collections/collections-sidebar" +import { ToolSidebarLayout } from "@/components/tools/tool-sidebar" import dynamic from "next/dynamic" import { RequestMethod, @@ -44,13 +45,11 @@ import type { ScriptContext } from "./workers/scripts-runner.worker" import { useTranslations } from "next-intl" import { toast } from "sonner" import { useIsMobile } from "@/components/hooks/use-mobile" -import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet" import { Button } from "@/components/ui/button" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { FolderOpen, PanelRight, MoreVertical, MoreHorizontal, Cookie, Download, Activity, Keyboard } from "lucide-react" +import { FolderOpen, MoreVertical, MoreHorizontal, Cookie, Download, Activity, Keyboard } from "lucide-react" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu" import { IconCode, IconSettings } from "@tabler/icons-react" -import { cn } from "@/lib/utils" import { ensureHttpScheme } from "@/lib/url-normalize" import { encodeBasicCredentials } from "@/lib/basic-auth" import { ensureFreshToken } from "@/lib/oauth2" @@ -131,8 +130,6 @@ function ApiClientInner() { const sessionVarsRef = React.useRef>({}) const isMobile = useIsMobile() - const [collectionsOpen, setCollectionsOpen] = React.useState(false) - const [sidebarOpen, setSidebarOpen] = React.useState(true) const [mobilePanel, setMobilePanel] = React.useState<'request' | 'response'>('request') const [envMgrOpen, setEnvMgrOpen] = React.useState(false) const [codeGenOpen, setCodeGenOpen] = React.useState(false) @@ -1105,31 +1102,15 @@ function ApiClientInner() { ) return ( -
+ } + className="mobile-nav-offset" + >
- {/* Mobile Collections Button */} - {isMobile && ( - - - - - -
-
- { - handleLoadRequest(request) - setCollectionsOpen(false) - }} - /> -
- - - )}
{isMobile ? ( /* Mobile: secondary actions collapsed into dropdown */ @@ -1270,15 +1251,6 @@ function ApiClientInner() {
- )} {/* Dialogs rendered outside the conditional so state is preserved */} @@ -1462,22 +1434,7 @@ function ApiClientInner() {
- - {/* Desktop Collections Sidebar */} - {!isMobile && ( -
- setSidebarOpen(false)} - /> -
- )} -
+ ) } diff --git a/apps/desktop-ui/src/components/api-client/collections/collections-sidebar.tsx b/apps/desktop-ui/src/components/api-client/collections/collections-sidebar.tsx index 1e8c41b6..d4164ce7 100644 --- a/apps/desktop-ui/src/components/api-client/collections/collections-sidebar.tsx +++ b/apps/desktop-ui/src/components/api-client/collections/collections-sidebar.tsx @@ -6,7 +6,7 @@ import { Checkbox } from "@/components/ui/checkbox" import { ScrollArea } from "@/components/ui/scroll-area" import { Collection, CollectionRequest } from "../types" import { CollectionItem } from "./collection-item" -import { FolderPlus, Trash2, Pencil, MoreHorizontal, Search, X, FileDown, Play, Server, Link2, Globe, PanelRightClose } from "lucide-react" +import { FolderPlus, Trash2, Pencil, MoreHorizontal, Search, X, FileDown, Play, Server, Link2, Globe } from "lucide-react" import { buildShareUrl } from "@/lib/share-link" import { apiFetch } from "@/lib/desktop/api-fetch" import { toast } from "sonner" @@ -47,16 +47,23 @@ import { useHistoryState, useHistoryActions } from "../context/history-context" import { CollectionsSidebarSkeleton, HistoryListSkeleton } from "../skeletons" import { useDebouncedValue } from "@/lib/use-debounced-value" import { isDesktop } from "@/lib/desktop/is-desktop" +import { ToolSidebarActions, useToolSidebarPanel } from "@/components/tools/tool-sidebar" interface CollectionsSidebarProps { onLoadRequest: (request: CollectionRequest) => void - onCollapse?: () => void } -export function CollectionsSidebar({ - onLoadRequest, - onCollapse, -}: CollectionsSidebarProps) { +export function CollectionsSidebar({ onLoadRequest: loadRequest }: CollectionsSidebarProps) { + const panel = useToolSidebarPanel() + // On mobile the panel is a sheet over the request pane — loading a request + // has to dismiss it or the loaded request stays hidden behind the sheet. + const onLoadRequest = React.useCallback( + (request: CollectionRequest) => { + loadRequest(request) + if (panel?.isMobile) panel.close() + }, + [loadRequest, panel], + ) const { collections, isLoading } = useCollectionsState() const { addFolder: onAddFolder, deleteItem: onDelete, toggleFolder: onToggle, createCollection: onCreateCollection, renameCollection: onRenameCollection, renameFolder: onRenameFolder, patchFolder, deleteMultipleCollections: onDeleteMultiple } = useCollectionsActions() const { history, isLoading: isHistoryLoading } = useHistoryState() @@ -189,45 +196,34 @@ export function CollectionsSidebar({ } return ( -
+ // Header (icon, title, collapse) is owned by ToolSidebarLayout in + // components/api-client/api-client.tsx — this renders the body only. + <> + + {selectedCollections.size > 0 && ( + + )} + + +
-
-

{t("title")}

-
- {selectedCollections.size > 0 && ( - - )} - - {onCollapse && ( - - )} -
-
+ ) } diff --git a/apps/desktop-ui/src/components/api-key-vault/api-key-list.tsx b/apps/desktop-ui/src/components/api-key-vault/api-key-list.tsx index cf0a9f96..36a5f3b7 100644 --- a/apps/desktop-ui/src/components/api-key-vault/api-key-list.tsx +++ b/apps/desktop-ui/src/components/api-key-vault/api-key-list.tsx @@ -19,6 +19,11 @@ import { deleteApiKeyEntry } from "@/lib/api-key-vault-api" import { cn } from "@/lib/utils" import { formatDistanceToNow } from "date-fns" import { AddApiKeyDialog, EditApiKeyDialog } from "./add-api-key-dialog" +import { + ToolSidebarFilterList, + ToolSidebarLayout, + type ToolSidebarFilterItem, +} from "@/components/tools/tool-sidebar" const REVEAL_TIMEOUT_MS = 30_000 @@ -174,8 +179,35 @@ export function ApiKeyList() { } } + const envFilters: ToolSidebarFilterItem[] = [ + { id: "all", label: "All keys", count: entries.length }, + { id: "development", label: ENV_META.development.label, count: counts.development, dot: ENV_META.development.dot }, + { id: "staging", label: ENV_META.staging.label, count: counts.staging, dot: ENV_META.staging.dot }, + { id: "production", label: ENV_META.production.label, count: counts.production, dot: ENV_META.production.dot }, + ] + return ( -
+ + + + } + sidebar={ + setEnvFilter(id as "all" | ApiKeyEnv)} + heading="Environment" + /> + } + > +
@@ -187,17 +219,6 @@ export function ApiKeyList() { className="pl-9" />
- -
+
+ ); } diff --git a/apps/desktop-ui/src/components/environment-manager/environment-set-list.tsx b/apps/desktop-ui/src/components/environment-manager/environment-set-list.tsx index a789d908..8c02bf95 100644 --- a/apps/desktop-ui/src/components/environment-manager/environment-set-list.tsx +++ b/apps/desktop-ui/src/components/environment-manager/environment-set-list.tsx @@ -9,7 +9,12 @@ import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" -import { Search, Copy, Pencil, Trash2, FileCode2, Eye, ArrowUpDown, Check } from "lucide-react" +import { Search, Copy, Pencil, Trash2, FileCode2, Eye, ArrowUpDown, Check, Folders as IconFolders } from "lucide-react" +import { + ToolSidebarFilterList, + ToolSidebarLayout, + type ToolSidebarFilterItem, +} from "@/components/tools/tool-sidebar" import { toast } from "sonner" import { deleteEnvSetEntry } from "@/lib/environment-manager-api" import { formatDotEnv } from "@/lib/environment-manager-utils" @@ -42,7 +47,6 @@ import { AddEnvironmentSetDialog } from "./add-environment-set-dialog" import { formatDistanceToNow } from "date-fns" import { cn } from "@/lib/utils" import { useIsMobile } from "@/components/hooks/use-mobile" -import { SidebarTrigger } from "@/components/ui/sidebar" import { Dialog, DialogContent, @@ -77,6 +81,7 @@ export function EnvironmentSetList() { const { copyToClipboard } = useCopyToClipboard() const [search, setSearch] = useState("") const [tagFilter, setTagFilter] = useState("") + const [projectFilter, setProjectFilter] = useState("all") const [sortOrder, setSortOrder] = useState("updatedDesc") const [editing, setEditing] = useState(null) const [editOpen, setEditOpen] = useState(false) @@ -91,10 +96,23 @@ export function EnvironmentSetList() { return [...s].sort() }, [sets]) + // Project buckets for the sidebar, sorted by name with an "all" row on top. + const projectFilters: ToolSidebarFilterItem[] = useMemo(() => { + const counts = new Map() + sets.forEach((e) => counts.set(e.project, (counts.get(e.project) ?? 0) + 1)) + return [ + { id: "all", label: t("allProjects"), count: sets.length }, + ...[...counts.entries()] + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([project, count]) => ({ id: project, label: project, count })), + ] + }, [sets, t]) + const filtered = useMemo(() => { const q = debouncedSearch.toLowerCase().trim() const tf = tagFilter.toLowerCase().trim() const result = sets.filter((e) => { + if (projectFilter !== "all" && e.project !== projectFilter) return false const matchTag = !tf || e.tags.some((x) => x.toLowerCase().includes(tf)) || tf === e.project.toLowerCase() if (!q) return matchTag @@ -107,7 +125,7 @@ export function EnvironmentSetList() { if (sortOrder === "updatedAsc") return a.updatedAt - b.updatedAt return `${a.project}/${a.environment}`.localeCompare(`${b.project}/${b.environment}`) }) - }, [sets, debouncedSearch, tagFilter, sortOrder]) + }, [sets, debouncedSearch, tagFilter, projectFilter, sortOrder]) const viewDotEnvRaw = useMemo( () => (viewingEntry ? formatDotEnv(viewingEntry.variables).trim() : ""), @@ -132,20 +150,25 @@ export function EnvironmentSetList() { } return ( - <> + + } + >
- {isMobile && ( -
- -

{t("title")}

-
- )} -
@@ -495,6 +518,6 @@ export function EnvironmentSetList() { - + ) } diff --git a/apps/desktop-ui/src/components/mobile-nav.tsx b/apps/desktop-ui/src/components/mobile-nav.tsx index 0c30dfb0..5e4fdb8b 100644 --- a/apps/desktop-ui/src/components/mobile-nav.tsx +++ b/apps/desktop-ui/src/components/mobile-nav.tsx @@ -3,7 +3,6 @@ import Link from "next/link" import { usePathname, useRouter } from "next/navigation" import { Home, LayoutGrid, Lock, User as UserIcon, Moon, Sun, Settings, HelpCircle } from "lucide-react" import { cn } from "@/lib/utils" -import { useSidebar } from "@/components/ui/sidebar" import { DropdownMenu, DropdownMenuContent, @@ -30,7 +29,6 @@ const navItems = [ export function MobileNav() { const pathname = usePathname() - const { toggleSidebar, openMobile } = useSidebar() const user = useAppUser() const { clearPasswords } = usePasswordStore() const { clearSets } = useEnvironmentManagerStore() @@ -47,9 +45,8 @@ export function MobileNav() { }, []) // Determine active tab for indicator animation - const getActiveTab = (): 'home' | 'tools' | 'profile' | null => { + const getActiveTab = (): 'home' | 'profile' | null => { if (pathname === '/dashboard') return 'home' - if (openMobile) return 'tools' return null } const activeTab = getActiveTab() @@ -136,24 +133,15 @@ export function MobileNav() { {navItems[0].label} - {/* Tools Button */} + {/* Tools Button — opens the global command palette (same event the + TopBar search button dispatches). The old sidebar it used to + toggle is gone; the palette is the tool switcher now. */} diff --git a/apps/desktop-ui/src/components/notes/NotesSidebar.tsx b/apps/desktop-ui/src/components/notes/NotesSidebar.tsx index 1435538d..bdf48fc5 100644 --- a/apps/desktop-ui/src/components/notes/NotesSidebar.tsx +++ b/apps/desktop-ui/src/components/notes/NotesSidebar.tsx @@ -29,7 +29,6 @@ import { ArrowUpDown, NotebookPen, FileText, - PanelLeftClose, ArrowUp, ArrowDown, } from "lucide-react"; @@ -56,6 +55,7 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Note } from "@/app/app/notes/types/Note"; +import { ToolSidebarActions, useToolSidebarPanel } from "@/components/tools/tool-sidebar"; import { useTranslations } from "next-intl"; import { extractSnippet } from "@/app/app/notes/utils/noteContentUtils"; import { type ChildrenMap, type SortDir, type SortKey, buildChildrenMap, getCachedPlainText } from "./notes-helpers"; @@ -83,6 +83,7 @@ const NoteItem = React.memo(({ const t = useTranslations("Notes.sidebar"); const { activeNoteId, setActiveNoteId } = useNotesUI(); const { createNote, pinNote, duplicateNote, updateNote } = useNotesActions(); + const panel = useToolSidebarPanel(); const [emojiPickerOpen, setEmojiPickerOpen] = useState(false); const children = childrenMap.get(note.id) ?? []; @@ -98,6 +99,9 @@ const NoteItem = React.memo(({ const handleSelect = () => { setActiveNoteId(note.id); onExpandPath(note.id); + // On mobile the panel is a sheet over the editor — close it so the + // opened note is actually visible. + if (panel?.isMobile) panel.close(); }; const handleCreateChild = async (e: React.MouseEvent) => { @@ -269,7 +273,7 @@ NoteItem.displayName = "NoteItem"; export default function NotesSidebar() { const t = useTranslations("Notes.sidebar"); const { notes, noteById, isLoading, searchIndexReady } = useNotesData(); - const { activeNoteId, setSidebarOpen } = useNotesUI(); + const { activeNoteId } = useNotesUI(); const { createNote, deleteNote, moveNote, warmSearchIndex } = useNotesActions(); const [noteToDelete, setNoteToDelete] = useState(null); const [noteToMove, setNoteToMove] = useState(null); @@ -404,17 +408,11 @@ export default function NotesSidebar() { return ( <> -
-
-
-
- - - - {t("title")} -
-
- + {/* Header (icon, title, collapse) is owned by ToolSidebarLayout in + app/app/notes/notes-client-layout.tsx. Sort and create ride the + actions portal because their state is local to this component. */} + + - {/* Collapse — desktop only; on mobile the sidebar lives in a Sheet */} - -
-
+ + + + +
-
+
{isLoading ? (
{t("loading")}
@@ -585,7 +573,6 @@ export default function NotesSidebar() { )}
-
!open && setNoteToDelete(null)}> diff --git a/apps/desktop-ui/src/components/password-manager/password-list.tsx b/apps/desktop-ui/src/components/password-manager/password-list.tsx index 46782f61..59cfca67 100644 --- a/apps/desktop-ui/src/components/password-manager/password-list.tsx +++ b/apps/desktop-ui/src/components/password-manager/password-list.tsx @@ -25,12 +25,17 @@ import { calculatePasswordStrength, getStrengthColor, getFaviconUrl, getPassword import { usePasswordStrengthReady } from "@/lib/use-password-strength" import { useVaultIconsEnabled } from "@/lib/vault-icon-pref" import { FaviconImg } from "@/components/favicon-img" +import { IconShieldLock } from "@tabler/icons-react" +import { + ToolSidebarFilterList, + ToolSidebarLayout, + type ToolSidebarFilterItem, +} from "@/components/tools/tool-sidebar" import { Badge } from "@/components/ui/badge" import { formatDistanceToNow } from "date-fns" import { ImportExportDialog } from "./import-export-dialog" import { useIsMobile } from "@/components/hooks/use-mobile" import { PasswordItemSwipeable } from "./password-item-swipeable" -import { SidebarTrigger } from "@/components/ui/sidebar" import { PasswordCard } from "./password-card" import { SecurityDashboard } from "./security-dashboard" import { BreachCheckDialog } from "./breach-check-dialog" @@ -52,6 +57,7 @@ export function PasswordList() { const [visiblePasswords, setVisiblePasswords] = useState>(new Set()) const [viewMode, setViewMode] = useState<"grid" | "list">("grid") const [quickFilter, setQuickFilter] = useState<"all" | "weak" | "reused" | "no-url">("all") + const [tagFilter, setTagFilter] = useState("all") const [sortBy, setSortBy] = useState<"name" | "date" | "strength">("name") const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) const [passwordToDelete, setPasswordToDelete] = useState(null) @@ -91,13 +97,16 @@ export function PasswordList() { } else if (quickFilter === "no-url") { filtered = bySearch.filter((entry) => !entry.url?.trim()) } + if (tagFilter !== "all") { + filtered = filtered.filter((entry) => entry.tags?.includes(tagFilter)) + } return [...filtered].sort((a, b) => { if (sortBy === "date") return b.updatedAt - a.updatedAt if (sortBy === "strength") return calculatePasswordStrength(b.password) - calculatePasswordStrength(a.password) return a.service.localeCompare(b.service) }) - }, [passwords, searchTerm, quickFilter, reusedPasswordIds, sortBy, strengthReady]) + }, [passwords, searchTerm, quickFilter, tagFilter, reusedPasswordIds, sortBy, strengthReady]) const weakCount = useMemo( () => passwords.filter((entry) => calculatePasswordStrength(entry.password) <= 2).length, @@ -111,6 +120,21 @@ export function PasswordList() { const reusedCount = reusedPasswordIds.size const hasActiveQuickFilter = quickFilter !== "all" + const securityFilters: ToolSidebarFilterItem[] = [ + { id: "all", label: t("filters.all"), count: passwords.length }, + { id: "weak", label: t("filters.weak"), count: weakCount, icon: AlertTriangle }, + { id: "reused", label: t("filters.reused"), count: reusedCount, icon: Repeat }, + { id: "no-url", label: t("filters.noUrl"), count: noUrlCount, icon: Link2Off }, + ] + + const tagFilters: ToolSidebarFilterItem[] = useMemo(() => { + const counts = new Map() + passwords.forEach((p) => p.tags?.forEach((tag) => counts.set(tag, (counts.get(tag) ?? 0) + 1))) + return [...counts.entries()] + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([tag, count]) => ({ id: tag, label: tag, count })) + }, [passwords]) + const runBreachCheck = async () => { if (breachStatus === "checking") return setBreachStatus("checking") @@ -236,6 +260,7 @@ export function PasswordList() { const clearFilters = () => { setSearchTerm("") setQuickFilter("all") + setTagFilter("all") } useEffect(() => { @@ -324,15 +349,43 @@ export function PasswordList() { } return ( + + + + } + sidebar={ +
+ setQuickFilter(id as typeof quickFilter)} + heading={t("filters.heading")} + /> + {tagFilters.length > 0 && ( + + )} +
+ } + >
{/* Mobile Header */} {isMobile && (
- -
)} - {!isMobile && ( + {/* Security and tag facets moved to the ToolSidebarLayout panel; the + reset shortcut stays here next to the search it also clears. */} + {!isMobile && (hasActiveQuickFilter || tagFilter !== "all" || searchTerm) && (
- - - - - {(hasActiveQuickFilter || searchTerm) && ( - - )}
Cmd/Ctrl+K search • Cmd/Ctrl+Shift+L lock
@@ -796,5 +837,6 @@ export function PasswordList() {
+ ) } diff --git a/apps/desktop-ui/src/components/s3-drive/bucket-sidebar.tsx b/apps/desktop-ui/src/components/s3-drive/bucket-sidebar.tsx index b633f2e8..6532c4f9 100644 --- a/apps/desktop-ui/src/components/s3-drive/bucket-sidebar.tsx +++ b/apps/desktop-ui/src/components/s3-drive/bucket-sidebar.tsx @@ -4,7 +4,6 @@ import { useState } from "react" import { toast } from "sonner" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" -import { ScrollArea } from "@/components/ui/scroll-area" import { DropdownMenu, DropdownMenuContent, @@ -24,6 +23,7 @@ import { import { IconBucket, IconPlus, IconDots, IconPencil, IconTrash, IconBrandAws, IconCloud, IconServer } from "@tabler/icons-react" import { deleteConnection } from "@/lib/s3-drive-api" import { useS3DriveStore, type DecryptedConnection } from "@/store/s3-drive-store" +import { useToolSidebarPanel } from "@/components/tools/tool-sidebar" import { AddBucketDialog } from "./add-bucket-dialog" type Props = { @@ -60,6 +60,7 @@ function connSubtitle(conn: DecryptedConnection): string { export function BucketSidebar({ encryptionKey }: Props) { const { connections, activeConnectionId, setActiveConnection, removeConnection } = useS3DriveStore() + const panel = useToolSidebarPanel() const [addOpen, setAddOpen] = useState(false) const [editTarget, setEditTarget] = useState(null) const [deleteTarget, setDeleteTarget] = useState(null) @@ -80,44 +81,24 @@ export function BucketSidebar({ encryptionKey }: Props) { } } + // Header (icon, title, collapse) is owned by ToolSidebarLayout in + // app/app/s3-drive/page.tsx — this renders the panel body only. return ( -
- {/* Header */} -
-
-
-
- -
- Buckets -
- -
- {connections.length > 0 && ( -

- {connections.length} connection{connections.length !== 1 ? "s" : ""} -

- )} -
- -
+ <> + {connections.length > 0 && ( +

+ {connections.length} connection{connections.length !== 1 ? "s" : ""} +

+ )} - -
+
{connections.length === 0 && (
-
+
@@ -149,7 +130,12 @@ export function BucketSidebar({ encryptionKey }: Props) { ? "bg-primary/8 text-foreground shadow-sm ring-1 ring-primary/10" : "hover:bg-accent/50 text-muted-foreground hover:text-foreground", )} - onClick={() => setActiveConnection(conn.id)} + onClick={() => { + setActiveConnection(conn.id) + // On mobile the panel is a sheet covering the + // browser — close it so the pick is visible. + if (panel?.isMobile) panel.close() + }} > {/* Active indicator line */}
) })} -
- +
{/* Footer */} {connections.length > 0 && ( -
+
+ ) } diff --git a/apps/desktop-ui/src/components/sidebar/app-sidebar.tsx b/apps/desktop-ui/src/components/sidebar/app-sidebar.tsx deleted file mode 100644 index 0f6c7f3a..00000000 --- a/apps/desktop-ui/src/components/sidebar/app-sidebar.tsx +++ /dev/null @@ -1,82 +0,0 @@ -'use client' -import Link from 'next/link' -import { usePathname } from 'next/navigation' -import { motion } from 'framer-motion' -import { cn } from '@/lib/utils' -import { - Sidebar, - SidebarContent, - SidebarRail, -} from '@/components/ui/sidebar' -import { NavGroup } from './nav-group' -import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar' -import { LayoutDashboard, Sparkles } from 'lucide-react' -import { usePinnedToolsForActiveWorkspace } from '@/store/pinned-tools-store' -import { useActiveWorkspace } from '@/store/workspace-store' -import { IconPin } from '@tabler/icons-react' -import { buildPinnedNavItems } from './app-sidebar.helpers' - -export function AppSidebar({ ...props }: React.ComponentProps) { - const pinnedTools = usePinnedToolsForActiveWorkspace() - const activeWs = useActiveWorkspace() - const pinnedNavItems = buildPinnedNavItems(pinnedTools, activeWs) - const pathname = usePathname(); - const isDashboardActive = pathname === '/dashboard'; - - return ( - - {/* Brand lockup moved to the unified TopBar (components/shell/top-bar.tsx). - The sidebar is now pure navigation. */} - - - - - - {isDashboardActive && ( - - - - )} - - Dashboard - - - - - {pinnedNavItems.length > 0 ? ( - - ) : ( -
- -

- No pinned tools yet. -

- - Browse all tools → - -
- )} -
- -
- ) -} diff --git a/apps/desktop-ui/src/components/sidebar/client-layout.tsx b/apps/desktop-ui/src/components/sidebar/client-layout.tsx index 46d48d8d..7b345565 100644 --- a/apps/desktop-ui/src/components/sidebar/client-layout.tsx +++ b/apps/desktop-ui/src/components/sidebar/client-layout.tsx @@ -1,7 +1,6 @@ 'use client' import React, { useEffect } from 'react'; import { usePathname } from 'next/navigation'; -import { SidebarProvider, useSidebar } from "@/components/ui/sidebar"; import { NavBar } from '@/components/nav-bar'; import { MobileNav } from '@/components/mobile-nav'; import { TopBar } from '@/components/shell/top-bar'; @@ -58,7 +57,6 @@ function TabSyncer() { } function Layout({ children }: { children: React.ReactNode }) { - const { state } = useSidebar(); const pathname = usePathname(); const { tabs } = useTabStore(); // Keying tool content by workspace remounts every mounted tab (and page) on @@ -69,17 +67,11 @@ function Layout({ children }: { children: React.ReactNode }) { const inTabMode = isTabRoute(pathname) && tabs.length > 0; return ( -
+
-
+
{!inTabMode && } @@ -119,9 +111,5 @@ export function ClientLayout({ children }: { children: React.ReactNode }) { } }, []); - return ( - - {children} - - ); + return {children}; } diff --git a/apps/desktop-ui/src/components/sidebar/nav-group.tsx b/apps/desktop-ui/src/components/sidebar/nav-group.tsx deleted file mode 100644 index dad65ef4..00000000 --- a/apps/desktop-ui/src/components/sidebar/nav-group.tsx +++ /dev/null @@ -1,408 +0,0 @@ -"use client"; - -import { ReactNode } from "react"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; -import { ChevronRight } from "lucide-react"; -import { motion } from "framer-motion"; -import { cn } from "@/lib/utils"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "../ui/collapsible"; -import { Tooltip, TooltipTrigger, TooltipContent } from "../ui/tooltip"; -import { - SidebarGroup, - SidebarGroupLabel, - SidebarGroupContent, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, - useSidebar, -} from "@/components/ui/sidebar"; -import { Badge } from "../ui/badge"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "../ui/dropdown-menu"; -import { NavCollapsible, NavItem, NavLink } from "./types"; // Import types -import useAuth from "@/utils/useAuth"; // Import useAuth -import { useTranslations } from "next-intl"; -import { getToolMessageKey } from "@/lib/tool-i18n"; -import { Star } from "lucide-react"; -import { usePinnedToolsStore, usePinnedToolsForActiveWorkspace } from "@/store/pinned-tools-store"; -import { useWorkspaceStore } from "@/store/workspace-store"; -import { useToolPermission } from "@/lib/workspace-rbac"; -import { useActiveWorkspace } from "@/store/workspace-store"; -import { sidebarUrlToToolSlug } from "@/lib/sidebar-tool-slug"; - -// Define the props interface for NavGroup -interface NavGroupProps { - title: string; - titleKey?: string; - items: (NavLink | NavCollapsible)[]; - collapsible?: boolean; - icon?: React.ElementType; - hiddenOnMobile?: boolean; -} - -/** - * Role-gated wrapper around a single NavLink item. - * For non-/app URLs or personal workspaces, always renders. - * For shared workspaces, hides the link when the active role lacks "read". - */ -function NavLinkGated({ - item, - href, -}: { - item: NavLink; - href: string; -}) { - const itemUrl = typeof item.url === "string" ? item.url : item.url.toString(); - const slug = sidebarUrlToToolSlug(itemUrl); - const canRead = useToolPermission(slug ?? "", "read"); - const activeWs = useActiveWorkspace(); - - // Non-matrix URLs (e.g. /dashboard) and personal workspaces always render. - if (!slug || activeWs?.is_personal) { - return ; - } - if (!canRead) return null; - return ; -} - -// Main NavGroup Component -export function NavGroup({ title, titleKey, items, collapsible, icon: Icon, hiddenOnMobile }: NavGroupProps) { - const { state, isMobile } = useSidebar(); - const pathname = usePathname(); - const { user, loading } = useAuth(false); // Check auth state with loading - const tNav = useTranslations("Navigation"); - const groupTitle = titleKey ? tNav(titleKey as never) : title; - - // Drop collapsible groups that ended up with no children. - const visibleItems = items.filter((item) => { - if ("items" in item && Array.isArray((item as NavCollapsible).items) && (item as NavCollapsible).items.length === 0) return false; - return true; - }); - - if (visibleItems.length === 0) { - return null; - } - - const content = ( - - {visibleItems.map((item) => { - const key = `${item.title}-${item.url}`; - - if (!("items" in item)) { - const itemUrl = - typeof item.url === "string" ? item.url : item.url.toString(); - return ( - - ); - } - - if (state === "collapsed") { - return ( - - ); - } - - return ( - - ); - })} - - ); - - if (collapsible) { - return ( - - - - - {Icon && ( - - - - - {groupTitle} - - )} - {groupTitle} - - - - - - {content} - - - - - ); - } - - return ( - - - {Icon && ( - - - - - {groupTitle} - - )} - {groupTitle} - - {content} - - ); -} - -// SidebarMenuLink Component -const SidebarMenuLink = ({ - item, - href, -}: { - item: NavLink; - href: string; -}) => { - const { setOpenMobile, state } = useSidebar(); - const tNav = useTranslations("Navigation"); - const togglePinKeyed = usePinnedToolsStore((s) => s.togglePin); - const activeWorkspaceId = useWorkspaceStore((s) => s.activeWorkspaceId); - const togglePin = (url: string) => { - if (activeWorkspaceId) togglePinKeyed(activeWorkspaceId, url) - }; - const pinnedTools = usePinnedToolsForActiveWorkspace(); - const itemUrl = - typeof item.url === "string" ? item.url : item.url.toString(); - const toolKey = getToolMessageKey(itemUrl); - const displayTitle = toolKey ? tNav(toolKey as never) : item.title; - const isPinned = pinnedTools.includes(itemUrl); - - const isActive = checkIsActive(href, item); - - return ( - - - - {isActive && ( - - - - )} - {item.icon && ( - - )} - - {displayTitle} - - {item.badge && {item.badge}} - - - {state !== "collapsed" && ( - - )} - - ); -}; - -// SidebarMenuCollapsible Component -const SidebarMenuCollapsible = ({ - item, - href, -}: { - item: NavCollapsible; - href: string; -}) => { - const { setOpenMobile } = useSidebar(); - const { user, loading } = useAuth(false); - const tNav = useTranslations("Navigation"); - const parentLabel = item.titleKey ? tNav(item.titleKey as never) : item.title; - - return ( - - - - - {item.icon && } - {parentLabel} - {item.badge && {item.badge}} - - - -
- - - {item.items.map((subItem) => { - const subItemUrl = - typeof subItem.url === "string" - ? subItem.url - : subItem.url.toString(); - const isSubActive = checkIsActive(href, subItem); - - return ( - - - setOpenMobile(false)} - className="relative flex items-center pl-4 overflow-hidden" - > - {isSubActive && ( - <> - - - - )} - {subItem.icon && } - {subItem.title} - {subItem.badge && {subItem.badge}} - - - - ); - })} - - -
-
-
- ); -}; - -// SidebarMenuCollapsedDropdown Component -const SidebarMenuCollapsedDropdown = ({ - item, - href, -}: { - item: NavCollapsible; - href: string; -}) => { - const { user, loading } = useAuth(false); - const tNav = useTranslations("Navigation"); - const parentLabel = item.titleKey ? tNav(item.titleKey as never) : item.title; - - return ( - -
- - - - {item.icon && } - {parentLabel} - {item.badge && {item.badge}} - - - - - - {parentLabel} {item.badge ? `(${item.badge})` : ""} - - - {item.items.map((sub) => { - const subUrl = - typeof sub.url === "string" ? sub.url : sub.url.toString(); - return ( - - - {sub.icon && } - {sub.title} - {sub.badge && ( - {sub.badge} - )} - - - ); - })} - - -
-
- ); -}; - -// Helper Components and Functions -const NavBadge = ({ children }: { children: ReactNode }) => ( - {children} -); - -function checkIsActive(href: string, item: NavItem, mainNav = false) { - return ( - href === item.url || - href.split("?")[0] === item.url || - !!item?.items?.filter((i) => i.url === href).length || - (mainNav && - href.split("/")[1] !== "" && - href.split("/")[1] === - (typeof item.url === "string" ? item.url.split("/")[1] : "")) - ); -} \ No newline at end of file diff --git a/apps/desktop-ui/src/components/snippet-manager/snippet-manager-tool.tsx b/apps/desktop-ui/src/components/snippet-manager/snippet-manager-tool.tsx index 77ab1af5..2fe6b3b9 100644 --- a/apps/desktop-ui/src/components/snippet-manager/snippet-manager-tool.tsx +++ b/apps/desktop-ui/src/components/snippet-manager/snippet-manager-tool.tsx @@ -68,11 +68,7 @@ import { SheetTitle, SheetTrigger, } from "@/components/ui/sheet"; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, -} from "@/components/ui/resizable"; +import { ToolSidebarActions, ToolSidebarLayout } from "@/components/tools/tool-sidebar"; import { cn } from "@/lib/utils"; import { SnippetMonaco, @@ -899,38 +895,21 @@ export function SnippetManagerTool() { // ── Desktop layout ──────────────────────────────────────────────────────── - return ( - -
- + {/* Header (icon, title, collapse) is owned by ToolSidebarLayout. */} + + -
-
+ + + {/* Search */}
@@ -965,25 +944,29 @@ export function SnippetManagerTool() { }} /> - {/* Count */} - {filtered.length > 0 && ( -
-

- {snHasMore ? `${snDisplayCount} / ${filtered.length}` : filtered.length}{" "} - {filtered.length === 1 ? "snippet" : "snippets"} -

-
- )} -
- - - + {/* Count */} + {filtered.length > 0 && ( +
+

+ {snHasMore ? `${snDisplayCount} / ${filtered.length}` : filtered.length}{" "} + {filtered.length === 1 ? "snippet" : "snippets"} +

+
+ )} + + ); - {/* Editor panel */} - - {editorPanel} - - + return ( + +
+
+ {editorPanel} +
- +
); } diff --git a/apps/desktop-ui/src/components/tools/tool-sidebar.tsx b/apps/desktop-ui/src/components/tools/tool-sidebar.tsx new file mode 100644 index 00000000..04169bd6 --- /dev/null +++ b/apps/desktop-ui/src/components/tools/tool-sidebar.tsx @@ -0,0 +1,262 @@ +'use client' + +import * as React from 'react' +import { createPortal } from 'react-dom' +import { PanelLeftClose, PanelLeftOpen } from 'lucide-react' +import { useTranslations } from 'next-intl' +import { Button } from '@/components/ui/button' +import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet' +import { useIsMobile } from '@/components/hooks/use-mobile' +import { useToolSidebarStore } from '@/store/tool-sidebar-store' +import { toolCategoryMap } from '@/lib/tool-categories' +import { categoryAccent } from '@/components/dashboard/types' +import { cn } from '@/lib/utils' + +/** + * The one in-tool left panel. Every app-like tool (notes, bookmarks, api-client, + * data-explorer, s3-drive, snippet-manager, password-manager, api-keys, + * environment-manager, to-do) renders its list/tree through this so the chrome — + * width, header, collapse affordance, mobile sheet — is identical everywhere. + * + * Desktop collapse state is persisted per tool; the mobile sheet is ephemeral. + */ + +interface ToolSidebarContextValue { + /** True when the panel is inside the mobile sheet rather than the column. */ + isMobile: boolean + /** + * Dismiss the panel. No-op on desktop when the panel is pinned open — call it + * from list rows so picking an item on mobile closes the sheet instead of + * leaving it covering the result. + */ + close: () => void +} + +const ToolSidebarContext = React.createContext(null) +const ActionsSlotContext = React.createContext(null) + +/** Panel-side helper. Returns null outside a ToolSidebarLayout. */ +export function useToolSidebarPanel() { + return React.useContext(ToolSidebarContext) +} + +/** + * Renders header buttons into the panel header from *inside* the panel, so a + * sidebar whose actions depend on its own local state (sort order, filters) + * doesn't have to lift that state into the page just to reach the header. + * Prefer the `actions` prop when the page already owns the handler. + */ +export function ToolSidebarActions({ children }: { children: React.ReactNode }) { + const slot = React.useContext(ActionsSlotContext) + return slot ? createPortal(children, slot) : null +} + +export interface ToolSidebarFilterItem { + id: string + label: string + count?: number + /** Tailwind bg-* class for a leading status dot. */ + dot?: string + icon?: React.ElementType +} + +/** + * The standard panel body for tools that group items by a facet (tags, project, + * environment, status) rather than a tree. One row per bucket, with counts. + */ +export function ToolSidebarFilterList({ + items, + value, + onChange, + heading, + className, +}: { + items: ToolSidebarFilterItem[] + value: string + onChange: (id: string) => void + /** Optional uppercase section label above the rows. */ + heading?: string + className?: string +}) { + const panel = useToolSidebarPanel() + return ( +
+ {heading && ( +

+ {heading} +

+ )} + {items.map((item) => { + const Icon = item.icon + const active = item.id === value + return ( + + ) + })} +
+ ) +} + +interface ToolSidebarLayoutProps { + /** Stable id — also the persistence key for the collapse state. */ + toolId: string + icon: React.ElementType + title: string + /** Header buttons (add, sort, filter…) rendered left of the collapse toggle. */ + actions?: React.ReactNode + /** + * Panel body: the tool's list, tree, or filter set. Laid out as a flex column + * that does NOT scroll — the panel owns its own `overflow-y-auto` region so a + * search field or footer can stay pinned while the list scrolls under it. + */ + sidebar: React.ReactNode + children: React.ReactNode + className?: string +} + +export function ToolSidebarLayout({ + toolId, + icon: Icon, + title, + actions, + sidebar, + children, + className, +}: ToolSidebarLayoutProps) { + const t = useTranslations('ToolSidebar') + const isMobile = useIsMobile() + const collapsed = useToolSidebarStore((s) => !!s.collapsed[toolId]) + const setCollapsed = useToolSidebarStore((s) => s.setCollapsed) + const [sheetOpen, setSheetOpen] = React.useState(false) + const [actionsSlot, setActionsSlot] = React.useState(null) + // Same category tint the dashboard card for this tool uses, so the identity + // colour is continuous from the grid to the tool. Falls back to primary. + const accent = categoryAccent(toolCategoryMap[toolId] ?? '') + + const close = React.useCallback(() => { + if (isMobile) setSheetOpen(false) + else setCollapsed(toolId, true) + }, [isMobile, setCollapsed, toolId]) + + const ctx = React.useMemo( + () => ({ isMobile, close }), + [isMobile, close], + ) + + const panel = ( +
+
+
+ + + + {title} +
+
+ {actions} +
+ {!isMobile && ( + + )} +
+
+ +
{sidebar}
+
+
+ ) + + const hidden = isMobile || collapsed + + return ( + +
+ {!isMobile && !collapsed &&
{panel}
} + + {isMobile && ( + + + + {title} + + {panel} + + + )} + + {/* Re-open lives in a 40px rail, not a floating overlay: every tool's + main pane already puts a toolbar or header at the top-left, and an + absolutely-positioned button would sit on top of it. */} + {hidden && ( +
+ +
+ )} + + {/* Same surface the 71 single-pane tools paint (dashboard-grid-bg + + dash-ambient), so moving between a converter and a workspace tool + doesn't change the background under you. */} +
+
+ {children} +
+
+ + ) +} diff --git a/apps/desktop-ui/src/components/ui/sidebar.tsx b/apps/desktop-ui/src/components/ui/sidebar.tsx deleted file mode 100644 index bbb986c6..00000000 --- a/apps/desktop-ui/src/components/ui/sidebar.tsx +++ /dev/null @@ -1,768 +0,0 @@ -"use client" - -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { VariantProps, cva } from "class-variance-authority" -import { ChevronLeft, ChevronRight, PanelLeft } from "lucide-react" - -import { useIsMobile } from "../hooks/use-mobile" -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Separator } from "@/components/ui/separator" -import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet" -import { Drawer, DrawerContent, DrawerTitle } from "@/components/ui/drawer" -import { Skeleton } from "@/components/ui/skeleton" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" - -const SIDEBAR_COOKIE_NAME = "sidebar:state" -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 -const SIDEBAR_WIDTH = "16rem" -const SIDEBAR_WIDTH_MOBILE = "20rem" -const SIDEBAR_WIDTH_ICON = "3rem" -const SIDEBAR_KEYBOARD_SHORTCUT = "b" - -type SidebarContext = { - state: "expanded" | "collapsed" - open: boolean - setOpen: (open: boolean) => void - openMobile: boolean - setOpenMobile: (open: boolean) => void - isMobile: boolean - toggleSidebar: () => void -} - -const SidebarContext = React.createContext(null) - -function useSidebar() { - const context = React.useContext(SidebarContext) - if (!context) { - throw new Error("useSidebar must be used within a SidebarProvider.") - } - - return context -} - -const SidebarProvider = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - defaultOpen?: boolean - open?: boolean - onOpenChange?: (open: boolean) => void - } ->( - ( - { - defaultOpen = true, - open: openProp, - onOpenChange: setOpenProp, - className, - style, - children, - ...props - }, - ref - ) => { - const isMobile = useIsMobile() - const [openMobile, setOpenMobile] = React.useState(false) - - // This is the internal state of the sidebar. - // We use openProp and setOpenProp for control from outside the component. - const [_open, _setOpen] = React.useState(defaultOpen) - const open = openProp ?? _open - const setOpen = React.useCallback( - (value: boolean | ((value: boolean) => boolean)) => { - const openState = typeof value === "function" ? value(open) : value - if (setOpenProp) { - setOpenProp(openState) - } else { - _setOpen(openState) - } - - // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` - }, - [setOpenProp, open] - ) - - // Helper to toggle the sidebar. - const toggleSidebar = React.useCallback(() => { - return isMobile - ? setOpenMobile((open) => !open) - : setOpen((open) => !open) - }, [isMobile, setOpen, setOpenMobile]) - - // Adds a keyboard shortcut to toggle the sidebar. - React.useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault() - toggleSidebar() - } - } - - window.addEventListener("keydown", handleKeyDown) - return () => window.removeEventListener("keydown", handleKeyDown) - }, [toggleSidebar]) - - // We add a state so that we can do data-state="expanded" or "collapsed". - // This makes it easier to style the sidebar with Tailwind classes. - const state = open ? "expanded" : "collapsed" - - const contextValue = React.useMemo( - () => ({ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - toggleSidebar, - }), - [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] - ) - - return ( - - -
- {children} -
-
-
- ) - } -) -SidebarProvider.displayName = "SidebarProvider" - -const Sidebar = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - side?: "left" | "right" - variant?: "sidebar" | "floating" | "inset" - collapsible?: "offcanvas" | "icon" | "none" - } ->( - ( - { - side = "left", - variant = "sidebar", - collapsible = "offcanvas", - className, - children, - ...props - }, - ref - ) => { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar() - - if (collapsible === "none") { - return ( -
- {children} -
- ) - } - - if (isMobile) { - return ( - - - Navigation Menu -
- {children} -
-
-
- ) - } - - return ( -
- {/* This is what handles the sidebar gap on desktop */} -
- -
- ) - } -) -Sidebar.displayName = "Sidebar" - -const SidebarTrigger = React.forwardRef< - React.ElementRef, - React.ComponentProps ->(({ className, onClick, ...props }, ref) => { - const { toggleSidebar } = useSidebar() - - return ( - - ) -}) -SidebarTrigger.displayName = "SidebarTrigger" - -const SidebarRail = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> ->(({ className, ...props }, ref) => { - const { toggleSidebar, state } = useSidebar() - const isCollapsed = state === "collapsed" - - return ( - - ) -}) -SidebarRail.displayName = "SidebarRail" - -const SidebarInset = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"main"> ->(({ className, ...props }, ref) => { - return ( -
- ) -}) -SidebarInset.displayName = "SidebarInset" - -const SidebarInput = React.forwardRef< - React.ElementRef, - React.ComponentProps ->(({ className, ...props }, ref) => { - return ( - - ) -}) -SidebarInput.displayName = "SidebarInput" - -const SidebarHeader = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => { - return ( -
- ) -}) -SidebarHeader.displayName = "SidebarHeader" - -const SidebarFooter = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => { - return ( -
- ) -}) -SidebarFooter.displayName = "SidebarFooter" - -const SidebarSeparator = React.forwardRef< - React.ElementRef, - React.ComponentProps ->(({ className, ...props }, ref) => { - return ( - - ) -}) -SidebarSeparator.displayName = "SidebarSeparator" - -const SidebarContent = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => { - return ( -
- ) -}) -SidebarContent.displayName = "SidebarContent" - -const SidebarGroup = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => { - return ( -
- ) -}) -SidebarGroup.displayName = "SidebarGroup" - -const SidebarGroupLabel = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { asChild?: boolean } ->(({ className, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "div" - - return ( - svg]:size-4 [&>svg]:shrink-0", - "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", - className - )} - {...props} - /> - ) -}) -SidebarGroupLabel.displayName = "SidebarGroupLabel" - -const SidebarGroupAction = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> & { asChild?: boolean } ->(({ className, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" - - return ( - svg]:size-4 [&>svg]:shrink-0", - // Increases the hit area of the button on mobile. - "after:absolute after:-inset-2 after:md:hidden", - "group-data-[collapsible=icon]:hidden", - className - )} - {...props} - /> - ) -}) -SidebarGroupAction.displayName = "SidebarGroupAction" - -const SidebarGroupContent = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => ( -
-)) -SidebarGroupContent.displayName = "SidebarGroupContent" - -const SidebarMenu = React.forwardRef< - HTMLUListElement, - React.ComponentProps<"ul"> ->(({ className, ...props }, ref) => ( -
    -)) -SidebarMenu.displayName = "SidebarMenu" - -const SidebarMenuItem = React.forwardRef< - HTMLLIElement, - React.ComponentProps<"li"> ->(({ className, ...props }, ref) => ( -
  • -)) -SidebarMenuItem.displayName = "SidebarMenuItem" - -const sidebarMenuButtonVariants = cva( - "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", - { - variants: { - variant: { - default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", - outline: - "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", - }, - size: { - default: "h-8 text-sm", - sm: "h-7 text-xs", - lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -const SidebarMenuButton = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> & { - asChild?: boolean - isActive?: boolean - tooltip?: string | React.ComponentProps - } & VariantProps ->( - ( - { - asChild = false, - isActive = false, - variant = "default", - size = "default", - tooltip, - className, - ...props - }, - ref - ) => { - const Comp = asChild ? Slot : "button" - const { isMobile, state } = useSidebar() - - const button = ( - - ) - - if (!tooltip) { - return button - } - - if (typeof tooltip === "string") { - tooltip = { - children: tooltip, - } - } - - return ( - - {button} - - ) - } -) -SidebarMenuButton.displayName = "SidebarMenuButton" - -const SidebarMenuAction = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> & { - asChild?: boolean - showOnHover?: boolean - } ->(({ className, asChild = false, showOnHover = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" - - return ( - svg]:size-4 [&>svg]:shrink-0", - // Increases the hit area of the button on mobile. - "after:absolute after:-inset-2 after:md:hidden", - "peer-data-[size=sm]/menu-button:top-1", - "peer-data-[size=default]/menu-button:top-1.5", - "peer-data-[size=lg]/menu-button:top-2.5", - "group-data-[collapsible=icon]:hidden", - showOnHover && - "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0", - className - )} - {...props} - /> - ) -}) -SidebarMenuAction.displayName = "SidebarMenuAction" - -const SidebarMenuBadge = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => ( -
    -)) -SidebarMenuBadge.displayName = "SidebarMenuBadge" - -const SidebarMenuSkeleton = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - showIcon?: boolean - } ->(({ className, showIcon = false, ...props }, ref) => { - // Random width between 50 to 90%. - const width = React.useMemo(() => { - return "70%" - }, []) - - return ( -
    - {showIcon && ( - - )} - -
    - ) -}) -SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton" - -const SidebarMenuSub = React.forwardRef< - HTMLUListElement, - React.ComponentProps<"ul"> ->(({ className, ...props }, ref) => ( -
      -)) -SidebarMenuSub.displayName = "SidebarMenuSub" - -const SidebarMenuSubItem = React.forwardRef< - HTMLLIElement, - React.ComponentProps<"li"> ->(({ ...props }, ref) =>
    • ) -SidebarMenuSubItem.displayName = "SidebarMenuSubItem" - -const SidebarMenuSubButton = React.forwardRef< - HTMLAnchorElement, - React.ComponentProps<"a"> & { - asChild?: boolean - size?: "sm" | "md" - isActive?: boolean - } ->(({ asChild = false, size = "md", isActive, className, ...props }, ref) => { - const Comp = asChild ? Slot : "a" - - return ( - span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", - "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", - size === "sm" && "text-xs", - size === "md" && "text-sm", - "group-data-[collapsible=icon]:hidden", - className - )} - {...props} - /> - ) -}) -SidebarMenuSubButton.displayName = "SidebarMenuSubButton" - -export { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupAction, - SidebarGroupContent, - SidebarGroupLabel, - SidebarHeader, - SidebarInput, - SidebarInset, - SidebarMenu, - SidebarMenuAction, - SidebarMenuBadge, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSkeleton, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, - SidebarProvider, - SidebarRail, - SidebarSeparator, - SidebarTrigger, - useSidebar, -} diff --git a/apps/desktop-ui/src/lib/tool-categories.ts b/apps/desktop-ui/src/lib/tool-categories.ts index c12ab4c1..d10f4569 100644 --- a/apps/desktop-ui/src/lib/tool-categories.ts +++ b/apps/desktop-ui/src/lib/tool-categories.ts @@ -5,6 +5,7 @@ export const toolCategoryMap: Record = { notes: 'Productivity', 'snippet-manager': 'Productivity', 'to-do': 'Productivity', + 'api-keys': 'Security', 'email-validator': 'Security', 'environment-manager': 'Security', 'jwt-decoder': 'Security', diff --git a/apps/desktop-ui/src/store/__tests__/tool-sidebar-store.test.ts b/apps/desktop-ui/src/store/__tests__/tool-sidebar-store.test.ts new file mode 100644 index 00000000..f4314ea2 --- /dev/null +++ b/apps/desktop-ui/src/store/__tests__/tool-sidebar-store.test.ts @@ -0,0 +1,36 @@ +/** + * Tests for tool-sidebar-store.ts — per-tool collapse state behind ToolSidebarLayout. + */ +import { useToolSidebarStore } from "../tool-sidebar-store" + +beforeEach(() => { + useToolSidebarStore.setState({ collapsed: {} }) +}) + +describe("tool-sidebar-store", () => { + it("treats an unseen tool as open", () => { + expect(useToolSidebarStore.getState().collapsed["notes"]).toBeUndefined() + }) + + it("collapses and reopens a tool", () => { + const { setCollapsed } = useToolSidebarStore.getState() + setCollapsed("notes", true) + expect(useToolSidebarStore.getState().collapsed["notes"]).toBe(true) + setCollapsed("notes", false) + expect(useToolSidebarStore.getState().collapsed["notes"]).toBe(false) + }) + + it("toggle flips from the open default and back", () => { + const { toggle } = useToolSidebarStore.getState() + toggle("bookmarks") + expect(useToolSidebarStore.getState().collapsed["bookmarks"]).toBe(true) + toggle("bookmarks") + expect(useToolSidebarStore.getState().collapsed["bookmarks"]).toBe(false) + }) + + it("keeps tools independent", () => { + const { setCollapsed } = useToolSidebarStore.getState() + setCollapsed("notes", true) + expect(useToolSidebarStore.getState().collapsed["s3-drive"]).toBeUndefined() + }) +}) diff --git a/apps/desktop-ui/src/store/tool-sidebar-store.ts b/apps/desktop-ui/src/store/tool-sidebar-store.ts new file mode 100644 index 00000000..5694945c --- /dev/null +++ b/apps/desktop-ui/src/store/tool-sidebar-store.ts @@ -0,0 +1,25 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +interface ToolSidebarState { + /** + * Desktop collapse state keyed by tool id. A missing key means "open" — the + * default — so a tool that has never been collapsed stores nothing. + */ + collapsed: Record + setCollapsed: (toolId: string, collapsed: boolean) => void + toggle: (toolId: string) => void +} + +export const useToolSidebarStore = create()( + persist( + (set) => ({ + collapsed: {}, + setCollapsed: (toolId, collapsed) => + set((s) => ({ collapsed: { ...s.collapsed, [toolId]: collapsed } })), + toggle: (toolId) => + set((s) => ({ collapsed: { ...s.collapsed, [toolId]: !s.collapsed[toolId] } })), + }), + { name: 'tool-sidebar-storage' }, + ), +) From 267729f7b5484640062681e54bf1543a5a2b8169 Mon Sep 17 00:00:00 2001 From: AKHIL Date: Sat, 15 Aug 2026 22:36:55 +0530 Subject: [PATCH 07/11] ui --- apps/desktop-ui/src/components/api-key-vault/api-key-list.tsx | 2 +- .../src/components/password-manager/password-list.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop-ui/src/components/api-key-vault/api-key-list.tsx b/apps/desktop-ui/src/components/api-key-vault/api-key-list.tsx index 36a5f3b7..9867dbfd 100644 --- a/apps/desktop-ui/src/components/api-key-vault/api-key-list.tsx +++ b/apps/desktop-ui/src/components/api-key-vault/api-key-list.tsx @@ -207,7 +207,7 @@ export function ApiKeyList() { /> } > -
      +
      diff --git a/apps/desktop-ui/src/components/password-manager/password-list.tsx b/apps/desktop-ui/src/components/password-manager/password-list.tsx index 59cfca67..dabc28c3 100644 --- a/apps/desktop-ui/src/components/password-manager/password-list.tsx +++ b/apps/desktop-ui/src/components/password-manager/password-list.tsx @@ -380,7 +380,7 @@ export function PasswordList() { } >
      {/* Mobile Header */} {isMobile && ( From 3cac7ae01cb6a34271de4e4c06f81919d3e9cf91 Mon Sep 17 00:00:00 2001 From: AKHIL Date: Sat, 15 Aug 2026 22:53:50 +0530 Subject: [PATCH 08/11] Add threatcrush Security scan ignore file --- .github/workflows/release.yml | 6 +++++ .threatcrushignore | 22 +++++++++++++++++++ .../components/api-client/request-tabs.tsx | 1 + .../jwt-decoder/jwt-decoder-layout.tsx | 3 +++ .../password-manager/password-card.tsx | 1 + .../password-item-swipeable.tsx | 1 + .../password-manager/password-list.tsx | 1 + .../components/s3-drive/add-bucket-dialog.tsx | 1 + .../ssh-key-generator-layout.tsx | 3 +++ apps/desktop-ui/src/lib/curl-to-code.ts | 2 ++ .../src/lib/data-explorer/firestore-api.ts | 1 + apps/desktop-ui/src/lib/jwt-decode.ts | 2 ++ apps/desktop-ui/src/lib/nosql-dialects.ts | 1 + 13 files changed, 45 insertions(+) create mode 100644 .threatcrushignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 216cbe71..170f1c21 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -112,6 +112,7 @@ jobs: SIG="$(ls "$BUNDLE"/macos/*.app.tar.gz.sig)" # Pull tauri's generated latest.json from the private release. + # threatcrush-disable-next-line secret-generic-credential GH_TOKEN="$GITHUB_TOKEN" gh release download "$TAG" \ --repo "$GITHUB_REPOSITORY" --pattern latest.json --dir . --clobber @@ -127,9 +128,14 @@ jobs: ' # Create the public release (once) and upload all four assets. + # GH_TOKEN is assigned from an env var, not a literal — the three + # lines below are scanner false positives, not embedded tokens. + # threatcrush-disable-next-line secret-generic-credential if ! GH_TOKEN="$RELEASES_TOKEN" gh release view "$TAG" --repo "$PUBLIC_REPO" >/dev/null 2>&1; then + # threatcrush-disable-next-line secret-generic-credential GH_TOKEN="$RELEASES_TOKEN" gh release create "$TAG" --repo "$PUBLIC_REPO" \ --title "MyDevTools $TAG" --notes "MyDevTools desktop $TAG" fi + # threatcrush-disable-next-line secret-generic-credential GH_TOKEN="$RELEASES_TOKEN" gh release upload "$TAG" --repo "$PUBLIC_REPO" \ --clobber "$DMG" "$TARGZ" "$SIG" latest.json diff --git a/.threatcrushignore b/.threatcrushignore new file mode 100644 index 00000000..0ae38730 --- /dev/null +++ b/.threatcrushignore @@ -0,0 +1,22 @@ +# Paths ThreatCrush should not scan. Everything listed here is sample data, +# UI copy, or generated output — never a credential this repo actually holds. +# Real code stays scanned: for a one-off false positive in source, put a +# `threatcrush-disable-next-line ` comment above the line instead of +# excluding the whole file. + +# Locale catalogues (27 locales x 2 apps). These are the UI strings for the +# SSH-key, JWT, S3 and database tools: example PEM markers, sample connection +# strings and placeholder keys shown to the user. 41 findings, all copy. +apps/*/messages + +# Blog posts: long-form tutorial prose with illustrative credentials in code +# fences ("mongodb://admin:secret@localhost", a demo master key). +apps/*/src/lib/blog/posts.ts + +# Audit lab scripts: fake tokens injected into a throwaway browser session to +# measure egress. Nothing here authenticates against anything. +scripts/audit + +# Generated / local-only trees. Not in a CI checkout, noisy in a local run. +.claude/worktrees +.open-next diff --git a/apps/desktop-ui/src/components/api-client/request-tabs.tsx b/apps/desktop-ui/src/components/api-client/request-tabs.tsx index d257efa8..d12e2e4f 100644 --- a/apps/desktop-ui/src/components/api-client/request-tabs.tsx +++ b/apps/desktop-ui/src/components/api-client/request-tabs.tsx @@ -1189,6 +1189,7 @@ export function RequestTabs({