├── .gitignore
├── .npmrc
├── .vscode
└── settings.json
├── README.md
├── apps
├── docs
│ ├── .gitignore
│ ├── README.md
│ ├── app
│ │ ├── favicon.ico
│ │ ├── fonts
│ │ │ ├── GeistMonoVF.woff
│ │ │ └── GeistVF.woff
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── page.module.css
│ │ └── page.tsx
│ ├── eslint.config.js
│ ├── next.config.js
│ ├── package.json
│ ├── public
│ │ ├── file-text.svg
│ │ ├── globe.svg
│ │ ├── next.svg
│ │ ├── turborepo-dark.svg
│ │ ├── turborepo-light.svg
│ │ ├── vercel.svg
│ │ └── window.svg
│ └── tsconfig.json
└── web
│ ├── .gitignore
│ ├── README.md
│ ├── app
│ ├── favicon.ico
│ ├── fonts
│ │ ├── GeistMonoVF.woff
│ │ └── GeistVF.woff
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.module.css
│ └── page.tsx
│ ├── eslint.config.js
│ ├── next.config.js
│ ├── package.json
│ ├── public
│ ├── file-text.svg
│ ├── globe.svg
│ ├── next.svg
│ ├── turborepo-dark.svg
│ ├── turborepo-light.svg
│ ├── vercel.svg
│ └── window.svg
│ └── tsconfig.json
├── package.json
├── packages
├── eslint-config
│ ├── README.md
│ ├── base.js
│ ├── next.js
│ ├── package.json
│ └── react-internal.js
├── typescript-config
│ ├── base.json
│ ├── nextjs.json
│ ├── package.json
│ └── react-library.json
└── ui
│ ├── eslint.config.mjs
│ ├── package.json
│ ├── src
│ ├── button.tsx
│ ├── card.tsx
│ └── code.tsx
│ ├── tsconfig.json
│ └── turbo
│ └── generators
│ ├── config.ts
│ └── templates
│ └── component.hbs
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json
/.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 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emilwidlund/tactile/8e3a490ab1d3b02a813c078a12c52015be8286d9/.npmrc
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.workingDirectories": [
3 | {
4 | "mode": "auto"
5 | }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Turborepo starter
2 |
3 | This Turborepo starter is maintained by the Turborepo core team.
4 |
5 | ## Using this example
6 |
7 | Run the following command:
8 |
9 | ```sh
10 | npx create-turbo@latest
11 | ```
12 |
13 | ## What's inside?
14 |
15 | This Turborepo includes the following packages/apps:
16 |
17 | ### Apps and Packages
18 |
19 | - `docs`: a [Next.js](https://nextjs.org/) app
20 | - `web`: another [Next.js](https://nextjs.org/) app
21 | - `@repo/ui`: a stub React component library shared by both `web` and `docs` applications
22 | - `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
23 | - `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo
24 |
25 | Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
26 |
27 | ### Utilities
28 |
29 | This Turborepo has some additional tools already setup for you:
30 |
31 | - [TypeScript](https://www.typescriptlang.org/) for static type checking
32 | - [ESLint](https://eslint.org/) for code linting
33 | - [Prettier](https://prettier.io) for code formatting
34 |
35 | ### Build
36 |
37 | To build all apps and packages, run the following command:
38 |
39 | ```
40 | cd my-turborepo
41 | pnpm build
42 | ```
43 |
44 | ### Develop
45 |
46 | To develop all apps and packages, run the following command:
47 |
48 | ```
49 | cd my-turborepo
50 | pnpm dev
51 | ```
52 |
53 | ### Remote Caching
54 |
55 | > [!TIP]
56 | > Vercel Remote Cache is free for all plans. Get started today at [vercel.com](https://vercel.com/signup?/signup?utm_source=remote-cache-sdk&utm_campaign=free_remote_cache).
57 |
58 | Turborepo can use a technique known as [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines.
59 |
60 | By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup?utm_source=turborepo-examples), then enter the following commands:
61 |
62 | ```
63 | cd my-turborepo
64 | npx turbo login
65 | ```
66 |
67 | This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview).
68 |
69 | Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo:
70 |
71 | ```
72 | npx turbo link
73 | ```
74 |
75 | ## Useful Links
76 |
77 | Learn more about the power of Turborepo:
78 |
79 | - [Tasks](https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks)
80 | - [Caching](https://turbo.build/repo/docs/core-concepts/caching)
81 | - [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching)
82 | - [Filtering](https://turbo.build/repo/docs/core-concepts/monorepos/filtering)
83 | - [Configuration Options](https://turbo.build/repo/docs/reference/configuration)
84 | - [CLI Usage](https://turbo.build/repo/docs/reference/command-line-reference)
85 |
--------------------------------------------------------------------------------
/apps/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # env files (can opt-in for commiting if needed)
29 | .env*
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/apps/docs/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
37 |
--------------------------------------------------------------------------------
/apps/docs/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emilwidlund/tactile/8e3a490ab1d3b02a813c078a12c52015be8286d9/apps/docs/app/favicon.ico
--------------------------------------------------------------------------------
/apps/docs/app/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emilwidlund/tactile/8e3a490ab1d3b02a813c078a12c52015be8286d9/apps/docs/app/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/apps/docs/app/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emilwidlund/tactile/8e3a490ab1d3b02a813c078a12c52015be8286d9/apps/docs/app/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/apps/docs/app/globals.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --background: #ffffff;
3 | --foreground: #171717;
4 | }
5 |
6 | @media (prefers-color-scheme: dark) {
7 | :root {
8 | --background: #0a0a0a;
9 | --foreground: #ededed;
10 | }
11 | }
12 |
13 | html,
14 | body {
15 | max-width: 100vw;
16 | overflow-x: hidden;
17 | }
18 |
19 | body {
20 | color: var(--foreground);
21 | background: var(--background);
22 | }
23 |
24 | * {
25 | box-sizing: border-box;
26 | padding: 0;
27 | margin: 0;
28 | }
29 |
30 | a {
31 | color: inherit;
32 | text-decoration: none;
33 | }
34 |
35 | .imgDark {
36 | display: none;
37 | }
38 |
39 | @media (prefers-color-scheme: dark) {
40 | html {
41 | color-scheme: dark;
42 | }
43 |
44 | .imgLight {
45 | display: none;
46 | }
47 | .imgDark {
48 | display: unset;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/apps/docs/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import localFont from "next/font/local";
3 | import "./globals.css";
4 |
5 | const geistSans = localFont({
6 | src: "./fonts/GeistVF.woff",
7 | variable: "--font-geist-sans",
8 | });
9 | const geistMono = localFont({
10 | src: "./fonts/GeistMonoVF.woff",
11 | variable: "--font-geist-mono",
12 | });
13 |
14 | export const metadata: Metadata = {
15 | title: "Create Next App",
16 | description: "Generated by create next app",
17 | };
18 |
19 | export default function RootLayout({
20 | children,
21 | }: Readonly<{
22 | children: React.ReactNode;
23 | }>) {
24 | return (
25 |
26 |
27 | {children}
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/apps/docs/app/page.module.css:
--------------------------------------------------------------------------------
1 | .page {
2 | --gray-rgb: 0, 0, 0;
3 | --gray-alpha-200: rgba(var(--gray-rgb), 0.08);
4 | --gray-alpha-100: rgba(var(--gray-rgb), 0.05);
5 |
6 | --button-primary-hover: #383838;
7 | --button-secondary-hover: #f2f2f2;
8 |
9 | display: grid;
10 | grid-template-rows: 20px 1fr 20px;
11 | align-items: center;
12 | justify-items: center;
13 | min-height: 100svh;
14 | padding: 80px;
15 | gap: 64px;
16 | font-synthesis: none;
17 | }
18 |
19 | @media (prefers-color-scheme: dark) {
20 | .page {
21 | --gray-rgb: 255, 255, 255;
22 | --gray-alpha-200: rgba(var(--gray-rgb), 0.145);
23 | --gray-alpha-100: rgba(var(--gray-rgb), 0.06);
24 |
25 | --button-primary-hover: #ccc;
26 | --button-secondary-hover: #1a1a1a;
27 | }
28 | }
29 |
30 | .main {
31 | display: flex;
32 | flex-direction: column;
33 | gap: 32px;
34 | grid-row-start: 2;
35 | }
36 |
37 | .main ol {
38 | font-family: var(--font-geist-mono);
39 | padding-left: 0;
40 | margin: 0;
41 | font-size: 14px;
42 | line-height: 24px;
43 | letter-spacing: -0.01em;
44 | list-style-position: inside;
45 | }
46 |
47 | .main li:not(:last-of-type) {
48 | margin-bottom: 8px;
49 | }
50 |
51 | .main code {
52 | font-family: inherit;
53 | background: var(--gray-alpha-100);
54 | padding: 2px 4px;
55 | border-radius: 4px;
56 | font-weight: 600;
57 | }
58 |
59 | .ctas {
60 | display: flex;
61 | gap: 16px;
62 | }
63 |
64 | .ctas a {
65 | appearance: none;
66 | border-radius: 128px;
67 | height: 48px;
68 | padding: 0 20px;
69 | border: none;
70 | font-family: var(--font-geist-sans);
71 | border: 1px solid transparent;
72 | transition: background 0.2s, color 0.2s, border-color 0.2s;
73 | cursor: pointer;
74 | display: flex;
75 | align-items: center;
76 | justify-content: center;
77 | font-size: 16px;
78 | line-height: 20px;
79 | font-weight: 500;
80 | }
81 |
82 | a.primary {
83 | background: var(--foreground);
84 | color: var(--background);
85 | gap: 8px;
86 | }
87 |
88 | a.secondary {
89 | border-color: var(--gray-alpha-200);
90 | min-width: 180px;
91 | }
92 |
93 | button.secondary {
94 | appearance: none;
95 | border-radius: 128px;
96 | height: 48px;
97 | padding: 0 20px;
98 | border: none;
99 | font-family: var(--font-geist-sans);
100 | border: 1px solid transparent;
101 | transition: background 0.2s, color 0.2s, border-color 0.2s;
102 | cursor: pointer;
103 | display: flex;
104 | align-items: center;
105 | justify-content: center;
106 | font-size: 16px;
107 | line-height: 20px;
108 | font-weight: 500;
109 | background: transparent;
110 | border-color: var(--gray-alpha-200);
111 | min-width: 180px;
112 | }
113 |
114 | .footer {
115 | font-family: var(--font-geist-sans);
116 | grid-row-start: 3;
117 | display: flex;
118 | gap: 24px;
119 | }
120 |
121 | .footer a {
122 | display: flex;
123 | align-items: center;
124 | gap: 8px;
125 | }
126 |
127 | .footer img {
128 | flex-shrink: 0;
129 | }
130 |
131 | /* Enable hover only on non-touch devices */
132 | @media (hover: hover) and (pointer: fine) {
133 | a.primary:hover {
134 | background: var(--button-primary-hover);
135 | border-color: transparent;
136 | }
137 |
138 | a.secondary:hover {
139 | background: var(--button-secondary-hover);
140 | border-color: transparent;
141 | }
142 |
143 | .footer a:hover {
144 | text-decoration: underline;
145 | text-underline-offset: 4px;
146 | }
147 | }
148 |
149 | @media (max-width: 600px) {
150 | .page {
151 | padding: 32px;
152 | padding-bottom: 80px;
153 | }
154 |
155 | .main {
156 | align-items: center;
157 | }
158 |
159 | .main ol {
160 | text-align: center;
161 | }
162 |
163 | .ctas {
164 | flex-direction: column;
165 | }
166 |
167 | .ctas a {
168 | font-size: 14px;
169 | height: 40px;
170 | padding: 0 16px;
171 | }
172 |
173 | a.secondary {
174 | min-width: auto;
175 | }
176 |
177 | .footer {
178 | flex-wrap: wrap;
179 | align-items: center;
180 | justify-content: center;
181 | }
182 | }
183 |
184 | @media (prefers-color-scheme: dark) {
185 | .logo {
186 | filter: invert();
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/apps/docs/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Image, { type ImageProps } from "next/image";
2 | import { Button } from "@repo/ui/button";
3 | import styles from "./page.module.css";
4 |
5 | type Props = Omit & {
6 | srcLight: string;
7 | srcDark: string;
8 | };
9 |
10 | const ThemeImage = (props: Props) => {
11 | const { srcLight, srcDark, ...rest } = props;
12 |
13 | return (
14 | <>
15 |
16 |
17 | >
18 | );
19 | };
20 |
21 | export default function Home() {
22 | return (
23 |
24 |
25 |
34 |
35 | -
36 | Get started by editing
apps/docs/app/page.tsx
37 |
38 | - Save and see your changes instantly.
39 |
40 |
41 |
66 |
69 |
70 |
100 |
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/apps/docs/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { nextJsConfig } from "@repo/eslint-config/next-js";
2 |
3 | /** @type {import("eslint").Linter.Config} */
4 | export default nextJsConfig;
5 |
--------------------------------------------------------------------------------
/apps/docs/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/apps/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "0.1.0",
4 | "type": "module",
5 | "private": true,
6 | "scripts": {
7 | "dev": "next dev --turbopack --port 3001",
8 | "build": "next build",
9 | "start": "next start",
10 | "lint": "next lint --max-warnings 0",
11 | "check-types": "tsc --noEmit"
12 | },
13 | "dependencies": {
14 | "@repo/ui": "workspace:*",
15 | "next": "^15.2.1",
16 | "react": "^19.0.0",
17 | "react-dom": "^19.0.0"
18 | },
19 | "devDependencies": {
20 | "@repo/eslint-config": "workspace:*",
21 | "@repo/typescript-config": "workspace:*",
22 | "@types/node": "^22.13.9",
23 | "@types/react": "19.0.10",
24 | "@types/react-dom": "19.0.4",
25 | "eslint": "^9.22.0",
26 | "typescript": "5.8.2"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/apps/docs/public/file-text.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/apps/docs/public/globe.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/apps/docs/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/docs/public/turborepo-dark.svg:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/apps/docs/public/turborepo-light.svg:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/apps/docs/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/apps/docs/public/window.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/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 | "**/*.ts",
12 | "**/*.tsx",
13 | "next-env.d.ts",
14 | "next.config.js",
15 | ".next/types/**/*.ts"
16 | ],
17 | "exclude": [
18 | "node_modules"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/apps/web/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # env files (can opt-in for commiting if needed)
29 | .env*
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/apps/web/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
37 |
--------------------------------------------------------------------------------
/apps/web/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emilwidlund/tactile/8e3a490ab1d3b02a813c078a12c52015be8286d9/apps/web/app/favicon.ico
--------------------------------------------------------------------------------
/apps/web/app/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emilwidlund/tactile/8e3a490ab1d3b02a813c078a12c52015be8286d9/apps/web/app/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/apps/web/app/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emilwidlund/tactile/8e3a490ab1d3b02a813c078a12c52015be8286d9/apps/web/app/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/apps/web/app/globals.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --background: #ffffff;
3 | --foreground: #171717;
4 | }
5 |
6 | @media (prefers-color-scheme: dark) {
7 | :root {
8 | --background: #0a0a0a;
9 | --foreground: #ededed;
10 | }
11 | }
12 |
13 | html,
14 | body {
15 | max-width: 100vw;
16 | overflow-x: hidden;
17 | }
18 |
19 | body {
20 | color: var(--foreground);
21 | background: var(--background);
22 | }
23 |
24 | * {
25 | box-sizing: border-box;
26 | padding: 0;
27 | margin: 0;
28 | }
29 |
30 | a {
31 | color: inherit;
32 | text-decoration: none;
33 | }
34 |
35 | .imgDark {
36 | display: none;
37 | }
38 |
39 | @media (prefers-color-scheme: dark) {
40 | html {
41 | color-scheme: dark;
42 | }
43 |
44 | .imgLight {
45 | display: none;
46 | }
47 | .imgDark {
48 | display: unset;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/apps/web/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import localFont from "next/font/local";
3 | import "./globals.css";
4 |
5 | const geistSans = localFont({
6 | src: "./fonts/GeistVF.woff",
7 | variable: "--font-geist-sans",
8 | });
9 | const geistMono = localFont({
10 | src: "./fonts/GeistMonoVF.woff",
11 | variable: "--font-geist-mono",
12 | });
13 |
14 | export const metadata: Metadata = {
15 | title: "Create Next App",
16 | description: "Generated by create next app",
17 | };
18 |
19 | export default function RootLayout({
20 | children,
21 | }: Readonly<{
22 | children: React.ReactNode;
23 | }>) {
24 | return (
25 |
26 |
27 | {children}
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/apps/web/app/page.module.css:
--------------------------------------------------------------------------------
1 | .page {
2 | --gray-rgb: 0, 0, 0;
3 | --gray-alpha-200: rgba(var(--gray-rgb), 0.08);
4 | --gray-alpha-100: rgba(var(--gray-rgb), 0.05);
5 |
6 | --button-primary-hover: #383838;
7 | --button-secondary-hover: #f2f2f2;
8 |
9 | display: grid;
10 | grid-template-rows: 20px 1fr 20px;
11 | align-items: center;
12 | justify-items: center;
13 | min-height: 100svh;
14 | padding: 80px;
15 | gap: 64px;
16 | font-synthesis: none;
17 | }
18 |
19 | @media (prefers-color-scheme: dark) {
20 | .page {
21 | --gray-rgb: 255, 255, 255;
22 | --gray-alpha-200: rgba(var(--gray-rgb), 0.145);
23 | --gray-alpha-100: rgba(var(--gray-rgb), 0.06);
24 |
25 | --button-primary-hover: #ccc;
26 | --button-secondary-hover: #1a1a1a;
27 | }
28 | }
29 |
30 | .main {
31 | display: flex;
32 | flex-direction: column;
33 | gap: 32px;
34 | grid-row-start: 2;
35 | }
36 |
37 | .main ol {
38 | font-family: var(--font-geist-mono);
39 | padding-left: 0;
40 | margin: 0;
41 | font-size: 14px;
42 | line-height: 24px;
43 | letter-spacing: -0.01em;
44 | list-style-position: inside;
45 | }
46 |
47 | .main li:not(:last-of-type) {
48 | margin-bottom: 8px;
49 | }
50 |
51 | .main code {
52 | font-family: inherit;
53 | background: var(--gray-alpha-100);
54 | padding: 2px 4px;
55 | border-radius: 4px;
56 | font-weight: 600;
57 | }
58 |
59 | .ctas {
60 | display: flex;
61 | gap: 16px;
62 | }
63 |
64 | .ctas a {
65 | appearance: none;
66 | border-radius: 128px;
67 | height: 48px;
68 | padding: 0 20px;
69 | border: none;
70 | font-family: var(--font-geist-sans);
71 | border: 1px solid transparent;
72 | transition: background 0.2s, color 0.2s, border-color 0.2s;
73 | cursor: pointer;
74 | display: flex;
75 | align-items: center;
76 | justify-content: center;
77 | font-size: 16px;
78 | line-height: 20px;
79 | font-weight: 500;
80 | }
81 |
82 | a.primary {
83 | background: var(--foreground);
84 | color: var(--background);
85 | gap: 8px;
86 | }
87 |
88 | a.secondary {
89 | border-color: var(--gray-alpha-200);
90 | min-width: 180px;
91 | }
92 |
93 | button.secondary {
94 | appearance: none;
95 | border-radius: 128px;
96 | height: 48px;
97 | padding: 0 20px;
98 | border: none;
99 | font-family: var(--font-geist-sans);
100 | border: 1px solid transparent;
101 | transition: background 0.2s, color 0.2s, border-color 0.2s;
102 | cursor: pointer;
103 | display: flex;
104 | align-items: center;
105 | justify-content: center;
106 | font-size: 16px;
107 | line-height: 20px;
108 | font-weight: 500;
109 | background: transparent;
110 | border-color: var(--gray-alpha-200);
111 | min-width: 180px;
112 | }
113 |
114 | .footer {
115 | font-family: var(--font-geist-sans);
116 | grid-row-start: 3;
117 | display: flex;
118 | gap: 24px;
119 | }
120 |
121 | .footer a {
122 | display: flex;
123 | align-items: center;
124 | gap: 8px;
125 | }
126 |
127 | .footer img {
128 | flex-shrink: 0;
129 | }
130 |
131 | /* Enable hover only on non-touch devices */
132 | @media (hover: hover) and (pointer: fine) {
133 | a.primary:hover {
134 | background: var(--button-primary-hover);
135 | border-color: transparent;
136 | }
137 |
138 | a.secondary:hover {
139 | background: var(--button-secondary-hover);
140 | border-color: transparent;
141 | }
142 |
143 | .footer a:hover {
144 | text-decoration: underline;
145 | text-underline-offset: 4px;
146 | }
147 | }
148 |
149 | @media (max-width: 600px) {
150 | .page {
151 | padding: 32px;
152 | padding-bottom: 80px;
153 | }
154 |
155 | .main {
156 | align-items: center;
157 | }
158 |
159 | .main ol {
160 | text-align: center;
161 | }
162 |
163 | .ctas {
164 | flex-direction: column;
165 | }
166 |
167 | .ctas a {
168 | font-size: 14px;
169 | height: 40px;
170 | padding: 0 16px;
171 | }
172 |
173 | a.secondary {
174 | min-width: auto;
175 | }
176 |
177 | .footer {
178 | flex-wrap: wrap;
179 | align-items: center;
180 | justify-content: center;
181 | }
182 | }
183 |
184 | @media (prefers-color-scheme: dark) {
185 | .logo {
186 | filter: invert();
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/apps/web/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Image, { type ImageProps } from "next/image";
2 | import { Button } from "@repo/ui/button";
3 | import styles from "./page.module.css";
4 |
5 | type Props = Omit & {
6 | srcLight: string;
7 | srcDark: string;
8 | };
9 |
10 | const ThemeImage = (props: Props) => {
11 | const { srcLight, srcDark, ...rest } = props;
12 |
13 | return (
14 | <>
15 |
16 |
17 | >
18 | );
19 | };
20 |
21 | export default function Home() {
22 | return (
23 |
24 |
25 |
34 |
35 | -
36 | Get started by editing
apps/web/app/page.tsx
37 |
38 | - Save and see your changes instantly.
39 |
40 |
41 |
66 |
69 |
70 |
100 |
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/apps/web/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { nextJsConfig } from "@repo/eslint-config/next-js";
2 |
3 | /** @type {import("eslint").Linter.Config} */
4 | export default nextJsConfig;
5 |
--------------------------------------------------------------------------------
/apps/web/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/apps/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "version": "0.1.0",
4 | "type": "module",
5 | "private": true,
6 | "scripts": {
7 | "dev": "next dev --turbopack --port 3000",
8 | "build": "next build",
9 | "start": "next start",
10 | "lint": "next lint --max-warnings 0",
11 | "check-types": "tsc --noEmit"
12 | },
13 | "dependencies": {
14 | "@repo/ui": "workspace:*",
15 | "next": "^15.2.1",
16 | "react": "^19.0.0",
17 | "react-dom": "^19.0.0"
18 | },
19 | "devDependencies": {
20 | "@repo/eslint-config": "workspace:*",
21 | "@repo/typescript-config": "workspace:*",
22 | "@types/node": "^22.13.9",
23 | "@types/react": "19.0.10",
24 | "@types/react-dom": "19.0.4",
25 | "eslint": "^9.22.0",
26 | "typescript": "5.8.2"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/apps/web/public/file-text.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/apps/web/public/globe.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/apps/web/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/turborepo-dark.svg:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/apps/web/public/turborepo-light.svg:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/apps/web/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/apps/web/public/window.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "plugins": [
5 | {
6 | "name": "next"
7 | }
8 | ]
9 | },
10 | "include": [
11 | "**/*.ts",
12 | "**/*.tsx",
13 | "next-env.d.ts",
14 | "next.config.js",
15 | ".next/types/**/*.ts"
16 | ],
17 | "exclude": [
18 | "node_modules"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tactile",
3 | "private": true,
4 | "scripts": {
5 | "build": "turbo run build",
6 | "dev": "turbo run dev",
7 | "lint": "turbo run lint",
8 | "format": "prettier --write \"**/*.{ts,tsx,md}\"",
9 | "check-types": "turbo run check-types"
10 | },
11 | "devDependencies": {
12 | "prettier": "^3.5.3",
13 | "turbo": "^2.4.4",
14 | "typescript": "5.8.2"
15 | },
16 | "packageManager": "pnpm@9.0.0",
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/base.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import eslintConfigPrettier from "eslint-config-prettier";
3 | import turboPlugin from "eslint-plugin-turbo";
4 | import tseslint from "typescript-eslint";
5 | import onlyWarn from "eslint-plugin-only-warn";
6 |
7 | /**
8 | * A shared ESLint configuration for the repository.
9 | *
10 | * @type {import("eslint").Linter.Config[]}
11 | * */
12 | export const config = [
13 | js.configs.recommended,
14 | eslintConfigPrettier,
15 | ...tseslint.configs.recommended,
16 | {
17 | plugins: {
18 | turbo: turboPlugin,
19 | },
20 | rules: {
21 | "turbo/no-undeclared-env-vars": "warn",
22 | },
23 | },
24 | {
25 | plugins: {
26 | onlyWarn,
27 | },
28 | },
29 | {
30 | ignores: ["dist/**"],
31 | },
32 | ];
33 |
--------------------------------------------------------------------------------
/packages/eslint-config/next.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import eslintConfigPrettier from "eslint-config-prettier";
3 | import tseslint from "typescript-eslint";
4 | import pluginReactHooks from "eslint-plugin-react-hooks";
5 | import pluginReact from "eslint-plugin-react";
6 | import globals from "globals";
7 | import pluginNext from "@next/eslint-plugin-next";
8 | import { config as baseConfig } from "./base.js";
9 |
10 | /**
11 | * A custom ESLint configuration for libraries that use Next.js.
12 | *
13 | * @type {import("eslint").Linter.Config[]}
14 | * */
15 | export const nextJsConfig = [
16 | ...baseConfig,
17 | js.configs.recommended,
18 | eslintConfigPrettier,
19 | ...tseslint.configs.recommended,
20 | {
21 | ...pluginReact.configs.flat.recommended,
22 | languageOptions: {
23 | ...pluginReact.configs.flat.recommended.languageOptions,
24 | globals: {
25 | ...globals.serviceworker,
26 | },
27 | },
28 | },
29 | {
30 | plugins: {
31 | "@next/next": pluginNext,
32 | },
33 | rules: {
34 | ...pluginNext.configs.recommended.rules,
35 | ...pluginNext.configs["core-web-vitals"].rules,
36 | },
37 | },
38 | {
39 | plugins: {
40 | "react-hooks": pluginReactHooks,
41 | },
42 | settings: { react: { version: "detect" } },
43 | rules: {
44 | ...pluginReactHooks.configs.recommended.rules,
45 | // React scope no longer necessary with new JSX transform.
46 | "react/react-in-jsx-scope": "off",
47 | },
48 | },
49 | ];
50 |
--------------------------------------------------------------------------------
/packages/eslint-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/eslint-config",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "private": true,
6 | "exports": {
7 | "./base": "./base.js",
8 | "./next-js": "./next.js",
9 | "./react-internal": "./react-internal.js"
10 | },
11 | "devDependencies": {
12 | "@eslint/js": "^9.22.0",
13 | "@next/eslint-plugin-next": "^15.2.1",
14 | "eslint": "^9.22.0",
15 | "eslint-config-prettier": "^10.1.1",
16 | "eslint-plugin-only-warn": "^1.1.0",
17 | "eslint-plugin-react": "^7.37.4",
18 | "eslint-plugin-react-hooks": "^5.2.0",
19 | "eslint-plugin-turbo": "^2.4.4",
20 | "globals": "^16.0.0",
21 | "typescript": "^5.8.2",
22 | "typescript-eslint": "^8.26.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/eslint-config/react-internal.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import eslintConfigPrettier from "eslint-config-prettier";
3 | import tseslint from "typescript-eslint";
4 | import pluginReactHooks from "eslint-plugin-react-hooks";
5 | import pluginReact from "eslint-plugin-react";
6 | import globals from "globals";
7 | import { config as baseConfig } from "./base.js";
8 |
9 | /**
10 | * A custom ESLint configuration for libraries that use React.
11 | *
12 | * @type {import("eslint").Linter.Config[]} */
13 | export const config = [
14 | ...baseConfig,
15 | js.configs.recommended,
16 | eslintConfigPrettier,
17 | ...tseslint.configs.recommended,
18 | pluginReact.configs.flat.recommended,
19 | {
20 | languageOptions: {
21 | ...pluginReact.configs.flat.recommended.languageOptions,
22 | globals: {
23 | ...globals.serviceworker,
24 | ...globals.browser,
25 | },
26 | },
27 | },
28 | {
29 | plugins: {
30 | "react-hooks": pluginReactHooks,
31 | },
32 | settings: { react: { version: "detect" } },
33 | rules: {
34 | ...pluginReactHooks.configs.recommended.rules,
35 | // React scope no longer necessary with new JSX transform.
36 | "react/react-in-jsx-scope": "off",
37 | },
38 | },
39 | ];
40 |
--------------------------------------------------------------------------------
/packages/typescript-config/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "declaration": true,
5 | "declarationMap": true,
6 | "esModuleInterop": true,
7 | "incremental": false,
8 | "isolatedModules": true,
9 | "lib": ["es2022", "DOM", "DOM.Iterable"],
10 | "module": "NodeNext",
11 | "moduleDetection": "force",
12 | "moduleResolution": "NodeNext",
13 | "noUncheckedIndexedAccess": true,
14 | "resolveJsonModule": true,
15 | "skipLibCheck": true,
16 | "strict": true,
17 | "target": "ES2022"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/typescript-config/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./base.json",
4 | "compilerOptions": {
5 | "plugins": [{ "name": "next" }],
6 | "module": "ESNext",
7 | "moduleResolution": "Bundler",
8 | "allowJs": true,
9 | "jsx": "preserve",
10 | "noEmit": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/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 | "extends": "./base.json",
4 | "compilerOptions": {
5 | "jsx": "react-jsx"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/ui/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { config } from "@repo/eslint-config/react-internal";
2 |
3 | /** @type {import("eslint").Linter.Config} */
4 | export default config;
5 |
--------------------------------------------------------------------------------
/packages/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/ui",
3 | "version": "0.0.0",
4 | "private": true,
5 | "exports": {
6 | "./*": "./src/*.tsx"
7 | },
8 | "scripts": {
9 | "lint": "eslint . --max-warnings 0",
10 | "generate:component": "turbo gen react-component",
11 | "check-types": "tsc --noEmit"
12 | },
13 | "devDependencies": {
14 | "@repo/eslint-config": "workspace:*",
15 | "@repo/typescript-config": "workspace:*",
16 | "@turbo/gen": "^2.4.4",
17 | "@types/node": "^22.13.9",
18 | "@types/react": "19.0.10",
19 | "@types/react-dom": "19.0.4",
20 | "eslint": "^9.22.0",
21 | "typescript": "5.8.2"
22 | },
23 | "dependencies": {
24 | "react": "^19.0.0",
25 | "react-dom": "^19.0.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/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 | import { type JSX } from "react";
2 |
3 | export function Card({
4 | className,
5 | title,
6 | children,
7 | href,
8 | }: {
9 | className?: string;
10 | title: string;
11 | children: React.ReactNode;
12 | href: string;
13 | }): JSX.Element {
14 | return (
15 |
21 |
22 | {title} ->
23 |
24 | {children}
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/packages/ui/src/code.tsx:
--------------------------------------------------------------------------------
1 | import { type JSX } from "react";
2 |
3 | export function Code({
4 | children,
5 | className,
6 | }: {
7 | children: React.ReactNode;
8 | className?: string;
9 | }): JSX.Element {
10 | return {children}
;
11 | }
12 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "ui": "tui",
4 | "tasks": {
5 | "build": {
6 | "dependsOn": ["^build"],
7 | "inputs": ["$TURBO_DEFAULT$", ".env*"],
8 | "outputs": [".next/**", "!.next/cache/**"]
9 | },
10 | "lint": {
11 | "dependsOn": ["^lint"]
12 | },
13 | "check-types": {
14 | "dependsOn": ["^check-types"]
15 | },
16 | "dev": {
17 | "cache": false,
18 | "persistent": true
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------