create-react-adam 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # create-react-adam
2
+
3
+ Create opinionated React apps with TypeScript, Vite, Wouter, and Tailwind CSS.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx create-react-adam@latest my-app
9
+ cd my-app
10
+ npm run dev
11
+ ```
12
+
13
+ ## CLI Options
14
+
15
+ - `--dir <path>` - Create the project in a specific directory
16
+
17
+ ```bash
18
+ npm create react-adam@latest my-app --dir ~/projects
19
+ ```
20
+
21
+ - `--with-e2e` - Include E2E testing setup without prompting
22
+
23
+ ```bash
24
+ npm create react-adam@latest my-app --with-e2e
25
+ ```
26
+
27
+ - `--no-e2e` - Skip E2E testing setup without prompting
28
+ ```bash
29
+ npm create react-adam@latest my-app --no-e2e
30
+ ```
31
+
32
+ ## What's Included
33
+
34
+ ### Core Stack
35
+
36
+ - **React** - Latest version with TypeScript support
37
+ - **Vite** - Lightning-fast development server with HMR
38
+ - **Wouter** - Minimalist client-side routing (~1.3kB)
39
+ - **Tailwind CSS 4** - Utility-first CSS
40
+ - **React Icons** - Popular icon library
41
+
42
+ ### Code Quality Tools
43
+
44
+ #### ESLint
45
+
46
+ Configured with a modern flat config (`eslint.config.js`) that includes:
47
+
48
+ - **TypeScript ESLint** - Recommended and strict presets for type-safe code
49
+ - **React Hooks** - Ensures correct hooks usage patterns
50
+ - **React Refresh** - Validates fast refresh compatibility
51
+ - **JSX Accessibility** - Enforces accessibility best practices (a11y)
52
+ - **Prettier integration** - Disables conflicting formatting rules
53
+
54
+ Why ESLint? It catches bugs early, enforces consistent code patterns, and integrates seamlessly with TypeScript. The strict preset helps maintain high code quality standards.
55
+
56
+ #### Prettier
57
+
58
+ Configured with two powerful plugins:
59
+
60
+ - **prettier-plugin-organize-imports** - Automatically sorts and removes unused imports
61
+ - **prettier-plugin-tailwindcss** - Sorts Tailwind class names consistently
62
+
63
+ Why Prettier? It eliminates debates about code style by automatically formatting code. The organize-imports plugin keeps imports clean, and the Tailwind plugin ensures class names follow the recommended ordering.
64
+
65
+ ### Optional E2E Testing
66
+
67
+ When you include E2E testing (via prompt or `--with-e2e` flag), you get:
68
+
69
+ - **Playwright** - Modern, reliable E2E testing framework
70
+ - **Allure Reports** - Beautiful, detailed test reports
71
+ - Pre-configured test setup with example tests
72
+ - HTML reports and Allure integration
73
+
74
+ ### Dependency Management
75
+
76
+ - **Exact version pinning** - All dependencies use exact versions (no `^` or `~`)
77
+ - **`.npmrc` configuration** - Ensures `save-exact=true` for all future installs
78
+ - **Renovate-friendly** - Compatible with automated dependency updates
79
+
80
+ ### GitHub Actions Workflows
81
+
82
+ Two CI workflows are included in the generated projects:
83
+
84
+ 1. **Code Checks** (`.github/workflows/check.yml`)
85
+
86
+ - TypeScript type checking
87
+ - Prettier formatting validation
88
+ - ESLint linting
89
+ - Production build verification
90
+
91
+ 2. **E2E Tests** (`.github/workflows/e2e.yml`) - if E2E is included
92
+ - Runs Playwright tests in CI
93
+ - Uploads test reports as artifacts
94
+ - Configures Allure results
95
+
96
+ ## Available Scripts
97
+
98
+ ### Development
99
+
100
+ #### `npm run dev`
101
+
102
+ Starts the Vite development server with hot module replacement.
103
+
104
+ #### `npm run build`
105
+
106
+ Builds the app for production to the `dist` folder. Runs TypeScript compiler and Vite build.
107
+
108
+ #### `npm run preview`
109
+
110
+ Previews the production build locally.
111
+
112
+ ### Code Quality
113
+
114
+ #### `npm run lint`
115
+
116
+ Runs ESLint and automatically fixes issues where possible.
117
+
118
+ #### `npm run lint:check`
119
+
120
+ Runs ESLint without fixing issues. Useful for CI environments.
121
+
122
+ #### `npm run format`
123
+
124
+ Formats all files with Prettier, organizing imports and sorting Tailwind classes.
125
+
126
+ #### `npm run format:check`
127
+
128
+ Checks if files are formatted correctly without modifying them. Useful for CI environments.
129
+
130
+ ### E2E Testing (if included)
131
+
132
+ #### `npm run test:e2e`
133
+
134
+ Runs all Playwright E2E tests headlessly.
135
+
136
+ #### `npm run test:e2e:ui`
137
+
138
+ Opens Playwright's interactive UI mode for debugging tests.
139
+
140
+ #### `npm run test:e2e:report`
141
+
142
+ Generates and opens the Allure test report.
143
+
144
+ ### Troubleshooting
145
+
146
+ #### `npm run itDoesNotWork`
147
+
148
+ Runs basic troubleshooting checks:
149
+
150
+ - Installs dependencies
151
+ - Checks if Vite dev server is running
152
+ - Provides helpful diagnostic information
153
+
154
+ ## Project Structure
155
+
156
+ ```
157
+ my-app/
158
+ ├── src/
159
+ │ ├── pages/ # Page components
160
+ │ │ ├── Home/
161
+ │ │ └── About/
162
+ │ ├── App.tsx # Main app with routing
163
+ │ ├── main.tsx # Entry point
164
+ │ └── app.css # Tailwind directives
165
+ ├── e2e/ # E2E tests (optional)
166
+ ├── .github/ # GitHub Actions workflows
167
+ └── package.json
168
+ ```
169
+
170
+ ## Why This Stack?
171
+
172
+ - **Modern**: Latest versions of React, Vite, and Tailwind
173
+ - **Lightweight**: Minimal dependencies, under 1MB node_modules (before dev deps)
174
+ - **Type-safe**: Full TypeScript support with strict checking
175
+ - **Fast**: Vite's instant HMR and optimized builds
176
+ - **Accessible**: Built-in a11y linting ensures inclusive UIs
177
+ - **Maintainable**: Automated formatting and linting prevent technical debt
178
+ - **CI-ready**: Workflows for testing and validation out of the box
179
+
180
+ ## License
181
+
182
+ MIT
package/bin/index.js ADDED
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+ import { readdir, mkdir, readFile, writeFile, copyFile, stat, rm } from 'fs/promises';
6
+ import { existsSync } from 'fs';
7
+ import { spawn } from 'child_process';
8
+ import * as readline from 'readline';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ const args = process.argv.slice(2);
14
+
15
+ function parseArgs(args) {
16
+ const parsed = {
17
+ projectName: null,
18
+ flags: {}
19
+ };
20
+
21
+ for (let i = 0; i < args.length; i++) {
22
+ const arg = args[i];
23
+ if (arg.startsWith('--')) {
24
+ const key = arg.slice(2);
25
+ const nextArg = args[i + 1];
26
+ if (nextArg && !nextArg.startsWith('--')) {
27
+ parsed.flags[key] = nextArg;
28
+ i++;
29
+ } else {
30
+ parsed.flags[key] = true;
31
+ }
32
+ } else if (!parsed.projectName) {
33
+ parsed.projectName = arg;
34
+ }
35
+ }
36
+
37
+ return parsed;
38
+ }
39
+
40
+ function validateProjectName(name) {
41
+ if (!name) {
42
+ return 'Project name is required';
43
+ }
44
+ if (!/^[a-z0-9-_]+$/i.test(name)) {
45
+ return 'Project name can only contain letters, numbers, hyphens, and underscores';
46
+ }
47
+ return null;
48
+ }
49
+
50
+ async function copyDirectory(src, dest, excludePaths = []) {
51
+ await mkdir(dest, { recursive: true });
52
+ const entries = await readdir(src, { withFileTypes: true });
53
+
54
+ for (const entry of entries) {
55
+ const srcPath = join(src, entry.name);
56
+ const destPath = join(dest, entry.name);
57
+
58
+ const shouldExclude = excludePaths.some(excludePath => {
59
+ if (entry.isDirectory() && entry.name === excludePath) {
60
+ return true;
61
+ }
62
+ return srcPath.endsWith(`/${excludePath}`) || srcPath.endsWith(`\\${excludePath}`);
63
+ });
64
+
65
+ if (shouldExclude) {
66
+ continue;
67
+ }
68
+
69
+ if (entry.isDirectory()) {
70
+ await copyDirectory(srcPath, destPath, excludePaths);
71
+ } else {
72
+ await copyFile(srcPath, destPath);
73
+ }
74
+ }
75
+ }
76
+
77
+ function askQuestion(query) {
78
+ const rl = readline.createInterface({
79
+ input: process.stdin,
80
+ output: process.stdout
81
+ });
82
+
83
+ return new Promise((resolve) => {
84
+ rl.question(query, (answer) => {
85
+ rl.close();
86
+ resolve(answer);
87
+ });
88
+ });
89
+ }
90
+
91
+ async function replaceInFile(filePath, replacements) {
92
+ let content = await readFile(filePath, 'utf-8');
93
+ for (const [key, value] of Object.entries(replacements)) {
94
+ content = content.replace(new RegExp(key, 'g'), value);
95
+ }
96
+ await writeFile(filePath, content, 'utf-8');
97
+ }
98
+
99
+ function runCommand(command, args, cwd) {
100
+ return new Promise((resolve, reject) => {
101
+ const child = spawn(command, args, {
102
+ cwd,
103
+ stdio: 'inherit'
104
+ });
105
+
106
+ child.on('close', (code) => {
107
+ if (code !== 0) {
108
+ reject(new Error(`Command failed with exit code ${code}`));
109
+ } else {
110
+ resolve();
111
+ }
112
+ });
113
+ });
114
+ }
115
+
116
+ async function main() {
117
+ const { projectName, flags } = parseArgs(args);
118
+
119
+ const validationError = validateProjectName(projectName);
120
+ if (validationError) {
121
+ console.error(`Error: ${validationError}`);
122
+ console.log('\nUsage: npm create react-adam@latest <project-name> [options]');
123
+ console.log('\nOptions:');
124
+ console.log(' --dir <path> Create the project in a specific directory');
125
+ console.log(' --with-e2e Include E2E testing setup (Playwright + Allure)');
126
+ console.log(' --no-e2e Skip E2E testing setup');
127
+ console.log(' --with-utils Include utility functions (classNames, Storage, useUrlState)');
128
+ console.log(' --no-utils Skip utility functions');
129
+ process.exit(1);
130
+ }
131
+
132
+ const baseDir = flags.dir || flags.directory || process.cwd();
133
+ const projectPath = join(baseDir, projectName);
134
+
135
+ if (existsSync(projectPath)) {
136
+ console.error(`Error: Directory "${projectName}" already exists`);
137
+ process.exit(1);
138
+ }
139
+
140
+ let includeE2E = false;
141
+ if (flags['with-e2e']) {
142
+ includeE2E = true;
143
+ } else if (flags['no-e2e']) {
144
+ includeE2E = false;
145
+ } else {
146
+ const answer = await askQuestion('Include E2E testing setup (Playwright + Allure)? (y/N): ');
147
+ includeE2E = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
148
+ }
149
+
150
+ let includeUtils = false;
151
+ if (flags['with-utils']) {
152
+ includeUtils = true;
153
+ } else if (flags['no-utils']) {
154
+ includeUtils = false;
155
+ } else {
156
+ const answer = await askQuestion('Include utility functions (classNames, Storage, useUrlState)? (y/N): ');
157
+ includeUtils = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
158
+ }
159
+
160
+ console.log(`Creating a new Adam React app in ${projectPath}...`);
161
+
162
+ const templatePath = join(__dirname, '..', 'template');
163
+ const excludePaths = [];
164
+
165
+ if (!includeE2E) {
166
+ excludePaths.push('e2e', 'playwright.config.ts', '.e2e-deps.json');
167
+ }
168
+
169
+ if (!includeUtils) {
170
+ excludePaths.push('utils');
171
+ }
172
+
173
+ await copyDirectory(templatePath, projectPath, excludePaths);
174
+
175
+ if (!includeUtils) {
176
+ await copyFile(
177
+ join(projectPath, 'src', 'pages', 'Home', 'index.no-utils.tsx'),
178
+ join(projectPath, 'src', 'pages', 'Home', 'index.tsx')
179
+ );
180
+ await copyFile(
181
+ join(projectPath, 'src', 'pages', 'About', 'index.no-utils.tsx'),
182
+ join(projectPath, 'src', 'pages', 'About', 'index.tsx')
183
+ );
184
+ await rm(join(projectPath, 'src', 'pages', 'Home', 'index.no-utils.tsx'));
185
+ await rm(join(projectPath, 'src', 'pages', 'About', 'index.no-utils.tsx'));
186
+ } else {
187
+ await rm(join(projectPath, 'src', 'pages', 'Home', 'index.no-utils.tsx'));
188
+ await rm(join(projectPath, 'src', 'pages', 'About', 'index.no-utils.tsx'));
189
+ }
190
+
191
+ const replacements = {
192
+ '__PROJECT_NAME__': projectName
193
+ };
194
+
195
+ await replaceInFile(join(projectPath, 'package.json'), replacements);
196
+ await replaceInFile(join(projectPath, 'index.html'), replacements);
197
+ await replaceInFile(join(projectPath, 'README.md'), replacements);
198
+
199
+ if (includeE2E) {
200
+ const e2eDepsPath = join(projectPath, '.e2e-deps.json');
201
+ const packageJsonPath = join(projectPath, 'package.json');
202
+
203
+ const e2eDeps = JSON.parse(await readFile(e2eDepsPath, 'utf-8'));
204
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
205
+
206
+ packageJson.scripts = { ...packageJson.scripts, ...e2eDeps.scripts };
207
+ packageJson.devDependencies = { ...packageJson.devDependencies, ...e2eDeps.devDependencies };
208
+
209
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
210
+ await rm(e2eDepsPath);
211
+ }
212
+
213
+ console.log('\nInstalling dependencies...');
214
+ await runCommand('npm', ['install'], projectPath);
215
+
216
+ console.log('\n✓ Success! Created', projectName, 'at', projectPath);
217
+ console.log('\nInside that directory, you can run several commands:\n');
218
+ console.log(' npm run dev');
219
+ console.log(' Starts the development server.\n');
220
+ console.log(' npm run build');
221
+ console.log(' Builds the app for production.\n');
222
+ console.log(' npm run preview');
223
+ console.log(' Previews the production build.\n');
224
+ console.log(' npm run lint');
225
+ console.log(' Runs the linter.\n');
226
+
227
+ if (includeE2E) {
228
+ console.log(' npm run test:e2e');
229
+ console.log(' Runs E2E tests with Playwright.\n');
230
+ console.log(' npm run test:e2e:ui');
231
+ console.log(' Opens Playwright UI mode.\n');
232
+ console.log(' npm run test:e2e:report');
233
+ console.log(' Generates and opens Allure report.\n');
234
+ }
235
+
236
+ console.log('We suggest that you begin by typing:\n');
237
+ console.log(` cd ${projectName}`);
238
+ console.log(' npm run dev\n');
239
+ }
240
+
241
+ main().catch((error) => {
242
+ console.error('Error:', error.message);
243
+ process.exit(1);
244
+ });
245
+
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "create-react-adam",
3
+ "version": "0.1.0",
4
+ "description": "Create opinionated React apps with TypeScript, Vite, Wouter, and Tailwind CSS",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-react-adam": "./bin/index.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "template"
12
+ ],
13
+ "keywords": [
14
+ "react",
15
+ "vite",
16
+ "typescript",
17
+ "wouter",
18
+ "tailwindcss",
19
+ "create-react-app",
20
+ "scaffolding"
21
+ ],
22
+ "author": "Adam",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/adamjsturge/create-react-adam.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/adamjsturge/create-react-adam/issues"
30
+ },
31
+ "homepage": "https://github.com/adamjsturge/create-react-adam#readme",
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ },
35
+ "scripts": {
36
+ "quick-make": "npm link && cd ./tmp && npx create-react-adam test-app-$(date +%Y%m%d-%H%M%S) --with-e2e --with-utils",
37
+ "qm": "npm run quick-make",
38
+ "itDoesNotWork": "echo 'Wrong Directory'",
39
+ "cleanup": "rm -rf ./tmp/* && rm -rf ./template/node_modules && rm -rf ./template/dist && rm -rf ./template/package-lock.json && echo 'Template Dev Files Deleted, you must run npm install in the template directory for those features'",
40
+ "dev": "npm run cleanup && TIMESTAMP=$(date +%Y%m%d-%H%M%S) && npm link && cd ./tmp && npx create-react-adam test-app-$TIMESTAMP --with-e2e --with-utils && cd test-app-$TIMESTAMP && npm run dev"
41
+ }
42
+ }
43
+
@@ -0,0 +1,11 @@
1
+ {
2
+ "scripts": {
3
+ "test:e2e": "playwright test",
4
+ "test:e2e:ui": "playwright test --ui",
5
+ "test:e2e:report": "allure generate ./allure-results --clean && allure open"
6
+ },
7
+ "devDependencies": {
8
+ "@playwright/test": "1.48.0",
9
+ "allure-playwright": "3.0.3"
10
+ }
11
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": ["config:recommended"],
4
+ "packageRules": [
5
+ {
6
+ "matchManagers": ["npm"],
7
+ "groupName": "JavaScript Dependencies",
8
+ "groupSlug": "javascript-dependencies",
9
+ "schedule": ["at any time"]
10
+ },
11
+ {
12
+ "matchManagers": ["github-actions"],
13
+ "groupName": "GitHub Actions Dependencies",
14
+ "groupSlug": "github-actions-dependencies",
15
+ "schedule": ["at any time"]
16
+ },
17
+ {
18
+ "matchManagers": ["nvm"],
19
+ "groupName": "Node Version Manager Dependencies",
20
+ "groupSlug": "nvm-dependencies",
21
+ "schedule": ["at any time"]
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,36 @@
1
+ name: Template Code Checks
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ check:
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v5
16
+
17
+ - name: Setup Node.js
18
+ uses: actions/setup-node@v6
19
+ with:
20
+ node-version: "24.11.0"
21
+ cache: "npm"
22
+
23
+ - name: Install CLI dependencies
24
+ run: npm install
25
+
26
+ - name: Run TypeScript check
27
+ run: npx tsc --noEmit
28
+
29
+ - name: Check formatting
30
+ run: npm run format:check
31
+
32
+ - name: Run linting
33
+ run: npm run lint:check
34
+
35
+ - name: Run build
36
+ run: npm run build
@@ -0,0 +1,48 @@
1
+ name: E2E Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ e2e:
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v5
16
+
17
+ - name: Setup Node.js
18
+ uses: actions/setup-node@v6
19
+ with:
20
+ node-version: "24.11.0"
21
+ cache: "npm"
22
+
23
+ - name: Install CLI dependencies
24
+ run: npm install
25
+
26
+ - name: Install Playwright browsers
27
+ working-directory: ./e2e/
28
+ run: npx playwright install --with-deps chromium
29
+
30
+ - name: Run E2E tests
31
+ working-directory: ./e2e/
32
+ run: npm run test:e2e
33
+
34
+ - name: Upload Playwright report
35
+ uses: actions/upload-artifact@v5
36
+ if: always()
37
+ with:
38
+ name: playwright-report
39
+ path: ./e2e/playwright-report/
40
+ retention-days: 30
41
+
42
+ - name: Upload Allure results
43
+ uses: actions/upload-artifact@v5
44
+ if: always()
45
+ with:
46
+ name: allure-results
47
+ path: ./e2e/allure-results/
48
+ retention-days: 30
@@ -0,0 +1,2 @@
1
+ 24.11.0
2
+
@@ -0,0 +1,3 @@
1
+ {
2
+ "plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"]
3
+ }
@@ -0,0 +1,83 @@
1
+ # **PROJECT_NAME**
2
+
3
+ A React application created with create-react-adam.
4
+
5
+ ## Getting Started
6
+
7
+ ```bash
8
+ npm run dev
9
+ ```
10
+
11
+ This will start the development server at `http://localhost:5173`.
12
+
13
+ ## Available Scripts
14
+
15
+ ### `npm run itDoesNotWork`
16
+
17
+ Runs basic troubleshooting checks:
18
+
19
+ - Installs dependencies
20
+ - Checks if Vite dev server is running
21
+ - Provides helpful diagnostic information
22
+
23
+ ### `npm run dev`
24
+
25
+ Starts the development server with hot module replacement.
26
+
27
+ ### `npm run build`
28
+
29
+ Builds the app for production. The build artifacts will be stored in the `dist/` directory.
30
+
31
+ ### `npm run preview`
32
+
33
+ Previews the production build locally.
34
+
35
+ ### `npm run lint`
36
+
37
+ Runs ESLint to check for code quality issues.
38
+
39
+ ### `npm run format`
40
+
41
+ Formats all files with Prettier, organizing imports and sorting Tailwind classes.
42
+
43
+ ## Tech Stack
44
+
45
+ - **React** - UI library
46
+ - **TypeScript** - Type safety
47
+ - **Vite** - Build tool and dev server
48
+ - **Wouter** - Lightweight routing
49
+ - **Tailwind CSS** - Utility-first CSS framework
50
+ - **ESLint & Prettier** - Code quality and formatting
51
+
52
+ ## Project Structure
53
+
54
+ ```
55
+ src/
56
+ ├── pages/ # Page components
57
+ │ ├── Home/index.tsx
58
+ │ └── About/index.tsx
59
+ ├── App.tsx # Main app with routes
60
+ ├── main.tsx # Entry point
61
+ └── app.css # Global styles (Tailwind imports)
62
+ ```
63
+
64
+ ## Adding New Pages
65
+
66
+ 1. Create a new component in `src/pages/`
67
+ 2. Import and add a route in `src/App.tsx`
68
+
69
+ Example:
70
+
71
+ ```tsx
72
+ import NewPage from "./pages/NewPage";
73
+
74
+ // In the Switch component:
75
+ <Route path="/new" component={NewPage} />;
76
+ ```
77
+
78
+ ## Learn More
79
+
80
+ - [React Documentation](https://react.dev)
81
+ - [Vite Documentation](https://vitejs.dev)
82
+ - [Wouter Documentation](https://github.com/molefrog/wouter)
83
+ - [Tailwind CSS Documentation](https://tailwindcss.com)