├── .commitlintrc.json ├── .env ├── .eslintignore ├── .eslintrc.cjs ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ ├── release-builtin.yml │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .node-version ├── .prettierrc.cjs ├── .snyk ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README_zh-CN.md ├── bump.config.ts ├── components.json ├── craco.config.cjs ├── jest.config.js ├── package.json ├── postcss.config.cjs ├── public ├── apple-touch-icon-120x120.png ├── apple-touch-icon-152x152.png ├── apple-touch-icon-180x180.png ├── favicon.ico ├── github-banner.png ├── icon-circle@512w.png ├── icon-square@192w.png ├── icon-square@512w.png ├── index.html ├── manifest.json └── robots.txt ├── scripts ├── build.mjs ├── shadcn-add.mjs └── verify-translations.mjs ├── src ├── App.tsx ├── AppContainer.tsx ├── bootstrap │ ├── Bootstrap.tsx │ └── index.ts ├── components │ ├── ActionsModal.tsx │ ├── BackButton │ │ └── index.tsx │ ├── BottomPanel.tsx │ ├── ChangeLanguage.tsx │ ├── CodeContent.tsx │ ├── CodeMirror │ │ ├── CodeMirror.tsx │ │ └── index.ts │ ├── CodeMirrorLoading.tsx │ ├── DarkModeToggle.tsx │ ├── Data │ │ ├── DataRowMain.tsx │ │ └── index.tsx │ ├── ErrorBoundary.tsx │ ├── FixedFullscreenContainer.tsx │ ├── FullLoading │ │ └── index.tsx │ ├── HorizontalSafeArea.tsx │ ├── ListCell.tsx │ ├── NetworkErrorModal.tsx │ ├── NewVersionAlert.tsx │ ├── PageContainer.tsx │ ├── PageLayout │ │ └── index.tsx │ ├── PageTitle │ │ └── index.tsx │ ├── ProfileCell │ │ └── index.tsx │ ├── ResponsiveDialog.tsx │ ├── RunInSurge.tsx │ ├── SWUpdateNotification.tsx │ ├── ScriptExecutionProvider │ │ ├── ScriptExecutionProvider.tsx │ │ ├── hooks.ts │ │ ├── index.ts │ │ └── types.ts │ ├── StatusChip │ │ ├── StatusChip.tsx │ │ └── index.ts │ ├── ThemeProvider │ │ ├── ThemeProvider.tsx │ │ └── index.ts │ ├── UIProvider │ │ ├── UIProvider.tsx │ │ ├── components │ │ │ ├── ConfirmationForm.tsx │ │ │ └── Confirmations.tsx │ │ ├── index.ts │ │ └── types.ts │ ├── VersionSupport.tsx │ ├── VersionTag.tsx │ ├── VerticalSafeArea.tsx │ └── ui │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── badge.tsx │ │ ├── button-group.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── select.tsx │ │ ├── switch.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle.tsx │ │ └── typography.tsx ├── data │ ├── api.ts │ └── index.ts ├── hooks │ ├── useSafeAreaInsets │ │ ├── context.ts │ │ ├── hooks.ts │ │ ├── index.ts │ │ └── provider.tsx │ ├── useTrafficUpdater │ │ ├── constants.ts │ │ ├── index.ts │ │ └── useTrafficUpdater.ts │ └── useVersionSupport │ │ ├── index.ts │ │ ├── useVersionSupport.spec.ts │ │ └── useVersionSupport.ts ├── i18n │ ├── en │ │ └── translation.json │ ├── index.ts │ └── zh │ │ └── translation.json ├── index.tsx ├── pages │ ├── Devices │ │ ├── components │ │ │ ├── DeviceIcon.tsx │ │ │ ├── DeviceItem.tsx │ │ │ ├── DeviceSettingsModal.tsx │ │ │ └── schemas.ts │ │ └── index.tsx │ ├── Dns │ │ └── index.tsx │ ├── Home │ │ ├── components │ │ │ ├── CapabilityTile.tsx │ │ │ ├── Events.tsx │ │ │ ├── HostInfo.tsx │ │ │ ├── MenuTile.tsx │ │ │ ├── SetHostModal.tsx │ │ │ └── TrafficCell │ │ │ │ ├── TrafficCell.tsx │ │ │ │ ├── chart-config.ts │ │ │ │ ├── components │ │ │ │ └── LineChart.tsx │ │ │ │ ├── constants.ts │ │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── menu.tsx │ ├── Landing │ │ ├── Regular.tsx │ │ ├── components │ │ │ ├── Header.tsx │ │ │ ├── HeaderInfo.tsx │ │ │ └── InstallCertificateModal.tsx │ │ ├── hooks.ts │ │ ├── index.tsx │ │ ├── types.ts │ │ └── utils.ts │ ├── Modules │ │ └── index.tsx │ ├── Policies │ │ ├── components │ │ │ ├── PolicyGroup.tsx │ │ │ └── PolicyNameItem.tsx │ │ ├── index.tsx │ │ └── usePolicyPerformance.ts │ ├── Profiles │ │ ├── Current │ │ │ └── index.tsx │ │ └── Manage │ │ │ ├── Manage.tsx │ │ │ └── index.ts │ ├── Requests │ │ ├── components │ │ │ ├── FilterPopover.tsx │ │ │ ├── ListItem.tsx │ │ │ ├── MethodBadge.tsx │ │ │ ├── RequestModal.tsx │ │ │ └── SorterPopover.tsx │ │ ├── hooks │ │ │ ├── filters.ts │ │ │ ├── reducer.ts │ │ │ └── useRequestsList.ts │ │ └── index.tsx │ ├── Scripting │ │ ├── Evaluate │ │ │ └── index.tsx │ │ └── index.tsx │ └── Traffic │ │ ├── components │ │ └── TrafficDataRow.tsx │ │ └── index.tsx ├── react-app-env.d.ts ├── router │ ├── context.ts │ ├── hooks.ts │ ├── index.ts │ ├── router.tsx │ └── types.ts ├── routes.tsx ├── service-worker.ts ├── serviceWorkerRegistration.ts ├── setupTests.ts ├── store │ ├── hooks.ts │ ├── index.ts │ ├── slices │ │ ├── history │ │ │ ├── index.ts │ │ │ ├── selectors.ts │ │ │ ├── slice.ts │ │ │ └── thunks.ts │ │ ├── profile │ │ │ ├── index.ts │ │ │ ├── selectors.ts │ │ │ ├── slice.ts │ │ │ └── thunks.ts │ │ └── traffic │ │ │ ├── index.ts │ │ │ ├── selectors.ts │ │ │ ├── slice.ts │ │ │ └── thunks.ts │ ├── store.ts │ └── types.ts ├── styles │ ├── global.css │ └── shadcn.css ├── types │ ├── index.ts │ └── ui.ts └── utils │ ├── constant.ts │ ├── fetcher.ts │ ├── index.ts │ ├── profiling.ts │ ├── shadcn.ts │ ├── store.ts │ ├── validation.ts │ └── with-profile.tsx ├── tailwind.config.cjs ├── tsconfig.json ├── vercel.json └── yarn.lock /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-angular" 4 | ], 5 | "rules": { 6 | "header-max-length": [ 7 | 1, 8 | "always", 9 | 72 10 | ], 11 | "type-enum": [ 12 | 2, 13 | "always", 14 | [ 15 | "build", 16 | "chore", 17 | "ci", 18 | "docs", 19 | "feat", 20 | "fix", 21 | "perf", 22 | "refactor", 23 | "revert", 24 | "style", 25 | "test", 26 | "sample" 27 | ] 28 | ], 29 | "subject-case": [ 30 | 2, 31 | "always", 32 | [ 33 | "sentence-case", 34 | "start-case", 35 | "lower-case" 36 | ] 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # https://github.com/facebook/create-react-app/issues/9873 2 | DISABLE_NEW_JSX_TRANSFORM=true 3 | # Enable React Refresh 4 | FAST_REFRESH=true 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build 2 | /node_modules 3 | /coverage 4 | /public 5 | /src/utils/shadcn.ts 6 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | settings: { 4 | react: { 5 | version: 'detect', 6 | }, 7 | 'import/parsers': { 8 | '@typescript-eslint/parser': ['.ts', '.tsx'], 9 | }, 10 | 'import/resolver': { 11 | typescript: { 12 | alwaysTryTypes: true, 13 | }, 14 | }, 15 | }, 16 | env: { 17 | browser: true, 18 | node: true, 19 | es6: true, 20 | }, 21 | parserOptions: { 22 | ecmaFeatures: { 23 | jsx: true, 24 | }, 25 | ecmaVersion: 'esnext', 26 | }, 27 | plugins: ['@typescript-eslint', 'import'], 28 | extends: [ 29 | 'eslint:recommended', 30 | 'plugin:@typescript-eslint/recommended', 31 | 'plugin:import/errors', 32 | 'plugin:import/warnings', 33 | 'plugin:import/typescript', 34 | 'plugin:react/recommended', 35 | 'plugin:react-hooks/recommended', 36 | 'plugin:prettier/recommended', 37 | ], 38 | rules: { 39 | 'no-debugger': 'warn', 40 | 'react/no-unknown-property': ['error', { ignore: ['css'] }], 41 | 'react/prop-types': 'off', 42 | '@typescript-eslint/ban-ts-comment': 'off', 43 | '@typescript-eslint/no-explicit-any': 'off', 44 | '@typescript-eslint/no-unused-vars': 'warn', 45 | 'import/no-named-as-default-member': 'off', 46 | 'import/order': [ 47 | 'error', 48 | { 49 | groups: [ 50 | 'builtin', 51 | 'external', 52 | 'internal', 53 | 'unknown', 54 | 'parent', 55 | 'sibling', 56 | 'index', 57 | 'object', 58 | 'type', 59 | ], 60 | 'newlines-between': 'always', 61 | pathGroups: [ 62 | { 63 | pattern: '@/**', 64 | group: 'internal', 65 | position: 'before', 66 | }, 67 | { 68 | pattern: 'react*', 69 | group: 'external', 70 | position: 'before', 71 | }, 72 | ], 73 | pathGroupsExcludedImportTypes: ['builtin'], 74 | distinctGroup: false, 75 | alphabetize: { 76 | order: 'asc', 77 | caseInsensitive: true, 78 | }, 79 | }, 80 | ], 81 | }, 82 | } 83 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: geekdada 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest] 17 | node-version: [20] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | 27 | - name: Get yarn cache directory path 28 | id: yarn-cache-dir-path 29 | run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT 30 | 31 | - uses: actions/cache@v3 32 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 33 | with: 34 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 35 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-yarn- 38 | 39 | - name: yarn install, build 40 | run: | 41 | yarn install 42 | yarn build 43 | 44 | - name: test, report coverage 45 | run: | 46 | yarn verify-translation 47 | yarn test 48 | 49 | # - uses: codecov/codecov-action@v1 50 | # if: success() && matrix.os == 'ubuntu-latest' 51 | # with: 52 | # token: ${{ secrets.CODECOV_TOKEN }} 53 | -------------------------------------------------------------------------------- /.github/workflows/release-builtin.yml: -------------------------------------------------------------------------------- 1 | name: Release built-in 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | release: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest] 14 | node-version: [20] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | 24 | - name: Get yarn cache directory path 25 | id: yarn-cache-dir-path 26 | run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT 27 | 28 | - uses: actions/cache@v3 29 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 30 | with: 31 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 32 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 33 | restore-keys: | 34 | ${{ runner.os }}-yarn- 35 | 36 | - name: yarn install, build, bundle 37 | run: | 38 | yarn install 39 | CI=false yarn build:surge 40 | 41 | - name: Upload Release Asset 42 | id: upload-release-asset 43 | uses: actions/upload-release-asset@v1 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | with: 47 | upload_url: ${{ github.event.release.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 48 | asset_path: ./yasd.tar.gz 49 | asset_name: built-in.tar.gz 50 | asset_content_type: application/tar+gzip 51 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | release: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest] 14 | node-version: [20] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | 24 | - name: Get yarn cache directory path 25 | id: yarn-cache-dir-path 26 | run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT 27 | 28 | - uses: actions/cache@v3 29 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 30 | with: 31 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 32 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 33 | restore-keys: | 34 | ${{ runner.os }}-yarn- 35 | 36 | - name: yarn install, build, bundle 37 | run: | 38 | yarn install 39 | CI=false yarn build:release 40 | 41 | - name: Upload Release Asset 42 | id: upload-release-asset 43 | uses: actions/upload-release-asset@v1 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | with: 47 | upload_url: ${{ github.event.release.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 48 | asset_path: ./build.tar.gz 49 | asset_name: build.tar.gz 50 | asset_content_type: application/tar+gzip 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | .idea 4 | 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | /*.tar.gz 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | .vercel 29 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | useTabs: false, 4 | tabWidth: 2, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | bracketSpacing: true 8 | } 9 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.14.1 3 | # ignores vulnerabilities until expiry date; change duration by modifying expiry date 4 | ignore: 5 | SNYK-JS-NODESASS-535498: 6 | - node-sass: 7 | reason: None given 8 | expires: '2020-12-01T06:01:46.480Z' 9 | SNYK-JS-NODESASS-535502: 10 | - node-sass: 11 | reason: None given 12 | expires: '2020-12-01T06:01:46.480Z' 13 | SNYK-JS-NODESASS-540956: 14 | - node-sass: 15 | reason: None given 16 | expires: '2020-12-01T06:01:46.480Z' 17 | SNYK-JS-NODESASS-540958: 18 | - node-sass: 19 | reason: None given 20 | expires: '2020-12-01T06:01:46.480Z' 21 | SNYK-JS-NODESASS-540964: 22 | - node-sass: 23 | reason: None given 24 | expires: '2020-12-01T06:01:46.480Z' 25 | SNYK-JS-NODESASS-540974: 26 | - node-sass: 27 | reason: None given 28 | expires: '2020-12-01T06:01:46.480Z' 29 | SNYK-JS-NODESASS-540978: 30 | - node-sass: 31 | reason: None given 32 | expires: '2020-12-01T06:01:46.480Z' 33 | SNYK-JS-NODESASS-540980: 34 | - node-sass: 35 | reason: None given 36 | expires: '2020-12-01T06:01:46.480Z' 37 | SNYK-JS-NODESASS-540990: 38 | - node-sass: 39 | reason: None given 40 | expires: '2020-12-01T06:01:46.480Z' 41 | SNYK-JS-NODESASS-540992: 42 | - node-sass: 43 | reason: None given 44 | expires: '2020-12-01T06:01:46.480Z' 45 | SNYK-JS-NODESASS-540994: 46 | - node-sass: 47 | reason: None given 48 | expires: '2020-12-01T06:01:46.480Z' 49 | SNYK-JS-NODESASS-540996: 50 | - node-sass: 51 | reason: None given 52 | expires: '2020-12-01T06:01:46.480Z' 53 | SNYK-JS-NODESASS-540998: 54 | - node-sass: 55 | reason: None given 56 | expires: '2020-12-01T06:01:46.481Z' 57 | SNYK-JS-NODESASS-541000: 58 | - node-sass: 59 | reason: None given 60 | expires: '2020-12-01T06:01:46.481Z' 61 | SNYK-JS-NODESASS-541002: 62 | - node-sass: 63 | reason: None given 64 | expires: '2020-12-01T06:01:46.481Z' 65 | patch: {} 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Roy Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | logo 4 | 5 |

