├── .eslintrc.js
├── .gitignore
├── .npmrc
├── .vscode
└── settings.json
├── README.md
├── apps
├── docs
│ ├── .eslintrc.js
│ ├── README.md
│ ├── app
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── page.module.css
│ │ └── page.tsx
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── package.json
│ ├── public
│ │ ├── circles.svg
│ │ ├── next.svg
│ │ ├── turborepo.svg
│ │ └── vercel.svg
│ └── tsconfig.json
└── web
│ ├── .env.example
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── README.md
│ ├── app
│ ├── (providers)
│ │ ├── shadcn
│ │ │ ├── components
│ │ │ │ ├── blocks
│ │ │ │ │ ├── charts-01.tsx
│ │ │ │ │ ├── dashboard-05.tsx
│ │ │ │ │ ├── dashboard-06.tsx
│ │ │ │ │ └── dashboard-07.tsx
│ │ │ │ ├── footer.tsx
│ │ │ │ ├── responsive-theme-editor.tsx
│ │ │ │ ├── theme-export.tsx
│ │ │ │ ├── theme-generator-input.tsx
│ │ │ │ ├── theme-generator-properties.tsx
│ │ │ │ ├── theme-generator.tsx
│ │ │ │ ├── theme-install-code.tsx
│ │ │ │ ├── theme-preset-selector.tsx
│ │ │ │ ├── theme-preview.tsx
│ │ │ │ └── theme-workspace.tsx
│ │ │ ├── constants.ts
│ │ │ └── page.tsx
│ │ └── vscode
│ │ │ ├── editor
│ │ │ └── page.tsx
│ │ │ └── page.tsx
│ ├── api
│ │ ├── enhance
│ │ │ ├── route.ts
│ │ │ ├── shadcn
│ │ │ │ └── route.ts
│ │ │ └── vscode
│ │ │ │ └── route.ts
│ │ ├── generate
│ │ │ ├── shadcn
│ │ │ │ └── route.ts
│ │ │ └── vscode
│ │ │ │ └── route.ts
│ │ ├── inngest
│ │ │ └── route.ts
│ │ ├── og
│ │ │ └── route.tsx
│ │ ├── shadcn
│ │ │ ├── [id]
│ │ │ │ └── route.ts
│ │ │ └── route.ts
│ │ ├── subscribe
│ │ │ └── route.ts
│ │ ├── theme
│ │ │ ├── [id]
│ │ │ │ ├── name
│ │ │ │ │ └── route.ts
│ │ │ │ ├── route.ts
│ │ │ │ └── status
│ │ │ │ │ └── route.ts
│ │ │ └── route.ts
│ │ ├── themes
│ │ │ └── route.ts
│ │ └── unsubscribe
│ │ │ └── route.ts
│ ├── globals.css
│ ├── icon.svg
│ ├── layout.tsx
│ ├── page.tsx
│ ├── s
│ │ └── [id]
│ │ │ └── route.ts
│ ├── t
│ │ └── [id]
│ │ │ ├── page.tsx
│ │ │ └── theme-content.tsx
│ └── utils.ts
│ ├── components.json
│ ├── components
│ ├── achievement-banner.tsx
│ ├── atom
│ │ ├── rh-tinte-link-logo.tsx
│ │ └── tinte-link-logo.tsx
│ ├── browser-theme-selector.tsx
│ ├── circular-gradient.tsx
│ ├── color-palette.tsx
│ ├── color-picker-button.tsx
│ ├── color-picker-input.tsx
│ ├── counterscale-script.tsx
│ ├── create-theme-dialog.tsx
│ ├── delete-theme-dialog.tsx
│ ├── dynamic-accent-title.tsx
│ ├── empty-state.tsx
│ ├── general-header.tsx
│ ├── header.tsx
│ ├── help-dialog.tsx
│ ├── landing-theme-generator.tsx
│ ├── language-switcher.tsx
│ ├── load-more-skeleton.tsx
│ ├── loading-page.tsx
│ ├── logo-3d.tsx
│ ├── preview-editor.tsx
│ ├── providers.tsx
│ ├── read-only-preview.tsx
│ ├── readonly-preview-editor.tsx
│ ├── scroll-to-top.tsx
│ ├── share-theme-dialog.tsx
│ ├── sign-in-dialog.tsx
│ ├── simplified-token-editor.tsx
│ ├── subscribed-email.tsx
│ ├── subscription-form.tsx
│ ├── theme-card-options.tsx
│ ├── theme-card.tsx
│ ├── theme-cards.tsx
│ ├── theme-config-panel.tsx
│ ├── theme-control-bar.tsx
│ ├── theme-customizer.tsx
│ ├── theme-generator.tsx
│ ├── theme-manager.tsx
│ ├── theme-preview.tsx
│ ├── theme-selector.tsx
│ ├── theme-sheet.tsx
│ ├── tinte-for-shadcn-modal.tsx
│ ├── token-editor.tsx
│ └── ui
│ │ ├── alert-dialog.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── breadcrumb.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── chart.tsx
│ │ ├── dialog.tsx
│ │ ├── drawer.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── icons.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── pagination.tsx
│ │ ├── popover.tsx
│ │ ├── progress.tsx
│ │ ├── resizable.tsx
│ │ ├── responsive-modal.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── shine-button.tsx
│ │ ├── slider.tsx
│ │ ├── sonner.tsx
│ │ ├── switch.tsx
│ │ ├── table.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toggle-group.tsx
│ │ ├── toggle.tsx
│ │ └── tooltip.tsx
│ ├── inngest
│ ├── client.ts
│ ├── functions.ts
│ └── sync-user.ts
│ ├── lib
│ ├── actions.ts
│ ├── actions
│ │ └── shadcn-theme-actions.ts
│ ├── api.ts
│ ├── atoms
│ │ └── index.ts
│ ├── constants.tsx
│ ├── copy-code
│ │ └── generators.ts
│ ├── core
│ │ ├── colors.ts
│ │ ├── config.ts
│ │ ├── index.ts
│ │ ├── tokens.ts
│ │ └── types.ts
│ ├── export-theme.ts
│ ├── hooks
│ │ ├── use-binary-theme.ts
│ │ ├── use-code-sample.ts
│ │ ├── use-debounce.ts
│ │ ├── use-highlighter.tsx
│ │ ├── use-infinite-themes.ts
│ │ ├── use-media-query.ts
│ │ ├── use-monaco-editor.tsx
│ │ ├── use-shadcn-theme-generator.ts
│ │ ├── use-theme-applier.ts
│ │ ├── use-theme-config.ts
│ │ ├── use-theme-enhancer.ts
│ │ ├── use-theme-export.ts
│ │ └── use-theme-generator.ts
│ ├── language-logos.tsx
│ ├── themes
│ │ ├── one-hunter-theme-dark.json
│ │ └── one-hunter-theme-light.json
│ ├── types.ts
│ └── utils.ts
│ ├── middleware.ts
│ ├── netlify.toml
│ ├── netlify
│ └── functions
│ │ └── createVSIX.js
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── package.json
│ ├── pages
│ └── api
│ │ └── export-vsix.ts
│ ├── postcss.config.js
│ ├── prisma
│ └── schema.prisma
│ ├── public
│ ├── circles.svg
│ ├── flexoki-logo.png
│ ├── fonts
│ │ ├── Geist-Bold.ttf
│ │ ├── Geist-Regular.ttf
│ │ └── GeistMono-Regular.ttf
│ ├── logos
│ │ ├── angular.svg
│ │ ├── astro.svg
│ │ ├── bash.svg
│ │ ├── c.svg
│ │ ├── cpp.svg
│ │ ├── csharp.svg
│ │ ├── css.svg
│ │ ├── dart.svg
│ │ ├── go.svg
│ │ ├── html.svg
│ │ ├── java.svg
│ │ ├── javascript.svg
│ │ ├── json.svg
│ │ ├── jsx.svg
│ │ ├── kotlin.svg
│ │ ├── lua.svg
│ │ ├── markdown.svg
│ │ ├── ocaml.svg
│ │ ├── php.svg
│ │ ├── python.svg
│ │ ├── r.svg
│ │ ├── raycast.svg
│ │ ├── ruby.svg
│ │ ├── rust.svg
│ │ ├── scala.svg
│ │ ├── solidity.svg
│ │ ├── sql.svg
│ │ ├── svelte.svg
│ │ ├── swift.svg
│ │ ├── toml.svg
│ │ ├── tsx.svg
│ │ ├── typescript.svg
│ │ ├── vue.svg
│ │ └── zig.svg
│ ├── next.svg
│ ├── og-image.png
│ ├── one-hunter-logo.png
│ ├── placeholder.svg
│ ├── rh-logo.svg
│ ├── supabase-icon 1.svg
│ ├── tailwindcss-icon 1.svg
│ ├── themes
│ │ └── zaffre.json
│ ├── tinte-1.png
│ ├── tinte-2.png
│ ├── tinte-3.png
│ ├── turborepo.svg
│ ├── vercel-icon 1.png
│ ├── vercel-icon 1.svg
│ └── vercel.svg
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ └── types.d.ts
├── package.json
├── packages
├── eslint-config
│ ├── README.md
│ ├── library.js
│ ├── next.js
│ ├── package.json
│ └── react-internal.js
├── tinte-core
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ ├── flexoki-dark.jpg
│ │ ├── flexoki-light.jpg
│ │ ├── one-hunter-dark.jpg
│ │ ├── one-hunter-light.jpg
│ │ ├── tinte-logo.png
│ │ └── transparent.png
│ ├── src
│ │ ├── config
│ │ │ ├── customize
│ │ │ │ └── vscode.ts
│ │ │ └── index.ts
│ │ ├── generators
│ │ │ ├── alacritty
│ │ │ │ └── generate.ts
│ │ │ ├── fzf
│ │ │ │ └── generate.ts
│ │ │ ├── gimp
│ │ │ │ ├── generate.ts
│ │ │ │ └── mappers.ts
│ │ │ ├── index.ts
│ │ │ ├── iterm2
│ │ │ │ ├── generate.ts
│ │ │ │ └── mappers.ts
│ │ │ ├── kitty
│ │ │ │ └── generate.ts
│ │ │ ├── lite-xl
│ │ │ │ └── generate.ts
│ │ │ ├── theme-sh
│ │ │ │ └── generate.ts
│ │ │ ├── types.ts
│ │ │ ├── vanilla-css
│ │ │ │ ├── generate.ts
│ │ │ │ └── mappers.ts
│ │ │ ├── vscode
│ │ │ │ ├── generate.ts
│ │ │ │ └── mappers.ts
│ │ │ ├── warp
│ │ │ │ └── generate.ts
│ │ │ ├── wezterm
│ │ │ │ └── generate.ts
│ │ │ ├── windows-terminal
│ │ │ │ └── generate.ts
│ │ │ └── xresources
│ │ │ │ └── generate.ts
│ │ ├── index.ts
│ │ ├── mapped-palette.ts
│ │ ├── palettes
│ │ │ ├── flexoki.ts
│ │ │ ├── one-hunter-material.ts
│ │ │ ├── types.ts
│ │ │ └── vercel-2024.ts
│ │ ├── types.ts
│ │ └── utils
│ │ │ ├── color.ts
│ │ │ ├── format.ts
│ │ │ └── index.ts
│ └── tsconfig.json
├── typescript-config
│ ├── base.json
│ ├── nextjs.json
│ ├── package.json
│ └── react-library.json
└── ui
│ ├── .eslintrc.js
│ ├── package.json
│ ├── src
│ ├── button.tsx
│ ├── card.tsx
│ └── code.tsx
│ ├── tsconfig.json
│ ├── tsconfig.lint.json
│ └── turbo
│ └── generators
│ ├── config.ts
│ └── templates
│ └── component.hbs
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── tinte-preview-2.png
├── tinte-preview.png
├── tsconfig.json
└── turbo.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // This configuration only applies to the package manager root.
2 | /** @type {import("eslint").Linter.Config} */
3 | module.exports = {
4 | ignorePatterns: ["apps/**", "packages/**"],
5 | extends: ["@repo/eslint-config/library.js"],
6 | parser: "@typescript-eslint/parser",
7 | parserOptions: {
8 | project: true,
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/.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 |
8 | # Local env files
9 | .env
10 | .env.local
11 | .env.development.local
12 | .env.test.local
13 | .env.production.local
14 |
15 | # Testing
16 | coverage
17 |
18 | # Turbo
19 | .turbo
20 |
21 | # Vercel
22 | .vercel
23 |
24 | # Build Outputs
25 | .next/
26 | out/
27 | build
28 | dist
29 |
30 |
31 | # Debug
32 | npm-debug.log*
33 | yarn-debug.log*
34 | yarn-error.log*
35 |
36 | # Misc
37 | .DS_Store
38 | *.pem
39 |
40 | # Local Netlify folder
41 | .netlify
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/.npmrc
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.workingDirectories": [
3 | {
4 | "mode": "auto"
5 | }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/apps/docs/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | module.exports = {
3 | root: true,
4 | extends: ["@repo/eslint-config/next.js"],
5 | parser: "@typescript-eslint/parser",
6 | parserOptions: {
7 | project: true,
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/apps/docs/README.md:
--------------------------------------------------------------------------------
1 | ## Getting Started
2 |
3 | First, run the development server:
4 |
5 | ```bash
6 | yarn dev
7 | ```
8 |
9 | Open [http://localhost:3001](http://localhost:3001) with your browser to see the result.
10 |
11 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
12 |
13 | To create [API routes](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) add an `api/` directory to the `app/` directory with a `route.ts` file. For individual endpoints, create a subfolder in the `api` directory, like `api/hello/route.ts` would map to [http://localhost:3001/api/hello](http://localhost:3001/api/hello).
14 |
15 | ## Learn More
16 |
17 | To learn more about Next.js, take a look at the following resources:
18 |
19 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
20 | - [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial.
21 |
22 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
23 |
24 | ## Deploy on Vercel
25 |
26 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js.
27 |
28 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
29 |
--------------------------------------------------------------------------------
/apps/docs/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/apps/docs/app/favicon.ico
--------------------------------------------------------------------------------
/apps/docs/app/globals.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --max-width: 1100px;
3 | --border-radius: 12px;
4 | --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
5 | "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
6 | "Fira Mono", "Droid Sans Mono", "Courier New", monospace;
7 |
8 | --foreground-rgb: 255, 255, 255;
9 | --background-start-rgb: 0, 0, 0;
10 | --background-end-rgb: 0, 0, 0;
11 |
12 | --callout-rgb: 20, 20, 20;
13 | --callout-border-rgb: 108, 108, 108;
14 | --card-rgb: 100, 100, 100;
15 | --card-border-rgb: 200, 200, 200;
16 |
17 | --glow-conic: conic-gradient(
18 | from 180deg at 50% 50%,
19 | #2a8af6 0deg,
20 | #a853ba 180deg,
21 | #e92a67 360deg
22 | );
23 | }
24 |
25 | * {
26 | box-sizing: border-box;
27 | padding: 0;
28 | margin: 0;
29 | }
30 |
31 | html,
32 | body {
33 | max-width: 100vw;
34 | overflow-x: hidden;
35 | }
36 |
37 | body {
38 | color: rgb(var(--foreground-rgb));
39 | background: linear-gradient(
40 | to bottom,
41 | transparent,
42 | rgb(var(--background-end-rgb))
43 | )
44 | rgb(var(--background-start-rgb));
45 | }
46 |
47 | a {
48 | color: inherit;
49 | text-decoration: none;
50 | }
51 |
--------------------------------------------------------------------------------
/apps/docs/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css";
2 | import type { Metadata } from "next";
3 | import { Inter } from "next/font/google";
4 |
5 | const inter = Inter({ subsets: ["latin"] });
6 |
7 | export const metadata: Metadata = {
8 | title: "Create Turborepo",
9 | description: "Generated by create turbo",
10 | };
11 |
12 | export default function RootLayout({
13 | children,
14 | }: {
15 | children: React.ReactNode;
16 | }): JSX.Element {
17 | return (
18 |
19 |
{children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/apps/docs/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 |
--------------------------------------------------------------------------------
/apps/docs/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | module.exports = {
3 | transpilePackages: ["@repo/ui"],
4 | };
5 |
--------------------------------------------------------------------------------
/apps/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --port 3001",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "eslint . --max-warnings 0"
10 | },
11 | "dependencies": {
12 | "@repo/ui": "workspace:*",
13 | "next": "^14.1.1",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0"
16 | },
17 | "devDependencies": {
18 | "@next/eslint-plugin-next": "^14.1.1",
19 | "@repo/eslint-config": "workspace:*",
20 | "@repo/typescript-config": "workspace:*",
21 | "@types/eslint": "^8.56.5",
22 | "@types/node": "^20.11.24",
23 | "@types/react": "^18.2.61",
24 | "@types/react-dom": "^18.2.19",
25 | "eslint": "^8.57.0",
26 | "typescript": "^5.3.3"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/apps/docs/public/circles.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/docs/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/docs/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "plugins": [
5 | {
6 | "name": "next"
7 | }
8 | ]
9 | },
10 | "include": [
11 | "next-env.d.ts",
12 | "next.config.js",
13 | "**/*.ts",
14 | "**/*.tsx",
15 | ".next/types/**/*.ts"
16 | ],
17 | "exclude": ["node_modules"]
18 | }
19 |
--------------------------------------------------------------------------------
/apps/web/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_EXPORT_API_URL=
2 | OPENAI_API_KEY=
3 | UPSTASH_REDIS_REST_URL=
4 | UPSTASH_REDIS_REST_TOKEN=
5 | RESEND_API_KEY=
6 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
7 | CLERK_SECRET_KEY=
--------------------------------------------------------------------------------
/apps/web/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | module.exports = {
3 | root: true,
4 | extends: ["@repo/eslint-config/next.js"],
5 | parser: "@typescript-eslint/parser",
6 | parserOptions: {
7 | project: true,
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/apps/web/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | # Keep environment variables out of version control
3 | .env
4 |
--------------------------------------------------------------------------------
/apps/web/app/(providers)/shadcn/components/responsive-theme-editor.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button } from "@/components/ui/button";
3 | import {
4 | Popover,
5 | PopoverContent,
6 | PopoverTrigger,
7 | } from "@/components/ui/popover";
8 | import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
9 | import { IconPalette } from "@/components/ui/icons";
10 | import { ThemeGeneratorProperties } from "./theme-generator-properties";
11 | import { Theme } from "@/lib/atoms";
12 | import { useMediaQuery } from "@/lib/hooks/use-media-query";
13 |
14 | interface ResponsiveThemeEditorProps {
15 | currentTheme: Theme;
16 | setCurrentTheme: React.Dispatch>;
17 | copyCode: (format: "css" | "tailwind" | "json") => void;
18 | }
19 |
20 | export function ResponsiveThemeEditor({
21 | currentTheme,
22 | setCurrentTheme,
23 | copyCode,
24 | }: ResponsiveThemeEditorProps) {
25 | const [open, setOpen] = React.useState(false);
26 | const isDesktop = useMediaQuery("(min-width: 768px)");
27 |
28 | const content = (
29 |
34 | );
35 |
36 | if (isDesktop) {
37 | return (
38 |
39 |
40 |
44 |
45 |
46 | {content}
47 |
48 |
49 | );
50 | }
51 |
52 | return (
53 |
54 |
55 |
59 |
60 |
61 | {content}
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/apps/web/app/(providers)/shadcn/components/theme-export.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { motion } from "framer-motion";
5 | import { useAtomValue } from "jotai";
6 | import { themeAtom } from "@/lib/atoms";
7 | import { Button } from "@/components/ui/button";
8 | import { IconDownload, IconShare } from "@/components/ui/icons";
9 | import {
10 | DropdownMenu,
11 | DropdownMenuContent,
12 | DropdownMenuItem,
13 | DropdownMenuTrigger,
14 | } from "@/components/ui/dropdown-menu";
15 |
16 | export function ThemeExport() {
17 | const theme = useAtomValue(themeAtom);
18 |
19 | const exportTheme = () => {
20 | const themeString = JSON.stringify(theme, null, 2);
21 | const blob = new Blob([themeString], { type: "application/json" });
22 | const url = URL.createObjectURL(blob);
23 | const a = document.createElement("a");
24 | a.href = url;
25 | a.download = "tinte-theme.json";
26 | a.click();
27 | URL.revokeObjectURL(url);
28 | };
29 |
30 | return (
31 |
32 |
33 |
37 |
38 |
39 |
40 |
41 |
45 |
46 |
47 | console.log("Copy link")}>
48 | Copy Link
49 |
50 | console.log("Share on Twitter")}>
51 | Share on Twitter
52 |
53 |
54 |
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/apps/web/app/(providers)/shadcn/components/theme-workspace.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { DynamicAccentTitle } from "@/components/dynamic-accent-title";
5 | import { IconPalette } from "@/components/ui/icons";
6 | import { createCopyCodeFunction } from "@/lib/copy-code/generators";
7 | import { GeneralHeader } from "@/components/general-header";
8 | import { useThemeApplier } from "@/lib/hooks/use-theme-applier";
9 | import { ResponsiveThemeEditor } from "./responsive-theme-editor";
10 | import { ThemeGenerator } from "./theme-generator";
11 | import { ThemePreview } from "./theme-preview";
12 | import { ShadcnThemes } from "@prisma/client";
13 | import { ThemeInstallCode } from "./theme-install-code";
14 |
15 | interface ThemeWorkspaceProps {
16 | allThemes: ShadcnThemes[];
17 | initialPagination: {
18 | currentPage: number;
19 | totalPages: number;
20 | totalItems: number;
21 | };
22 | }
23 |
24 | export function ThemeWorkspace({
25 | allThemes,
26 | initialPagination,
27 | }: ThemeWorkspaceProps) {
28 | const { currentTheme, currentChartTheme, setCurrentTheme } =
29 | useThemeApplier();
30 |
31 | const copyCode = createCopyCodeFunction(currentTheme);
32 |
33 | const headerActions = [
34 | {
35 | label: "Customize",
36 | icon: IconPalette,
37 | component: (
38 |
43 | ),
44 | },
45 | ];
46 |
47 | return (
48 | <>
49 |
50 |
51 |
59 |
65 |
66 |
75 |
76 | >
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/apps/web/app/(providers)/shadcn/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Footer } from "./components/footer";
3 | import { getThemes } from "@/lib/actions/shadcn-theme-actions";
4 | import { ThemeWorkspace } from "./components/theme-workspace";
5 | import { cookies } from "next/headers";
6 | import { TinteForShadcnModal } from "@/components/tinte-for-shadcn-modal";
7 |
8 | export default async function ShadcnThemesPage() {
9 | const { themes, pagination } = await getThemes(1, 5);
10 | const cookieStore = cookies();
11 | const hasSeenModal = cookieStore.get("hasSeenTinteForShadcnModal");
12 |
13 | return (
14 |
15 |
16 |
17 | {!hasSeenModal && }
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/apps/web/app/(providers)/vscode/editor/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ThemeCustomizer } from "@/components/theme-customizer";
3 | import { getAllThemes } from "@/lib/api";
4 |
5 | export default async function VSCodePage() {
6 | const allThemes = await getAllThemes();
7 |
8 | return ;
9 | }
10 |
--------------------------------------------------------------------------------
/apps/web/app/(providers)/vscode/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { cookies } from "next/headers";
3 | import { ThemeManager } from "@/components/theme-manager";
4 | import { GeneralHeader } from "@/components/general-header";
5 | import { TinteForShadcnModal } from "@/components/tinte-for-shadcn-modal";
6 | import { getInitialThemes } from "@/lib/api";
7 |
8 | export default async function Page() {
9 | const initialThemes = await getInitialThemes();
10 | const cookieStore = cookies();
11 | const hasSeenModal = cookieStore.get("hasSeenTinteForShadcnModal");
12 |
13 | return (
14 | <>
15 |
16 |
17 | {!hasSeenModal && }
18 | >
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/apps/web/app/api/enhance/route.ts:
--------------------------------------------------------------------------------
1 | import { generateObject } from "ai";
2 | import { openai } from "@ai-sdk/openai";
3 | import { z } from "zod";
4 | import { Ratelimit } from "@upstash/ratelimit";
5 | import { Redis } from "@upstash/redis";
6 | import { NextRequest } from "next/server";
7 |
8 | export const maxDuration = 15;
9 |
10 | const ratelimit = new Ratelimit({
11 | redis: Redis.fromEnv(),
12 | limiter: Ratelimit.slidingWindow(20, "10s"),
13 | analytics: true,
14 | });
15 |
16 | const outputSchema = z.object({
17 | enhancedPrompt: z
18 | .string()
19 | .min(10)
20 | .max(200)
21 | .describe("Enhanced theme description prompt"),
22 | });
23 |
24 | export async function POST(req: NextRequest) {
25 | const ip = req.ip ?? req.headers.get("X-Forwarded-For") ?? "ip";
26 | const { success } = await ratelimit.limit(ip);
27 | const { prompt }: { prompt: string } = await req.json();
28 |
29 | if (!success) {
30 | return new Response("Ratelimited!", { status: 429 });
31 | }
32 |
33 | const result = await generateObject({
34 | model: openai("gpt-4o-mini"),
35 | system: `You are an expert at enhancing theme descriptions for Visual Studio Code. Your task is to take a user's initial theme idea and expand it into a more detailed and creative description. Follow these guidelines:
36 | - Maintain the core concept of the original prompt
37 | - Add specific color suggestions or palettes
38 | - Keep the enhanced prompt concise (max 150 characters)
39 | - Use descriptive and evocative language`,
40 | schema: outputSchema,
41 | prompt: `Enhance this theme description: "${prompt}"`,
42 | });
43 |
44 | return Response.json({ enhancedPrompt: result.object.enhancedPrompt });
45 | }
46 |
--------------------------------------------------------------------------------
/apps/web/app/api/enhance/vscode/route.ts:
--------------------------------------------------------------------------------
1 | import { generateObject } from "ai";
2 | import { openai } from "@ai-sdk/openai";
3 | import { z } from "zod";
4 | import { Ratelimit } from "@upstash/ratelimit";
5 | import { Redis } from "@upstash/redis";
6 | import { NextRequest } from "next/server";
7 | import { track } from "@vercel/analytics/server";
8 |
9 | export const maxDuration = 15;
10 |
11 | const ratelimit = new Ratelimit({
12 | redis: Redis.fromEnv(),
13 | limiter: Ratelimit.slidingWindow(20, "10s"),
14 | analytics: true,
15 | });
16 |
17 | const outputSchema = z.object({
18 | enhancedPrompt: z
19 | .string()
20 | .describe(
21 | "Enhanced theme description prompt for vscode (ideally 10-200 characters)",
22 | ),
23 | });
24 |
25 | export async function POST(req: NextRequest) {
26 | const ip = req.ip ?? req.headers.get("X-Forwarded-For") ?? "ip";
27 | const { success } = await ratelimit.limit(ip);
28 | const { prompt }: { prompt: string } = await req.json();
29 |
30 | if (!success) {
31 | await track("VSCode Theme Description Enhancement Ratelimited", { ip });
32 | return new Response("Ratelimited!", { status: 429 });
33 | }
34 |
35 | try {
36 | const result = await generateObject({
37 | model: openai("gpt-4o-mini"),
38 | system: `You are an expert at enhancing theme descriptions for Visual Studio Code. Your task is to take a user's initial theme idea and expand it into a more detailed and creative description. Follow these guidelines:
39 | - Maintain the core concept of the original prompt
40 | - Add specific color suggestions or palettes
41 | - Keep the enhanced prompt concise (max 150 characters)
42 | - Use descriptive and evocative language`,
43 | schema: outputSchema,
44 | prompt: `Enhance this theme description: "${prompt}"`,
45 | });
46 |
47 | await track("VSCode Theme Description Enhanced", {
48 | originalLength: prompt.length,
49 | enhancedLength: result.object.enhancedPrompt.length,
50 | });
51 |
52 | return Response.json({ enhancedPrompt: result.object.enhancedPrompt });
53 | } catch (error) {
54 | console.error("VSCode theme description enhancement error:", error);
55 | await track("VSCode Theme Description Enhancement Failed", {
56 | error: (error as Error).message,
57 | });
58 | return Response.json(
59 | { error: "Failed to enhance VSCode theme description" },
60 | { status: 500 },
61 | );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/apps/web/app/api/inngest/route.ts:
--------------------------------------------------------------------------------
1 | import { inngest } from "@/inngest/client";
2 | import { functions } from "@/inngest/functions";
3 | import { serve } from "inngest/next";
4 |
5 | export const { GET, POST, PUT } = serve({
6 | client: inngest,
7 | functions,
8 | });
9 |
--------------------------------------------------------------------------------
/apps/web/app/api/shadcn/route.ts:
--------------------------------------------------------------------------------
1 | // app/api/shadcn/route.ts
2 |
3 | import { NextRequest, NextResponse } from "next/server";
4 | import { PrismaClient, ShadcnThemes } from "@prisma/client";
5 | import { sanitizeJsonInput } from "@/lib/utils";
6 | import { Theme } from "@/lib/atoms";
7 |
8 | const prisma = new PrismaClient();
9 |
10 | export async function POST(req: NextRequest) {
11 | const body = (await req.json()) as Partial & { userId: string | null };
12 |
13 | try {
14 | const newTheme = await prisma.shadcnThemes.create({
15 | data: {
16 | name: body.name!,
17 | display_name: body.displayName,
18 | User: body.userId,
19 | light_scheme: sanitizeJsonInput(body.light),
20 | dark_scheme: sanitizeJsonInput(body.dark),
21 | //fonts: sanitizeJsonInput(body.fonts),
22 | radius: String(body.radius),
23 | //space: String(body.space),
24 | //shadow: body.shadow,
25 | charts: sanitizeJsonInput(body.charts),
26 | //icons: body.icons,
27 | },
28 | });
29 |
30 | return NextResponse.json(newTheme);
31 | } catch (error) {
32 | console.error("Error creating ShadcnTheme:", error);
33 | return NextResponse.json(
34 | { error: "Failed to create theme" },
35 | { status: 500 },
36 | );
37 | } finally {
38 | await prisma.$disconnect();
39 | }
40 | }
41 |
42 | export async function GET(req: NextRequest) {
43 | const searchParams = req.nextUrl.searchParams;
44 | const page = Number(searchParams.get("page")) || 1;
45 | const limit = Number(searchParams.get("limit")) || 20;
46 | const userId = searchParams.get("userId");
47 |
48 | try {
49 | const themes = await prisma.shadcnThemes.findMany({
50 | where: userId ? { User: userId } : undefined,
51 | skip: (page - 1) * limit,
52 | take: limit,
53 | orderBy: { xata_createdat: "desc" },
54 | });
55 |
56 | const total = await prisma.shadcnThemes.count({
57 | where: userId ? { User: userId } : undefined,
58 | });
59 |
60 | return NextResponse.json({
61 | themes: themes as ShadcnThemes[],
62 | pagination: {
63 | currentPage: page,
64 | totalPages: Math.ceil(total / limit),
65 | totalItems: total,
66 | },
67 | });
68 | } catch (error) {
69 | console.error("Error fetching ShadcnThemes:", error);
70 | return NextResponse.json(
71 | { error: "Failed to fetch themes" },
72 | { status: 500 },
73 | );
74 | } finally {
75 | await prisma.$disconnect();
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/apps/web/app/api/subscribe/route.ts:
--------------------------------------------------------------------------------
1 | import { Resend } from "resend";
2 | import { Redis } from "@upstash/redis";
3 | import { Ratelimit } from "@upstash/ratelimit";
4 | import { NextRequest, NextResponse } from "next/server";
5 | import { SubscribedEmail } from "@/components/subscribed-email";
6 |
7 | const resend = new Resend(process.env.RESEND_API_KEY!);
8 | const redis = Redis.fromEnv();
9 | const ratelimit = new Ratelimit({
10 | redis: Redis.fromEnv(),
11 | limiter: Ratelimit.slidingWindow(10, "1 h"),
12 | });
13 |
14 | export async function POST(request: NextRequest) {
15 | const { email, firstName } = await request.json();
16 |
17 | const ip = request.ip ?? request.headers.get("X-Forwarded-For") ?? "ip";
18 |
19 | const { success } = await ratelimit.limit(ip);
20 | if (!success) {
21 | return NextResponse.json(
22 | { error: "Too many requests. Please try again later." },
23 | { status: 429 },
24 | );
25 | }
26 |
27 | const hashKey = `subscriptions:${email}`;
28 | const field = "tinte";
29 | const productName = "Tinte";
30 |
31 | const isSubscribed = await redis.hget(hashKey, field);
32 |
33 | if (isSubscribed) {
34 | return NextResponse.json(
35 | { error: "Email already subscribed" },
36 | { status: 400 },
37 | );
38 | }
39 |
40 | await redis.hset(hashKey, { [field]: "subscribed" });
41 |
42 | await resend.emails.send({
43 | from: "Railly Hugo ",
44 | to: email,
45 | subject: "Subscription Confirmation",
46 | react: SubscribedEmail({
47 | firstName,
48 | productName,
49 | unsubscribeLink: `https://tinte.railly.dev/api/unsubscribe?email=${encodeURIComponent(
50 | email,
51 | )}`,
52 | }),
53 | text: `Welcome, ${firstName}! Thank you for subscribing to ${productName}. We're thrilled to have you on board! As a subscriber, you'll be the first to know about our latest features, updates, and exclusive offers. If you wish to unsubscribe, click here: https://tinte.railly.dev/api/unsubscribe?email=${encodeURIComponent(
54 | email,
55 | )}`,
56 | });
57 |
58 | return NextResponse.json(
59 | { message: "Email subscribed successfully" },
60 | { status: 200 },
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/apps/web/app/api/theme/[id]/name/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { revalidatePath } from "next/cache";
3 | import { PrismaClient } from "@prisma/client";
4 | import { getThemeName } from "@/app/utils";
5 |
6 | const prisma = new PrismaClient();
7 |
8 | export const PATCH = async (
9 | req: Request,
10 | { params }: { params: { id: string } }
11 | ) => {
12 | const { id } = params;
13 | const { displayName, userId } = (await req.json()) as {
14 | displayName: string;
15 | userId: string;
16 | };
17 |
18 | if (!userId) {
19 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
20 | }
21 |
22 | try {
23 | const theme = await prisma.themes.update({
24 | where: {
25 | xata_id: id,
26 | User: userId,
27 | },
28 | data: {
29 | name: getThemeName(displayName),
30 | display_name: displayName,
31 | },
32 | });
33 |
34 | revalidatePath("/", "page");
35 |
36 | return NextResponse.json(theme);
37 | } catch (error) {
38 | console.error("Error updating theme name:", error);
39 | return NextResponse.json(
40 | { error: "Failed to update theme name" },
41 | { status: 500 }
42 | );
43 | } finally {
44 | await prisma.$disconnect();
45 | }
46 | };
47 |
--------------------------------------------------------------------------------
/apps/web/app/api/theme/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { revalidatePath } from "next/cache";
3 | import { ThemeConfig } from "@/lib/core/types";
4 | import { PrismaClient } from "@prisma/client";
5 | import { invertPalette } from "@/app/utils";
6 |
7 | const prisma = new PrismaClient();
8 |
9 | export const PUT = async (
10 | req: Request,
11 | { params }: { params: { id: string } }
12 | ) => {
13 | const { id } = params;
14 | const { name, displayName, palette, userId } =
15 | (await req.json()) as ThemeConfig & { userId: string };
16 |
17 | try {
18 | const theme = await prisma.themes.update({
19 | where: {
20 | xata_id: id,
21 | User: userId,
22 | },
23 | data: {
24 | name,
25 | display_name: displayName,
26 | category: "user",
27 | ThemePalettes: {
28 | update: [
29 | {
30 | where: {
31 | xata_id: palette.light.id,
32 | mode: "light",
33 | },
34 | data: invertPalette(palette.light),
35 | },
36 | {
37 | where: {
38 | xata_id: palette.dark.id,
39 | mode: "dark",
40 | },
41 | data: invertPalette(palette.dark),
42 | },
43 | ],
44 | },
45 | },
46 | include: {
47 | ThemePalettes: true,
48 | TokenColors: true,
49 | },
50 | });
51 |
52 | revalidatePath("/", "page");
53 |
54 | return NextResponse.json(theme);
55 | } catch (error) {
56 | console.error("Error updating theme:", error);
57 | return NextResponse.json(
58 | { error: "Failed to update theme" },
59 | { status: 500 }
60 | );
61 | } finally {
62 | await prisma.$disconnect();
63 | }
64 | };
65 |
66 | export async function DELETE(
67 | req: Request,
68 | { params }: { params: { id: string } }
69 | ) {
70 | const { id } = params;
71 | const { userId } = await req.json();
72 |
73 | if (!userId) {
74 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
75 | }
76 |
77 | try {
78 | await prisma.themes.delete({
79 | where: {
80 | xata_id: id,
81 | User: userId,
82 | },
83 | });
84 |
85 | revalidatePath("/");
86 |
87 | return NextResponse.json({ message: "Theme deleted successfully" });
88 | } catch (error) {
89 | console.error("Error deleting theme:", error);
90 | return NextResponse.json(
91 | { error: "Failed to delete theme" },
92 | { status: 500 }
93 | );
94 | } finally {
95 | await prisma.$disconnect();
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/apps/web/app/api/theme/[id]/status/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { revalidatePath } from "next/cache";
3 | import { PrismaClient } from "@prisma/client";
4 |
5 | const prisma = new PrismaClient();
6 |
7 | export async function PATCH(
8 | req: Request,
9 | { params }: { params: { id: string } }
10 | ) {
11 | const { id } = params;
12 | const { isPublic, userId } = (await req.json()) as {
13 | isPublic: boolean;
14 | userId: string;
15 | };
16 |
17 | if (!userId) {
18 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
19 | }
20 |
21 | try {
22 | const theme = await prisma.themes.update({
23 | where: {
24 | xata_id: id,
25 | User: userId,
26 | },
27 | data: {
28 | is_public: isPublic,
29 | },
30 | });
31 |
32 | revalidatePath("/");
33 |
34 | return NextResponse.json(theme);
35 | } catch (error) {
36 | console.error("Error updating theme public status:", error);
37 | return NextResponse.json(
38 | { error: "Failed to update theme public status" },
39 | { status: 500 }
40 | );
41 | } finally {
42 | await prisma.$disconnect();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/apps/web/app/api/theme/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { Prisma, PrismaClient } from "@prisma/client";
3 | import {
4 | formatTheme,
5 | invertPaletteWithoutId,
6 | invertTokenColors,
7 | sortThemes,
8 | } from "@/app/utils";
9 | import { defaultThemeConfig } from "@/lib/core/config";
10 | import { revalidatePath } from "next/cache";
11 | import { ThemeConfig } from "@/lib/core/types";
12 |
13 | const prisma = new PrismaClient();
14 |
15 | export const GET = async () => {
16 | try {
17 | const themes = await prisma.themes.findMany({
18 | include: {
19 | ThemePalettes: true,
20 | TokenColors: true,
21 | Users: true,
22 | },
23 | });
24 |
25 | const formattedThemes = sortThemes(themes.map(formatTheme));
26 |
27 | return NextResponse.json(formattedThemes);
28 | } catch (error) {
29 | console.error("Error fetching themes:", error);
30 | return NextResponse.json(
31 | { error: "Failed to fetch themes" },
32 | { status: 500 }
33 | );
34 | } finally {
35 | await prisma.$disconnect();
36 | }
37 | };
38 |
39 | export const POST = async (req: Request) => {
40 | const { name, displayName, palette, userId } =
41 | (await req.json()) as ThemeConfig & { userId: string | undefined };
42 |
43 | try {
44 | const themeData: Prisma.ThemesCreateInput = {
45 | name: name,
46 | display_name: displayName,
47 | category: "user",
48 | is_public: true,
49 | ThemePalettes: {
50 | create: [
51 | { mode: "light", ...invertPaletteWithoutId(palette.light) },
52 | { mode: "dark", ...invertPaletteWithoutId(palette.dark) },
53 | ],
54 | },
55 | TokenColors: {
56 | create: [invertTokenColors(defaultThemeConfig.tokenColors)],
57 | },
58 | };
59 |
60 | if (userId !== undefined) {
61 | themeData.Users = {
62 | connect: { xata_id: userId },
63 | };
64 | }
65 |
66 | const theme = await prisma.themes.create({
67 | data: themeData,
68 | include: {
69 | ThemePalettes: true,
70 | TokenColors: true,
71 | Users: true,
72 | },
73 | });
74 |
75 | revalidatePath("/", "page");
76 |
77 | return NextResponse.json(theme);
78 | } catch (error) {
79 | console.error("Error creating theme:", error);
80 | return NextResponse.json(
81 | { error: "Failed to create theme" },
82 | { status: 500 }
83 | );
84 | } finally {
85 | await prisma.$disconnect();
86 | }
87 | };
88 |
--------------------------------------------------------------------------------
/apps/web/app/api/themes/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { getInitialThemes } from "@/lib/api";
3 |
4 | export async function GET(request: Request) {
5 | const { searchParams } = new URL(request.url);
6 | const page = Number(searchParams.get("page")) || 1;
7 | const limit = Number(searchParams.get("limit")) || 20;
8 | const category = searchParams.get("category") || undefined;
9 | const userId = searchParams.get("userId") || undefined;
10 |
11 | try {
12 | const themes = await getInitialThemes(page, limit, category, userId);
13 | return NextResponse.json(themes);
14 | } catch (error) {
15 | return NextResponse.json(
16 | { error: "Failed to fetch themes" },
17 | { status: 500 },
18 | );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/apps/web/app/api/unsubscribe/route.ts:
--------------------------------------------------------------------------------
1 | import { Redis } from "@upstash/redis";
2 | import { NextRequest, NextResponse } from "next/server";
3 |
4 | const redis = Redis.fromEnv();
5 |
6 | export async function GET(request: NextRequest) {
7 | const { searchParams } = new URL(request.url);
8 | const email = searchParams.get("email");
9 |
10 | if (!email) {
11 | return NextResponse.json(
12 | { error: "Email parameter is missing" },
13 | { status: 400 }
14 | );
15 | }
16 |
17 | const hashKey = `subscriptions:${email}`;
18 | const field = "tinte";
19 |
20 | const isSubscribed = await redis.hget(hashKey, field);
21 | if (!isSubscribed) {
22 | return NextResponse.json(
23 | { error: "Email is not subscribed" },
24 | { status: 400 }
25 | );
26 | }
27 |
28 | await redis.hdel(hashKey, field);
29 |
30 | return NextResponse.json(
31 | { message: "Email unsubscribed successfully" },
32 | { status: 200 }
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/apps/web/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 3% 10%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 20 14.3% 4.1%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 20 14.3% 4.1%;
13 | --primary: 240 3% 10%;
14 | --primary-foreground: 60 9.1% 97.8%;
15 | --secondary: 60 4.8% 95.9%;
16 | --secondary-foreground: 24 9.8% 10%;
17 | --muted: 60 4.8% 95.9%;
18 | --muted-foreground: 25 5.3% 44.7%;
19 | --accent: 60 4.8% 95.9%;
20 | --accent-foreground: 24 9.8% 10%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 60 9.1% 97.8%;
23 | --border: 20 5.9% 90%;
24 | --input: 20 5.9% 90%;
25 | --ring: 20 14.3% 4.1%;
26 | --chart-1: 12 76% 61%;
27 | --chart-2: 173 58% 39%;
28 | --chart-3: 197 37% 24%;
29 | --chart-4: 43 74% 66%;
30 | --chart-5: 27 87% 67%;
31 |
32 | --radius: 0.5rem;
33 | --space: 0.25rem;
34 | --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
35 | }
36 |
37 | .dark {
38 | --background: 240 3% 10%;
39 | --foreground: 0 0% 100%;
40 | --card: 240 3% 10%;
41 | --card-foreground: 0 0% 100%;
42 | --popover: 240 3% 10%;
43 | --popover-foreground: 0 0% 100%;
44 | --primary: 0 0% 100%;
45 | --primary-foreground: 240 3% 10%;
46 | --secondary: 240 3% 20%;
47 | --secondary-foreground: 0 0% 100%;
48 | --muted: 240 3% 20%;
49 | --muted-foreground: 240 5% 64.9%;
50 | --accent: 240 3% 20%;
51 | --accent-foreground: 0 0% 100%;
52 | --destructive: 0 62.8% 30.6%;
53 | --destructive-foreground: 0 0% 100%;
54 | --border: 240 3% 20%;
55 | --input: 240 3% 20%;
56 | --ring: 240 4.9% 83.9%;
57 | --chart-1: 12 76% 61%;
58 | --chart-2: 173 58% 59%;
59 | --chart-3: 197 37% 54%;
60 | --chart-4: 43 74% 66%;
61 | --chart-5: 27 87% 67%;
62 | }
63 | }
64 |
65 | @layer base {
66 | * {
67 | @apply border-border;
68 | }
69 | body {
70 | @apply bg-background text-foreground;
71 | font-feature-settings:
72 | "rlig" 1,
73 | "calt" 1;
74 | }
75 |
76 | html.dark .shiki,
77 | html.dark .shiki span {
78 | color: var(--shiki-dark) !important;
79 | background-color: var(--shiki-dark-bg) !important;
80 | }
81 |
82 | .custom-shadow {
83 | box-shadow:
84 | 0 0 10px 0 hsl(var(--background) / 0.8),
85 | 0 0 30px 10px hsl(var(--background) / 0.6),
86 | 0 0 60px 20px hsl(var(--background) / 0.4),
87 | 0 0 100px 40px hsl(var(--background) / 0.2);
88 | }
89 | }
90 |
91 | .cl-modalBackdrop {
92 | align-items: center !important;
93 | }
94 |
--------------------------------------------------------------------------------
/apps/web/app/t/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable turbo/no-undeclared-env-vars */
2 | import { Metadata } from "next";
3 | import { getThemeById } from "@/lib/api";
4 | import { ThemeContent } from "./theme-content";
5 |
6 | interface ThemePageProps {
7 | params: {
8 | id: string;
9 | };
10 | }
11 |
12 | export async function generateMetadata({
13 | params,
14 | }: ThemePageProps): Promise {
15 | const theme = await getThemeById(params.id);
16 |
17 | if (!theme) {
18 | return {
19 | title: "Theme Not Found",
20 | };
21 | }
22 |
23 | const ogImageUrl = `${process.env.NEXT_PUBLIC_BASE_URL}/api/og?id=${params.id}`;
24 |
25 | return {
26 | title: `${theme.displayName} - Tinte VS Code Theme`,
27 | description: `Explore the ${theme.displayName} theme for VS Code, created by ${theme.user?.username || "Anonymous"}. A ${theme.category} theme that enhances your coding experience.`,
28 | openGraph: {
29 | title: `${theme.displayName} - Tinte VS Code Theme`,
30 | description: `Explore the ${theme.displayName} theme for VS Code, created by ${theme.user?.username || "Anonymous"}. A ${theme.category} theme that enhances your coding experience.`,
31 | url: `${process.env.NEXT_PUBLIC_BASE_URL}/t/${params.id}`,
32 | siteName: "Tinte",
33 | images: [
34 | {
35 | url: ogImageUrl,
36 | width: 1200,
37 | height: 630,
38 | alt: `${theme.displayName} VS Code Theme`,
39 | },
40 | ],
41 | locale: "en-US",
42 | type: "website",
43 | },
44 | twitter: {
45 | card: "summary_large_image",
46 | title: `${theme.displayName} - Tinte VS Code Theme`,
47 | description: `Explore the ${theme.displayName} theme for VS Code, created by ${theme.user?.username || "Anonymous"}. A ${theme.category} theme that enhances your coding experience.`,
48 | images: [ogImageUrl],
49 | creator: "@raillyhugo",
50 | },
51 | };
52 | }
53 |
54 | export default async function ThemePage({ params }: ThemePageProps) {
55 | const theme = await getThemeById(params.id);
56 |
57 | return ;
58 | }
59 |
--------------------------------------------------------------------------------
/apps/web/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/globals.css",
9 | "baseColor": "stone",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/apps/web/components/achievement-banner.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { motion } from "framer-motion";
3 |
4 | const MidudevLogo = () => (
5 |
34 | );
35 |
36 | export const AchievementBanner: React.FC = () => {
37 | return (
38 |
44 |
45 | 🏆 We won 3rd place at the
46 | ▲ Vercel
47 | x
48 |
49 |
50 | Midudev
51 |
52 | Hackathon!
53 |
54 |
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/apps/web/components/atom/rh-tinte-link-logo.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { IconTinte } from "@/components/ui/icons";
3 | import RHLogoIcon from "@/public/rh-logo.svg";
4 | import Link from "next/link";
5 |
6 | export function RHTinteLinkLogo() {
7 | return (
8 |
9 |
15 |
16 |
17 |
{"/"}
18 |
19 |
20 |
tinte
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/apps/web/components/atom/tinte-link-logo.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { IconTinte } from "@/components/ui/icons";
3 | import Link from "next/link";
4 |
5 | export function TinteLinkLogo() {
6 | return (
7 |
8 |
9 | tinte
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/apps/web/components/circular-gradient.tsx:
--------------------------------------------------------------------------------
1 | import { Palette } from "@/lib/core/types";
2 | import { cn } from "@/lib/utils";
3 |
4 | interface CircularGradientProps {
5 | className?: string;
6 | palette: Palette;
7 | }
8 |
9 | export const CircularGradient = ({
10 | className,
11 | palette,
12 | }: CircularGradientProps) => {
13 | return (
14 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/apps/web/components/color-picker-button.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Button } from "@/components/ui/button";
3 | import {
4 | Popover,
5 | PopoverContent,
6 | PopoverTrigger,
7 | } from "@/components/ui/popover";
8 | import { Colorful } from "@uiw/react-color";
9 | import { hsvaToHex, hsvaToHsla } from "@uiw/color-convert";
10 | import { IconChevronDown } from "./ui/icons";
11 | import { useDebounce } from "@/lib/hooks/use-debounce";
12 | import { cn } from "@/lib/utils";
13 |
14 | interface ColorPickerButtonProps {
15 | color: { h: number; s: number; v: number; a: number };
16 | onChange: (color: { h: number; s: number; v: number; a: number }) => void;
17 | disabled?: boolean;
18 | }
19 | const getTextColorFromHSL = (l: number) => {
20 | return l > 50 ? "#000000" : "#FFFFFF";
21 | };
22 |
23 | const ColorPickerButton = ({
24 | color,
25 | onChange,
26 | disabled,
27 | }: ColorPickerButtonProps) => {
28 | const [isOpen, setIsOpen] = useState(false);
29 | const [localColor, setLocalColor] = useState(color);
30 |
31 | const debouncedOnChange = useDebounce(
32 | (newColor: { h: number; s: number; v: number; a: number }) => {
33 | onChange(newColor);
34 | },
35 | 100
36 | );
37 |
38 | const handleColorChange = (newColor: {
39 | hsva: { h: number; s: number; v: number; a: number };
40 | }) => {
41 | setLocalColor(newColor.hsva);
42 | debouncedOnChange(newColor.hsva);
43 | };
44 |
45 | useEffect(() => {
46 | setLocalColor(color);
47 | }, [color]);
48 |
49 | const hsla = hsvaToHsla(localColor);
50 | const hslString = `${Math.round(hsla.h)}° ${Math.round(hsla.s)}% ${Math.round(hsla.l)}%`;
51 |
52 | const backgroundColor = hsvaToHex(localColor);
53 | const textColor = getTextColorFromHSL(hsla.l);
54 |
55 | return (
56 | !disabled && setIsOpen(open)}
59 | >
60 |
61 |
73 |
74 |
75 |
80 |
81 |
82 | );
83 | };
84 |
85 | export default ColorPickerButton;
86 |
--------------------------------------------------------------------------------
/apps/web/components/counterscale-script.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Script from "next/script";
4 |
5 | export default function CounterscaleScript() {
6 | return (
7 | <>
8 |
17 |
22 | >
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/apps/web/components/empty-state.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/no-img-element */
2 | import { IconTinte, IconUser, IconGrid, IconZap } from "@/components/ui/icons";
3 | import IconRaycast from "@/public/logos/raycast.svg";
4 | import { ShineButton } from "./ui/shine-button";
5 | import { useRouter } from "next/navigation";
6 | import { SignedIn, SignedOut, useUser } from "@clerk/nextjs";
7 |
8 | interface EmptyStateProps {
9 | type: "community" | "user" | "all" | "featured" | "rayso";
10 | }
11 |
12 | export function EmptyState({ type }: EmptyStateProps) {
13 | const user = useUser();
14 | const content = {
15 | all: {
16 | icon: ,
17 | title: "No Themes Available",
18 | message: "There are currently no themes available in any category.",
19 | },
20 | featured: {
21 | icon: ,
22 | title: "No Featured Themes",
23 | message: "There are no featured themes at the moment. Check back later!",
24 | },
25 | rayso: {
26 | icon: ,
27 | title: "No Ray.so Themes",
28 | message: "There are currently no Ray.so themes available.",
29 | },
30 | community: {
31 | icon: ,
32 | title: "No Community Themes Yet",
33 | message: "Be the first to contribute a theme to the community!",
34 | },
35 | user: {
36 | icon: (
37 | <>
38 |
39 |
43 |
44 |
45 |
46 |
47 | >
48 | ),
49 | title: "No Custom Themes Yet",
50 | message: "Start creating your own custom themes to see them here.",
51 | },
52 | };
53 |
54 | const { icon, title, message } = content[type];
55 | const router = useRouter();
56 |
57 | return (
58 |
59 | {icon}
60 |
{title}
61 |
{message}
62 |
router.push("/vscode")}>
63 | Create a Theme
64 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/apps/web/components/language-switcher.tsx:
--------------------------------------------------------------------------------
1 | // components/LanguageSwitcher.tsx
2 | import React from "react";
3 | import {
4 | Select,
5 | SelectContent,
6 | SelectItem,
7 | SelectTrigger,
8 | SelectValue,
9 | } from "@/components/ui/select";
10 | import { Label } from "@/components/ui/label";
11 | import { DefaultIcon, LanguageLogos } from "@/lib/language-logos";
12 | import { LANGS } from "@/lib/constants";
13 |
14 | interface LanguageSwitcherProps {
15 | selectedLanguage: string;
16 | onLanguageChange: (language: string) => void;
17 | noLabel?: boolean;
18 | }
19 |
20 | export const LanguageSwitcher: React.FC = ({
21 | selectedLanguage,
22 | onLanguageChange,
23 | noLabel,
24 | }) => {
25 | return (
26 |
27 | {!noLabel && (
28 |
31 | )}
32 |
52 |
53 | );
54 | };
55 |
56 | function getLanguageDisplayName(lang: string) {
57 | if (
58 | (lang.length === 3 || lang.length === 4) &&
59 | !["ruby", "vue", "dart", "java", "rust", "zig"].includes(lang)
60 | ) {
61 | return lang.toUpperCase();
62 | }
63 | return lang.charAt(0).toUpperCase() + lang.slice(1);
64 | }
65 |
66 | function LanguageLogo({ language }: { language: string }): JSX.Element {
67 | const IconComponent = LanguageLogos[language] || DefaultIcon;
68 | return ;
69 | }
70 |
--------------------------------------------------------------------------------
/apps/web/components/loading-page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { motion } from "framer-motion";
3 |
4 | const LoadingPage = () => {
5 | return (
6 |
7 |
8 |
22 |
23 |
24 |
29 | Generating Themes
30 |
31 |
37 | Crafting beautiful color palettes just for you...
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default LoadingPage;
45 |
--------------------------------------------------------------------------------
/apps/web/components/providers.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { ThemeProvider as NextThemesProvider } from "next-themes";
5 | import { ThemeProviderProps } from "next-themes/dist/types";
6 |
7 | export function Providers({ children, ...props }: ThemeProviderProps) {
8 | return (
9 |
16 | {children}
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/apps/web/components/scroll-to-top.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowUpIcon } from "@radix-ui/react-icons";
2 | import React, { useState, useEffect } from "react";
3 | import { Button } from "./ui/button";
4 |
5 | export function ScrollToTopButton() {
6 | const [isVisible, setIsVisible] = useState(false);
7 |
8 | useEffect(() => {
9 | const toggleVisibility = () => {
10 | // Adjust this value based on when you want the button to appear
11 | setIsVisible(window.scrollY > 640);
12 | };
13 |
14 | window.addEventListener("scroll", toggleVisibility);
15 |
16 | return () => window.removeEventListener("scroll", toggleVisibility);
17 | }, []);
18 |
19 | const scrollToTop = () => {
20 | window.scrollTo({
21 | top: 0,
22 | behavior: "smooth",
23 | });
24 | };
25 |
26 | if (!isVisible) {
27 | return null;
28 | }
29 |
30 | return (
31 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/apps/web/components/sign-in-dialog.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { useUser } from "@clerk/nextjs";
3 | import { SignIn } from "@clerk/nextjs";
4 | import { Dialog, DialogOverlay, DialogPortal } from "@/components/ui/dialog";
5 | import { useTheme } from "next-themes";
6 | import { dark, experimental__simple } from "@clerk/themes";
7 | import { DialogContent } from "@radix-ui/react-dialog";
8 |
9 | interface SignInDialogProps {
10 | label?: ReactNode;
11 | redirectUrl?: string;
12 | open?: boolean;
13 | setOpen?: (open: boolean) => void;
14 | }
15 | export const SignInDialog: React.FC = ({
16 | redirectUrl = "/",
17 | open,
18 | setOpen,
19 | }) => {
20 | const { theme } = useTheme();
21 | const { isSignedIn } = useUser();
22 |
23 | if (isSignedIn) {
24 | return null;
25 | }
26 |
27 | return (
28 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/apps/web/components/theme-preview.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useState } from "react";
3 | import ReadOnlyPreview from "@/components/read-only-preview";
4 | import { CODE_SAMPLES, CODE_SAMPLES_SMALL } from "@/lib/constants";
5 | import { GeneratedVSCodeTheme } from "@/lib/core";
6 | import { cn } from "@/lib/utils";
7 | import { ThemeConfig } from "@/lib/core/types";
8 |
9 | interface ThemePreviewProps {
10 | vsCodeTheme: GeneratedVSCodeTheme;
11 | themeConfig?: ThemeConfig;
12 | width?: string;
13 | height?: string;
14 | small?: boolean;
15 | withEditButton?: boolean;
16 | }
17 |
18 | export function ThemePreview({
19 | vsCodeTheme,
20 | themeConfig,
21 | width = "w-full md:w-96",
22 | height = "h-[13.8rem]",
23 | small = true,
24 | withEditButton = false,
25 | }: ThemePreviewProps) {
26 | const [selectedLanguage, setSelectedLanguage] = useState("typescript");
27 | const _CODE_SAMPLES = small ? CODE_SAMPLES_SMALL : CODE_SAMPLES;
28 |
29 | return (
30 |
37 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/apps/web/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/apps/web/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | },
24 | );
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | );
34 | }
35 |
36 | export { Badge, badgeVariants };
37 |
--------------------------------------------------------------------------------
/apps/web/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | "outline-destructive":
23 | "border text-foreground hover:bg-destructive/10 hover:border-destructive/70",
24 | },
25 | size: {
26 | default: "h-8 px-4",
27 | sm: "h-7 rounded-md px-3 text-xs",
28 | lg: "h-9 rounded-md px-8",
29 | icon: "h-9 w-9",
30 | },
31 | },
32 | defaultVariants: {
33 | variant: "default",
34 | size: "default",
35 | },
36 | },
37 | );
38 |
39 | export interface ButtonProps
40 | extends React.ButtonHTMLAttributes,
41 | VariantProps {
42 | asChild?: boolean;
43 | }
44 |
45 | const Button = React.forwardRef(
46 | ({ className, variant, size, asChild = false, ...props }, ref) => {
47 | const Comp = asChild ? Slot : "button";
48 | return (
49 |
54 | );
55 | },
56 | );
57 | Button.displayName = "Button";
58 |
59 | export { Button, buttonVariants };
60 |
--------------------------------------------------------------------------------
/apps/web/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ))
42 | CardTitle.displayName = "CardTitle"
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLParagraphElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ))
54 | CardDescription.displayName = "CardDescription"
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ))
62 | CardContent.displayName = "CardContent"
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ))
74 | CardFooter.displayName = "CardFooter"
75 |
76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
77 |
--------------------------------------------------------------------------------
/apps/web/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | }
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/apps/web/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as LabelPrimitive from "@radix-ui/react-label";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const labelVariants = cva(
10 | "text-xs font-semibold leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ));
24 | Label.displayName = LabelPrimitive.Root.displayName;
25 |
26 | export { Label };
27 |
--------------------------------------------------------------------------------
/apps/web/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverAnchor = PopoverPrimitive.Anchor
13 |
14 | const PopoverContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
18 |
19 |
29 |
30 | ))
31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
32 |
33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
34 |
--------------------------------------------------------------------------------
/apps/web/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/apps/web/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { DragHandleDots2Icon } from "@radix-ui/react-icons";
4 | import * as ResizablePrimitive from "react-resizable-panels";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const ResizablePanelGroup = ({
9 | className,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
19 | );
20 |
21 | const ResizablePanel = ResizablePrimitive.Panel;
22 |
23 | const ResizableHandle = ({
24 | withHandle,
25 | className,
26 | ...props
27 | }: React.ComponentProps & {
28 | withHandle?: boolean;
29 | }) => (
30 | div]:rotate-90",
33 | className
34 | )}
35 | {...props}
36 | >
37 | {withHandle && (
38 |
39 |
40 |
41 | )}
42 |
43 | );
44 |
45 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
46 |
--------------------------------------------------------------------------------
/apps/web/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/apps/web/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | );
29 | Separator.displayName = SeparatorPrimitive.Root.displayName;
30 |
31 | export { Separator };
32 |
--------------------------------------------------------------------------------
/apps/web/components/ui/shine-button.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from "react";
2 | import { cn } from "@/lib/utils";
3 | import { Button, ButtonProps } from "./button";
4 |
5 | export const ShineButton = forwardRef(
6 | ({ className, ...props }, ref) => {
7 | return (
8 |
18 | );
19 | }
20 | );
21 |
22 | ShineButton.displayName = "ShineButton";
23 |
--------------------------------------------------------------------------------
/apps/web/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SliderPrimitive from "@radix-ui/react-slider"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 | ))
26 | Slider.displayName = SliderPrimitive.Root.displayName
27 |
28 | export { Slider }
29 |
--------------------------------------------------------------------------------
/apps/web/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/apps/web/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SwitchPrimitives from "@radix-ui/react-switch";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ));
27 | Switch.displayName = SwitchPrimitives.Root.displayName;
28 |
29 | export { Switch };
30 |
--------------------------------------------------------------------------------
/apps/web/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/apps/web/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
5 | import { type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { toggleVariants } from "@/components/ui/toggle"
9 |
10 | const ToggleGroupContext = React.createContext<
11 | VariantProps
12 | >({
13 | size: "default",
14 | variant: "default",
15 | })
16 |
17 | const ToggleGroup = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef &
20 | VariantProps
21 | >(({ className, variant, size, children, ...props }, ref) => (
22 |
27 |
28 | {children}
29 |
30 |
31 | ))
32 |
33 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
34 |
35 | const ToggleGroupItem = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef &
38 | VariantProps
39 | >(({ className, children, variant, size, ...props }, ref) => {
40 | const context = React.useContext(ToggleGroupContext)
41 |
42 | return (
43 |
54 | {children}
55 |
56 | )
57 | })
58 |
59 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
60 |
61 | export { ToggleGroup, ToggleGroupItem }
62 |
--------------------------------------------------------------------------------
/apps/web/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TogglePrimitive from "@radix-ui/react-toggle"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const toggleVariants = cva(
10 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
17 | },
18 | size: {
19 | default: "h-9 px-3",
20 | sm: "h-8 px-2",
21 | lg: "h-10 px-3",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | }
29 | )
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef &
34 | VariantProps
35 | >(({ className, variant, size, ...props }, ref) => (
36 |
41 | ))
42 |
43 | Toggle.displayName = TogglePrimitive.Root.displayName
44 |
45 | export { Toggle, toggleVariants }
46 |
--------------------------------------------------------------------------------
/apps/web/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 |
--------------------------------------------------------------------------------
/apps/web/inngest/client.ts:
--------------------------------------------------------------------------------
1 | import { Inngest } from "inngest";
2 |
3 | export const inngest = new Inngest({ id: "tinte" });
4 |
--------------------------------------------------------------------------------
/apps/web/inngest/functions.ts:
--------------------------------------------------------------------------------
1 | import { syncUser } from "./sync-user";
2 |
3 | export const functions = [syncUser];
4 |
5 | export { inngest } from "./client";
6 |
--------------------------------------------------------------------------------
/apps/web/inngest/sync-user.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 | import { inngest } from "./client";
3 |
4 | const prisma = new PrismaClient();
5 |
6 | export const syncUser = inngest.createFunction(
7 | { id: "sync-user-from-clerk" },
8 | { event: "clerk/user.created" },
9 | async ({ event, step }) => {
10 | const user = event.data;
11 | const { id: clerk_id, username, image_url } = user;
12 |
13 | await step.run("Sync user to database", async () => {
14 | await prisma.users.create({
15 | data: {
16 | clerk_id,
17 | username,
18 | xata_id: clerk_id,
19 | image_url,
20 | },
21 | });
22 | });
23 |
24 | return { result: "User synced successfully" };
25 | },
26 | );
27 |
--------------------------------------------------------------------------------
/apps/web/lib/core/index.ts:
--------------------------------------------------------------------------------
1 | import { getVSCodeColors } from "./colors";
2 | import { generateTokenColors } from "./tokens";
3 | import { ThemeConfig } from "./types";
4 |
5 | export function generateVSCodeTheme(themeConfig: ThemeConfig) {
6 | const { displayName, palette, tokenColors } = themeConfig;
7 |
8 | const dark = {
9 | name: "one-hunter-dark",
10 | displayName,
11 | type: "dark",
12 | colors: getVSCodeColors(palette.dark, "dark"),
13 | tokenColors: generateTokenColors(palette.dark, tokenColors),
14 | };
15 |
16 | const light = {
17 | name: "one-hunter-light",
18 | displayName,
19 | type: "light",
20 | colors: getVSCodeColors(palette.light, "light"),
21 | tokenColors: generateTokenColors(palette.light, tokenColors),
22 | };
23 |
24 | return { dark, light };
25 | }
26 |
27 | export type GeneratedVSCodeTheme = ReturnType;
28 |
--------------------------------------------------------------------------------
/apps/web/lib/core/tokens.ts:
--------------------------------------------------------------------------------
1 | import { entries } from "../utils";
2 | import { tokenToScopeMapping } from "./config";
3 | import { Palette, SemanticToken, TokenColorMap } from "./types";
4 |
5 | export function generateTokenColors(
6 | palette: Palette,
7 | tokenColors: TokenColorMap
8 | ) {
9 | return entries(tokenColors).map(([token, colorKey]) => ({
10 | name: token,
11 | scope: mapTokenToScope(token),
12 | settings: {
13 | foreground: palette[colorKey],
14 | },
15 | }));
16 | }
17 |
18 | function mapTokenToScope(token: SemanticToken): string | string[] {
19 | return tokenToScopeMapping[token];
20 | }
21 |
22 | export { mapTokenToScope };
23 |
--------------------------------------------------------------------------------
/apps/web/lib/core/types.ts:
--------------------------------------------------------------------------------
1 | import { Users } from "@prisma/client";
2 |
3 | export type Palette = {
4 | id: string;
5 | text: string;
6 | "text-2": string;
7 | "text-3": string;
8 | interface: string;
9 | "interface-2": string;
10 | "interface-3": string;
11 | background: string;
12 | "background-2": string;
13 | primary: string;
14 | secondary: string;
15 | accent: string;
16 | "accent-2": string;
17 | "accent-3": string;
18 | };
19 |
20 | export type ThemeType = "light" | "dark";
21 |
22 | export type DarkLightPalette = {
23 | dark: Palette;
24 | light: Palette;
25 | };
26 |
27 | export type ThemeConfig = {
28 | id: string;
29 | name: string;
30 | displayName: string;
31 | isPublic: boolean;
32 | category: "featured" | "rayso" | "community" | "user";
33 | createdAt: Date;
34 | user: Users | null;
35 | palette: DarkLightPalette;
36 | tokenColors: TokenColorMap;
37 | };
38 |
39 | export interface VSCodeTokenColor {
40 | name: string;
41 | scope: string | string[];
42 | settings: {
43 | foreground: string;
44 | };
45 | }
46 |
47 | export type TokenColorMap = Record;
48 |
49 | export type SemanticToken =
50 | | "plain"
51 | | "classes"
52 | | "interfaces"
53 | | "structs"
54 | | "enums"
55 | | "keys"
56 | | "methods"
57 | | "functions"
58 | | "variables"
59 | | "variablesOther"
60 | | "globalVariables"
61 | | "localVariables"
62 | | "parameters"
63 | | "properties"
64 | | "strings"
65 | | "stringEscapeSequences"
66 | | "keywords"
67 | | "keywordsControl"
68 | | "storageModifiers"
69 | | "comments"
70 | | "docComments"
71 | | "numbers"
72 | | "booleans"
73 | | "operators"
74 | | "macros"
75 | | "preprocessor"
76 | | "urls"
77 | | "tags"
78 | | "jsxTags"
79 | | "attributes"
80 | | "types"
81 | | "constants"
82 | | "labels"
83 | | "namespaces"
84 | | "modules"
85 | | "typeParameters"
86 | | "exceptions"
87 | | "decorators"
88 | | "calls"
89 | | "punctuation";
90 |
--------------------------------------------------------------------------------
/apps/web/lib/export-theme.ts:
--------------------------------------------------------------------------------
1 | import { ThemeConfig } from "@/lib/core/types";
2 | import { generateVSCodeTheme } from "@/lib/core";
3 | import { getThemeName } from "@/app/utils";
4 |
5 | export const exportThemeAsJSON = (themeConfig: ThemeConfig) => {
6 | const theme = generateVSCodeTheme(themeConfig);
7 | const jsonString = JSON.stringify(theme, null, 2);
8 | const blob = new Blob([jsonString], { type: "application/json" });
9 | const url = URL.createObjectURL(blob);
10 |
11 | const link = document.createElement("a");
12 | link.href = url;
13 | link.download = `${themeConfig.displayName}.json`;
14 | link.click();
15 |
16 | URL.revokeObjectURL(url);
17 | };
18 |
19 | const cleanFileName = (name: string): string => {
20 | return name
21 | .toLowerCase()
22 | .replace(/[^a-z0-9]+/g, "-")
23 | .replace(/^-+|-+$/g, "")
24 | .substring(0, 50);
25 | };
26 |
27 | export const exportThemeAsVSIX = async (
28 | themeConfig: ThemeConfig,
29 | isDark: boolean,
30 | ) => {
31 | const cleanedThemeName = cleanFileName(
32 | themeConfig.displayName || themeConfig.name,
33 | );
34 |
35 | const cleanedThemeConfig = {
36 | ...themeConfig,
37 | name: cleanedThemeName,
38 | displayName: cleanedThemeName,
39 | };
40 |
41 | const response = await fetch(process.env.NEXT_PUBLIC_EXPORT_API_URL!, {
42 | method: "POST",
43 | headers: {
44 | "Content-Type": "application/json",
45 | },
46 | body: JSON.stringify({
47 | themeConfig: cleanedThemeConfig,
48 | isDark,
49 | }),
50 | });
51 |
52 | if (!response.ok) {
53 | throw new Error("Failed to export VSIX");
54 | }
55 |
56 | const blob = await response.blob();
57 | const url = window.URL.createObjectURL(blob);
58 | const contentDisposition = response.headers.get("Content-Disposition");
59 |
60 | const fileNameMatch =
61 | contentDisposition && contentDisposition.match(/filename="(.+)"/);
62 |
63 | const fileName =
64 | fileNameMatch && fileNameMatch[1]
65 | ? cleanFileName(fileNameMatch[1])
66 | : `${cleanedThemeName}-${isDark ? "dark" : "light"}-0.0.1.vsix`;
67 |
68 | const link = document.createElement("a");
69 | link.href = url;
70 | link.download = fileName;
71 | link.click();
72 | window.URL.revokeObjectURL(url);
73 | };
74 |
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-binary-theme.ts:
--------------------------------------------------------------------------------
1 | import { useTheme } from "next-themes";
2 |
3 | export const useBinaryTheme = () => {
4 | const { theme, setTheme, systemTheme } = useTheme();
5 |
6 | const getCurrentTheme = (): "light" | "dark" => {
7 | if (theme === "system") {
8 | return systemTheme === "light" ? "light" : "dark";
9 | }
10 | return theme === "light" ? "light" : "dark";
11 | };
12 |
13 | const currentTheme = getCurrentTheme();
14 |
15 | return { currentTheme, setTheme };
16 | };
17 |
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-code-sample.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { CODE_SAMPLES, DEFAULT_LANGUAGE } from "@/lib/constants";
3 | import { debounce } from "../utils";
4 |
5 | export const useCodeSample = () => {
6 | const [colorPickerShouldBeHighlighted, setColorPickerShouldBeHighlighted] =
7 | useState({
8 | key: "",
9 | value: false,
10 | });
11 |
12 | const [lastLanguage, setLastLanguage] = useState(null);
13 | const defaultLanguage = lastLanguage || DEFAULT_LANGUAGE;
14 | const [selectedLanguage, setSelectedLanguage] = useState(defaultLanguage);
15 | const [code, setCode] = useState(
16 | CODE_SAMPLES[defaultLanguage],
17 | );
18 |
19 | const handleLanguageChange = (language: string) => {
20 | setSelectedLanguage(language);
21 | handleCodeChange(CODE_SAMPLES[language]);
22 | window.localStorage.setItem("lastLanguage", language);
23 | };
24 |
25 | useEffect(() => {
26 | const lastLanguage = window.localStorage.getItem("lastLanguage");
27 | setLastLanguage(lastLanguage);
28 | setCode(
29 | lastLanguage ? CODE_SAMPLES[lastLanguage] : CODE_SAMPLES[defaultLanguage],
30 | );
31 | setSelectedLanguage(lastLanguage || DEFAULT_LANGUAGE);
32 | }, []);
33 |
34 | const handleCodeChange = debounce((value: string | undefined) => {
35 | setCode(value);
36 | }, 500);
37 |
38 | return {
39 | selectedLanguage,
40 | code,
41 | handleCodeChange,
42 | handleLanguageChange,
43 | colorPickerShouldBeHighlighted,
44 | setColorPickerShouldBeHighlighted,
45 | };
46 | };
47 |
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-debounce.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useRef } from "react";
2 |
3 | export function useDebounce(callback: Function, delay: number) {
4 | const timeoutRef = useRef(null);
5 |
6 | return useCallback(
7 | (...args: any[]) => {
8 | if (timeoutRef.current) {
9 | clearTimeout(timeoutRef.current);
10 | }
11 | timeoutRef.current = setTimeout(() => {
12 | callback(...args);
13 | }, delay);
14 | },
15 | [callback, delay]
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-highlighter.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from "next-themes";
2 | import { useEffect, useState } from "react";
3 | import { ThemedToken, getHighlighter } from "shiki";
4 | import { LANGS, MONACO_SHIKI_LANGS } from "../constants";
5 | import { MonacoToken } from "../types";
6 |
7 | export function useHighlighter({
8 | theme,
9 | text,
10 | language,
11 | tokens,
12 | }: {
13 | theme: {
14 | light: any;
15 | dark: any;
16 | };
17 | text?: string;
18 | language: string;
19 | tokens?: Array;
20 | }) {
21 | const { theme: nextTheme } = useTheme();
22 | const [highlightedText, setHighlightedText] = useState("");
23 |
24 | useEffect(() => {
25 | async function highlight() {
26 | if (!text || !theme) return;
27 |
28 | const highlighter = await getHighlighter({
29 | themes: [theme.light, theme.dark],
30 | langs: Object.values(MONACO_SHIKI_LANGS),
31 | });
32 |
33 | setHighlightedText(
34 | highlighter.codeToHtml(text, {
35 | lang:
36 | MONACO_SHIKI_LANGS[language as keyof typeof MONACO_SHIKI_LANGS] ||
37 | "plaintext",
38 | themes: {
39 | light: theme.light.name,
40 | dark: theme.dark.name,
41 | },
42 | })
43 | );
44 | }
45 |
46 | highlight();
47 | }, [text, theme, nextTheme, language, tokens]);
48 |
49 | return { highlightedText };
50 | }
51 |
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-infinite-themes.ts:
--------------------------------------------------------------------------------
1 | import useSWRInfinite from "swr/infinite";
2 | import { useCallback } from "react";
3 | import { ThemeConfig } from "@/lib/core/types";
4 |
5 | const fetcher = (url: string) => fetch(url).then((res) => res.json());
6 |
7 | interface ThemeResponse {
8 | themes: ThemeConfig[];
9 | hasMore: boolean;
10 | }
11 |
12 | export function useInfiniteThemes(
13 | initialData: ThemeResponse,
14 | category: string,
15 | userId?: string | null,
16 | ) {
17 | const getKey = (
18 | pageIndex: number,
19 | previousPageData: ThemeResponse | null,
20 | ) => {
21 | if (previousPageData && !previousPageData.hasMore) return null;
22 | const url = new URL(`/api/themes`, window.location.origin);
23 | url.searchParams.append("page", (pageIndex + 1).toString());
24 | url.searchParams.append("limit", "20");
25 | url.searchParams.append("category", category);
26 | if (userId) url.searchParams.append("userId", userId);
27 | return url.toString();
28 | };
29 |
30 | const { data, error, size, setSize, isValidating, mutate } =
31 | useSWRInfinite(getKey, fetcher, {
32 | fallbackData: [initialData],
33 | revalidateFirstPage: false,
34 | revalidateAll: false,
35 | persistSize: true,
36 | });
37 |
38 | const refreshFirstPage = useCallback(() => {
39 | mutate(
40 | async (currentData) => {
41 | const firstPageUrl = getKey(0, null);
42 | if (firstPageUrl) {
43 | const updatedFirstPage = await fetcher(firstPageUrl);
44 | return [
45 | updatedFirstPage,
46 | ...(currentData?.slice(1) || []),
47 | ] as ThemeResponse[];
48 | }
49 | return currentData as ThemeResponse[];
50 | },
51 | { revalidate: false },
52 | );
53 | }, [mutate, getKey]);
54 |
55 | return {
56 | data,
57 | error,
58 | size,
59 | setSize,
60 | isValidating,
61 | refreshFirstPage,
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-media-query.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | export function useMediaQuery(query: string): boolean {
4 | const [matches, setMatches] = useState(false);
5 |
6 | useEffect(() => {
7 | const media = window.matchMedia(query);
8 | if (media.matches !== matches) {
9 | setMatches(media.matches);
10 | }
11 | const listener = () => setMatches(media.matches);
12 | media.addListener(listener);
13 | return () => media.removeListener(listener);
14 | }, [matches, query]);
15 |
16 | return matches;
17 | }
18 |
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-theme-applier.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo } from "react";
2 | import { useAtom } from "jotai";
3 | import { themeAtom } from "@/lib/atoms";
4 | import { useTheme } from "next-themes";
5 |
6 | export function useThemeApplier() {
7 | const [shadcnTheme, setShadcnTheme] = useAtom(themeAtom);
8 | const { resolvedTheme } = useTheme();
9 |
10 | const isCurrentlyDark = resolvedTheme === "dark";
11 |
12 | const currentColorScheme = useMemo(
13 | () => (isCurrentlyDark ? shadcnTheme.dark : shadcnTheme.light),
14 | [isCurrentlyDark, shadcnTheme.dark, shadcnTheme.light],
15 | );
16 |
17 | const currentChartTheme = useMemo(
18 | () =>
19 | isCurrentlyDark ? shadcnTheme.charts.dark : shadcnTheme.charts.light,
20 | [isCurrentlyDark, shadcnTheme.charts.dark, shadcnTheme.charts.light],
21 | );
22 |
23 | useEffect(() => {
24 | const applyTheme = () => {
25 | // Apply theme colors
26 | Object.entries(currentColorScheme).forEach(([key, value]) => {
27 | document.documentElement.style.setProperty(
28 | `--${key}`,
29 | `${value.h} ${value.s}% ${value.l}%`,
30 | );
31 | });
32 |
33 | // Apply chart colors
34 | Object.entries(currentChartTheme).forEach(([key, value]) => {
35 | document.documentElement.style.setProperty(
36 | `--${key}`,
37 | `${value.h} ${value.s}% ${value.l}%`,
38 | );
39 | });
40 |
41 | // Apply other theme properties
42 | document.documentElement.style.setProperty(
43 | "--radius",
44 | `${shadcnTheme?.radius}rem`,
45 | );
46 | };
47 |
48 | applyTheme();
49 |
50 | // Set up a MutationObserver to watch for theme changes
51 | const observer = new MutationObserver((mutations) => {
52 | mutations.forEach((mutation) => {
53 | if (
54 | mutation.type === "attributes" &&
55 | mutation.attributeName === "class"
56 | ) {
57 | applyTheme();
58 | }
59 | });
60 | });
61 |
62 | observer.observe(document.documentElement, {
63 | attributes: true,
64 | attributeFilter: ["class"],
65 | });
66 |
67 | return () => observer.disconnect();
68 | }, [shadcnTheme, isCurrentlyDark]); // Only depend on shadcnTheme and isCurrentlyDark
69 |
70 | return {
71 | currentTheme: shadcnTheme,
72 | setCurrentTheme: setShadcnTheme,
73 | currentColorScheme,
74 | currentChartTheme,
75 | isCurrentlyDark,
76 | };
77 | }
78 |
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-theme-enhancer.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { toast } from "sonner";
3 |
4 | type EnhancementType = "shadcn" | "vscode";
5 |
6 | export const useDescriptionEnhancer = () => {
7 | const [isEnhancing, setIsEnhancing] = useState(false);
8 |
9 | const enhanceDescription = async (
10 | themeDescription: string,
11 | type: EnhancementType,
12 | ) => {
13 | if (themeDescription.trim().length < 3) {
14 | toast.error("Please provide a longer theme description to enhance");
15 | return;
16 | }
17 |
18 | setIsEnhancing(true);
19 | try {
20 | const response = await fetch(`/api/enhance/${type}`, {
21 | method: "POST",
22 | headers: {
23 | "Content-Type": "application/json",
24 | },
25 | body: JSON.stringify({ prompt: themeDescription }),
26 | });
27 |
28 | if (!response.ok) {
29 | throw new Error("Failed to enhance description");
30 | }
31 |
32 | const { enhancedPrompt } = await response.json();
33 | toast.success("Description enhanced successfully");
34 | return enhancedPrompt;
35 | } catch (error) {
36 | console.error("Error enhancing description:", error);
37 | toast.error("Failed to enhance description");
38 | } finally {
39 | setIsEnhancing(false);
40 | }
41 | };
42 |
43 | return { isEnhancing, enhanceDescription };
44 | };
45 |
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-theme-export.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { toast } from "sonner";
3 | import { ThemeConfig } from "@/lib/core/types";
4 | import { exportThemeAsJSON, exportThemeAsVSIX } from "../export-theme";
5 |
6 | export const useThemeExport = () => {
7 | const [loading, setLoading] = useState(false);
8 |
9 | const exportJSON = (themeConfig: ThemeConfig) => {
10 | exportThemeAsJSON(themeConfig);
11 | toast.success("Theme exported as JSON");
12 | };
13 |
14 | const exportVSIX = async (themeConfig: ThemeConfig, isDark: boolean) => {
15 | try {
16 | setLoading(true);
17 | toast.info("Exporting theme as VSIX...");
18 | await exportThemeAsVSIX(themeConfig, isDark);
19 | toast.dismiss();
20 | toast.success("Theme exported as VSIX");
21 | } catch (error) {
22 | toast.dismiss();
23 | console.error("Failed to export VSIX:", error);
24 | toast.error("Failed to export theme as VSIX");
25 | } finally {
26 | setLoading(false);
27 | }
28 | };
29 |
30 | return { loading, exportJSON, exportVSIX };
31 | };
32 |
--------------------------------------------------------------------------------
/apps/web/lib/types.ts:
--------------------------------------------------------------------------------
1 | export type MonacoToken = {
2 | text: string;
3 | type: string;
4 | className?: string | null;
5 | foreground?: string | null;
6 | lineNumber: number;
7 | tokenIndex: number;
8 | };
9 |
--------------------------------------------------------------------------------
/apps/web/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { Prisma } from "@prisma/client";
2 | import { type ClassValue, clsx } from "clsx";
3 | import { twMerge } from "tailwind-merge";
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs));
7 | }
8 |
9 | export const entries = (obj: O) =>
10 | Object.entries(obj) as { [K in keyof O]: [K, O[K]] }[keyof O][];
11 |
12 | export function debounce void>(
13 | callback: T,
14 | delay: number,
15 | ): (...args: Parameters) => void {
16 | let timeoutId: NodeJS.Timeout;
17 | return (...args: Parameters): void => {
18 | clearTimeout(timeoutId);
19 | timeoutId = setTimeout(() => {
20 | callback(...args);
21 | }, delay);
22 | };
23 | }
24 |
25 | export const sanitizeJsonInput = (input: unknown): Prisma.InputJsonValue => {
26 | if (input !== null) {
27 | return JSON.parse(JSON.stringify(input));
28 | }
29 | return JSON.parse(JSON.stringify({}));
30 | };
31 |
--------------------------------------------------------------------------------
/apps/web/middleware.ts:
--------------------------------------------------------------------------------
1 | import { clerkMiddleware } from "@clerk/nextjs/server";
2 |
3 | export default clerkMiddleware();
4 |
5 | export const config = {
6 | matcher: [
7 | "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
8 | "/(api|trpc)(.*)",
9 | ],
10 | };
11 |
--------------------------------------------------------------------------------
/apps/web/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | functions = "netlify/functions"
3 |
4 | [[redirects]]
5 | from = "/api/*"
6 | to = "/.netlify/functions/:splat"
7 | status = 200
8 |
--------------------------------------------------------------------------------
/apps/web/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | // NOTE: This file should not be edited
6 | // see https://nextjs.org/docs/basic-features/typescript for more information.
7 |
--------------------------------------------------------------------------------
/apps/web/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | module.exports = {
3 | transpilePackages: ["@repo/ui"],
4 | webpack(config) {
5 | config.module.rules.push({
6 | test: /\.svg$/i,
7 | issuer: /\.[jt]sx?$/,
8 | use: ["@svgr/webpack"],
9 | });
10 | return config;
11 | },
12 | experimental: {
13 | optimizePackageImports: ["shiki", "@shikijs/monaco"],
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/apps/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/apps/web/public/circles.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/web/public/flexoki-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/apps/web/public/flexoki-logo.png
--------------------------------------------------------------------------------
/apps/web/public/fonts/Geist-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/apps/web/public/fonts/Geist-Bold.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/Geist-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/apps/web/public/fonts/Geist-Regular.ttf
--------------------------------------------------------------------------------
/apps/web/public/fonts/GeistMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/apps/web/public/fonts/GeistMono-Regular.ttf
--------------------------------------------------------------------------------
/apps/web/public/logos/angular.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/astro.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/bash.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/c.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/cpp.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/csharp.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/css.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/dart.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/html.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/java.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/apps/web/public/logos/javascript.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/kotlin.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/lua.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/markdown.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/php.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/r.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/raycast.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/apps/web/public/logos/scala.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/apps/web/public/logos/solidity.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/sql.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/apps/web/public/logos/svelte.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/swift.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/apps/web/public/logos/toml.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/typescript.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/vue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/logos/zig.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/og-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/apps/web/public/og-image.png
--------------------------------------------------------------------------------
/apps/web/public/one-hunter-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/apps/web/public/one-hunter-logo.png
--------------------------------------------------------------------------------
/apps/web/public/placeholder.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/apps/web/public/rh-logo.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/apps/web/public/supabase-icon 1.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/apps/web/public/tailwindcss-icon 1.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/apps/web/public/themes/zaffre.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "theme-zaffre",
3 | "type": "registry:theme",
4 | "cssVars": {
5 | "light": {
6 | "primary": "221.2 83.2% 53.3%",
7 | "muted": "60 4.8% 94.9%",
8 | "accent": "60 4.8% 94.9%",
9 | "border": "60 4.8% 94.9%",
10 | "input": "60 4.8% 94.9%",
11 | "radius": "1.2rem",
12 | "space": "0.25rem",
13 | "shadow": "0.4"
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/apps/web/public/tinte-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/apps/web/public/tinte-1.png
--------------------------------------------------------------------------------
/apps/web/public/tinte-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/apps/web/public/tinte-2.png
--------------------------------------------------------------------------------
/apps/web/public/tinte-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/apps/web/public/tinte-3.png
--------------------------------------------------------------------------------
/apps/web/public/vercel-icon 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/apps/web/public/vercel-icon 1.png
--------------------------------------------------------------------------------
/apps/web/public/vercel-icon 1.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/apps/web/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": ["./*"]
7 | },
8 | "plugins": [
9 | {
10 | "name": "next"
11 | }
12 | ],
13 | "strictNullChecks": true
14 | },
15 | "include": [
16 | "next-env.d.ts",
17 | "next.config.js",
18 | "tailwind.config.js",
19 | "postcss.config.js",
20 | "netlify/functions/createVSIX.js",
21 | "**/*.ts",
22 | "**/*.tsx",
23 | ".next/types/**/*.ts"
24 | ],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/apps/web/types.d.ts:
--------------------------------------------------------------------------------
1 | interface EyeDropperOptions {
2 | signal?: AbortSignal;
3 | }
4 |
5 | interface EyeDropperResult {
6 | sRGBHex: string;
7 | }
8 |
9 | interface EyeDropper {
10 | open(options?: EyeDropperOptions): Promise;
11 | }
12 |
13 | interface Window {
14 | EyeDropper: {
15 | new (): EyeDropper;
16 | };
17 | }
18 |
19 | declare module "*.svg" {
20 | import React from "react";
21 | const SVG: React.VFC>;
22 | export default SVG;
23 | }
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tinte-2",
3 | "private": true,
4 | "scripts": {
5 | "build": "turbo build",
6 | "dev": "turbo dev",
7 | "lint": "turbo lint",
8 | "format": "prettier --write \"**/*.{ts,tsx,md}\""
9 | },
10 | "devDependencies": {
11 | "@repo/eslint-config": "workspace:*",
12 | "@repo/typescript-config": "workspace:*",
13 | "prettier": "^3.2.5",
14 | "turbo": "latest"
15 | },
16 | "packageManager": "pnpm@9.1.1",
17 | "engines": {
18 | "node": ">=18"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/eslint-config/README.md:
--------------------------------------------------------------------------------
1 | # `@turbo/eslint-config`
2 |
3 | Collection of internal eslint configurations.
4 |
--------------------------------------------------------------------------------
/packages/eslint-config/library.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /** @type {import("eslint").Linter.Config} */
6 | module.exports = {
7 | extends: [
8 | "eslint:recommended",
9 | "plugin:@typescript-eslint/eslint-recommended",
10 | "plugin:@typescript-eslint/recommended",
11 | "prettier",
12 | "eslint-config-turbo",
13 | ],
14 | plugins: ["only-warn"],
15 | globals: {
16 | React: true,
17 | JSX: true,
18 | },
19 | env: {
20 | node: true,
21 | },
22 | settings: {
23 | "import/resolver": {
24 | typescript: {
25 | project,
26 | },
27 | },
28 | },
29 | ignorePatterns: [
30 | // Ignore dotfiles
31 | ".*.js",
32 | "node_modules/",
33 | "dist/",
34 | ],
35 | overrides: [
36 | {
37 | files: ["*.js?(x)", "*.ts?(x)"],
38 | },
39 | ],
40 | };
41 |
--------------------------------------------------------------------------------
/packages/eslint-config/next.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /** @type {import("eslint").Linter.Config} */
6 | module.exports = {
7 | extends: [
8 | "eslint:recommended",
9 | "plugin:@typescript-eslint/eslint-recommended",
10 | "plugin:@typescript-eslint/recommended",
11 | "prettier",
12 | require.resolve("@vercel/style-guide/eslint/next"),
13 | "eslint-config-turbo",
14 | ],
15 | globals: {
16 | React: true,
17 | JSX: true,
18 | },
19 | env: {
20 | node: true,
21 | browser: true,
22 | },
23 | plugins: ["only-warn"],
24 | settings: {
25 | "import/resolver": {
26 | typescript: {
27 | project,
28 | },
29 | },
30 | },
31 | ignorePatterns: [
32 | // Ignore dotfiles
33 | ".*.js",
34 | "node_modules/",
35 | ],
36 | overrides: [{ files: ["*.js?(x)", "*.ts?(x)"] }],
37 | };
38 |
--------------------------------------------------------------------------------
/packages/eslint-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/eslint-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "files": [
6 | "library.js",
7 | "next.js",
8 | "react-internal.js"
9 | ],
10 | "devDependencies": {
11 | "@vercel/style-guide": "^5.2.0",
12 | "eslint-config-turbo": "^1.12.4",
13 | "eslint-config-prettier": "^9.1.0",
14 | "eslint-plugin-only-warn": "^1.1.0",
15 | "@typescript-eslint/parser": "^7.1.0",
16 | "@typescript-eslint/eslint-plugin": "^7.1.0",
17 | "typescript": "^5.3.3"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/eslint-config/react-internal.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /*
6 | * This is a custom ESLint configuration for use with
7 | * internal (bundled by their consumer) libraries
8 | * that utilize React.
9 | */
10 |
11 | /** @type {import("eslint").Linter.Config} */
12 | module.exports = {
13 | extends: ["eslint:recommended", "prettier", "eslint-config-turbo"],
14 | plugins: ["only-warn"],
15 | globals: {
16 | React: true,
17 | JSX: true,
18 | },
19 | env: {
20 | browser: true,
21 | },
22 | settings: {
23 | "import/resolver": {
24 | typescript: {
25 | project,
26 | },
27 | },
28 | },
29 | ignorePatterns: [
30 | // Ignore dotfiles
31 | ".*.js",
32 | "node_modules/",
33 | "dist/",
34 | ],
35 | overrides: [
36 | // Force ESLint to detect .tsx files
37 | { files: ["*.js?(x)", "*.ts?(x)"] },
38 | ],
39 | };
40 |
--------------------------------------------------------------------------------
/packages/tinte-core/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/packages/tinte-core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tinte",
3 | "type": "module",
4 | "version": "0.1.0",
5 | "description": "An opinionated tool for generating multi-platform color themes",
6 | "main": "index.js",
7 | "scripts": {
8 | "build": "node --no-warnings --loader ts-node/esm src/index.ts"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/Railly/tinte.git"
13 | },
14 | "keywords": [
15 | "ink",
16 | "flexoki",
17 | "one-hunter",
18 | "vscode",
19 | "neovim",
20 | "windows-terminal",
21 | "theme-generator",
22 | "color-scheme"
23 | ],
24 | "author": "Railly Hugo",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/Railly/tinte/issues"
28 | },
29 | "homepage": "https://github.com/Railly/tinte#readme",
30 | "devDependencies": {
31 | "@types/node": "^20.8.4",
32 | "ts-node": "^10.9.1",
33 | "typescript": "^5.2.2"
34 | }
35 | }
--------------------------------------------------------------------------------
/packages/tinte-core/public/flexoki-dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/packages/tinte-core/public/flexoki-dark.jpg
--------------------------------------------------------------------------------
/packages/tinte-core/public/flexoki-light.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/packages/tinte-core/public/flexoki-light.jpg
--------------------------------------------------------------------------------
/packages/tinte-core/public/one-hunter-dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/packages/tinte-core/public/one-hunter-dark.jpg
--------------------------------------------------------------------------------
/packages/tinte-core/public/one-hunter-light.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/packages/tinte-core/public/one-hunter-light.jpg
--------------------------------------------------------------------------------
/packages/tinte-core/public/tinte-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/packages/tinte-core/public/tinte-logo.png
--------------------------------------------------------------------------------
/packages/tinte-core/public/transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/packages/tinte-core/public/transparent.png
--------------------------------------------------------------------------------
/packages/tinte-core/src/config/index.ts:
--------------------------------------------------------------------------------
1 | import { FlexokiPalette } from "../palettes/flexoki.ts";
2 | import { OneHunterMaterialPalette } from "../palettes/one-hunter-material.ts";
3 | import { Palette } from "../palettes/types.ts";
4 | import { Vercel2024Palette } from "../palettes/vercel-2024.ts";
5 | import { MyTheme } from "../types.ts";
6 |
7 | export const currentTheme: MyTheme = "Vercel 2024 Flexoki";
8 |
9 | const myPalettes: Record = {
10 | Flexoki: FlexokiPalette,
11 | "One Hunter Material": OneHunterMaterialPalette,
12 | "One Hunter Flexoki": FlexokiPalette,
13 | "Vercel 2024": Vercel2024Palette,
14 | "Vercel 2024 Flexoki": FlexokiPalette,
15 | };
16 |
17 | export const currentPalette = myPalettes[currentTheme];
18 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/generators/alacritty/generate.ts:
--------------------------------------------------------------------------------
1 | import { mappedPalette } from "../../mapped-palette.ts";
2 | import { processPaletteHexToInt, toYAML } from "../../utils/format.ts";
3 | import { getThemeName, writeFile } from "../../utils/index.ts";
4 | import { ThemeType } from "../types.ts";
5 |
6 | export const generateAlacrittyTheme = ({
7 | name,
8 | themeType,
9 | }: {
10 | name: string;
11 | themeType: ThemeType;
12 | }) => {
13 | const themeName = getThemeName(name, themeType);
14 | const slugifiedName = getThemeName(name);
15 | const theme = {
16 | colors: {
17 | primary: processPaletteHexToInt({
18 | background: mappedPalette.bg[themeType],
19 | foreground: mappedPalette.tx[themeType],
20 | dim_foreground: mappedPalette.tx[themeType],
21 | bright_foreground: mappedPalette.tx[themeType],
22 | dim_background: mappedPalette.bg["dark"],
23 | bright_background: mappedPalette.bg["light"],
24 | }),
25 | cursor: processPaletteHexToInt({
26 | text: mappedPalette["tx-2"][themeType],
27 | cursor: mappedPalette["tx-2"][themeType],
28 | }),
29 | normal: processPaletteHexToInt({
30 | black: mappedPalette.bg["dark"],
31 | red: mappedPalette["re-2"][themeType],
32 | green: mappedPalette["gr-2"][themeType],
33 | yellow: mappedPalette["ye-2"][themeType],
34 | blue: mappedPalette["bl-2"][themeType],
35 | magenta: mappedPalette["pu-2"][themeType],
36 | cyan: mappedPalette["bl-2"][themeType],
37 | white: mappedPalette.tx[themeType],
38 | }),
39 | bright: processPaletteHexToInt({
40 | black: mappedPalette["tx-3"]["dark"],
41 | red: mappedPalette.re[themeType],
42 | green: mappedPalette.gr[themeType],
43 | yellow: mappedPalette.ye[themeType],
44 | blue: mappedPalette.bl[themeType],
45 | magenta: mappedPalette.pu[themeType],
46 | cyan: mappedPalette.cy[themeType],
47 | white: mappedPalette.bg["light"],
48 | }),
49 | dim: processPaletteHexToInt({
50 | black: mappedPalette.bg["dark"],
51 | red: mappedPalette["re-2"][themeType],
52 | green: mappedPalette["gr-2"][themeType],
53 | yellow: mappedPalette["ye-2"][themeType],
54 | blue: mappedPalette["bl-2"][themeType],
55 | magenta: mappedPalette["pu-2"][themeType],
56 | cyan: mappedPalette["bl-2"][themeType],
57 | white: mappedPalette.tx[themeType],
58 | }),
59 | },
60 | };
61 |
62 | const filePath = `./_generated/${slugifiedName}/alacritty/${themeName}.yaml`;
63 | writeFile(filePath, toYAML(theme));
64 | };
65 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/generators/fzf/generate.ts:
--------------------------------------------------------------------------------
1 | import { mappedPalette } from "../../mapped-palette.ts";
2 | import { toMD } from "../../utils/format.ts";
3 | import { getThemeName, writeFile } from "../../utils/index.ts";
4 | import { ThemeType } from "../types.ts";
5 |
6 | export const generateFzFTheme = ({
7 | name,
8 | themeType,
9 | }: {
10 | name: string;
11 | themeType: ThemeType;
12 | }) => {
13 | const slugifiedName = getThemeName(name);
14 |
15 | const themeLight = {
16 | name: `${slugifiedName}-light`,
17 | color: [
18 | {
19 | fg: mappedPalette["tx-3"].light,
20 | bg: mappedPalette.bg.light,
21 | hl: mappedPalette.tx.light,
22 | },
23 | {
24 | "fg+": mappedPalette["tx-3"].light,
25 | "bg+": mappedPalette["bg-2"].light,
26 | "hl+": mappedPalette.tx.light,
27 | },
28 | {
29 | border: mappedPalette.re.light,
30 | header: mappedPalette.tx.light,
31 | gutter: mappedPalette.bg.light,
32 | },
33 | {
34 | spinner: mappedPalette["cy-2"][themeType],
35 | info: mappedPalette["cy-2"][themeType],
36 | separator: mappedPalette["bg-2"][themeType],
37 | },
38 | {
39 | pointer: mappedPalette["ye-2"][themeType],
40 | marker: mappedPalette["re-2"][themeType],
41 | prompt: mappedPalette["ye-2"][themeType],
42 | },
43 | ],
44 | };
45 |
46 | const themeDark = {
47 | name: `${slugifiedName}-dark`,
48 | color: [
49 | {
50 | fg: mappedPalette["tx-2"].dark,
51 | bg: mappedPalette.bg.dark,
52 | hl: mappedPalette.bg.light,
53 | },
54 | {
55 | "fg+": mappedPalette["tx-2"].dark,
56 | "bg+": mappedPalette["bg-2"].dark,
57 | "hl+": mappedPalette.bg.light,
58 | },
59 | {
60 | border: mappedPalette["re-2"].dark,
61 | header: mappedPalette.bg.light,
62 | gutter: mappedPalette.bg.dark,
63 | },
64 | ...themeLight.color.slice(3),
65 | ],
66 | };
67 |
68 | const filePath = `./_generated/${slugifiedName}/fzf/theme.md`;
69 |
70 | const content = `${toMD(themeLight)}\n\n ${toMD(themeDark)}`;
71 |
72 | writeFile(filePath, content);
73 | };
74 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/generators/gimp/generate.ts:
--------------------------------------------------------------------------------
1 | import { mappedPalette } from "../../mapped-palette.ts";
2 | import { Color } from "../../utils/color.ts";
3 | import { entries, getThemeName, writeFile } from "../../utils/index.ts";
4 | import { ThemeType } from "../types.ts";
5 | import { formatAbbreviationToSemantic } from "./mappers.ts";
6 |
7 | export const generateGimpTheme = ({
8 | name,
9 | themeType,
10 | }: {
11 | name: string;
12 | themeType: ThemeType;
13 | }) => {
14 | const themeName = getThemeName(name);
15 |
16 | const maxLengths = {
17 | red: Math.max(
18 | ...Object.values(mappedPalette).map(
19 | (color) => Color.fromHex(color[themeType]).asInt.red.toString().length
20 | )
21 | ),
22 | green: Math.max(
23 | ...Object.values(mappedPalette).map(
24 | (color) => Color.fromHex(color[themeType]).asInt.green.toString().length
25 | )
26 | ),
27 | blue: Math.max(
28 | ...Object.values(mappedPalette).map(
29 | (color) => Color.fromHex(color[themeType]).asInt.blue.toString().length
30 | )
31 | ),
32 | };
33 |
34 | const theme = `GIMP Palette
35 | Name: ${themeName}
36 | Columns: 8
37 | ${entries(mappedPalette).map(([key, color]) => {
38 | const rgbaColor = Color.fromHex(color[themeType]);
39 |
40 | const paddedRed = rgbaColor.asInt.red
41 | .toString()
42 | .padStart(maxLengths.red, " ");
43 | const paddedGreen = rgbaColor.asInt.green
44 | .toString()
45 | .padStart(maxLengths.green, " ");
46 | const paddedBlue = rgbaColor.asInt.blue
47 | .toString()
48 | .padStart(maxLengths.blue, " ");
49 |
50 | return `${paddedRed} ${paddedGreen} ${paddedBlue} ${formatAbbreviationToSemantic(
51 | key
52 | )}`;
53 | }).join(`
54 | `)}`;
55 |
56 | const filePath = `./_generated/${themeName}/gimp/${themeName}.gpl`;
57 | writeFile(filePath, theme);
58 | };
59 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/generators/gimp/mappers.ts:
--------------------------------------------------------------------------------
1 | import { AllColorAbbreviations, AllToneAbbreviations } from "../../types.ts";
2 |
3 | const baseToneMappings = {
4 | tx: "Text",
5 | "tx-2": "Text Muted",
6 | "tx-3": "Text Faint",
7 | ui: "Interface",
8 | "ui-2": "Interface Hover",
9 | "ui-3": "Interface active",
10 | bg: "Background",
11 | "bg-2": "Background Secondary",
12 | } as const;
13 |
14 | const invertedColorAbbreviations = {
15 | re: "Red",
16 | or: "Orange",
17 | ye: "Yellow",
18 | gr: "Green",
19 | cy: "Cyan",
20 | bl: "Blue",
21 | pu: "Purple",
22 | ma: "Magenta",
23 | } as const;
24 |
25 | export const formatAbbreviationToSemantic = (
26 | abbreviation: AllColorAbbreviations | AllToneAbbreviations
27 | ) => {
28 | if (abbreviation in baseToneMappings)
29 | return baseToneMappings[abbreviation as keyof typeof baseToneMappings];
30 |
31 | if (abbreviation.endsWith("-2")) {
32 | const color = abbreviation.slice(0, abbreviation.length - 2);
33 | const semantic =
34 | invertedColorAbbreviations[
35 | color as keyof typeof invertedColorAbbreviations
36 | ];
37 | return `Light ${semantic}`;
38 | }
39 |
40 | return `Dark ${
41 | invertedColorAbbreviations[
42 | abbreviation as keyof typeof invertedColorAbbreviations
43 | ]
44 | }`;
45 | };
46 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/generators/index.ts:
--------------------------------------------------------------------------------
1 | import { generateAlacrittyTheme } from "./alacritty/generate.ts";
2 | import { generateGimpTheme } from "./gimp/generate.ts";
3 | import { generateITerm2Theme } from "./iterm2/generate.ts";
4 | import { generateKittyTheme } from "./kitty/generate.ts";
5 | import { generateLiteXLTheme } from "./lite-xl/generate.ts";
6 | import { generateThemeSHTheme } from "./theme-sh/generate.ts";
7 | import { generateVanillaCSSTheme } from "./vanilla-css/generate.ts";
8 | import { generateVSCodeTheme } from "./vscode/generate.ts";
9 | import { generateWarpTheme } from "./warp/generate.ts";
10 | import { generateWeztermTheme } from "./wezterm/generate.ts";
11 | import { generateWindowsTerminalTheme } from "./windows-terminal/generate.ts";
12 | import { generateXResourcesTheme } from "./xresources/generate.ts";
13 | import { ThemeType } from "./types.ts";
14 | import { generateFzFTheme } from "./fzf/generate.ts";
15 |
16 | type Provider = keyof typeof generators;
17 |
18 | export const generators = {
19 | vscode: generateVSCodeTheme,
20 | "windows-terminal": generateWindowsTerminalTheme,
21 | iterm2: generateITerm2Theme,
22 | alacritty: generateAlacrittyTheme,
23 | "vanilla-css": generateVanillaCSSTheme,
24 | gimp: generateGimpTheme,
25 | kitty: generateKittyTheme,
26 | "lite-xl": generateLiteXLTheme,
27 | "theme-sh": generateThemeSHTheme,
28 | xresources: generateXResourcesTheme,
29 | warp: generateWarpTheme,
30 | wezterm: generateWeztermTheme,
31 | fzf: generateFzFTheme,
32 | } as const;
33 |
34 | export const providers: Array<{
35 | name: Provider;
36 | themes: ThemeType[];
37 | }> = [
38 | {
39 | name: "vscode",
40 | themes: ["light", "dark"],
41 | },
42 | {
43 | name: "windows-terminal",
44 | themes: ["light", "dark"],
45 | },
46 | {
47 | name: "iterm2",
48 | themes: ["light", "dark"],
49 | },
50 | {
51 | name: "alacritty",
52 | themes: ["light", "dark"],
53 | },
54 | {
55 | name: "vanilla-css",
56 | themes: ["light"],
57 | },
58 | {
59 | name: "gimp",
60 | themes: ["dark"],
61 | },
62 | {
63 | name: "kitty",
64 | themes: ["light", "dark"],
65 | },
66 | {
67 | name: "lite-xl",
68 | themes: ["light", "dark"],
69 | },
70 | {
71 | name: "theme-sh",
72 | themes: ["light", "dark"],
73 | },
74 | {
75 | name: "xresources",
76 | themes: ["light", "dark"],
77 | },
78 | {
79 | name: "warp",
80 | themes: ["light", "dark"],
81 | },
82 | {
83 | name: "wezterm",
84 | themes: ["light", "dark"],
85 | },
86 | {
87 | name: "fzf",
88 | themes: ["light", "dark"],
89 | },
90 | ];
91 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/generators/iterm2/generate.ts:
--------------------------------------------------------------------------------
1 | import { mappedPalette } from "../../mapped-palette.ts";
2 | import { Color } from "../../utils/color.ts";
3 | import { entries, getThemeName, writeFile } from "../../utils/index.ts";
4 | import { ThemeType } from "../types.ts";
5 | import { mapITerm2Color } from "./mappers.ts";
6 |
7 | export const generateITerm2Theme = ({
8 | name,
9 | themeType,
10 | }: {
11 | name: string;
12 | themeType: ThemeType;
13 | }) => {
14 | const themeName = getThemeName(name, themeType);
15 | const slugifiedName = getThemeName(name);
16 |
17 | const theme = `
18 |
19 |
20 |
21 | ${entries(mapITerm2Color)
22 | .map(([key, color]) => {
23 | const rgbaColor = Color.fromHex(mappedPalette[color][themeType]);
24 | return ` ${key}
25 |
26 | Color Space
27 | sRGB
28 | Red Component
29 | ${rgbaColor.asFloat.red}
30 | Green Component
31 | ${rgbaColor.asFloat.green}
32 | Blue Component
33 | ${rgbaColor.asFloat.blue}
34 | Alpha Component
35 | ${rgbaColor.asFloat.alpha}
36 | `;
37 | })
38 | .join("\n")}
39 |
40 | `;
41 |
42 | const filePath = `./_generated/${slugifiedName}/iterm2/${themeName}-iterm2.xml`;
43 | writeFile(filePath, theme);
44 | };
45 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/generators/iterm2/mappers.ts:
--------------------------------------------------------------------------------
1 | import { AllColorAbbreviations, AllToneAbbreviations } from "../../types";
2 | import { ITerm2Key } from "../types";
3 |
4 | export const mapITerm2Color: Record<
5 | ITerm2Key,
6 | AllColorAbbreviations | AllToneAbbreviations
7 | > = {
8 | "Ansi 0 Color": "bg-2",
9 | "Ansi 1 Color": "re-2",
10 | "Ansi 2 Color": "gr-2",
11 | "Ansi 3 Color": "ye-2",
12 | "Ansi 4 Color": "bl-2",
13 | "Ansi 5 Color": "ma-2",
14 | "Ansi 6 Color": "cy-2",
15 | "Ansi 7 Color": "tx-3",
16 | "Ansi 8 Color": "ui",
17 | "Ansi 9 Color": "re",
18 | "Ansi 10 Color": "gr",
19 | "Ansi 11 Color": "ye",
20 | "Ansi 12 Color": "bl",
21 | "Ansi 13 Color": "ma",
22 | "Ansi 14 Color": "cy",
23 | "Ansi 15 Color": "tx",
24 | "Background Color": "bg",
25 | "Bold Color": "tx",
26 | "Cursor Color": "tx",
27 | "Cursor Text Color": "bg",
28 | "Foreground Color": "tx",
29 | "Link Color": "bl",
30 | "Selected Text Color": "tx",
31 | "Selection Color": "bg",
32 | };
33 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/generators/kitty/generate.ts:
--------------------------------------------------------------------------------
1 | import { mappedPalette } from "../../mapped-palette.ts";
2 | import { toConf } from "../../utils/format.ts";
3 | import { getThemeName, writeFile } from "../../utils/index.ts";
4 | import { ThemeType } from "../types.ts";
5 |
6 | export const generateKittyTheme = ({
7 | name,
8 | themeType,
9 | }: {
10 | name: string;
11 | themeType: ThemeType;
12 | }) => {
13 | const themeName = getThemeName(name, themeType);
14 | const slugifiedName = getThemeName(name);
15 | const theme = {
16 | "basic colors": {
17 | foreground: mappedPalette.tx[themeType],
18 | background: mappedPalette.bg[themeType],
19 | selection_foreground: mappedPalette.tx[themeType],
20 | selection_background: mappedPalette["ui-3"][themeType],
21 | },
22 | "cursor colors": {
23 | cursor: mappedPalette.tx[themeType],
24 | cursor_text_color: mappedPalette.bg[themeType],
25 | },
26 | "window border colors": {
27 | active_border_color: mappedPalette.re[themeType],
28 | inactive_border_color: mappedPalette["ui-3"][themeType],
29 | },
30 | "tab bar colors": {
31 | active_tab_foreground: mappedPalette.tx[themeType],
32 | active_tab_background: mappedPalette["ui-3"][themeType],
33 | inactive_tab_foreground: mappedPalette["tx-2"][themeType],
34 | inactive_tab_background: mappedPalette["ui"][themeType],
35 | },
36 |
37 | black: {
38 | color0: mappedPalette.bg["dark"],
39 | color8: mappedPalette["tx-2"][themeType],
40 | },
41 | red: {
42 | color1: mappedPalette.re[themeType],
43 | color9: mappedPalette["re-2"][themeType],
44 | },
45 | green: {
46 | color2: mappedPalette.gr[themeType],
47 | color10: mappedPalette["gr-2"][themeType],
48 | },
49 |
50 | yellow: {
51 | color3: mappedPalette.ye[themeType],
52 | color11: mappedPalette["ye-2"][themeType],
53 | },
54 | blue: {
55 | color4: mappedPalette.bl[themeType],
56 | color12: mappedPalette["bl-2"][themeType],
57 | },
58 |
59 | magenta: {
60 | color5: mappedPalette.pu[themeType],
61 | color13: mappedPalette["pu-2"][themeType],
62 | },
63 | cyan: {
64 | color6: mappedPalette.cy[themeType],
65 | color14: mappedPalette["cy-2"][themeType],
66 | },
67 | white: {
68 | color7: mappedPalette.tx[themeType],
69 | color15: mappedPalette.bg["light"],
70 | },
71 | };
72 |
73 | const filePath = `./_generated/${slugifiedName}/kitty/${themeName}.conf`;
74 | writeFile(filePath, toConf(theme));
75 | };
76 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/generators/lite-xl/generate.ts:
--------------------------------------------------------------------------------
1 | import { mappedPalette } from "../../mapped-palette.ts";
2 | import { getThemeName, writeFile } from "../../utils/index.ts";
3 | import { ThemeType } from "../types.ts";
4 |
5 | export const generateLiteXLTheme = ({
6 | name,
7 | themeType,
8 | }: {
9 | name: string;
10 | themeType: ThemeType;
11 | }) => {
12 | const themeName = getThemeName(name, themeType);
13 | const slugifiedName = getThemeName(name);
14 |
15 | const theme = `
16 | local style = require "core.style"
17 | local common = require "core.common"
18 |
19 | style.background = { common.color "${mappedPalette.bg[themeType]}" }
20 | style.background2 = { common.color "${mappedPalette["bg-2"][themeType]}" }
21 | style.background3 = { common.color "${mappedPalette["ui"][themeType]}" }
22 | style.text = { common.color "${mappedPalette.tx[themeType]}" }
23 | style.caret = { common.color "${mappedPalette.tx[themeType]}" }
24 | style.accent = { common.color "${mappedPalette["bl"][themeType]}" }
25 | style.dim = { common.color "${mappedPalette.tx[themeType]}" }
26 | style.divider = { common.color "${mappedPalette["ui-3"][themeType]}" }
27 | style.selection = { common.color "${mappedPalette["ui-2"][themeType]}" }
28 | style.line_number = { common.color "${mappedPalette["tx-2"][themeType]}" }
29 | style.line_number2 = { common.color "${mappedPalette["bg"][themeType]}" }
30 | style.line_highlight = { common.color "${mappedPalette["tx-3"][themeType]}" }
31 | style.scrollbar = { common.color "${mappedPalette["tx"][themeType]}" }
32 | style.scrollbar2 = { common.color "${mappedPalette["tx-2"][themeType]}" }
33 |
34 | style.syntax["normal"] = { common.color "${mappedPalette["ui"][themeType]}" }
35 | style.syntax["symbol"] = { common.color "${mappedPalette["pu"][themeType]}" }
36 | style.syntax["comment"] = { common.color "${mappedPalette["tx-2"][themeType]}" }
37 | style.syntax["keyword"] = { common.color "${mappedPalette["re"][themeType]}" }
38 | style.syntax["keyword2"] = { common.color "${mappedPalette["bl"][themeType]}" }
39 | style.syntax["number"] = { common.color "${mappedPalette["gr"][themeType]}" }
40 | style.syntax["literal"] = { common.color "${mappedPalette["ye"][themeType]}" }
41 | style.syntax["string"] = { common.color "${mappedPalette["or"][themeType]}" }
42 | style.syntax["operator"] = { common.color "${mappedPalette["gr"][themeType]}" }
43 | style.syntax["function"] = { common.color "${mappedPalette["cy"][themeType]}" }
44 | `;
45 | const filePath = `./_generated/${slugifiedName}/lite-xl/${themeName}.lua`;
46 | writeFile(filePath, theme);
47 | };
48 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/generators/theme-sh/generate.ts:
--------------------------------------------------------------------------------
1 | import { mappedPalette } from "../../mapped-palette.ts";
2 | import { getThemeName, writeFile } from "../../utils/index.ts";
3 | import { toThemeSH } from "../../utils/format.ts";
4 | import { ThemeType } from "../types.ts";
5 |
6 | export const generateThemeSHTheme = ({
7 | name,
8 | themeType,
9 | }: {
10 | name: string;
11 | themeType: ThemeType;
12 | }) => {
13 | const themeName = getThemeName(name, themeType);
14 | const slugifiedName = getThemeName(name);
15 |
16 | const theme = {
17 | color0: mappedPalette.bg[themeType],
18 | color1: mappedPalette.re[themeType],
19 | color2: mappedPalette.gr[themeType],
20 | color3: mappedPalette.ye[themeType],
21 | color4: mappedPalette.bl[themeType],
22 | color5: mappedPalette.pu[themeType],
23 | color6: mappedPalette.cy[themeType],
24 | color7: mappedPalette["tx-2"][themeType],
25 | color8: mappedPalette["ui"][themeType],
26 | color9: mappedPalette["re-2"][themeType],
27 | color10: mappedPalette["gr-2"][themeType],
28 | color11: mappedPalette["ye-2"][themeType],
29 | color12: mappedPalette["bl-2"][themeType],
30 | color13: mappedPalette["pu-2"][themeType],
31 | color14: mappedPalette["cy-2"][themeType],
32 | color15: mappedPalette["tx-2"][themeType],
33 | foreground: mappedPalette.tx[themeType],
34 | background: mappedPalette.bg[themeType],
35 | cursor: mappedPalette.tx[themeType],
36 | };
37 |
38 | const filePath = `./_generated/${slugifiedName}/theme-sh/${themeName}`;
39 | writeFile(filePath, toThemeSH(theme));
40 | };
41 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/generators/types.ts:
--------------------------------------------------------------------------------
1 | import { Color } from "../utils/color";
2 |
3 | export type ThemeType = "light" | "dark";
4 |
5 | export type VSCodeTokenColor = {
6 | name: string;
7 | scope: string | string[];
8 | settings: {
9 | foreground: string;
10 | fontStyle?: string;
11 | };
12 | };
13 |
14 | export type VSCodeTheme = {
15 | name: string;
16 | type: string;
17 | colors: {
18 | [key: string]: string;
19 | };
20 | tokenColors: VSCodeTokenColor[];
21 | };
22 |
23 | export type ITerm2Key =
24 | | "Ansi 0 Color"
25 | | "Ansi 1 Color"
26 | | "Ansi 2 Color"
27 | | "Ansi 3 Color"
28 | | "Ansi 4 Color"
29 | | "Ansi 5 Color"
30 | | "Ansi 6 Color"
31 | | "Ansi 7 Color"
32 | | "Ansi 8 Color"
33 | | "Ansi 9 Color"
34 | | "Ansi 10 Color"
35 | | "Ansi 11 Color"
36 | | "Ansi 12 Color"
37 | | "Ansi 13 Color"
38 | | "Ansi 14 Color"
39 | | "Ansi 15 Color"
40 | | "Background Color"
41 | | "Bold Color"
42 | | "Cursor Color"
43 | | "Cursor Text Color"
44 | | "Foreground Color"
45 | | "Link Color"
46 | | "Selected Text Color"
47 | | "Selection Color";
48 |
49 | export type iTerm2Theme = Record;
50 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/generators/vanilla-css/generate.ts:
--------------------------------------------------------------------------------
1 | import { ThemeType } from "../types.ts";
2 | import { entries, getThemeName, writeFile } from "../../utils/index.ts";
3 | import { mappedPalette } from "../../mapped-palette.ts";
4 | import { toCSS } from "../../utils/format.ts";
5 | import { formatAbbreviationToSemantic } from "./mappers.ts";
6 |
7 | export const generateVanillaCSSTheme = ({
8 | name,
9 | }: {
10 | name: string;
11 | themeType: ThemeType;
12 | }) => {
13 | const themeName = getThemeName(name);
14 | const lowercaseName = name.toLocaleLowerCase();
15 | const theme: Record = {
16 | ":root": {},
17 | ".dark": {},
18 | };
19 |
20 | for (const [key, color] of entries(mappedPalette)) {
21 | const semanticKey = formatAbbreviationToSemantic(key);
22 | theme[":root"][`--${lowercaseName}-${semanticKey}`] = color["light"];
23 | theme[".dark"][`--${lowercaseName}-${semanticKey}`] = color["dark"];
24 | }
25 |
26 | const filePath = `./_generated/${themeName}/vanilla-css/${themeName}-vanilla.css`;
27 | writeFile(filePath, toCSS(theme));
28 | };
29 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/generators/vanilla-css/mappers.ts:
--------------------------------------------------------------------------------
1 | import { AllColorAbbreviations, AllToneAbbreviations } from "../../types.ts";
2 |
3 | const baseToneMappings = {
4 | tx: "text",
5 | "tx-2": "text-muted",
6 | "tx-3": "text-faint",
7 | ui: "ui",
8 | "ui-2": "ui-hover",
9 | "ui-3": "ui-active",
10 | bg: "bg",
11 | "bg-2": "bg-secondary",
12 | } as const;
13 |
14 | const invertedColorAbbreviations = {
15 | re: "red",
16 | or: "orange",
17 | ye: "yellow",
18 | gr: "green",
19 | cy: "cyan",
20 | bl: "blue",
21 | pu: "purple",
22 | ma: "magenta",
23 | } as const;
24 |
25 | export const formatAbbreviationToSemantic = (
26 | abbreviation: AllColorAbbreviations | AllToneAbbreviations
27 | ) => {
28 | if (abbreviation in baseToneMappings)
29 | return baseToneMappings[abbreviation as keyof typeof baseToneMappings];
30 |
31 | if (abbreviation.endsWith("-2")) {
32 | const color = abbreviation.slice(0, abbreviation.length - 2);
33 | const semantic =
34 | invertedColorAbbreviations[
35 | color as keyof typeof invertedColorAbbreviations
36 | ];
37 | return `${semantic}-secondary`;
38 | }
39 |
40 | return invertedColorAbbreviations[
41 | abbreviation as keyof typeof invertedColorAbbreviations
42 | ];
43 | };
44 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/generators/warp/generate.ts:
--------------------------------------------------------------------------------
1 | import { mappedPalette } from "../../mapped-palette.ts";
2 | import { toYAML } from "../../utils/format.ts";
3 | import { getThemeName, writeFile } from "../../utils/index.ts";
4 | import { ThemeType } from "../types.ts";
5 |
6 | export const generateWarpTheme = ({
7 | name,
8 | themeType,
9 | }: {
10 | name: string;
11 | themeType: ThemeType;
12 | }) => {
13 | const themeName = getThemeName(name, themeType);
14 | const slugifiedName = getThemeName(name);
15 |
16 | const theme = {
17 | accent: mappedPalette.or[themeType],
18 | background: mappedPalette.bg[themeType],
19 | details: themeType === "light" ? "lighter" : "darker",
20 | foreground: mappedPalette.tx[themeType],
21 | terminal_colors: {
22 | bright: {
23 | black: mappedPalette.bg["dark"],
24 | blue: mappedPalette.bl[themeType],
25 | cyan: mappedPalette.cy[themeType],
26 | green: mappedPalette.gr[themeType],
27 | magenta: mappedPalette.ma[themeType],
28 | red: mappedPalette.re[themeType],
29 | white: mappedPalette.tx[themeType],
30 | yellow: mappedPalette.ye[themeType],
31 | },
32 | normal: {
33 | black: mappedPalette.bg["dark"],
34 | blue: mappedPalette["bl-2"][themeType],
35 | cyan: mappedPalette["cy-2"][themeType],
36 | green: mappedPalette["gr-2"][themeType],
37 | magenta: mappedPalette["ma-2"][themeType],
38 | red: mappedPalette["re-2"][themeType],
39 | white: mappedPalette.tx[themeType],
40 | yellow: mappedPalette["ye-2"][themeType],
41 | },
42 | },
43 | };
44 |
45 | const filePath = `./_generated/${slugifiedName}/warp/${themeName}.yaml`;
46 | writeFile(filePath, toYAML(theme));
47 | };
48 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/generators/windows-terminal/generate.ts:
--------------------------------------------------------------------------------
1 | import { mappedPalette } from "../../mapped-palette.ts";
2 | import { toJSON } from "../../utils/format.ts";
3 | import { getThemeName, writeFile } from "../../utils/index.ts";
4 | import { ThemeType } from "../types.ts";
5 |
6 | export const generateWindowsTerminalTheme = ({
7 | name,
8 | themeType,
9 | }: {
10 | name: string;
11 | themeType: ThemeType;
12 | }) => {
13 | const themeName = getThemeName(name, themeType);
14 | const slugifiedName = getThemeName(name);
15 |
16 | const theme = {
17 | // General
18 | name: themeName,
19 | background: mappedPalette.bg[themeType],
20 | foreground: mappedPalette.tx[themeType],
21 |
22 | // Black & White
23 | black: mappedPalette.bg["dark"],
24 | white: mappedPalette["bg-2"]["light"],
25 |
26 | // Bright colors
27 | brightBlack: mappedPalette["tx-3"]["dark"],
28 | brightWhite: mappedPalette.bg["light"],
29 | brightBlue: mappedPalette.bl[themeType],
30 | brightCyan: mappedPalette.cy[themeType],
31 | brightGreen: mappedPalette.gr[themeType],
32 | brightPurple: mappedPalette.pu[themeType],
33 | brightRed: mappedPalette.re[themeType],
34 | brightYellow: mappedPalette.ye[themeType],
35 |
36 | // Normal colors
37 | blue: mappedPalette["bl-2"][themeType],
38 | cyan: mappedPalette["bl-2"][themeType],
39 | green: mappedPalette["gr-2"][themeType],
40 | purple: mappedPalette["pu-2"][themeType],
41 | red: mappedPalette["re-2"][themeType],
42 | yellow: mappedPalette["ye-2"][themeType],
43 |
44 | // Selection colors
45 | selectionBackground: mappedPalette["tx-2"][themeType],
46 |
47 | // Cursor colors
48 | cursorColor: mappedPalette["tx-2"][themeType],
49 | };
50 |
51 | const filePath = `./_generated/${slugifiedName}/windows-terminal/${themeName}-wt.json`;
52 | writeFile(filePath, toJSON(theme));
53 | };
54 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/index.ts:
--------------------------------------------------------------------------------
1 | import { exit } from "process";
2 | import { getThemeName } from "./utils/index.ts";
3 | import { generators, providers } from "./generators/index.ts";
4 | import { currentTheme } from "./config/index.ts";
5 |
6 | function main() {
7 | try {
8 | for (const { name: providerName, themes } of providers) {
9 | // Here we extract the generator function
10 | const generator = generators[providerName];
11 | if (!generator) {
12 | throw new Error(`Unknown provider: ${providerName}`);
13 | }
14 |
15 | for (const themeType of themes) {
16 | // We obtain the slugified name + theme type
17 | const themeName = getThemeName(currentTheme, themeType);
18 |
19 | console.debug(`[${providerName.toUpperCase()}]`);
20 | console.log(`Generating ${themeName} theme...`);
21 |
22 | // We call the generator function
23 | generator({ name: currentTheme, themeType });
24 | }
25 |
26 | console.log(
27 | `\x1b[32mSuccessfully generated themes for ${providerName}!\x1b[0m`
28 | );
29 | }
30 | } catch (error) {
31 | console.error(`\x1b[31mAn error occurred while generating themes:\x1b[0m`);
32 | exit(1);
33 | }
34 | }
35 |
36 | void main();
37 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/mapped-palette.ts:
--------------------------------------------------------------------------------
1 | import { currentPalette } from "./config/index.ts";
2 | import { ColorMap, Shade } from "./types.ts";
3 | import { entries } from "./utils/index.ts";
4 |
5 | const colorAbbreviations = {
6 | red: "re",
7 | orange: "or",
8 | yellow: "ye",
9 | green: "gr",
10 | cyan: "cy",
11 | blue: "bl",
12 | purple: "pu",
13 | magenta: "ma",
14 | } as const;
15 |
16 | const textTones = {
17 | tx: {
18 | light: currentPalette.base[900],
19 | dark: currentPalette.base[50],
20 | },
21 | "tx-2": {
22 | light: currentPalette.base[500],
23 | dark: currentPalette.base[200],
24 | },
25 | "tx-3": {
26 | light: currentPalette.base[300],
27 | dark: currentPalette.base[300],
28 | },
29 | };
30 |
31 | const interfaceTones = {
32 | ui: {
33 | light: currentPalette.base[50],
34 | dark: currentPalette.base[900],
35 | },
36 | "ui-2": {
37 | light: currentPalette.base[100],
38 | dark: currentPalette.base[850],
39 | },
40 | "ui-3": {
41 | light: currentPalette.base[150],
42 | dark: currentPalette.base[800],
43 | },
44 | };
45 |
46 | const backgroundTones = {
47 | bg: {
48 | light: currentPalette.base.paper,
49 | dark: currentPalette.base.black,
50 | },
51 | "bg-2": {
52 | light: currentPalette.base[50],
53 | dark: currentPalette.base[950],
54 | },
55 | };
56 |
57 | const generateColorTones = ({
58 | lightContrastShade = 500,
59 | darkContrastShade = 300,
60 | }: {
61 | lightContrastShade: Shade;
62 | darkContrastShade: Shade;
63 | }): ColorMap => {
64 | const colorMap: ColorMap = {} as ColorMap;
65 |
66 | for (const [colorName, abbreviation] of entries(colorAbbreviations)) {
67 | colorMap[abbreviation] = {
68 | light: currentPalette[colorName][lightContrastShade],
69 | dark: currentPalette[colorName][darkContrastShade],
70 | };
71 | colorMap[`${abbreviation}-2`] = {
72 | light: currentPalette[colorName][darkContrastShade],
73 | dark: currentPalette[colorName][lightContrastShade],
74 | };
75 | }
76 |
77 | return colorMap;
78 | };
79 |
80 | export const mappedPalette = {
81 | ...textTones,
82 | ...interfaceTones,
83 | ...backgroundTones,
84 | // ...generateColorTones({ lightContrastShade: 700, darkContrastShade: 400 }),
85 | ...generateColorTones({ lightContrastShade: 500, darkContrastShade: 300 }),
86 | };
87 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/palettes/flexoki.ts:
--------------------------------------------------------------------------------
1 | import { Palette } from "./types.ts";
2 |
3 | export const FlexokiPalette: Palette = {
4 | base: {
5 | black: "#100F0F",
6 | 950: "#1C1B1A",
7 | 900: "#282726",
8 | 850: "#343331",
9 | 800: "#403E3C",
10 | 700: "#575653",
11 | 600: "#6F6E69",
12 | 500: "#878580",
13 | 300: "#B7B5AC",
14 | 200: "#CECDC3",
15 | 150: "#DAD8CE",
16 | 100: "#E6E4D9",
17 | 50: "#F2F0E5",
18 | paper: "#FFFCF0",
19 | },
20 | red: {
21 | 950: "",
22 | 900: "",
23 | 800: "",
24 | 700: "",
25 | 600: "",
26 | 500: "#AF3029",
27 | 400: "",
28 | 300: "#D14D41",
29 | 200: "",
30 | 100: "",
31 | 50: "",
32 | },
33 | orange: {
34 | 950: "",
35 | 900: "",
36 | 800: "",
37 | 700: "",
38 | 600: "",
39 | 500: "#BC5215",
40 | 400: "",
41 | 300: "#DA702C",
42 | 200: "",
43 | 100: "",
44 | 50: "",
45 | },
46 | yellow: {
47 | 950: "",
48 | 900: "",
49 | 800: "",
50 | 700: "",
51 | 600: "",
52 | 500: "#AD8301",
53 | 400: "",
54 | 300: "#D0A215",
55 | 200: "",
56 | 100: "",
57 | 50: "",
58 | },
59 | green: {
60 | 950: "",
61 | 900: "",
62 | 800: "",
63 | 700: "",
64 | 600: "",
65 | 500: "#66800B",
66 | 400: "",
67 | 300: "#879A39",
68 | 200: "",
69 | 100: "",
70 | 50: "",
71 | },
72 | cyan: {
73 | 950: "",
74 | 900: "",
75 | 800: "",
76 | 700: "",
77 | 600: "",
78 | 500: "#24837B",
79 | 400: "",
80 | 300: "#3AA99F",
81 | 200: "",
82 | 100: "",
83 | 50: "",
84 | },
85 | blue: {
86 | 950: "",
87 | 900: "",
88 | 800: "",
89 | 700: "",
90 | 600: "",
91 | 500: "#205EA6",
92 | 400: "",
93 | 300: "#4385BE",
94 | 200: "",
95 | 100: "",
96 | 50: "",
97 | },
98 | purple: {
99 | 950: "",
100 | 900: "",
101 | 800: "",
102 | 700: "",
103 | 600: "",
104 | 500: "#5E409D",
105 | 400: "",
106 | 300: "#8B7EC8",
107 | 200: "",
108 | 100: "",
109 | 50: "",
110 | },
111 | magenta: {
112 | 950: "",
113 | 900: "",
114 | 800: "",
115 | 700: "",
116 | 600: "",
117 | 500: "#A02F6F",
118 | 400: "",
119 | 300: "#CE5D97",
120 | 200: "",
121 | 100: "",
122 | 50: "",
123 | },
124 | };
125 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/palettes/types.ts:
--------------------------------------------------------------------------------
1 | type BaseColors = {
2 | black: string;
3 | 50: string;
4 | 100: string;
5 | 150: string;
6 | 200: string;
7 | 300: string;
8 | 500: string;
9 | 600: string;
10 | 700: string;
11 | 800: string;
12 | 850: string;
13 | 900: string;
14 | 950: string;
15 | paper: string;
16 | };
17 |
18 | type ColorMap = {
19 | 950: string;
20 | 900: string;
21 | 800: string;
22 | 700: string;
23 | 600: string;
24 | 500: string;
25 | 400: string;
26 | 300: string;
27 | 200: string;
28 | 100: string;
29 | 50: string;
30 | };
31 |
32 | export type Palette = {
33 | base: BaseColors;
34 | red: ColorMap;
35 | orange: ColorMap;
36 | yellow: ColorMap;
37 | green: ColorMap;
38 | cyan: ColorMap;
39 | blue: ColorMap;
40 | purple: ColorMap;
41 | magenta: ColorMap;
42 | };
43 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/palettes/vercel-2024.ts:
--------------------------------------------------------------------------------
1 | import { Palette } from "./types.ts";
2 |
3 | export const Vercel2024Palette: Palette = {
4 | base: {
5 | black: "#000000",
6 | 950: "#0D0D0D",
7 | 900: "#171717",
8 | 850: "#212121",
9 | 800: "#2B2B2B",
10 | 700: "#3F3F3F",
11 | 600: "#525252",
12 | 500: "#666666",
13 | 300: "#8F8F8F",
14 | 200: "#A3A3A3",
15 | 150: "#BDBDBD",
16 | 100: "#D8D8D8",
17 | 50: "#EDEDED",
18 | paper: "#FFFFFF",
19 | },
20 | red: {
21 | 950: "",
22 | 900: "",
23 | 800: "",
24 | 700: "",
25 | 600: "",
26 | 500: "#AF3029",
27 | 400: "",
28 | 300: "#D14D41",
29 | 200: "",
30 | 100: "",
31 | 50: "",
32 | },
33 | orange: {
34 | 950: "",
35 | 900: "",
36 | 800: "",
37 | 700: "",
38 | 600: "",
39 | 500: "#C55D17",
40 | 400: "",
41 | 300: "#F27F35",
42 | 200: "",
43 | 100: "",
44 | 50: "",
45 | },
46 | yellow: {
47 | 950: "",
48 | 900: "",
49 | 800: "",
50 | 700: "",
51 | 600: "",
52 | 500: "#B38F00",
53 | 400: "",
54 | 300: "#E5B800",
55 | 200: "",
56 | 100: "",
57 | 50: "",
58 | },
59 | green: {
60 | 950: "",
61 | 900: "",
62 | 800: "",
63 | 700: "",
64 | 600: "",
65 | 500: "#0F7E32",
66 | 400: "",
67 | 300: "#00CA51",
68 | 200: "",
69 | 100: "",
70 | 50: "",
71 | },
72 | cyan: {
73 | 950: "",
74 | 900: "",
75 | 800: "",
76 | 700: "",
77 | 600: "",
78 | 500: "#1B9E97",
79 | 400: "",
80 | 300: "#4CC0BA",
81 | 200: "",
82 | 100: "",
83 | 50: "",
84 | },
85 | blue: {
86 | 950: "",
87 | 900: "",
88 | 800: "",
89 | 700: "",
90 | 600: "",
91 | 500: "#0060F1",
92 | 400: "",
93 | 300: "#47A8FF",
94 | 200: "",
95 | 100: "",
96 | 50: "",
97 | },
98 | purple: {
99 | 950: "",
100 | 900: "",
101 | 800: "",
102 | 700: "",
103 | 600: "",
104 | 500: "#7D00CC",
105 | 400: "",
106 | 300: "#C372FC",
107 | 200: "",
108 | 100: "",
109 | 50: "",
110 | },
111 | magenta: {
112 | 950: "",
113 | 900: "",
114 | 800: "",
115 | 700: "",
116 | 600: "",
117 | 500: "#C31562",
118 | 400: "",
119 | 300: "#FF4C8D",
120 | 200: "",
121 | 100: "",
122 | 50: "",
123 | },
124 | };
125 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/types.ts:
--------------------------------------------------------------------------------
1 | export type MyTheme =
2 | | "Flexoki"
3 | | "One Hunter Material"
4 | | "One Hunter Flexoki"
5 | | "Vercel 2024"
6 | | "Vercel 2024 Flexoki";
7 |
8 | export type Shade = (
9 | | 50
10 | | 100
11 | | 200
12 | | 300
13 | | 400
14 | | 500
15 | | 600
16 | | 700
17 | | 800
18 | | 900
19 | | 950
20 | ) &
21 | (number | string);
22 |
23 | export type ColorName =
24 | | "red"
25 | | "orange"
26 | | "yellow"
27 | | "green"
28 | | "cyan"
29 | | "blue"
30 | | "purple"
31 | | "magenta";
32 |
33 | export type ColorAbbreviation =
34 | | "re"
35 | | "or"
36 | | "ye"
37 | | "gr"
38 | | "cy"
39 | | "bl"
40 | | "pu"
41 | | "ma";
42 |
43 | export type AllColorAbbreviations =
44 | | ColorAbbreviation
45 | | `${ColorAbbreviation}-2`;
46 |
47 | export type BaseToneAbbreviation = "tx" | "ui" | "bg";
48 |
49 | export type AllToneAbbreviations =
50 | | "tx"
51 | | "tx-2"
52 | | "tx-3"
53 | | "ui"
54 | | "ui-2"
55 | | "ui-3"
56 | | "bg"
57 | | "bg-2";
58 |
59 | export type Abbreviations = AllColorAbbreviations | AllToneAbbreviations;
60 |
61 | export type ColorEntry = {
62 | light: string;
63 | dark: string;
64 | };
65 |
66 | export type ColorMap = {
67 | [key in AllColorAbbreviations]: ColorEntry;
68 | };
69 |
70 | export type SemanticToken =
71 | | "plain"
72 | | "classes"
73 | | "interfaces"
74 | | "structs"
75 | | "enums"
76 | | "keys"
77 | | "methods"
78 | | "functions"
79 | | "variables"
80 | | "variablesOther"
81 | | "globalVariables"
82 | | "localVariables"
83 | | "parameters"
84 | | "properties"
85 | | "strings"
86 | | "stringEscapeSequences"
87 | | "keywords"
88 | | "keywordsControl"
89 | | "storageModifiers"
90 | | "comments"
91 | | "docComments"
92 | | "numbers"
93 | | "booleans"
94 | | "operators"
95 | | "macros"
96 | | "preprocessor"
97 | | "urls"
98 | | "tags"
99 | | "jsxTags"
100 | | "attributes"
101 | | "types"
102 | | "constants"
103 | | "labels"
104 | | "namespaces"
105 | | "modules"
106 | | "typeParameters"
107 | | "exceptions"
108 | | "decorators"
109 | | "calls"
110 | | "punctuation"
111 | | "yellow"
112 | | "green"
113 | | "cyan"
114 | | "blue"
115 | | "purple"
116 | | "magenta"
117 | | "red"
118 | | "orange";
119 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/utils/color.ts:
--------------------------------------------------------------------------------
1 | export class Color {
2 | private constructor(
3 | private readonly red = 255,
4 | private readonly green = 255,
5 | private readonly blue = 255,
6 | private readonly alpha = 255
7 | ) {}
8 |
9 | static fromHex(color: string): Color {
10 | const sanitized = color.replace("#", "");
11 |
12 | if (sanitized.length !== 6 && sanitized.length !== 8) {
13 | throw new Error("Bad color format, should be #001122 or #00112233");
14 | }
15 |
16 | const colorComponents: number[] = [];
17 | for (let i = 1; i < sanitized.length; i += 2) {
18 | const firstByte = sanitized[i - 1];
19 | const secondByte = sanitized[i];
20 |
21 | if (firstByte && secondByte) {
22 | colorComponents.push(parseInt(firstByte + secondByte, 16));
23 | }
24 | }
25 | return new Color(
26 | colorComponents.shift(),
27 | colorComponents.shift(),
28 | colorComponents.shift(),
29 | colorComponents.shift()
30 | );
31 | }
32 |
33 | get asHexRGBA(): string {
34 | return [
35 | "#",
36 | this.red.toString(16).padStart(2, "0"),
37 | this.green.toString(16).padStart(2, "0"),
38 | this.blue.toString(16).padStart(2, "0"),
39 | this.alpha.toString(16).padStart(2, "0"),
40 | ].join("");
41 | }
42 |
43 | get asHexRGB(): string {
44 | return [
45 | "#",
46 | this.red.toString(16).padStart(2, "0"),
47 | this.green.toString(16).padStart(2, "0"),
48 | this.blue.toString(16).padStart(2, "0"),
49 | ].join("");
50 | }
51 |
52 | get asIntRGB(): string {
53 | return (
54 | "0x" +
55 | [
56 | this.red.toString(16).padStart(2, "0"),
57 | this.green.toString(16).padStart(2, "0"),
58 | this.blue.toString(16).padStart(2, "0"),
59 | ].join("")
60 | );
61 | }
62 |
63 | get asRGB(): string {
64 | return `rgb(${this.red}, ${this.green}, ${this.blue})`;
65 | }
66 |
67 | get asFloat(): { red: number; green: number; blue: number; alpha: number } {
68 | return {
69 | red: this.red / 255,
70 | green: this.green / 255,
71 | blue: this.blue / 255,
72 | alpha: this.alpha / 255,
73 | };
74 | }
75 |
76 | get asInt(): { red: number; green: number; blue: number; alpha: number } {
77 | return {
78 | red: this.red,
79 | green: this.green,
80 | blue: this.blue,
81 | alpha: this.alpha,
82 | };
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/packages/tinte-core/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import path from "node:path";
3 |
4 | export const entries = (obj: O) =>
5 | Object.entries(obj) as { [K in keyof O]: [K, O[K]] }[keyof O][];
6 |
7 | export const writeFile = (filePath: string, data: string) => {
8 | const pathArray = filePath.split("/");
9 | pathArray.pop();
10 | const dirPath = pathArray.join("/");
11 | if (!fs.existsSync(dirPath)) {
12 | fs.mkdirSync(dirPath, { recursive: true });
13 | }
14 | fs.writeFileSync(path.join(process.cwd(), filePath), data);
15 | };
16 |
17 | export const getThemeName = (...args: string[]) => {
18 | const themeName = args.join("-").toLowerCase();
19 | return themeName.replace(/\s/g, "-");
20 | };
21 |
--------------------------------------------------------------------------------
/packages/tinte-core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/base.json",
3 | "compilerOptions": {
4 | "outDir": "dist"
5 | },
6 | "include": ["src"],
7 | "exclude": ["node_modules", "dist"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/typescript-config/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "declaration": true,
6 | "declarationMap": true,
7 | "esModuleInterop": true,
8 | "incremental": false,
9 | "isolatedModules": true,
10 | "lib": ["es2022", "DOM", "DOM.Iterable"],
11 | "module": "NodeNext",
12 | "moduleDetection": "force",
13 | "moduleResolution": "NodeNext",
14 | "noUncheckedIndexedAccess": true,
15 | "resolveJsonModule": true,
16 | "skipLibCheck": true,
17 | "strict": true,
18 | "target": "ES2022"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/typescript-config/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Next.js",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "plugins": [{ "name": "next" }],
7 | "module": "ESNext",
8 | "moduleResolution": "Bundler",
9 | "allowJs": true,
10 | "jsx": "preserve",
11 | "noEmit": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/typescript-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/typescript-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "publishConfig": {
7 | "access": "public"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/typescript-config/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "React Library",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "jsx": "react-jsx"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/ui/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | module.exports = {
3 | root: true,
4 | extends: ["@repo/eslint-config/react-internal.js"],
5 | parser: "@typescript-eslint/parser",
6 | parserOptions: {
7 | project: "./tsconfig.lint.json",
8 | tsconfigRootDir: __dirname,
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/packages/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/ui",
3 | "version": "0.0.0",
4 | "private": true,
5 | "exports": {
6 | "./button": "./src/button.tsx",
7 | "./card": "./src/card.tsx",
8 | "./code": "./src/code.tsx"
9 | },
10 | "scripts": {
11 | "lint": "eslint . --max-warnings 0",
12 | "generate:component": "turbo gen react-component"
13 | },
14 | "devDependencies": {
15 | "@repo/eslint-config": "workspace:*",
16 | "@repo/typescript-config": "workspace:*",
17 | "@turbo/gen": "^1.12.4",
18 | "@types/node": "^20.11.24",
19 | "@types/eslint": "^8.56.5",
20 | "@types/react": "^18.2.61",
21 | "@types/react-dom": "^18.2.19",
22 | "eslint": "^8.57.0",
23 | "react": "^18.2.0",
24 | "typescript": "^5.3.3"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/ui/src/button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ReactNode } from "react";
4 |
5 | interface ButtonProps {
6 | children: ReactNode;
7 | className?: string;
8 | appName: string;
9 | }
10 |
11 | export const Button = ({ children, className, appName }: ButtonProps) => {
12 | return (
13 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/packages/ui/src/card.tsx:
--------------------------------------------------------------------------------
1 | export function Card({
2 | className,
3 | title,
4 | children,
5 | href,
6 | }: {
7 | className?: string;
8 | title: string;
9 | children: React.ReactNode;
10 | href: string;
11 | }): JSX.Element {
12 | return (
13 |
19 |
20 | {title} ->
21 |
22 | {children}
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/packages/ui/src/code.tsx:
--------------------------------------------------------------------------------
1 | export function Code({
2 | children,
3 | className,
4 | }: {
5 | children: React.ReactNode;
6 | className?: string;
7 | }): JSX.Element {
8 | return {children}
;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/react-library.json",
3 | "compilerOptions": {
4 | "outDir": "dist"
5 | },
6 | "include": ["src"],
7 | "exclude": ["node_modules", "dist"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/ui/tsconfig.lint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/react-library.json",
3 | "compilerOptions": {
4 | "outDir": "dist"
5 | },
6 | "include": ["src", "turbo"],
7 | "exclude": ["node_modules", "dist"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/ui/turbo/generators/config.ts:
--------------------------------------------------------------------------------
1 | import type { PlopTypes } from "@turbo/gen";
2 |
3 | // Learn more about Turborepo Generators at https://turbo.build/repo/docs/core-concepts/monorepos/code-generation
4 |
5 | export default function generator(plop: PlopTypes.NodePlopAPI): void {
6 | // A simple generator to add a new React component to the internal UI library
7 | plop.setGenerator("react-component", {
8 | description: "Adds a new react component",
9 | prompts: [
10 | {
11 | type: "input",
12 | name: "name",
13 | message: "What is the name of the component?",
14 | },
15 | ],
16 | actions: [
17 | {
18 | type: "add",
19 | path: "src/{{kebabCase name}}.tsx",
20 | templateFile: "templates/component.hbs",
21 | },
22 | {
23 | type: "append",
24 | path: "package.json",
25 | pattern: /"exports": {(?)/g,
26 | template: '"./{{kebabCase name}}": "./src/{{kebabCase name}}.tsx",',
27 | },
28 | ],
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/packages/ui/turbo/generators/templates/component.hbs:
--------------------------------------------------------------------------------
1 | export const {{ pascalCase name }} = ({ children }: { children: React.ReactNode }) => {
2 | return (
3 |
4 |
{{ pascalCase name }} Component
5 | {children}
6 |
7 | );
8 | };
9 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "apps/*"
3 | - "packages/*"
4 |
--------------------------------------------------------------------------------
/tinte-preview-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/tinte-preview-2.png
--------------------------------------------------------------------------------
/tinte-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/tinte-preview.png
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/base.json"
3 | }
4 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "globalDependencies": [
4 | "**/.env",
5 | "**/.env.*local",
6 | "NEXT_PUBLIC_*",
7 | "NODE_ENV"
8 | ],
9 | "pipeline": {
10 | "build": {
11 | "dependsOn": ["^build"],
12 | "outputs": [".next/**", "!.next/cache/**"]
13 | },
14 | "lint": {
15 | "dependsOn": ["^lint"]
16 | },
17 | "dev": {
18 | "cache": false,
19 | "persistent": true
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------