├── .changeset ├── README.md ├── config.json ├── happy-brooms-clean.md ├── healthy-tables-tap.md ├── hot-cheetahs-smell.md ├── khaki-kings-sneeze.md ├── little-spies-decide.md ├── lucky-zoos-lay.md ├── pretty-sheep-protect.md ├── proud-bugs-stare.md ├── real-tigers-worry.md ├── rich-dodos-cheer.md ├── short-needles-remember.md ├── silver-books-grin.md ├── ten-balloons-sell.md └── thirty-tools-reply.md ├── .coderabbit.yaml ├── .eslintignore ├── .eslintrc.cjs ├── .github ├── assets │ ├── fui_logo_logotype_dark.svg │ └── fui_logo_logotype_light.svg ├── pull_request_template.md └── workflows │ ├── changeset.yml │ ├── ci.yml │ ├── example-ci.yml │ └── typos.yml ├── .gitignore ├── .ignore ├── .npmignore ├── .prettierignore ├── .prettierrc.cjs ├── .vscode └── extensions.json ├── README.md ├── _typos.toml ├── example ├── nextjs-app │ ├── .vscode │ │ └── settings.json │ ├── CHANGELOG.md │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ └── index.tsx │ ├── postcss.config.js │ ├── styles │ │ ├── forty.css │ │ └── global.css │ ├── tailwind.config.js │ └── tsconfig.json └── vite-app │ ├── CHANGELOG.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.cjs │ ├── public │ ├── images │ │ └── login │ │ │ ├── google.png │ │ │ └── google.svg │ └── vite.svg │ ├── src │ ├── App.tsx │ ├── main.tsx │ ├── pages │ │ └── index.tsx │ ├── router.tsx │ ├── styles │ │ ├── forty.css │ │ └── global.css │ └── vite-env.d.ts │ ├── tailwind.config.cjs │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── tsconfig.paths.json │ └── vite.config.ts ├── package-lock.json ├── package.json ├── packages ├── eslint-config │ ├── CHANGELOG.md │ ├── package.json │ └── src │ │ ├── index.cjs │ │ └── react.cjs ├── for-ui │ ├── .storybook │ │ ├── main.cjs │ │ ├── manager.js │ │ ├── preview-head.html │ │ └── preview.tsx │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── image.png │ │ ├── logo.png │ │ └── transparent.svg │ ├── react-table-index.js │ ├── src │ │ ├── badge │ │ │ ├── Badge.stories.tsx │ │ │ ├── Badge.test.tsx │ │ │ ├── Badge.tsx │ │ │ ├── ConstantBadge.tsx │ │ │ ├── OutlinedBadge.tsx │ │ │ ├── TextBadge.tsx │ │ │ └── index.tsx │ │ ├── button │ │ │ ├── Button.stories.tsx │ │ │ ├── Button.test.tsx │ │ │ ├── Button.tsx │ │ │ └── index.tsx │ │ ├── callout │ │ │ ├── Callout.stories.tsx │ │ │ ├── Callout.test.tsx │ │ │ ├── Callout.tsx │ │ │ └── index.ts │ │ ├── checkbox │ │ │ ├── Checkbox.stories.tsx │ │ │ ├── Checkbox.test.tsx │ │ │ ├── Checkbox.tsx │ │ │ ├── CheckboxGroup.tsx │ │ │ └── index.tsx │ │ ├── chip │ │ │ ├── Chip.stories.tsx │ │ │ ├── Chip.tsx │ │ │ ├── FullChip.tsx │ │ │ ├── LimitedChip.tsx │ │ │ └── index.tsx │ │ ├── drawer │ │ │ ├── Drawer.stories.tsx │ │ │ ├── Drawer.tsx │ │ │ └── index.tsx │ │ ├── dropzone │ │ │ ├── Dropzone.stories.tsx │ │ │ ├── Dropzone.tsx │ │ │ └── index.tsx │ │ ├── index.ts │ │ ├── loader │ │ │ ├── Loader.tsx │ │ │ ├── Loading.tsx │ │ │ └── index.tsx │ │ ├── menu │ │ │ ├── Menu.stories.tsx │ │ │ ├── Menu.tsx │ │ │ ├── MenuDivider.stories.tsx │ │ │ ├── MenuDivider.tsx │ │ │ ├── MenuItem.stories.tsx │ │ │ ├── MenuItem.tsx │ │ │ ├── MenuList.tsx │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── modal │ │ │ ├── Modal.stories.tsx │ │ │ ├── Modal.tsx │ │ │ ├── ModalContent.tsx │ │ │ ├── ModalFooter.tsx │ │ │ ├── ModalHeader.tsx │ │ │ └── index.tsx │ │ ├── popper │ │ │ ├── Popper.stories.tsx │ │ │ ├── Popper.tsx │ │ │ └── index.tsx │ │ ├── radio │ │ │ ├── Radio.stories.tsx │ │ │ ├── Radio.tsx │ │ │ ├── RadioGroup.tsx │ │ │ └── index.tsx │ │ ├── select │ │ │ ├── Select.stories.tsx │ │ │ ├── Select.tsx │ │ │ └── index.tsx │ │ ├── skeleton │ │ │ ├── Skeleton.stories.tsx │ │ │ ├── Skeleton.tsx │ │ │ └── index.tsx │ │ ├── snackbar │ │ │ ├── Snackbar.stories.tsx │ │ │ ├── Snackbar.test.tsx │ │ │ ├── Snackbar.tsx │ │ │ ├── SnackbarContext.tsx │ │ │ └── index.tsx │ │ ├── stepper │ │ │ ├── Step.tsx │ │ │ ├── Stepper.stories.tsx │ │ │ ├── Stepper.tsx │ │ │ └── index.tsx │ │ ├── switch │ │ │ ├── Switch.stories.tsx │ │ │ ├── Switch.tsx │ │ │ ├── SwitchGroup.tsx │ │ │ └── index.tsx │ │ ├── system │ │ │ ├── PropsCascader.tsx │ │ │ ├── TextDefaultStyler.tsx │ │ │ ├── componentType.ts │ │ │ ├── fsx.ts │ │ │ ├── preservedOmit.ts │ │ │ └── walkChildren.ts │ │ ├── table │ │ │ ├── ColumnDef.ts │ │ │ ├── Table.stories.tsx │ │ │ ├── Table.tsx │ │ │ ├── TableCell.tsx │ │ │ ├── TablePagination.test.tsx │ │ │ ├── TablePagination.tsx │ │ │ ├── TableScroller.tsx │ │ │ └── index.tsx │ │ ├── tabs │ │ │ ├── Tab.stories.tsx │ │ │ ├── Tab.tsx │ │ │ ├── TabContext.tsx │ │ │ ├── TabList.tsx │ │ │ ├── TabPanel.tsx │ │ │ ├── Tabs.stories.tsx │ │ │ ├── Tabs.tsx │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── testing │ │ │ └── rhf.tsx │ │ ├── text │ │ │ ├── Text.stories.mdx │ │ │ ├── Text.stories.tsx │ │ │ ├── Text.tsx │ │ │ └── index.tsx │ │ ├── textArea │ │ │ ├── TextArea.stories.tsx │ │ │ ├── TextArea.test.tsx │ │ │ ├── TextArea.tsx │ │ │ └── index.tsx │ │ ├── textField │ │ │ ├── TextField.stories.tsx │ │ │ ├── TextField.test.tsx │ │ │ ├── TextField.tsx │ │ │ └── index.tsx │ │ ├── tooltip │ │ │ ├── Tooltip.stories.tsx │ │ │ ├── Tooltip.test.tsx │ │ │ ├── Tooltip.tsx │ │ │ └── index.ts │ │ └── utils │ │ │ ├── makeData.ts │ │ │ └── prepareForSlot.tsx │ ├── styles │ │ ├── forty.css │ │ ├── global.css │ │ ├── tailwind.v1.css │ │ └── tailwind.v2.css │ ├── tailwind.config.base.js │ ├── tailwind.config.js │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── types │ │ ├── react-table-config.d.ts │ │ └── react-table.d.ts │ ├── vite.config.ts │ ├── vitest.config.ts │ └── vitest.setup.ts └── prettier-config │ ├── CHANGELOG.md │ ├── package.json │ └── src │ └── index.cjs ├── renovate.json └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "4-design/for-ui" }], 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.changeset/happy-brooms-clean.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@4design/for-ui": patch 3 | --- 4 | 5 | chore(deps): update dependency jsdom to v24 6 | -------------------------------------------------------------------------------- /.changeset/healthy-tables-tap.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@4design/for-ui": patch 3 | --- 4 | 5 | fix(deps): update dependency react-number-format to v5.3.4 6 | -------------------------------------------------------------------------------- /.changeset/hot-cheetahs-smell.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@4design/for-ui": patch 3 | --- 4 | 5 | chore(deps): update dependency @tanstack/react-table to v8.15.0 6 | -------------------------------------------------------------------------------- /.changeset/khaki-kings-sneeze.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@4design/for-ui": patch 3 | --- 4 | 5 | chore(deps): update dependency autoprefixer to v10.4.19 6 | -------------------------------------------------------------------------------- /.changeset/little-spies-decide.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@4design/for-ui": patch 3 | --- 4 | 5 | chore(deps): update dependency npm-run-all2 to v5.0.2 6 | -------------------------------------------------------------------------------- /.changeset/lucky-zoos-lay.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@4design/prettier-config": patch 3 | --- 4 | 5 | chore(deps): update dependency prettier to v3.2.5 6 | -------------------------------------------------------------------------------- /.changeset/pretty-sheep-protect.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@4design/eslint-config": patch 3 | --- 4 | 5 | chore(deps): update dependency eslint-plugin-mdx to v3 6 | -------------------------------------------------------------------------------- /.changeset/proud-bugs-stare.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@4design/for-ui": patch 3 | --- 4 | 5 | fix(TextField): Mobile SafariでTextField type=dateの高さが潰れてしまうのを修正 6 | -------------------------------------------------------------------------------- /.changeset/real-tigers-worry.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@4design/for-ui": patch 3 | --- 4 | 5 | chore(deps): replace dependency npm-run-all with npm-run-all2 5.0.0 6 | -------------------------------------------------------------------------------- /.changeset/rich-dodos-cheer.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@4design/for-ui": patch 3 | --- 4 | 5 | chore(deps): update dependency npm-run-all2 to v6 6 | -------------------------------------------------------------------------------- /.changeset/short-needles-remember.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@4design/eslint-config": patch 3 | --- 4 | 5 | chore(deps): update dependency eslint-plugin-react to v7.34.1 6 | -------------------------------------------------------------------------------- /.changeset/silver-books-grin.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@4design/for-ui": patch 3 | --- 4 | 5 | fix(deps): update dependency clsx to v2.1.0 6 | -------------------------------------------------------------------------------- /.changeset/ten-balloons-sell.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@4design/for-ui": patch 3 | --- 4 | 5 | chore(deps): update dependency tailwindcss to v3.4.3 6 | -------------------------------------------------------------------------------- /.changeset/thirty-tools-reply.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@4design/for-ui": patch 3 | --- 4 | 5 | fix(Menu): MenuItemの高さが小さい画面で崩れるのを修正 6 | -------------------------------------------------------------------------------- /.coderabbit.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json 2 | language: "ja" 3 | early_access: true 4 | 5 | reviews: 6 | profile: "chill" 7 | request_changes_workflow: false 8 | high_level_summary: true 9 | poem: true 10 | review_status: true 11 | collapse_walkthrough: false 12 | auto_review: 13 | enabled: true 14 | drafts: false 15 | chat: 16 | auto_reply: true 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | types 4 | tailwind.config.base.js 5 | .github 6 | **/packages/**/tailwind.config.base.js 7 | **/packages/**/tailwind.config.js 8 | **/types 9 | **/.next/* 10 | **/.yalc/* 11 | .next/** 12 | .yalc/** 13 | .changeset 14 | storybook-static 15 | .coderabbit.yaml 16 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@4design', '@4design/eslint-config/react'], 4 | env: { 5 | node: true, 6 | }, 7 | settings: { 8 | tailwindcss: { 9 | // NOTE: eslint の設定はデフォルトに合わせる 10 | config: 'packages/for-ui/tailwindcss.config.js', 11 | callees: ['fsx'], 12 | }, 13 | }, 14 | rules: {}, 15 | }; 16 | -------------------------------------------------------------------------------- /.github/assets/fui_logo_logotype_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/assets/fui_logo_logotype_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## チケット 2 | 3 | - 4 | 5 | ## 実装内容 6 | 7 | - 8 | 9 | ## スクリーンショット 10 | 11 | | 変更前 | 変更後 | 12 | | ------ | ------ | 13 | | | | 14 | 15 | ## 相談内容(あれば) 16 | 17 | - 18 | -------------------------------------------------------------------------------- /.github/workflows/changeset.yml: -------------------------------------------------------------------------------- 1 | name: Release / Changesets 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 2 19 | 20 | - name: setup node 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 16 24 | cache: npm 25 | 26 | - name: install 27 | run: npm install --no-save turbo-linux-64 && npm install 28 | 29 | - name: Create Release Pull Request or Publish to npm 30 | # https://github.com/changesets/action 31 | uses: changesets/action@v1 32 | with: 33 | # this expects you to have a script called release which does a build for your packages and calls changeset publish 34 | publish: npm run release 35 | version: npm run version-packages 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | NPM_TOKEN: ${{ secrets.NPM_SECRETS }} 39 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [pull_request] 3 | 4 | jobs: 5 | lint: 6 | name: runner / lint 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: checkout 10 | uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 2 13 | 14 | - name: setup node 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 16 18 | cache: npm 19 | 20 | - name: install 21 | run: npm install --no-save turbo-linux-64 && npm install 22 | 23 | - name: Check lint 24 | run: npm run lint 25 | 26 | - name: Test 27 | run: npm run test 28 | 29 | - name: Build 30 | run: npm run build 31 | -------------------------------------------------------------------------------- /.github/workflows/example-ci.yml: -------------------------------------------------------------------------------- 1 | name: CI for Example 2 | on: [pull_request] 3 | 4 | jobs: 5 | build-vite: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | with: 10 | fetch-depth: 2 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 16 14 | cache: npm 15 | - run: npm install --no-save turbo-linux-64 16 | - run: npm ci 17 | - run: npm run build 18 | working-directory: packages/for-ui 19 | - run: npx yalc publish 20 | working-directory: packages/for-ui 21 | - run: npx yalc link @4design/for-ui 22 | working-directory: example/vite-app 23 | - run: npx yalc update 24 | working-directory: example/vite-app 25 | - run: npm ci 26 | working-directory: example/vite-app 27 | - run: npm run build 28 | working-directory: example/vite-app 29 | 30 | build-nextjs: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | with: 35 | fetch-depth: 2 36 | - uses: actions/setup-node@v4 37 | with: 38 | node-version: 16 39 | cache: npm 40 | - run: npm install --no-save turbo-linux-64 41 | - run: npm ci 42 | - run: npm run build 43 | working-directory: packages/for-ui 44 | - run: npx yalc publish 45 | working-directory: packages/for-ui 46 | - run: npx yalc link @4design/for-ui 47 | working-directory: example/nextjs-app 48 | - run: npx yalc update 49 | working-directory: example/nextjs-app 50 | - run: npm ci 51 | working-directory: example/nextjs-app 52 | - run: npm run build 53 | working-directory: example/nextjs-app 54 | -------------------------------------------------------------------------------- /.github/workflows/typos.yml: -------------------------------------------------------------------------------- 1 | name: Test GitHub Action 2 | on: [pull_request] 3 | jobs: 4 | run: 5 | name: Spell Check with Typos 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout Actions Repository 9 | uses: actions/checkout@v4 10 | - name: Writes changes in the local checkout 11 | uses: crate-ci/typos@master 12 | -------------------------------------------------------------------------------- /.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 | # testing 9 | /coverage 10 | 11 | # next.js 12 | .next/ 13 | /out/ 14 | 15 | # storybook 16 | storybook-static 17 | 18 | # production 19 | /src/build 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | *.pem 25 | 26 | # debug 27 | *.tmp 28 | *.log* 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | 33 | # local env files 34 | *.env 35 | .env.local 36 | .env.development.local 37 | .env.test.local 38 | .env.production.local 39 | 40 | # vercel 41 | .vercel 42 | 43 | *-debug.log 44 | */*-debug.log 45 | 46 | .yalc 47 | yalc.lock 48 | .env 49 | 50 | ## firebase 51 | .firebase/ 52 | .runtimeconfig.json 53 | firebase-adminsdk.json 54 | 55 | ## rollup 56 | .size-snapshot.json 57 | 58 | dist 59 | 60 | # yarn 61 | .pnp.* 62 | .yarn 63 | .yarn/* 64 | !.yarn/patches 65 | !.yarn/plugins 66 | !.yarn/releases 67 | !.yarn/sdks 68 | !.yarn/versions 69 | .yarn 70 | 71 | .turbo 72 | tsconfig.tsbuildinfo 73 | -------------------------------------------------------------------------------- /.ignore: -------------------------------------------------------------------------------- 1 | **/CHANGELOG.md 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | types 4 | storybook-static 5 | 6 | .changeset 7 | .next 8 | .yarn 9 | .yalc 10 | .coderabbit.yaml 11 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require('@4design/prettier-config'); 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["lightyen.tailwindcss-intellisense-twin"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | packages/for-ui/README.md -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | extend-ignore-identifiers-re = [ 3 | "7a872ba", # Commit id is unexpectedly checked so ignored 4 | ] 5 | -------------------------------------------------------------------------------- /example/nextjs-app/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } 5 | -------------------------------------------------------------------------------- /example/nextjs-app/README.md: -------------------------------------------------------------------------------- 1 | # TypeScript Next.js example 2 | 3 | This is a really simple project that shows the usage of Next.js with TypeScript. 4 | 5 | ## Deploy your own 6 | 7 | Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) or preview live with [StackBlitz](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/with-typescript) 8 | 9 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-typescript&project-name=with-typescript&repository-name=with-typescript) 10 | 11 | ## How to use it? 12 | 13 | Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example: 14 | 15 | ```bash 16 | npx create-next-app --example with-typescript with-typescript-app 17 | ``` 18 | 19 | ```bash 20 | yarn create next-app --example with-typescript with-typescript-app 21 | ``` 22 | 23 | ```bash 24 | pnpm create next-app --example with-typescript with-typescript-app 25 | ``` 26 | 27 | Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). 28 | 29 | ## Notes 30 | 31 | This example shows how to integrate the TypeScript type system into Next.js. Since TypeScript is supported out of the box with Next.js, all we have to do is to install TypeScript. 32 | 33 | ``` 34 | npm install --save-dev typescript 35 | ``` 36 | 37 | To enable TypeScript's features, we install the type declarations for React and Node. 38 | 39 | ``` 40 | npm install --save-dev @types/react @types/react-dom @types/node 41 | ``` 42 | 43 | When we run `next dev` the next time, Next.js will start looking for any `.ts` or `.tsx` files in our project and builds it. It even automatically creates a `tsconfig.json` file for our project with the recommended settings. 44 | 45 | Next.js has built-in TypeScript declarations, so we'll get autocompletion for Next.js' modules straight away. 46 | 47 | A `type-check` script is also added to `package.json`, which runs TypeScript's `tsc` CLI in `noEmit` mode to run type-checking separately. You can then include this, for example, in your `test` scripts. 48 | -------------------------------------------------------------------------------- /example/nextjs-app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /example/nextjs-app/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | experimental: { 3 | appDir: true, 4 | }, 5 | transpilePackages: ['@4design/for-ui'], 6 | }; 7 | -------------------------------------------------------------------------------- /example/nextjs-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@4design/nextjs-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "NODE_ENV=production next build", 7 | "dev": "next", 8 | "start": "next start", 9 | "type-check": "tsc --pretty --noEmit" 10 | }, 11 | "dependencies": { 12 | "@4design/for-ui": "1.1.11", 13 | "@emotion/react": "11.11.1", 14 | "@emotion/styled": "11.11.0", 15 | "@mui/base": "5.0.0-alpha.126", 16 | "@mui/lab": "5.0.0-alpha.127", 17 | "@mui/material": "5.12.1", 18 | "react": "18.2.0", 19 | "react-dom": "18.2.0", 20 | "react-icons": "4.10.1" 21 | }, 22 | "devDependencies": { 23 | "@tanstack/react-table": "8.15.0", 24 | "@types/node": "12.20.55", 25 | "@types/react": "18.2.37", 26 | "@types/react-dom": "18.2.15", 27 | "@types/react-table": "7.7.18", 28 | "autoprefixer": "10.4.19", 29 | "next": "13.4.19", 30 | "postcss": "8.4.31", 31 | "tailwindcss": "3.4.3", 32 | "typescript": "5.1.6", 33 | "yalc": "1.0.0-pre.53" 34 | }, 35 | "volta": { 36 | "node": "16.20.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/nextjs-app/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NextPage } from 'next'; 3 | import { AppProps } from 'next/app'; 4 | import { StyledEngineProvider } from '@mui/material'; 5 | import '../styles/forty.css'; 6 | import '../styles/global.css'; 7 | 8 | const MyApp: NextPage = ({ Component, pageProps }) => { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default MyApp; 17 | -------------------------------------------------------------------------------- /example/nextjs-app/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { MdMoreVert, MdOutlineAdd, MdOutlineCheck, MdOutlineFileDownload } from 'react-icons/md'; 2 | import { Button, Chip, Dropzone, Menu, MenuItem, Text } from '@4design/for-ui'; 3 | 4 | const IndexPage = () => ( 5 |
6 | 7 | For UI Example Site with Next.js 8 | 9 | void 0} icon={} /> 10 |
11 | ファイルは3つまで添付できます 12 | 16 | 21 | 22 | 23 | } 24 | > 25 | オプション A 26 | オプション B 27 | 28 |
29 | void 0} onRemove={() => () => void 0} /> 30 | 34 |
35 | ); 36 | 37 | export default IndexPage; 38 | -------------------------------------------------------------------------------- /example/nextjs-app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /example/nextjs-app/styles/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | @tailwind variants; 5 | 6 | @layer base { 7 | body { 8 | @apply text-shade-dark-default text-r font-sans antialiased min-h-screen; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/nextjs-app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | important: ':is(#__next, .MuiPopover-root)', 3 | presets: [require('@4design/for-ui/tailwind.config.base.js')], 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx}', 6 | './components/**/*.{js,ts,jsx,tsx}', 7 | './node_modules/@4design/for-ui/dist/**/*.js', 8 | ], 9 | theme: { 10 | extend: {}, 11 | }, 12 | plugins: [], 13 | }; 14 | -------------------------------------------------------------------------------- /example/nextjs-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "jsxImportSource": "@emotion/react", 17 | "incremental": true, 18 | "plugins": [ 19 | { 20 | "name": "next" 21 | } 22 | ] 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /example/vite-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/vite-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@4design/vite-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "tsc && vite build", 7 | "dev": "vite", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@4design/for-ui": "1.1.11", 12 | "@emotion/react": "11.11.1", 13 | "@emotion/styled": "11.11.0", 14 | "@mui/base": "5.0.0-alpha.126", 15 | "@mui/lab": "5.0.0-alpha.127", 16 | "@mui/material": "5.12.1", 17 | "react": "18.2.0", 18 | "react-dom": "18.2.0", 19 | "react-icons": "4.10.1", 20 | "react-router-dom": "6.22.3" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "18.2.37", 24 | "@types/react-dom": "18.2.15", 25 | "@typescript-eslint/eslint-plugin": "5.62.0", 26 | "@typescript-eslint/parser": "5.62.0", 27 | "@vitejs/plugin-react": "2.2.0", 28 | "autoprefixer": "10.4.19", 29 | "postcss": "8.4.31", 30 | "prettier": "3.2.5", 31 | "tailwindcss": "3.4.3", 32 | "typescript": "5.1.6", 33 | "vite": "3.2.7", 34 | "yalc": "1.0.0-pre.53" 35 | }, 36 | "volta": { 37 | "node": "16.20.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /example/vite-app/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /example/vite-app/public/images/login/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4-design/for-ui/876faaa0793d41020f560c560e859a90546d9952/example/vite-app/public/images/login/google.png -------------------------------------------------------------------------------- /example/vite-app/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/vite-app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { createTheme, StyledEngineProvider, ThemeProvider } from '@mui/material'; 2 | import { Router } from './router'; 3 | 4 | export const App = () => { 5 | const rootElement = window.document.getElementById('root'); 6 | const theme = createTheme({ 7 | components: { 8 | MuiModal: { 9 | defaultProps: { 10 | container: rootElement, 11 | }, 12 | }, 13 | MuiPopover: { 14 | defaultProps: { 15 | container: rootElement, 16 | }, 17 | }, 18 | MuiPopper: { 19 | defaultProps: { 20 | container: rootElement, 21 | }, 22 | }, 23 | }, 24 | }); 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /example/vite-app/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { App } from './App'; 4 | import './styles/forty.css'; 5 | import './styles/global.css'; 6 | 7 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 8 | 9 | 10 | , 11 | ); 12 | -------------------------------------------------------------------------------- /example/vite-app/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { MdMoreVert, MdOutlineAdd, MdOutlineCheck, MdOutlineFileDownload } from 'react-icons/md'; 2 | import { Button, Chip, Dropzone, Menu, MenuItem, Text } from '@4design/for-ui'; 3 | 4 | const IndexPage = () => ( 5 |
6 | 7 | For UI Example Site with Next.js 8 | 9 | void 0} icon={} /> 10 |
11 | ファイルは3つまで添付できます 12 | 16 | 21 | 22 | 23 | } 24 | > 25 | オプション A 26 | オプション B 27 | 28 |
29 | void 0} onRemove={() => () => void 0} /> 30 | 34 |
35 | ); 36 | 37 | export default IndexPage; 38 | -------------------------------------------------------------------------------- /example/vite-app/src/router.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Route, Routes } from 'react-router-dom'; 2 | import Index from './pages/index'; 3 | 4 | export const Router = () => { 5 | return ( 6 | 7 | 8 | } /> 9 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /example/vite-app/src/styles/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | @tailwind variants; 5 | 6 | @layer base { 7 | body { 8 | @apply text-shade-dark-default text-r font-sans antialiased min-h-screen; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/vite-app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/vite-app/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | important: '#root', 3 | presets: [require('@4design/for-ui/tailwind.config.base.js')], 4 | }; 5 | -------------------------------------------------------------------------------- /example/vite-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.paths.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 7 | "allowJs": false, 8 | "skipLibCheck": true, 9 | "esModuleInterop": false, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "ESNext", 14 | "moduleResolution": "Node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "incremental": true, 19 | "jsx": "react-jsx" 20 | }, 21 | "include": ["./src"], 22 | "exclude": ["./node_modules", "node_modules"], 23 | "references": [{ "path": "./tsconfig.node.json" }] 24 | } 25 | -------------------------------------------------------------------------------- /example/vite-app/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /example/vite-app/tsconfig.paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "paths": { 5 | "@/*": ["./*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example/vite-app/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.join(__dirname, 'src/'), 11 | }, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "4design", 3 | "repository": { 4 | "type": "git", 5 | "url": "git+https://github.com/4-design/for-ui.git" 6 | }, 7 | "homepage": "https://github.com/4-design/for-ui", 8 | "author": "locona ", 9 | "license": "MIT", 10 | "private": true, 11 | "workspaces": [ 12 | "packages/*" 13 | ], 14 | "engines": { 15 | "node": ">=14.0.0", 16 | "npm": ">=7.0.0" 17 | }, 18 | "scripts": { 19 | "build": "turbo run build", 20 | "dev": "turbo run dev", 21 | "fmt": "run-p fmt:'*'", 22 | "fmt:eslint": "eslint . --fix", 23 | "fmt:prettier": "prettier --write --list-different .", 24 | "lint": "run-p lint:'*'", 25 | "lint:eslint": "eslint .", 26 | "lint:packages": "turbo run lint", 27 | "lint:prettier": "prettier --check .", 28 | "release": "run-p build && changeset publish", 29 | "test": "run-p test:*", 30 | "test:packages": "turbo run test", 31 | "type-check": "turbo run type-check", 32 | "version-packages": "changeset version" 33 | }, 34 | "devDependencies": { 35 | "@changesets/changelog-github": "0.5.0", 36 | "@changesets/cli": "2.27.1", 37 | "eslint": "8.57.0", 38 | "eslint-import-resolver-typescript": "3.6.1", 39 | "html-webpack-plugin": "^5.5.0", 40 | "lint-staged": "15.2.2", 41 | "npm-run-all2": "6.1.2", 42 | "prettier": "3.2.5", 43 | "turbo": "1.11.2", 44 | "typescript": "5.1.6" 45 | }, 46 | "volta": { 47 | "node": "16.20.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@4design/eslint-config", 3 | "version": "1.0.14", 4 | "description": "4-design's shared ESLint config", 5 | "repository": { 6 | "url": "4-design/for-ui", 7 | "directory": "packages/eslint-config" 8 | }, 9 | "author": "locona (https://github.com/locona)", 10 | "license": "MIT", 11 | "exports": { 12 | ".": "./src/index.cjs", 13 | "./*": "./src/*.cjs" 14 | }, 15 | "keywords": [ 16 | "eslint", 17 | "eslint-config" 18 | ], 19 | "peerDependencies": { 20 | "eslint": "^8" 21 | }, 22 | "dependencies": { 23 | "@rushstack/eslint-patch": "^1.1.3", 24 | "@typescript-eslint/eslint-plugin": "5.62.0", 25 | "@typescript-eslint/parser": "5.62.0", 26 | "eslint-config-prettier": "9.0.0", 27 | "eslint-plugin-import": "2.28.0", 28 | "eslint-plugin-promise": "^6.0.0", 29 | "eslint-plugin-sonarjs": "^0.20.0", 30 | "eslint-plugin-tailwindcss": "3.13.0", 31 | "eslint-plugin-unicorn": "^48.0.0" 32 | }, 33 | "optionalDependencies": { 34 | "eslint-plugin-jsx-a11y": "^6.5.1", 35 | "eslint-plugin-mdx": "^3.0.0", 36 | "eslint-plugin-react": "7.34.1", 37 | "eslint-plugin-react-hooks": "^4.5.0" 38 | }, 39 | "publishConfig": { 40 | "access": "public" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/eslint-config/src/index.cjs: -------------------------------------------------------------------------------- 1 | require('@rushstack/eslint-patch/modern-module-resolution'); 2 | 3 | module.exports = { 4 | reportUnusedDisableDirectives: true, 5 | ignorePatterns: [ 6 | '!.*', // Don't ignore dot-files because by default ESLint ignore dot-files (except for .eslintrc.*) and dot-folders 7 | ], 8 | overrides: [ 9 | { 10 | files: '*.{,c,m}{j,t}s{,x}', 11 | parser: '@typescript-eslint/parser', 12 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 13 | plugins: ['sonarjs', 'unicorn', 'promise', 'import'], 14 | rules: { 15 | // Disallows if statements as the only statement in else blocks 16 | // https://eslint.org/docs/rules/no-lonely-if 17 | 'no-lonely-if': 'error', 18 | // Disallows the use of console 19 | // https://eslint.org/docs/rules/no-console 20 | 'no-console': ['error', { allow: ['info', 'warn', 'error', 'trace'] }], 21 | // Requires method and property shorthand syntax for object literals 22 | // https://eslint.org/docs/rules/object-shorthand 23 | 'object-shorthand': ['error', 'always'], 24 | // Disallows loops with a body that allows only one iteration 25 | // https://eslint.org/docs/rules/no-unreachable-loop 26 | 'no-unreachable-loop': 'error', 27 | 'sonarjs/no-one-iteration-loop': 'off', // similar to 'no-unreachable-loop' but reports less cases 28 | 'prefer-arrow-callback': ['error', { allowNamedFunctions: true }], 29 | 30 | 'sonarjs/no-unused-collection': 'error', 31 | 'sonarjs/no-identical-conditions': 'error', 32 | 'sonarjs/no-inverted-boolean-check': 'error', 33 | 'sonarjs/no-use-of-empty-return-value': 'error', 34 | 'sonarjs/no-gratuitous-expressions': 'error', 35 | 'sonarjs/no-nested-switch': 'error', 36 | 'unicorn/no-lonely-if': 'error', 37 | 'sonarjs/no-collapsible-if': 'off', // same as 'unicorn/no-lonely-if' 38 | 'unicorn/filename-case': 'off', 39 | 'unicorn/no-array-push-push': 'error', 40 | 'unicorn/no-instanceof-array': 'error', 41 | 'unicorn/no-empty-file': 'error', 42 | 'unicorn/no-useless-fallback-in-spread': 'error', 43 | 'unicorn/prefer-array-find': 'error', 44 | 'unicorn/no-useless-spread': 'error', 45 | 'unicorn/prefer-includes': 'error', 46 | 47 | 'no-else-return': ['error', { allowElseIf: false }], 48 | 'promise/no-nesting': 'error', 49 | 50 | 'import/no-default-export': 'error', 51 | 'import/no-extraneous-dependencies': 'error', 52 | 'import/prefer-default-export': 'off', // disable opposite of 'import/no-default-export' 53 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], 54 | }, 55 | settings: { 56 | 'import/extensions': ['.ts', '.tsx'], 57 | 'import/resolver': { 58 | typescript: {}, 59 | }, 60 | }, 61 | }, 62 | { 63 | files: ['vite.config.ts', 'jest.config.js', '*.d.ts'], 64 | rules: { 65 | 'import/no-default-export': 'off', 66 | }, 67 | }, 68 | { 69 | files: ['*.graphql.ts'], 70 | rules: { 71 | 'import/no-default-export': 'off', 72 | }, 73 | }, 74 | ], 75 | }; 76 | -------------------------------------------------------------------------------- /packages/eslint-config/src/react.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | }, 5 | overrides: [ 6 | { 7 | files: '*.{,c,m}{j,t}s{,x}', 8 | plugins: ['tailwindcss'], 9 | extends: [ 10 | 'plugin:react/recommended', 11 | 'plugin:react-hooks/recommended', 12 | 'plugin:jsx-a11y/recommended', 13 | 'prettier', 14 | 'plugin:tailwindcss/recommended', 15 | ], 16 | rules: { 17 | 'react/display-name': 'off', 18 | 'react/jsx-curly-brace-presence': 'error', 19 | 'jsx-a11y/anchor-is-valid': 'off', 20 | 'jsx-a11y/no-static-element-interactions': 'off', 21 | 'jsx-a11y/click-events-have-key-events': 'off', 22 | 'react/react-in-jsx-scope': 'off', // import of React is no longer required starting from react@17 23 | 'tailwindcss/no-custom-classname': 'off', 24 | }, 25 | settings: { 26 | react: { 27 | version: 'detect', 28 | }, 29 | }, 30 | }, 31 | { 32 | files: [ 33 | '**/pages/**', // Next.js pages directory use default export 34 | '*.stories.tsx', 35 | 'next.config.{,c,m}{j,t}s', 36 | ], 37 | rules: { 38 | 'import/no-default-export': 'off', 39 | }, 40 | }, 41 | { 42 | files: ['next.config.mjs'], 43 | env: { 44 | node: true, 45 | }, 46 | }, 47 | { 48 | files: ['*.tsx'], 49 | rules: { 50 | 'react/prop-types': 'off', 51 | }, 52 | }, 53 | ], 54 | }; 55 | -------------------------------------------------------------------------------- /packages/for-ui/.storybook/main.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)', '../src/**/*.stories.mdx'], 3 | addons: [ 4 | '@storybook/addon-links', 5 | '@storybook/addon-essentials', 6 | '@storybook/addon-backgrounds', 7 | { 8 | name: '@storybook/addon-postcss', 9 | options: { 10 | postcssLoaderOptions: { 11 | implementation: require('postcss'), 12 | }, 13 | }, 14 | }, 15 | ], 16 | core: { 17 | builder: '@storybook/builder-vite', 18 | }, 19 | framework: '@storybook/react', 20 | features: { 21 | storyStoreV7: true, 22 | // previewMdx2: true, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /packages/for-ui/.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-backgrounds/register'; 2 | import { addons } from '@storybook/addons'; 3 | import { create } from '@storybook/theming'; 4 | 5 | addons.setConfig({ 6 | theme: create({ 7 | base: 'light', 8 | brandTitle: 'for-ui', 9 | }), 10 | panelPosition: 'bottom', 11 | }); 12 | -------------------------------------------------------------------------------- /packages/for-ui/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 12 | -------------------------------------------------------------------------------- /packages/for-ui/.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createTheme, StyledEngineProvider, ThemeProvider } from '@mui/material/styles'; 3 | import '../styles/forty.css'; 4 | import '../styles/global.css'; 5 | 6 | export const parameters = { 7 | layout: 'fullscreen', 8 | backgrounds: { 9 | default: 'default', 10 | values: [ 11 | { 12 | name: 'default', 13 | value: '#FFF', 14 | }, 15 | ], 16 | }, 17 | controls: { expanded: true }, 18 | options: { 19 | storySort: { 20 | order: ['General', 'Form', 'Data Display', 'Feedback', 'Navigation'], 21 | }, 22 | }, 23 | }; 24 | 25 | const rootElement = document.getElementById('root'); 26 | 27 | const theme = createTheme({ 28 | components: { 29 | MuiModal: { 30 | defaultProps: { 31 | container: rootElement, 32 | }, 33 | }, 34 | MuiPopover: { 35 | defaultProps: { 36 | container: rootElement, 37 | }, 38 | }, 39 | MuiPopper: { 40 | defaultProps: { 41 | container: rootElement, 42 | }, 43 | }, 44 | }, 45 | }); 46 | 47 | export const decorators = [ 48 | (Story) => ( 49 | 50 | 51 |
52 | 53 |
54 |
55 |
56 | ), 57 | ]; 58 | -------------------------------------------------------------------------------- /packages/for-ui/README.md: -------------------------------------------------------------------------------- 1 |

2 | For UI 3 | 4 |

5 | 6 |
7 | 8 | [![npm version](https://badge.fury.io/js/@4design%2Ffor-ui.svg)](https://badge.fury.io/js/@4design%2Ffor-ui) 9 | [![CI](https://github.com/4-design/for-ui/actions/workflows/ci.yml/badge.svg)](https://github.com/4-design/for-ui/actions/workflows/ci.yml) 10 | 11 |
12 | 13 | For UI は 株式会社スリーシェイクのデザインシステム「For Design System」のデザイントークンとコンポーネントライブラリを実装したパッケージです。 14 | 15 | React と MUI と Tailwind CSS で作られています。 16 | 17 | ## インストール 18 | 19 | ``` 20 | npm i @4design/for-ui 21 | ``` 22 | 23 | Peer dependencies のインストール 24 | 25 | ``` 26 | npm i tailwindcss @mui/lab @mui/material @mui/base react-icons react @tanstack/react-table 27 | npm i --save-dev @types/react 28 | ``` 29 | 30 | ## セットアップ 31 | 32 | ### 1. Tailwind CSS を導入 33 | 34 | #### Tailwind CSS の CSS を読み込む 35 | 36 | - `global.css` (名前は任意) を作成 37 | 38 | - 以下の内容を置く 39 | 40 | ```css 41 | @tailwind base; 42 | @tailwind components; 43 | @tailwind utilities; 44 | @tailwind variants; 45 | 46 | @layer base { 47 | body { 48 | @apply text-shade-dark-default text-r font-sans antialiased; 49 | } 50 | } 51 | ``` 52 | 53 | - `app.tsx` で `global.css` を読み込む 54 | 55 | #### `postcss.config.js` の作成 56 | 57 | - `postcss.config.js` が存在しない場合は作成する。 58 | 59 | - デフォルトの構成を自分で定義する必要があることに注意 ([参考](https://nextjs.org/docs/advanced-features/customizing-postcss-config#customizing-plugins)) 60 | 61 | - 以下を追加 62 | 63 | ```js 64 | plugins: { 65 | tailwindcss: {}, 66 | autoprefixer: {}, 67 | // ... 68 | } 69 | ``` 70 | 71 | ### 2. For UI の設定を読み込む 72 | 73 | #### preset の読み込み 74 | 75 | ```js 76 | preset: [require('@4design/for-ui/tailwind.config.base.js')], 77 | ``` 78 | 79 | #### 共通フォントの読み込み 80 | 81 | ```html 82 | 86 | ``` 87 | 88 | またはこれに相当するものを置いてください 89 | 90 | #### MUI のスタイル上書きのための important 設定 91 | 92 | ```js 93 | important: #root, // 任意の上位セレクタを指定する 94 | ``` 95 | 96 | または 97 | 98 | ```js 99 | important: true; 100 | ``` 101 | 102 | を指定 103 | 104 | (複数の important が必要な場合は[こちら](https://github.com/4-design/for-ui/discussions/1093)を参考にしてください) 105 | 106 | #### `tailwind.config.js` 設定例 107 | 108 | ```js 109 | module.exports = { 110 | important: ':is(#__next, .MuiPopover-root)', 111 | presets: [require('@4design/for-ui/tailwind.config.base.js')], 112 | content: [ 113 | './pages/**/*.{js,ts,jsx,tsx}', 114 | './components/**/*.{js,ts,jsx,tsx}', 115 | './node_modules/@4design/for-ui/dist/**/*.js', 116 | ], 117 | plugins: [], 118 | }; 119 | ``` 120 | 121 | 詳細は `/example` 以下に実装例があるので参考にしてください。 122 | 123 | ## License 124 | 125 | MIT License 126 | 127 | ## Author 128 | 129 | 4 design (3-shake Inc.) 130 | -------------------------------------------------------------------------------- /packages/for-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@4design/for-ui", 3 | "version": "1.1.11", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/4-design/for-ui.git" 7 | }, 8 | "homepage": "https://github.com/4-design/for-ui", 9 | "author": "locona ", 10 | "license": "MIT", 11 | "engines": { 12 | "node": ">=14.0.0" 13 | }, 14 | "main": "dist/commonjs/for-ui.js", 15 | "module": "dist/esm/for-ui.esm.js", 16 | "exports": { 17 | ".": { 18 | "import": "./dist/esm/for-ui.esm.js", 19 | "require": "./dist/commonjs/for-ui.js", 20 | "types": "./dist/types/index.d.ts" 21 | }, 22 | "./tailwind.config.base.js": "./tailwind.config.base.js" 23 | }, 24 | "types": "dist/types/index.d.ts", 25 | "files": [ 26 | "dist", 27 | "tailwind.config.base.js" 28 | ], 29 | "scripts": { 30 | "build": "run-p build:'*'", 31 | "build-storybook": "build-storybook", 32 | "build:js": "NODE_ENV=production vite build", 33 | "build:types": "tsc -p tsconfig.build.json", 34 | "dev": "run-p storybook", 35 | "lint": "run-p lint:*", 36 | "lint:type-check": "tsc --pretty", 37 | "storybook": "start-storybook --no-manager-cache -p 6006 -s public", 38 | "test": "run-p test:*", 39 | "test:unit": "vitest run" 40 | }, 41 | "peerDependencies": { 42 | "@mui/base": ">=5.0.0-alpha.120", 43 | "@mui/lab": ">=5.0.0-alpha.73", 44 | "@mui/material": ">=5.9.3", 45 | "@tanstack/react-table": ">=8.5.22", 46 | "@types/react": ">=18.0.0", 47 | "react": ">=18.0.0", 48 | "react-icons": ">=4.3.0", 49 | "tailwindcss": ">=3.0.0" 50 | }, 51 | "dependencies": { 52 | "clsx": "2.1.0", 53 | "material-ui-popup-state": "5.0.9", 54 | "react-dropzone": "14.2.3", 55 | "react-number-format": "5.3.4", 56 | "react-transition-group": "4.4.5", 57 | "tailwind-merge": "1.14.0" 58 | }, 59 | "devDependencies": { 60 | "@emotion/react": "11.11.1", 61 | "@emotion/styled": "11.11.0", 62 | "@hookform/resolvers": "3.2.0", 63 | "@mdx-js/react": "1.6.22", 64 | "@mui/base": "5.0.0-alpha.126", 65 | "@mui/lab": "5.0.0-alpha.127", 66 | "@mui/material": "5.12.1", 67 | "@mui/types": "7.2.4", 68 | "@storybook/addon-actions": "6.5.15", 69 | "@storybook/addon-backgrounds": "6.5.15", 70 | "@storybook/addon-essentials": "6.5.15", 71 | "@storybook/addon-links": "6.5.15", 72 | "@storybook/addon-postcss": "2.0.0", 73 | "@storybook/addons": "6.5.16", 74 | "@storybook/builder-vite": "0.2.6", 75 | "@storybook/builder-webpack5": "6.5.14", 76 | "@storybook/manager-webpack5": "6.5.14", 77 | "@storybook/mdx2-csf": "0.0.3", 78 | "@storybook/react": "6.5.15", 79 | "@storybook/theming": "6.5.16", 80 | "@tanstack/react-table": "8.15.0", 81 | "@testing-library/jest-dom": "5.17.0", 82 | "@testing-library/react": "14.2.2", 83 | "@testing-library/user-event": "14.5.2", 84 | "@types/react": "18.2.37", 85 | "@types/react-dom": "18.2.15", 86 | "@types/testing-library__jest-dom": "5.14.9", 87 | "@vitejs/plugin-react": "2.2.0", 88 | "autoprefixer": "10.4.19", 89 | "babel-loader": "9.1.0", 90 | "jsdom": "24.0.0", 91 | "npm-run-all2": "6.1.2", 92 | "postcss": "8.4.31", 93 | "react": "18.2.0", 94 | "react-dom": "18.2.0", 95 | "react-hook-form": "7.45.4", 96 | "react-icons": "4.10.1", 97 | "tailwindcss": "3.4.3", 98 | "typescript": "5.1.6", 99 | "vite": "3.2.5", 100 | "vitest": "0.33.0", 101 | "webpack": "5.88.2", 102 | "yup": "1.2.0" 103 | }, 104 | "overrides": { 105 | "@mdx-js/react": { 106 | "react": "^18.2.0", 107 | "react-dom": "^18.2.0" 108 | } 109 | }, 110 | "sideEffects": false, 111 | "volta": { 112 | "node": "16.20.2" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /packages/for-ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/for-ui/public/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4-design/for-ui/876faaa0793d41020f560c560e859a90546d9952/packages/for-ui/public/image.png -------------------------------------------------------------------------------- /packages/for-ui/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/4-design/for-ui/876faaa0793d41020f560c560e859a90546d9952/packages/for-ui/public/logo.png -------------------------------------------------------------------------------- /packages/for-ui/public/transparent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/for-ui/react-table-index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/react-table.production.min.js'); 2 | -------------------------------------------------------------------------------- /packages/for-ui/src/badge/Badge.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | MdDeleteOutline, 4 | MdOutlineEdit, 5 | MdOutlineMail, 6 | MdOutlinePhone, 7 | MdPersonOutline, 8 | MdPriorityHigh, 9 | } from 'react-icons/md'; 10 | import { Meta, Story } from '@storybook/react/types-6-0'; 11 | import { Badge } from './Badge'; 12 | 13 | const sampleIcons = { 14 | undefined, 15 | MdDeleteOutline: , 16 | MdOutlineEdit: , 17 | MdOutlinePhone: , 18 | MdOutlineMail: , 19 | MdPersonOutline: , 20 | MdPriorityHigh: , 21 | }; 22 | 23 | export default { 24 | title: 'Data Display / Badge', 25 | component: Badge, 26 | argTypes: { 27 | icon: { 28 | options: Object.keys(sampleIcons), 29 | mapping: sampleIcons, 30 | }, 31 | }, 32 | } as Meta; 33 | 34 | export const Playground = { 35 | args: { 36 | label: 'バッジ', 37 | variant: 'text', 38 | intention: 'subtle', 39 | icon: MdDeleteOutline, 40 | }, 41 | }; 42 | 43 | export const Constant: Story = () => ( 44 |
45 |
46 | } /> 47 | } /> 48 | } /> 49 | } /> 50 | } /> 51 | } /> 52 | } /> 53 | } /> 54 |
55 |
56 | ); 57 | 58 | export const Text: Story = () => ( 59 |
60 |
61 | } /> 62 | } /> 63 | } /> 64 | } /> 65 | } /> 66 | } /> 67 | } /> 68 | } /> 69 |
70 |
71 | ); 72 | 73 | export const Outlined: Story = () => ( 74 |
75 |
76 | } /> 77 | } /> 78 | } /> 79 | } /> 80 | } /> 81 | } /> 82 | } /> 83 | } /> 84 |
85 |
86 | ); 87 | -------------------------------------------------------------------------------- /packages/for-ui/src/badge/Badge.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { Badge } from './Badge'; 4 | 5 | describe('Badge', () => { 6 | it('renders label', () => { 7 | render(); 8 | expect(screen.getByText('badge')).toBeInTheDocument(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /packages/for-ui/src/badge/Badge.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | import { ConstantBadge } from './ConstantBadge'; 3 | import { OutlinedBadge } from './OutlinedBadge'; 4 | import { TextBadge } from './TextBadge'; 5 | 6 | type BadgeInterface = { 7 | /** 8 | * ユーザーに提示したい意図 (e.g. エラーならばnegative) を指定 9 | * 10 | * @default shade 11 | */ 12 | intention?: 'subtle' | 'shade' | 'primary' | 'secondary' | 'positive' | 'negative' | 'notice' | 'informative'; 13 | 14 | /** 15 | * 表示されるlabelを指定 16 | */ 17 | label: string; 18 | 19 | className?: string; 20 | }; 21 | 22 | export type ConstantBadgeProps = BadgeInterface & { 23 | /** 24 | * variantを指定 25 | * 26 | * @default text 27 | */ 28 | variant: 'constant'; 29 | 30 | /** 31 | * iconを指定 32 | */ 33 | icon: ReactNode; 34 | }; 35 | 36 | export type DynamicBadgeProps = BadgeInterface & { 37 | /** 38 | * variantを指定 39 | * 40 | * @default text 41 | */ 42 | variant?: 'text' | 'outlined'; 43 | 44 | /** 45 | * iconを指定 46 | */ 47 | icon?: ReactNode; 48 | }; 49 | 50 | export type BadgeProps = ConstantBadgeProps | DynamicBadgeProps; 51 | 52 | export const Badge: FC = ({ variant = 'text', icon, ...rest }) => { 53 | switch (variant) { 54 | case 'text': 55 | return ; 56 | case 'outlined': 57 | return ; 58 | case 'constant': 59 | return ; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /packages/for-ui/src/badge/ConstantBadge.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { fsx } from '../system/fsx'; 3 | import { Text } from '../text'; 4 | import { ConstantBadgeProps } from './Badge'; 5 | 6 | export const ConstantBadge: FC> = ({ 7 | intention = 'subtle', 8 | icon, 9 | label, 10 | className, 11 | }) => ( 12 | 28 | svg]:h-full [&>svg]:w-full`, 31 | { 32 | subtle: `bg-shade-medium-default`, 33 | shade: `bg-shade-dark-default`, 34 | primary: `bg-primary-dark-default`, 35 | secondary: `bg-secondary-dark-default`, 36 | positive: `bg-positive-dark-default`, 37 | negative: `bg-negative-dark-default`, 38 | notice: `bg-notice-dark-default`, 39 | informative: `bg-informative-dark-default`, 40 | }[intention], 41 | ])} 42 | > 43 | {icon} 44 | 45 | 46 | {label} 47 | 48 | 49 | ); 50 | -------------------------------------------------------------------------------- /packages/for-ui/src/badge/OutlinedBadge.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { fsx } from '../system/fsx'; 3 | import { Text } from '../text'; 4 | import { BadgeProps } from './Badge'; 5 | 6 | export const OutlinedBadge: FC> = ({ intention = 'subtle', icon, label, className }) => ( 7 | 23 | {icon && ( 24 | 38 | {icon} 39 | 40 | )} 41 | 42 | {label} 43 | 44 | 45 | ); 46 | -------------------------------------------------------------------------------- /packages/for-ui/src/badge/TextBadge.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { fsx } from '../system/fsx'; 3 | import { Text } from '../text'; 4 | import { BadgeProps } from './Badge'; 5 | 6 | export const TextBadge: FC> = ({ intention = 'subtle', icon, label, className }) => ( 7 | 23 | {icon && ( 24 | 38 | {icon} 39 | 40 | )} 41 | 42 | {label} 43 | 44 | 45 | ); 46 | -------------------------------------------------------------------------------- /packages/for-ui/src/badge/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Badge'; 2 | -------------------------------------------------------------------------------- /packages/for-ui/src/button/Button.test.tsx: -------------------------------------------------------------------------------- 1 | import { MdOutlineCheck } from 'react-icons/md'; 2 | import { describe, expect, it } from 'vitest'; 3 | import { render, screen } from '@testing-library/react'; 4 | import userEvent from '@testing-library/user-event'; 5 | import { Text } from '../text'; 6 | import { Button } from './Button'; 7 | 8 | describe('Button', () => { 9 | it('with text only is rendered', () => { 10 | render(); 11 | expect(screen.queryByRole('button', { name: 'button' })).toBeInTheDocument(); 12 | }); 13 | it('with icon and text is rendered', () => { 14 | render( 15 | , 19 | ); 20 | expect(screen.queryByRole('button', { name: 'button' })).toBeInTheDocument(); 21 | }); 22 | it('with text and icon is rendered', () => { 23 | render( 24 | , 28 | ); 29 | expect(screen.queryByRole('button', { name: 'button' })).toBeInTheDocument(); 30 | }); 31 | it('with icon and aria-label is rendered', () => { 32 | render( 33 | , 36 | ); 37 | expect(screen.queryByRole('button', { name: 'button' })).toBeInTheDocument(); 38 | }); 39 | it('with nested text is rendered', () => { 40 | render( 41 | , 46 | ); 47 | expect(screen.queryByRole('button', { name: 'button test' })).toBeInTheDocument(); 48 | }); 49 | it('does not fire onClick event when not clicked', async () => { 50 | const onClick = vi.fn(); 51 | render(); 52 | expect(onClick).not.toHaveBeenCalled(); 53 | }); 54 | it('fires onClick event when clicked', async () => { 55 | const user = userEvent.setup(); 56 | const onClick = vi.fn(); 57 | render(); 58 | await user.click(screen.getByRole('button', { name: 'button' })); 59 | expect(onClick).toHaveBeenCalledOnce(); 60 | }); 61 | it('does not fire onClick event when disabled', async () => { 62 | const user = userEvent.setup(); 63 | const onClick = vi.fn(); 64 | render( 65 | , 68 | ); 69 | await user.click(screen.getByRole('button', { name: 'button' })); 70 | expect(onClick).not.toHaveBeenCalled(); 71 | }); 72 | it('does not allow users to click when loading', async () => { 73 | const user = userEvent.setup(); 74 | const onClick = vi.fn(); 75 | render( 76 | , 79 | ); 80 | const element = screen.getByText('button')?.closest('button'); 81 | expect(element).toBeTruthy(); 82 | if (!element) return; // Workaround for that expect does not assert element is not null 83 | await user.click(element); 84 | expect(onClick).not.toHaveBeenCalled(); 85 | }); 86 | it('works as link when specified as anchor tag by `as`', async () => { 87 | render( 88 | , 91 | ); 92 | const element = await screen.findByRole('link', { name: 'test' }); 93 | expect(element).toBeInTheDocument(); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /packages/for-ui/src/button/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Button'; 2 | -------------------------------------------------------------------------------- /packages/for-ui/src/callout/Callout.stories.tsx: -------------------------------------------------------------------------------- 1 | import { MdErrorOutline } from 'react-icons/md'; 2 | import { Meta } from '@storybook/react/types-6-0'; 3 | import { Text } from '../text'; 4 | import { Callout } from './Callout'; 5 | 6 | const sampleIcons = { 7 | undefined, 8 | MdErrorOutline: , 9 | }; 10 | 11 | export default { 12 | title: 'Feedback / Callout', 13 | component: Callout, 14 | argTypes: { 15 | intention: ['subtle', 'positive', 'negative', 'notice', 'informative'], 16 | icon: { 17 | options: Object.keys(sampleIcons), 18 | mapping: sampleIcons, 19 | }, 20 | }, 21 | } as Meta; 22 | 23 | export const Playground = { 24 | args: { 25 | children: 'テキスト', 26 | icon: 'MdErrorOutline', 27 | }, 28 | }; 29 | 30 | export const Default = () => ( 31 |
32 | 33 | アイコンなし 34 | 35 | 自動診断がオフになっています。 36 | 自動診断がオフになっています。 37 | 自動診断がオフになっています。 38 | 自動診断がオフになっています。 39 | 自動診断がオフになっています。 40 | 41 | アイコン付き 42 | 43 | } intention="subtle"> 44 | 自動診断がオフになっています。 45 | 46 | } intention="positive"> 47 | 自動診断がオフになっています。 48 | 49 | } intention="negative"> 50 | 自動診断がオフになっています。 51 | 52 | } intention="notice"> 53 | 自動診断がオフになっています。 54 | 55 | } intention="informative"> 56 | 自動診断がオフになっています。 57 | 58 |
59 | ); 60 | -------------------------------------------------------------------------------- /packages/for-ui/src/callout/Callout.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { Callout } from './Callout'; 3 | 4 | describe('Callout', () => { 5 | it('is rendered', () => { 6 | render(Hello); 7 | expect(screen.queryByRole('status', { description: 'Hello' })).toBeInTheDocument(); 8 | }); 9 | it('of negative intention is rendered', () => { 10 | render(Hello); 11 | expect(screen.queryByRole('alert', { description: 'Hello' })).toBeInTheDocument(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/for-ui/src/callout/Callout.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode, useId } from 'react'; 2 | import { fsx } from '../system/fsx'; 3 | import { Text } from '../text'; 4 | 5 | export type CalloutProps = { 6 | /** 7 | * 操作の意図を示す場合に指定 8 | * 9 | * 例: 通常、コンテンツの削除など取り返しのつかないユーザーの操作を赤色で表示しますが、その場合intentionにnegativeを設定してください。 10 | * 11 | * @default subtle 12 | */ 13 | intention?: 'subtle' | 'positive' | 'negative' | 'notice' | 'informative'; 14 | 15 | /** 16 | * iconを表示する場合に設定 17 | * 18 | * 色や大きさはCallout側から設定するので指定しないでください。例: `icon={}` 19 | */ 20 | icon?: ReactNode; 21 | 22 | /** 23 | * 表示させるテキストを指定 24 | * 25 | * リンク等を入れられるようReactNodeにしてありますが、通常はstringを指定してください。Button等は含めないでください。 26 | */ 27 | children: ReactNode; 28 | 29 | className?: string; 30 | }; 31 | 32 | export const Callout: FC = ({ icon, children, intention = 'subtle', className, ...props }) => { 33 | const descriptionId = useId(); 34 | return ( 35 | 51 | {icon && ( 52 | 56 | {icon} 57 | 58 | )} 59 | 60 | {children} 61 | 62 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /packages/for-ui/src/callout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Callout'; 2 | -------------------------------------------------------------------------------- /packages/for-ui/src/checkbox/Checkbox.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | import * as yup from 'yup'; 4 | import { yupResolver } from '@hookform/resolvers/yup'; 5 | import { Meta } from '@storybook/react/types-6-0'; 6 | import { Button } from '../button'; 7 | import { fsx } from '../system/fsx'; 8 | import { Text } from '../text'; 9 | import { Checkbox } from './Checkbox'; 10 | import { CheckboxGroup } from './CheckboxGroup'; 11 | 12 | export default { 13 | title: 'Form / Checkbox', 14 | component: Checkbox, 15 | decorators: [(Story) => ], 16 | } as Meta; 17 | 18 | export const Playground = { 19 | args: { 20 | label: '', 21 | disabled: false, 22 | checked: false, 23 | indeterminate: false, 24 | }, 25 | }; 26 | 27 | export const Basic = (): JSX.Element => { 28 | const { register, handleSubmit } = useForm(); 29 | const onSubmit = (data: unknown) => console.info(data); 30 | 31 | return ( 32 |
33 |
34 |
35 | 36 | ラベル 37 | 38 | 39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 |
47 | 48 | 49 |
50 | ); 51 | }; 52 | 53 | export const WithNopadding = (): JSX.Element => { 54 | const { register, handleSubmit } = useForm(); 55 | const onSubmit = (data: unknown) => console.info(data); 56 | 57 | return ( 58 |
59 |
60 |
61 | 62 | ラベル 63 | 64 | 65 |
66 | 67 | 68 | 69 | 70 |
71 |
72 |
73 | 74 | 75 |
76 | ); 77 | }; 78 | 79 | const allPreferences = { 80 | spring: '春', 81 | summer: '夏', 82 | autumn: '秋', 83 | winter: '冬', 84 | } as const; 85 | 86 | const schema = yup.object({ 87 | preferences: yup.object(Object.fromEntries(Object.keys(allPreferences).map((k) => [k, yup.boolean()]))), 88 | }); 89 | 90 | type FieldValue = yup.InferType; 91 | 92 | export const WithCheckboxGroup = () => { 93 | const { 94 | register, 95 | handleSubmit, 96 | formState: { errors }, 97 | } = useForm({ 98 | resolver: yupResolver(schema), 99 | }); 100 | const onSubmit = useCallback((data: unknown) => { 101 | console.info(data); 102 | }, []); 103 | return ( 104 |
105 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 |
119 | ); 120 | }; 121 | 122 | export const CustomLabel = () => ( 123 | 126 | ラベル 127 | 128 | } 129 | /> 130 | ); 131 | -------------------------------------------------------------------------------- /packages/for-ui/src/checkbox/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { FC, forwardRef, ReactNode } from 'react'; 2 | import { MdCheck, MdRemove } from 'react-icons/md'; 3 | import MuiCheckbox, { CheckboxProps as MuiCheckboxProps } from '@mui/material/Checkbox'; 4 | import { fsx } from '../system/fsx'; 5 | import { TextDefaultStyler } from '../system/TextDefaultStyler'; 6 | import { Text } from '../text'; 7 | 8 | export type CheckboxProps = MuiCheckboxProps & { 9 | label?: ReactNode; 10 | nopadding?: boolean; 11 | className?: string; 12 | }; 13 | 14 | const Indicator: FC<{ state: 'default' | 'checked' | 'intermediate'; disabled: boolean }> = ({ state, disabled }) => ( 15 | 25 | { 26 | { 27 | default: null, 28 | checked: , 29 | intermediate: , 30 | }[state] 31 | } 32 | 33 | ); 34 | 35 | export const Checkbox = forwardRef( 36 | ({ label, nopadding = false, disabled, className, ...rest }, ref) => ( 37 | 38 | } 41 | checkedIcon={} 42 | indeterminateIcon={} 43 | disabled={disabled} 44 | className={fsx(nopadding ? 'p-0' : 'p-1')} 45 | inputRef={ref} 46 | {...rest} 47 | /> 48 | ( 51 | 56 | )} 57 | /> 58 | 59 | ), 60 | ); 61 | -------------------------------------------------------------------------------- /packages/for-ui/src/checkbox/CheckboxGroup.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, ReactNode } from 'react'; 2 | import FormControl from '@mui/material/FormControl'; 3 | import FormGroup, { FormGroupProps } from '@mui/material/FormGroup'; 4 | import { fsx } from '../system/fsx'; 5 | import { TextDefaultStyler } from '../system/TextDefaultStyler'; 6 | import { Text } from '../text'; 7 | 8 | export type CheckboxGroupProps = Omit & { 9 | name?: string; 10 | row?: boolean; 11 | required?: boolean; 12 | disabled?: boolean; 13 | label?: ReactNode; 14 | error?: boolean; 15 | helperText?: ReactNode; 16 | className?: string; 17 | children: ReactNode; 18 | }; 19 | 20 | export const CheckboxGroup = forwardRef( 21 | ({ required, disabled, label, error, row, helperText, className, ...rest }, ref) => { 22 | return ( 23 | 30 | ( 33 | 34 | {/* due to the CSS bug, legend element cannot be styled if contents not specified. But contents makes difficult to style, so wrap by Text. */} 35 | 36 | {children} 37 | {required && *} 38 | 39 | 40 | )} 41 | /> 42 | 43 | ( 46 | 52 | )} 53 | /> 54 | 55 | ); 56 | }, 57 | ); 58 | -------------------------------------------------------------------------------- /packages/for-ui/src/checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Checkbox'; 2 | export * from './CheckboxGroup'; 3 | -------------------------------------------------------------------------------- /packages/for-ui/src/chip/Chip.tsx: -------------------------------------------------------------------------------- 1 | import { ElementType, forwardRef, MouseEvent, ReactNode } from 'react'; 2 | import { ComponentPropsWithAs, Element, ElementTypeToHTMLElement, Ref } from '../system/componentType'; 3 | import { FullChip } from './FullChip'; 4 | import { LimitedChip } from './LimitedChip'; 5 | 6 | export type ChipProps = ComponentPropsWithAs< 7 | { 8 | /** 9 | * ユーザーに提示したい意図 (e.g. エラーならばnegative) を指定 10 | * 11 | * @default 'shade' 12 | */ 13 | intention?: 'shade' | 'primary' | 'secondary' | 'positive' | 'negative' | 'notice' | 'informative'; 14 | 15 | /** 16 | * ラベルを指定 17 | */ 18 | label: string; 19 | 20 | /** 21 | * クリックできる範囲を指定 22 | * 23 | * 通常、limitedは削除可能なChipに使い、他の用途ではなるべくfullを使うことが推奨されます。 24 | * 25 | * @default 'full' 26 | */ 27 | clickableArea?: 'full' | 'limited'; 28 | 29 | /** 30 | * アイコンを指定 31 | * 32 | * clickableAreahがlimitedの場合、デフォルトのiconは (Xボタン) になります。 33 | */ 34 | icon?: ReactNode; 35 | 36 | /** 37 | * クリックしたときの動作を指定 38 | * 39 | * これは必須項目です。クリックできないものについてはBadgeの使用を検討してください。 40 | * クリックできる範囲はclickableArea propsで指定してください。 41 | */ 42 | onClick: (e: MouseEvent>) => void; 43 | 44 | className?: string; 45 | }, 46 | As 47 | >; 48 | 49 | type ChipComponent = (props: ChipProps) => Element; 50 | 51 | export const Chip: ChipComponent = forwardRef( 52 | ({ clickableArea = 'full', ...props }: ChipProps, ref?: Ref) => 53 | ({ 54 | full: {...props} ref={ref} />, 55 | limited: {...props} ref={ref} />, 56 | })[clickableArea], 57 | ); 58 | -------------------------------------------------------------------------------- /packages/for-ui/src/chip/FullChip.tsx: -------------------------------------------------------------------------------- 1 | import { ElementType, forwardRef } from 'react'; 2 | import { Element, Ref } from '../system/componentType'; 3 | import { fsx } from '../system/fsx'; 4 | import { Text } from '../text'; 5 | import { ChipProps } from './Chip'; 6 | 7 | type FullChipProps = Omit, 'clickableArea'>; 8 | 9 | type FullChipComponent = (props: FullChipProps) => Element; 10 | 11 | export const FullChip: FullChipComponent = forwardRef( 12 | (props: FullChipProps, ref: Ref) => { 13 | const { as, label, icon, intention = 'shade', className, ...rest } = props; 14 | const Component: ElementType = as || 'button'; 15 | return ( 16 | 33 | {icon && ( 34 | 48 | {icon} 49 | 50 | )} 51 | 66 | {label} 67 | 68 | 69 | ); 70 | }, 71 | ); 72 | -------------------------------------------------------------------------------- /packages/for-ui/src/chip/LimitedChip.tsx: -------------------------------------------------------------------------------- 1 | import { ElementType, forwardRef } from 'react'; 2 | import { MdClose } from 'react-icons/md'; 3 | import { Element, Ref } from '../system/componentType'; 4 | import { fsx } from '../system/fsx'; 5 | import { Text } from '../text'; 6 | import { ChipProps } from './Chip'; 7 | 8 | type LimitedChipProps = Omit, 'clickableArea'>; 9 | 10 | type LimitedChipComponent = (props: LimitedChipProps) => Element; 11 | 12 | export const LimitedChip: LimitedChipComponent = forwardRef( 13 | (props: LimitedChipProps, ref: Ref) => { 14 | const { as, label, icon = , intention = 'shade', onClick, className, ...rest } = props; 15 | const Component: ElementType = as || 'button'; 16 | 17 | return ( 18 | 34 | 49 | {label} 50 | 51 | 67 | {icon} 68 | 69 | 70 | ); 71 | }, 72 | ); 73 | -------------------------------------------------------------------------------- /packages/for-ui/src/chip/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Chip'; 2 | -------------------------------------------------------------------------------- /packages/for-ui/src/drawer/Drawer.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MdOutlineEdit } from 'react-icons/md'; 3 | import { Meta, Story } from '@storybook/react/types-6-0'; 4 | import { Button } from '../button'; 5 | import { Text } from '../text'; 6 | import { Drawer, drawerAnchorPositions } from './Drawer'; 7 | 8 | export default { 9 | title: 'Feedback / Drawer', 10 | component: Drawer, 11 | argTypes: { 12 | backgroundcolor: { control: 'color' }, 13 | open: { control: 'boolean' }, 14 | anchor: { options: ['left', 'right'], control: 'select' }, 15 | onClose: { action: 'onClose' }, 16 | }, 17 | decorators: [ 18 | (Story: Story) => ( 19 |
20 |
21 | 22 | Drawer 23 | 24 |
25 | 26 | 27 |
28 | ), 29 | ], 30 | } as Meta; 31 | 32 | const Template: Story = (args) => ( 33 | 34 | 35 | Title 36 | 37 | Children 38 | Children1/Children2 39 | 40 | ); 41 | 42 | export const Base = Template.bind({}); 43 | Base.args = { 44 | open: true, 45 | backgroundcolor: '#fff', 46 | }; 47 | 48 | export const WithNavigation = Template.bind({}); 49 | WithNavigation.args = { 50 | open: true, 51 | navigation: ( 52 | 56 | ), 57 | }; 58 | 59 | export const AnchorLeading = Template.bind({}); 60 | AnchorLeading.args = { 61 | open: true, 62 | anchor: drawerAnchorPositions.leading, 63 | }; 64 | 65 | export const AnchorTrailing = Template.bind({}); 66 | AnchorTrailing.args = { 67 | open: true, 68 | anchor: drawerAnchorPositions.trailing, 69 | }; 70 | 71 | export const FixedWidthLeading = Template.bind({}); 72 | FixedWidthLeading.args = { 73 | open: true, 74 | width: 640, 75 | anchor: drawerAnchorPositions.leading, 76 | }; 77 | 78 | export const FixedWidthTrailing = Template.bind({}); 79 | FixedWidthTrailing.args = { 80 | open: true, 81 | width: 640, 82 | anchor: drawerAnchorPositions.trailing, 83 | }; 84 | 85 | export const WithTriggerComponent = Template.bind({}); 86 | WithTriggerComponent.args = { 87 | TriggerComponent: , 88 | onClose: undefined, 89 | }; 90 | 91 | export const WithoutBackdrop = Template.bind({}); 92 | WithoutBackdrop.args = { 93 | TriggerComponent: , 94 | onClose: undefined, 95 | hideBackdrop: true, 96 | }; 97 | -------------------------------------------------------------------------------- /packages/for-ui/src/drawer/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Drawer'; 2 | -------------------------------------------------------------------------------- /packages/for-ui/src/dropzone/Dropzone.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react/types-6-0'; 3 | import { Dropzone } from './Dropzone'; 4 | 5 | export default { 6 | title: 'Form / Dropzone', 7 | component: Dropzone, 8 | decorators: [ 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | (Story: any) => , 11 | ], 12 | argTypes: { 13 | multiple: { control: 'boolean' }, 14 | message: { control: 'text' }, 15 | }, 16 | } as Meta; 17 | 18 | export const Basic = (): JSX.Element => { 19 | const [filesState, setFilesState] = React.useState([]); 20 | const multiple = true; 21 | 22 | const onDrop = React.useCallback( 23 | (files: File[]) => { 24 | if (!multiple && filesState.length > 0) { 25 | return; 26 | } 27 | setFilesState((prevState) => [...prevState, ...files]); 28 | }, 29 | [multiple, filesState.length], 30 | ); 31 | 32 | const onRemove = React.useCallback( 33 | (file: File) => (e: React.MouseEvent) => { 34 | e.stopPropagation(); 35 | setFilesState((prevState) => { 36 | const newState = [...prevState]; 37 | newState.splice(prevState.indexOf(file), 1); 38 | return [...newState]; 39 | }); 40 | }, 41 | [], 42 | ); 43 | 44 | return ; 45 | }; 46 | -------------------------------------------------------------------------------- /packages/for-ui/src/dropzone/Dropzone.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDropzone } from 'react-dropzone'; 3 | import { MdFileUpload } from 'react-icons/md'; 4 | import { Chip } from '../chip'; 5 | import { fsx } from '../system/fsx'; 6 | import { Text } from '../text'; 7 | 8 | export type DropzoneProps = { 9 | files: File[]; 10 | message?: string; 11 | multiple?: boolean; 12 | onDrop: (acceptedFiles: File[]) => void; 13 | onRemove: (file: File) => (e: React.MouseEvent) => void; 14 | className?: string; 15 | }; 16 | 17 | export const Dropzone: React.FC = ({ 18 | files, 19 | onDrop, 20 | onRemove, 21 | message = 'ここにファイルをドロップしてアップロード', 22 | multiple = false, 23 | className, 24 | }) => { 25 | const { getRootProps, getInputProps } = useDropzone({ onDrop, multiple }); 26 | 27 | return ( 28 |
36 | 37 | 38 |
39 | 40 | {message} 41 |
42 | 43 | {files.length > 0 && ( 44 |
45 | {files.map((file) => ( 46 | onRemove(file)(e)} /> 47 | ))} 48 |
49 | )} 50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/for-ui/src/dropzone/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Dropzone'; 2 | -------------------------------------------------------------------------------- /packages/for-ui/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './badge'; 2 | export * from './button'; 3 | export * from './callout'; 4 | export * from './checkbox'; 5 | export * from './chip'; 6 | export * from './drawer'; 7 | export * from './dropzone'; 8 | export * from './menu'; 9 | export * from './modal'; 10 | export * from './radio'; 11 | export * from './popper'; 12 | export * from './select'; 13 | export * from './skeleton'; 14 | export * from './snackbar'; 15 | export * from './stepper'; 16 | export * from './switch'; 17 | export * from './text'; 18 | export * from './textArea'; 19 | export * from './textField'; 20 | export * from './tabs'; 21 | export * from './table'; 22 | export * from './tooltip'; 23 | -------------------------------------------------------------------------------- /packages/for-ui/src/loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { fsx } from '../system/fsx'; 3 | import { Loading } from './Loading'; 4 | 5 | export const Loader: FC<{ className?: string }> = ({ className }) => ( 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/for-ui/src/loader/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { FC, SVGProps } from 'react'; 2 | 3 | export const Loading: FC> = (props) => ( 4 | 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /packages/for-ui/src/loader/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Loader'; 2 | -------------------------------------------------------------------------------- /packages/for-ui/src/menu/Menu.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MdDeleteOutline, MdOutlineEdit, MdOutlineMail, MdOutlinePhone } from 'react-icons/md'; 3 | import { Meta } from '@storybook/react/types-6-0'; 4 | import { Button } from '../button/Button'; 5 | import { Text } from '../text'; 6 | import { Menu } from './Menu'; 7 | import { MenuDivider } from './MenuDivider'; 8 | import { MenuItem } from './MenuItem'; 9 | import { MenuList } from './MenuList'; 10 | 11 | export default { 12 | title: 'Navigation / Menu', 13 | component: Menu, 14 | decorators: [ 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | (Story: any) => ( 17 |
18 | 19 |
20 | ), 21 | ], 22 | } as Meta; 23 | 24 | export const Base = () => { 25 | return ( 26 |
27 | 28 | Menu 29 | 30 | 31 | プロジェクト} 35 | > 36 | 37 | プロフィール 38 | 39 | 40 | 設定 41 | 42 | 43 | プロフィール 44 | 45 | 46 |
47 | ); 48 | }; 49 | 50 | export const WithIcon = () => { 51 | return ( 52 |
53 | 54 | Menu 55 | 56 | 57 | メニューを開く} 61 | > 62 | }>編集 63 | }> 64 | 削除 65 | 66 | 67 | 71 | 電話での問い合わせは今年度末のリリース予定しています。 72 |
73 | 使用可能になり次第、ダッシュボードでお知らせします。 74 | 75 | } 76 | icon={} 77 | > 78 | 電話で問い合わせ 79 |
80 | }> 81 | メールで問い合わせ 82 | 83 |
84 |
85 | ); 86 | }; 87 | 88 | export const WithMenuList = () => ( 89 | 90 | プロフィール 91 | 設定 92 | コンタクト 93 | 94 | ); 95 | -------------------------------------------------------------------------------- /packages/for-ui/src/menu/Menu.tsx: -------------------------------------------------------------------------------- 1 | import { cloneElement, forwardRef, Fragment, ReactElement } from 'react'; 2 | import { bindMenu, bindTrigger } from 'material-ui-popup-state'; 3 | import { usePopupState } from 'material-ui-popup-state/hooks'; 4 | import MuiMenu, { MenuProps as MuiMenuProps } from '@mui/material/Menu'; 5 | import { fsx } from '../system/fsx'; 6 | import { style } from './style'; 7 | 8 | export type MenuProps = Omit & { 9 | TriggerComponent: ReactElement; 10 | className?: string; 11 | }; 12 | 13 | export const Menu = forwardRef( 14 | ( 15 | { 16 | anchorOrigin = { 17 | vertical: 'bottom', 18 | horizontal: 'right', 19 | }, 20 | transformOrigin = { 21 | vertical: 'top', 22 | horizontal: 'right', 23 | }, 24 | children, 25 | TriggerComponent, 26 | className, 27 | ...rest 28 | }, 29 | ref, 30 | ) => { 31 | const popupState = usePopupState({ 32 | variant: 'popover', 33 | popupId: undefined, 34 | }); 35 | 36 | const _TriggerComponent = cloneElement(TriggerComponent, { 37 | ...bindTrigger(popupState), 38 | }); 39 | 40 | return ( 41 | 42 | {_TriggerComponent} 43 | 44 | 56 | {children} 57 | 58 | 59 | ); 60 | }, 61 | ); 62 | -------------------------------------------------------------------------------- /packages/for-ui/src/menu/MenuDivider.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react/types-6-0'; 3 | import { MenuDivider } from './MenuDivider'; 4 | 5 | export default { 6 | title: 'Navigation / Menu / MenuDivider', 7 | component: MenuDivider, 8 | decorators: [ 9 | (Story: Story) => ( 10 |
11 | 12 |
13 | ), 14 | ], 15 | } as Meta; 16 | 17 | export const Playground = {}; 18 | -------------------------------------------------------------------------------- /packages/for-ui/src/menu/MenuDivider.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | import MuiMenuDivider, { DividerProps } from '@mui/material/Divider'; 3 | import { fsx } from '../system/fsx'; 4 | 5 | export type MenuDividerProps = DividerProps<'li'> & { 6 | className?: string; 7 | }; 8 | 9 | export const MenuDivider = forwardRef(({ className, ...rest }, ref) => ( 10 | 16 | )); 17 | -------------------------------------------------------------------------------- /packages/for-ui/src/menu/MenuItem.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MdDeleteOutline, MdOutlineEdit, MdOutlineMail, MdOutlinePhone } from 'react-icons/md'; 3 | import { Meta, Story } from '@storybook/react/types-6-0'; 4 | import { MenuItem } from './MenuItem'; 5 | 6 | const sampleIcons = { 7 | undefined, 8 | MdDeleteOutline: , 9 | MdOutlineEdit: , 10 | MdOutlinePhone: , 11 | MdOutlineMail: , 12 | }; 13 | 14 | export default { 15 | title: 'Navigation / Menu / MenuItem', 16 | component: MenuItem, 17 | argTypes: { 18 | icon: { 19 | options: Object.keys(sampleIcons), 20 | mapping: sampleIcons, 21 | }, 22 | }, 23 | decorators: [ 24 | (Story: Story) => ( 25 |
26 | 27 |
28 | ), 29 | ], 30 | } as Meta; 31 | 32 | export const Playground = { 33 | args: { 34 | children: 'メニューアイテム', 35 | intention: 'shade', 36 | icon: , 37 | description: '説明文', 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/for-ui/src/menu/MenuItem.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, ReactNode } from 'react'; 2 | import { MdCheck } from 'react-icons/md'; 3 | import MuiMenuItem, { MenuItemProps as MuiMenuItemProps } from '@mui/material/MenuItem'; 4 | import { fsx } from '../system/fsx'; 5 | import { Text } from '../text'; 6 | 7 | export type MenuItemProps = Omit & { 8 | /** 9 | * アイコンを表示したい場合にreact-iconsのアイコンや画像を指定 10 | */ 11 | icon?: ReactNode; 12 | 13 | /** 14 | * MenuItemに説明を追加したい場合に指定 15 | */ 16 | description?: ReactNode; 17 | 18 | /** 19 | * 操作の意図を示す場合に指定 20 | * 21 | * 通常、コンテンツの削除など取り返しのつかないユーザーの操作を赤色で表示しますが、その場合intentionにnegativeを設定してください。 22 | * 23 | * @default 'shade' 24 | */ 25 | intention?: 'shade' | 'negative'; 26 | 27 | /** 28 | * MenuItemがMenuやMenuListの開閉に関わらず有効な状態であることを示す場合に指定 29 | * 30 | * 通常、Selectで対象の選択肢を選択中であることを示すために使います 31 | * 32 | * @default false 33 | */ 34 | selected?: boolean; 35 | 36 | className?: string; 37 | }; 38 | 39 | export const MenuItem = forwardRef( 40 | ({ icon, description, intention = 'shade', disabled, className, children, selected, ...rest }, ref) => ( 41 | 60 | {icon && ( 61 |
svg]:h-full [&>svg]:w-full`, 64 | { 65 | shade: `[&_svg]:fill-shade-medium-default`, 66 | negative: `[&_svg]:fill-negative-medium-default`, 67 | }[intention], 68 | disabled && `[&_svg]:fill-shade-medium-disabled`, 69 | ])} 70 | > 71 | {icon} 72 |
73 | )} 74 | 75 | 76 | {children} 77 | 78 | 82 | {description} 83 | 84 | 85 | {selected && ( 86 |
svg]:h-full [&>svg]:w-full`, 89 | { 90 | shade: `[&_svg]:fill-shade-dark-default`, 91 | negative: `[&_svg]:fill-negative-dark-default`, 92 | }[intention], 93 | disabled && `[&_svg]:fill-shade-dark-disabled`, 94 | ])} 95 | > 96 | 97 |
98 | )} 99 |
100 | ), 101 | ); 102 | -------------------------------------------------------------------------------- /packages/for-ui/src/menu/MenuList.tsx: -------------------------------------------------------------------------------- 1 | import { ElementType, forwardRef, Ref } from 'react'; 2 | import MuiMenuList, { MenuListProps as MuiMenuListProps } from '@mui/material/MenuList'; 3 | import { fsx } from '../system/fsx'; 4 | import { style } from './style'; 5 | 6 | export type MenuListProps

= MuiMenuListProps

& { 7 | /** 8 | * レンダリングするコンポーネントを指定 (例: h1, p, strong) 9 | * @default span 10 | */ 11 | as?: P; 12 | 13 | ref?: Ref

; 14 | }; 15 | 16 | const _MenuList =

({ 17 | as, 18 | className, 19 | _ref, 20 | ...rest 21 | }: MenuListProps

& { 22 | _ref: Ref

; 23 | }) => ( 24 | } className={fsx(style, className)} {...rest} /> 25 | ); 26 | 27 | export const MenuList = forwardRef((props, ref) => <_MenuList _ref={ref} {...props} />) as unknown as < 28 | P extends ElementType = 'ul', 29 | >( 30 | props: MenuListProps

, 31 | ) => JSX.Element; 32 | -------------------------------------------------------------------------------- /packages/for-ui/src/menu/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Menu'; 2 | export * from './MenuItem'; 3 | export * from './MenuList'; 4 | export * from './MenuDivider'; 5 | -------------------------------------------------------------------------------- /packages/for-ui/src/menu/style.ts: -------------------------------------------------------------------------------- 1 | import { fsx } from '../system/fsx'; 2 | 3 | export const style = fsx( 4 | `z-modal bg-shade-white-default shadow-more divide-shade-light-default border-shade-light-default grid w-full min-w-min grid-cols-1 divide-y rounded border px-0 py-1`, 5 | ); 6 | -------------------------------------------------------------------------------- /packages/for-ui/src/modal/Modal.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState } from '@storybook/addons'; 3 | import { Meta } from '@storybook/react/types-6-0'; 4 | import { Modal } from '.'; 5 | import { Button } from '../button'; 6 | import { ModalContent } from './ModalContent'; 7 | import { ModalFooter } from './ModalFooter'; 8 | import { ModalHeader } from './ModalHeader'; 9 | 10 | export default { 11 | title: 'Feedback / Modal', 12 | component: Modal, 13 | } as Meta; 14 | 15 | export const ModalDefault = (): JSX.Element => { 16 | const [showModal, setShowModal] = useState(true); 17 | 18 | const onSubmit = () => { 19 | console.error('submit'); 20 | }; 21 | 22 | return ( 23 |

24 |

Default Modal

25 |
26 | 29 | 30 | setShowModal(false)}> 31 | 本当に削除してよろしいですか? 32 | 33 |

34 | アカウントを無効にしてもよろしいですか?すべてのデータは完全に削除されます。このアクションは元に戻せません。 35 |

36 |
37 | 38 | 41 | 44 | 45 |
46 |
47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /packages/for-ui/src/modal/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import MuiBackdrop, { BackdropProps as MuiBackdropProps } from '@mui/material/Backdrop'; 3 | import MuiModal, { ModalProps as MuiModalProps } from '@mui/material/Modal'; 4 | 5 | type BackdropProps = MuiBackdropProps; 6 | 7 | export type ModalProps = Omit & { 8 | /** Whether the Dialog is open */ 9 | open: boolean; 10 | 11 | children: React.ReactNode | React.ReactNode[]; 12 | 13 | /** Handler that is called when the 'cancel' button of a dismissable Dialog is clicked. */ 14 | onClose?(event: React.MouseEvent | React.KeyboardEvent): void; 15 | 16 | className?: string; 17 | }; 18 | 19 | const Backdrop: React.FC = ({ open, children, onClick }) => { 20 | return ( 21 | 28 | {children} 29 | 30 | ); 31 | }; 32 | 33 | export const Modal: React.FC = forwardRef(({ open, onClose, children, className, ...props }, ref) => { 34 | return ( 35 | 48 |
49 |
50 | {children} 51 |
52 |
53 |
54 | ); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/for-ui/src/modal/ModalContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export type ModalContentProps = { 4 | children: React.ReactNode; 5 | }; 6 | 7 | export const ModalContent: React.FC = ({ children }) => { 8 | return
{children}
; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/for-ui/src/modal/ModalFooter.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | 3 | type Props = { 4 | className?: string; 5 | /** The contents of the Dialog. */ 6 | children: ReactNode; 7 | }; 8 | 9 | export const ModalFooter: React.FC = ({ children }) => { 10 | return
{children}
; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/for-ui/src/modal/ModalHeader.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { Text } from '../text'; 3 | 4 | type Props = { 5 | /** The contents of the Dialog. */ 6 | children: ReactNode | string; 7 | }; 8 | 9 | export const ModalHeader: React.FC = ({ children }) => { 10 | return ( 11 |
12 | 13 | {children} 14 | 15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/for-ui/src/modal/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Modal'; 2 | export * from './ModalContent'; 3 | export * from './ModalHeader'; 4 | export * from './ModalFooter'; 5 | -------------------------------------------------------------------------------- /packages/for-ui/src/popper/Popper.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Meta } from '@storybook/react/types-6-0'; 3 | import { Button } from '../button/Button'; 4 | import { MenuItem } from '../menu'; 5 | import { Text } from '../text'; 6 | import { Popper } from './Popper'; 7 | 8 | export default { 9 | title: 'Navigation / Popper', 10 | component: Popper, 11 | argTypes: { 12 | backgroundColor: { control: 'color' }, 13 | }, 14 | decorators: [ 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | (Story: any) => ( 17 |
18 | 19 |
20 | ), 21 | ], 22 | } as Meta; 23 | 24 | export const Base = () => { 25 | return ( 26 |
27 |
28 | 29 | Popper 30 | 31 |
32 | 33 | プロジェクト}> 34 |
35 |
こんにちは
36 |
こんにちは
37 |
こんにちは
38 |
39 |
40 |
41 | ); 42 | }; 43 | 44 | export const WithCustomClose = () => { 45 | return ( 46 |
47 |
48 | 49 | Popper 50 | 51 |
52 | 53 | TriggerComponent={}> 54 | {({ onClick }) => ( 55 |
56 | プロフィール 57 | 設定 58 | 閉じる 59 |
60 | )} 61 | 62 |
63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /packages/for-ui/src/popper/Popper.tsx: -------------------------------------------------------------------------------- 1 | import { cloneElement, Fragment, isValidElement, MouseEvent, ReactElement, ReactNode } from 'react'; 2 | import { bindPopper, bindTrigger } from 'material-ui-popup-state'; 3 | import { usePopupState } from 'material-ui-popup-state/hooks'; 4 | import Fade from '@mui/material/Fade'; 5 | import Paper from '@mui/material/Paper'; 6 | import MuiPopper from '@mui/material/Popper'; 7 | import { fsx } from '../system/fsx'; 8 | 9 | type PoppeChildrenProps = { 10 | onClick: (e: MouseEvent) => void; 11 | }; 12 | 13 | export type PopperProps = { 14 | TriggerComponent: ReactNode; 15 | 16 | children: ReactElement | ((props: PoppeChildrenProps) => ReactElement); 17 | }; 18 | 19 | export const Popper = (props: PopperProps) => { 20 | const popupState = usePopupState({ 21 | variant: 'popover', 22 | popupId: undefined, 23 | }); 24 | 25 | const trigger = bindTrigger(popupState); 26 | let _TriggerComponent = <>; 27 | if (isValidElement(props.TriggerComponent)) { 28 | _TriggerComponent = cloneElement(props.TriggerComponent, { 29 | ...trigger, 30 | }); 31 | } 32 | 33 | return ( 34 | 35 | {_TriggerComponent} 36 | 37 | 38 | {({ TransitionProps }) => ( 39 | 40 | 45 | {typeof props.children === 'function' ? props.children({ onClick: popupState.close }) : props.children} 46 | 47 | 48 | )} 49 | 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/for-ui/src/popper/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Popper'; 2 | -------------------------------------------------------------------------------- /packages/for-ui/src/radio/Radio.tsx: -------------------------------------------------------------------------------- 1 | import { FC, forwardRef, ReactNode } from 'react'; 2 | import MuiRadio, { RadioProps as MuiRadioProps } from '@mui/material/Radio'; 3 | import { fsx } from '../system/fsx'; 4 | import { TextDefaultStyler } from '../system/TextDefaultStyler'; 5 | import { Text } from '../text'; 6 | 7 | export type RadioProps = Omit & { 8 | label?: ReactNode; 9 | nopadding?: boolean; 10 | className?: string; 11 | }; 12 | 13 | const Indicator: FC<{ checked: boolean; disabled: boolean }> = ({ checked, disabled }) => ( 14 | 21 | ); 22 | 23 | export const Radio = forwardRef( 24 | ({ nopadding, label, value, disabled, className, ...rest }, ref) => ( 25 | 26 | } 32 | checkedIcon={} 33 | classes={{ 34 | root: fsx(nopadding ? `p-0` : `p-1`), 35 | }} 36 | {...rest} 37 | /> 38 | ( 41 | 46 | )} 47 | /> 48 | 49 | ), 50 | ); 51 | -------------------------------------------------------------------------------- /packages/for-ui/src/radio/RadioGroup.tsx: -------------------------------------------------------------------------------- 1 | import { Children, cloneElement, ComponentProps, forwardRef, isValidElement, PropsWithoutRef, ReactNode } from 'react'; 2 | import FormControl from '@mui/material/FormControl'; 3 | import MuiRadioGroup, { RadioGroupProps as MuiRadioGroupProps } from '@mui/material/RadioGroup'; 4 | import { fsx } from '../system/fsx'; 5 | import { TextDefaultStyler } from '../system/TextDefaultStyler'; 6 | import { Text } from '../text'; 7 | import { Radio } from './Radio'; 8 | 9 | export type RadioGroupProps = Omit, 'color'> & { 10 | name?: string; 11 | row?: boolean; 12 | disabled?: boolean; 13 | required?: boolean; 14 | label?: ReactNode; 15 | error?: boolean; 16 | helperText?: ReactNode; 17 | className?: string; 18 | children: ReactNode; 19 | }; 20 | 21 | export const RadioGroup = forwardRef( 22 | ( 23 | { className, name, label, row, defaultValue, error, helperText, children, required = false, disabled, ...rest }, 24 | ref, 25 | ) => ( 26 | 33 | ( 36 | 37 | {/* due to the CSS bug, legend element cannot be styled if contents not specified. But contents makes difficult to style, so wrap by Text. */} 38 | 39 | {children} 40 | {required && *} 41 | 42 | 43 | )} 44 | /> 45 | 53 | { 54 | // Pass ref to children (Radio) if provided to RadioGroup 55 | Children.map(children, (child) => 56 | isValidElement>(child) 57 | ? cloneElement(child, { ref: ref ? ref : undefined }) 58 | : child, 59 | ) 60 | } 61 | 62 | ( 65 | 71 | )} 72 | /> 73 | 74 | ), 75 | ); 76 | -------------------------------------------------------------------------------- /packages/for-ui/src/radio/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Radio'; 2 | export * from './RadioGroup'; 3 | -------------------------------------------------------------------------------- /packages/for-ui/src/select/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Select'; 2 | -------------------------------------------------------------------------------- /packages/for-ui/src/skeleton/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Children, cloneElement, FC, Fragment, isValidElement, ReactNode } from 'react'; 2 | import MuiSkeleton, { SkeletonProps as MuiSkeletonProps } from '@mui/material/Skeleton'; 3 | import { fsx } from '../system/fsx'; 4 | import { TextProps } from '../text'; 5 | 6 | export type SkeletonProps = MuiSkeletonProps & { 7 | loading?: boolean; 8 | className?: string; 9 | count?: number; 10 | 11 | /** 12 | * テキストの代わりに表示する場合はテキストのサイズを指定 13 | */ 14 | size?: Exclude['size'], 'inherit'>; 15 | }; 16 | 17 | export const Skeleton: FC = ({ loading = false, count = 1, className, children, size, ...rest }) => { 18 | if (!children) { 19 | return ( 20 | 37 | ); 38 | } 39 | if (loading) { 40 | return ( 41 | <> 42 | {[...Array(count)].map((_, idx) => ( 43 | 44 | {Children.count(children) > 0 && Children.toArray(children)[0]} 45 | 46 | ))} 47 | 48 | ); 49 | } 50 | if (isValidElement(children)) { 51 | return children; 52 | } 53 | return {children}; 54 | }; 55 | 56 | const recursiveChildren = (children: ReactNode, empty: ReactNode): ReactNode => { 57 | if (!children) return <>; 58 | 59 | return Children.map(children, (child: ReactNode) => { 60 | if (!isValidElement(child)) { 61 | return child; 62 | } 63 | 64 | if (!isValidElement(empty)) { 65 | return empty; 66 | } 67 | 68 | if (Children.count(child.props.children) > 1) { 69 | return recursiveChildren(child.props.children, empty); 70 | } 71 | if (Children.count(child.props.children) === 0) { 72 | return {cloneElement(empty, empty.props)}; 73 | } 74 | return {cloneElement(child, child.props)}; 75 | }); 76 | }; 77 | 78 | type SkeletonXProps = MuiSkeletonProps & { 79 | loading?: boolean; 80 | empty?: ReactNode; 81 | }; 82 | 83 | export const SkeletonX: FC = ({ 84 | loading = false, 85 | empty =
xxxxxxxxxxxxxxx
, 86 | children, 87 | }) => { 88 | if (loading) { 89 | return {recursiveChildren(children, empty)}; 90 | } 91 | if (isValidElement(children)) { 92 | return children; 93 | } 94 | return {children}; 95 | }; 96 | -------------------------------------------------------------------------------- /packages/for-ui/src/skeleton/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Skeleton'; 2 | -------------------------------------------------------------------------------- /packages/for-ui/src/snackbar/Snackbar.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, useState } from 'react'; 2 | import { Meta } from '@storybook/react/types-6-0'; 3 | import { Button } from '../button'; 4 | import { Text } from '../text'; 5 | import { Snackbar, SnackbarProps } from './Snackbar'; 6 | import { SnackbarProvider, useSnackbar } from './SnackbarContext'; 7 | 8 | export default { 9 | title: 'Feedback / Snackbar', 10 | component: Snackbar, 11 | } as Meta; 12 | 13 | export const Playground = { 14 | args: { 15 | open: true, 16 | message: 'テスト', 17 | autoHide: true, 18 | autoHideDuration: null, 19 | }, 20 | }; 21 | 22 | export const Stateless = () => 開く} message="操作が完了しました" />; 23 | 24 | export const StatelessAutoHide = () => ( 25 | 開く} autoHide message="操作が完了しました" /> 26 | ); 27 | 28 | export const Stateful = () => { 29 | const [open, setOpen] = useState(false); 30 | const openHandler = () => { 31 | setOpen(true); 32 | }; 33 | const closeHandler = () => { 34 | setOpen(false); 35 | }; 36 | return ( 37 | 38 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | export const StatefulAutoHide = () => { 45 | const [open, setOpen] = useState(false); 46 | const openHandler = () => { 47 | setOpen(true); 48 | }; 49 | const closeHandler = () => { 50 | setOpen(false); 51 | }; 52 | return ( 53 | 54 | 55 | 56 | 57 | ); 58 | }; 59 | 60 | export const WithContext = () => { 61 | const App = () => { 62 | const { openSnackbar } = useSnackbar(); 63 | return ( 64 |
65 | 68 | 71 |
72 | ); 73 | }; 74 | return ( 75 | 76 | 77 | Snackbarは1画面に1つのみ表示することを強く推奨しています。この挙動はSnackbarProviderを使うことで簡単に実現できます。 78 | 79 | 80 | 81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /packages/for-ui/src/snackbar/Snackbar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | cloneElement, 3 | forwardRef, 4 | Fragment, 5 | isValidElement, 6 | ReactElement, 7 | SyntheticEvent, 8 | useId, 9 | useState, 10 | } from 'react'; 11 | import MuiSnackbar, { SnackbarProps as MuiSnackbarProps, SnackbarCloseReason } from '@mui/material/Snackbar'; 12 | import MuiSnackbarContent, { SnackbarContentProps as MuiSnackbarContentProps } from '@mui/material/SnackbarContent'; 13 | import { Button } from '../button'; 14 | import { fsx } from '../system/fsx'; 15 | import { Text } from '../text'; 16 | 17 | export type SnackbarProps = MuiSnackbarProps & { 18 | /** 19 | * 自動的にSnackbarが消えるまでの時間を指定 20 | * 21 | * 指定されているときは閉じるボタンは表示されません。 22 | * 消えるまでの時間はミリ秒で指定します。_message_を読むのに十分な時間を与えるようにしてください。 23 | * 24 | * @default 5000 25 | */ 26 | autoHideDuration?: MuiSnackbarProps['autoHideDuration']; 27 | 28 | /** 29 | * 自動的にSnackbarを消すかどうかを指定 30 | * 31 | * ユーザーが閉じるボタンを押す あるいは SnackbarProvider下では新しいSnackbarが開かれる ことでのみ閉じられます。 32 | */ 33 | autoHide?: boolean; 34 | 35 | /** 36 | * 表示するメッセージを指定 37 | */ 38 | message: MuiSnackbarProps['message']; 39 | 40 | /** 41 | * 操作の起点となるコンポーネントを指定 42 | * 43 | * ボタンを押すと即座に表示のように使うことができますが、通常はユーザーの操作と処理の完了は完全に同義ではないため、そのような場面では使用は非推奨です。 44 | */ 45 | TriggerComponent?: ReactElement<{ onClick: () => void }>; 46 | 47 | className?: string; 48 | }; 49 | 50 | export const Snackbar = forwardRef( 51 | ( 52 | { 53 | TriggerComponent, 54 | autoHideDuration = 5000, 55 | autoHide = false, 56 | onClose, 57 | onClick, 58 | message, 59 | action, 60 | className, 61 | TransitionProps, 62 | ...rest 63 | }, 64 | ref, 65 | ) => { 66 | const [open, setOpen] = useState(true); 67 | const handleClose = (e: Event | SyntheticEvent, reason: SnackbarCloseReason) => { 68 | onClose?.(e, reason); 69 | setOpen(false); 70 | }; 71 | const handleOpen = () => { 72 | setOpen(true); 73 | }; 74 | const Trigger = 75 | isValidElement(TriggerComponent) && 76 | cloneElement(TriggerComponent, { 77 | onClick: handleOpen, 78 | }); 79 | return ( 80 | 81 | {Trigger} 82 | { 108 | onClick?.(e); 109 | autoHide && handleClose(e, 'escapeKeyDown'); 110 | }} 111 | {...rest} 112 | > 113 | 114 | 115 | 116 | ); 117 | }, 118 | ); 119 | 120 | export type SnackbarContentProps = MuiSnackbarContentProps & { 121 | autoHide?: boolean; 122 | onClose?: SnackbarProps['onClose']; 123 | message?: MuiSnackbarContentProps['message']; 124 | action?: MuiSnackbarContentProps['action']; 125 | }; 126 | 127 | export const SnackbarContent = forwardRef( 128 | ({ autoHide, message, action, onClose, onClick, className, ...rest }, ref) => { 129 | const messageId = useId(); 130 | return ( 131 | { 142 | onClick?.(e); 143 | autoHide && onClose?.(e, 'escapeKeyDown'); 144 | }} 145 | message={{message}} 146 | action={ 147 | action || 148 | (!autoHide && ( 149 | 159 | )) 160 | } 161 | {...rest} 162 | /> 163 | ); 164 | }, 165 | ); 166 | -------------------------------------------------------------------------------- /packages/for-ui/src/snackbar/SnackbarContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, FC, ReactNode, useContext, useState } from 'react'; 2 | import { Snackbar, SnackbarProps } from './Snackbar'; 3 | 4 | type SnackbarContextValue = { 5 | inProvider: boolean; 6 | snackbar?: SnackbarProps; 7 | enqueue: (props: SnackbarProps) => void; 8 | }; 9 | 10 | // TODO: Rewrite using `satisfies` once jest supports TypeScript v4.9 11 | const initialSnackbarContextValue: SnackbarContextValue = { 12 | inProvider: false, 13 | enqueue: () => undefined, 14 | }; 15 | 16 | const SnackbarContext = createContext(initialSnackbarContextValue); 17 | 18 | // TODO: Refactor using Portal. See https://react.dev/reference/react-dom/createPortal 19 | const SnackbarView = () => { 20 | const { snackbar } = useContext(SnackbarContext); 21 | if (!snackbar) { 22 | return null; 23 | } 24 | return ; 25 | }; 26 | 27 | export type SnackbarProviderProps = { 28 | children: ReactNode; 29 | }; 30 | 31 | export const SnackbarProvider: FC = ({ children, ...rest }) => { 32 | const [snackbar, setSnackbar] = useState(); 33 | const enqueue = (props: SnackbarProps) => { 34 | setSnackbar(props); 35 | }; 36 | return ( 37 | 45 | {children} 46 | 47 | 48 | ); 49 | }; 50 | 51 | type UseSnackbar = { 52 | inProvider: boolean; 53 | openSnackbar: (props: SnackbarProps) => void; 54 | }; 55 | 56 | export const useSnackbar = (): UseSnackbar => { 57 | const { inProvider, enqueue } = useContext(SnackbarContext); 58 | return { 59 | inProvider, 60 | openSnackbar: enqueue, 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /packages/for-ui/src/snackbar/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Snackbar'; 2 | export * from './SnackbarContext'; 3 | -------------------------------------------------------------------------------- /packages/for-ui/src/stepper/Step.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | import { MdOutlineDone } from 'react-icons/md'; 3 | import MuiStep, { StepProps as MuiStepProps } from '@mui/material/Step'; 4 | import { StepIconProps as MuiStepIconProps } from '@mui/material/StepIcon'; 5 | import MuiStepLabel, { StepLabelProps as MuiStepLabelProps } from '@mui/material/StepLabel'; 6 | import { fsx } from '../system/fsx'; 7 | import { Text } from '../text'; 8 | 9 | export type StepProps = MuiStepProps; 10 | 11 | export type StepLabelProps = MuiStepLabelProps; 12 | 13 | export const Step = forwardRef(({ children, active, className, ...rest }, ref) => ( 14 | 15 | 24 | 25 | {children} 26 | 27 | 28 | 29 | )); 30 | 31 | const Icon = ({ completed, active, icon }: Partial) => ( 32 | 40 | 44 | {completed ? : icon} 45 | 46 | 47 | ); 48 | -------------------------------------------------------------------------------- /packages/for-ui/src/stepper/Stepper.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta, Story } from '@storybook/react/types-6-0'; 3 | import { Text } from '../text'; 4 | import { Step } from './Step'; 5 | import { Stepper } from './Stepper'; 6 | 7 | export default { 8 | title: 'Navigation / Stepper', 9 | component: Stepper, 10 | argTypes: { 11 | activeStep: { control: { min: 1 } }, 12 | alternativeLabel: { control: 'boolean' }, 13 | backgroundColor: { control: 'color' }, 14 | }, 15 | decorators: [(Story: Story) => ], 16 | } as Meta; 17 | 18 | const steps = ['First', 'Second', 'Third', 'Last']; 19 | 20 | export const LabelBottom: Story = (args) => ( 21 |
22 | 23 | Stepper 24 | 25 | 26 | {steps.map((step, index) => ( 27 | {step} 28 | ))} 29 | 30 | 31 | {steps.map((step, index) => ( 32 | {step} 33 | ))} 34 | 35 | 36 | {steps.map((step, index) => ( 37 | {step} 38 | ))} 39 | 40 | 41 | {steps.map((step, index) => ( 42 | {step} 43 | ))} 44 | 45 |
46 | ); 47 | 48 | export const LabelTrailing: Story = (args) => ( 49 |
50 | 51 | Stepper 52 | 53 | 54 | {steps.map((step, index) => ( 55 | {step} 56 | ))} 57 | 58 | 59 | {steps.map((step, index) => ( 60 | {step} 61 | ))} 62 | 63 | 64 | {steps.map((step, index) => ( 65 | {step} 66 | ))} 67 | 68 | 69 | {steps.map((step, index) => ( 70 | {step} 71 | ))} 72 | 73 |
74 | ); 75 | -------------------------------------------------------------------------------- /packages/for-ui/src/stepper/Stepper.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | import { StepConnector } from '@mui/material'; 3 | import MuiStepper, { StepperProps as MuiStepperProps } from '@mui/material/Stepper'; 4 | import { fsx } from '../system/fsx'; 5 | 6 | export type StepperProps = Omit & { 7 | /** 8 | * ラベルを表示する位置を指定 9 | * 10 | * @default 'bottom' 11 | */ 12 | labelPosition?: 'bottom' | 'trailing'; 13 | 14 | /** 15 | * @deprecated labelPositionを使用してください 16 | */ 17 | alternativeLabel?: MuiStepperProps['alternativeLabel']; 18 | 19 | className?: string; 20 | }; 21 | 22 | export const Stepper = forwardRef( 23 | ({ activeStep, alternativeLabel, labelPosition = 'bottom', children, className, ...rest }, ref) => ( 24 | .MuiStepConnector-line]:border-primary-dark-default -left-2/4 right-0 top-0 flex h-6 w-full items-center px-0 [&.MuiStepConnector-alternativeLabel]:pl-3 [&.MuiStepConnector-alternativeLabel]:pr-4`, 33 | ), 34 | line: fsx(`border-shade-dark-disabled w-full border-t-2`), 35 | }} 36 | /> 37 | } 38 | className={fsx(`gap-1`, className)} 39 | {...rest} 40 | > 41 | {children} 42 | 43 | ), 44 | ); 45 | -------------------------------------------------------------------------------- /packages/for-ui/src/stepper/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Stepper'; 2 | export * from './Step'; 3 | -------------------------------------------------------------------------------- /packages/for-ui/src/switch/Switch.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { Controller, useForm } from 'react-hook-form'; 3 | import * as yup from 'yup'; 4 | import { yupResolver } from '@hookform/resolvers/yup'; 5 | import { Meta } from '@storybook/react/types-6-0'; 6 | import { Button } from '../button'; 7 | import { fsx } from '../system/fsx'; 8 | import { Text } from '../text'; 9 | import { Switch } from './Switch'; 10 | import { SwitchGroup } from './SwitchGroup'; 11 | 12 | export default { 13 | title: 'Form / Switch', 14 | component: Switch, 15 | argTypes: {}, 16 | } as Meta; 17 | 18 | export const Basic = (): JSX.Element => { 19 | const { control, handleSubmit } = useForm<{ 20 | default1: boolean; 21 | default2: boolean; 22 | disable1: boolean; 23 | disable2: boolean; 24 | }>({ 25 | reValidateMode: 'onChange', 26 | }); 27 | 28 | const onSubmit = (data: unknown) => console.info(data); 29 | 30 | return ( 31 |
32 |
33 | 34 | { 39 | return ; 40 | }} 41 | /> 42 | { 47 | return ; 48 | }} 49 | /> 50 | { 55 | return ; 56 | }} 57 | /> 58 | { 63 | return ; 64 | }} 65 | /> 66 | 67 | 68 |
69 | 70 |
71 |
72 |
73 | ); 74 | }; 75 | 76 | export const WithReactHookForm = () => { 77 | const schema = yup.object({ 78 | autosave: yup.string().required(), 79 | }); 80 | type FieldValue = yup.InferType; 81 | 82 | const { register, handleSubmit } = useForm({ 83 | resolver: yupResolver(schema), 84 | }); 85 | const onSubmit = (data: unknown) => { 86 | console.info(data); 87 | }; 88 | 89 | return ( 90 |
91 | 92 | 93 | 94 | ); 95 | }; 96 | 97 | export const WithSwitchGroup = () => { 98 | const allPreferences = { 99 | spring: '春', 100 | summer: '夏', 101 | autumn: '秋', 102 | winter: '冬', 103 | } as const; 104 | 105 | const schema = yup.object({ 106 | preferences: yup.object(Object.fromEntries(Object.keys(allPreferences).map((k) => [k, yup.boolean()]))), 107 | }); 108 | 109 | type FieldValue = yup.InferType; 110 | 111 | const { 112 | register, 113 | handleSubmit, 114 | formState: { errors }, 115 | } = useForm({ 116 | resolver: yupResolver(schema), 117 | }); 118 | const onSubmit = useCallback((data: unknown) => { 119 | console.info(data); 120 | }, []); 121 | return ( 122 |
123 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 |
137 | ); 138 | }; 139 | 140 | export const CustomLabel = () => ( 141 | 144 | ラベル 145 |
146 | } 147 | /> 148 | ); 149 | -------------------------------------------------------------------------------- /packages/for-ui/src/switch/Switch.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithRef, forwardRef, ReactNode } from 'react'; 2 | import { fsx } from '../system/fsx'; 3 | import { TextDefaultStyler } from '../system/TextDefaultStyler'; 4 | import { Text } from '../text'; 5 | 6 | export type SwitchProps = ComponentPropsWithRef<'input'> & { 7 | label?: ReactNode; 8 | className?: string; 9 | }; 10 | 11 | export const Switch = forwardRef(({ label, disabled, className, ...rest }, ref) => ( 12 | 13 | 26 | ( 29 | 30 | )} 31 | /> 32 | 33 | )); 34 | -------------------------------------------------------------------------------- /packages/for-ui/src/switch/SwitchGroup.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, ReactNode } from 'react'; 2 | import FormControl from '@mui/material/FormControl'; 3 | import FormGroup, { FormGroupProps } from '@mui/material/FormGroup'; 4 | import { fsx } from '../system/fsx'; 5 | import { TextDefaultStyler } from '../system/TextDefaultStyler'; 6 | import { Text } from '../text'; 7 | 8 | export type SwitchGroupProps = Omit & { 9 | name?: string; 10 | row?: boolean; 11 | required?: boolean; 12 | disabled?: boolean; 13 | label?: ReactNode; 14 | error?: boolean; 15 | helperText?: ReactNode; 16 | className?: string; 17 | children: ReactNode; 18 | }; 19 | 20 | export const SwitchGroup = forwardRef( 21 | ({ required, disabled, label, error, row, helperText, className, ...rest }, ref) => { 22 | return ( 23 | 30 | ( 33 | 34 | {/* due to the CSS bug, legend element cannot be styled if contents not specified. But contents makes difficult to style, so wrap by Text. */} 35 | 36 | {children} 37 | {required && *} 38 | 39 | 40 | )} 41 | /> 42 | 43 | ( 46 | 52 | )} 53 | /> 54 | 55 | ); 56 | }, 57 | ); 58 | -------------------------------------------------------------------------------- /packages/for-ui/src/switch/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Switch'; 2 | export * from './SwitchGroup'; 3 | -------------------------------------------------------------------------------- /packages/for-ui/src/system/PropsCascader.tsx: -------------------------------------------------------------------------------- 1 | import { Children, cloneElement, FC, ForwardedRef, forwardRef, HTMLAttributes, isValidElement, ReactNode } from 'react'; 2 | 3 | type PropsCascaderProps> = T & { 4 | children: ReactNode; 5 | }; 6 | 7 | type PropsCascaderComponent = >(props: PropsCascaderProps) => ReturnType; 8 | 9 | export const PropsCascader: PropsCascaderComponent = forwardRef( 10 | >( 11 | { children, ...props }: PropsCascaderProps, 12 | ref: ForwardedRef, 13 | ) => { 14 | return Children.map(children, (child: ReactNode) => { 15 | if (!isValidElement(child)) { 16 | return child; 17 | } 18 | return cloneElement(child, { ...props, ...child.props, ref }); 19 | }); 20 | }, 21 | ); 22 | -------------------------------------------------------------------------------- /packages/for-ui/src/system/TextDefaultStyler.tsx: -------------------------------------------------------------------------------- 1 | import { FC, isValidElement, ReactElement, ReactNode } from 'react'; 2 | 3 | export type TextDefaultStylerProps = { 4 | /** 5 | * 適用されるラベル等を指定 6 | * 7 | * contentがstringやnumberなどReactElementではない場合にdefaultRendererを用いて描画されます。 8 | * contentがReactElementの場合はdefaultRendererを用いずにそのまま描画されます。 9 | */ 10 | content: ReactNode; 11 | 12 | /** 13 | * contentがReactElementではない場合に描画に用いるコンポーネントを指定 14 | */ 15 | defaultRenderer: FC<{ children: Exclude }>; 16 | }; 17 | 18 | export const TextDefaultStyler: FC = ({ content, defaultRenderer: DefaultRenderer }) => { 19 | if (!content) { 20 | return null; 21 | } 22 | // TODO: To eliminate the gap, `any` is used. (ReactNode's ReactElement is ReactElement, but isValidElement's returned ReactElement is ReactElement) 23 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 24 | if (isValidElement(content)) { 25 | return content; 26 | } 27 | return {content}; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/for-ui/src/system/componentType.ts: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, ComponentPropsWithRef, ElementType, FC } from 'react'; 2 | import { PreservedOmit } from './preservedOmit'; 3 | 4 | export type Ref = ComponentPropsWithRef['ref']; 5 | 6 | export type RefProps = { ref?: Ref }; 7 | 8 | export type AsProps = { 9 | /** 10 | * レンダリングするコンポーネントを指定 (例: button, a, input) 11 | */ 12 | as?: As; 13 | }; 14 | 15 | export type ComponentPropsWithAs = AsProps & 16 | Props & 17 | RefProps & 18 | PreservedOmit, keyof (Props & AsProps & RefProps)>; 19 | 20 | export type ElementTypeToHTMLElement = Element extends keyof HTMLElementTagNameMap 21 | ? HTMLElementTagNameMap[Element] 22 | : Element; 23 | 24 | export type Element = ReturnType; 25 | -------------------------------------------------------------------------------- /packages/for-ui/src/system/fsx.ts: -------------------------------------------------------------------------------- 1 | // fsx merges tailwind classes and other classes along with For Design System Token. 2 | import clsx, { ClassValue } from 'clsx'; 3 | import { extendTailwindMerge } from 'tailwind-merge'; 4 | 5 | const twMerge = extendTailwindMerge({ 6 | classGroups: { 7 | 'font-size': [ 8 | { 9 | text: ['xs', 's', 'r', 'xr', 'l', 'xl'], 10 | }, 11 | ], 12 | }, 13 | }); 14 | 15 | export const fsx = (...classes: ClassValue[]): string => twMerge(clsx(classes)); 16 | -------------------------------------------------------------------------------- /packages/for-ui/src/system/preservedOmit.ts: -------------------------------------------------------------------------------- 1 | export type PreservedOmit = { 2 | [Property in keyof T as Exclude]: T[Property]; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/for-ui/src/system/walkChildren.ts: -------------------------------------------------------------------------------- 1 | import { Children, isValidElement, ReactNode } from 'react'; 2 | 3 | // walkChildren visits children recursively for given ReactNode root. 4 | // If given f returns true, it will stop walking at the node (no more seeking children recursively). 5 | export const walkChildren = (root: ReactNode, f: (node: ReactNode) => boolean | void): boolean => { 6 | Children.forEach(root, (node) => { 7 | if (f(node)) { 8 | return; 9 | } 10 | if (!isValidElement(node)) { 11 | return; 12 | } 13 | walkChildren(node.props.children, f); 14 | }); 15 | return true; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/for-ui/src/table/ColumnDef.ts: -------------------------------------------------------------------------------- 1 | export type { ColumnDef } from '@tanstack/react-table'; 2 | -------------------------------------------------------------------------------- /packages/for-ui/src/table/TableCell.tsx: -------------------------------------------------------------------------------- 1 | import { FC, forwardRef, HTMLAttributes, ReactNode } from 'react'; 2 | import { MdArrowDownward, MdArrowUpward } from 'react-icons/md'; 3 | import { ComponentPropsWithAs, Element, Ref } from '../system/componentType'; 4 | import { fsx } from '../system/fsx'; 5 | import { Text } from '../text'; 6 | 7 | export type TableCellProps = ComponentPropsWithAs< 8 | { 9 | /** 10 | * @deprecated `as` propを使ってください 11 | */ 12 | component?: 'th' | 'td'; 13 | 14 | className?: string; 15 | }, 16 | As 17 | >; 18 | 19 | type TableCellComponent = (props: TableCellProps) => Element; 20 | 21 | export const TableCell: TableCellComponent = forwardRef( 22 | ({ component = 'td', as, className, ...rest }: TableCellProps, ref?: Ref) => { 23 | const Component = as || component || 'td'; 24 | return ( 25 | 37 | ); 38 | }, 39 | ); 40 | 41 | export const SortableTableCellHead: FC< 42 | TableCellProps<'th'> & { 43 | sortable: boolean; 44 | sorted: false | 'asc' | 'desc'; 45 | nextSortingOrder: false | 'asc' | 'desc'; 46 | children: ReactNode; 47 | disabled?: boolean; 48 | onClick?: HTMLAttributes['onClick']; 49 | } 50 | > = ({ sortable, sorted, nextSortingOrder, onClick, disabled, children, ...rest }) => ( 51 | 57 | {sortable ? ( 58 | 85 | ) : ( 86 | {children} 87 | )} 88 | 89 | ); 90 | -------------------------------------------------------------------------------- /packages/for-ui/src/table/TablePagination.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { render, screen } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { Pagination } from './TablePagination'; 5 | 6 | describe('Pagination', () => { 7 | it('is rendered with first, last, prev, next, and current page buttons', async () => { 8 | render(); 9 | expect(screen.queryByRole('button', { name: '最初のページへ移動' })).toBeInTheDocument(); 10 | expect(screen.queryByRole('button', { name: '最後のページへ移動' })).toBeInTheDocument(); 11 | expect(screen.queryByRole('button', { name: '前のページへ移動' })).toBeInTheDocument(); 12 | expect(screen.queryByRole('button', { name: '次のページへ移動' })).toBeInTheDocument(); 13 | expect(screen.queryByRole('button', { name: 'ページ1へ移動' })).toBeInTheDocument(); 14 | expect(screen.queryByRole('button', { name: 'ページ100へ移動' })).toBeInTheDocument(); 15 | expect(screen.queryByRole('button', { name: '現在のページ ページ50' })).toBeInTheDocument(); 16 | }); 17 | it('calls event handlers when first page button is clicked', async () => { 18 | const user = userEvent.setup(); 19 | const onChangePage = vi.fn((page: number) => page); 20 | const onClickFirstPageButton = vi.fn(); 21 | render( 22 | , 28 | ); 29 | await user.click(screen.getByRole('button', { name: '最初のページへ移動' })); 30 | expect(onChangePage).toHaveBeenCalledOnce(); 31 | expect(onChangePage).toHaveLastReturnedWith(1); 32 | expect(onClickFirstPageButton).toHaveBeenCalledOnce(); 33 | }); 34 | it('calls event handlers when last page button is clicked', async () => { 35 | const user = userEvent.setup(); 36 | const onChangePage = vi.fn((page: number) => page); 37 | const onClickLastPageButton = vi.fn(); 38 | render( 39 | , 45 | ); 46 | await user.click(screen.getByRole('button', { name: '最後のページへ移動' })); 47 | expect(onChangePage).toHaveBeenCalledOnce(); 48 | expect(onChangePage).toHaveLastReturnedWith(100); 49 | expect(onClickLastPageButton).toHaveBeenCalledOnce(); 50 | }); 51 | it('calls event handlers when previous page button is clicked', async () => { 52 | const user = userEvent.setup(); 53 | const onChangePage = vi.fn((page: number) => page); 54 | const onClickPreviousPageButton = vi.fn(); 55 | render( 56 | , 62 | ); 63 | await user.click(screen.getByRole('button', { name: '前のページへ移動' })); 64 | expect(onChangePage).toHaveBeenCalledOnce(); 65 | expect(onChangePage).toHaveLastReturnedWith(49); 66 | expect(onClickPreviousPageButton).toHaveBeenCalledOnce(); 67 | }); 68 | it('calls event handlers when next page button is clicked', async () => { 69 | const user = userEvent.setup(); 70 | const onChangePage = vi.fn((page: number) => page); 71 | const onClickNextPageButton = vi.fn(); 72 | render( 73 | , 79 | ); 80 | await user.click(screen.getByRole('button', { name: '次のページへ移動' })); 81 | expect(onChangePage).toHaveBeenCalledOnce(); 82 | expect(onChangePage).toHaveLastReturnedWith(51); 83 | expect(onClickNextPageButton).toHaveBeenCalledOnce(); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /packages/for-ui/src/table/TableScroller.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | 3 | type Props = PropsWithChildren<{ 4 | height?: number | string; 5 | }>; 6 | 7 | export const TableScroller = ({ height, ...rest }: Props) => ( 8 |
14 | ); 15 | -------------------------------------------------------------------------------- /packages/for-ui/src/table/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Table'; 2 | export * from './TablePagination'; 3 | export * from './TableCell'; 4 | export * from './TableScroller'; 5 | export * from './ColumnDef'; 6 | -------------------------------------------------------------------------------- /packages/for-ui/src/tabs/Tab.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react/types-6-0'; 3 | import { Badge } from '../badge'; 4 | import { Tab, TabProps } from './Tab'; 5 | 6 | export default { 7 | title: 'Navigation / Tabs / Tab', 8 | component: Tab, 9 | } as Meta; 10 | 11 | export const Playground = { 12 | args: { 13 | label: 'ラベル', 14 | badge: , 15 | disabled: false, 16 | } satisfies TabProps, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/for-ui/src/tabs/Tab.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactElement } from 'react'; 2 | import MuiTab, { TabProps as MuiTabProps } from '@mui/material/Tab'; 3 | import { fsx } from '../system/fsx'; 4 | 5 | export interface TabProps extends Omit { 6 | minWidth?: number; 7 | badge?: ReactElement; 8 | } 9 | 10 | export const Tab: FC = ({ minWidth, tabIndex, className, badge, ...rest }) => ( 11 | 23 | ); 24 | -------------------------------------------------------------------------------- /packages/for-ui/src/tabs/TabContext.tsx: -------------------------------------------------------------------------------- 1 | import TabContext, { TabContextProps } from '@mui/lab/TabContext'; 2 | 3 | export { TabContext }; 4 | export type { TabContextProps }; 5 | -------------------------------------------------------------------------------- /packages/for-ui/src/tabs/TabList.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import MuiTabList, { TabListProps as MuiTabListProps } from '@mui/lab/TabList'; 3 | import { fsx } from '../system/fsx'; 4 | import { tabWrapperStyle } from './style'; 5 | 6 | export interface TabListProps extends MuiTabListProps { 7 | noBorder?: boolean; 8 | reverse?: boolean; 9 | intention?: 'primary' | 'secondary'; 10 | } 11 | 12 | export const TabList: FC = ({ intention = 'primary', className, ...rest }) => ( 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /packages/for-ui/src/tabs/TabPanel.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import MuiTabPanel, { TabPanelProps as MuiTabPanelProps } from '@mui/lab/TabPanel'; 3 | import { fsx } from '../system/fsx'; 4 | 5 | export type TabPanelProps = MuiTabPanelProps; 6 | 7 | export const TabPanel: FC = ({ className, ...rest }) => ( 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /packages/for-ui/src/tabs/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import MuiTabs, { TabsProps as MuiTabsProps } from '@mui/material/Tabs'; 3 | import { fsx } from '../system/fsx'; 4 | import { tabWrapperStyle } from './style'; 5 | 6 | export interface TabsProps extends MuiTabsProps { 7 | intention?: 'primary' | 'secondary'; 8 | } 9 | 10 | export const Tabs: FC = ({ intention = 'primary', className, ...rest }) => ( 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /packages/for-ui/src/tabs/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Tab'; 2 | export * from './TabContext'; 3 | export * from './TabPanel'; 4 | export * from './Tabs'; 5 | export * from './TabList'; 6 | -------------------------------------------------------------------------------- /packages/for-ui/src/tabs/style.ts: -------------------------------------------------------------------------------- 1 | import { fsx } from '../system/fsx'; 2 | 3 | export const tabWrapperStyle = (intention: 'primary' | 'secondary') => 4 | fsx([ 5 | `border-shade-light-default h-10 min-h-min w-full overflow-visible border-b border-solid [&_.MuiTabs-indicator]:hidden [&_.MuiTabs-scroller]:h-10`, 6 | { 7 | primary: `[&_[aria-selected=true]]:text-primary-dark-default [&_[aria-selected=true]]:border-b-primary-dark-default`, 8 | secondary: `[&_[aria-selected=true]]:text-secondary-dark-default [&_[aria-selected=true]]:border-b-secondary-dark-default`, 9 | }[intention], 10 | ]); 11 | -------------------------------------------------------------------------------- /packages/for-ui/src/testing/rhf.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { FieldValues, useForm, UseFormProps, UseFormRegister } from 'react-hook-form'; 3 | import { act, fireEvent, screen } from '@testing-library/react'; 4 | 5 | export const withRHF = ({ 6 | useFormOptions, 7 | onSubmit, 8 | Component, 9 | }: { 10 | useFormOptions?: UseFormProps; 11 | onSubmit: (value: Value) => void; 12 | Component: FC<{ register: UseFormRegister }>; 13 | }): { 14 | Form: FC; 15 | submit: () => Promise; 16 | } => { 17 | return { 18 | Form: (): JSX.Element => { 19 | const { register, handleSubmit } = useForm(useFormOptions); 20 | return ( 21 |
22 | 23 | 24 | ); 25 | }, 26 | submit: async () => { 27 | await act(async () => { 28 | fireEvent.submit(screen.getByRole('form', { name: 'form' })); 29 | }); 30 | }, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/for-ui/src/text/Text.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/react/types-6-0'; 2 | import { Text } from './Text'; 3 | 4 | export default { 5 | title: 'General / Text', 6 | component: Text, 7 | } as Meta; 8 | 9 | export const Base = { 10 | args: { 11 | children: '「For Design System」で叶えるデザインシステムの夢。', 12 | as: 'span', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /packages/for-ui/src/text/Text.tsx: -------------------------------------------------------------------------------- 1 | import { ElementType, FC, forwardRef, ReactNode } from 'react'; 2 | import { ComponentPropsWithAs, Ref } from '../system/componentType'; 3 | import { fsx } from '../system/fsx'; 4 | 5 | type WithInherit = T | 'inherit'; 6 | 7 | type Size = 'xs' | 's' | 'r' | 'xr' | 'l' | 'xl'; 8 | 9 | type Weight = 'regular' | 'bold'; 10 | 11 | type Typeface = 'sansSerif' | 'monospaced'; 12 | 13 | const style = (size: WithInherit, weight: WithInherit, typeface: WithInherit): string => { 14 | return fsx( 15 | { 16 | inherit: ``, 17 | regular: `font-normal`, 18 | bold: `font-bold`, 19 | }[weight], 20 | { 21 | inherit: ``, 22 | sansSerif: `font-sans`, 23 | monospaced: `font-mono`, 24 | }[typeface], 25 | { 26 | inherit: ``, 27 | xs: `text-xs`, 28 | s: `text-s`, 29 | r: `text-r`, 30 | xr: `text-xr`, 31 | l: `text-l`, 32 | xl: `text-xl`, 33 | }[size], 34 | ); 35 | }; 36 | 37 | export type TextProps = ComponentPropsWithAs< 38 | { 39 | /** 40 | * テキストのサイズを指定 41 | * @default inherit 42 | */ 43 | size?: WithInherit; 44 | 45 | /** 46 | * 表示するテキストのウェイトを指定 47 | * @default inherit 48 | */ 49 | weight?: WithInherit; 50 | 51 | /** 52 | * 表示する書体の種別を指定 53 | * @default inherit 54 | */ 55 | typeface?: WithInherit; 56 | 57 | className?: string; 58 | 59 | /** 60 | * 文字列またはstrong等のコンポーネント (HTML的にvalidになるようにしてください) 61 | */ 62 | children: ReactNode; 63 | }, 64 | As 65 | >; 66 | 67 | type TextComponent = (props: TextProps) => ReturnType; 68 | 69 | export const Text: TextComponent = forwardRef( 70 | ( 71 | { as, size = 'inherit', weight = 'inherit', typeface = 'inherit', className, children, ...rests }: TextProps, 72 | ref: Ref, 73 | ) => { 74 | const Component = as || 'span'; 75 | return ( 76 | 77 | {children} 78 | 79 | ); 80 | }, 81 | ); 82 | -------------------------------------------------------------------------------- /packages/for-ui/src/text/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Text'; 2 | -------------------------------------------------------------------------------- /packages/for-ui/src/textArea/TextArea.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | import * as yup from 'yup'; 4 | import { yupResolver } from '@hookform/resolvers/yup'; 5 | import { Meta } from '@storybook/react/types-6-0'; 6 | import { Button } from '../button/Button'; 7 | import { Text } from '../text'; 8 | import { TextArea } from './TextArea'; 9 | 10 | export default { 11 | title: 'Form / TextArea', 12 | component: TextArea, 13 | } as Meta; 14 | 15 | export const Playground = { 16 | args: { 17 | label: '', 18 | placeholder: '', 19 | }, 20 | }; 21 | 22 | const schema = yup.object().shape({ 23 | email: yup.string().required(), 24 | password: yup.string().required(), 25 | }); 26 | 27 | export const Default = (): JSX.Element => { 28 | const { 29 | register, 30 | handleSubmit, 31 | formState: { errors }, 32 | } = useForm({ 33 | resolver: yupResolver(schema), 34 | }); 35 | const onSubmit = (data: unknown) => console.info(data); 36 | 37 | return ( 38 |
39 | 40 | TextArea 41 | 42 | 43 |