6 | 7 | # Surge Web Dashboard (formerly YASD) 8 | 9 | ![Github Actions][github-actions-image] 10 | [![Test coverage][codecov-image]][codecov-url] 11 | [![Known Vulnerabilities][snyk-image]][snyk-url] 12 | 13 | [codecov-image]: https://codecov.io/gh/geekdada/yasd/branch/master/graph/badge.svg 14 | [codecov-url]: https://codecov.io/gh/geekdada/yasd 15 | [snyk-image]: https://snyk.io/test/github/geekdada/yasd/badge.svg 16 | [snyk-url]: https://snyk.io/test/github/geekdada/yasd 17 | [github-actions-image]: https://github.com/geekdada/yasd/workflows/Node%20CI/badge.svg 18 | 19 | [中文](/README_zh-CN.md) | [English](/README.md) 20 | 21 | ## What is Surge Web Dashboard? 22 | 23 | We are happy to announce Surge Web Dashboard (formerly YASD) has joined Surge! This project will remain open-source and active. 24 | 25 | Starting from Surge iOS 4.4.0 and Surge Mac 4.0.0, Surge added support for [HTTP API](https://manual.nssurge.com/others/http-api.html), which makes it possible to control Surge from a browser. 26 | 27 | You can use Surge Web Dashboard to control policies, inspect requests and more from another device! 28 | 29 | ## How-to 30 | 31 | Surge has Surge Web Dashboard built in now, you can turn it on in advance settings (it may require a subscription to activate built-in Surge Web Dashboard). 32 | 33 | We also provide standalone version, so you can manage all Surge instances at one place (it may require a subscription to activate HTTP API). 34 | 35 | - [HTTP](http://yasd.nerdynerd.org) 36 | - [HTTPS](https://yasd.royli.dev) 37 | 38 | You can also find the full bundle in [releases](https://github.com/geekdada/yasd/releases) and deploy yourself. 39 | 40 | ## Roadmap 41 | 42 | See [Roadmap](https://github.com/geekdada/yasd/projects/1) 43 | 44 | ## License 45 | 46 | [MIT](https://github.com/geekdada/yasd/blob/master/LICENSE) 47 | -------------------------------------------------------------------------------- /README_zh-CN.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | logo 4 | 5 |

