├── src ├── app │ ├── [locale] │ │ ├── opengraph-image.alt.txt │ │ ├── twitter-image.alt.txt │ │ ├── opengraph-image.png │ │ ├── twitter-image.png │ │ ├── tools │ │ │ ├── layout.tsx │ │ │ ├── clock │ │ │ │ ├── clock.tsx │ │ │ │ ├── clock.spec.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── stopwatch │ │ │ │ │ ├── page.tsx │ │ │ │ │ ├── stopwatch.spec.tsx │ │ │ │ │ └── stopwatch.tsx │ │ │ ├── color │ │ │ │ ├── picker │ │ │ │ │ ├── color-picker.spec.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── color-picker.tsx │ │ │ │ └── random │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── random-color.tsx │ │ │ ├── crypto │ │ │ │ ├── hex │ │ │ │ │ ├── page.tsx │ │ │ │ │ ├── hex.spec.tsx │ │ │ │ │ └── hex.tsx │ │ │ │ ├── morse │ │ │ │ │ ├── page.tsx │ │ │ │ │ ├── morse.spec.tsx │ │ │ │ │ └── morse.tsx │ │ │ │ ├── binary │ │ │ │ │ ├── page.tsx │ │ │ │ │ ├── binary-code.tsx │ │ │ │ │ └── binary-code.spec.tsx │ │ │ │ ├── caesar-cipher │ │ │ │ │ ├── page.tsx │ │ │ │ │ ├── caesar-cipher.spec.tsx │ │ │ │ │ └── caesar-cipher.tsx │ │ │ │ └── qr-code │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── qr-code.tsx │ │ │ ├── units │ │ │ │ └── length │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── length.tsx │ │ │ ├── dev │ │ │ │ ├── css │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── css-minifier.tsx │ │ │ │ └── json │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── json-formatter.tsx │ │ │ ├── text │ │ │ │ └── page.tsx │ │ │ ├── security │ │ │ │ └── password-generator │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── password-generator.tsx │ │ │ ├── todo │ │ │ │ ├── page.tsx │ │ │ │ ├── todo-list.tsx │ │ │ │ ├── todo-form.tsx │ │ │ │ ├── todo-store.ts │ │ │ │ └── todo-item.tsx │ │ │ ├── page.tsx │ │ │ └── currency │ │ │ │ ├── page.tsx │ │ │ │ └── currency-converter.tsx │ │ ├── page.tsx │ │ ├── layout.tsx │ │ └── about │ │ │ └── page.tsx │ ├── icon.png │ ├── favicon.ico │ └── manifest.ts ├── app.d.ts ├── hooks │ ├── use-ids.ts │ ├── use-on-mount.ts │ ├── use-interval.ts │ ├── use-clipboard.ts │ └── use-hotkeys.ts ├── @types │ ├── utils.ts │ └── metadata.ts ├── config │ ├── fonts.ts │ ├── site.ts │ ├── length.ts │ ├── currency.ts │ └── header.ts ├── components │ ├── providers.tsx │ ├── ui │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── slider.tsx │ │ ├── switch.tsx │ │ ├── popover.tsx │ │ ├── tooltip.tsx │ │ ├── checkbox.tsx │ │ ├── text.tsx │ │ ├── button.tsx │ │ ├── scroll-area.tsx │ │ ├── heading.tsx │ │ ├── combobox.tsx │ │ ├── textfield.tsx │ │ ├── alert-dialog.tsx │ │ ├── sheet.tsx │ │ ├── dialog.tsx │ │ ├── command.tsx │ │ └── select.tsx │ ├── tools │ │ ├── code-editor.tsx │ │ └── tool-card.tsx │ ├── home │ │ ├── hero-section.tsx │ │ ├── feature-section.tsx │ │ └── stars-section.tsx │ ├── copy-button.tsx │ ├── site-header.tsx │ ├── main-nav.tsx │ ├── site-footer.tsx │ ├── settings-dropdown.tsx │ ├── mobile-nav.tsx │ └── command-menu.tsx ├── lib │ ├── i18n │ │ ├── navigation.ts │ │ └── index.ts │ └── env │ │ └── index.js ├── styles │ └── globals.css ├── middleware.ts └── utils │ ├── index.ts │ └── crypto.ts ├── __tests__ ├── setup.ts └── render.tsx ├── commitlint.config.cjs ├── .husky ├── pre-commit ├── commit-msg └── prepare-commit-msg ├── postcss.config.cjs ├── .vscode └── settings.json ├── next-env.d.ts ├── vitest.config.ts ├── .env.example ├── .gitignore ├── tsconfig.json ├── next.config.js ├── biome.json ├── tailwind.config.ts ├── package.json └── README.md /src/app/[locale]/opengraph-image.alt.txt: -------------------------------------------------------------------------------- 1 | Useful Tools -------------------------------------------------------------------------------- /src/app/[locale]/twitter-image.alt.txt: -------------------------------------------------------------------------------- 1 | Useful Tools -------------------------------------------------------------------------------- /__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/vitest"; 2 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /src/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fellipeutaka/useful-tools/HEAD/src/app/icon.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fellipeutaka/useful-tools/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | type Messages = typeof import("./lib/i18n/messages/en.json"); 2 | declare interface IntlMessages extends Messages {} 3 | -------------------------------------------------------------------------------- /src/app/[locale]/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fellipeutaka/useful-tools/HEAD/src/app/[locale]/opengraph-image.png -------------------------------------------------------------------------------- /src/app/[locale]/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fellipeutaka/useful-tools/HEAD/src/app/[locale]/twitter-image.png -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | exec < /dev/tty && npx cz --hook || true 5 | -------------------------------------------------------------------------------- /src/hooks/use-ids.ts: -------------------------------------------------------------------------------- 1 | import { useId } from "react"; 2 | 3 | export function useIds(length: number) { 4 | return Array.from({ length }, useId); 5 | } 6 | -------------------------------------------------------------------------------- /src/hooks/use-on-mount.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | export function useOnMount(effect: React.EffectCallback) { 4 | return useEffect(effect, []); 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.localesPaths": ["src/lib/i18n/messages"], 3 | "i18n-ally.keystyle": "nested", 4 | "typescript.tsdk": "node_modules\\typescript\\lib" 5 | } 6 | -------------------------------------------------------------------------------- /src/@types/utils.ts: -------------------------------------------------------------------------------- 1 | export type PropsWithChildren = T & { children: React.ReactNode }; 2 | 3 | export type PropsWithOptionalChildren = React.PropsWithChildren; 4 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /src/@types/metadata.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | 3 | export type PageParams = { params: { locale: string } }; 4 | 5 | export type GenerateMetadata = ( 6 | params: PageParams, 7 | ) => Metadata | Promise; 8 | -------------------------------------------------------------------------------- /src/config/fonts.ts: -------------------------------------------------------------------------------- 1 | import { GeistMono } from "geist/font/mono"; 2 | import { GeistSans } from "geist/font/sans"; 3 | 4 | const sans = GeistSans; 5 | const mono = GeistMono; 6 | 7 | export const fonts = { 8 | sans, 9 | mono, 10 | }; 11 | -------------------------------------------------------------------------------- /src/config/site.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | name: "Useful Tools", 3 | url: "https://usefultools.vercel.app", 4 | links: { 5 | twitter: "https://twitter.com/fellipeutaka", 6 | github: "https://github.com/fellipeutaka/useful-tools", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/app/[locale]/tools/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from "~/@types/utils"; 2 | 3 | export default function Layout({ children }: PropsWithChildren) { 4 | return ( 5 |
6 | {children} 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/config/length.ts: -------------------------------------------------------------------------------- 1 | export const lengthUnits = [ 2 | "kilometer", 3 | "meter", 4 | "centimeter", 5 | "millimeter", 6 | "micrometers", 7 | "nanometers", 8 | "mile", 9 | "yard", 10 | "foot", 11 | "inch", 12 | "nautical mile", 13 | ] as const; 14 | 15 | export type LengthUnits = (typeof lengthUnits)[number]; 16 | -------------------------------------------------------------------------------- /src/components/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider } from "next-themes"; 4 | import { Toaster } from "./ui/toast"; 5 | 6 | export function Providers(props: React.PropsWithChildren) { 7 | return ( 8 | 14 | {props.children} 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/i18n/navigation.ts: -------------------------------------------------------------------------------- 1 | import type createMiddleware from "next-intl/middleware"; 2 | import { createSharedPathnamesNavigation } from "next-intl/navigation"; 3 | import { locales } from "."; 4 | 5 | type LocalePrefix = Parameters["0"]["localePrefix"]; 6 | 7 | export const localePrefix = "as-needed" satisfies LocalePrefix; 8 | 9 | export const { Link, redirect, usePathname, useRouter } = 10 | createSharedPathnamesNavigation({ locales, localePrefix }); 11 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import { defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig(({ mode }) => ({ 5 | plugins: [react()], 6 | test: { 7 | environment: "jsdom", 8 | setupFiles: "./__tests__/setup.ts", 9 | }, 10 | resolve: { 11 | conditions: mode === "test" ? ["browser"] : [], 12 | alias: { 13 | "~/tests": new URL("./__tests__", import.meta.url).pathname, 14 | "~": new URL("./src", import.meta.url).pathname, 15 | }, 16 | }, 17 | })); 18 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Since the ".env" file is gitignored, you can use the ".env.example" file to 2 | # build a new ".env" file when you clone the repo. Keep this file up-to-date 3 | # when you add new variables to `.env.local`. 4 | 5 | # This file will be committed to version control, so make sure not to have any 6 | # secrets in it. If you are cloning this repo, create a copy of this file named 7 | # ".env.local" and populate it with your secrets. 8 | 9 | # When adding additional environment variables, the schema in "/src/lib/env/index.js" 10 | # should be updated accordingly. 11 | 12 | # Currency 13 | CURRENCY_API_KEY="52d3acd70d0f1b3ced038a1b8696593d" -------------------------------------------------------------------------------- /src/app/manifest.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from "next"; 2 | 3 | export default function manifest(): MetadataRoute.Manifest { 4 | return { 5 | name: "Useful Tools", 6 | short_name: "Useful Tools", 7 | display: "standalone", 8 | background_color: "#000000", 9 | theme_color: "#000000", 10 | start_url: "/", 11 | icons: [ 12 | { 13 | src: "/icon.png", 14 | sizes: "512x512", 15 | type: "image/png", 16 | purpose: "any", 17 | }, 18 | { 19 | src: "/favicon.ico", 20 | sizes: "any", 21 | type: "image/x-icon", 22 | }, 23 | ], 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { forwardRef } from "react"; 4 | 5 | import { Root } from "@radix-ui/react-label"; 6 | 7 | import { TextStyles } from "./text"; 8 | 9 | export type LabelProps = React.ComponentPropsWithoutRef & { 10 | htmlFor: string; 11 | }; 12 | 13 | export const Label = forwardRef, LabelProps>( 14 | ({ className, htmlFor, ...props }, ref) => ( 15 | 21 | ), 22 | ); 23 | Label.displayName = "Label"; 24 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | ::-webkit-scrollbar { 6 | @apply h-1.5 w-1.5; 7 | } 8 | 9 | ::-webkit-scrollbar-thumb { 10 | @apply rounded-lg bg-border transition-colors; 11 | } 12 | 13 | ::-webkit-scrollbar-thumb:hover, 14 | ::-webkit-scrollbar-thumb:active { 15 | @apply bg-border/80; 16 | } 17 | 18 | ::-webkit-color-swatch-wrapper { 19 | padding: 0; 20 | } 21 | 22 | ::-webkit-color-swatch { 23 | border: 0; 24 | border-radius: 0; 25 | } 26 | 27 | ::-moz-color-swatch, 28 | ::-moz-focus-inner { 29 | border: 0; 30 | } 31 | 32 | ::-moz-focus-inner { 33 | padding: 0; 34 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # PWA 39 | public/sw.js 40 | public/swe-worker-*.js 41 | public/sw.js.map 42 | workbox-*.js 43 | workbox-*.js.map -------------------------------------------------------------------------------- /src/app/[locale]/tools/clock/clock.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | 5 | import { useInterval } from "~/hooks/use-interval"; 6 | import { useOnMount } from "~/hooks/use-on-mount"; 7 | 8 | export function Clock() { 9 | const [date, setDate] = useState(new Date()); 10 | 11 | const interval = useInterval( 12 | () => setDate((state) => new Date(state.getTime() + 1000)), 13 | 1000, 14 | ); 15 | 16 | useOnMount(() => { 17 | interval.start(); 18 | 19 | return interval.stop; 20 | }); 21 | 22 | return ( 23 |
24 |

{date.toLocaleTimeString()}

25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /__tests__/render.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from "@testing-library/react"; 2 | import { NextIntlClientProvider } from "next-intl"; 3 | import { afterEach } from "vitest"; 4 | import messages from "~/lib/i18n/messages/en.json"; 5 | 6 | afterEach(() => { 7 | cleanup(); 8 | }); 9 | 10 | function customRender(ui: React.ReactElement, options = {}) { 11 | return render(ui, { 12 | wrapper: ({ children }) => ( 13 | 14 | {children} 15 | 16 | ), 17 | ...options, 18 | }); 19 | } 20 | 21 | export * from "@testing-library/react"; 22 | export { default as userEvent } from "@testing-library/user-event"; 23 | export { customRender as render }; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "target": "ES2022", 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "~/*": ["./src/*"], 23 | "~/tests/*": ["./__tests__/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import createMiddleware from "next-intl/middleware"; 2 | import { locales } from "./lib/i18n"; 3 | import { localePrefix } from "./lib/i18n/navigation"; 4 | 5 | export default createMiddleware({ 6 | locales, 7 | defaultLocale: "en", 8 | localePrefix, 9 | }); 10 | 11 | export const config = { 12 | // Matcher entries are linked with a logical "or", therefore 13 | // if one of them matches, the middleware will be invoked. 14 | matcher: [ 15 | // Match all pathnames except for 16 | // - … if they start with `/api`, `/_next` or `/_vercel` 17 | // - … the ones containing a dot (e.g. `favicon.ico`) 18 | "/((?!api|_next|_vercel|.*\\..*).*)", 19 | // However, match all pathnames within `/users`, optionally with a locale prefix 20 | "/([\\w-]+)?/about/(.+)", 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /src/app/[locale]/tools/clock/clock.spec.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { render, waitFor } from "~/tests/render"; 3 | import { delay } from "~/utils"; 4 | import { Clock } from "./clock"; 5 | 6 | describe("Clock", () => { 7 | it("should render all components", () => { 8 | const { getByText } = render(); 9 | const time = getByText(new Date().toLocaleTimeString()); 10 | expect(time).toBeVisible(); 11 | }); 12 | 13 | it("should update time", async () => { 14 | const { getByText } = render(); 15 | const currentTime = new Date().toLocaleTimeString(); 16 | const time = getByText(currentTime); 17 | 18 | await delay(1000); 19 | 20 | await waitFor(() => { 21 | expect(time).not.toHaveTextContent(currentTime); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/[locale]/page.tsx: -------------------------------------------------------------------------------- 1 | import { unstable_setRequestLocale } from "next-intl/server"; 2 | import type { PageParams } from "~/@types/metadata"; 3 | import { FeatureSection } from "~/components/home/feature-section"; 4 | import { HeroSection } from "~/components/home/hero-section"; 5 | import { StarsSection } from "~/components/home/stars-section"; 6 | import { getStaticParams } from "~/lib/i18n"; 7 | 8 | export function generateStaticParams() { 9 | return getStaticParams(); 10 | } 11 | 12 | export default function Page({ params }: PageParams) { 13 | unstable_setRequestLocale(params.locale); 14 | 15 | return ( 16 |
17 | 18 | 19 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { getRequestConfig } from "next-intl/server"; 2 | import { notFound } from "next/navigation"; 3 | 4 | export const locales = ["en", "pt"] as const; 5 | 6 | export type Locale = (typeof locales)[number]; 7 | 8 | /** 9 | * The generateStaticParams function can be used in combination with dynamic 10 | * route segments to statically generate routes at build time instead of 11 | * on-demand at request time. 12 | * 13 | * @see https://nextjs.org/docs/app/api-reference/functions/generate-static-params 14 | */ 15 | export function getStaticParams() { 16 | return locales.map((locale) => ({ locale })); 17 | } 18 | 19 | export default getRequestConfig(async ({ locale }) => { 20 | if (!locales.includes(locale as Locale)) notFound(); 21 | 22 | return { 23 | messages: (await import(`./messages/${locale}.json`)).default, 24 | }; 25 | }); 26 | -------------------------------------------------------------------------------- /src/hooks/use-interval.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef, useState } from "react"; 2 | 3 | export function useInterval(fn: () => void, interval: number) { 4 | const [active, setActive] = useState(false); 5 | const intervalRef = useRef(); 6 | 7 | const start = useCallback(() => { 8 | setActive((old) => { 9 | if (!(old || intervalRef.current)) { 10 | intervalRef.current = window.setInterval(fn, interval); 11 | } 12 | return true; 13 | }); 14 | }, [fn, interval]); 15 | 16 | const stop = useCallback(() => { 17 | setActive(false); 18 | window.clearInterval(intervalRef.current); 19 | intervalRef.current = undefined; 20 | }, []); 21 | 22 | const toggle = useCallback(() => { 23 | if (active) { 24 | stop(); 25 | } else { 26 | start(); 27 | } 28 | }, [active, start, stop]); 29 | 30 | return { start, stop, toggle, active }; 31 | } 32 | -------------------------------------------------------------------------------- /src/app/[locale]/tools/color/picker/color-picker.spec.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { render } from "~/tests/render"; 3 | import { ColorPicker } from "./color-picker"; 4 | 5 | describe("Color Picker", () => { 6 | it("should render all components", () => { 7 | const { getByDisplayValue } = render(); 8 | const hex = getByDisplayValue("#000000"); 9 | const hexa = getByDisplayValue("#000000ff"); 10 | const rgb = getByDisplayValue("rgb(0, 0, 0)"); 11 | const rgba = getByDisplayValue("rgba(0, 0%, 0%, 1)"); 12 | const hsl = getByDisplayValue("hsl(0, 0%, 0%)"); 13 | const hsla = getByDisplayValue("hsla(0, 0%, 0%, 1)"); 14 | expect(hex).toBeVisible(); 15 | expect(hexa).toBeVisible(); 16 | expect(rgb).toBeVisible(); 17 | expect(rgba).toBeVisible(); 18 | expect(hsl).toBeVisible(); 19 | expect(hsla).toBeVisible(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/tools/code-editor.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Editor, type EditorProps } from "@monaco-editor/react"; 4 | import { useTheme } from "next-themes"; 5 | 6 | import { Icons } from "../icons"; 7 | import { TextareaStyles } from "../ui/textarea"; 8 | 9 | export const EDITOR_OPTIONS: EditorProps["options"] = { 10 | cursorBlinking: "smooth", 11 | tabSize: 2, 12 | minimap: { enabled: false }, 13 | }; 14 | 15 | export function CodeEditor({ 16 | options = EDITOR_OPTIONS, 17 | className, 18 | ...props 19 | }: EditorProps) { 20 | const { theme } = useTheme(); 21 | 22 | return ( 23 | } 26 | options={{ 27 | ...EDITOR_OPTIONS, 28 | ...options, 29 | }} 30 | theme={theme === "light" ? "vs-light" : "vs-dark"} 31 | {...props} 32 | /> 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import { tv } from "mizuhara/utils"; 2 | import { forwardRef } from "react"; 3 | 4 | export const TextareaStyles = tv({ 5 | base: [ 6 | "flex min-h-[5rem] w-full resize-none rounded-md border border-input bg-background px-3 py-2 text-sm outline-none ring-offset-background transition", 7 | "placeholder:text-muted-foreground", 8 | "disabled:cursor-not-allowed disabled:opacity-50", 9 | "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", 10 | ], 11 | }); 12 | 13 | export type TextareaProps = React.ComponentPropsWithoutRef<"textarea">; 14 | 15 | export const Textarea = forwardRef( 16 | ({ className, ...props }, ref) => { 17 | return ( 18 |