From b6927d3e0c109af5513143c8dfeca5efd3078297 Mon Sep 17 00:00:00 2001 From: Mark Evola Date: Sun, 10 Aug 2025 00:18:59 -0500 Subject: [PATCH 1/4] ui component improvements; theme provider --- components.json | 21 + package.json | 7 +- pnpm-lock.yaml | 361 ++++++++++++++++-- src/components/Navbar/Navbar.tsx | 96 +++-- .../NavbarFileSaveStatus.tsx | 36 ++ .../ThemeProvider/ThemeProvider.tsx | 84 ++++ src/components/icons/Close/Close.tsx | 4 +- .../icons/CloudUpload/CloudUpload.tsx | 11 + src/components/ui/menubar.tsx | 274 +++++++++++++ src/renderer/PageShell.css | 117 ------ src/renderer/PageShell.tsx | 2 +- src/renderer/_default.page.client.tsx | 9 +- src/renderer/_default.page.server.tsx | 9 +- src/renderer/index.css | 60 ++- src/state/hooks/useTheme.ts | 8 + tailwind.config.js | 66 +++- 16 files changed, 960 insertions(+), 205 deletions(-) create mode 100644 components.json create mode 100644 src/components/NavbarFileSaveStatus/NavbarFileSaveStatus.tsx create mode 100644 src/components/ThemeProvider/ThemeProvider.tsx create mode 100644 src/components/icons/CloudUpload/CloudUpload.tsx create mode 100644 src/components/ui/menubar.tsx delete mode 100644 src/renderer/PageShell.css create mode 100644 src/state/hooks/useTheme.ts diff --git a/components.json b/components.json new file mode 100644 index 0000000..e3b8873 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/renderer/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/package.json b/package.json index 12ed8dc..7f5724b 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,9 @@ "dependencies": { "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", - "@radix-ui/react-menubar": "^1.1.11", + "@microsoft/signalr": "^8.0.7", + "@radix-ui/react-menubar": "^1.1.15", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.3", "@types/compression": "^1.7.2", "@types/express": "^4.17.17", @@ -40,9 +42,11 @@ "@typescript-eslint/parser": "^6.3.0", "@vitejs/plugin-react": "^4.0.4", "autoprefixer": "^10.4.21", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "compression": "^1.7.4", "cross-env": "^7.0.3", + "dotenv": "^16.4.7", "eslint": "^8.47.0", "eslint-plugin-react": "^7.33.1", "eslint-plugin-react-hooks": "^4.6.0", @@ -56,6 +60,7 @@ "react-streaming": "^0.3.46", "sirv": "^2.0.3", "tailwindcss": "^3.4.17", + "tailwindcss-animate": "^1.0.7", "ts-node": "^10.9.2", "typescript": "^5.1.6", "uuid": "^10.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da53ae1..949b151 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,15 @@ dependencies: '@emotion/styled': specifier: ^11.13.0 version: 11.13.0(@emotion/react@11.13.3)(@types/react@19.0.2)(react@19.0.0) + '@microsoft/signalr': + specifier: ^8.0.7 + version: 8.0.17 '@radix-ui/react-menubar': - specifier: ^1.1.11 - version: 1.1.11(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.3(@types/react@19.0.2)(react@19.0.0) '@radix-ui/react-tooltip': specifier: ^1.2.3 version: 1.2.3(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) @@ -47,6 +53,9 @@ dependencies: autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 clsx: specifier: ^2.1.1 version: 2.1.1 @@ -56,6 +65,9 @@ dependencies: cross-env: specifier: ^7.0.3 version: 7.0.3 + dotenv: + specifier: ^16.4.7 + version: 16.6.1 eslint: specifier: ^8.47.0 version: 8.47.0 @@ -95,6 +107,9 @@ dependencies: tailwindcss: specifier: ^3.4.17 version: 3.4.17(ts-node@10.9.2) + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.17) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@20.4.10)(typescript@5.1.6) @@ -1342,6 +1357,20 @@ packages: '@jridgewell/sourcemap-codec': 1.5.0 dev: false + /@microsoft/signalr@8.0.17: + resolution: {integrity: sha512-5pM6xPtKZNJLO0Tq5nQasVyPFwi/WBY3QB5uc/v3dIPTpS1JXQbaXAQAPxFoQ5rTBFE094w8bbqkp17F9ReQvA==} + dependencies: + abort-controller: 3.0.0 + eventsource: 2.0.2 + fetch-cookie: 2.2.0 + node-fetch: 2.7.0 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1399,8 +1428,28 @@ packages: react-dom: 19.0.0(react@19.0.0) dev: false - /@radix-ui/react-collection@1.1.4(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): - resolution: {integrity: sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==} + /@radix-ui/react-arrow@1.1.7(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@types/react': 19.0.2 + '@types/react-dom': 19.0.2(@types/react@19.0.2) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + dev: false + + /@radix-ui/react-collection@1.1.7(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1414,8 +1463,8 @@ packages: dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.2)(react@19.0.0) '@radix-ui/react-context': 1.1.2(@types/react@19.0.2)(react@19.0.0) - '@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) - '@radix-ui/react-slot': 1.2.0(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.0.2)(react@19.0.0) '@types/react': 19.0.2 '@types/react-dom': 19.0.2(@types/react@19.0.2) react: 19.0.0 @@ -1461,6 +1510,30 @@ packages: react: 19.0.0 dev: false + /@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): + resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.0.2)(react@19.0.0) + '@types/react': 19.0.2 + '@types/react-dom': 19.0.2(@types/react@19.0.2) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + dev: false + /@radix-ui/react-dismissable-layer@1.1.7(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): resolution: {integrity: sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==} peerDependencies: @@ -1498,8 +1571,8 @@ packages: react: 19.0.0 dev: false - /@radix-ui/react-focus-scope@1.1.4(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): - resolution: {integrity: sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA==} + /@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1512,7 +1585,7 @@ packages: optional: true dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.2)(react@19.0.0) - '@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.2)(react@19.0.0) '@types/react': 19.0.2 '@types/react-dom': 19.0.2(@types/react@19.0.2) @@ -1534,8 +1607,8 @@ packages: react: 19.0.0 dev: false - /@radix-ui/react-menu@2.1.11(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): - resolution: {integrity: sha512-sbFI4Qaw02J0ogmR9tOMsSqsdrGNpUanlPYAqTE2JJafow8ecHtykg4fSTjNHBdDl4deiKMK+RhTEwyVhP7UDA==} + /@radix-ui/react-menu@2.1.15(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): + resolution: {integrity: sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1548,20 +1621,20 @@ packages: optional: true dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.4(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.2)(react@19.0.0) '@radix-ui/react-context': 1.1.2(@types/react@19.0.2)(react@19.0.0) '@radix-ui/react-direction': 1.1.1(@types/react@19.0.2)(react@19.0.0) - '@radix-ui/react-dismissable-layer': 1.1.7(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.0.2)(react@19.0.0) - '@radix-ui/react-focus-scope': 1.1.4(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) '@radix-ui/react-id': 1.1.1(@types/react@19.0.2)(react@19.0.0) - '@radix-ui/react-popper': 1.2.4(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) - '@radix-ui/react-portal': 1.1.6(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) - '@radix-ui/react-presence': 1.1.3(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) - '@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) - '@radix-ui/react-roving-focus': 1.1.7(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) - '@radix-ui/react-slot': 1.2.0(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.0.2)(react@19.0.0) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.2)(react@19.0.0) '@types/react': 19.0.2 '@types/react-dom': 19.0.2(@types/react@19.0.2) @@ -1571,8 +1644,8 @@ packages: react-remove-scroll: 2.6.3(@types/react@19.0.2)(react@19.0.0) dev: false - /@radix-ui/react-menubar@1.1.11(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): - resolution: {integrity: sha512-p+eVYsEiIyJOgVeUNqjKPSKWQAbRLQDWJHkAHODZu1HLFAAz1G/yFinEayprzJnmmH+FqUD/LjHzFO4qNj+GhQ==} + /@radix-ui/react-menubar@1.1.15(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): + resolution: {integrity: sha512-Z71C7LGD+YDYo3TV81paUs8f3Zbmkvg6VLRQpKYfzioOE6n7fOhA3ApK/V/2Odolxjoc4ENk8AYCjohCNayd5A==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1585,14 +1658,14 @@ packages: optional: true dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.4(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.2)(react@19.0.0) '@radix-ui/react-context': 1.1.2(@types/react@19.0.2)(react@19.0.0) '@radix-ui/react-direction': 1.1.1(@types/react@19.0.2)(react@19.0.0) '@radix-ui/react-id': 1.1.1(@types/react@19.0.2)(react@19.0.0) - '@radix-ui/react-menu': 2.1.11(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) - '@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) - '@radix-ui/react-roving-focus': 1.1.7(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.2)(react@19.0.0) '@types/react': 19.0.2 '@types/react-dom': 19.0.2(@types/react@19.0.2) @@ -1629,6 +1702,35 @@ packages: react-dom: 19.0.0(react@19.0.0) dev: false + /@radix-ui/react-popper@1.2.7(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): + resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/rect': 1.1.1 + '@types/react': 19.0.2 + '@types/react-dom': 19.0.2(@types/react@19.0.2) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + dev: false + /@radix-ui/react-portal@1.1.6(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): resolution: {integrity: sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==} peerDependencies: @@ -1650,6 +1752,27 @@ packages: react-dom: 19.0.0(react@19.0.0) dev: false + /@radix-ui/react-portal@1.1.9(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.2)(react@19.0.0) + '@types/react': 19.0.2 + '@types/react-dom': 19.0.2(@types/react@19.0.2) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + dev: false + /@radix-ui/react-presence@1.1.3(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): resolution: {integrity: sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==} peerDependencies: @@ -1671,6 +1794,27 @@ packages: react-dom: 19.0.0(react@19.0.0) dev: false + /@radix-ui/react-presence@1.1.4(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): + resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.2)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.2)(react@19.0.0) + '@types/react': 19.0.2 + '@types/react-dom': 19.0.2(@types/react@19.0.2) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + dev: false + /@radix-ui/react-primitive@2.1.0(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): resolution: {integrity: sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==} peerDependencies: @@ -1691,8 +1835,28 @@ packages: react-dom: 19.0.0(react@19.0.0) dev: false - /@radix-ui/react-roving-focus@1.1.7(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): - resolution: {integrity: sha512-C6oAg451/fQT3EGbWHbCQjYTtbyjNO1uzQgMzwyivcHT3GKNEmu1q3UuREhN+HzHAVtv3ivMVK08QlC+PkYw9Q==} + /@radix-ui/react-primitive@2.1.3(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.0.2)(react@19.0.0) + '@types/react': 19.0.2 + '@types/react-dom': 19.0.2(@types/react@19.0.2) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + dev: false + + /@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): + resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1705,12 +1869,12 @@ packages: optional: true dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.4(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.2)(react@19.0.0) '@radix-ui/react-context': 1.1.2(@types/react@19.0.2)(react@19.0.0) '@radix-ui/react-direction': 1.1.1(@types/react@19.0.2)(react@19.0.0) '@radix-ui/react-id': 1.1.1(@types/react@19.0.2)(react@19.0.0) - '@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0) '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.2)(react@19.0.0) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.2)(react@19.0.0) '@types/react': 19.0.2 @@ -1733,6 +1897,20 @@ packages: react: 19.0.0 dev: false + /@radix-ui/react-slot@1.2.3(@types/react@19.0.2)(react@19.0.0): + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.2)(react@19.0.0) + '@types/react': 19.0.2 + react: 19.0.0 + dev: false + /@radix-ui/react-tooltip@1.2.3(@types/react-dom@19.0.2)(@types/react@19.0.2)(react-dom@19.0.0)(react@19.0.0): resolution: {integrity: sha512-0KX7jUYFA02np01Y11NWkk6Ip6TqMNmD4ijLelYAzeIndl2aVeltjJFJ2gwjNa1P8U/dgjQ+8cr9Y3Ni+ZNoRA==} peerDependencies: @@ -4030,6 +4208,13 @@ packages: tinyrainbow: 1.2.0 dev: true + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: false + /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -4440,6 +4625,12 @@ packages: fsevents: 2.3.3 dev: false + /class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + dependencies: + clsx: 2.1.1 + dev: false + /cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -4767,6 +4958,11 @@ packages: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} dev: true + /dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + dev: false + /dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -5181,10 +5377,20 @@ packages: engines: {node: '>= 0.6'} dev: false + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: false + /eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} dev: true + /eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + dev: false + /execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} @@ -5273,6 +5479,13 @@ packages: dependencies: reusify: 1.0.4 + /fetch-cookie@2.2.0: + resolution: {integrity: sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==} + dependencies: + set-cookie-parser: 2.7.1 + tough-cookie: 4.1.4 + dev: false + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -6312,6 +6525,18 @@ packages: engines: {node: '>= 0.6'} dev: false + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + /node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} dev: false @@ -6686,6 +6911,12 @@ packages: ipaddr.js: 1.9.1 dev: false + /psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + dependencies: + punycode: 2.3.1 + dev: false + /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -6697,6 +6928,10 @@ packages: side-channel: 1.1.0 dev: false + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: false + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -6983,6 +7218,10 @@ packages: set-function-name: 2.0.2 dev: false + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: false + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -7171,6 +7410,10 @@ packages: - supports-color dev: false + /set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + dev: false + /set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -7477,6 +7720,14 @@ packages: '@pkgr/core': 0.2.9 dev: true + /tailwindcss-animate@1.0.7(tailwindcss@3.4.17): + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + dependencies: + tailwindcss: 3.4.17(ts-node@10.9.2) + dev: false + /tailwindcss@3.4.17(ts-node@10.9.2): resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} engines: {node: '>=14.0.0'} @@ -7583,6 +7834,16 @@ packages: engines: {node: '>=6'} dev: false + /tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: false + /tough-cookie@5.1.0: resolution: {integrity: sha512-rvZUv+7MoBYTiDmFPBrhL7Ujx9Sk+q9wwm22x8c8T5IJaR+Wsyc7TNxbVxo84kZoRJZZMazowFLqpankBEQrGg==} engines: {node: '>=16'} @@ -7590,6 +7851,10 @@ packages: tldts: 6.1.76 dev: true + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + /tr46@5.0.0: resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} engines: {node: '>=18'} @@ -7724,6 +7989,11 @@ packages: which-boxed-primitive: 1.1.1 dev: false + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: false + /unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -7745,6 +8015,13 @@ packages: dependencies: punycode: 2.3.1 + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: false + /use-callback-ref@1.3.3(@types/react@19.0.2)(react@19.0.0): resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -8002,6 +8279,10 @@ packages: xml-name-validator: 5.0.0 dev: true + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + /webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -8027,6 +8308,13 @@ packages: webidl-conversions: 7.0.0 dev: true + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + /which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -8127,6 +8415,19 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + /ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx index b936e25..0b97598 100644 --- a/src/components/Navbar/Navbar.tsx +++ b/src/components/Navbar/Navbar.tsx @@ -1,6 +1,6 @@ // Lib import logo from "@/assets/icons/IdeaDrawnNewLogo_transparent.png"; -import { useRef, useCallback, useEffect } from "react"; +import { useRef, useCallback, useEffect, useState } from "react"; import { useShallow } from "zustand/shallow"; import useStore from "@/state/hooks/useStore"; import LayersStore from "@/state/stores/LayersStore"; @@ -11,13 +11,21 @@ import Fullscreen from "@/components/icons/Fullscreen/Fullscreen"; import Image from "@/components/icons/Image/Image"; import Export from "@/components/icons/Export/Export"; import FloppyDisk from "@/components/icons/FloppyDisk/FloppyDisk"; +import Close from "@/components/icons/Close/Close"; // Types import type { ComponentProps, ReactElement, ReactNode } from "react"; // Components -import * as Menubar from "@radix-ui/react-menubar"; -import Tooltip from "../Tooltip/Tooltip"; +import { + Menubar, + MenubarContent, + MenubarItem, + MenubarTrigger, + MenubarMenu, + MenubarPortal +} from "@/components/ui/menubar"; +import NavbarFileSaveStatus from "../NavbarFileSaveStatus/NavbarFileSaveStatus"; function Navbar(): ReactNode { const { prepareForExport, prepareForSave, toggleReferenceWindow } = useStore( @@ -28,7 +36,9 @@ function Navbar(): ReactNode { })) ); const downloadRef = useRef(null); - + const [saveStatus, setSaveStatus] = useState<"saving" | "saved" | "error">( + "saved" + ); const menuTabs = ["File", "Edit", "View", "Filter", "Admin"]; type MenuOptions = { @@ -41,6 +51,7 @@ function Navbar(): ReactNode { const handleSaveFile = useCallback(async () => { try { + setSaveStatus("saving"); const { layers, elements } = prepareForSave(); if (layers.length === 0) { @@ -61,7 +72,7 @@ function Navbar(): ReactNode { await Promise.all(promises); - alert("Saved!"); + setSaveStatus("saved"); } catch (e) { alert("Error saving file. Reason: " + (e as Error).message); } @@ -148,54 +159,55 @@ function Navbar(): ReactNode { alt="logo" /> - + {menuTabs.map((tab) => { - if (!menuOptions[tab]) { - return ( - - - {tab} + const options = menuOptions[tab]; + let content; + if (!options || options.length === 0) { + content = ( + + + - + No options available + ); + } else { + content = options.map((option) => { + return ( + + {option.icon && ( + + + + )} + {option.text} + + ); + }); } - return ( - - + + {tab} - - - + + - {menuOptions[tab].map((option) => ( - - {option.icon && ( - - - - )} - {option.text} - - ))} - - - + {content} + + + ); })} - + + + + + + ); + } + if (status === "saved") { + return ( + + + + ); + } + + return ( + + + + ); +} + +export default NavbarFileSaveStatus; diff --git a/src/components/ThemeProvider/ThemeProvider.tsx b/src/components/ThemeProvider/ThemeProvider.tsx new file mode 100644 index 0000000..315482e --- /dev/null +++ b/src/components/ThemeProvider/ThemeProvider.tsx @@ -0,0 +1,84 @@ +import { + ReactNode, + createContext, + useState, + useEffect, + useMemo, + useCallback +} from "react"; + +type Theme = "light" | "dark" | "system"; + +type ThemeContext = { + theme: Theme; + setTheme: (theme: Theme) => void; +}; + +const ThemeContext = createContext({ + theme: "light", + setTheme: () => {} +}); + +type ThemeProviderProps = { + children: ReactNode; + initialTheme?: Theme; + key?: string; +}; + +function ThemeProvider({ + children, + initialTheme = "dark", + key = "id-theme" +}: ThemeProviderProps) { + const [theme, setTheme] = useState(() => { + if (typeof window === "undefined") { + return initialTheme; + } + const storedTheme = window.localStorage.getItem(key); + if (storedTheme) { + return storedTheme as Theme; + } + return initialTheme; + }); + + useEffect(() => { + const root = document.documentElement; + + if (theme === "system") { + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") + .matches + ? "dark" + : "light"; + + root.classList.add(systemTheme); + } else { + root.classList.add(theme); + } + + return () => { + root.classList.remove("light", "dark", "system"); + }; + }, []); + + const updateTheme = useCallback( + (theme: Theme) => { + window.localStorage.setItem(key, theme); + setTheme(theme); + }, + [key] + ); + + const value = useMemo( + () => ({ + theme, + setTheme: updateTheme + }), + [theme, updateTheme] + ); + + return ( + {children} + ); +} + +export { ThemeProvider, ThemeContext }; diff --git a/src/components/icons/Close/Close.tsx b/src/components/icons/Close/Close.tsx index 23a3dbc..3318324 100644 --- a/src/components/icons/Close/Close.tsx +++ b/src/components/icons/Close/Close.tsx @@ -1,8 +1,8 @@ -import { CircleX as LucideCircleX } from "lucide-react"; +import { XIcon as LucideXIcon } from "lucide-react"; import type { ComponentProps } from "react"; const Close = (props: ComponentProps<"svg">) => ( - diff --git a/src/components/icons/CloudUpload/CloudUpload.tsx b/src/components/icons/CloudUpload/CloudUpload.tsx new file mode 100644 index 0000000..4d11ede --- /dev/null +++ b/src/components/icons/CloudUpload/CloudUpload.tsx @@ -0,0 +1,11 @@ +import { CloudUpload as LucideCloudUpload } from "lucide-react"; +import type { ComponentProps } from "react"; + +const CloudUpload = (props: ComponentProps<"svg">) => ( + +); + +export default CloudUpload; \ No newline at end of file diff --git a/src/components/ui/menubar.tsx b/src/components/ui/menubar.tsx new file mode 100644 index 0000000..f4c2615 --- /dev/null +++ b/src/components/ui/menubar.tsx @@ -0,0 +1,274 @@ +import * as React from "react" +import * as MenubarPrimitive from "@radix-ui/react-menubar" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import clsx from "clsx" + +function Menubar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MenubarMenu({ + ...props +}: React.ComponentProps) { + return +} + +function MenubarGroup({ + ...props +}: React.ComponentProps) { + return +} + +function MenubarPortal({ + ...props +}: React.ComponentProps) { + return +} + +function MenubarRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MenubarTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MenubarContent({ + className, + align = "start", + alignOffset = -4, + sideOffset = 8, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function MenubarItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function MenubarCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function MenubarRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function MenubarLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function MenubarSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function MenubarShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function MenubarSub({ + ...props +}: React.ComponentProps) { + return +} + +function MenubarSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function MenubarSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Menubar, + MenubarPortal, + MenubarMenu, + MenubarTrigger, + MenubarContent, + MenubarGroup, + MenubarSeparator, + MenubarLabel, + MenubarItem, + MenubarShortcut, + MenubarCheckboxItem, + MenubarRadioGroup, + MenubarRadioItem, + MenubarSub, + MenubarSubTrigger, + MenubarSubContent, +} diff --git a/src/renderer/PageShell.css b/src/renderer/PageShell.css deleted file mode 100644 index 3d702d4..0000000 --- a/src/renderer/PageShell.css +++ /dev/null @@ -1,117 +0,0 @@ -/* This CSS is common to all pages - -body { - margin: 0; - font-family: sans-serif; -} -* { - box-sizing: border-box; -} -a { - text-decoration: none; -} - -.navitem { - padding: 3px 10px; -} -.navitem.is-active { - background-color: #eee; -} */ -* { - box-sizing: border-box; - user-select: none; -} - -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - /* color: rgba(255, 255, 255, 0.87); */ - background-color: #16191f; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - margin: 0; - padding: 0; - /* min-height: 100vh; */ - /* height: 100vh; */ - /* max-height: 100vh; */ - /* overflow: hidden; */ -} -/* -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} */ - -/* @media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} */ - -#MarchingAnts { - fill: none; - stroke: #d1836a; - stroke-width: 2px; - stroke-dasharray: 2px; - animation: MarchingAnts 1s linear infinite; - shape-rendering: geometricPrecision; - stroke-dashoffset: 8px; -} - -@keyframes MarchingAnts { - to { - stroke-dashoffset: 0; - } -} diff --git a/src/renderer/PageShell.tsx b/src/renderer/PageShell.tsx index 6dda794..bf81186 100644 --- a/src/renderer/PageShell.tsx +++ b/src/renderer/PageShell.tsx @@ -1,7 +1,7 @@ import React from "react"; import { PageContextProvider } from "./usePageContext"; import type { PageContext } from "./types"; -import "./PageShell.css"; +import useTheme from "@/state/hooks/useTheme"; export { PageShell }; diff --git a/src/renderer/_default.page.client.tsx b/src/renderer/_default.page.client.tsx index 54662bb..dc10cc9 100644 --- a/src/renderer/_default.page.client.tsx +++ b/src/renderer/_default.page.client.tsx @@ -2,6 +2,7 @@ export { render }; import { hydrateRoot } from "react-dom/client"; import { PageShell } from "./PageShell"; +import { ThemeProvider } from "@/components/ThemeProvider/ThemeProvider"; import type { PageContextClient } from "./types"; import "./index.css"; @@ -17,9 +18,11 @@ async function render(pageContext: PageContextClient) { hydrateRoot( root, - - - + + + + + ); } diff --git a/src/renderer/_default.page.server.tsx b/src/renderer/_default.page.server.tsx index 586192f..a008b87 100644 --- a/src/renderer/_default.page.server.tsx +++ b/src/renderer/_default.page.server.tsx @@ -9,6 +9,7 @@ import type { PageContextServer } from "./types"; import { renderToStream } from "react-streaming/server"; import { initializeStore } from "@/state/store"; import type { SliceStores } from "@/types"; +import { ThemeProvider } from "@/components/ThemeProvider/ThemeProvider"; async function render(pageContext: PageContextServer) { const { Page, pageProps } = pageContext; @@ -27,9 +28,11 @@ async function render(pageContext: PageContextServer) { pageContext.zustandState = stateWithoutFunctions; const html = await renderToStream( - - - + + + + + ); // See https://vite-plugin-ssr.com/head diff --git a/src/renderer/index.css b/src/renderer/index.css index bd6213e..6487839 100644 --- a/src/renderer/index.css +++ b/src/renderer/index.css @@ -1,3 +1,61 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; + +@config "../../tailwind.config.js"; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 5.9% 10%; + --radius: 0.5rem; + } + + .dark { + --background: 220 17% 10.4%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 220 17% 10.4%; + --popover-foreground: 0 0% 100%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/src/state/hooks/useTheme.ts b/src/state/hooks/useTheme.ts new file mode 100644 index 0000000..9ba46b0 --- /dev/null +++ b/src/state/hooks/useTheme.ts @@ -0,0 +1,8 @@ +import { useContext } from "react"; +import { ThemeContext } from "@/components/ThemeProvider/ThemeProvider"; + +function useTheme() { + return useContext(ThemeContext); +} + +export default useTheme; \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index 1b55a7d..aa75949 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,8 +1,52 @@ /** @type {import('tailwindcss').Config} */ export default { + darkMode: "class", + prefix: "", content: ["./src/**/*.{js,jsx,ts,tsx}", "./src/renderer/*.tsx"], theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2x1": "1400pxs" + } + }, extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))" + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))" + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))" + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))" + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))" + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))" + }, + // Custom accent color + accent: { + DEFAULT: "rgba(234, 146, 118, 0.6)" + } + }, animation: { "tooltip-slide-down": "slideDownAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)", @@ -14,12 +58,24 @@ export default { "slideLeftAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)", "menubar-appear": "menubarAppear 400ms cubic-bezier(0.16, 1, 0.3, 1)", "popover-slide": "popoverSlide 200ms", - "popover-slide-reverse": "popoverSlide 200ms reverse ease-in" + "popover-slide-reverse": "popoverSlide 200ms reverse ease-in", + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out" }, - gridTemplateAreas: { - "color-slider": ["label output", "track track"] + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)" }, keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" } + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" } + }, slideUpAndFade: { from: { opacity: 0, transform: "translateY(2px)" }, to: { opacity: 1, transform: "translateY(0)" } @@ -41,11 +97,11 @@ export default { to: { opacity: 1, transform: "scale(1)" } }, popoverSlide: { - from: { transform: "var(--origin)", opacity: 0 }, + from: { transform: "tanslateY(-10px)", opacity: 0 }, to: { transform: "translateY(0)", opacity: 1 } } } } }, - plugins: [] + plugins: [require("tailwindcss-animate")] }; From 087b585b6997748c5a6a82ef2d007ac9f6acf699 Mon Sep 17 00:00:00 2001 From: Mark Evola Date: Sun, 10 Aug 2025 16:14:40 -0500 Subject: [PATCH 2/4] add theme context --- package.json | 1 + pnpm-lock.yaml | 16 ++++++++++++++++ src/components/ThemeProvider/ThemeProvider.tsx | 8 ++++++-- src/lib/utils.ts | 8 +++++++- src/renderer/PageShell.tsx | 1 - src/renderer/_default.page.client.tsx | 4 ++-- src/renderer/_default.page.server.tsx | 13 +++++++++---- src/renderer/types.ts | 1 + src/server/index.ts | 6 +++++- 9 files changed, 47 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 7f5724b..9c55dfe 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "compression": "^1.7.4", + "cookie-parser": "^1.4.7", "cross-env": "^7.0.3", "dotenv": "^16.4.7", "eslint": "^8.47.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 949b151..59c09bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ dependencies: compression: specifier: ^1.7.4 version: 1.7.4 + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -4730,6 +4733,14 @@ packages: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: false + /cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + dev: false + /cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} dev: false @@ -4739,6 +4750,11 @@ packages: engines: {node: '>= 0.6'} dev: false + /cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + dev: false + /cosmiconfig@7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} diff --git a/src/components/ThemeProvider/ThemeProvider.tsx b/src/components/ThemeProvider/ThemeProvider.tsx index 315482e..a0195b5 100644 --- a/src/components/ThemeProvider/ThemeProvider.tsx +++ b/src/components/ThemeProvider/ThemeProvider.tsx @@ -1,3 +1,4 @@ +import { getCookie } from "@/lib/utils"; import { ReactNode, createContext, @@ -34,7 +35,8 @@ function ThemeProvider({ if (typeof window === "undefined") { return initialTheme; } - const storedTheme = window.localStorage.getItem(key); + + const storedTheme = getCookie(key); if (storedTheme) { return storedTheme as Theme; } @@ -44,6 +46,8 @@ function ThemeProvider({ useEffect(() => { const root = document.documentElement; + root.classList.remove("light", "dark", "system"); + if (theme === "system") { const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") .matches @@ -62,7 +66,7 @@ function ThemeProvider({ const updateTheme = useCallback( (theme: Theme) => { - window.localStorage.setItem(key, theme); + document.cookie = `${key}=${theme}; path=/; max-age=31536000; secure; samesite=strict`; setTheme(theme); }, [key] diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 512b1b4..22c7145 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -190,6 +190,11 @@ function debounce( }, ms); }; } + +function getCookie(name: string): string | null { + const match = document.cookie.match(/(^| )${name}=([^;]+)/); + return match ? decodeURIComponent(match[2]) : null; +} export { capitalize, @@ -198,5 +203,6 @@ export { getCanvasPosition, navigateTo, isRectIntersecting, - debounce + debounce, + getCookie }; diff --git a/src/renderer/PageShell.tsx b/src/renderer/PageShell.tsx index bf81186..95e4c1e 100644 --- a/src/renderer/PageShell.tsx +++ b/src/renderer/PageShell.tsx @@ -1,7 +1,6 @@ import React from "react"; import { PageContextProvider } from "./usePageContext"; import type { PageContext } from "./types"; -import useTheme from "@/state/hooks/useTheme"; export { PageShell }; diff --git a/src/renderer/_default.page.client.tsx b/src/renderer/_default.page.client.tsx index dc10cc9..5d0f880 100644 --- a/src/renderer/_default.page.client.tsx +++ b/src/renderer/_default.page.client.tsx @@ -8,7 +8,7 @@ import "./index.css"; // This render() hook only supports SSR, see https://vite-plugin-ssr.com/render-modes for how to modify render() to support SPA async function render(pageContext: PageContextClient) { - const { Page, pageProps } = pageContext; + const { Page, pageProps, theme } = pageContext; if (!Page) throw new Error( "Client-side render() hook expects pageContext.Page to be defined" @@ -18,7 +18,7 @@ async function render(pageContext: PageContextClient) { hydrateRoot( root, - + diff --git a/src/renderer/_default.page.server.tsx b/src/renderer/_default.page.server.tsx index a008b87..67141d3 100644 --- a/src/renderer/_default.page.server.tsx +++ b/src/renderer/_default.page.server.tsx @@ -1,6 +1,11 @@ export { render }; // See https://vite-plugin-ssr.com/data-fetching -export const passToClient = ["pageProps", "urlPathname", "zustandState"]; +export const passToClient = [ + "pageProps", + "urlPathname", + "zustandState", + "theme" +]; import { PageShell } from "./PageShell"; import { escapeInject } from "vite-plugin-ssr/server"; @@ -12,7 +17,7 @@ import type { SliceStores } from "@/types"; import { ThemeProvider } from "@/components/ThemeProvider/ThemeProvider"; async function render(pageContext: PageContextServer) { - const { Page, pageProps } = pageContext; + const { Page, pageProps, theme } = pageContext; // This render() hook only supports SSR, see https://vite-plugin-ssr.com/render-modes for how to modify render() to support SPA if (!Page) throw new Error("My render() hook expects pageContext.Page to be defined"); @@ -28,7 +33,7 @@ async function render(pageContext: PageContextServer) { pageContext.zustandState = stateWithoutFunctions; const html = await renderToStream( - + @@ -53,7 +58,7 @@ async function render(pageContext: PageContextServer) { ${title} - +
${html as unknown as ReadableStream}
`; diff --git a/src/renderer/types.ts b/src/renderer/types.ts index 99e1d51..49965fd 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -23,6 +23,7 @@ export type PageContextCustom = { pageProps?: PageProps; urlPathname: string; zustandState: Partial; + theme: "light" | "dark" | "system"; exports: { documentProps?: { title?: string; diff --git a/src/server/index.ts b/src/server/index.ts index e0d4884..3af06ca 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -13,6 +13,7 @@ import express from "express"; import compression from "compression"; +import cookieParser from "cookie-parser"; import { renderPage } from "vite-plugin-ssr/server"; import { root } from "./root.js"; const isProduction = process.env.NODE_ENV === "production"; @@ -23,6 +24,7 @@ async function startServer() { const app = express(); app.use(compression()); + app.use(cookieParser()); // Vite integration if (isProduction) { @@ -51,8 +53,10 @@ async function startServer() { // Vite-plugin-ssr middleware. It should always be our last middleware (because it's a // catch-all middleware superseding any middleware placed after it). app.get("*", async (req, res, next) => { + const theme = req.cookies.theme || "dark"; const pageContextInit = { - urlOriginal: req.originalUrl + urlOriginal: req.originalUrl, + theme, }; const pageContext = await renderPage(pageContextInit); const { httpResponse } = pageContext; From a3aad91d7dcf04bf9f9975453319f63d771a4a53 Mon Sep 17 00:00:00 2001 From: Mark Evola Date: Sun, 10 Aug 2025 16:26:31 -0500 Subject: [PATCH 3/4] fix tests --- .../NavbarFileSaveStatus/NavbarFileSaveStatus.tsx | 15 ++++++++++++--- src/tests/integration/Navbar.test.tsx | 4 ++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/NavbarFileSaveStatus/NavbarFileSaveStatus.tsx b/src/components/NavbarFileSaveStatus/NavbarFileSaveStatus.tsx index 3a4becf..e357b7f 100644 --- a/src/components/NavbarFileSaveStatus/NavbarFileSaveStatus.tsx +++ b/src/components/NavbarFileSaveStatus/NavbarFileSaveStatus.tsx @@ -14,21 +14,30 @@ function NavbarFileSaveStatus({ status }: NavbarFileSaveStatusProps) { if (status === "saving") { return ( - + ); } if (status === "saved") { return ( - + ); } return ( - + ); } diff --git a/src/tests/integration/Navbar.test.tsx b/src/tests/integration/Navbar.test.tsx index aa3e391..74a9fdb 100644 --- a/src/tests/integration/Navbar.test.tsx +++ b/src/tests/integration/Navbar.test.tsx @@ -83,7 +83,7 @@ describe("Navbar functionality", () => { await userEvent.click(saveFileOption); await vi.waitFor(() => { - expect(alertSpy).toHaveBeenCalledWith("Saved!"); + expect(screen.getByLabelText("saved-indicator")).toBeInTheDocument(); }); }); @@ -116,7 +116,7 @@ describe("Navbar functionality", () => { fireEvent.keyDown(document, { key: "s", ctrlKey: true }); await vi.waitFor(() => { - expect(alertSpy).toHaveBeenCalledWith("Saved!"); + expect(screen.getByLabelText("saved-indicator")).toBeInTheDocument(); }); }); From 92a421af3aac314be47631d26fb3e1d24fef4af9 Mon Sep 17 00:00:00 2001 From: Mark Evola Date: Sun, 10 Aug 2025 16:38:12 -0500 Subject: [PATCH 4/4] add theme tests --- src/tests/test-utils.tsx | 9 +++++++-- src/tests/unit/useTheme.test.ts | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/tests/unit/useTheme.test.ts diff --git a/src/tests/test-utils.tsx b/src/tests/test-utils.tsx index a94e3ee..0e8bc2b 100644 --- a/src/tests/test-utils.tsx +++ b/src/tests/test-utils.tsx @@ -12,6 +12,7 @@ import { render, renderHook } from "@testing-library/react"; import { StoreProvider } from "@/components/StoreContext/StoreContext"; import { initializeStore } from "@/state/store"; +import { ThemeProvider } from "@/components/ThemeProvider/ThemeProvider"; type ExtendedRenderOptions = Omit & { preloadedState?: Partial; @@ -39,7 +40,9 @@ export function renderWithProviders( }: ExtendedRenderOptions = {} ): RenderResult { const Wrapper = ({ children }: PropsWithChildren) => ( - {children} + + {children} + ); return render(ui, { wrapper: Wrapper, ...renderOptions }); @@ -60,7 +63,9 @@ export function renderHookWithProviders( }: ExtendedRenderHookOptions = {} ): RenderHookResult { const Wrapper = ({ children }: PropsWithChildren) => ( - {children} + + {children} + ); return renderHook(hook, { wrapper: Wrapper, ...renderOptions }); diff --git a/src/tests/unit/useTheme.test.ts b/src/tests/unit/useTheme.test.ts new file mode 100644 index 0000000..cb50677 --- /dev/null +++ b/src/tests/unit/useTheme.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; + +import { act } from "@testing-library/react"; + +import useTheme from "@/state/hooks/useTheme"; +import { renderHookWithProviders } from "../test-utils"; + +describe("useTheme functionality", () => { + it("should be dark by default", () => { + const { result } = renderHookWithProviders(() => useTheme()); + + expect(result.current.theme).toBe("dark"); + }); + + it("should update the theme", () => { + const { result } = renderHookWithProviders(() => useTheme()); + + act(() => { + // Update the theme to light + result.current.setTheme("light"); + }); + + expect(result.current.theme).toBe("light"); + + act(() => { + // Update the theme back to dark + result.current.setTheme("dark"); + }); + + expect(result.current.theme).toBe("dark"); + }); +});