diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 0bc993fcbc6a..5ffd11314766 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -15,5 +15,9 @@ "logs", "coverage", "*.md" - ] + ], + "experimentalTailwindcss": { + "stylesheet": "./frontend/src/styles/tailwind.css", + "functions": ["cn"] + } } diff --git a/README.md b/README.md index f6dc3e426c80..bdb5ad5c39aa 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,22 @@ [![ChartJs](https://img.shields.io/badge/Chart.js-FF6384?style=for-the-badge&logo=chartdotjs&logoColor=white)](https://www.chartjs.org/) [![Eslint](https://img.shields.io/badge/eslint-4B32C3?style=for-the-badge&logo=eslint&logoColor=white)](https://eslint.org/) [![Express](https://img.shields.io/badge/-Express-373737?style=for-the-badge&logo=Express&logoColor=white)](https://expressjs.com/) -[![Firebase](https://img.shields.io/badge/firebase-ffca28?style=for-the-badge&logo=firebase&logoColor=black)](https://firebase.google.com/) +[![Firebase](https://img.shields.io/badge/firebase-DD2C00?style=for-the-badge&logo=firebase&logoColor=black)](https://firebase.google.com/) [![Fontawesome](https://img.shields.io/badge/fontawesome-538DD7?style=for-the-badge&logo=fontawesome&logoColor=white)](https://fontawesome.com/) -[![HTML5](https://img.shields.io/badge/html5-%23E34F26.svg?style=for-the-badge&logo=html5&logoColor=white)](https://developer.mozilla.org/en-US/docs/Web/HTML) -[![MongoDB](https://img.shields.io/badge/-MongoDB-13aa52?style=for-the-badge&logo=mongodb&logoColor=white)](https://www.mongodb.com/) -[![OXLint](https://img.shields.io/badge/%E2%9A%93%20oxlint-2b3c5a?style=for-the-badge&logoColor=white)](https://oxc.rs/docs/guide/usage/linter.html) +[![HTML5](https://img.shields.io/badge/html5-E34F26?style=for-the-badge&logo=html5&logoColor=white)](https://developer.mozilla.org/en-US/docs/Web/HTML) +[![MongoDB](https://img.shields.io/badge/-MongoDB-47A248?style=for-the-badge&logo=mongodb&logoColor=white)](https://www.mongodb.com/) +[![OXLint](https://img.shields.io/badge/oxlint-2b3c5a?style=for-the-badge&logo=oxc&logoColor=white)](https://oxc.rs/docs/guide/usage/linter.html) [![PNPM](https://img.shields.io/badge/pnpm-F69220?style=for-the-badge&logo=pnpm&logoColor=white)](https://pnpm.io/) -[![Redis](https://img.shields.io/badge/Redis-DC382D?style=for-the-badge&logo=redis&logoColor=white)](https://redis.io/) -[![SASS](https://img.shields.io/badge/SASS-hotpink.svg?style=for-the-badge&logo=SASS&logoColor=white)](https://sass-lang.com/) +[![Redis](https://img.shields.io/badge/Redis-FF4438?style=for-the-badge&logo=redis&logoColor=white)](https://redis.io/) +[![SASS](https://img.shields.io/badge/SASS-CC6699?style=for-the-badge&logo=SASS&logoColor=white)](https://sass-lang.com/) +[![Solid](https://img.shields.io/badge/solid-2C4F7C?style=for-the-badge&logo=solid&logoColor=white)](https://www.solidjs.com/) +[![Tailwind](https://img.shields.io/badge/tailwind-06B6D4?style=for-the-badge&logo=tailwindcss&logoColor=white)](https://tailwindcss.com/) [![TsRest](https://img.shields.io/badge/-TSREST-9333ea?style=for-the-badge&logoColor=white&logo=data:image/svg%2bxml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB3aWR0aD0iMjAuMzA2Nzc4bW0iCiAgIGhlaWdodD0iMTIuMDgzMjMzbW0iCiAgIHZpZXdCb3g9IjAgMCAyMC4zMDY3NzggMTIuMDgzMjMzIgogICB2ZXJzaW9uPSIxLjEiCiAgIGlkPSJzdmcxIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0ibmFtZWR2aWV3MSIKICAgICBwYWdlY29sb3I9IiM1MDUwNTAiCiAgICAgYm9yZGVyY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMSIKICAgICBpbmtzY2FwZTpzaG93cGFnZXNoYWRvdz0iMCIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMCIKICAgICBpbmtzY2FwZTpwYWdlY2hlY2tlcmJvYXJkPSIxIgogICAgIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0ibW0iIC8+CiAgPGRlZnMKICAgICBpZD0iZGVmczEiIC8+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiCiAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTMuODE5ODA1NCwtMi4yMTQ3MTkzKSI+CiAgICA8cGF0aAogICAgICAgZD0ibSAxNS40NTgwMzUsOC45NzMzOTUzIDguNjMzMjUsMC4wNDQ4NyAwLjAwOSwtMS42NjgxOTggLTguNjMzMjIsLTAuMDQ0ODUgeiBtIDAuMDI2MywtNS4wNTYxMDggOC42MzMyNSwwLjA0NDg1IDAuMDA5LC0xLjcwMjU2OCAtOC42MzMyNSwtMC4wNDQ4NSB6IG0gLTAuMDQ0OCw4LjYzMzI0NzcgOC42MzMyMywwLjA0NDg1IC0wLjAwOSwxLjcwMjU2NyAtOC42MzMyNSwtMC4wNDQ4NSB6IgogICAgICAgZmlsbD0iI2ZmZmZmZiIKICAgICAgIGlkPSJwYXRoMSIKICAgICAgIHN0eWxlPSJzdHJva2Utd2lkdGg6MC4yNjQ1ODMiIC8+CiAgICA8cGF0aAogICAgICAgZD0ibSAxMS4xMTE3MjUsMTAuMjg2NjI4IGMgMS42NTEsLTAuNjE5MTI0NyAyLjU5Njg4LC0xLjk2MDU2MjcgMi41OTY4OCwtMy44MDA3Mzk3IDAsLTIuNjQ4NDc5IC0xLjkyNjE2LC00LjI0Nzg4NSAtNS4wNzMzNzk2LC00LjI0Nzg4NSBoIC00LjgxNTQyIHYgMS43MDI1OTQgaCA0Ljc0NjYzIGMgMi4wODA5Mzk2LDAgMy4xNjQ0MDk2LDAuOTI4Njg3IDMuMTY0NDA5NiwyLjU0NTI5MSAwLDEuNTk5NDA2IC0xLjA4MzQ3LDIuNTQ1MjkyIC0zLjE2NDQwOTYsMi41NDUyOTIgaCAtNC43NDY2MyB2IDUuMjQ1MzYzNyBoIDEuOTYwNTYgdiAtMy41NzcxNjYgaCAyLjg1NDg2IGMgMC4yMDYzNywwIDAuNDI5OTUsMCAwLjYxOTEyLC0wLjAxNzIgbCAyLjUyODA5OTYsMy41OTQzNjQgaCAyLjEzMjU0IHoiCiAgICAgICBmaWxsPSIjZmZmZmZmIgogICAgICAgaWQ9InBhdGgyIgogICAgICAgc3R5bGU9InN0cm9rZS13aWR0aDowLjI2NDU4MyIgLz4KICA8L2c+Cjwvc3ZnPgo=)](https://ts-rest.com/) -[![Turborepo](https://img.shields.io/badge/-Turborepo-EF4444?style=for-the-badge&logo=turborepo&logoColor=white)](https://turborepo.org/) -[![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) -[![Vite](https://img.shields.io/badge/Vite-646CFF?style=for-the-badge&logo=Vite&logoColor=white)](https://vitejs.dev/) -[![Vitest](https://img.shields.io/badge/vitest-6E9F18?style=for-the-badge&logo=vitest&logoColor=white)](https://vitest.dev/) -[![Zod](https://img.shields.io/badge/-Zod-3E67B1?style=for-the-badge&logo=zod&logoColor=white)](https://zod.dev/) +[![Turborepo](https://img.shields.io/badge/-Turborepo-FF1E56?style=for-the-badge&logo=turborepo&logoColor=white)](https://turborepo.org/) +[![TypeScript](https://img.shields.io/badge/typescript-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) +[![Vite](https://img.shields.io/badge/Vite-9135FF?style=for-the-badge&logo=Vite&logoColor=white)](https://vitejs.dev/) +[![Vitest](https://img.shields.io/badge/vitest-00FF74?style=for-the-badge&logo=vitest&logoColor=white)](https://vitest.dev/) +[![Zod](https://img.shields.io/badge/-Zod-408AFF?style=for-the-badge&logo=zod&logoColor=white)](https://zod.dev/) # About diff --git a/frontend/__tests__/components/AnimatedModal.spec.tsx b/frontend/__tests__/components/AnimatedModal.spec.tsx index c0ecde36f85c..0237e8ffb20e 100644 --- a/frontend/__tests__/components/AnimatedModal.spec.tsx +++ b/frontend/__tests__/components/AnimatedModal.spec.tsx @@ -15,7 +15,7 @@ describe("AnimatedModal", () => { function renderModal(props: { onEscape?: (e: KeyboardEvent) => void; onBackdropClick?: (e: MouseEvent) => void; - class?: string; + wrapperClass?: string; beforeShow?: () => void | Promise; afterShow?: () => void | Promise; beforeHide?: () => void | Promise; @@ -46,7 +46,7 @@ describe("AnimatedModal", () => { const { dialog } = renderModal({}); expect(dialog).toHaveAttribute("id", "SupportModal"); - expect(dialog).toHaveClass("modalWrapper", "hidden"); + expect(dialog).toHaveClass("hidden"); }); it("renders children inside modal div", () => { @@ -71,10 +71,10 @@ describe("AnimatedModal", () => { it("applies custom class to dialog", () => { const { dialog } = renderModal({ - class: "customClass", + wrapperClass: "customClass", }); - expect(dialog).toHaveClass("modalWrapper", "hidden", "customClass"); + expect(dialog).toHaveClass("customClass"); }); it("renders with animationMode none", () => { diff --git a/frontend/__tests__/components/ScrollToTop.spec.tsx b/frontend/__tests__/components/ScrollToTop.spec.tsx index 6c4d608ee971..b7c4dc545c13 100644 --- a/frontend/__tests__/components/ScrollToTop.spec.tsx +++ b/frontend/__tests__/components/ScrollToTop.spec.tsx @@ -21,7 +21,7 @@ describe("ScrollToTop", () => { // oxlint-disable-next-line no-non-null-assertion container: container.children[0]! as HTMLElement, // oxlint-disable-next-line no-non-null-assertion - button: container.querySelector("div.button")!, + button: container.querySelector("button")!, }; } @@ -29,28 +29,28 @@ describe("ScrollToTop", () => { const { container, button } = renderElement(); expect(container).toHaveClass("content-grid", "ScrollToTop"); - expect(button).toHaveClass("breakout", "button"); + expect(button).toHaveClass("breakout"); expect(button).toContainHTML(``); }); it("renders invisible when scrollY is 0", () => { const { button } = renderElement(); - expect(button).toHaveClass("invisible"); + expect(button).toHaveClass("opacity-0"); }); it("becomes visible when scrollY > 100 on non-test pages", () => { const { button } = renderElement(); scrollTo(150); - expect(button).not.toHaveClass("invisible"); + expect(button).not.toHaveClass("opacity-0"); }); it("stays invisible on test page at scroll 0", () => { getActivePageMock.mockReturnValue("test"); const { button } = renderElement(); - expect(button).toHaveClass("invisible"); + expect(button).toHaveClass("opacity-0"); }); it("stays invisible on test page even with scroll > 100", () => { @@ -58,16 +58,16 @@ describe("ScrollToTop", () => { const { button } = renderElement(); scrollTo(150); - expect(button).toHaveClass("invisible"); + expect(button).toHaveClass("opacity-0"); }); it("becomes invisible when scroll < 100 on non-test pages", () => { const { button } = renderElement(); scrollTo(150); - expect(button).not.toHaveClass("invisible"); + expect(button).not.toHaveClass("opacity-0"); scrollTo(50); - expect(button).toHaveClass("invisible"); + expect(button).toHaveClass("opacity-0"); }); it("scrolls to top and hides button on click", async () => { @@ -82,7 +82,7 @@ describe("ScrollToTop", () => { top: 0, behavior: "smooth", }); - expect(button).toHaveClass("invisible"); + expect(button).toHaveClass("opacity-0"); }); it("cleans up scroll listener on unmount", () => { diff --git a/frontend/package.json b/frontend/package.json index 09994f084fbe..8e8d55784cdc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,6 +37,7 @@ "chartjs-adapter-date-fns": "3.0.0", "chartjs-plugin-annotation": "2.2.1", "chartjs-plugin-trendline": "1.0.2", + "clsx": "2.1.1", "color-blend": "4.0.0", "damerau-levenshtein": "1.0.8", "date-fns": "3.6.0", @@ -52,6 +53,7 @@ "object-hash": "3.0.0", "slim-select": "2.9.2", "stemmer": "2.0.1", + "tailwind-merge": "3.4.0", "throttle-debounce": "5.0.2", "zod": "3.23.8", "zod-urlsearchparams": "0.0.16" @@ -62,6 +64,7 @@ "@monkeytype/oxlint-config": "workspace:*", "@monkeytype/typescript-config": "workspace:*", "@solidjs/testing-library": "0.8.10", + "@tailwindcss/vite": "4.1.18", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/user-event": "14.6.1", @@ -93,6 +96,7 @@ "sass": "1.70.0", "solid-js": "1.9.10", "subset-font": "2.3.0", + "tailwindcss": "4.1.18", "tsx": "4.16.2", "typescript": "5.9.3", "unplugin-inject-preload": "3.0.0", diff --git a/frontend/src/html/head.html b/frontend/src/html/head.html index 1588dfd003b9..64bc55ee89c1 100644 --- a/frontend/src/html/head.html +++ b/frontend/src/html/head.html @@ -113,8 +113,14 @@ - + + + + diff --git a/frontend/src/html/pages/account.html b/frontend/src/html/pages/account.html index b1d7a856c0f7..77bb5a5ea236 100644 --- a/frontend/src/html/pages/account.html +++ b/frontend/src/html/pages/account.html @@ -1,4 +1,4 @@ -