├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── VERSIONING_POLICY.md ├── package-lock.json ├── package.json └── src ├── 第3章_Tailwind_CSSを導入する ├── gulpfile.js ├── package.json ├── postcss.config.js ├── src │ ├── index.html │ └── main.css └── webpack.config.js ├── 第6章_Tailwind_CSSでコンポーネントを設計する ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── components.json ├── index.html ├── package.json ├── postcss.config.js ├── public │ └── vite.svg ├── src │ ├── App.tsx │ ├── components │ │ ├── framer-motion │ │ │ └── AnimatedList.tsx │ │ ├── headlessui │ │ │ ├── MyDialog.tsx │ │ │ └── Transition.tsx │ │ ├── react-aria │ │ │ ├── Dialog.tsx │ │ │ ├── DialogWithComponent.tsx │ │ │ └── Popover.tsx │ │ ├── react-spring │ │ │ └── FadeIn.tsx │ │ └── ui │ │ │ ├── button.tsx │ │ │ └── card.tsx │ ├── index.css │ ├── lib │ │ └── utils.ts │ └── main.tsx ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── 第7章_Tailwind_CSSをカスタマイズする ├── 7_3_JavaScript_APIを利用する │ ├── extractClassNames.js │ ├── index.html │ ├── jest │ │ ├── jest.config.js │ │ └── preset.test.js │ ├── my-preset.js │ ├── tailwind.config.js │ └── vitest │ │ └── preset.test.js ├── 7_4_独自のプラグインを作成する │ ├── index.html │ ├── main.css │ ├── plugins │ │ ├── cssVariablePlugin.js │ │ ├── langPlugin.js │ │ └── trimmedTextPlugin.js │ ├── tailwind.config.js │ └── theme │ │ └── myColorTheme.js └── package.json └── 第8章_Tailwind_CSSを既存のプロジェクトに導入する ├── less ├── ng.less └── ok.less ├── package.json ├── run.sh ├── sass ├── ng.scss └── ok.scss └── stylus ├── ng.styl └── ok.styl /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | 12 | # CSS - https://developer.mozilla.org/ja/docs/Web/CSS 13 | # Sass(SCSS) - http://sass-lang.com/ 14 | # Stylus - https://learnboost.github.io/stylus/ 15 | # PostCSS - https://postcss.org/ 16 | [*.{css,scss,sass,styl,less,pcss}] 17 | indent_style = space 18 | indent_size = 2 19 | trim_trailing_whitespace = true 20 | 21 | # Handlebars.js - http://handlebarsjs.com/ 22 | [*.hbs] 23 | indent_style = space 24 | indent_size = 2 25 | 26 | # JavaScript - https://developer.mozilla.org/ja/docs/Web/JavaScript 27 | # TypeScript - https://www.typescriptlang.org/ 28 | # React - https://reactjs.org/ 29 | # Vue - https://vuejs.org/ 30 | [*.{js,ts,jsx,tsx,vue}] 31 | indent_style = space 32 | indent_size = 2 33 | 34 | # JSON - http://json.org/ 35 | # Composer - https://getcomposer.org/doc/04-schema.md 36 | [{*.json,composer.lock}] 37 | indent_style = space 38 | indent_size = 2 39 | 40 | # NPM - https://docs.npmjs.com/files/package.json 41 | # TypeScript - https://www.typescriptlang.org/ 42 | # Stylint - https://rosspatton.github.io/stylint/ 43 | # prettier - https://prettier.io/ 44 | # ESlint - https://eslint.org/ 45 | # VSCode Settings - https://code.visualstudio.com/docs/getstarted/settings 46 | [{**/package.json,**/tsconfig.*json,.stylintrc,.prettierrc,.eslintrc,.vscode/*.json}] 47 | indent_style = space 48 | indent_size = 2 49 | 50 | # PHP - http://php.net/ 51 | [*.php] 52 | indent_style = space 53 | indent_size = 4 54 | 55 | # Python - https://www.python.org/ 56 | [*.py] 57 | indent_style = space 58 | indent_size = 4 59 | 60 | # Ruby - https://www.ruby-lang.org/ 61 | # Rake - https://github.com/ruby/rake 62 | [{*.rb,*.rake,Rakefile}] 63 | indent_style = space 64 | indent_size = 2 65 | 66 | # Shell script (bash) - https://www.gnu.org/software/bash/manual/bash.html 67 | [*.sh] 68 | indent_style = space 69 | indent_size = 4 70 | 71 | # SQL (MySQL) - https://www.mysql.com/ 72 | [*.sql] 73 | indent_style = space 74 | indent_size = 2 75 | 76 | # Smarty 2 -http://www.smarty.net/docsv2/en/ 77 | [*.tpl] 78 | indent_style = space 79 | indent_size = 4 80 | 81 | # XHTML - http://www.w3.org/TR/xhtml1/ 82 | [*.html] 83 | indent_style = space 84 | indent_size = 2 85 | 86 | # YAML - http://yaml.org/ 87 | [{*.yml,*.yaml}] 88 | indent_style = space 89 | indent_size = 2 90 | 91 | # p(ixi)v -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | 8 | - package-ecosystem: 'npm' 9 | directory: '/' 10 | schedule: 11 | interval: 'weekly' 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | pull_request: 10 | branches: ["main"] 11 | schedule: 12 | - cron: "0 0 * * *" 13 | workflow_dispatch: 14 | 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | 23 | strategy: 24 | matrix: 25 | node-version: [20.x] 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Use Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | cache: "npm" 34 | 35 | - run: npm ci 36 | - run: npm run build 2> $GITHUB_WORKSPACE/build.stderr.log 37 | - run: npm run test 2> $GITHUB_WORKSPACE/test.stderr.log 38 | 39 | - run: | 40 | if grep "warn" $GITHUB_WORKSPACE/*.log; then 41 | echo "::warning:: Looks like tailwind build has unexpected warnings" 42 | exit 1 43 | fi 44 | 45 | automerge: 46 | runs-on: ubuntu-latest 47 | needs: build 48 | if: github.actor == 'dependabot[bot]' 49 | steps: 50 | - name: Dependabot metadata 51 | id: metadata 52 | uses: dependabot/fetch-metadata@v2 53 | with: 54 | github-token: "${{ secrets.GITHUB_TOKEN }}" 55 | - name: Enable auto-merge for Dependabot PRs 56 | run: gh pr merge --auto --merge "$PR_URL" 57 | env: 58 | PR_URL: ${{ github.event.pull_request.html_url }} 59 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "bradlc.vscode-tailwindcss", 5 | "github.vscode-github-actions" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 fsubal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 『Tailwind CSS実践入門』サンプルコード 2 | 3 | 『Tailwind CSS実践入門(著: 工藤智祥、技術評論社)』のサンプルコードが置かれたリポジトリです。 4 | 5 | -------------------------------------------------------------------------------- /VERSIONING_POLICY.md: -------------------------------------------------------------------------------- 1 | # バージョニングポリシー 2 | 3 | このリポジトリはSemVer(Semantic Versioning)に従います。おおむね次のポリシーに従います。 4 | 5 | - major: 版が変わった(本の内容自体が更新された) 6 | - minor: 使っている依存パッケージの更新と、それに伴うコードの変更 7 | - patch: バグ修正、誤字など 8 | 9 | ## バージョン更新の際には以下のことを行います 10 | 11 | - package.jsonのversionフィールドの更新 12 | - git tag 13 | - Releasesページに更新履歴の追加 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gihyo_tailwindcss_book", 3 | "version": "0.0.0", 4 | "description": "", 5 | "private": true, 6 | "workspaces": [ 7 | "src/*" 8 | ], 9 | "engines": { 10 | "node": "20" 11 | }, 12 | "scripts": { 13 | "build": "npm run build --workspaces --if-present", 14 | "test": "npm run test --workspaces --if-present" 15 | }, 16 | "license": "MIT", 17 | "author": "fsubal ", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/fsubal/gihyo_tailwindcss_book" 21 | }, 22 | "devDependencies": { 23 | "npm-run-all2": "latest", 24 | "prettier": "latest", 25 | "tailwindcss": "latest", 26 | "@tailwindcss/postcss": "latest", 27 | "@tailwindcss/cli": "latest" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/第3章_Tailwind_CSSを導入する/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require("gulp"); 2 | const postcss = require("gulp-postcss"); 3 | const tailwindcss = require("@tailwindcss/postcss"); 4 | const autoprefixer = require("autoprefixer"); 5 | 6 | gulp.task("tailwindcss:build", () => 7 | gulp 8 | .src("src/main.css") 9 | .pipe(postcss([tailwindcss, autoprefixer])) 10 | .pipe(gulp.dest("./dist")), 11 | ); 12 | -------------------------------------------------------------------------------- /src/第3章_Tailwind_CSSを導入する/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter3", 3 | "version": "0.0.0", 4 | "description": "第3章 Tailwind CSSを導入する", 5 | "scripts": { 6 | "build": "run-s --print-label build:*", 7 | "build:tailwind-cli": "tailwindcss --config tailwind.config.js --input src/main.css --output dist/main.css", 8 | "build:postcss-cli": "postcss src/main.css --output dist/main.css", 9 | "build:webpack": "webpack build --progress", 10 | "build:gulp": "gulp tailwindcss:build" 11 | }, 12 | "devDependencies": { 13 | "@babel/core": "^7.27.1", 14 | "@babel/preset-env": "^7.27.2", 15 | "autoprefixer": "^10.4.21", 16 | "css-loader": "^7.1.2", 17 | "gulp": "^5.0.1", 18 | "gulp-postcss": "^10.0.0", 19 | "mini-css-extract-plugin": "^2.9.2", 20 | "postcss": "^8.5.3", 21 | "postcss-cli": "^11.0.1", 22 | "postcss-loader": "^8.1.1", 23 | "webpack": "^5.99.8", 24 | "webpack-cli": "^6.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/第3章_Tailwind_CSSを導入する/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/第3章_Tailwind_CSSを導入する/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tailwind CSS 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/第3章_Tailwind_CSSを導入する/src/main.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | -------------------------------------------------------------------------------- /src/第3章_Tailwind_CSSを導入する/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | 4 | module.exports = { 5 | mode: "development", 6 | entry: { 7 | tailwindcss: path.resolve(__dirname, "src", "main.css"), 8 | }, 9 | output: { 10 | path: path.resolve(__dirname, "..", "..", "dist"), 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.css$/i, 16 | use: [ 17 | MiniCssExtractPlugin.loader, 18 | { loader: "css-loader" }, 19 | { 20 | loader: "postcss-loader", 21 | options: { 22 | postcssOptions: { 23 | plugins: [require("@tailwindcss/postcss")], 24 | }, 25 | }, 26 | }, 27 | ], 28 | }, 29 | ], 30 | }, 31 | plugins: [ 32 | new MiniCssExtractPlugin({ 33 | filename: "main.css", 34 | }), 35 | ], 36 | }; 37 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | ignorePatterns: ["dist", ".eslintrc.cjs"], 10 | parser: "@typescript-eslint/parser", 11 | plugins: ["react-refresh"], 12 | rules: { 13 | "react-refresh/only-export-components": [ 14 | "warn", 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter6", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@headlessui/react": "^2.2.4", 14 | "@radix-ui/react-slot": "^1.2.2", 15 | "@react-spring/web": "^10.0.1", 16 | "class-variance-authority": "^0.7.1", 17 | "classnames": "^2.5.1", 18 | "clsx": "^2.1.1", 19 | "framer-motion": "^12.15.0", 20 | "lucide-react": "^0.510.0", 21 | "react": "^19.0.0", 22 | "react-aria": "^3.37.0", 23 | "react-aria-components": "^1.9.0", 24 | "react-dom": "^19.1.0", 25 | "tailwind-merge": "^3.2.0", 26 | "tailwind-variants": "^1.0.0", 27 | "tailwindcss-animate": "^1.0.7" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "^22.15.21", 31 | "@types/react": "^19.1.2", 32 | "@types/react-dom": "^19.1.2", 33 | "@vitejs/plugin-react": "^4.5.0", 34 | "autoprefixer": "^10.4.21", 35 | "eslint": "^9.27.0", 36 | "eslint-plugin-react": "^7.37.5", 37 | "eslint-plugin-react-hooks": "^5.2.0", 38 | "eslint-plugin-react-refresh": "^0.4.20", 39 | "postcss": "^8.5.3", 40 | "tailwindcss": "insiders", 41 | "vite": "^6.3.5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MyDialog as HeadlessUIDialog } from "./components/headlessui/MyDialog"; 3 | import { Button } from "./components/ui/button"; 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardFooter, 9 | CardHeader, 10 | CardTitle, 11 | } from "./components/ui/card"; 12 | import { Dialog as ReactAriaDialog } from "./components/react-aria/Dialog"; 13 | import { DialogWithComponents } from "./components/react-aria/DialogWithComponent"; 14 | 15 | const Heading = ({ children }: React.PropsWithChildren) => ( 16 |