6 | 7 | # Surge Web Dashboard (前身为 YASD) 8 | 9 | ![Github Actions][github-actions-image] 10 | [![Test coverage][codecov-image]][codecov-url] 11 | [![Known Vulnerabilities][snyk-image]][snyk-url] 12 | 13 | [codecov-image]: https://codecov.io/gh/geekdada/yasd/branch/master/graph/badge.svg 14 | [codecov-url]: https://codecov.io/gh/geekdada/yasd 15 | [snyk-image]: https://snyk.io/test/github/geekdada/yasd/badge.svg 16 | [snyk-url]: https://snyk.io/test/github/geekdada/yasd 17 | [github-actions-image]: https://github.com/geekdada/yasd/workflows/Node%20CI/badge.svg 18 | 19 | [中文](/README_zh-CN.md) | [English](/README.md) 20 | 21 | ## 什么是 Surge Web Dashboard? 22 | 23 | 我们很高兴地宣布 Surge Web Dashboard(前身为 YASD)已加入 Surge!这个项目将保持开源和活跃开发维护。 24 | 25 | 从 Surge i0S 4.4.0 和Surge Mac 4.0.0 开始,Surge 增加了对 [HTTP API](https://manual.nssurge.com/others/http-api.html) 的支持,这使得从浏览器控制 Surge 成为可能。 26 | 27 | 您可以使用 Surge Web Dashboard 从另一台设备上控制策略、检查请求等! 28 | 29 | ## 如何使用本项目 30 | 31 | 新版本的 Surge 已经内建 Surge Web Dashboard, 又可以在高级设置中将其打开。 32 | 33 | 内建的 Surge Web Dashboard 可能需要您拥有一个有效中的订阅才能开启,但是使用公开的独立版本(见后)则不受此限制。 34 | 35 | 公开的独立版本地址如下。你可以通过这个版本的 Surge Web Dashboard 管理多个 Surge 实例。 36 | 37 | - [HTTP](http://yasd.nerdynerd.org) 38 | - [HTTPS](https://yasd.royli.dev) 39 | 40 | 你也可以在 [releases](https://github.com/geekdada/yasd/releases) 找到完整应用包自主部署。 41 | 42 | ## 路线图 43 | 44 | 见 [路线图](https://github.com/geekdada/yasd/projects/1) 45 | 46 | ## License 47 | 48 | [MIT](https://github.com/geekdada/yasd/blob/master/LICENSE) 49 | -------------------------------------------------------------------------------- /bump.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'bumpp' 2 | 3 | export default defineConfig({ 4 | execute: 'npm run changelog', 5 | all: true, 6 | }) 7 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.cjs", 8 | "css": "src/styles/shadcn.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/utils/shadcn" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /craco.config.cjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const pkg = require('./package.json') 3 | 4 | process.env.REACT_APP_VERSION = pkg.version 5 | 6 | /** 7 | * @type {import('@craco/types').CracoConfig} 8 | */ 9 | const config = { 10 | eslint: { 11 | enable: true, 12 | mode: 'file', 13 | }, 14 | style: { 15 | postcss: { 16 | mode: 'file', 17 | }, 18 | }, 19 | webpack: { 20 | alias: { 21 | '@': `${__dirname}/src`, 22 | }, 23 | configure: (webpackConfig) => { 24 | if ( 25 | process.env.NODE_ENV === 'production' && 26 | process.env.REACT_APP_RUN_IN_SURGE === 'true' 27 | ) { 28 | webpackConfig.devtool = false 29 | } 30 | return webpackConfig 31 | }, 32 | }, 33 | babel: { 34 | plugins: ['babel-plugin-macros', '@emotion/babel-plugin'], 35 | presets: [ 36 | [ 37 | '@babel/preset-react', 38 | { runtime: 'automatic', importSource: '@emotion/react' }, 39 | ], 40 | ], 41 | }, 42 | } 43 | 44 | module.exports = config 45 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | import { pathsToModuleNameMapper } from 'ts-jest' 2 | 3 | import tsConfig from './tsconfig.json' assert { type: 'json' } 4 | 5 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 6 | const config = { 7 | preset: 'ts-jest/presets/default-esm', 8 | testEnvironment: 'jsdom', 9 | testMatch: ['/src/**/*.spec.{ts,tsx}'], 10 | roots: [''], 11 | modulePaths: [tsConfig.compilerOptions.baseUrl], 12 | moduleNameMapper: pathsToModuleNameMapper(tsConfig.compilerOptions.paths, { 13 | useESM: true, 14 | prefix: '/', 15 | }), 16 | transform: { 17 | '^.+\\.[tj]sx?$': [ 18 | 'ts-jest', 19 | { 20 | useESM: true, 21 | }, 22 | ], 23 | }, 24 | setupFilesAfterEnv: ['/src/setupTests.ts'], 25 | extensionsToTreatAsEsm: ['.ts', '.tsx'], 26 | } 27 | 28 | export default config 29 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('tailwindcss'), 5 | require('autoprefixer'), 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /public/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekdada/yasd/e50734733fd5c2534c322d31d5e15e9bb79a7efc/public/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /public/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekdada/yasd/e50734733fd5c2534c322d31d5e15e9bb79a7efc/public/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /public/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekdada/yasd/e50734733fd5c2534c322d31d5e15e9bb79a7efc/public/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekdada/yasd/e50734733fd5c2534c322d31d5e15e9bb79a7efc/public/favicon.ico -------------------------------------------------------------------------------- /public/github-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekdada/yasd/e50734733fd5c2534c322d31d5e15e9bb79a7efc/public/github-banner.png -------------------------------------------------------------------------------- /public/icon-circle@512w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekdada/yasd/e50734733fd5c2534c322d31d5e15e9bb79a7efc/public/icon-circle@512w.png -------------------------------------------------------------------------------- /public/icon-square@192w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekdada/yasd/e50734733fd5c2534c322d31d5e15e9bb79a7efc/public/icon-square@192w.png -------------------------------------------------------------------------------- /public/icon-square@512w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekdada/yasd/e50734733fd5c2534c322d31d5e15e9bb79a7efc/public/icon-square@512w.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Surge Web Dashboard 21 | 22 | 23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "YASD", 3 | "name": "Surge Web Dashboard", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "icon-square@192w.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "icon-square@512w.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": "/home", 22 | "display": "standalone", 23 | "theme_color": "#f6f9fb", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /scripts/build.mjs: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | import path from 'path' 4 | 5 | import fs from 'fs-extra' 6 | 7 | await (async () => { 8 | const { argv } = process 9 | const target = argv[3] 10 | const validTargets = ['release-vercel', 'release-ci', 'surge'] 11 | 12 | if (!validTargets.includes(target)) { 13 | throw new Error('Invalid build target.') 14 | } 15 | 16 | await $`yarn verify-translation` 17 | await clean() 18 | console.info('🚧 Build artifact') 19 | 20 | // Treating warnings as errors because process.env.CI = true. 21 | process.env.CI = 'false' 22 | 23 | switch (target) { 24 | case 'release-vercel': 25 | process.env.NODE_ENV = 'production' 26 | process.env.REACT_APP_USE_SW = 'true' 27 | await $`craco build` 28 | await insertSashimiScript() 29 | 30 | break 31 | 32 | case 'release-ci': 33 | process.env.NODE_ENV = 'production' 34 | process.env.REACT_APP_HASH_ROUTER = 'true' 35 | process.env.PUBLIC_URL = getUrlPathPrefix() 36 | await $`craco build` 37 | await changeManifest({ 38 | start_url: `${getUrlPathPrefix()}/#/home`, 39 | }) 40 | await bundleArtifact() 41 | 42 | break 43 | 44 | case 'surge': 45 | process.env.NODE_ENV = 'production' 46 | process.env.REACT_APP_HASH_ROUTER = 'true' 47 | process.env.REACT_APP_RUN_IN_SURGE = 'true' 48 | process.env.REACT_APP_URL_PATH_PREFIX = '/web' 49 | process.env.PUBLIC_URL = getUrlPathPrefix() 50 | await $`craco build` 51 | await changeManifest({ 52 | short_name: 'Dashboard', 53 | name: 'Surge Web Dashboard', 54 | start_url: `${getUrlPathPrefix()}/index.html#/home`, 55 | }) 56 | await bundleArtifact() 57 | await $`mv ./build.tar.gz ./yasd.tar.gz` 58 | 59 | break 60 | 61 | default: 62 | process.env.NODE_ENV = 'production' 63 | process.env.REACT_APP_USE_SW = 'true' 64 | process.env.PUBLIC_URL = getUrlPathPrefix() 65 | await $`craco build` 66 | 67 | if (process.env.REACT_APP_HASH_ROUTER === 'true') { 68 | await changeManifest({ 69 | start_url: `${getUrlPathPrefix()}/#/home`, 70 | }) 71 | } 72 | } 73 | })() 74 | 75 | async function changeManifest(obj = {}) { 76 | const manifest = await fs.readJson('build/manifest.json') 77 | 78 | await fs.writeJSON( 79 | 'build/manifest.json', 80 | { 81 | ...manifest, 82 | ...obj, 83 | }, 84 | { spaces: 2 }, 85 | ) 86 | } 87 | 88 | async function insertSashimiScript() { 89 | const script = `` 90 | const indexHTMLPath = path.join(__dirname, '../build/index.html') 91 | 92 | const indexHTML = await fs.readFile(indexHTMLPath, 'utf-8') 93 | const newHTML = indexHTML.replace('', `${script}`) 94 | await fs.writeFile(indexHTMLPath, newHTML) 95 | } 96 | 97 | async function bundleArtifact() { 98 | await $`(cd ./build; tar -czf ../build.tar.gz ./)` 99 | } 100 | 101 | async function clean() { 102 | console.info('🧹 Clean up') 103 | await $`rimraf ./build` 104 | await $`rimraf ./*.tar.gz` 105 | } 106 | 107 | function getUrlPathPrefix() { 108 | return 'REACT_APP_URL_PATH_PREFIX' in process.env 109 | ? process.env.REACT_APP_URL_PATH_PREFIX 110 | : '' 111 | } 112 | -------------------------------------------------------------------------------- /scripts/shadcn-add.mjs: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | ;(async () => { 4 | await $`npx shadcn add ${process.argv[3]}` 5 | await $`npx eslint --fix --ext .ts,.tsx ./src/components/ui` 6 | })() 7 | -------------------------------------------------------------------------------- /scripts/verify-translations.mjs: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | 3 | import fs from 'fs-extra' 4 | import get from 'lodash-es/get.js' 5 | 6 | await (async () => { 7 | const baseline = await fs.readJson( 8 | join(__dirname, '../src/i18n/en/translation.json'), 9 | ) 10 | const keys = walkKeys([], '', baseline) 11 | const langs = ['zh'] 12 | 13 | for (const lang of langs) { 14 | const json = await fs.readJson( 15 | join(__dirname, `../src/i18n/${lang}/translation.json`), 16 | ) 17 | 18 | for (const key of keys) { 19 | if (!get(json, key)) { 20 | throw new Error(`⚠️ Cannot find '${key}' for translation '${lang}'.`) 21 | } 22 | } 23 | } 24 | 25 | console.info('🌎 Translation files are good to go!') 26 | })() 27 | 28 | function walkKeys(arr, currKey, obj) { 29 | for (const key in obj) { 30 | if (typeof obj[key] === 'object') { 31 | walkKeys(arr, currKey + key + '.', obj[key]) 32 | } else { 33 | arr.push(currKey + key) 34 | } 35 | } 36 | 37 | return arr 38 | } 39 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useRef, useState } from 'react' 2 | import { Toaster, toast } from 'react-hot-toast' 3 | import { useTranslation } from 'react-i18next' 4 | import { Outlet, useLocation, useNavigate } from 'react-router-dom' 5 | import { SWRConfig } from 'swr' 6 | 7 | import NetworkErrorModal from '@/components/NetworkErrorModal' 8 | import NewVersionAlert from '@/components/NewVersionAlert' 9 | import PageLayout from '@/components/PageLayout' 10 | import RunInSurge from '@/components/RunInSurge' 11 | import useTrafficUpdater from '@/hooks/useTrafficUpdater' 12 | import { 13 | usePlatformVersion, 14 | useAppDispatch, 15 | useHistory, 16 | useProfile, 17 | } from '@/store' 18 | import { historyActions } from '@/store/slices/history' 19 | import { profileActions } from '@/store/slices/profile' 20 | import { isRunInSurge } from '@/utils' 21 | import { httpClient } from '@/utils/fetcher' 22 | 23 | const App: React.FC = () => { 24 | const { t } = useTranslation() 25 | const [isNetworkModalOpen, setIsNetworkModalOpen] = useState(false) 26 | const location = useLocation() 27 | const navigate = useNavigate() 28 | 29 | const dispatch = useAppDispatch() 30 | const platformVersion = usePlatformVersion() 31 | const profile = useProfile() 32 | const history = useHistory() 33 | 34 | const isCurrentVersionFetched = useRef(true) 35 | 36 | useTrafficUpdater() 37 | 38 | const onCloseApplication = useCallback(() => { 39 | if (isRunInSurge()) { 40 | dispatch(historyActions.deleteAllHistory()) 41 | } 42 | 43 | window.location.replace('/') 44 | }, [dispatch]) 45 | 46 | useEffect(() => { 47 | if (history && !profile && location.pathname !== '/') { 48 | navigate('/', { replace: true }) 49 | } 50 | }, [history, location, navigate, profile]) 51 | 52 | useEffect(() => { 53 | if ( 54 | !profile?.platform || 55 | !isCurrentVersionFetched.current || 56 | location.pathname === '/' 57 | ) { 58 | return 59 | } 60 | 61 | httpClient 62 | .request({ 63 | url: '/environment', 64 | method: 'GET', 65 | }) 66 | .then((res) => { 67 | const currentPlatformVersion = res.headers['x-surge-version'] 68 | 69 | if (currentPlatformVersion !== platformVersion) { 70 | dispatch( 71 | profileActions.updatePlatformVersion({ 72 | platformVersion: currentPlatformVersion, 73 | }), 74 | ) 75 | } 76 | 77 | isCurrentVersionFetched.current = false 78 | }) 79 | .catch((err) => { 80 | console.error(err) 81 | toast.error(t('common.surge_too_old')) 82 | }) 83 | }, [dispatch, location, platformVersion, profile?.platform, t]) 84 | 85 | return ( 86 | { 89 | if (location.pathname !== '/') { 90 | if (!error.response && error.request) { 91 | // 无法连接服务器 92 | setIsNetworkModalOpen(true) 93 | } 94 | } 95 | }, 96 | refreshWhenOffline: true, 97 | }} 98 | > 99 | 100 | 101 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | ) 116 | } 117 | 118 | export default App 119 | -------------------------------------------------------------------------------- /src/AppContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | import { HelmetProvider } from 'react-helmet-async' 3 | import { Provider as ReduxProvider } from 'react-redux' 4 | 5 | import Bootstrap from '@/bootstrap' 6 | import { ThemeProvider } from '@/components/ThemeProvider' 7 | import { UIProvider } from '@/components/UIProvider' 8 | import { SafeAreaInsetsProvider } from '@/hooks/useSafeAreaInsets' 9 | import { store } from '@/store' 10 | 11 | const AppContainer: React.FC<{ children: ReactNode }> = ({ children }) => { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | 27 | export default AppContainer 28 | -------------------------------------------------------------------------------- /src/bootstrap/Bootstrap.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | import { useLocation } from 'react-router-dom' 4 | import store from 'store2' 5 | 6 | import { useAppDispatch, useHistory } from '@/store' 7 | import { historyActions } from '@/store/slices/history' 8 | import { isRunInSurge } from '@/utils' 9 | import { LastUsedLanguage } from '@/utils/constant' 10 | 11 | export const Bootstrap: React.FC<{ 12 | children: React.ReactNode 13 | }> = ({ children }) => { 14 | const { i18n } = useTranslation() 15 | const dispatch = useAppDispatch() 16 | const history = useHistory() 17 | const location = useLocation() 18 | 19 | const [isTranslationLoaded, setIsTranslationLoaded] = useState(false) 20 | 21 | useEffect(() => { 22 | const loadLastUsedProfile = location.pathname !== '/' || isRunInSurge() 23 | 24 | dispatch( 25 | historyActions.loadHistoryFromLocalStorage({ 26 | loadLastUsedProfile, 27 | }), 28 | ) 29 | }, [dispatch, location.pathname]) 30 | 31 | useEffect(() => { 32 | const language: string | null = store.get(LastUsedLanguage) 33 | 34 | if (language && language !== i18n.language) { 35 | i18n.changeLanguage(language).then(() => { 36 | setIsTranslationLoaded(true) 37 | }) 38 | } else { 39 | setIsTranslationLoaded(true) 40 | } 41 | }, [i18n]) 42 | 43 | if (history === undefined || !isTranslationLoaded) { 44 | return null 45 | } 46 | 47 | return <>{children} 48 | } 49 | -------------------------------------------------------------------------------- /src/bootstrap/index.ts: -------------------------------------------------------------------------------- 1 | export { Bootstrap as default } from './Bootstrap' 2 | -------------------------------------------------------------------------------- /src/components/ActionsModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | 4 | import { useResponsiveDialog } from '@/components/ResponsiveDialog' 5 | import { Button } from '@/components/ui/button' 6 | import type { Dialog } from '@/components/ui/dialog' 7 | 8 | export type Action = { 9 | id: number | string 10 | title: string 11 | onClick: () => void 12 | } 13 | 14 | type ActionsModalProps = { 15 | title: string 16 | actions: ReadonlyArray 17 | } & Omit, 'children'> 18 | 19 | const ActionsModal = ({ 20 | title, 21 | actions, 22 | ...props 23 | }: ActionsModalProps): JSX.Element => { 24 | const { t } = useTranslation() 25 | const { 26 | Dialog, 27 | DialogContent, 28 | DialogHeader, 29 | DialogTitle, 30 | DialogFooter, 31 | DialogClose, 32 | DialogDescription, 33 | } = useResponsiveDialog() 34 | 35 | return ( 36 | 37 | 38 | 39 | {title} 40 | {title} 41 | 42 | 43 |
44 | {actions.map((action) => ( 45 | 48 | ))} 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 |
58 | ) 59 | } 60 | 61 | export default ActionsModal 62 | -------------------------------------------------------------------------------- /src/components/BackButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useNavigate } from 'react-router-dom' 3 | import { ChevronLeft } from 'lucide-react' 4 | 5 | import { Button } from '@/components/ui/button' 6 | 7 | const BackButton = ({ title }: { title?: string }) => { 8 | const navigate = useNavigate() 9 | 10 | return ( 11 |
12 | 20 | {title ? {title} : null} 21 |
22 | ) 23 | } 24 | 25 | export default BackButton 26 | -------------------------------------------------------------------------------- /src/components/BottomPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from '@emotion/react' 3 | 4 | import { cn } from '@/utils/shadcn' 5 | 6 | const BottomPanel = ({ 7 | className, 8 | children, 9 | ...props 10 | }: React.HTMLAttributes) => { 11 | return ( 12 |
19 |
23 | {children} 24 |
25 |
26 | ) 27 | } 28 | 29 | export default BottomPanel 30 | -------------------------------------------------------------------------------- /src/components/ChangeLanguage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | import dayjs from 'dayjs' 4 | import store from 'store2' 5 | 6 | import { 7 | Select, 8 | SelectContent, 9 | SelectItem, 10 | SelectTrigger, 11 | SelectValue, 12 | } from '@/components/ui/select' 13 | import { LastUsedLanguage } from '@/utils/constant' 14 | 15 | const ChangeLanguage = (): JSX.Element => { 16 | const { i18n } = useTranslation() 17 | const options = [ 18 | { 19 | value: 'en', 20 | label: 'English', 21 | }, 22 | { 23 | value: 'zh', 24 | label: '中文', 25 | }, 26 | ] 27 | const [isLoading, setIsLoading] = useState(false) 28 | 29 | const onChange = useCallback( 30 | async (newVal: string) => { 31 | setIsLoading(true) 32 | store.set(LastUsedLanguage, newVal) 33 | 34 | try { 35 | await i18n.changeLanguage(newVal) 36 | } catch (err) { 37 | console.error(err) 38 | } finally { 39 | setIsLoading(false) 40 | } 41 | }, 42 | [i18n], 43 | ) 44 | 45 | useEffect(() => { 46 | switch (i18n.language) { 47 | case 'zh': 48 | setZH().catch(console.error) 49 | break 50 | case 'en': 51 | setEN().catch(console.error) 52 | break 53 | } 54 | }, [i18n.language]) 55 | 56 | return ( 57 | 75 | ) 76 | } 77 | 78 | async function setZH() { 79 | const mod = await import('dayjs/locale/zh') 80 | dayjs.locale(mod.default) 81 | } 82 | 83 | async function setEN() { 84 | const mod = await import('dayjs/locale/en') 85 | dayjs.locale(mod.default) 86 | } 87 | 88 | export default ChangeLanguage 89 | -------------------------------------------------------------------------------- /src/components/CodeContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from '@emotion/react' 3 | 4 | import { cn } from '@/utils/shadcn' 5 | 6 | type CodeContentProps = { 7 | content?: string | null 8 | } & Omit, 'content'> 9 | 10 | const CodeContent = ({ className, content, ...props }: CodeContentProps) => { 11 | return ( 12 |
22 |       {content}
23 |     
24 | ) 25 | } 26 | 27 | export default CodeContent 28 | -------------------------------------------------------------------------------- /src/components/CodeMirror/CodeMirror.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | import { javascript } from '@codemirror/lang-javascript' 3 | import { EditorView } from '@codemirror/view' 4 | import { css } from '@emotion/react' 5 | import { material } from '@uiw/codemirror-theme-material' 6 | import { 7 | default as ReactCodeMirror, 8 | ReactCodeMirrorProps, 9 | } from '@uiw/react-codemirror' 10 | 11 | import { cn } from '@/utils/shadcn' 12 | 13 | type CodeMirrorProps = { 14 | className?: string 15 | isJavaScript?: boolean 16 | } & ReactCodeMirrorProps 17 | 18 | const CodeMirror = ({ className, isJavaScript, ...props }: CodeMirrorProps) => { 19 | const extensions = useMemo(() => { 20 | const exts = [EditorView.lineWrapping] 21 | 22 | if (isJavaScript) { 23 | exts.push(javascript()) 24 | } 25 | 26 | return exts 27 | }, [isJavaScript]) 28 | 29 | return ( 30 | 43 | ) 44 | } 45 | 46 | export default CodeMirror 47 | -------------------------------------------------------------------------------- /src/components/CodeMirror/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './CodeMirror' 2 | -------------------------------------------------------------------------------- /src/components/CodeMirrorLoading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | 4 | const CodeMirrorLoading = (): JSX.Element => { 5 | const { t } = useTranslation() 6 | 7 | return ( 8 |
9 | {t('common.is_loading')}... 10 |
11 | ) 12 | } 13 | 14 | export default CodeMirrorLoading 15 | -------------------------------------------------------------------------------- /src/components/DarkModeToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | import { Moon, Sun } from 'lucide-react' 4 | 5 | import { useTheme } from '@/components/ThemeProvider' 6 | import { Button } from '@/components/ui/button' 7 | import { 8 | DropdownMenu, 9 | DropdownMenuContent, 10 | DropdownMenuItem, 11 | DropdownMenuTrigger, 12 | } from '@/components/ui/dropdown-menu' 13 | 14 | export default function DarkModeToggle() { 15 | const { setTheme } = useTheme() 16 | const { t } = useTranslation() 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme('light')}> 29 | {t('common.light')} 30 | 31 | setTheme('dark')}> 32 | {t('common.dark')} 33 | 34 | setTheme('system')}> 35 | {t('common.system')} 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/components/Data/DataRowMain.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import { ChevronRight } from 'lucide-react' 3 | 4 | import { cn } from '@/utils/shadcn' 5 | 6 | type DataRowMainProps = { 7 | onClick?: () => void 8 | hideArrow?: boolean 9 | destructive?: boolean 10 | disabled?: boolean 11 | responsiveFont?: boolean 12 | } & React.HTMLAttributes 13 | 14 | export const DataRowMain = ({ 15 | children, 16 | className, 17 | onClick, 18 | hideArrow, 19 | destructive, 20 | disabled, 21 | responsiveFont, 22 | ...props 23 | }: DataRowMainProps) => { 24 | const handleClick = useCallback(() => { 25 | if (disabled) return 26 | onClick?.() 27 | }, [disabled, onClick]) 28 | const isClickable = typeof onClick === 'function' 29 | 30 | const clickableChildren = ( 31 | <> 32 |
{children}
33 | {!hideArrow && } 34 | 35 | ) 36 | 37 | responsiveFont = responsiveFont ?? true 38 | 39 | return ( 40 |
handleClick()} 51 | {...props} 52 | > 53 | {isClickable ? clickableChildren : children} 54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Data/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from '@emotion/styled' 3 | import tw from 'twin.macro' 4 | 5 | import { cn } from '@/utils/shadcn' 6 | 7 | export { DataRowMain } from './DataRowMain' 8 | 9 | export const DataGroup: React.FC<{ 10 | title?: string 11 | children: React.ReactNode 12 | className?: string 13 | responsiveTitle?: boolean 14 | }> = (props) => { 15 | const responsiveTitle = props.responsiveTitle ?? true 16 | 17 | return ( 18 |
19 | {props.title && ( 20 |
26 | {props.title} 27 |
28 | )} 29 | 30 |
31 | {props.children} 32 |
33 |
34 | ) 35 | } 36 | 37 | export const DataRow = styled.div`` 38 | 39 | export const DataRowSub = styled.div` 40 | ${tw`flex items-center justify-between px-3 md:px-5 leading-normal text-xs lg:text-sm lg:leading-relaxed`} 41 | ` 42 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useRouteError } from 'react-router-dom' 3 | 4 | export default function ErrorBoundary() { 5 | const error = useRouteError() 6 | 7 | console.error(error) 8 | 9 | return
Dang!
10 | } 11 | 12 | ErrorBoundary.displayName = 'ErrorBoundary' 13 | 14 | export { ErrorBoundary } 15 | -------------------------------------------------------------------------------- /src/components/FixedFullscreenContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from '@emotion/react' 3 | 4 | import { cn } from '@/utils/shadcn' 5 | 6 | const FixedFullscreenContainer: React.FC<{ 7 | offsetBottom?: boolean 8 | children: React.ReactNode | React.ReactNode[] 9 | }> = (props) => { 10 | let offsetBottom = true 11 | 12 | if (typeof props.offsetBottom === 'boolean') { 13 | offsetBottom = props.offsetBottom 14 | } 15 | 16 | return ( 17 |
30 |
{props.children}
31 |
32 | ) 33 | } 34 | 35 | export default FixedFullscreenContainer 36 | -------------------------------------------------------------------------------- /src/components/FullLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const FullLoading: React.FC = () => { 4 | return ( 5 |
6 | ) 7 | } 8 | 9 | export default FullLoading 10 | -------------------------------------------------------------------------------- /src/components/HorizontalSafeArea.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from '@emotion/react' 3 | 4 | import { cn } from '@/utils/shadcn' 5 | 6 | type HorizontalSafeAreaProps = React.HTMLAttributes 7 | 8 | const HorizontalSafeArea: React.FC = ({ 9 | className, 10 | children, 11 | ...props 12 | }) => { 13 | return ( 14 |
20 |
21 | {children} 22 |
23 |
24 | ) 25 | } 26 | 27 | export default HorizontalSafeArea 28 | -------------------------------------------------------------------------------- /src/components/ListCell.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from '@emotion/react' 3 | import { cva, VariantProps } from 'class-variance-authority' 4 | 5 | import { cn } from '@/utils/shadcn' 6 | 7 | const variants = cva('flex w-full flex-col select-none', { 8 | variants: { 9 | interactive: { 10 | true: 'cursor-pointer hover:bg-muted', 11 | false: '', 12 | }, 13 | }, 14 | defaultVariants: { 15 | interactive: true, 16 | }, 17 | }) 18 | 19 | type ListCellProps = { 20 | children: React.ReactNode 21 | } & React.HTMLAttributes & 22 | VariantProps 23 | 24 | const ListCell: React.FC = ({ 25 | children, 26 | className, 27 | interactive, 28 | ...props 29 | }) => { 30 | return ( 31 |
39 | {children} 40 |
41 | ) 42 | } 43 | 44 | type ListFullHeightCellProps = { 45 | children: React.ReactNode 46 | } & React.HTMLAttributes 47 | 48 | const ListFullHeightCell = ({ 49 | children, 50 | className, 51 | ...props 52 | }: ListFullHeightCellProps) => { 53 | return ( 54 |
61 | {children} 62 |
63 | ) 64 | } 65 | 66 | export { ListCell, ListFullHeightCell } 67 | -------------------------------------------------------------------------------- /src/components/NetworkErrorModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { KeyboardEvent, MouseEvent } from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | 4 | import { Button } from '@/components/ui/button' 5 | import { ButtonGroup } from '@/components/ui/button-group' 6 | import { 7 | Dialog, 8 | DialogContent, 9 | DialogFooter, 10 | DialogHeader, 11 | DialogTitle, 12 | } from '@/components/ui/dialog' 13 | 14 | interface NetworkErrorModalProps { 15 | onClose: (event?: MouseEvent | KeyboardEvent) => void 16 | isOpen: boolean 17 | reloadButton?: boolean 18 | } 19 | 20 | const NetworkErrorModal: React.FC = ({ 21 | isOpen, 22 | onClose, 23 | reloadButton, 24 | }) => { 25 | const { t } = useTranslation() 26 | 27 | return ( 28 | { 31 | if (!open) { 32 | onClose() 33 | } 34 | }} 35 | > 36 | 37 | 38 | {t('common.network_error_title')} 39 | 40 | 41 |
{t('common.network_error_message')}
42 | 43 | 44 | 45 | {reloadButton ? ( 46 | 54 | ) : ( 55 | 56 | )} 57 | 58 | 59 | 60 |
61 |
62 | ) 63 | } 64 | 65 | export default NetworkErrorModal 66 | -------------------------------------------------------------------------------- /src/components/NewVersionAlert.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | import satisfies from 'semver/functions/satisfies' 4 | import store from 'store2' 5 | 6 | import { Button } from '@/components/ui/button' 7 | import { 8 | Dialog, 9 | DialogContent, 10 | DialogFooter, 11 | DialogHeader, 12 | DialogTitle, 13 | } from '@/components/ui/dialog' 14 | import { LastUsedVersion } from '@/utils/constant' 15 | 16 | const currentVersion = process.env.REACT_APP_VERSION as string 17 | 18 | const NewVersionAlert: React.FC = () => { 19 | const [isOpen, setIsOpen] = useState(false) 20 | const [versionUrl, setVersionUrl] = useState('#') 21 | const { t } = useTranslation() 22 | 23 | useEffect(() => { 24 | const lastUsedVersion = store.get(LastUsedVersion) 25 | 26 | if (lastUsedVersion && !satisfies(currentVersion, `~${lastUsedVersion}`)) { 27 | setVersionUrl( 28 | `https://github.com/geekdada/yasd/releases/tag/v${currentVersion}`, 29 | ) 30 | setIsOpen(true) 31 | } 32 | 33 | store.set(LastUsedVersion, currentVersion) 34 | }, []) 35 | 36 | return ( 37 | { 40 | setIsOpen(open) 41 | }} 42 | > 43 | 44 | 45 | {t('new_version_alert.title')} 46 | 47 |
{t('new_version_alert.message')}
48 | 49 | 50 | 53 | 54 | 55 |
56 |
57 | ) 58 | } 59 | 60 | export default NewVersionAlert 61 | -------------------------------------------------------------------------------- /src/components/PageContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const PageContainer: React.FC<{ children: React.ReactNode }> = ({ 4 | children, 5 | }) => { 6 | return
{children}
7 | } 8 | 9 | export default PageContainer 10 | -------------------------------------------------------------------------------- /src/components/PageLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import FixedFullscreenContainer from '@/components/FixedFullscreenContainer' 4 | import PageContainer from '@/components/PageContainer' 5 | import { useRouteOptions } from '@/router' 6 | import { cn } from '@/utils/shadcn' 7 | 8 | const PageLayout: React.FC> = ({ 9 | children, 10 | className, 11 | ...props 12 | }) => { 13 | const routeOptions = useRouteOptions() 14 | const isFullscreen = routeOptions?.fullscreen ?? false 15 | const isBottomSafeAreaShown = routeOptions?.bottomSafeArea ?? true 16 | 17 | return ( 18 |
25 |
26 |
27 | {isFullscreen ? ( 28 | 29 | {children} 30 | 31 | ) : ( 32 | {children} 33 | )} 34 |
35 |
36 |
37 | ) 38 | } 39 | 40 | export default PageLayout 41 | -------------------------------------------------------------------------------- /src/components/PageTitle/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react' 2 | import { css } from '@emotion/react' 3 | 4 | import { TypographyH3 } from '@/components/ui/typography' 5 | import { cn } from '@/utils/shadcn' 6 | 7 | import BackButton from '../BackButton' 8 | 9 | interface PageTitleProps { 10 | title: string 11 | hasAutoRefresh?: boolean 12 | defaultAutoRefreshState?: boolean 13 | onAutoRefreshStateChange?: (newState: boolean) => void 14 | sticky?: boolean 15 | } 16 | 17 | const PageTitle: React.FC = (props) => { 18 | const [isAutoRefresh, setIsAutoRefresh] = useState( 19 | () => props.defaultAutoRefreshState ?? false, 20 | ) 21 | const isSticky = useMemo( 22 | () => (typeof props.sticky === 'undefined' ? true : props.sticky), 23 | [props.sticky], 24 | ) 25 | 26 | useEffect(() => { 27 | if (props.hasAutoRefresh && props.onAutoRefreshStateChange) { 28 | props.onAutoRefreshStateChange(isAutoRefresh) 29 | } 30 | }, [isAutoRefresh, props]) 31 | 32 | return ( 33 | 39 |
45 | 46 |
47 | 48 | {props.hasAutoRefresh && ( 49 |
setIsAutoRefresh(!isAutoRefresh)} 51 | className={cn( 52 | 'relative bg-green-100 cursor-pointer w-7 h-7 rounded-full flex items-center justify-center transition-colors duration-200 ease-in-out', 53 | isAutoRefresh && 'bg-red-100', 54 | )} 55 | css={[ 56 | css` 57 | margin-right: env(safe-area-inset-right); 58 | `, 59 | ]} 60 | > 61 | 67 | 73 |
74 | )} 75 |
76 | ) 77 | } 78 | 79 | export default PageTitle 80 | -------------------------------------------------------------------------------- /src/components/ResponsiveDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import { css } from '@emotion/react' 3 | import tw from 'twin.macro' 4 | import { useMediaQuery } from 'usehooks-ts' 5 | 6 | import { 7 | Dialog, 8 | DialogClose, 9 | DialogContent, 10 | DialogDescription, 11 | DialogHeader, 12 | DialogFooter, 13 | DialogTitle, 14 | DialogTrigger, 15 | } from '@/components/ui/dialog' 16 | import { 17 | Drawer, 18 | DrawerClose, 19 | DrawerContent, 20 | DrawerDescription, 21 | DrawerFooter, 22 | DrawerHeader, 23 | DrawerTitle, 24 | DrawerTrigger, 25 | } from '@/components/ui/drawer' 26 | 27 | const CustomDrawerContent = tw(DrawerContent)`px-6` 28 | const CustomDrawerHeader = tw(DrawerHeader)`px-0` 29 | const CustomDrawerFooter = memo(function CustomDrawerFooter({ 30 | children, 31 | ...props 32 | }: React.ComponentPropsWithoutRef) { 33 | return ( 34 | 41 | {children} 42 | 43 | ) 44 | }) 45 | 46 | export const useResponsiveDialog = () => { 47 | const isDesktop = useMediaQuery('(min-width: 768px)') 48 | 49 | return isDesktop 50 | ? ({ 51 | Dialog, 52 | DialogClose, 53 | DialogContent, 54 | DialogDescription, 55 | DialogFooter, 56 | DialogHeader, 57 | DialogTitle, 58 | DialogTrigger, 59 | } as const) 60 | : ({ 61 | Dialog: Drawer, 62 | DialogClose: DrawerClose, 63 | DialogContent: CustomDrawerContent, 64 | DialogDescription: DrawerDescription, 65 | DialogFooter: CustomDrawerFooter, 66 | DialogHeader: CustomDrawerHeader, 67 | DialogTitle: DrawerTitle, 68 | DialogTrigger: DrawerTrigger, 69 | } as const) 70 | } 71 | -------------------------------------------------------------------------------- /src/components/RunInSurge.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { isRunInSurge } from '@/utils' 4 | 5 | const RunInSurge: React.FC<{ 6 | children: React.ReactNode 7 | not?: boolean 8 | }> = ({ not, children }) => { 9 | const runInSurge = isRunInSurge() 10 | 11 | return not ? !runInSurge && <>{children} : runInSurge && <>{children} 12 | } 13 | 14 | export default RunInSurge 15 | -------------------------------------------------------------------------------- /src/components/SWUpdateNotification.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | import { InfoIcon } from 'lucide-react' 4 | 5 | import { Button } from '@/components/ui/button' 6 | import { TypographyP } from '@/components/ui/typography' 7 | 8 | const SWUpdateNotification = ({ 9 | registration, 10 | }: { 11 | registration?: ServiceWorkerRegistration 12 | }) => { 13 | const { t } = useTranslation() 14 | 15 | const onClick = useCallback(() => { 16 | if (registration) { 17 | registration.waiting?.postMessage({ type: 'SKIP_WAITING' }) 18 | } 19 | 20 | window.location.reload() 21 | }, [registration]) 22 | 23 | return ( 24 |
25 | 26 |
27 | {t('common.sw_updated')} 28 | 31 |
32 |
33 | ) 34 | } 35 | 36 | export default SWUpdateNotification 37 | -------------------------------------------------------------------------------- /src/components/ScriptExecutionProvider/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | 3 | import { ScriptExecutionContext } from './ScriptExecutionProvider' 4 | 5 | export const useExecuteScript = () => { 6 | const context = useContext(ScriptExecutionContext) 7 | 8 | if ( 9 | !context.execute || 10 | !context.evaluateCronScript || 11 | !context.clearExecution 12 | ) { 13 | throw new Error( 14 | 'useExecuteScript must be used within a ScriptExecutionProvider', 15 | ) 16 | } 17 | 18 | return { 19 | execute: context.execute, 20 | evaluateCronScript: context.evaluateCronScript, 21 | execution: context.execution, 22 | clearExecution: context.clearExecution, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/ScriptExecutionProvider/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ScriptExecutionProvider, 3 | withScriptExecutionProvider, 4 | } from './ScriptExecutionProvider' 5 | export * from './hooks' 6 | -------------------------------------------------------------------------------- /src/components/ScriptExecutionProvider/types.ts: -------------------------------------------------------------------------------- 1 | export type ExecutionOptions = { 2 | timeout?: number 3 | } 4 | 5 | export type ExecutionResult = { 6 | isLoading: boolean 7 | done: boolean 8 | result: string | null 9 | error: Error | null 10 | } 11 | 12 | export type ScriptExecutionContextType = { 13 | execution?: ExecutionResult 14 | evaluateCronScript?: ( 15 | scriptName: string, 16 | ) => Promise 17 | execute?: ( 18 | code: string, 19 | options?: ExecutionOptions, 20 | ) => Promise 21 | clearExecution?: () => void 22 | } 23 | -------------------------------------------------------------------------------- /src/components/StatusChip/StatusChip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { cva, type VariantProps } from 'class-variance-authority' 3 | 4 | import { cn } from '@/utils/shadcn' 5 | 6 | const chipVariants = cva('inline-block font-medium ring-1 ring-inset', { 7 | variants: { 8 | variant: { 9 | info: 'text-green-700 bg-green-50 ring-green-600/20', 10 | error: 'text-red-700 bg-red-50 ring-red-600/10', 11 | warn: 'text-yellow-600 bg-yellow-50 ring-yellow-500/10', 12 | }, 13 | size: { 14 | default: 'text-sm rounded-md py-1 px-2', 15 | sm: 'text-xs rounded-md py-0.5 px-2', 16 | lg: 'text-base rounded-lg py-2 px-3', 17 | }, 18 | }, 19 | defaultVariants: { 20 | variant: 'info', 21 | size: 'default', 22 | }, 23 | }) 24 | 25 | export interface StatusChipProps 26 | extends React.ButtonHTMLAttributes, 27 | VariantProps { 28 | text?: string 29 | } 30 | 31 | const StatusChip = React.forwardRef( 32 | ({ className, text, variant, size, ...props }, ref) => { 33 | return ( 34 |
39 | {text || variant} 40 |
41 | ) 42 | }, 43 | ) 44 | 45 | StatusChip.displayName = 'StatusChip' 46 | 47 | export { StatusChip } 48 | -------------------------------------------------------------------------------- /src/components/StatusChip/index.ts: -------------------------------------------------------------------------------- 1 | export * from './StatusChip' 2 | -------------------------------------------------------------------------------- /src/components/ThemeProvider/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from 'react' 2 | import { Helmet } from 'react-helmet-async' 3 | 4 | type ThemeProviderProps = { 5 | children: React.ReactNode 6 | defaultTheme?: string 7 | storageKey?: string 8 | } 9 | 10 | type ThemeProviderState = { 11 | theme: string 12 | setTheme: (theme: string) => void 13 | } 14 | 15 | const initialState = { 16 | theme: 'system', 17 | setTheme: () => null, 18 | } 19 | 20 | const ThemeProviderContext = createContext(initialState) 21 | 22 | export function ThemeProvider({ 23 | children, 24 | defaultTheme = 'system', 25 | storageKey = 'yasd-ui-theme', 26 | ...props 27 | }: ThemeProviderProps) { 28 | const [theme, setTheme] = useState( 29 | () => localStorage.getItem(storageKey) || defaultTheme, 30 | ) 31 | 32 | useEffect(() => { 33 | const root = window.document.documentElement 34 | 35 | root.classList.remove('light', 'dark') 36 | 37 | if (theme === 'system') { 38 | const systemTheme = window.matchMedia('(prefers-color-scheme: dark)') 39 | .matches 40 | ? 'dark' 41 | : 'light' 42 | 43 | root.classList.add(systemTheme) 44 | return 45 | } 46 | 47 | root.classList.add(theme) 48 | }, [theme]) 49 | 50 | const value = { 51 | theme, 52 | setTheme: (theme: string) => { 53 | localStorage.setItem(storageKey, theme) 54 | setTheme(theme) 55 | }, 56 | } 57 | 58 | return ( 59 | 60 | 61 | 65 | 66 | {children} 67 | 68 | ) 69 | } 70 | 71 | export const useTheme = () => { 72 | const context = useContext(ThemeProviderContext) 73 | 74 | if (context === undefined) 75 | throw new Error('useTheme must be used within a ThemeProvider') 76 | 77 | return context 78 | } 79 | -------------------------------------------------------------------------------- /src/components/ThemeProvider/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ThemeProvider' 2 | -------------------------------------------------------------------------------- /src/components/UIProvider/UIProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useCallback } from 'react' 2 | import { z, ZodObject } from 'zod' 3 | 4 | import Confirmations from './components/Confirmations' 5 | 6 | import type { 7 | ConfirmProperties, 8 | FormConfirmProperties, 9 | SimpleConfirmProperties, 10 | } from './types' 11 | 12 | interface UIState { 13 | confirmations: ConfirmProperties[] 14 | } 15 | 16 | type ConfirmResult = T extends FormConfirmProperties 17 | ? z.infer> | false 18 | : T extends SimpleConfirmProperties 19 | ? boolean 20 | : never 21 | 22 | interface UIContext extends UIState { 23 | confirm:

>( 24 | properties: P, 25 | ) => Promise 26 | cleanConfirmation: (index: number) => Promise 27 | } 28 | 29 | const UIContext = createContext(undefined) 30 | 31 | export const UIProvider = ({ children }: { children: React.ReactNode }) => { 32 | const [uiState, setUIState] = React.useState({ 33 | confirmations: [], 34 | }) 35 | 36 | const confirm = useCallback(async (properties) => { 37 | const isFormConfirm = 'form' in properties 38 | 39 | return new Promise((resolve) => { 40 | const newConirmation = isFormConfirm 41 | ? ({ 42 | ...properties, 43 | open: true, 44 | onConfirm: (result) => { 45 | resolve(result) 46 | }, 47 | onCancel: () => { 48 | resolve(false) 49 | }, 50 | } as FormConfirmProperties) 51 | : ({ 52 | ...properties, 53 | open: true, 54 | onConfirm: () => { 55 | resolve(true) 56 | }, 57 | onCancel: () => { 58 | resolve(false) 59 | }, 60 | } as SimpleConfirmProperties) 61 | 62 | setUIState((prevState) => { 63 | return { 64 | ...prevState, 65 | confirmations: [...prevState.confirmations, newConirmation], 66 | } 67 | }) 68 | }) 69 | }, []) 70 | 71 | const cleanConfirmation = useCallback(async (index: number) => { 72 | setUIState((prevState) => { 73 | const confirmations = [...prevState.confirmations] 74 | confirmations[index].open = false 75 | 76 | return { 77 | ...prevState, 78 | confirmations, 79 | } 80 | }) 81 | 82 | await new Promise((resolve) => setTimeout(resolve, 200)) 83 | 84 | setUIState((prevState) => { 85 | const confirmations = [...prevState.confirmations] 86 | confirmations.splice(index, 1) 87 | 88 | return { 89 | ...prevState, 90 | confirmations, 91 | } 92 | }) 93 | }, []) 94 | 95 | return ( 96 | 103 | 104 | 105 | {children} 106 | 107 | ) 108 | } 109 | 110 | export const useConfirm = () => { 111 | const context = React.useContext(UIContext) 112 | 113 | if (context === undefined) { 114 | throw new Error('useConfirm must be used within a UIProvider') 115 | } 116 | 117 | return context.confirm 118 | } 119 | 120 | export const useConfirmations = () => { 121 | const context = React.useContext(UIContext) 122 | 123 | if (context === undefined) { 124 | throw new Error('useConfirmations must be used within a UIProvider') 125 | } 126 | 127 | return { 128 | confirmations: context.confirmations, 129 | cleanConfirmation: context.cleanConfirmation, 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/components/UIProvider/components/Confirmations.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | 4 | import { 5 | AlertDialog, 6 | AlertDialogAction, 7 | AlertDialogCancel, 8 | AlertDialogContent, 9 | AlertDialogDescription, 10 | AlertDialogFooter, 11 | AlertDialogHeader, 12 | AlertDialogTitle, 13 | } from '@/components/ui/alert-dialog' 14 | 15 | import { useConfirmations } from '../UIProvider' 16 | 17 | import { ConfirmationForm } from './ConfirmationForm' 18 | 19 | import type { SimpleConfirmProperties } from '../types' 20 | 21 | const Confirmation = ({ 22 | confirmation, 23 | index, 24 | }: { 25 | confirmation: SimpleConfirmProperties 26 | index: number 27 | }) => { 28 | const { t } = useTranslation() 29 | const { cleanConfirmation } = useConfirmations() 30 | 31 | const handleAction = useCallback( 32 | (confirmation: SimpleConfirmProperties, index: number) => { 33 | if (confirmation.onConfirm) { 34 | confirmation.onConfirm() 35 | } 36 | 37 | cleanConfirmation(index) 38 | }, 39 | [cleanConfirmation], 40 | ) 41 | 42 | const handleCancel = useCallback( 43 | (confirmation: SimpleConfirmProperties, index: number) => { 44 | if (confirmation.onCancel) { 45 | confirmation.onCancel() 46 | } 47 | 48 | cleanConfirmation(index) 49 | }, 50 | [cleanConfirmation], 51 | ) 52 | 53 | return ( 54 | 55 | 56 | 57 | {confirmation.title} 58 | 59 | {confirmation.description ? ( 60 | 61 | {confirmation.description} 62 | 63 | ) : null} 64 | 65 | 66 | 67 | handleCancel(confirmation, index)}> 68 | {confirmation.cancelText ?? t('common.cancel')} 69 | 70 | handleAction(confirmation, index)}> 71 | {confirmation.confirmText ?? t('common.confirm')} 72 | 73 | 74 | 75 | 76 | ) 77 | } 78 | 79 | const Confirmations = () => { 80 | const { confirmations } = useConfirmations() 81 | 82 | return ( 83 | <> 84 | {confirmations.map((confirmation, index) => 85 | 'form' in confirmation ? ( 86 | 91 | ) : ( 92 | 97 | ), 98 | )} 99 | 100 | ) 101 | } 102 | 103 | export default Confirmations 104 | -------------------------------------------------------------------------------- /src/components/UIProvider/index.ts: -------------------------------------------------------------------------------- 1 | export { useConfirm, UIProvider } from './UIProvider' 2 | -------------------------------------------------------------------------------- /src/components/UIProvider/types.ts: -------------------------------------------------------------------------------- 1 | import type { z, ZodObject, ZodRawShape, ZodTypeAny } from 'zod' 2 | 3 | export type SimpleConfirmProperties = { 4 | title: string 5 | description?: string 6 | confirmText?: string 7 | cancelText?: string 8 | onConfirm?: () => void 9 | onCancel?: () => void 10 | open?: boolean 11 | } 12 | 13 | export type FormConfirmProperties< 14 | Fields extends ZodRawShape = { 15 | [key: string]: ZodTypeAny 16 | }, 17 | Keys extends (keyof Fields)[] = (keyof Fields)[], 18 | > = { 19 | title: string 20 | form: Fields 21 | formLabels?: Partial> 22 | formDescriptions?: Partial> 23 | formDefaultValues?: Partial> 24 | description?: string 25 | confirmText?: string 26 | cancelText?: string 27 | onConfirm?: (result: z.infer>) => void 28 | onCancel?: () => void 29 | open?: boolean 30 | } 31 | 32 | export type ConfirmProperties = SimpleConfirmProperties | FormConfirmProperties 33 | -------------------------------------------------------------------------------- /src/components/VersionSupport.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { useVersionSupport } from '@/hooks/useVersionSupport' 4 | 5 | interface VersionSupportProps { 6 | macos?: string | boolean 7 | ios?: string | boolean 8 | tvos?: string | boolean 9 | children: React.ReactNode 10 | } 11 | 12 | const VersionSupport: React.FC = ({ 13 | macos, 14 | ios, 15 | tvos, 16 | children, 17 | }) => { 18 | const isSupported = useVersionSupport({ macos, ios, tvos }) 19 | 20 | if (isSupported) { 21 | return <>{children} 22 | } 23 | 24 | return null 25 | } 26 | 27 | export default VersionSupport 28 | -------------------------------------------------------------------------------- /src/components/VersionTag.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { usePlatform, usePlatformBuild, usePlatformVersion } from '@/store' 4 | 5 | const VersionTag = () => { 6 | const platform = usePlatform() 7 | const platformVersion = usePlatformVersion() 8 | const platformBuild = usePlatformBuild() 9 | 10 | const isPlatformInfoShown = Boolean( 11 | platform && platformBuild && platformVersion, 12 | ) 13 | 14 | const content = isPlatformInfoShown 15 | ? `v${process.env.REACT_APP_VERSION}` + 16 | '\n' + 17 | `${platform} v${platformVersion} (${platformBuild})` 18 | : `v${process.env.REACT_APP_VERSION}` 19 | 20 | return ( 21 | 22 |

{content}
23 | 24 | ) 25 | } 26 | 27 | export default VersionTag 28 | -------------------------------------------------------------------------------- /src/components/VerticalSafeArea.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from '@emotion/react' 3 | 4 | export const BottomSafeArea = () => { 5 | return ( 6 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { cva, type VariantProps } from 'class-variance-authority' 3 | 4 | import { cn } from '@/utils/shadcn' 5 | 6 | const alertVariants = cva( 7 | 'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', 8 | { 9 | variants: { 10 | variant: { 11 | default: 'bg-background text-foreground', 12 | destructive: 13 | 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: 'default', 18 | }, 19 | }, 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = 'Alert' 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = 'AlertTitle' 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = 'AlertDescription' 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { cva, type VariantProps } from 'class-variance-authority' 3 | 4 | import { cn } from '@/utils/shadcn' 5 | 6 | const badgeVariants = cva( 7 | 'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80', 13 | secondary: 14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 15 | destructive: 16 | 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80', 17 | outline: 'text-foreground', 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: 'default', 22 | }, 23 | }, 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /src/components/ui/button-group.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { cva, VariantProps } from 'class-variance-authority' 3 | 4 | import { cn } from '@/utils/shadcn' 5 | 6 | const variants = cva('flex items-center space-x-3', { 7 | variants: { 8 | align: { 9 | left: 'justify-start', 10 | center: 'justify-center', 11 | right: 'justify-end', 12 | }, 13 | }, 14 | defaultVariants: { 15 | align: 'left', 16 | }, 17 | }) 18 | 19 | type ButtonGroupProps = React.HTMLAttributes & 20 | VariantProps 21 | 22 | const ButtonGroup = React.forwardRef( 23 | ({ children, className, align, ...props }, ref) => ( 24 |
34 | {children} 35 |
36 | ), 37 | ) 38 | 39 | ButtonGroup.displayName = 'ButtonGroup' 40 | 41 | export { ButtonGroup } 42 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { css } from '@emotion/react' 3 | import { Slot } from '@radix-ui/react-slot' 4 | import { cva, type VariantProps } from 'class-variance-authority' 5 | import { Loader2 } from 'lucide-react' 6 | import tw from 'twin.macro' 7 | 8 | import { cn } from '@/utils/shadcn' 9 | 10 | const buttonVariants = cva( 11 | 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', 12 | { 13 | variants: { 14 | variant: { 15 | default: 16 | 'bg-primary text-primary-foreground shadow hover:bg-primary/90', 17 | destructive: 18 | 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', 19 | outline: 20 | 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', 21 | secondary: 22 | 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', 23 | ghost: 'hover:bg-accent hover:text-accent-foreground', 24 | link: 'text-primary underline-offset-4 hover:underline', 25 | }, 26 | size: { 27 | default: 'h-9 px-4 py-2', 28 | sm: 'h-8 rounded-md px-3 text-xs', 29 | lg: 'h-10 rounded-md px-8', 30 | icon: 'h-9 w-9', 31 | }, 32 | stretch: { 33 | true: 'w-full', 34 | false: '', 35 | }, 36 | }, 37 | defaultVariants: { 38 | variant: 'default', 39 | size: 'default', 40 | }, 41 | }, 42 | ) 43 | 44 | export interface ButtonProps 45 | extends React.ButtonHTMLAttributes, 46 | VariantProps { 47 | asChild?: boolean 48 | isLoading?: boolean 49 | loadingLabel?: string 50 | stretch?: boolean 51 | } 52 | 53 | const Button = React.forwardRef( 54 | ( 55 | { 56 | className, 57 | variant, 58 | size, 59 | asChild = false, 60 | isLoading, 61 | loadingLabel, 62 | stretch, 63 | ...props 64 | }, 65 | ref, 66 | ) => { 67 | const Comp = asChild ? Slot : 'button' 68 | 69 | if (isLoading) { 70 | return ( 71 | 77 | 78 | {loadingLabel} 79 | 80 | ) 81 | } 82 | 83 | return ( 84 | * { 90 | ${tw`w-4 h-4`}; 91 | } 92 | `, 93 | ]} 94 | ref={ref} 95 | {...props} 96 | /> 97 | ) 98 | }, 99 | ) 100 | Button.displayName = 'Button' 101 | 102 | export { Button, buttonVariants } 103 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/utils/shadcn' 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
14 | )) 15 | Card.displayName = 'Card' 16 | 17 | const CardHeader = React.forwardRef< 18 | HTMLDivElement, 19 | React.HTMLAttributes 20 | >(({ className, ...props }, ref) => ( 21 |
26 | )) 27 | CardHeader.displayName = 'CardHeader' 28 | 29 | const CardTitle = React.forwardRef< 30 | HTMLParagraphElement, 31 | React.HTMLAttributes 32 | >(({ className, ...props }, ref) => ( 33 |

38 | )) 39 | CardTitle.displayName = 'CardTitle' 40 | 41 | const CardDescription = React.forwardRef< 42 | HTMLParagraphElement, 43 | React.HTMLAttributes 44 | >(({ className, ...props }, ref) => ( 45 |

50 | )) 51 | CardDescription.displayName = 'CardDescription' 52 | 53 | const CardContent = React.forwardRef< 54 | HTMLDivElement, 55 | React.HTMLAttributes 56 | >(({ className, ...props }, ref) => ( 57 |

58 | )) 59 | CardContent.displayName = 'CardContent' 60 | 61 | const CardFooter = React.forwardRef< 62 | HTMLDivElement, 63 | React.HTMLAttributes 64 | >(({ className, ...props }, ref) => ( 65 |
70 | )) 71 | CardFooter.displayName = 'CardFooter' 72 | 73 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 74 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox' 3 | import { CheckIcon } from '@radix-ui/react-icons' 4 | 5 | import { cn } from '@/utils/shadcn' 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | 22 | 23 | 24 | 25 | )) 26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 27 | 28 | export { Checkbox } 29 | -------------------------------------------------------------------------------- /src/components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Drawer as DrawerPrimitive } from 'vaul' 3 | 4 | import { cn } from '@/utils/shadcn' 5 | 6 | const Drawer = ({ 7 | shouldScaleBackground = true, 8 | ...props 9 | }: React.ComponentProps) => ( 10 | 14 | ) 15 | Drawer.displayName = 'Drawer' 16 | 17 | const DrawerTrigger = DrawerPrimitive.Trigger 18 | 19 | const DrawerPortal = DrawerPrimitive.Portal 20 | 21 | const DrawerClose = DrawerPrimitive.Close 22 | 23 | const DrawerOverlay = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName 34 | 35 | const DrawerContent = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, children, ...props }, ref) => ( 39 | 40 | 41 | 49 |
50 | {children} 51 | 52 | 53 | )) 54 | DrawerContent.displayName = 'DrawerContent' 55 | 56 | const DrawerHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
64 | ) 65 | DrawerHeader.displayName = 'DrawerHeader' 66 | 67 | const DrawerFooter = ({ 68 | className, 69 | ...props 70 | }: React.HTMLAttributes) => ( 71 |
75 | ) 76 | DrawerFooter.displayName = 'DrawerFooter' 77 | 78 | const DrawerTitle = React.forwardRef< 79 | React.ElementRef, 80 | React.ComponentPropsWithoutRef 81 | >(({ className, ...props }, ref) => ( 82 | 90 | )) 91 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName 92 | 93 | const DrawerDescription = React.forwardRef< 94 | React.ElementRef, 95 | React.ComponentPropsWithoutRef 96 | >(({ className, ...props }, ref) => ( 97 | 102 | )) 103 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName 104 | 105 | export { 106 | Drawer, 107 | DrawerPortal, 108 | DrawerOverlay, 109 | DrawerTrigger, 110 | DrawerClose, 111 | DrawerContent, 112 | DrawerHeader, 113 | DrawerFooter, 114 | DrawerTitle, 115 | DrawerDescription, 116 | } 117 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/utils/shadcn' 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | }, 22 | ) 23 | Input.displayName = 'Input' 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as LabelPrimitive from '@radix-ui/react-label' 3 | import { cva, type VariantProps } from 'class-variance-authority' 4 | 5 | import { cn } from '@/utils/shadcn' 6 | 7 | const labelVariants = cva( 8 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as PopoverPrimitive from '@radix-ui/react-popover' 3 | 4 | import { cn } from '@/utils/shadcn' 5 | 6 | const Popover = PopoverPrimitive.Root 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger 9 | 10 | const PopoverContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( 14 | 15 | 25 | 26 | )) 27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 28 | 29 | export { Popover, PopoverTrigger, PopoverContent } 30 | -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as SwitchPrimitives from '@radix-ui/react-switch' 3 | 4 | import { cn } from '@/utils/shadcn' 5 | 6 | const Switch = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 23 | 24 | )) 25 | Switch.displayName = SwitchPrimitives.Root.displayName 26 | 27 | export { Switch } 28 | -------------------------------------------------------------------------------- /src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as TabsPrimitive from '@radix-ui/react-tabs' 3 | 4 | import { cn } from '@/utils/shadcn' 5 | 6 | const Tabs = TabsPrimitive.Root 7 | 8 | const TabsList = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | TabsList.displayName = TabsPrimitive.List.displayName 22 | 23 | const TabsTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 35 | )) 36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 37 | 38 | const TabsContent = React.forwardRef< 39 | React.ElementRef, 40 | React.ComponentPropsWithoutRef 41 | >(({ className, ...props }, ref) => ( 42 | 50 | )) 51 | TabsContent.displayName = TabsPrimitive.Content.displayName 52 | 53 | export { Tabs, TabsList, TabsTrigger, TabsContent } 54 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/utils/shadcn' 4 | 5 | const Textarea = React.forwardRef< 6 | HTMLTextAreaElement, 7 | React.ComponentProps<'textarea'> 8 | >(({ className, ...props }, ref) => { 9 | return ( 10 |