{children}

17 | ); 18 | 19 | function App() { 20 | return ( 21 |
22 |
23 | Using shadcn-ui 24 | 25 | 26 | Card Title 27 | Card Description 28 | 29 | 30 |

Card Content

31 | 32 |
33 | 34 |

Card Footer

35 |
36 |
37 |
38 | 39 |
40 | Using Headless UI 41 | 42 |
43 | 44 |
45 | Using React Aria 46 | AAAAAA 47 |
48 | 49 |
50 | Using React Aria Components 51 | 52 | AAAAAA 53 | 54 |
55 |
56 | ); 57 | } 58 | 59 | export default App; 60 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/src/components/framer-motion/AnimatedList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useAnimate } from "framer-motion"; 3 | import { animate } from "framer-motion/dom"; 4 | 5 | export function AnimatedList({ children }: React.PropsWithChildren) { 6 | const [scope, animate] = useAnimate(); 7 | 8 | useEffect(() => { 9 | animate("li", { opacity: 1 }, { duration: 1 }); 10 | }); 11 | 12 | return ; 13 | } 14 | 15 | export function fadeout() { 16 | const box = document.getElementById("box"); 17 | if (box) { 18 | animate(box, { opacity: 0 }, { duration: 0.5 }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/src/components/headlessui/MyDialog.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from "react"; 2 | import classNames from "classnames"; 3 | import { Dialog } from "@headlessui/react"; 4 | 5 | interface Props { 6 | title: string; 7 | description: string; 8 | okLabel?: string; 9 | cancelLabel?: string; 10 | } 11 | 12 | export function MyDialog({ 13 | title, 14 | description, 15 | okLabel = "OK", 16 | cancelLabel = "キャンセル", 17 | }: Props) { 18 | const [isOpen, setIsOpen] = useState(true); 19 | const onClose = useCallback(() => setIsOpen(false), []); 20 | 21 | return ( 22 | 27 | {/* 背景(バックドロップ) */} 28 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/src/components/headlessui/Transition.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from "react"; 2 | import { Transition } from "@headlessui/react"; 3 | 4 | export function MyComponent() { 5 | const [isShowing, setIsShowing] = useState(false); 6 | const onToggle = useCallback(() => { 7 | setIsShowing((prev) => !prev); 8 | }, []); 9 | 10 | return ( 11 | <> 12 | 13 | 22 | フェードインする 23 | 24 | 25 | ); 26 | } 27 | 28 | interface SidebarProps { 29 | isShowing?: boolean; 30 | } 31 | 32 | export function Sidebar({ isShowing }: SidebarProps) { 33 | return ( 34 | 35 | {/* 背景の暗い部分はフェードする */} 36 | 44 | {/* ... */} 45 | 46 | 47 | {/* サイドバーの本体はスライドする */} 48 | 56 | {/* ... */} 57 | 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/src/components/react-aria/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | import { useDialog, AriaDialogProps } from "react-aria"; 3 | 4 | interface Props extends AriaDialogProps { 5 | title?: React.ReactNode; 6 | children: React.ReactNode; 7 | } 8 | 9 | export function Dialog({ title, children, ...props }: Props) { 10 | const ref = useRef(null); 11 | const { dialogProps, titleProps } = useDialog(props, ref); 12 | 13 | return ( 14 |
15 | {title && ( 16 |

17 | {title} 18 |

19 | )} 20 | {children} 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/src/components/react-aria/DialogWithComponent.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Dialog, DialogTrigger, Modal } from "react-aria-components"; 2 | 3 | interface Props { 4 | title?: React.ReactNode; 5 | children: React.ReactNode; 6 | } 7 | 8 | export function DialogWithComponents({ title, children }: Props) { 9 | return ( 10 | 11 | 12 | 13 | 14 | {({ close }) => ( 15 |
16 |

{title}

17 | {children} 18 | 19 |
20 | )} 21 |
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/src/components/react-aria/Popover.tsx: -------------------------------------------------------------------------------- 1 | import { Popover } from "react-aria-components"; 2 | 3 | interface Props {} 4 | 5 | export function PopoverAnimation({}: Props) { 6 | return ( 7 | 8 | It's popover 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/src/components/react-spring/FadeIn.tsx: -------------------------------------------------------------------------------- 1 | import { useSpring, animated } from "@react-spring/web"; 2 | 3 | export function FadeIn() { 4 | const [props] = useSpring( 5 | () => ({ 6 | from: { opacity: 0 }, 7 | to: { opacity: 1 }, 8 | }), 9 | [], 10 | ); 11 | 12 | return Hello World; 13 | } 14 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/src/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 rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/src/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 |

44 | )); 45 | CardTitle.displayName = "CardTitle"; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )); 57 | CardDescription.displayName = "CardDescription"; 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )); 65 | CardContent.displayName = "CardContent"; 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardFooter.displayName = "CardFooter"; 78 | 79 | export { 80 | Card, 81 | CardHeader, 82 | CardFooter, 83 | CardTitle, 84 | CardDescription, 85 | CardContent, 86 | }; 87 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @config "../tailwind.config.js"; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | 78 | @keyframes fadeIn { 79 | 0% { 80 | } 81 | 100% { 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | './pages/**/*.{ts,tsx}', 6 | './components/**/*.{ts,tsx}', 7 | './app/**/*.{ts,tsx}', 8 | './src/**/*.{ts,tsx}', 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: "2rem", 14 | screens: { 15 | "2xl": "1400px", 16 | }, 17 | }, 18 | extend: { 19 | colors: { 20 | border: "hsl(var(--border))", 21 | input: "hsl(var(--input))", 22 | ring: "hsl(var(--ring))", 23 | background: "hsl(var(--background))", 24 | foreground: "hsl(var(--foreground))", 25 | primary: { 26 | DEFAULT: "hsl(var(--primary))", 27 | foreground: "hsl(var(--primary-foreground))", 28 | }, 29 | secondary: { 30 | DEFAULT: "hsl(var(--secondary))", 31 | foreground: "hsl(var(--secondary-foreground))", 32 | }, 33 | destructive: { 34 | DEFAULT: "hsl(var(--destructive))", 35 | foreground: "hsl(var(--destructive-foreground))", 36 | }, 37 | muted: { 38 | DEFAULT: "hsl(var(--muted))", 39 | foreground: "hsl(var(--muted-foreground))", 40 | }, 41 | accent: { 42 | DEFAULT: "hsl(var(--accent))", 43 | foreground: "hsl(var(--accent-foreground))", 44 | }, 45 | popover: { 46 | DEFAULT: "hsl(var(--popover))", 47 | foreground: "hsl(var(--popover-foreground))", 48 | }, 49 | card: { 50 | DEFAULT: "hsl(var(--card))", 51 | foreground: "hsl(var(--card-foreground))", 52 | }, 53 | }, 54 | borderRadius: { 55 | lg: "var(--radius)", 56 | md: "calc(var(--radius) - 2px)", 57 | sm: "calc(var(--radius) - 4px)", 58 | }, 59 | keyframes: { 60 | "accordion-down": { 61 | from: { height: 0 }, 62 | to: { height: "var(--radix-accordion-content-height)" }, 63 | }, 64 | "accordion-up": { 65 | from: { height: "var(--radix-accordion-content-height)" }, 66 | to: { height: 0 }, 67 | }, 68 | }, 69 | animation: { 70 | "accordion-down": "accordion-down 0.2s ease-out", 71 | "accordion-up": "accordion-up 0.2s ease-out", 72 | }, 73 | }, 74 | }, 75 | plugins: [require("tailwindcss-animate")], 76 | } -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* For shadcn-ui */ 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": ["./src/*"] 13 | }, 14 | 15 | /* Bundler mode */ 16 | "moduleResolution": "bundler", 17 | "allowImportingTsExtensions": true, 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | 23 | /* Linting */ 24 | "strict": true, 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": true, 27 | "noFallthroughCasesInSwitch": true 28 | }, 29 | "include": ["src"], 30 | "references": [{ "path": "./tsconfig.node.json" }] 31 | } 32 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/第6章_Tailwind_CSSでコンポーネントを設計する/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { defineConfig } from "vite"; 3 | import react from "@vitejs/plugin-react"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | resolve: { 9 | alias: { 10 | "@": path.resolve(__dirname, "./src"), 11 | }, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/第7章_Tailwind_CSSをカスタマイズする/7_3_JavaScript_APIを利用する/extractClassNames.js: -------------------------------------------------------------------------------- 1 | import tailwindcss from "@tailwindcss/postcss"; 2 | import postcss from "postcss"; 3 | import postcssSelectorParser from "postcss-selector-parser"; 4 | 5 | /** 6 | * @deprecated safelistプロパティはv4でサポートされなくなりました。この方法でのテストはv3までしか動きません。 7 | * 8 | * @param {string} input 9 | * @param {Partial} preset 10 | * @return {Promise} 11 | */ 12 | export async function extractClassNames(input, preset) { 13 | const plugin = tailwindcss({ 14 | presets: [preset], 15 | content: [ 16 | // contentを空にするとwarningが出るので適当なものを入れている 17 | // 別に真似する必要はない 18 | { raw: '', extension: 'html' } 19 | ], 20 | safelist: [ 21 | { 22 | // 一切purgeしない 23 | pattern: /./u, 24 | 25 | // ここに画面幅などを含むことを明示してもよい 26 | variants: [], 27 | }, 28 | ], 29 | }); 30 | 31 | const result = await postcss([plugin]).process(input, { 32 | // ビルド元のファイルなし 33 | from: undefined, 34 | 35 | // ビルド先のファイルなし 36 | to: undefined, 37 | 38 | // sourcemapなし 39 | map: false, 40 | }); 41 | 42 | // CSSをパースして構文木を走査し、セレクタからクラス名を得る 43 | // 実装にはpostcss-selector-parserなどのライブラリを用いる 44 | return getAllClassNames(result); 45 | } 46 | 47 | const selectorParser = postcssSelectorParser(); 48 | 49 | /** 50 | * @param {import('postcss').Result} result 51 | * @return {string[]} 52 | */ 53 | function getAllClassNames(result) { 54 | /** @type {Set} */ 55 | const classNames = new Set(); 56 | 57 | result.root.walkRules((rule) => { 58 | // たとえば`h1, .hoge, .fuga::before { ... }`みたいなruleがあるとする 59 | // これをパースして ['h1', '.hoge', '.fuga::before'] のような配列(実際にはそれを表すオブジェクトの配列)を得たい 60 | const { nodes } = selectorParser.astSync(rule.selector); 61 | 62 | // クラス一覧を得たい(それ以外の、h1とかのセレクタは無視したい) 63 | // ので、クラスだけをSetに追加していく 64 | nodes.forEach((childNode) => 65 | childNode.nodes.forEach((node) => { 66 | if (node.type === "class") { 67 | const className = node.value.trim(); 68 | classNames.add(className); 69 | } 70 | }), 71 | ); 72 | }); 73 | 74 | // 同じクラスが除外されてほしいのでSetを使っていたが、利用側にはArrayを返す 75 | return Array.from(classNames); 76 | } 77 | -------------------------------------------------------------------------------- /src/第7章_Tailwind_CSSをカスタマイズする/7_3_JavaScript_APIを利用する/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tailwind CSS 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/第7章_Tailwind_CSSをカスタマイズする/7_3_JavaScript_APIを利用する/jest/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('jest').Config} */ 2 | module.exports = { 3 | transform: { 4 | // NOTICE: この設定は真似しなくても良いです 5 | // babelの設定はこのリポジトリの他の部分にとっては不要なものであり、 6 | // あくまで「src/第7章_Tailwind_CSSをカスタマイズする/7_3_JavaScript_APIを利用する/jest」の中だけで利用したいという理由から 7 | // .babelrc相当の内容をjest.config.jsで書いているだけです 8 | // ふつうにjest+babelを組み合わせて利用する場合は公式ドキュメントを見てください 9 | // @see https://jestjs.io/ja/docs/getting-started 10 | "\\.js$": [ 11 | "babel-jest", 12 | { presets: [["@babel/preset-env", { targets: { node: "current" } }]] }, 13 | ], 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/第7章_Tailwind_CSSをカスタマイズする/7_3_JavaScript_APIを利用する/jest/preset.test.js: -------------------------------------------------------------------------------- 1 | import { extractClassNames } from "../extractClassNames"; 2 | 3 | const input = '@import "tailwindcss";'; 4 | 5 | const preset = { 6 | theme: {}, 7 | plugins: [], 8 | }; 9 | 10 | const SUPER_DEPRECATED_NO_LONGER_SUPPORTED = describe.skip 11 | 12 | /** 13 | * このスナップショットテストはv4以上で動きません。 14 | * 当時のスナップショットがどういう内容だったかを見たい場合は以下。 15 | * 16 | * @see https://github.com/fsubal/gihyo_tailwindcss_book/tree/e5e1d1fe0bbb017ba3a3d177397bce46de47ae08/src/%E7%AC%AC7%E7%AB%A0_Tailwind_CSS%E3%82%92%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA%E3%81%99%E3%82%8B/7_3_JavaScript_API%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B 17 | */ 18 | SUPER_DEPRECATED_NO_LONGER_SUPPORTED('tailwind.config.jsに対するスナップショットはv4で動かなくなりました', () => { 19 | describe("tailwind.config.js", () => { 20 | test("outputs", async () => { 21 | /** @type {string[]} */ 22 | const classNames = await extractClassNames(input, preset); 23 | 24 | expect(classNames).toMatchSnapshot(); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/第7章_Tailwind_CSSをカスタマイズする/7_3_JavaScript_APIを利用する/my-preset.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @type {Partial} 5 | */ 6 | const preset = { 7 | theme: { 8 | colors: { 9 | blue: { 10 | light: "#85d7ff", 11 | DEFAULT: "#1fb6ff", 12 | dark: "#009eeb", 13 | // ... 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | }; 19 | 20 | module.exports = preset; 21 | -------------------------------------------------------------------------------- /src/第7章_Tailwind_CSSをカスタマイズする/7_3_JavaScript_APIを利用する/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: { 4 | // NOTICE: ここでrelative: trueを指定しているのは、 5 | // プロジェクトルートにないtailwind.config.jsを使っているからです(説明の都合上フォルダを節ごとに分けております) 6 | // tailwind.config.jsをふつうにプロジェクトルートに置いてるケースでは特に真似する必要はありません 7 | relative: true, 8 | files: ["./index.html"], 9 | }, 10 | theme: { 11 | extend: {}, 12 | }, 13 | plugins: [], 14 | }; 15 | -------------------------------------------------------------------------------- /src/第7章_Tailwind_CSSをカスタマイズする/7_3_JavaScript_APIを利用する/vitest/preset.test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { describe, test, expect } from "vitest"; 4 | import { extractClassNames } from "../extractClassNames"; 5 | 6 | const input = '@import "tailwindcss";'; 7 | 8 | const preset = { 9 | theme: {}, 10 | plugins: [], 11 | }; 12 | 13 | const SUPER_DEPRECATED_NO_LONGER_SUPPORTED = describe.skip 14 | 15 | /** 16 | * このスナップショットテストはv4以上で動きません。 17 | * 当時のスナップショットがどういう内容だったかを見たい場合は以下。 18 | * 19 | * @see https://github.com/fsubal/gihyo_tailwindcss_book/tree/e5e1d1fe0bbb017ba3a3d177397bce46de47ae08/src/%E7%AC%AC7%E7%AB%A0_Tailwind_CSS%E3%82%92%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA%E3%81%99%E3%82%8B/7_3_JavaScript_API%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B 20 | */ 21 | SUPER_DEPRECATED_NO_LONGER_SUPPORTED('tailwind.config.jsに対するスナップショットはv4で動かなくなりました', () => { 22 | describe("tailwind.config.js", () => { 23 | test("outputs", async () => { 24 | /** @type {string[]} */ 25 | const classNames = await extractClassNames(input, preset); 26 | 27 | expect(classNames).toMatchSnapshot(); 28 | }); 29 | }); 30 | }); 31 | 32 | -------------------------------------------------------------------------------- /src/第7章_Tailwind_CSSをカスタマイズする/7_4_独自のプラグインを作成する/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tailwind CSS 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/第7章_Tailwind_CSSをカスタマイズする/7_4_独自のプラグインを作成する/main.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @config "./tailwind.config.js"; 4 | -------------------------------------------------------------------------------- /src/第7章_Tailwind_CSSをカスタマイズする/7_4_独自のプラグインを作成する/plugins/cssVariablePlugin.js: -------------------------------------------------------------------------------- 1 | import plugin from "tailwindcss/plugin"; 2 | 3 | export const defineVarNameInGlobalCss = plugin.withOptions( 4 | /** 5 | * @param {Record<'light' | 'dark', Record>} theme 6 | */ 7 | ({ light, dark }) => 8 | ({ addBase }) => { 9 | addBase({ 10 | ":root": toCssVariables(light), 11 | "@media (prefers-color-scheme: dark)": { 12 | ":root": toCssVariables(dark), 13 | }, 14 | }); 15 | }, 16 | ); 17 | 18 | /** 19 | * @param {Record} theme 20 | */ 21 | export function generateVarNames(theme) { 22 | return Object.fromEntries( 23 | Object.keys(theme).map((name) => [name, `var(--color-${name})`]), 24 | ); 25 | } 26 | 27 | /** 28 | * @param {Record} theme 29 | */ 30 | function toCssVariables(theme) { 31 | return Object.fromEntries( 32 | Object.entries(theme).map(([name, value]) => [`--color-${name}`, value]), 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/第7章_Tailwind_CSSをカスタマイズする/7_4_独自のプラグインを作成する/plugins/langPlugin.js: -------------------------------------------------------------------------------- 1 | import plugin from "tailwindcss/plugin"; 2 | 3 | export const langPlugin = plugin( 4 | ({ matchVariant, theme }) => 5 | matchVariant("lang", (value) => `&:lang(${value})`, { 6 | values: theme("lang"), 7 | }), 8 | { 9 | theme: { 10 | lang: { 11 | ja: "ja", 12 | en: "en", 13 | }, 14 | }, 15 | }, 16 | ); 17 | -------------------------------------------------------------------------------- /src/第7章_Tailwind_CSSをカスタマイズする/7_4_独自のプラグインを作成する/plugins/trimmedTextPlugin.js: -------------------------------------------------------------------------------- 1 | import plugin from "tailwindcss/plugin"; 2 | 3 | const TEXT_SIZE = { 4 | 12: { fontSize: 12, lineHeight: 20 }, 5 | 14: { fontSize: 14, lineHeight: 20 }, 6 | 16: { fontSize: 16, lineHeight: 24 }, 7 | 20: { fontSize: 20, lineHeight: 28 }, 8 | }; 9 | 10 | const leadingCancel = { 11 | display: "block", 12 | width: 0, 13 | height: 0, 14 | content: '""', 15 | }; 16 | 17 | export const trimmedTextPlugin = plugin(({ addComponents, addUtilities }) => { 18 | const trimmedTextClasses = Object.fromEntries( 19 | Object.entries(TEXT_SIZE).map(([name, { fontSize, lineHeight }]) => { 20 | const margin = (lineHeight - fontSize) / 2; 21 | 22 | return [ 23 | `.trimmed-text-${name}`, 24 | { 25 | "font-size": `${fontSize}px`, 26 | "line-height": `${lineHeight}px`, 27 | "&::before": { 28 | ...leadingCancel, 29 | marginTop: `${-margin}px`, 30 | }, 31 | "&::after": { 32 | ...leadingCancel, 33 | marginBottom: `${-margin}px`, 34 | }, 35 | }, 36 | ]; 37 | }), 38 | ); 39 | 40 | addComponents(trimmedTextClasses); 41 | 42 | addUtilities({ 43 | ".no-trim": { 44 | "&::before": { 45 | content: "none", 46 | }, 47 | "&::after": { 48 | content: "none", 49 | }, 50 | }, 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/第7章_Tailwind_CSSをカスタマイズする/7_4_独自のプラグインを作成する/tailwind.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { trimmedTextPlugin } = require("./plugins/trimmedTextPlugin"); 3 | 4 | const { 5 | defineVarNameInGlobalCss, 6 | generateVarNames, 7 | } = require("./plugins/cssVariablePlugin"); 8 | 9 | const { langPlugin } = require("./plugins/langPlugin"); 10 | 11 | const myColorTheme = require("./theme/myColorTheme"); 12 | 13 | /** @type {import('tailwindcss').Config} */ 14 | module.exports = { 15 | content: { 16 | // NOTICE: ここでrelative: trueを指定しているのは、 17 | // プロジェクトルートにないtailwind.config.jsを使っているからです(説明の都合上フォルダを章ごとに分けております) 18 | // tailwind.config.jsをふつうにプロジェクトルートに置いてるケースでは特に真似する必要はありません 19 | relative: true, 20 | files: ["./index.html"], 21 | }, 22 | theme: { 23 | colors: generateVarNames(myColorTheme.light), 24 | extend: {}, 25 | }, 26 | plugins: [ 27 | trimmedTextPlugin, 28 | defineVarNameInGlobalCss(myColorTheme), 29 | langPlugin, 30 | ], 31 | }; 32 | -------------------------------------------------------------------------------- /src/第7章_Tailwind_CSSをカスタマイズする/7_4_独自のプラグインを作成する/theme/myColorTheme.js: -------------------------------------------------------------------------------- 1 | const colors = { 2 | blue_400: "#60a5fa", 3 | blue_500: "#3B82F6", 4 | }; 5 | 6 | module.exports = { 7 | light: { 8 | primary: colors.blue_400, 9 | }, 10 | dark: { 11 | primary: colors.blue_500, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/第7章_Tailwind_CSSをカスタマイズする/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter7", 3 | "version": "0.0.0", 4 | "description": "第7章 Tailwind CSSをカスタマイズする", 5 | "scripts": { 6 | "build": "run-s --print-label build:*", 7 | "test": "run-s --print-label test:*", 8 | "build:plugins": "tailwindcss --config 7_4_独自のプラグインを作成する/tailwind.config.js --input 7_4_独自のプラグインを作成する/main.css --output dist/main.css", 9 | "test:jest": "jest 7_3_JavaScript_APIを利用する/jest/preset.test.js --config 7_3_JavaScript_APIを利用する/jest/jest.config.js", 10 | "test:vitest": "vitest run 7_3_JavaScript_APIを利用する/vitest/preset.test.js" 11 | }, 12 | "devDependencies": { 13 | "jest": "^29.7.0", 14 | "postcss": "^8.5.3", 15 | "postcss-selector-parser": "^7.1.0", 16 | "tailwindcss": "insiders", 17 | "vitest": "^3.1.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/第8章_Tailwind_CSSを既存のプロジェクトに導入する/less/ng.less: -------------------------------------------------------------------------------- 1 | .card { 2 | @apply rounded-none; 3 | 4 | @screen sm { 5 | @apply rounded-lg; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/第8章_Tailwind_CSSを既存のプロジェクトに導入する/less/ok.less: -------------------------------------------------------------------------------- 1 | .card { 2 | @apply rounded-none; 3 | } 4 | 5 | @screen sm { 6 | .card { 7 | @apply rounded-lg; 8 | } 9 | } 10 | 11 | .card { 12 | @apply rounded-none; 13 | 14 | @media (min-width: theme(--breakpoint-sm)) { 15 | @apply rounded-lg; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/第8章_Tailwind_CSSを既存のプロジェクトに導入する/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter8", 3 | "version": "0.0.0", 4 | "description": "第8章 Tailwind CSSを既存のプロジェクトに導入する", 5 | "scripts": { 6 | "build": "run-s --print-label build:*", 7 | "build:chapter8": "./run.sh" 8 | }, 9 | "devDependencies": { 10 | "less": "^4.3.0", 11 | "sass": "^1.89.0", 12 | "stylus": "^0.64.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/第8章_Tailwind_CSSを既存のプロジェクトに導入する/run.sh: -------------------------------------------------------------------------------- 1 | npx sass sass/ok.scss 2 | ! npx sass sass/ng.scss # コンパイルエラーになって欲しい 3 | 4 | npx lessc less/ok.less 5 | npx lessc less/ng.less # 意図しない出力になるが、コンパイルエラーにはならない 6 | 7 | npx stylus stylus/ok.styl --print 8 | npx stylus stylus/ng.styl --print # 意図しない出力になるが、コンパイルエラーにはならない 9 | -------------------------------------------------------------------------------- /src/第8章_Tailwind_CSSを既存のプロジェクトに導入する/sass/ng.scss: -------------------------------------------------------------------------------- 1 | .alert { 2 | /** ◎ 動作する */ 3 | @apply bg-red-500 !important; 4 | } 5 | -------------------------------------------------------------------------------- /src/第8章_Tailwind_CSSを既存のプロジェクトに導入する/sass/ok.scss: -------------------------------------------------------------------------------- 1 | .alert { 2 | /** ◎ 動作する */ 3 | @apply bg-red-500 #{!important}; 4 | } 5 | -------------------------------------------------------------------------------- /src/第8章_Tailwind_CSSを既存のプロジェクトに導入する/stylus/ng.styl: -------------------------------------------------------------------------------- 1 | .card 2 | @apply rounded-lg bg-white p-4 -------------------------------------------------------------------------------- /src/第8章_Tailwind_CSSを既存のプロジェクトに導入する/stylus/ok.styl: -------------------------------------------------------------------------------- 1 | @css { 2 | .card { 3 | @apply rounded-lg bg-white p-4; 4 | } 5 | } --------------------------------------------------------------------------------