Эх сурвалжийг харах

refactor(projects): 项目初始化

zhangtao 2 долоо хоног өмнө
parent
commit
75cbd09178
100 өөрчлөгдсөн 6166 нэмэгдсэн , 74 устгасан
  1. 11 0
      .editorconfig
  2. 61 0
      .env
  3. 7 0
      .env.prod
  4. 9 0
      .env.test
  5. 13 0
      .gitattributes
  6. 35 0
      .gitignore
  7. 4 0
      .npmrc
  8. 20 0
      .vscode/extensions.json
  9. 22 0
      .vscode/launch.json
  10. 31 0
      .vscode/settings.json
  11. 5 0
      CHANGELOG.md
  12. 21 72
      LICENSE
  13. 5 2
      README.md
  14. 2 0
      build/config/index.ts
  15. 55 0
      build/config/proxy.ts
  16. 12 0
      build/config/time.ts
  17. 9 0
      build/plugins/devtools.ts
  18. 13 0
      build/plugins/html.ts
  19. 24 0
      build/plugins/index.ts
  20. 58 0
      build/plugins/router.ts
  21. 32 0
      build/plugins/unocss.ts
  22. 49 0
      build/plugins/unplugin.ts
  23. 26 0
      eslint.config.js
  24. 14 0
      index.html
  25. 138 0
      package.json
  26. 21 0
      packages/axios/package.json
  27. 5 0
      packages/axios/src/constant.ts
  28. 176 0
      packages/axios/src/index.ts
  29. 60 0
      packages/axios/src/options.ts
  30. 28 0
      packages/axios/src/shared.ts
  31. 127 0
      packages/axios/src/type.ts
  32. 20 0
      packages/axios/tsconfig.json
  33. 16 0
      packages/color/package.json
  34. 2 0
      packages/color/src/constant/index.ts
  35. 1579 0
      packages/color/src/constant/name.ts
  36. 356 0
      packages/color/src/constant/palette.ts
  37. 7 0
      packages/color/src/index.ts
  38. 176 0
      packages/color/src/palette/antd.ts
  39. 45 0
      packages/color/src/palette/index.ts
  40. 152 0
      packages/color/src/palette/recommend.ts
  41. 93 0
      packages/color/src/shared/colord.ts
  42. 2 0
      packages/color/src/shared/index.ts
  43. 49 0
      packages/color/src/shared/name.ts
  44. 58 0
      packages/color/src/types/index.ts
  45. 20 0
      packages/color/tsconfig.json
  46. 16 0
      packages/hooks/package.json
  47. 9 0
      packages/hooks/src/index.ts
  48. 31 0
      packages/hooks/src/use-boolean.ts
  49. 96 0
      packages/hooks/src/use-context.ts
  50. 68 0
      packages/hooks/src/use-count-down.ts
  51. 16 0
      packages/hooks/src/use-loading.ts
  52. 79 0
      packages/hooks/src/use-request.ts
  53. 144 0
      packages/hooks/src/use-signal.ts
  54. 50 0
      packages/hooks/src/use-svg-icon-render.ts
  55. 132 0
      packages/hooks/src/use-table.ts
  56. 20 0
      packages/hooks/tsconfig.json
  57. 19 0
      packages/materials/package.json
  58. 6 0
      packages/materials/src/index.ts
  59. 63 0
      packages/materials/src/libs/admin-layout/index.module.css
  60. 18 0
      packages/materials/src/libs/admin-layout/index.module.css.d.ts
  61. 5 0
      packages/materials/src/libs/admin-layout/index.ts
  62. 236 0
      packages/materials/src/libs/admin-layout/index.vue
  63. 68 0
      packages/materials/src/libs/admin-layout/shared.ts
  64. 53 0
      packages/materials/src/libs/page-tab/button-tab.vue
  65. 31 0
      packages/materials/src/libs/page-tab/chrome-tab-bg.vue
  66. 58 0
      packages/materials/src/libs/page-tab/chrome-tab.vue
  67. 97 0
      packages/materials/src/libs/page-tab/index.module.css
  68. 15 0
      packages/materials/src/libs/page-tab/index.module.css.d.ts
  69. 3 0
      packages/materials/src/libs/page-tab/index.ts
  70. 72 0
      packages/materials/src/libs/page-tab/index.vue
  71. 31 0
      packages/materials/src/libs/page-tab/shared.ts
  72. 18 0
      packages/materials/src/libs/page-tab/svg-close.vue
  73. 3 0
      packages/materials/src/libs/simple-scrollbar/index.ts
  74. 18 0
      packages/materials/src/libs/simple-scrollbar/index.vue
  75. 288 0
      packages/materials/src/types/index.ts
  76. 20 0
      packages/materials/tsconfig.json
  77. 15 0
      packages/ofetch/package.json
  78. 10 0
      packages/ofetch/src/index.ts
  79. 20 0
      packages/ofetch/tsconfig.json
  80. 3 0
      packages/scripts/bin.ts
  81. 27 0
      packages/scripts/package.json
  82. 10 0
      packages/scripts/src/commands/changelog.ts
  83. 5 0
      packages/scripts/src/commands/cleanup.ts
  84. 84 0
      packages/scripts/src/commands/git-commit.ts
  85. 6 0
      packages/scripts/src/commands/index.ts
  86. 12 0
      packages/scripts/src/commands/release.ts
  87. 90 0
      packages/scripts/src/commands/router.ts
  88. 5 0
      packages/scripts/src/commands/update-pkg.ts
  89. 39 0
      packages/scripts/src/config/index.ts
  90. 109 0
      packages/scripts/src/index.ts
  91. 82 0
      packages/scripts/src/locales/index.ts
  92. 7 0
      packages/scripts/src/shared/index.ts
  93. 31 0
      packages/scripts/src/types/index.ts
  94. 20 0
      packages/scripts/tsconfig.json
  95. 12 0
      packages/uno-preset/package.json
  96. 55 0
      packages/uno-preset/src/index.ts
  97. 20 0
      packages/uno-preset/tsconfig.json
  98. 22 0
      packages/utils/package.json
  99. 27 0
      packages/utils/src/crypto.ts
  100. 4 0
      packages/utils/src/index.ts

+ 11 - 0
.editorconfig

@@ -0,0 +1,11 @@
+# Editor configuration, see http://editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+trim_trailing_whitespace = true
+insert_final_newline = true

+ 61 - 0
.env

@@ -0,0 +1,61 @@
+# the base url of the application, the default is "/"
+# if use a sub directory, it must be end with "/", like "/admin/" but not "/admin"
+VITE_BASE_URL=/
+
+VITE_APP_TITLE=中数未来管理系统
+
+VITE_APP_DESC=中数未来管理系统 admin template
+
+# the prefix of the icon name
+VITE_ICON_PREFIX=icon
+
+# the prefix of the local svg icon component, must include VITE_ICON_PREFIX
+# format {VITE_ICON_PREFIX}-{local icon name}
+VITE_ICON_LOCAL_PREFIX=icon-local
+
+# auth route mode: static | dynamic
+VITE_AUTH_ROUTE_MODE=dynamic
+
+# static auth route home
+VITE_ROUTE_HOME=home
+
+# default menu icon
+VITE_MENU_ICON=mdi:menu
+
+# whether to enable http proxy when is dev mode
+VITE_HTTP_PROXY=Y
+
+# vue-router mode: hash | history | memory
+VITE_ROUTER_HISTORY_MODE=history
+
+# success code of backend service, when the code is received, the request is successful
+VITE_SERVICE_SUCCESS_CODE=200
+# VITE_SERVICE_SUCCESS_CODE=0000
+
+# logout codes of backend service, when the code is received, the user will be logged out and redirected to login page
+VITE_SERVICE_LOGOUT_CODES=8888,8889
+
+# modal logout codes of backend service, when the code is received, the user will be logged out by displaying a modal
+VITE_SERVICE_MODAL_LOGOUT_CODES=7777,7778
+
+# token expired codes of backend service, when the code is received, it will refresh the token and resend the request
+VITE_SERVICE_EXPIRED_TOKEN_CODES=9999,9998,3333
+
+# when the route mode is static, the defined super role
+VITE_STATIC_SUPER_ROLE=R_SUPER
+
+# sourcemap
+VITE_SOURCE_MAP=N
+
+# Used to differentiate storage across different domains
+VITE_STORAGE_PREFIX=SOY_
+
+# used to control whether the program automatically detects updates
+VITE_AUTOMATICALLY_DETECT_UPDATE=Y
+
+# show proxy url log in terminal
+VITE_PROXY_LOG=Y
+
+# used to control whether to launch editor
+# by the way, this plugin is only available in dev mode, not in build mode
+VITE_DEVTOOLS_LAUNCH_EDITOR=code

+ 7 - 0
.env.prod

@@ -0,0 +1,7 @@
+# backend service base url, prod environment
+VITE_SERVICE_BASE_URL=https://mock.apifox.cn/m1/3109515-0-default
+
+# other backend service base url, prod environment
+VITE_OTHER_SERVICE_BASE_URL= `{
+  "demo": "http://localhost:9529"
+}`

+ 9 - 0
.env.test

@@ -0,0 +1,9 @@
+# backend service base url, test environment
+VITE_SERVICE_BASE_URL=http://192.168.1.206:8081
+# VITE_SERVICE_BASE_URL=https://mock.apifox.cn/m1/3109515-0-default
+
+
+# other backend service base url, test environment
+VITE_OTHER_SERVICE_BASE_URL= `{
+  "demo": "http://localhost:9528"
+}`

+ 13 - 0
.gitattributes

@@ -0,0 +1,13 @@
+"*.vue"    eol=lf
+"*.js"     eol=lf
+"*.ts"     eol=lf
+"*.jsx"    eol=lf
+"*.tsx"    eol=lf
+"*.mjs"    eol=lf
+"*.json"   eol=lf
+"*.html"   eol=lf
+"*.css"    eol=lf
+"*.scss"   eol=lf
+"*.md"     eol=lf
+"*.yaml"   eol=lf
+"*.yml"    eol=lf

+ 35 - 0
.gitignore

@@ -0,0 +1,35 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+!.vscode/settings.json
+!.vscode/launch.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+package-lock.json
+yarn.lock
+
+.VSCodeCounter

+ 4 - 0
.npmrc

@@ -0,0 +1,4 @@
+registry=https://registry.npmmirror.com/
+shamefully-hoist=true
+ignore-workspace-root-check=true
+link-workspace-packages=true

+ 20 - 0
.vscode/extensions.json

@@ -0,0 +1,20 @@
+{
+  "recommendations": [
+    "afzalsayed96.icones",
+    "antfu.iconify",
+    "antfu.unocss",
+    "dbaeumer.vscode-eslint",
+    "editorconfig.editorconfig",
+    "esbenp.prettier-vscode",
+    "lokalise.i18n-ally",
+    "mhutchie.git-graph",
+    "mikestead.dotenv",
+    "naumovs.color-highlight",
+    "pkief.material-icon-theme",
+    "sdras.vue-vscode-snippets",
+    "vue.volar",
+    "whtouche.vscode-js-console-utils",
+    "zhuangtongfa.material-theme",
+    "tu6ge.naive-ui-intelligence"
+  ]
+}

+ 22 - 0
.vscode/launch.json

@@ -0,0 +1,22 @@
+{
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "type": "chrome",
+      "request": "launch",
+      "name": "Vue Debugger",
+      "url": "http://localhost:9527",
+      "webRoot": "${workspaceFolder}"
+    },
+    {
+      "type": "node",
+      "request": "launch",
+      "name": "TS Debugger",
+      "runtimeExecutable": "tsx",
+      "skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"],
+      "program": "${file}",
+      "console": "integratedTerminal",
+      "internalConsoleOptions": "neverOpen"
+    }
+  ]
+}

+ 31 - 0
.vscode/settings.json

@@ -0,0 +1,31 @@
+{
+  "editor.codeActionsOnSave": {
+    "source.fixAll.eslint": "explicit",
+    "source.organizeImports": "never"
+  },
+  "editor.formatOnSave": false,
+  "eslint.validate": [
+    "html",
+    "css",
+    "scss",
+    "json",
+    "jsonc",
+    "javascript",
+    "javascriptreact",
+    "typescript",
+    "typescriptreact",
+    "vue"
+  ],
+  "i18n-ally.displayLanguage": "zh-cn",
+  "i18n-ally.enabledParsers": ["ts"],
+  "i18n-ally.enabledFrameworks": ["vue"],
+  "i18n-ally.editor.preferEditor": true,
+  "i18n-ally.keystyle": "nested",
+  "i18n-ally.localesPaths": ["src/locales/langs"],
+  "i18n-ally.parsers.typescript.compilerOptions": {
+    "moduleResolution": "node"
+  },
+  "prettier.enable": false,
+  "typescript.tsdk": "node_modules/typescript/lib",
+  "unocss.root": ["./"]
+}

+ 5 - 0
CHANGELOG.md

@@ -0,0 +1,5 @@
+#pnpm  i
+#pnpm  run dev
+#pnpm  run build
+#pnpm  run start
+#pnpm  run test

+ 21 - 72
LICENSE

@@ -1,72 +1,21 @@
-Apache License 
-Version 2.0, January 2004 
-http://www.apache.org/licenses/
-TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-1. Definitions.
-
-"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
-
-"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
-
-"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
-
-"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
-
-"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
-
-"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
-
-"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
-
-"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
-
-"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
-
-"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
-
-2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
-
-3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
-
-4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
-
-(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
-
-(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
-
-(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
-
-(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
-
-You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
-
-5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
-
-6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
-
-7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
-
-8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
-
-9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
-
-END OF TERMS AND CONDITIONS
-
-APPENDIX: How to apply the Apache License to your work.
-
-To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
-
-Copyright [yyyy] [name of copyright owner]
-
-Licensed under the Apache License, Version 2.0 (the "License"); 
-you may not use this file except in compliance with the License. 
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software 
-distributed under the License is distributed on an "AS IS" BASIS, 
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
-See the License for the specific language governing permissions and 
-limitations under the License.
+MIT License
+
+Copyright (c) 2021 Soybean
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 5 - 2
README.md

@@ -1,2 +1,5 @@
-# InstantRetailAdmin
-
+#pnpm  i
+#pnpm  run dev
+#pnpm  run build
+#pnpm  run start
+#pnpm  run test

+ 2 - 0
build/config/index.ts

@@ -0,0 +1,2 @@
+export * from './proxy';
+export * from './time';

+ 55 - 0
build/config/proxy.ts

@@ -0,0 +1,55 @@
+import type { HttpProxy, ProxyOptions } from 'vite';
+import { bgRed, bgYellow, green, lightBlue } from 'kolorist';
+import { consola } from 'consola';
+import { createServiceConfig } from '../../src/utils/service';
+
+/**
+ * Set http proxy
+ *
+ * @param env - The current env
+ * @param enable - If enable http proxy
+ */
+export function createViteProxy(env: Env.ImportMeta, enable: boolean) {
+  const isEnableHttpProxy = enable && env.VITE_HTTP_PROXY === 'Y';
+
+  if (!isEnableHttpProxy) return undefined;
+
+  const isEnableProxyLog = env.VITE_PROXY_LOG === 'Y';
+
+  const { baseURL, proxyPattern, other } = createServiceConfig(env);
+
+  const proxy: Record<string, ProxyOptions> = createProxyItem({ baseURL, proxyPattern }, isEnableProxyLog);
+
+  other.forEach(item => {
+    Object.assign(proxy, createProxyItem(item, isEnableProxyLog));
+  });
+
+  return proxy;
+}
+
+function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean) {
+  const proxy: Record<string, ProxyOptions> = {};
+
+  proxy[item.proxyPattern] = {
+    target: item.baseURL,
+    changeOrigin: true,
+    configure: (_proxy: HttpProxy.Server, options: ProxyOptions) => {
+      _proxy.on('proxyReq', (_proxyReq, req, _res) => {
+        if (!enableLog) return;
+
+        const requestUrl = `${lightBlue('[proxy url]')}: ${bgYellow(` ${req.method} `)} ${green(`${item.proxyPattern}${req.url}`)}`;
+
+        const proxyUrl = `${lightBlue('[real request url]')}: ${green(`${options.target}${req.url}`)}`;
+
+        consola.log(`${requestUrl}\n${proxyUrl}`);
+      });
+      _proxy.on('error', (_err, req, _res) => {
+        if (!enableLog) return;
+        consola.log(bgRed(`Error: ${req.method} `), green(`${options.target}${req.url}`));
+      });
+    },
+    rewrite: path => path.replace(new RegExp(`^${item.proxyPattern}`), '')
+  };
+
+  return proxy;
+}

+ 12 - 0
build/config/time.ts

@@ -0,0 +1,12 @@
+import dayjs from 'dayjs';
+import utc from 'dayjs/plugin/utc';
+import timezone from 'dayjs/plugin/timezone';
+
+export function getBuildTime() {
+  dayjs.extend(utc);
+  dayjs.extend(timezone);
+
+  const buildTime = dayjs.tz(Date.now(), 'Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss');
+
+  return buildTime;
+}

+ 9 - 0
build/plugins/devtools.ts

@@ -0,0 +1,9 @@
+import VueDevtools from 'vite-plugin-vue-devtools';
+
+export function setupDevtoolsPlugin(viteEnv: Env.ImportMeta) {
+  const { VITE_DEVTOOLS_LAUNCH_EDITOR } = viteEnv;
+
+  return VueDevtools({
+    launchEditor: VITE_DEVTOOLS_LAUNCH_EDITOR
+  });
+}

+ 13 - 0
build/plugins/html.ts

@@ -0,0 +1,13 @@
+import type { Plugin } from 'vite';
+
+export function setupHtmlPlugin(buildTime: string) {
+  const plugin: Plugin = {
+    name: 'html-plugin',
+    apply: 'build',
+    transformIndexHtml(html) {
+      return html.replace('<head>', `<head>\n    <meta name="buildTime" content="${buildTime}">`);
+    }
+  };
+
+  return plugin;
+}

+ 24 - 0
build/plugins/index.ts

@@ -0,0 +1,24 @@
+import type { PluginOption } from 'vite';
+import vue from '@vitejs/plugin-vue';
+import vueJsx from '@vitejs/plugin-vue-jsx';
+import progress from 'vite-plugin-progress';
+import { setupElegantRouter } from './router';
+import { setupUnocss } from './unocss';
+import { setupUnplugin } from './unplugin';
+import { setupHtmlPlugin } from './html';
+import { setupDevtoolsPlugin } from './devtools';
+
+export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) {
+  const plugins: PluginOption = [
+    vue(),
+    vueJsx(),
+    setupDevtoolsPlugin(viteEnv),
+    setupElegantRouter(),
+    setupUnocss(viteEnv),
+    ...setupUnplugin(viteEnv),
+    progress(),
+    setupHtmlPlugin(buildTime)
+  ];
+
+  return plugins;
+}

+ 58 - 0
build/plugins/router.ts

@@ -0,0 +1,58 @@
+import type { RouteMeta } from 'vue-router';
+import ElegantVueRouter from '@elegant-router/vue/vite';
+import type { RouteKey } from '@elegant-router/types';
+
+export function setupElegantRouter() {
+  return ElegantVueRouter({
+    layouts: {
+      base: 'src/layouts/base-layout/index.vue',
+      blank: 'src/layouts/blank-layout/index.vue'
+    },
+    customRoutes: {
+      names: [
+        'exception_403',
+        'exception_404',
+        'exception_500',
+        'document_project',
+        'document_project-link',
+        'document_video',
+        'document_vue',
+        'document_vite',
+        'document_unocss',
+        'document_naive',
+        'document_pro-naive',
+        'document_antd',
+        'document_alova'
+      ]
+    },
+    routePathTransformer(routeName, routePath) {
+      const key = routeName as RouteKey;
+
+      if (key === 'login') {
+        const modules: UnionKey.LoginModule[] = ['pwd-login', 'code-login', 'register', 'reset-pwd', 'bind-wechat'];
+
+        const moduleReg = modules.join('|');
+
+        return `/login/:module(${moduleReg})?`;
+      }
+
+      return routePath;
+    },
+    onRouteMetaGen(routeName) {
+      const key = routeName as RouteKey;
+
+      const constantRoutes: RouteKey[] = ['login', '403', '404', '500'];
+
+      const meta: Partial<RouteMeta> = {
+        title: key,
+        i18nKey: `route.${key}` as App.I18n.I18nKey
+      };
+
+      if (constantRoutes.includes(key)) {
+        meta.constant = true;
+      }
+
+      return meta;
+    }
+  });
+}

+ 32 - 0
build/plugins/unocss.ts

@@ -0,0 +1,32 @@
+import process from 'node:process';
+import path from 'node:path';
+import unocss from '@unocss/vite';
+import presetIcons from '@unocss/preset-icons';
+import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders';
+
+export function setupUnocss(viteEnv: Env.ImportMeta) {
+  const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
+
+  const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon');
+
+  /** The name of the local icon collection */
+  const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '');
+
+  return unocss({
+    presets: [
+      presetIcons({
+        prefix: `${VITE_ICON_PREFIX}-`,
+        scale: 1,
+        extraProperties: {
+          display: 'inline-block'
+        },
+        collections: {
+          [collectionName]: FileSystemIconLoader(localIconPath, svg =>
+            svg.replace(/^<svg\s/, '<svg width="1em" height="1em" ')
+          )
+        },
+        warn: true
+      })
+    ]
+  });
+}

+ 49 - 0
build/plugins/unplugin.ts

@@ -0,0 +1,49 @@
+import process from 'node:process';
+import path from 'node:path';
+import type { PluginOption } from 'vite';
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
+import Icons from 'unplugin-icons/vite';
+import IconsResolver from 'unplugin-icons/resolver';
+import Components from 'unplugin-vue-components/vite';
+import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
+import { ProNaiveUIResolver } from 'pro-naive-ui-resolver';
+import { FileSystemIconLoader } from 'unplugin-icons/loaders';
+
+export function setupUnplugin(viteEnv: Env.ImportMeta) {
+  const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv;
+
+  const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon');
+
+  /** The name of the local icon collection */
+  const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '');
+
+  const plugins: PluginOption[] = [
+    Icons({
+      compiler: 'vue3',
+      customCollections: {
+        [collectionName]: FileSystemIconLoader(localIconPath, svg =>
+          svg.replace(/^<svg\s/, '<svg width="1em" height="1em" ')
+        )
+      },
+      scale: 1,
+      defaultClass: 'inline-block'
+    }),
+    Components({
+      dts: 'src/typings/components.d.ts',
+      types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
+      resolvers: [
+        NaiveUiResolver(),
+        ProNaiveUIResolver(),
+        IconsResolver({ customCollections: [collectionName], componentPrefix: VITE_ICON_PREFIX })
+      ]
+    }),
+    createSvgIconsPlugin({
+      iconDirs: [localIconPath],
+      symbolId: `${VITE_ICON_LOCAL_PREFIX}-[dir]-[name]`,
+      inject: 'body-last',
+      customDomId: '__SVG_ICON_LOCAL__'
+    })
+  ];
+
+  return plugins;
+}

+ 26 - 0
eslint.config.js

@@ -0,0 +1,26 @@
+import { defineConfig } from '@soybeanjs/eslint-config';
+
+export default defineConfig(
+  { vue: true, unocss: true },
+  {
+    rules: {
+      'no-console': 'off',
+      eqeqeq: 'off',
+      'vue/multi-word-component-names': [
+        'warn',
+        {
+          ignores: ['index', 'App', 'Register', '[id]', '[url]']
+        }
+      ],
+      'vue/component-name-in-template-casing': [
+        'warn',
+        'PascalCase',
+        {
+          registeredComponentsOnly: false,
+          ignores: ['/^icon-/']
+        }
+      ],
+      'unocss/order-attributify': 'off'
+    }
+  }
+);

+ 14 - 0
index.html

@@ -0,0 +1,14 @@
+<!doctype html>
+<html lang="zh-cmn-Hans">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta name="color-scheme" content="light dark" />
+    <title>%VITE_APP_TITLE%</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 138 - 0
package.json

@@ -0,0 +1,138 @@
+{
+  "name": "soybean-admin",
+  "type": "module",
+  "version": "1.3.15",
+  "description": "A fresh and elegant admin template, based on Vue3、Vite7、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite7、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
+  "author": {
+    "name": "Soybean",
+    "email": "soybeanjs@outlook.com",
+    "url": "https://github.com/soybeanjs"
+  },
+  "license": "MIT",
+  "homepage": "https://github.com/soybeanjs/soybean-admin",
+  "repository": {
+    "url": "https://github.com/soybeanjs/soybean-admin.git"
+  },
+  "bugs": {
+    "url": "https://github.com/soybeanjs/soybean-admin/issues"
+  },
+  "keywords": [
+    "Vue3 admin ",
+    "vue-admin-template",
+    "Vite7",
+    "TypeScript",
+    "naive-ui",
+    "naive-ui-admin",
+    "ant-design-vue v4",
+    "UnoCSS"
+  ],
+  "engines": {
+    "node": ">=20.19.0",
+    "pnpm": ">=10.5.0"
+  },
+  "scripts": {
+    "build": "vite build --mode prod",
+    "build:test": "vite build --mode test",
+    "cleanup": "sa cleanup",
+    "commit": "sa git-commit",
+    "commit:zh": "sa git-commit -l=zh-cn",
+    "dev": "vite --mode test",
+    "dev:prod": "vite --mode prod",
+    "gen-route": "sa gen-route",
+    "lint": "eslint . --fix",
+    "prepare": "simple-git-hooks",
+    "preview": "vite preview",
+    "release": "sa release",
+    "typecheck": "vue-tsc --noEmit --skipLibCheck",
+    "update-pkg": "sa update-pkg"
+  },
+  "dependencies": {
+    "@antv/data-set": "0.11.8",
+    "@antv/g2": "5.3.4",
+    "@antv/g6": "5.0.49",
+    "@better-scroll/core": "2.5.1",
+    "@iconify/vue": "5.0.0",
+    "@sa/axios": "workspace:*",
+    "@sa/color": "workspace:*",
+    "@sa/hooks": "workspace:*",
+    "@sa/materials": "workspace:*",
+    "@sa/utils": "workspace:*",
+    "@visactor/vchart": "2.0.0",
+    "@visactor/vchart-theme": "1.12.2",
+    "@visactor/vtable-editors": "1.19.3",
+    "@visactor/vtable-gantt": "1.19.3",
+    "@visactor/vue-vtable": "1.19.3",
+    "@vueuse/components": "13.5.0",
+    "@vueuse/core": "13.5.0",
+    "clipboard": "2.0.11",
+    "dayjs": "1.11.13",
+    "defu": "6.1.4",
+    "dhtmlx-gantt": "9.0.13",
+    "dompurify": "3.2.6",
+    "echarts": "5.6.0",
+    "jsbarcode": "3.12.1",
+    "json5": "2.2.3",
+    "naive-ui": "2.42.0",
+    "nprogress": "0.2.0",
+    "pinia": "3.0.3",
+    "pinyin-pro": "3.26.0",
+    "print-js": "1.6.0",
+    "pro-naive-ui": "2.3.2",
+    "swiper": "11.2.10",
+    "tailwind-merge": "3.3.1",
+    "typeit": "8.8.7",
+    "vditor": "3.11.1",
+    "vue": "3.5.17",
+    "vue-draggable-plus": "0.6.0",
+    "vue-i18n": "11.1.10",
+    "vue-pdf-embed": "2.1.2",
+    "vue-router": "4.5.1",
+    "vue-types": "^6.0.0",
+    "vuedraggable": "^2.24.3",
+    "wangeditor": "4.7.15",
+    "xgplayer": "3.0.22",
+    "xlsx": "0.18.5"
+  },
+  "devDependencies": {
+    "@amap/amap-jsapi-types": "0.0.15",
+    "@elegant-router/vue": "0.3.8",
+    "@iconify/json": "2.2.359",
+    "@sa/scripts": "workspace:*",
+    "@sa/uno-preset": "workspace:*",
+    "@soybeanjs/eslint-config": "1.7.1",
+    "@types/bmapgl": "0.0.7",
+    "@types/node": "24.0.15",
+    "@types/nprogress": "0.2.3",
+    "@unocss/eslint-config": "66.3.3",
+    "@unocss/preset-icons": "66.3.3",
+    "@unocss/preset-uno": "66.3.3",
+    "@unocss/transformer-directives": "66.3.3",
+    "@unocss/transformer-variant-group": "66.3.3",
+    "@unocss/vite": "66.3.3",
+    "@vitejs/plugin-vue": "6.0.0",
+    "@vitejs/plugin-vue-jsx": "5.0.1",
+    "consola": "3.4.2",
+    "eslint": "9.31.0",
+    "eslint-plugin-vue": "10.3.0",
+    "kolorist": "1.8.0",
+    "lint-staged": "16.1.2",
+    "pro-naive-ui-resolver": "1.0.2",
+    "sass": "1.89.2",
+    "simple-git-hooks": "2.13.0",
+    "tsx": "4.20.3",
+    "typescript": "5.8.3",
+    "unplugin-icons": "22.1.0",
+    "unplugin-vue-components": "28.8.0",
+    "vite": "7.0.5",
+    "vite-plugin-progress": "0.0.7",
+    "vite-plugin-svg-icons": "2.0.1",
+    "vite-plugin-vue-devtools": "7.7.7",
+    "vue-eslint-parser": "10.2.0",
+    "vue-tsc": "3.0.3"
+  },
+  "simple-git-hooks": {
+    "commit-msg": "pnpm sa git-commit-verify",
+    "pre-commit": "pnpm typecheck && pnpm lint && git diff --exit-code"
+  },
+  "website": "https://admin.soybeanjs.cn"
+}

+ 21 - 0
packages/axios/package.json

@@ -0,0 +1,21 @@
+{
+  "name": "@sa/axios",
+  "version": "1.3.15",
+  "exports": {
+    ".": "./src/index.ts"
+  },
+  "typesVersions": {
+    "*": {
+      "*": ["./src/*"]
+    }
+  },
+  "dependencies": {
+    "@sa/utils": "workspace:*",
+    "axios": "1.10.0",
+    "axios-retry": "4.5.0",
+    "qs": "6.14.0"
+  },
+  "devDependencies": {
+    "@types/qs": "6.14.0"
+  }
+}

+ 5 - 0
packages/axios/src/constant.ts

@@ -0,0 +1,5 @@
+/** request id key */
+export const REQUEST_ID_KEY = 'X-Request-Id';
+
+/** the backend error code key */
+export const BACKEND_ERROR_CODE = 'BACKEND_ERROR';

+ 176 - 0
packages/axios/src/index.ts

@@ -0,0 +1,176 @@
+import axios, { AxiosError } from 'axios';
+import type { AxiosResponse, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
+import axiosRetry from 'axios-retry';
+import { nanoid } from '@sa/utils';
+import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options';
+import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant';
+import type {
+  CustomAxiosRequestConfig,
+  FlatRequestInstance,
+  MappedType,
+  RequestInstance,
+  RequestOption,
+  ResponseType
+} from './type';
+
+function createCommonRequest<
+  ResponseData,
+  ApiData = ResponseData,
+  State extends Record<string, unknown> = Record<string, unknown>
+>(axiosConfig?: CreateAxiosDefaults, options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
+  const opts = createDefaultOptions<ResponseData, ApiData, State>(options);
+
+  const axiosConf = createAxiosConfig(axiosConfig);
+  const instance = axios.create(axiosConf);
+
+  const abortControllerMap = new Map<string, AbortController>();
+
+  // config axios retry
+  const retryOptions = createRetryOptions(axiosConf);
+  axiosRetry(instance, retryOptions);
+
+  instance.interceptors.request.use(conf => {
+    const config: InternalAxiosRequestConfig = { ...conf };
+
+    // set request id
+    const requestId = nanoid();
+    config.headers.set(REQUEST_ID_KEY, requestId);
+
+    // config abort controller
+    if (!config.signal) {
+      const abortController = new AbortController();
+      config.signal = abortController.signal;
+      abortControllerMap.set(requestId, abortController);
+    }
+
+    // handle config by hook
+    const handledConfig = opts.onRequest?.(config) || config;
+
+    return handledConfig;
+  });
+
+  instance.interceptors.response.use(
+    async response => {
+      const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json';
+
+      if (responseType !== 'json' || opts.isBackendSuccess(response)) {
+        return Promise.resolve(response);
+      }
+
+      const fail = await opts.onBackendFail(response, instance);
+      if (fail) {
+        return fail;
+      }
+
+      const backendError = new AxiosError<ResponseData>(
+        'the backend request error',
+        BACKEND_ERROR_CODE,
+        response.config,
+        response.request,
+        response
+      );
+
+      await opts.onError(backendError);
+
+      return Promise.reject(backendError);
+    },
+    async (error: AxiosError<ResponseData>) => {
+      await opts.onError(error);
+
+      return Promise.reject(error);
+    }
+  );
+
+  function cancelAllRequest() {
+    abortControllerMap.forEach(abortController => {
+      abortController.abort();
+    });
+    abortControllerMap.clear();
+  }
+
+  return {
+    instance,
+    opts,
+    cancelAllRequest
+  };
+}
+
+/**
+ * create a request instance
+ *
+ * @param axiosConfig axios config
+ * @param options request options
+ */
+export function createRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
+  axiosConfig?: CreateAxiosDefaults,
+  options?: Partial<RequestOption<ResponseData, ApiData, State>>
+) {
+  const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
+
+  const request: RequestInstance<ApiData, State> = async function request<
+    T extends ApiData = ApiData,
+    R extends ResponseType = 'json'
+  >(config: CustomAxiosRequestConfig) {
+    const response: AxiosResponse<ResponseData> = await instance(config);
+
+    const responseType = response.config?.responseType || 'json';
+
+    if (responseType === 'json') {
+      return opts.transform(response);
+    }
+
+    return response.data as MappedType<R, T>;
+  } as RequestInstance<ApiData, State>;
+
+  request.cancelAllRequest = cancelAllRequest;
+  request.state = {} as State;
+
+  return request;
+}
+
+/**
+ * create a flat request instance
+ *
+ * The response data is a flat object: { data: any, error: AxiosError }
+ *
+ * @param axiosConfig axios config
+ * @param options request options
+ */
+export function createFlatRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
+  axiosConfig?: CreateAxiosDefaults,
+  options?: Partial<RequestOption<ResponseData, ApiData, State>>
+) {
+  const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
+
+  const flatRequest: FlatRequestInstance<ResponseData, ApiData, State> = async function flatRequest<
+    T extends ApiData = ApiData,
+    R extends ResponseType = 'json'
+  >(config: CustomAxiosRequestConfig) {
+    try {
+      const response: AxiosResponse<ResponseData> = await instance(config);
+
+      const responseType = response.config?.responseType || 'json';
+
+      if (responseType === 'json') {
+        const data = await opts.transform(response);
+
+        return { data, error: null, response };
+      }
+
+      return { data: response.data as MappedType<R, T>, error: null, response };
+    } catch (error) {
+      return { data: null, error, response: (error as AxiosError<ResponseData>).response };
+    }
+  } as FlatRequestInstance<ResponseData, ApiData, State>;
+
+  flatRequest.cancelAllRequest = cancelAllRequest;
+  flatRequest.state = {
+    ...opts.defaultState
+  } as State;
+
+  return flatRequest;
+}
+
+export { BACKEND_ERROR_CODE, REQUEST_ID_KEY };
+export type * from './type';
+export type { CreateAxiosDefaults, AxiosError };

+ 60 - 0
packages/axios/src/options.ts

@@ -0,0 +1,60 @@
+import type { CreateAxiosDefaults } from 'axios';
+import type { IAxiosRetryConfig } from 'axios-retry';
+import { stringify } from 'qs';
+import { isHttpSuccess } from './shared';
+import type { RequestOption } from './type';
+
+export function createDefaultOptions<
+  ResponseData,
+  ApiData = ResponseData,
+  State extends Record<string, unknown> = Record<string, unknown>
+>(options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
+  const opts: RequestOption<ResponseData, ApiData, State> = {
+    defaultState: {} as State,
+    transform: async response => response.data as unknown as ApiData,
+    transformBackendResponse: async response => response.data as unknown as ApiData,
+    onRequest: async config => config,
+    isBackendSuccess: _response => true,
+    onBackendFail: async () => {},
+    onError: async () => {}
+  };
+
+  if (options?.transform) {
+    opts.transform = options.transform;
+  } else {
+    opts.transform = options?.transformBackendResponse || opts.transform;
+  }
+
+  Object.assign(opts, options);
+
+  return opts;
+}
+
+export function createRetryOptions(config?: Partial<CreateAxiosDefaults>) {
+  const retryConfig: IAxiosRetryConfig = {
+    retries: 0
+  };
+
+  Object.assign(retryConfig, config);
+
+  return retryConfig;
+}
+
+export function createAxiosConfig(config?: Partial<CreateAxiosDefaults>) {
+  const TEN_SECONDS = 10 * 10000;
+
+  const axiosConfig: CreateAxiosDefaults = {
+    timeout: TEN_SECONDS,
+    headers: {
+      'Content-Type': 'application/json'
+    },
+    validateStatus: isHttpSuccess,
+    paramsSerializer: params => {
+      return stringify(params);
+    }
+  };
+
+  Object.assign(axiosConfig, config);
+
+  return axiosConfig;
+}

+ 28 - 0
packages/axios/src/shared.ts

@@ -0,0 +1,28 @@
+import type { AxiosHeaderValue, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
+
+export function getContentType(config: InternalAxiosRequestConfig) {
+  const contentType: AxiosHeaderValue = config.headers?.['Content-Type'] || 'application/json';
+
+  return contentType;
+}
+
+/**
+ * check if http status is success
+ *
+ * @param status
+ */
+export function isHttpSuccess(status: number) {
+  const isSuccessCode = status >= 200 && status < 300;
+  return isSuccessCode || status === 304;
+}
+
+/**
+ * is response json
+ *
+ * @param response axios response
+ */
+export function isResponseJson(response: AxiosResponse) {
+  const { responseType } = response.config;
+
+  return responseType === 'json' || responseType === undefined;
+}

+ 127 - 0
packages/axios/src/type.ts

@@ -0,0 +1,127 @@
+import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
+
+export type ContentType =
+  | 'text/html'
+  | 'text/plain'
+  | 'multipart/form-data'
+  | 'application/json'
+  | 'application/x-www-form-urlencoded'
+  | 'application/octet-stream';
+
+export type ResponseTransform<Input = any, Output = any> = (input: Input) => Output | Promise<Output>;
+
+export interface RequestOption<
+  ResponseData,
+  ApiData = ResponseData,
+  State extends Record<string, unknown> = Record<string, unknown>
+> {
+  /**
+   * The default state
+   */
+  defaultState?: State;
+  /**
+   * transform the response data to the api data
+   *
+   * @param response Axios response
+   */
+  transform: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
+  /**
+   * transform the response data to the api data
+   *
+   * @deprecated use `transform` instead, will be removed in the next major version v3
+   * @param response Axios response
+   */
+  transformBackendResponse: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
+  /**
+   * The hook before request
+   *
+   * For example: You can add header token in this hook
+   *
+   * @param config Axios config
+   */
+  onRequest: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
+  /**
+   * The hook to check backend response is success or not
+   *
+   * @param response Axios response
+   */
+  isBackendSuccess: (response: AxiosResponse<ResponseData>) => boolean;
+  /**
+   * The hook after backend request fail
+   *
+   * For example: You can handle the expired token in this hook
+   *
+   * @param response Axios response
+   * @param instance Axios instance
+   */
+  onBackendFail: (
+    response: AxiosResponse<ResponseData>,
+    instance: AxiosInstance
+  ) => Promise<AxiosResponse | null> | Promise<void>;
+  /**
+   * The hook to handle error
+   *
+   * For example: You can show error message in this hook
+   *
+   * @param error
+   */
+  onError: (error: AxiosError<ResponseData>) => void | Promise<void>;
+}
+
+interface ResponseMap {
+  blob: Blob;
+  text: string;
+  arrayBuffer: ArrayBuffer;
+  stream: ReadableStream<Uint8Array>;
+  document: Document;
+}
+export type ResponseType = keyof ResponseMap | 'json';
+
+export type MappedType<R extends ResponseType, JsonType = any> = R extends keyof ResponseMap
+  ? ResponseMap[R]
+  : JsonType;
+
+export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<AxiosRequestConfig, 'responseType'> & {
+  responseType?: R;
+};
+
+export interface RequestInstanceCommon<State extends Record<string, unknown>> {
+  /**
+   * cancel all request
+   *
+   * if the request provide abort controller sign from config, it will not collect in the abort controller map
+   */
+  cancelAllRequest: () => void;
+  /** you can set custom state in the request instance */
+  state: State;
+}
+
+/** The request instance */
+export interface RequestInstance<ApiData, State extends Record<string, unknown>> extends RequestInstanceCommon<State> {
+  <T extends ApiData = ApiData, R extends ResponseType = 'json'>(
+    config: CustomAxiosRequestConfig<R>
+  ): Promise<MappedType<R, T>>;
+}
+
+export type FlatResponseSuccessData<ResponseData, ApiData> = {
+  data: ApiData;
+  error: null;
+  response: AxiosResponse<ResponseData>;
+};
+
+export type FlatResponseFailData<ResponseData> = {
+  data: null;
+  error: AxiosError<ResponseData>;
+  response: AxiosResponse<ResponseData>;
+};
+
+export type FlatResponseData<ResponseData, ApiData> =
+  | FlatResponseSuccessData<ResponseData, ApiData>
+  | FlatResponseFailData<ResponseData>;
+
+export interface FlatRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
+  extends RequestInstanceCommon<State> {
+  <T extends ApiData = ApiData, R extends ResponseType = 'json'>(
+    config: CustomAxiosRequestConfig<R>
+  ): Promise<FlatResponseData<ResponseData, MappedType<R, T>>>;
+}

+ 20 - 0
packages/axios/tsconfig.json

@@ -0,0 +1,20 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "jsx": "preserve",
+    "lib": ["DOM", "ESNext"],
+    "baseUrl": ".",
+    "module": "ESNext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "types": ["node"],
+    "strict": true,
+    "strictNullChecks": true,
+    "noUnusedLocals": true,
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+    "forceConsistentCasingInFileNames": true
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules", "dist"]
+}

+ 16 - 0
packages/color/package.json

@@ -0,0 +1,16 @@
+{
+  "name": "@sa/color",
+  "version": "1.3.15",
+  "exports": {
+    ".": "./src/index.ts"
+  },
+  "typesVersions": {
+    "*": {
+      "*": ["./src/*"]
+    }
+  },
+  "dependencies": {
+    "@sa/utils": "workspace:*",
+    "colord": "2.9.3"
+  }
+}

+ 2 - 0
packages/color/src/constant/index.ts

@@ -0,0 +1,2 @@
+export * from './name';
+export * from './palette';

+ 1579 - 0
packages/color/src/constant/name.ts

@@ -0,0 +1,1579 @@
+export const colorNames: [hex: string, name: string][] = [
+  ['#000000', 'Black'],
+  ['#000080', 'Navy Blue'],
+  ['#0000c8', 'Dark Blue'],
+  ['#0000ff', 'Blue'],
+  ['#000741', 'Stratos'],
+  ['#001b1c', 'Swamp'],
+  ['#002387', 'Resolution Blue'],
+  ['#002900', 'Deep Fir'],
+  ['#002e20', 'Burnham'],
+  ['#002fa7', 'International Klein Blue'],
+  ['#003153', 'Prussian Blue'],
+  ['#003366', 'Midnight Blue'],
+  ['#003399', 'Smalt'],
+  ['#003532', 'Deep Teal'],
+  ['#003e40', 'Cyprus'],
+  ['#004620', 'Kaitoke Green'],
+  ['#0047ab', 'Cobalt'],
+  ['#004816', 'Crusoe'],
+  ['#004950', 'Sherpa Blue'],
+  ['#0056a7', 'Endeavour'],
+  ['#00581a', 'Camarone'],
+  ['#0066cc', 'Science Blue'],
+  ['#0066ff', 'Blue Ribbon'],
+  ['#00755e', 'Tropical Rain Forest'],
+  ['#0076a3', 'Allports'],
+  ['#007ba7', 'Deep Cerulean'],
+  ['#007ec7', 'Lochmara'],
+  ['#007fff', 'Azure Radiance'],
+  ['#008080', 'Teal'],
+  ['#0095b6', 'Bondi Blue'],
+  ['#009dc4', 'Pacific Blue'],
+  ['#00a693', 'Persian Green'],
+  ['#00a86b', 'Jade'],
+  ['#00cc99', 'Caribbean Green'],
+  ['#00cccc', "Robin's Egg Blue"],
+  ['#00ff00', 'Green'],
+  ['#00ff7f', 'Spring Green'],
+  ['#00ffff', 'Cyan Aqua'],
+  ['#010d1a', 'Blue Charcoal'],
+  ['#011635', 'Midnight'],
+  ['#011d13', 'Holly'],
+  ['#012731', 'Daintree'],
+  ['#01361c', 'Cardin Green'],
+  ['#01371a', 'County Green'],
+  ['#013e62', 'Astronaut Blue'],
+  ['#013f6a', 'Regal Blue'],
+  ['#014b43', 'Aqua Deep'],
+  ['#015e85', 'Orient'],
+  ['#016162', 'Blue Stone'],
+  ['#016d39', 'Fun Green'],
+  ['#01796f', 'Pine Green'],
+  ['#017987', 'Blue Lagoon'],
+  ['#01826b', 'Deep Sea'],
+  ['#01a368', 'Green Haze'],
+  ['#022d15', 'English Holly'],
+  ['#02402c', 'Sherwood Green'],
+  ['#02478e', 'Congress Blue'],
+  ['#024e46', 'Evening Sea'],
+  ['#026395', 'Bahama Blue'],
+  ['#02866f', 'Observatory'],
+  ['#02a4d3', 'Cerulean'],
+  ['#03163c', 'Tangaroa'],
+  ['#032b52', 'Green Vogue'],
+  ['#036a6e', 'Mosque'],
+  ['#041004', 'Midnight Moss'],
+  ['#041322', 'Black Pearl'],
+  ['#042e4c', 'Blue Whale'],
+  ['#044022', 'Zuccini'],
+  ['#044259', 'Teal Blue'],
+  ['#051040', 'Deep Cove'],
+  ['#051657', 'Gulf Blue'],
+  ['#055989', 'Venice Blue'],
+  ['#056f57', 'Watercourse'],
+  ['#062a78', 'Catalina Blue'],
+  ['#063537', 'Tiber'],
+  ['#069b81', 'Gossamer'],
+  ['#06a189', 'Niagara'],
+  ['#073a50', 'Tarawera'],
+  ['#080110', 'Jaguar'],
+  ['#081910', 'Black Bean'],
+  ['#082567', 'Deep Sapphire'],
+  ['#088370', 'Elf Green'],
+  ['#08e8de', 'Bright Turquoise'],
+  ['#092256', 'Downriver'],
+  ['#09230f', 'Palm Green'],
+  ['#09255d', 'Madison'],
+  ['#093624', 'Bottle Green'],
+  ['#095859', 'Deep Sea Green'],
+  ['#097f4b', 'Salem'],
+  ['#0a001c', 'Black Russian'],
+  ['#0a480d', 'Dark Fern'],
+  ['#0a6906', 'Japanese Laurel'],
+  ['#0a6f75', 'Atoll'],
+  ['#0b0b0b', 'Cod Gray'],
+  ['#0b0f08', 'Marshland'],
+  ['#0b1107', 'Gordons Green'],
+  ['#0b1304', 'Black Forest'],
+  ['#0b6207', 'San Felix'],
+  ['#0bda51', 'Malachite'],
+  ['#0c0b1d', 'Ebony'],
+  ['#0c0d0f', 'Woodsmoke'],
+  ['#0c1911', 'Racing Green'],
+  ['#0c7a79', 'Surfie Green'],
+  ['#0c8990', 'Blue Chill'],
+  ['#0d0332', 'Black Rock'],
+  ['#0d1117', 'Bunker'],
+  ['#0d1c19', 'Aztec'],
+  ['#0d2e1c', 'Bush'],
+  ['#0e0e18', 'Cinder'],
+  ['#0e2a30', 'Firefly'],
+  ['#0f2d9e', 'Torea Bay'],
+  ['#10121d', 'Vulcan'],
+  ['#101405', 'Green Waterloo'],
+  ['#105852', 'Eden'],
+  ['#110c6c', 'Arapawa'],
+  ['#120a8f', 'Ultramarine'],
+  ['#123447', 'Elephant'],
+  ['#126b40', 'Jewel'],
+  ['#130000', 'Diesel'],
+  ['#130a06', 'Asphalt'],
+  ['#13264d', 'Blue Zodiac'],
+  ['#134f19', 'Parsley'],
+  ['#140600', 'Nero'],
+  ['#1450aa', 'Tory Blue'],
+  ['#151f4c', 'Bunting'],
+  ['#1560bd', 'Denim'],
+  ['#15736b', 'Genoa'],
+  ['#161928', 'Mirage'],
+  ['#161d10', 'Hunter Green'],
+  ['#162a40', 'Big Stone'],
+  ['#163222', 'Celtic'],
+  ['#16322c', 'Timber Green'],
+  ['#163531', 'Gable Green'],
+  ['#171f04', 'Pine Tree'],
+  ['#175579', 'Chathams Blue'],
+  ['#182d09', 'Deep Forest Green'],
+  ['#18587a', 'Blumine'],
+  ['#19330e', 'Palm Leaf'],
+  ['#193751', 'Nile Blue'],
+  ['#1959a8', 'Fun Blue'],
+  ['#1a1a68', 'Lucky Point'],
+  ['#1ab385', 'Mountain Meadow'],
+  ['#1b0245', 'Tolopea'],
+  ['#1b1035', 'Haiti'],
+  ['#1b127b', 'Deep Koamaru'],
+  ['#1b1404', 'Acadia'],
+  ['#1b2f11', 'Seaweed'],
+  ['#1b3162', 'Biscay'],
+  ['#1b659d', 'Matisse'],
+  ['#1c1208', 'Crowshead'],
+  ['#1c1e13', 'Rangoon Green'],
+  ['#1c39bb', 'Persian Blue'],
+  ['#1c402e', 'Everglade'],
+  ['#1c7c7d', 'Elm'],
+  ['#1d6142', 'Green Pea'],
+  ['#1e0f04', 'Creole'],
+  ['#1e1609', 'Karaka'],
+  ['#1e1708', 'El Paso'],
+  ['#1e385b', 'Cello'],
+  ['#1e433c', 'Te Papa Green'],
+  ['#1e90ff', 'Dodger Blue'],
+  ['#1e9ab0', 'Eastern Blue'],
+  ['#1f120f', 'Night Rider'],
+  ['#1fc2c2', 'Java'],
+  ['#20208d', 'Jacksons Purple'],
+  ['#202e54', 'Cloud Burst'],
+  ['#204852', 'Blue Dianne'],
+  ['#211a0e', 'Eternity'],
+  ['#220878', 'Deep Blue'],
+  ['#228b22', 'Forest Green'],
+  ['#233418', 'Mallard'],
+  ['#240a40', 'Violet'],
+  ['#240c02', 'Kilamanjaro'],
+  ['#242a1d', 'Log Cabin'],
+  ['#242e16', 'Black Olive'],
+  ['#24500f', 'Green House'],
+  ['#251607', 'Graphite'],
+  ['#251706', 'Cannon Black'],
+  ['#251f4f', 'Port Gore'],
+  ['#25272c', 'Shark'],
+  ['#25311c', 'Green Kelp'],
+  ['#2596d1', 'Curious Blue'],
+  ['#260368', 'Paua'],
+  ['#26056a', 'Paris M'],
+  ['#261105', 'Wood Bark'],
+  ['#261414', 'Gondola'],
+  ['#262335', 'Steel Gray'],
+  ['#26283b', 'Ebony Clay'],
+  ['#273a81', 'Bay Of Many'],
+  ['#27504b', 'Plantation'],
+  ['#278a5b', 'Eucalyptus'],
+  ['#281e15', 'Oil'],
+  ['#283a77', 'Astronaut'],
+  ['#286acd', 'Mariner'],
+  ['#290c5e', 'Violent Violet'],
+  ['#292130', 'Bastille'],
+  ['#292319', 'Zeus'],
+  ['#292937', 'Charade'],
+  ['#297b9a', 'Jelly Bean'],
+  ['#29ab87', 'Jungle Green'],
+  ['#2a0359', 'Cherry Pie'],
+  ['#2a140e', 'Coffee Bean'],
+  ['#2a2630', 'Baltic Sea'],
+  ['#2a380b', 'Turtle Green'],
+  ['#2a52be', 'Cerulean Blue'],
+  ['#2b0202', 'Sepia Black'],
+  ['#2b194f', 'Valhalla'],
+  ['#2b3228', 'Heavy Metal'],
+  ['#2c0e8c', 'Blue Gem'],
+  ['#2c1632', 'Revolver'],
+  ['#2c2133', 'Bleached Cedar'],
+  ['#2c8c84', 'Lochinvar'],
+  ['#2d2510', 'Mikado'],
+  ['#2d383a', 'Outer Space'],
+  ['#2d569b', 'St Tropaz'],
+  ['#2e0329', 'Jacaranda'],
+  ['#2e1905', 'Jacko Bean'],
+  ['#2e3222', 'Rangitoto'],
+  ['#2e3f62', 'Rhino'],
+  ['#2e8b57', 'Sea Green'],
+  ['#2ebfd4', 'Scooter'],
+  ['#2f270e', 'Onion'],
+  ['#2f3cb3', 'Governor Bay'],
+  ['#2f519e', 'Sapphire'],
+  ['#2f5a57', 'Spectra'],
+  ['#2f6168', 'Casal'],
+  ['#300529', 'Melanzane'],
+  ['#301f1e', 'Cocoa Brown'],
+  ['#302a0f', 'Woodrush'],
+  ['#304b6a', 'San Juan'],
+  ['#30d5c8', 'Turquoise'],
+  ['#311c17', 'Eclipse'],
+  ['#314459', 'Pickled Bluewood'],
+  ['#315ba1', 'Azure'],
+  ['#31728d', 'Calypso'],
+  ['#317d82', 'Paradiso'],
+  ['#32127a', 'Persian Indigo'],
+  ['#32293a', 'Blackcurrant'],
+  ['#323232', 'Mine Shaft'],
+  ['#325d52', 'Stromboli'],
+  ['#327c14', 'Bilbao'],
+  ['#327da0', 'Astral'],
+  ['#33036b', 'Christalle'],
+  ['#33292f', 'Thunder'],
+  ['#33cc99', 'Shamrock'],
+  ['#341515', 'Tamarind'],
+  ['#350036', 'Mardi Gras'],
+  ['#350e42', 'Valentino'],
+  ['#350e57', 'Jagger'],
+  ['#353542', 'Tuna'],
+  ['#354e8c', 'Chambray'],
+  ['#363050', 'Martinique'],
+  ['#363534', 'Tuatara'],
+  ['#363c0d', 'Waiouru'],
+  ['#36747d', 'Ming'],
+  ['#368716', 'La Palma'],
+  ['#370202', 'Chocolate'],
+  ['#371d09', 'Clinker'],
+  ['#37290e', 'Brown Tumbleweed'],
+  ['#373021', 'Birch'],
+  ['#377475', 'Oracle'],
+  ['#380474', 'Blue Diamond'],
+  ['#381a51', 'Grape'],
+  ['#383533', 'Dune'],
+  ['#384555', 'Oxford Blue'],
+  ['#384910', 'Clover'],
+  ['#394851', 'Limed Spruce'],
+  ['#396413', 'Dell'],
+  ['#3a0020', 'Toledo'],
+  ['#3a2010', 'Sambuca'],
+  ['#3a2a6a', 'Jacarta'],
+  ['#3a686c', 'William'],
+  ['#3a6a47', 'Killarney'],
+  ['#3ab09e', 'Keppel'],
+  ['#3b000b', 'Temptress'],
+  ['#3b0910', 'Aubergine'],
+  ['#3b1f1f', 'Jon'],
+  ['#3b2820', 'Treehouse'],
+  ['#3b7a57', 'Amazon'],
+  ['#3b91b4', 'Boston Blue'],
+  ['#3c0878', 'Windsor'],
+  ['#3c1206', 'Rebel'],
+  ['#3c1f76', 'Meteorite'],
+  ['#3c2005', 'Dark Ebony'],
+  ['#3c3910', 'Camouflage'],
+  ['#3c4151', 'Bright Gray'],
+  ['#3c4443', 'Cape Cod'],
+  ['#3c493a', 'Lunar Green'],
+  ['#3d0c02', 'Bean  '],
+  ['#3d2b1f', 'Bistre'],
+  ['#3d7d52', 'Goblin'],
+  ['#3e0480', 'Kingfisher Daisy'],
+  ['#3e1c14', 'Cedar'],
+  ['#3e2b23', 'English Walnut'],
+  ['#3e2c1c', 'Black Marlin'],
+  ['#3e3a44', 'Ship Gray'],
+  ['#3eabbf', 'Pelorous'],
+  ['#3f2109', 'Bronze'],
+  ['#3f2500', 'Cola'],
+  ['#3f3002', 'Madras'],
+  ['#3f307f', 'Minsk'],
+  ['#3f4c3a', 'Cabbage Pont'],
+  ['#3f583b', 'Tom Thumb'],
+  ['#3f5d53', 'Mineral Green'],
+  ['#3fc1aa', 'Puerto Rico'],
+  ['#3fff00', 'Harlequin'],
+  ['#401801', 'Brown Pod'],
+  ['#40291d', 'Cork'],
+  ['#403b38', 'Masala'],
+  ['#403d19', 'Thatch Green'],
+  ['#405169', 'Fiord'],
+  ['#40826d', 'Viridian'],
+  ['#40a860', 'Chateau Green'],
+  ['#410056', 'Ripe Plum'],
+  ['#411f10', 'Paco'],
+  ['#412010', 'Deep Oak'],
+  ['#413c37', 'Merlin'],
+  ['#414257', 'Gun Powder'],
+  ['#414c7d', 'East Bay'],
+  ['#4169e1', 'Royal Blue'],
+  ['#41aa78', 'Ocean Green'],
+  ['#420303', 'Burnt Maroon'],
+  ['#423921', 'Lisbon Brown'],
+  ['#427977', 'Faded Jade'],
+  ['#431560', 'Scarlet Gum'],
+  ['#433120', 'Iroko'],
+  ['#433e37', 'Armadillo'],
+  ['#434c59', 'River Bed'],
+  ['#436a0d', 'Green Leaf'],
+  ['#44012d', 'Barossa'],
+  ['#441d00', 'Morocco Brown'],
+  ['#444954', 'Mako'],
+  ['#454936', 'Kelp'],
+  ['#456cac', 'San Marino'],
+  ['#45b1e8', 'Picton Blue'],
+  ['#460b41', 'Loulou'],
+  ['#462425', 'Crater Brown'],
+  ['#465945', 'Gray Asparagus'],
+  ['#4682b4', 'Steel Blue'],
+  ['#480404', 'Rustic Red'],
+  ['#480607', 'Bulgarian Rose'],
+  ['#480656', 'Clairvoyant'],
+  ['#481c1c', 'Cocoa Bean'],
+  ['#483131', 'Woody Brown'],
+  ['#483c32', 'Taupe'],
+  ['#49170c', 'Van Cleef'],
+  ['#492615', 'Brown Derby'],
+  ['#49371b', 'Metallic Bronze'],
+  ['#495400', 'Verdun Green'],
+  ['#496679', 'Blue Bayoux'],
+  ['#497183', 'Bismark'],
+  ['#4a2a04', 'Bracken'],
+  ['#4a3004', 'Deep Bronze'],
+  ['#4a3c30', 'Mondo'],
+  ['#4a4244', 'Tundora'],
+  ['#4a444b', 'Gravel'],
+  ['#4a4e5a', 'Trout'],
+  ['#4b0082', 'Pigment Indigo'],
+  ['#4b5d52', 'Nandor'],
+  ['#4c3024', 'Saddle'],
+  ['#4c4f56', 'Abbey'],
+  ['#4d0135', 'Blackberry'],
+  ['#4d0a18', 'Cab Sav'],
+  ['#4d1e01', 'Indian Tan'],
+  ['#4d282d', 'Cowboy'],
+  ['#4d282e', 'Livid Brown'],
+  ['#4d3833', 'Rock'],
+  ['#4d3d14', 'Punga'],
+  ['#4d400f', 'Bronzetone'],
+  ['#4d5328', 'Woodland'],
+  ['#4e0606', 'Mahogany'],
+  ['#4e2a5a', 'Bossanova'],
+  ['#4e3b41', 'Matterhorn'],
+  ['#4e420c', 'Bronze Olive'],
+  ['#4e4562', 'Mulled Wine'],
+  ['#4e6649', 'Axolotl'],
+  ['#4e7f9e', 'Wedgewood'],
+  ['#4eabd1', 'Shakespeare'],
+  ['#4f1c70', 'Honey Flower'],
+  ['#4f2398', 'Daisy Bush'],
+  ['#4f69c6', 'Indigo'],
+  ['#4f7942', 'Fern Green'],
+  ['#4f9d5d', 'Fruit Salad'],
+  ['#4fa83d', 'Apple'],
+  ['#504351', 'Mortar'],
+  ['#507096', 'Kashmir Blue'],
+  ['#507672', 'Cutty Sark'],
+  ['#50c878', 'Emerald'],
+  ['#514649', 'Emperor'],
+  ['#516e3d', 'Chalet Green'],
+  ['#517c66', 'Como'],
+  ['#51808f', 'Smalt Blue'],
+  ['#52001f', 'Castro'],
+  ['#520c17', 'Maroon Oak'],
+  ['#523c94', 'Gigas'],
+  ['#533455', 'Voodoo'],
+  ['#534491', 'Victoria'],
+  ['#53824b', 'Hippie Green'],
+  ['#541012', 'Heath'],
+  ['#544333', 'Judge Gray'],
+  ['#54534d', 'Fuscous Gray'],
+  ['#549019', 'Vida Loca'],
+  ['#55280c', 'Cioccolato'],
+  ['#555b10', 'Saratoga'],
+  ['#556d56', 'Finlandia'],
+  ['#5590d9', 'Havelock Blue'],
+  ['#56b4be', 'Fountain Blue'],
+  ['#578363', 'Spring Leaves'],
+  ['#583401', 'Saddle Brown'],
+  ['#585562', 'Scarpa Flow'],
+  ['#587156', 'Cactus'],
+  ['#589aaf', 'Hippie Blue'],
+  ['#591d35', 'Wine Berry'],
+  ['#592804', 'Brown Bramble'],
+  ['#593737', 'Congo Brown'],
+  ['#594433', 'Millbrook'],
+  ['#5a6e9c', 'Waikawa Gray'],
+  ['#5a87a0', 'Horizon'],
+  ['#5b3013', 'Jambalaya'],
+  ['#5c0120', 'Bordeaux'],
+  ['#5c0536', 'Mulberry Wood'],
+  ['#5c2e01', 'Carnaby Tan'],
+  ['#5c5d75', 'Comet'],
+  ['#5d1e0f', 'Redwood'],
+  ['#5d4c51', 'Don Juan'],
+  ['#5d5c58', 'Chicago'],
+  ['#5d5e37', 'Verdigris'],
+  ['#5d7747', 'Dingley'],
+  ['#5da19f', 'Breaker Bay'],
+  ['#5e483e', 'Kabul'],
+  ['#5e5d3b', 'Hemlock'],
+  ['#5f3d26', 'Irish Coffee'],
+  ['#5f5f6e', 'Mid Gray'],
+  ['#5f6672', 'Shuttle Gray'],
+  ['#5fa777', 'Aqua Forest'],
+  ['#5fb3ac', 'Tradewind'],
+  ['#604913', 'Horses Neck'],
+  ['#605b73', 'Smoky'],
+  ['#606e68', 'Corduroy'],
+  ['#6093d1', 'Danube'],
+  ['#612718', 'Espresso'],
+  ['#614051', 'Eggplant'],
+  ['#615d30', 'Costa Del Sol'],
+  ['#61845f', 'Glade Green'],
+  ['#622f30', 'Buccaneer'],
+  ['#623f2d', 'Quincy'],
+  ['#624e9a', 'Butterfly Bush'],
+  ['#625119', 'West Coast'],
+  ['#626649', 'Finch'],
+  ['#639a8f', 'Patina'],
+  ['#63b76c', 'Fern'],
+  ['#6456b7', 'Blue Violet'],
+  ['#646077', 'Dolphin'],
+  ['#646463', 'Storm Dust'],
+  ['#646a54', 'Siam'],
+  ['#646e75', 'Nevada'],
+  ['#6495ed', 'Cornflower Blue'],
+  ['#64ccdb', 'Viking'],
+  ['#65000b', 'Rosewood'],
+  ['#651a14', 'Cherrywood'],
+  ['#652dc1', 'Purple Heart'],
+  ['#657220', 'Fern Frond'],
+  ['#65745d', 'Willow Grove'],
+  ['#65869f', 'Hoki'],
+  ['#660045', 'Pompadour'],
+  ['#660099', 'Purple'],
+  ['#66023c', 'Tyrian Purple'],
+  ['#661010', 'Dark Tan'],
+  ['#66b58f', 'Silver Tree'],
+  ['#66ff00', 'Bright Green'],
+  ['#66ff66', 'Screamin Green'],
+  ['#67032d', 'Black Rose'],
+  ['#675fa6', 'Scampi'],
+  ['#676662', 'Ironside Gray'],
+  ['#678975', 'Viridian Green'],
+  ['#67a712', 'Christi'],
+  ['#683600', 'Nutmeg Wood Finish'],
+  ['#685558', 'Zambezi'],
+  ['#685e6e', 'Salt Box'],
+  ['#692545', 'Tawny Port'],
+  ['#692d54', 'Finn'],
+  ['#695f62', 'Scorpion'],
+  ['#697e9a', 'Lynch'],
+  ['#6a442e', 'Spice'],
+  ['#6a5d1b', 'Himalaya'],
+  ['#6a6051', 'Soya Bean'],
+  ['#6b2a14', 'Hairy Heath'],
+  ['#6b3fa0', 'Royal Purple'],
+  ['#6b4e31', 'Shingle Fawn'],
+  ['#6b5755', 'Dorado'],
+  ['#6b8ba2', 'Bermuda Gray'],
+  ['#6b8e23', 'Olive Drab'],
+  ['#6c3082', 'Eminence'],
+  ['#6cdae7', 'Turquoise Blue'],
+  ['#6d0101', 'Lonestar'],
+  ['#6d5e54', 'Pine Cone'],
+  ['#6d6c6c', 'Dove Gray'],
+  ['#6d9292', 'Juniper'],
+  ['#6d92a1', 'Gothic'],
+  ['#6e0902', 'Red Oxide'],
+  ['#6e1d14', 'Moccaccino'],
+  ['#6e4826', 'Pickled Bean'],
+  ['#6e4b26', 'Dallas'],
+  ['#6e6d57', 'Kokoda'],
+  ['#6e7783', 'Pale Sky'],
+  ['#6f440c', 'Cafe Royale'],
+  ['#6f6a61', 'Flint'],
+  ['#6f8e63', 'Highland'],
+  ['#6f9d02', 'Limeade'],
+  ['#6fd0c5', 'Downy'],
+  ['#701c1c', 'Persian Plum'],
+  ['#704214', 'Sepia'],
+  ['#704a07', 'Antique Bronze'],
+  ['#704f50', 'Ferra'],
+  ['#706555', 'Coffee'],
+  ['#708090', 'Slate Gray'],
+  ['#711a00', 'Cedar Wood Finish'],
+  ['#71291d', 'Metallic Copper'],
+  ['#714693', 'Affair'],
+  ['#714ab2', 'Studio'],
+  ['#715d47', 'Tobacco Brown'],
+  ['#716338', 'Yellow Metal'],
+  ['#716b56', 'Peat'],
+  ['#716e10', 'Olivetone'],
+  ['#717486', 'Storm Gray'],
+  ['#718080', 'Sirocco'],
+  ['#71d9e2', 'Aquamarine Blue'],
+  ['#72010f', 'Venetian Red'],
+  ['#724a2f', 'Old Copper'],
+  ['#726d4e', 'Go Ben'],
+  ['#727b89', 'Raven'],
+  ['#731e8f', 'Seance'],
+  ['#734a12', 'Raw Umber'],
+  ['#736c9f', 'Kimberly'],
+  ['#736d58', 'Crocodile'],
+  ['#737829', 'Crete'],
+  ['#738678', 'Xanadu'],
+  ['#74640d', 'Spicy Mustard'],
+  ['#747d63', 'Limed Ash'],
+  ['#747d83', 'Rolling Stone'],
+  ['#748881', 'Blue Smoke'],
+  ['#749378', 'Laurel'],
+  ['#74c365', 'Mantis'],
+  ['#755a57', 'Russett'],
+  ['#7563a8', 'Deluge'],
+  ['#76395d', 'Cosmic'],
+  ['#7666c6', 'Blue Marguerite'],
+  ['#76bd17', 'Lima'],
+  ['#76d7ea', 'Sky Blue'],
+  ['#770f05', 'Dark Burgundy'],
+  ['#771f1f', 'Crown Of Thorns'],
+  ['#773f1a', 'Walnut'],
+  ['#776f61', 'Pablo'],
+  ['#778120', 'Pacifika'],
+  ['#779e86', 'Oxley'],
+  ['#77dd77', 'Pastel Green'],
+  ['#780109', 'Japanese Maple'],
+  ['#782d19', 'Mocha'],
+  ['#782f16', 'Peanut'],
+  ['#78866b', 'Camouflage Green'],
+  ['#788a25', 'Wasabi'],
+  ['#788bba', 'Ship Cove'],
+  ['#78a39c', 'Sea Nymph'],
+  ['#795d4c', 'Roman Coffee'],
+  ['#796878', 'Old Lavender'],
+  ['#796989', 'Rum'],
+  ['#796a78', 'Fedora'],
+  ['#796d62', 'Sandstone'],
+  ['#79deec', 'Spray'],
+  ['#7a013a', 'Siren'],
+  ['#7a58c1', 'Fuchsia Blue'],
+  ['#7a7a7a', 'Boulder'],
+  ['#7a89b8', 'Wild Blue Yonder'],
+  ['#7ac488', 'De York'],
+  ['#7b3801', 'Red Beech'],
+  ['#7b3f00', 'Cinnamon'],
+  ['#7b6608', 'Yukon Gold'],
+  ['#7b7874', 'Tapa'],
+  ['#7b7c94', 'Waterloo '],
+  ['#7b8265', 'Flax Smoke'],
+  ['#7b9f80', 'Amulet'],
+  ['#7ba05b', 'Asparagus'],
+  ['#7c1c05', 'Kenyan Copper'],
+  ['#7c7631', 'Pesto'],
+  ['#7c778a', 'Topaz'],
+  ['#7c7b7a', 'Concord'],
+  ['#7c7b82', 'Jumbo'],
+  ['#7c881a', 'Trendy Green'],
+  ['#7ca1a6', 'Gumbo'],
+  ['#7cb0a1', 'Acapulco'],
+  ['#7cb7bb', 'Neptune'],
+  ['#7d2c14', 'Pueblo'],
+  ['#7da98d', 'Bay Leaf'],
+  ['#7dc8f7', 'Malibu'],
+  ['#7dd8c6', 'Bermuda'],
+  ['#7e3a15', 'Copper Canyon'],
+  ['#7f1734', 'Claret'],
+  ['#7f3a02', 'Peru Tan'],
+  ['#7f626d', 'Falcon'],
+  ['#7f7589', 'Mobster'],
+  ['#7f76d3', 'Moody Blue'],
+  ['#7fff00', 'Chartreuse'],
+  ['#7fffd4', 'Aquamarine'],
+  ['#800000', 'Maroon'],
+  ['#800b47', 'Rose Bud Cherry'],
+  ['#801818', 'Falu Red'],
+  ['#80341f', 'Red Robin'],
+  ['#803790', 'Vivid Violet'],
+  ['#80461b', 'Russet'],
+  ['#807e79', 'Friar Gray'],
+  ['#808000', 'Olive'],
+  ['#808080', 'Gray'],
+  ['#80b3ae', 'Gulf Stream'],
+  ['#80b3c4', 'Glacier'],
+  ['#80ccea', 'Seagull'],
+  ['#81422c', 'Nutmeg'],
+  ['#816e71', 'Spicy Pink'],
+  ['#817377', 'Empress'],
+  ['#819885', 'Spanish Green'],
+  ['#826f65', 'Sand Dune'],
+  ['#828685', 'Gunsmoke'],
+  ['#828f72', 'Battleship Gray'],
+  ['#831923', 'Merlot'],
+  ['#837050', 'Shadow'],
+  ['#83aa5d', 'Chelsea Cucumber'],
+  ['#83d0c6', 'Monte Carlo'],
+  ['#843179', 'Plum'],
+  ['#84a0a0', 'Granny Smith'],
+  ['#8581d9', 'Chetwode Blue'],
+  ['#858470', 'Bandicoot'],
+  ['#859faf', 'Bali Hai'],
+  ['#85c4cc', 'Half Baked'],
+  ['#860111', 'Red Devil'],
+  ['#863c3c', 'Lotus'],
+  ['#86483c', 'Ironstone'],
+  ['#864d1e', 'Bull Shot'],
+  ['#86560a', 'Rusty Nail'],
+  ['#868974', 'Bitter'],
+  ['#86949f', 'Regent Gray'],
+  ['#871550', 'Disco'],
+  ['#87756e', 'Americano'],
+  ['#877c7b', 'Hurricane'],
+  ['#878d91', 'Oslo Gray'],
+  ['#87ab39', 'Sushi'],
+  ['#885342', 'Spicy Mix'],
+  ['#886221', 'Kumera'],
+  ['#888387', 'Suva Gray'],
+  ['#888d65', 'Avocado'],
+  ['#893456', 'Camelot'],
+  ['#893843', 'Solid Pink'],
+  ['#894367', 'Cannon Pink'],
+  ['#897d6d', 'Makara'],
+  ['#8a3324', 'Burnt Umber'],
+  ['#8a73d6', 'True V'],
+  ['#8a8360', 'Clay Creek'],
+  ['#8a8389', 'Monsoon'],
+  ['#8a8f8a', 'Stack'],
+  ['#8ab9f1', 'Jordy Blue'],
+  ['#8b00ff', 'Electric Violet'],
+  ['#8b0723', 'Monarch'],
+  ['#8b6b0b', 'Corn Harvest'],
+  ['#8b8470', 'Olive Haze'],
+  ['#8b847e', 'Schooner'],
+  ['#8b8680', 'Natural Gray'],
+  ['#8b9c90', 'Mantle'],
+  ['#8b9fee', 'Portage'],
+  ['#8ba690', 'Envy'],
+  ['#8ba9a5', 'Cascade'],
+  ['#8be6d8', 'Riptide'],
+  ['#8c055e', 'Cardinal Pink'],
+  ['#8c472f', 'Mule Fawn'],
+  ['#8c5738', 'Potters Clay'],
+  ['#8c6495', 'Trendy Pink'],
+  ['#8d0226', 'Paprika'],
+  ['#8d3d38', 'Sanguine Brown'],
+  ['#8d3f3f', 'Tosca'],
+  ['#8d7662', 'Cement'],
+  ['#8d8974', 'Granite Green'],
+  ['#8d90a1', 'Manatee'],
+  ['#8da8cc', 'Polo Blue'],
+  ['#8e0000', 'Red Berry'],
+  ['#8e4d1e', 'Rope'],
+  ['#8e6f70', 'Opium'],
+  ['#8e775e', 'Domino'],
+  ['#8e8190', 'Mamba'],
+  ['#8eabc1', 'Nepal'],
+  ['#8f021c', 'Pohutukawa'],
+  ['#8f3e33', 'El Salva'],
+  ['#8f4b0e', 'Korma'],
+  ['#8f8176', 'Squirrel'],
+  ['#8fd6b4', 'Vista Blue'],
+  ['#900020', 'Burgundy'],
+  ['#901e1e', 'Old Brick'],
+  ['#907874', 'Hemp'],
+  ['#907b71', 'Almond Frost'],
+  ['#908d39', 'Sycamore'],
+  ['#92000a', 'Sangria'],
+  ['#924321', 'Cumin'],
+  ['#926f5b', 'Beaver'],
+  ['#928573', 'Stonewall'],
+  ['#928590', 'Venus'],
+  ['#9370db', 'Medium Purple'],
+  ['#93ccea', 'Cornflower'],
+  ['#93dfb8', 'Algae Green'],
+  ['#944747', 'Copper Rust'],
+  ['#948771', 'Arrowtown'],
+  ['#950015', 'Scarlett'],
+  ['#956387', 'Strikemaster'],
+  ['#959396', 'Mountain Mist'],
+  ['#960018', 'Carmine'],
+  ['#964b00', 'Brown'],
+  ['#967059', 'Leather'],
+  ['#9678b6', "Purple Mountain's Majesty"],
+  ['#967bb6', 'Lavender Purple'],
+  ['#96a8a1', 'Pewter'],
+  ['#96bbab', 'Summer Green'],
+  ['#97605d', 'Au Chico'],
+  ['#9771b5', 'Wisteria'],
+  ['#97cd2d', 'Atlantis'],
+  ['#983d61', 'Vin Rouge'],
+  ['#9874d3', 'Lilac Bush'],
+  ['#98777b', 'Bazaar'],
+  ['#98811b', 'Hacienda'],
+  ['#988d77', 'Pale Oyster'],
+  ['#98ff98', 'Mint Green'],
+  ['#990066', 'Fresh Eggplant'],
+  ['#991199', 'Violet Eggplant'],
+  ['#991613', 'Tamarillo'],
+  ['#991b07', 'Totem Pole'],
+  ['#996666', 'Copper Rose'],
+  ['#9966cc', 'Amethyst'],
+  ['#997a8d', 'Mountbatten Pink'],
+  ['#9999cc', 'Blue Bell'],
+  ['#9a3820', 'Prairie Sand'],
+  ['#9a6e61', 'Toast'],
+  ['#9a9577', 'Gurkha'],
+  ['#9ab973', 'Olivine'],
+  ['#9ac2b8', 'Shadow Green'],
+  ['#9b4703', 'Oregon'],
+  ['#9b9e8f', 'Lemon Grass'],
+  ['#9c3336', 'Stiletto'],
+  ['#9d5616', 'Hawaiian Tan'],
+  ['#9dacb7', 'Gull Gray'],
+  ['#9dc209', 'Pistachio'],
+  ['#9de093', 'Granny Smith Apple'],
+  ['#9de5ff', 'Anakiwa'],
+  ['#9e5302', 'Chelsea Gem'],
+  ['#9e5b40', 'Sepia Skin'],
+  ['#9ea587', 'Sage'],
+  ['#9ea91f', 'Citron'],
+  ['#9eb1cd', 'Rock Blue'],
+  ['#9edee0', 'Morning Glory'],
+  ['#9f381d', 'Cognac'],
+  ['#9f821c', 'Reef Gold'],
+  ['#9f9f9c', 'Star Dust'],
+  ['#9fa0b1', 'Santas Gray'],
+  ['#9fd7d3', 'Sinbad'],
+  ['#9fdd8c', 'Feijoa'],
+  ['#a02712', 'Tabasco'],
+  ['#a1750d', 'Buttered Rum'],
+  ['#a1adb5', 'Hit Gray'],
+  ['#a1c50a', 'Citrus'],
+  ['#a1dad7', 'Aqua Island'],
+  ['#a1e9de', 'Water Leaf'],
+  ['#a2006d', 'Flirt'],
+  ['#a23b6c', 'Rouge'],
+  ['#a26645', 'Cape Palliser'],
+  ['#a2aab3', 'Gray Chateau'],
+  ['#a2aeab', 'Edward'],
+  ['#a3807b', 'Pharlap'],
+  ['#a397b4', 'Amethyst Smoke'],
+  ['#a3e3ed', 'Blizzard Blue'],
+  ['#a4a49d', 'Delta'],
+  ['#a4a6d3', 'Wistful'],
+  ['#a4af6e', 'Green Smoke'],
+  ['#a50b5e', 'Jazzberry Jam'],
+  ['#a59b91', 'Zorba'],
+  ['#a5cb0c', 'Bahia'],
+  ['#a62f20', 'Roof Terracotta'],
+  ['#a65529', 'Paarl'],
+  ['#a68b5b', 'Barley Corn'],
+  ['#a69279', 'Donkey Brown'],
+  ['#a6a29a', 'Dawn'],
+  ['#a72525', 'Mexican Red'],
+  ['#a7882c', 'Luxor Gold'],
+  ['#a85307', 'Rich Gold'],
+  ['#a86515', 'Reno Sand'],
+  ['#a86b6b', 'Coral Tree'],
+  ['#a8989b', 'Dusty Gray'],
+  ['#a899e6', 'Dull Lavender'],
+  ['#a8a589', 'Tallow'],
+  ['#a8ae9c', 'Bud'],
+  ['#a8af8e', 'Locust'],
+  ['#a8bd9f', 'Norway'],
+  ['#a8e3bd', 'Chinook'],
+  ['#a9a491', 'Gray Olive'],
+  ['#a9acb6', 'Aluminium'],
+  ['#a9b2c3', 'Cadet Blue'],
+  ['#a9b497', 'Schist'],
+  ['#a9bdbf', 'Tower Gray'],
+  ['#a9bef2', 'Perano'],
+  ['#a9c6c2', 'Opal'],
+  ['#aa375a', 'Night Shadz'],
+  ['#aa4203', 'Fire'],
+  ['#aa8b5b', 'Muesli'],
+  ['#aa8d6f', 'Sandal'],
+  ['#aaa5a9', 'Shady Lady'],
+  ['#aaa9cd', 'Logan'],
+  ['#aaabb7', 'Spun Pearl'],
+  ['#aad6e6', 'Regent St Blue'],
+  ['#aaf0d1', 'Magic Mint'],
+  ['#ab0563', 'Lipstick'],
+  ['#ab3472', 'Royal Heath'],
+  ['#ab917a', 'Sandrift'],
+  ['#aba0d9', 'Cold Purple'],
+  ['#aba196', 'Bronco'],
+  ['#ac8a56', 'Limed Oak'],
+  ['#ac91ce', 'East Side'],
+  ['#ac9e22', 'Lemon Ginger'],
+  ['#aca494', 'Napa'],
+  ['#aca586', 'Hillary'],
+  ['#aca59f', 'Cloudy'],
+  ['#acacac', 'Silver Chalice'],
+  ['#acb78e', 'Swamp Green'],
+  ['#accbb1', 'Spring Rain'],
+  ['#acdd4d', 'Conifer'],
+  ['#ace1af', 'Celadon'],
+  ['#ad781b', 'Mandalay'],
+  ['#adbed1', 'Casper'],
+  ['#addfad', 'Moss Green'],
+  ['#ade6c4', 'Padua'],
+  ['#adff2f', 'Green Yellow'],
+  ['#ae4560', 'Hippie Pink'],
+  ['#ae6020', 'Desert'],
+  ['#ae809e', 'Bouquet'],
+  ['#af4035', 'Medium Carmine'],
+  ['#af4d43', 'Apple Blossom'],
+  ['#af593e', 'Brown Rust'],
+  ['#af8751', 'Driftwood'],
+  ['#af8f2c', 'Alpine'],
+  ['#af9f1c', 'Lucky'],
+  ['#afa09e', 'Martini'],
+  ['#afb1b8', 'Bombay'],
+  ['#afbdd9', 'Pigeon Post'],
+  ['#b04c6a', 'Cadillac'],
+  ['#b05d54', 'Matrix'],
+  ['#b05e81', 'Tapestry'],
+  ['#b06608', 'Mai Tai'],
+  ['#b09a95', 'Del Rio'],
+  ['#b0e0e6', 'Powder Blue'],
+  ['#b0e313', 'Inch Worm'],
+  ['#b10000', 'Bright Red'],
+  ['#b14a0b', 'Vesuvius'],
+  ['#b1610b', 'Pumpkin Skin'],
+  ['#b16d52', 'Santa Fe'],
+  ['#b19461', 'Teak'],
+  ['#b1e2c1', 'Fringy Flower'],
+  ['#b1f4e7', 'Ice Cold'],
+  ['#b20931', 'Shiraz'],
+  ['#b2a1ea', 'Biloba Flower'],
+  ['#b32d29', 'Tall Poppy'],
+  ['#b35213', 'Fiery Orange'],
+  ['#b38007', 'Hot Toddy'],
+  ['#b3af95', 'Taupe Gray'],
+  ['#b3c110', 'La Rioja'],
+  ['#b43332', 'Well Read'],
+  ['#b44668', 'Blush'],
+  ['#b4cfd3', 'Jungle Mist'],
+  ['#b57281', 'Turkish Rose'],
+  ['#b57edc', 'Lavender'],
+  ['#b5a27f', 'Mongoose'],
+  ['#b5b35c', 'Olive Green'],
+  ['#b5d2ce', 'Jet Stream'],
+  ['#b5ecdf', 'Cruise'],
+  ['#b6316c', 'Hibiscus'],
+  ['#b69d98', 'Thatch'],
+  ['#b6b095', 'Heathered Gray'],
+  ['#b6baa4', 'Eagle'],
+  ['#b6d1ea', 'Spindle'],
+  ['#b6d3bf', 'Gum Leaf'],
+  ['#b7410e', 'Rust'],
+  ['#b78e5c', 'Muddy Waters'],
+  ['#b7a214', 'Sahara'],
+  ['#b7a458', 'Husk'],
+  ['#b7b1b1', 'Nobel'],
+  ['#b7c3d0', 'Heather'],
+  ['#b7f0be', 'Madang'],
+  ['#b81104', 'Milano Red'],
+  ['#b87333', 'Copper'],
+  ['#b8b56a', 'Gimblet'],
+  ['#b8c1b1', 'Green Spring'],
+  ['#b8c25d', 'Celery'],
+  ['#b8e0f9', 'Sail'],
+  ['#b94e48', 'Chestnut'],
+  ['#b95140', 'Crail'],
+  ['#b98d28', 'Marigold'],
+  ['#b9c46a', 'Wild Willow'],
+  ['#b9c8ac', 'Rainee'],
+  ['#ba0101', 'Guardsman Red'],
+  ['#ba450c', 'Rock Spray'],
+  ['#ba6f1e', 'Bourbon'],
+  ['#ba7f03', 'Pirate Gold'],
+  ['#bab1a2', 'Nomad'],
+  ['#bac7c9', 'Submarine'],
+  ['#baeef9', 'Charlotte'],
+  ['#bb3385', 'Medium Red Violet'],
+  ['#bb8983', 'Brandy Rose'],
+  ['#bbd009', 'Rio Grande'],
+  ['#bbd7c1', 'Surf'],
+  ['#bcc9c2', 'Powder Ash'],
+  ['#bd5e2e', 'Tuscany'],
+  ['#bd978e', 'Quicksand'],
+  ['#bdb1a8', 'Silk'],
+  ['#bdb2a1', 'Malta'],
+  ['#bdb3c7', 'Chatelle'],
+  ['#bdbbd7', 'Lavender Gray'],
+  ['#bdbdc6', 'French Gray'],
+  ['#bdc8b3', 'Clay Ash'],
+  ['#bdc9ce', 'Loblolly'],
+  ['#bdedfd', 'French Pass'],
+  ['#bea6c3', 'London Hue'],
+  ['#beb5b7', 'Pink Swan'],
+  ['#bede0d', 'Fuego'],
+  ['#bf5500', 'Rose Of Sharon'],
+  ['#bfb8b0', 'Tide'],
+  ['#bfbed8', 'Blue Haze'],
+  ['#bfc1c2', 'Silver Sand'],
+  ['#bfc921', 'Key Lime Pie'],
+  ['#bfdbe2', 'Ziggurat'],
+  ['#bfff00', 'Lime'],
+  ['#c02b18', 'Thunderbird'],
+  ['#c04737', 'Mojo'],
+  ['#c08081', 'Old Rose'],
+  ['#c0c0c0', 'Silver'],
+  ['#c0d3b9', 'Pale Leaf'],
+  ['#c0d8b6', 'Pixie Green'],
+  ['#c1440e', 'Tia Maria'],
+  ['#c154c1', 'Fuchsia Pink'],
+  ['#c1a004', 'Buddha Gold'],
+  ['#c1b7a4', 'Bison Hide'],
+  ['#c1bab0', 'Tea'],
+  ['#c1becd', 'Gray Suit'],
+  ['#c1d7b0', 'Sprout'],
+  ['#c1f07c', 'Sulu'],
+  ['#c26b03', 'Indochine'],
+  ['#c2955d', 'Twine'],
+  ['#c2bdb6', 'Cotton Seed'],
+  ['#c2cac4', 'Pumice'],
+  ['#c2e8e5', 'Jagged Ice'],
+  ['#c32148', 'Maroon Flush'],
+  ['#c3b091', 'Indian Khaki'],
+  ['#c3bfc1', 'Pale Slate'],
+  ['#c3c3bd', 'Gray Nickel'],
+  ['#c3cde6', 'Periwinkle Gray'],
+  ['#c3d1d1', 'Tiara'],
+  ['#c3ddf9', 'Tropical Blue'],
+  ['#c41e3a', 'Cardinal'],
+  ['#c45655', 'Fuzzy Wuzzy Brown'],
+  ['#c45719', 'Orange Roughy'],
+  ['#c4c4bc', 'Mist Gray'],
+  ['#c4d0b0', 'Coriander'],
+  ['#c4f4eb', 'Mint Tulip'],
+  ['#c54b8c', 'Mulberry'],
+  ['#c59922', 'Nugget'],
+  ['#c5994b', 'Tussock'],
+  ['#c5dbca', 'Sea Mist'],
+  ['#c5e17a', 'Yellow Green'],
+  ['#c62d42', 'Brick Red'],
+  ['#c6726b', 'Contessa'],
+  ['#c69191', 'Oriental Pink'],
+  ['#c6a84b', 'Roti'],
+  ['#c6c3b5', 'Ash'],
+  ['#c6c8bd', 'Kangaroo'],
+  ['#c6e610', 'Las Palmas'],
+  ['#c7031e', 'Monza'],
+  ['#c71585', 'Red Violet'],
+  ['#c7bca2', 'Coral Reef'],
+  ['#c7c1ff', 'Melrose'],
+  ['#c7c4bf', 'Cloud'],
+  ['#c7c9d5', 'Ghost'],
+  ['#c7cd90', 'Pine Glade'],
+  ['#c7dde5', 'Botticelli'],
+  ['#c88a65', 'Antique Brass'],
+  ['#c8a2c8', 'Lilac'],
+  ['#c8a528', 'Hokey Pokey'],
+  ['#c8aabf', 'Lily'],
+  ['#c8b568', 'Laser'],
+  ['#c8e3d7', 'Edgewater'],
+  ['#c96323', 'Piper'],
+  ['#c99415', 'Pizza'],
+  ['#c9a0dc', 'Light Wisteria'],
+  ['#c9b29b', 'Rodeo Dust'],
+  ['#c9b35b', 'Sundance'],
+  ['#c9b93b', 'Earls Green'],
+  ['#c9c0bb', 'Silver Rust'],
+  ['#c9d9d2', 'Conch'],
+  ['#c9ffa2', 'Reef'],
+  ['#c9ffe5', 'Aero Blue'],
+  ['#ca3435', 'Flush Mahogany'],
+  ['#cabb48', 'Turmeric'],
+  ['#cadcd4', 'Paris White'],
+  ['#cae00d', 'Bitter Lemon'],
+  ['#cae6da', 'Skeptic'],
+  ['#cb8fa9', 'Viola'],
+  ['#cbcab6', 'Foggy Gray'],
+  ['#cbd3b0', 'Green Mist'],
+  ['#cbdbd6', 'Nebula'],
+  ['#cc3333', 'Persian Red'],
+  ['#cc5500', 'Burnt Orange'],
+  ['#cc7722', 'Ochre'],
+  ['#cc8899', 'Puce'],
+  ['#cccaa8', 'Thistle Green'],
+  ['#ccccff', 'Periwinkle'],
+  ['#ccff00', 'Electric Lime'],
+  ['#cd5700', 'Tenn'],
+  ['#cd5c5c', 'Chestnut Rose'],
+  ['#cd8429', 'Brandy Punch'],
+  ['#cdf4ff', 'Onahau'],
+  ['#ceb98f', 'Sorrell Brown'],
+  ['#cebaba', 'Cold Turkey'],
+  ['#cec291', 'Yuma'],
+  ['#cec7a7', 'Chino'],
+  ['#cfa39d', 'Eunry'],
+  ['#cfb53b', 'Old Gold'],
+  ['#cfdccf', 'Tasman'],
+  ['#cfe5d2', 'Surf Crest'],
+  ['#cff9f3', 'Humming Bird'],
+  ['#cffaf4', 'Scandal'],
+  ['#d05f04', 'Red Stage'],
+  ['#d06da1', 'Hopbush'],
+  ['#d07d12', 'Meteor'],
+  ['#d0bef8', 'Perfume'],
+  ['#d0c0e5', 'Prelude'],
+  ['#d0f0c0', 'Tea Green'],
+  ['#d18f1b', 'Geebung'],
+  ['#d1bea8', 'Vanilla'],
+  ['#d1c6b4', 'Soft Amber'],
+  ['#d1d2ca', 'Celeste'],
+  ['#d1d2dd', 'Mischka'],
+  ['#d1e231', 'Pear'],
+  ['#d2691e', 'Hot Cinnamon'],
+  ['#d27d46', 'Raw Sienna'],
+  ['#d29eaa', 'Careys Pink'],
+  ['#d2b48c', 'Tan'],
+  ['#d2da97', 'Deco'],
+  ['#d2f6de', 'Blue Romance'],
+  ['#d2f8b0', 'Gossip'],
+  ['#d3cbba', 'Sisal'],
+  ['#d3cdc5', 'Swirl'],
+  ['#d47494', 'Charm'],
+  ['#d4b6af', 'Clam Shell'],
+  ['#d4bf8d', 'Straw'],
+  ['#d4c4a8', 'Akaroa'],
+  ['#d4cd16', 'Bird Flower'],
+  ['#d4d7d9', 'Iron'],
+  ['#d4dfe2', 'Geyser'],
+  ['#d4e2fc', 'Hawkes Blue'],
+  ['#d54600', 'Grenadier'],
+  ['#d591a4', 'Can Can'],
+  ['#d59a6f', 'Whiskey'],
+  ['#d5d195', 'Winter Hazel'],
+  ['#d5f6e3', 'Granny Apple'],
+  ['#d69188', 'My Pink'],
+  ['#d6c562', 'Tacha'],
+  ['#d6cef6', 'Moon Raker'],
+  ['#d6d6d1', 'Quill Gray'],
+  ['#d6ffdb', 'Snowy Mint'],
+  ['#d7837f', 'New York Pink'],
+  ['#d7c498', 'Pavlova'],
+  ['#d7d0ff', 'Fog'],
+  ['#d84437', 'Valencia'],
+  ['#d87c63', 'Japonica'],
+  ['#d8bfd8', 'Thistle'],
+  ['#d8c2d5', 'Maverick'],
+  ['#d8fcfa', 'Foam'],
+  ['#d94972', 'Cabaret'],
+  ['#d99376', 'Burning Sand'],
+  ['#d9b99b', 'Cameo'],
+  ['#d9d6cf', 'Timberwolf'],
+  ['#d9dcc1', 'Tana'],
+  ['#d9e4f5', 'Link Water'],
+  ['#d9f7ff', 'Mabel'],
+  ['#da3287', 'Cerise'],
+  ['#da5b38', 'Flame Pea'],
+  ['#da6304', 'Bamboo'],
+  ['#da6a41', 'Red Damask'],
+  ['#da70d6', 'Orchid'],
+  ['#da8a67', 'Copperfield'],
+  ['#daa520', 'Golden Grass'],
+  ['#daecd6', 'Zanah'],
+  ['#daf4f0', 'Iceberg'],
+  ['#dafaff', 'Oyster Bay'],
+  ['#db5079', 'Cranberry'],
+  ['#db9690', 'Petite Orchid'],
+  ['#db995e', 'Di Serria'],
+  ['#dbdbdb', 'Alto'],
+  ['#dbfff8', 'Frosted Mint'],
+  ['#dc143c', 'Crimson'],
+  ['#dc4333', 'Punch'],
+  ['#dcb20c', 'Galliano'],
+  ['#dcb4bc', 'Blossom'],
+  ['#dcd747', 'Wattle'],
+  ['#dcd9d2', 'Westar'],
+  ['#dcddcc', 'Moon Mist'],
+  ['#dcedb4', 'Caper'],
+  ['#dcf0ea', 'Swans Down'],
+  ['#ddd6d5', 'Swiss Coffee'],
+  ['#ddf9f1', 'White Ice'],
+  ['#de3163', 'Cerise Red'],
+  ['#de6360', 'Roman'],
+  ['#dea681', 'Tumbleweed'],
+  ['#deba13', 'Gold Tips'],
+  ['#dec196', 'Brandy'],
+  ['#decbc6', 'Wafer'],
+  ['#ded4a4', 'Sapling'],
+  ['#ded717', 'Barberry'],
+  ['#dee5c0', 'Beryl Green'],
+  ['#def5ff', 'Pattens Blue'],
+  ['#df73ff', 'Heliotrope'],
+  ['#dfbe6f', 'Apache'],
+  ['#dfcd6f', 'Chenin'],
+  ['#dfcfdb', 'Lola'],
+  ['#dfecda', 'Willow Brook'],
+  ['#dfff00', 'Chartreuse Yellow'],
+  ['#e0b0ff', 'Mauve'],
+  ['#e0b646', 'Anzac'],
+  ['#e0b974', 'Harvest Gold'],
+  ['#e0c095', 'Calico'],
+  ['#e0ffff', 'Baby Blue'],
+  ['#e16865', 'Sunglo'],
+  ['#e1bc64', 'Equator'],
+  ['#e1c0c8', 'Pink Flare'],
+  ['#e1e6d6', 'Periglacial Blue'],
+  ['#e1ead4', 'Kidnapper'],
+  ['#e1f6e8', 'Tara'],
+  ['#e25465', 'Mandy'],
+  ['#e2725b', 'Terracotta'],
+  ['#e28913', 'Golden Bell'],
+  ['#e292c0', 'Shocking'],
+  ['#e29418', 'Dixie'],
+  ['#e29cd2', 'Light Orchid'],
+  ['#e2d8ed', 'Snuff'],
+  ['#e2ebed', 'Mystic'],
+  ['#e2f3ec', 'Apple Green'],
+  ['#e30b5c', 'Razzmatazz'],
+  ['#e32636', 'Alizarin Crimson'],
+  ['#e34234', 'Cinnabar'],
+  ['#e3bebe', 'Cavern Pink'],
+  ['#e3f5e1', 'Peppermint'],
+  ['#e3f988', 'Mindaro'],
+  ['#e47698', 'Deep Blush'],
+  ['#e49b0f', 'Gamboge'],
+  ['#e4c2d5', 'Melanie'],
+  ['#e4cfde', 'Twilight'],
+  ['#e4d1c0', 'Bone'],
+  ['#e4d422', 'Sunflower'],
+  ['#e4d5b7', 'Grain Brown'],
+  ['#e4d69b', 'Zombie'],
+  ['#e4f6e7', 'Frostee'],
+  ['#e4ffd1', 'Snow Flurry'],
+  ['#e52b50', 'Amaranth'],
+  ['#e5841b', 'Zest'],
+  ['#e5ccc9', 'Dust Storm'],
+  ['#e5d7bd', 'Stark White'],
+  ['#e5d8af', 'Hampton'],
+  ['#e5e0e1', 'Bon Jour'],
+  ['#e5e5e5', 'Mercury'],
+  ['#e5f9f6', 'Polar'],
+  ['#e64e03', 'Trinidad'],
+  ['#e6be8a', 'Gold Sand'],
+  ['#e6bea5', 'Cashmere'],
+  ['#e6d7b9', 'Double Spanish White'],
+  ['#e6e4d4', 'Satin Linen'],
+  ['#e6f2ea', 'Harp'],
+  ['#e6f8f3', 'Off Green'],
+  ['#e6ffe9', 'Hint Of Green'],
+  ['#e6ffff', 'Tranquil'],
+  ['#e77200', 'Mango Tango'],
+  ['#e7730a', 'Christine'],
+  ['#e79f8c', 'Tonys Pink'],
+  ['#e79fc4', 'Kobi'],
+  ['#e7bcb4', 'Rose Fog'],
+  ['#e7bf05', 'Corn'],
+  ['#e7cd8c', 'Putty'],
+  ['#e7ece6', 'Gray Nurse'],
+  ['#e7f8ff', 'Lily White'],
+  ['#e7feff', 'Bubbles'],
+  ['#e89928', 'Fire Bush'],
+  ['#e8b9b3', 'Shilo'],
+  ['#e8e0d5', 'Pearl Bush'],
+  ['#e8ebe0', 'Green White'],
+  ['#e8f1d4', 'Chrome White'],
+  ['#e8f2eb', 'Gin'],
+  ['#e8f5f2', 'Aqua Squeeze'],
+  ['#e96e00', 'Clementine'],
+  ['#e97451', 'Burnt Sienna'],
+  ['#e97c07', 'Tahiti Gold'],
+  ['#e9cecd', 'Oyster Pink'],
+  ['#e9d75a', 'Confetti'],
+  ['#e9e3e3', 'Ebb'],
+  ['#e9f8ed', 'Ottoman'],
+  ['#e9fffd', 'Clear Day'],
+  ['#ea88a8', 'Carissma'],
+  ['#eaae69', 'Porsche'],
+  ['#eab33b', 'Tulip Tree'],
+  ['#eac674', 'Rob Roy'],
+  ['#eadab8', 'Raffia'],
+  ['#eae8d4', 'White Rock'],
+  ['#eaf6ee', 'Panache'],
+  ['#eaf6ff', 'Solitude'],
+  ['#eaf9f5', 'Aqua Spring'],
+  ['#eafffe', 'Dew'],
+  ['#eb9373', 'Apricot'],
+  ['#ebc2af', 'Zinnwaldite'],
+  ['#eca927', 'Fuel Yellow'],
+  ['#ecc54e', 'Ronchi'],
+  ['#ecc7ee', 'French Lilac'],
+  ['#eccdb9', 'Just Right'],
+  ['#ece090', 'Wild Rice'],
+  ['#ecebbd', 'Fall Green'],
+  ['#ecebce', 'Aths Special'],
+  ['#ecf245', 'Starship'],
+  ['#ed0a3f', 'Red Ribbon'],
+  ['#ed7a1c', 'Tango'],
+  ['#ed9121', 'Carrot Orange'],
+  ['#ed989e', 'Sea Pink'],
+  ['#edb381', 'Tacao'],
+  ['#edc9af', 'Desert Sand'],
+  ['#edcdab', 'Pancho'],
+  ['#eddcb1', 'Chamois'],
+  ['#edea99', 'Primrose'],
+  ['#edf5dd', 'Frost'],
+  ['#edf5f5', 'Aqua Haze'],
+  ['#edf6ff', 'Zumthor'],
+  ['#edf9f1', 'Narvik'],
+  ['#edfc84', 'Honeysuckle'],
+  ['#ee82ee', 'Lavender Magenta'],
+  ['#eec1be', 'Beauty Bush'],
+  ['#eed794', 'Chalky'],
+  ['#eed9c4', 'Almond'],
+  ['#eedc82', 'Flax'],
+  ['#eededa', 'Bizarre'],
+  ['#eee3ad', 'Double Colonial White'],
+  ['#eeeee8', 'Cararra'],
+  ['#eeef78', 'Manz'],
+  ['#eef0c8', 'Tahuna Sands'],
+  ['#eef0f3', 'Athens Gray'],
+  ['#eef3c3', 'Tusk'],
+  ['#eef4de', 'Loafer'],
+  ['#eef6f7', 'Catskill White'],
+  ['#eefdff', 'Twilight Blue'],
+  ['#eeff9a', 'Jonquil'],
+  ['#eeffe2', 'Rice Flower'],
+  ['#ef863f', 'Jaffa'],
+  ['#efefef', 'Gallery'],
+  ['#eff2f3', 'Porcelain'],
+  ['#f091a9', 'Mauvelous'],
+  ['#f0d52d', 'Golden Dream'],
+  ['#f0db7d', 'Golden Sand'],
+  ['#f0dc82', 'Buff'],
+  ['#f0e2ec', 'Prim'],
+  ['#f0e68c', 'Khaki'],
+  ['#f0eefd', 'Selago'],
+  ['#f0eeff', 'Titan White'],
+  ['#f0f8ff', 'Alice Blue'],
+  ['#f0fcea', 'Feta'],
+  ['#f18200', 'Gold Drop'],
+  ['#f19bab', 'Wewak'],
+  ['#f1e788', 'Sahara Sand'],
+  ['#f1e9d2', 'Parchment'],
+  ['#f1e9ff', 'Blue Chalk'],
+  ['#f1eec1', 'Mint Julep'],
+  ['#f1f1f1', 'Seashell'],
+  ['#f1f7f2', 'Saltpan'],
+  ['#f1ffad', 'Tidal'],
+  ['#f1ffc8', 'Chiffon'],
+  ['#f2552a', 'Flamingo'],
+  ['#f28500', 'Tangerine'],
+  ['#f2c3b2', 'Mandys Pink'],
+  ['#f2f2f2', 'Concrete'],
+  ['#f2fafa', 'Black Squeeze'],
+  ['#f34723', 'Pomegranate'],
+  ['#f3ad16', 'Buttercup'],
+  ['#f3d69d', 'New Orleans'],
+  ['#f3d9df', 'Vanilla Ice'],
+  ['#f3e7bb', 'Sidecar'],
+  ['#f3e9e5', 'Dawn Pink'],
+  ['#f3edcf', 'Wheatfield'],
+  ['#f3fb62', 'Canary'],
+  ['#f3fbd4', 'Orinoco'],
+  ['#f3ffd8', 'Carla'],
+  ['#f400a1', 'Hollywood Cerise'],
+  ['#f4a460', 'Sandy brown'],
+  ['#f4c430', 'Saffron'],
+  ['#f4d81c', 'Ripe Lemon'],
+  ['#f4ebd3', 'Janna'],
+  ['#f4f2ee', 'Pampas'],
+  ['#f4f4f4', 'Wild Sand'],
+  ['#f4f8ff', 'Zircon'],
+  ['#f57584', 'Froly'],
+  ['#f5c85c', 'Cream Can'],
+  ['#f5c999', 'Manhattan'],
+  ['#f5d5a0', 'Maize'],
+  ['#f5deb3', 'Wheat'],
+  ['#f5e7a2', 'Sandwisp'],
+  ['#f5e7e2', 'Pot Pourri'],
+  ['#f5e9d3', 'Albescent White'],
+  ['#f5edef', 'Soft Peach'],
+  ['#f5f3e5', 'Ecru White'],
+  ['#f5f5dc', 'Beige'],
+  ['#f5fb3d', 'Golden Fizz'],
+  ['#f5ffbe', 'Australian Mint'],
+  ['#f64a8a', 'French Rose'],
+  ['#f653a6', 'Brilliant Rose'],
+  ['#f6a4c9', 'Illusion'],
+  ['#f6f0e6', 'Merino'],
+  ['#f6f7f7', 'Black Haze'],
+  ['#f6ffdc', 'Spring Sun'],
+  ['#f7468a', 'Violet Red'],
+  ['#f77703', 'Chilean Fire'],
+  ['#f77fbe', 'Persian Pink'],
+  ['#f7b668', 'Rajah'],
+  ['#f7c8da', 'Azalea'],
+  ['#f7dbe6', 'We Peep'],
+  ['#f7f2e1', 'Quarter Spanish White'],
+  ['#f7f5fa', 'Whisper'],
+  ['#f7faf7', 'Snow Drift'],
+  ['#f8b853', 'Casablanca'],
+  ['#f8c3df', 'Chantilly'],
+  ['#f8d9e9', 'Cherub'],
+  ['#f8db9d', 'Marzipan'],
+  ['#f8dd5c', 'Energy Yellow'],
+  ['#f8e4bf', 'Givry'],
+  ['#f8f0e8', 'White Linen'],
+  ['#f8f4ff', 'Magnolia'],
+  ['#f8f6f1', 'Spring Wood'],
+  ['#f8f7dc', 'Coconut Cream'],
+  ['#f8f7fc', 'White Lilac'],
+  ['#f8f8f7', 'Desert Storm'],
+  ['#f8f99c', 'Texas'],
+  ['#f8facd', 'Corn Field'],
+  ['#f8fdd3', 'Mimosa'],
+  ['#f95a61', 'Carnation'],
+  ['#f9bf58', 'Saffron Mango'],
+  ['#f9e0ed', 'Carousel Pink'],
+  ['#f9e4bc', 'Dairy Cream'],
+  ['#f9e663', 'Portica'],
+  ['#f9eaf3', 'Amour'],
+  ['#f9f8e4', 'Rum Swizzle'],
+  ['#f9ff8b', 'Dolly'],
+  ['#f9fff6', 'Sugar Cane'],
+  ['#fa7814', 'Ecstasy'],
+  ['#fa9d5a', 'Tan Hide'],
+  ['#fad3a2', 'Corvette'],
+  ['#fadfad', 'Peach Yellow'],
+  ['#fae600', 'Turbo'],
+  ['#faeab9', 'Astra'],
+  ['#faeccc', 'Champagne'],
+  ['#faf0e6', 'Linen'],
+  ['#faf3f0', 'Fantasy'],
+  ['#faf7d6', 'Citrine White'],
+  ['#fafafa', 'Alabaster'],
+  ['#fafde4', 'Hint Of Yellow'],
+  ['#faffa4', 'Milan'],
+  ['#fb607f', 'Brink Pink'],
+  ['#fb8989', 'Geraldine'],
+  ['#fba0e3', 'Lavender Rose'],
+  ['#fba129', 'Sea Buckthorn'],
+  ['#fbac13', 'Sun'],
+  ['#fbaed2', 'Lavender Pink'],
+  ['#fbb2a3', 'Rose Bud'],
+  ['#fbbeda', 'Cupid'],
+  ['#fbcce7', 'Classic Rose'],
+  ['#fbceb1', 'Apricot Peach'],
+  ['#fbe7b2', 'Banana Mania'],
+  ['#fbe870', 'Marigold Yellow'],
+  ['#fbe96c', 'Festival'],
+  ['#fbea8c', 'Sweet Corn'],
+  ['#fbec5d', 'Candy Corn'],
+  ['#fbf9f9', 'Hint Of Red'],
+  ['#fbffba', 'Shalimar'],
+  ['#fc0fc0', 'Shocking Pink'],
+  ['#fc80a5', 'Tickle Me Pink'],
+  ['#fc9c1d', 'Tree Poppy'],
+  ['#fcc01e', 'Lightning Yellow'],
+  ['#fcd667', 'Goldenrod'],
+  ['#fcd917', 'Candlelight'],
+  ['#fcda98', 'Cherokee'],
+  ['#fcf4d0', 'Double Pearl Lusta'],
+  ['#fcf4dc', 'Pearl Lusta'],
+  ['#fcf8f7', 'Vista White'],
+  ['#fcfbf3', 'Bianca'],
+  ['#fcfeda', 'Moon Glow'],
+  ['#fcffe7', 'China Ivory'],
+  ['#fcfff9', 'Ceramic'],
+  ['#fd0e35', 'Torch Red'],
+  ['#fd5b78', 'Wild Watermelon'],
+  ['#fd7b33', 'Crusta'],
+  ['#fd7c07', 'Sorbus'],
+  ['#fd9fa2', 'Sweet Pink'],
+  ['#fdd5b1', 'Light Apricot'],
+  ['#fdd7e4', 'Pig Pink'],
+  ['#fde1dc', 'Cinderella'],
+  ['#fde295', 'Golden Glow'],
+  ['#fde910', 'Lemon'],
+  ['#fdf5e6', 'Old Lace'],
+  ['#fdf6d3', 'Half Colonial White'],
+  ['#fdf7ad', 'Drover'],
+  ['#fdfeb8', 'Pale Prim'],
+  ['#fdffd5', 'Cumulus'],
+  ['#fe28a2', 'Persian Rose'],
+  ['#fe4c40', 'Sunset Orange'],
+  ['#fe6f5e', 'Bittersweet'],
+  ['#fe9d04', 'California'],
+  ['#fea904', 'Yellow Sea'],
+  ['#febaad', 'Melon'],
+  ['#fed33c', 'Bright Sun'],
+  ['#fed85d', 'Dandelion'],
+  ['#fedb8d', 'Salomie'],
+  ['#fee5ac', 'Cape Honey'],
+  ['#feebf3', 'Remy'],
+  ['#feefce', 'Oasis'],
+  ['#fef0ec', 'Bridesmaid'],
+  ['#fef2c7', 'Beeswax'],
+  ['#fef3d8', 'Bleach White'],
+  ['#fef4cc', 'Pipi'],
+  ['#fef4db', 'Half Spanish White'],
+  ['#fef4f8', 'Wisp Pink'],
+  ['#fef5f1', 'Provincial Pink'],
+  ['#fef7de', 'Half Dutch White'],
+  ['#fef8e2', 'Solitaire'],
+  ['#fef8ff', 'White Pointer'],
+  ['#fef9e3', 'Off Yellow'],
+  ['#fefced', 'Orange White'],
+  ['#ff0000', 'Red'],
+  ['#ff007f', 'Rose'],
+  ['#ff00cc', 'Purple Pizzazz'],
+  ['#ff00ff', 'Magenta Fuchsia'],
+  ['#ff2400', 'Scarlet'],
+  ['#ff3399', 'Wild Strawberry'],
+  ['#ff33cc', 'Razzle Dazzle Rose'],
+  ['#ff355e', 'Radical Red'],
+  ['#ff3f34', 'Red Orange'],
+  ['#ff4040', 'Coral Red'],
+  ['#ff4d00', 'Vermilion'],
+  ['#ff4f00', 'International Orange'],
+  ['#ff6037', 'Outrageous Orange'],
+  ['#ff6600', 'Blaze Orange'],
+  ['#ff66ff', 'Pink Flamingo'],
+  ['#ff681f', 'Orange'],
+  ['#ff69b4', 'Hot Pink'],
+  ['#ff6b53', 'Persimmon'],
+  ['#ff6fff', 'Blush Pink'],
+  ['#ff7034', 'Burning Orange'],
+  ['#ff7518', 'Pumpkin'],
+  ['#ff7d07', 'Flamenco'],
+  ['#ff7f00', 'Flush Orange'],
+  ['#ff7f50', 'Coral'],
+  ['#ff8c69', 'Salmon'],
+  ['#ff9000', 'Pizazz'],
+  ['#ff910f', 'West Side'],
+  ['#ff91a4', 'Pink Salmon'],
+  ['#ff9933', 'Neon Carrot'],
+  ['#ff9966', 'Atomic Tangerine'],
+  ['#ff9980', 'Vivid Tangerine'],
+  ['#ff9e2c', 'Sunshade'],
+  ['#ffa000', 'Orange Peel'],
+  ['#ffa194', 'Mona Lisa'],
+  ['#ffa500', 'Web Orange'],
+  ['#ffa6c9', 'Carnation Pink'],
+  ['#ffab81', 'Hit Pink'],
+  ['#ffae42', 'Yellow Orange'],
+  ['#ffb0ac', 'Cornflower Lilac'],
+  ['#ffb1b3', 'Sundown'],
+  ['#ffb31f', 'My Sin'],
+  ['#ffb555', 'Texas Rose'],
+  ['#ffb7d5', 'Cotton Candy'],
+  ['#ffb97b', 'Macaroni And Cheese'],
+  ['#ffba00', 'Selective Yellow'],
+  ['#ffbd5f', 'Koromiko'],
+  ['#ffbf00', 'Amber'],
+  ['#ffc0a8', 'Wax Flower'],
+  ['#ffc0cb', 'Pink'],
+  ['#ffc3c0', 'Your Pink'],
+  ['#ffc901', 'Supernova'],
+  ['#ffcba4', 'Flesh'],
+  ['#ffcc33', 'Sunglow'],
+  ['#ffcc5c', 'Golden Tainoi'],
+  ['#ffcc99', 'Peach Orange'],
+  ['#ffcd8c', 'Chardonnay'],
+  ['#ffd1dc', 'Pastel Pink'],
+  ['#ffd2b7', 'Romantic'],
+  ['#ffd38c', 'Grandis'],
+  ['#ffd700', 'Gold'],
+  ['#ffd800', 'School Bus Yellow'],
+  ['#ffd8d9', 'Cosmos'],
+  ['#ffdb58', 'Mustard'],
+  ['#ffdcd6', 'Peach Schnapps'],
+  ['#ffddaf', 'Caramel'],
+  ['#ffddcd', 'Tuft Bush'],
+  ['#ffddcf', 'Watusi'],
+  ['#ffddf4', 'Pink Lace'],
+  ['#ffdead', 'Navajo White'],
+  ['#ffdeb3', 'Frangipani'],
+  ['#ffe1df', 'Pippin'],
+  ['#ffe1f2', 'Pale Rose'],
+  ['#ffe2c5', 'Negroni'],
+  ['#ffe5a0', 'Cream Brulee'],
+  ['#ffe5b4', 'Peach'],
+  ['#ffe6c7', 'Tequila'],
+  ['#ffe772', 'Kournikova'],
+  ['#ffeac8', 'Sandy Beach'],
+  ['#ffead4', 'Karry'],
+  ['#ffec13', 'Broom'],
+  ['#ffedbc', 'Colonial White'],
+  ['#ffeed8', 'Derby'],
+  ['#ffefa1', 'Vis Vis'],
+  ['#ffefc1', 'Egg White'],
+  ['#ffefd5', 'Papaya Whip'],
+  ['#ffefec', 'Fair Pink'],
+  ['#fff0db', 'Peach Cream'],
+  ['#fff0f5', 'Lavender Blush'],
+  ['#fff14f', 'Gorse'],
+  ['#fff1b5', 'Buttermilk'],
+  ['#fff1d8', 'Pink Lady'],
+  ['#fff1ee', 'Forget Me Not'],
+  ['#fff1f9', 'Tutu'],
+  ['#fff39d', 'Picasso'],
+  ['#fff3f1', 'Chardon'],
+  ['#fff46e', 'Paris Daisy'],
+  ['#fff4ce', 'Barley White'],
+  ['#fff4dd', 'Egg Sour'],
+  ['#fff4e0', 'Sazerac'],
+  ['#fff4e8', 'Serenade'],
+  ['#fff4f3', 'Chablis'],
+  ['#fff5ee', 'Seashell Peach'],
+  ['#fff5f3', 'Sauvignon'],
+  ['#fff6d4', 'Milk Punch'],
+  ['#fff6df', 'Varden'],
+  ['#fff6f5', 'Rose White'],
+  ['#fff8d1', 'Baja White'],
+  ['#fff9e2', 'Gin Fizz'],
+  ['#fff9e6', 'Early Dawn'],
+  ['#fffacd', 'Lemon Chiffon'],
+  ['#fffaf4', 'Bridal Heath'],
+  ['#fffbdc', 'Scotch Mist'],
+  ['#fffbf9', 'Soapstone'],
+  ['#fffc99', 'Witch Haze'],
+  ['#fffcea', 'Buttery White'],
+  ['#fffcee', 'Island Spice'],
+  ['#fffdd0', 'Cream'],
+  ['#fffde6', 'Chilean Heath'],
+  ['#fffde8', 'Travertine'],
+  ['#fffdf3', 'Orchid White'],
+  ['#fffdf4', 'Quarter Pearl Lusta'],
+  ['#fffee1', 'Half And Half'],
+  ['#fffeec', 'Apricot White'],
+  ['#fffef0', 'Rice Cake'],
+  ['#fffef6', 'Black White'],
+  ['#fffefd', 'Romance'],
+  ['#ffff00', 'Yellow'],
+  ['#ffff66', 'Laser Lemon'],
+  ['#ffff99', 'Pale Canary'],
+  ['#ffffb4', 'Portafino'],
+  ['#fffff0', 'Ivory'],
+  ['#ffffff', 'White']
+];
+
+/**
+ * Map Of hex color values to color names
+ *
+ * - key: hex value
+ * - value: color name
+ */
+export const colorNameMap = colorNames.reduce<Record<string, string>>((acc, [hex, name]) => {
+  acc[hex] = name;
+  return acc;
+}, {});

+ 356 - 0
packages/color/src/constant/palette.ts

@@ -0,0 +1,356 @@
+import type { ColorPaletteFamily } from '../types';
+
+export const colorPalettes: ColorPaletteFamily[] = [
+  {
+    name: 'Slate',
+    palettes: [
+      { hex: '#f8fafc', number: 50 },
+      { hex: '#f1f5f9', number: 100 },
+      { hex: '#e2e8f0', number: 200 },
+      { hex: '#cbd5e1', number: 300 },
+      { hex: '#94a3b8', number: 400 },
+      { hex: '#64748b', number: 500 },
+      { hex: '#475569', number: 600 },
+      { hex: '#334155', number: 700 },
+      { hex: '#1e293b', number: 800 },
+      { hex: '#0f172a', number: 900 },
+      { hex: '#020617', number: 950 }
+    ]
+  },
+  {
+    name: 'Gray',
+    palettes: [
+      { hex: '#f9fafb', number: 50 },
+      { hex: '#f3f4f6', number: 100 },
+      { hex: '#e5e7eb', number: 200 },
+      { hex: '#d1d5db', number: 300 },
+      { hex: '#9ca3af', number: 400 },
+      { hex: '#6b7280', number: 500 },
+      { hex: '#4b5563', number: 600 },
+      { hex: '#374151', number: 700 },
+      { hex: '#1f2937', number: 800 },
+      { hex: '#111827', number: 900 },
+      { hex: '#030712', number: 950 }
+    ]
+  },
+  {
+    name: 'Zinc',
+    palettes: [
+      { hex: '#fafafa', number: 50 },
+      { hex: '#f4f4f5', number: 100 },
+      { hex: '#e4e4e7', number: 200 },
+      { hex: '#d4d4d8', number: 300 },
+      { hex: '#a1a1aa', number: 400 },
+      { hex: '#71717a', number: 500 },
+      { hex: '#52525b', number: 600 },
+      { hex: '#3f3f46', number: 700 },
+      { hex: '#27272a', number: 800 },
+      { hex: '#18181b', number: 900 },
+      { hex: '#09090b', number: 950 }
+    ]
+  },
+  {
+    name: 'Neutral',
+    palettes: [
+      { hex: '#fafafa', number: 50 },
+      { hex: '#f5f5f5', number: 100 },
+      { hex: '#e5e5e5', number: 200 },
+      { hex: '#d4d4d4', number: 300 },
+      { hex: '#a3a3a3', number: 400 },
+      { hex: '#737373', number: 500 },
+      { hex: '#525252', number: 600 },
+      { hex: '#404040', number: 700 },
+      { hex: '#262626', number: 800 },
+      { hex: '#171717', number: 900 },
+      { hex: '#0a0a0a', number: 950 }
+    ]
+  },
+  {
+    name: 'Stone',
+    palettes: [
+      { hex: '#fafaf9', number: 50 },
+      { hex: '#f5f5f4', number: 100 },
+      { hex: '#e7e5e4', number: 200 },
+      { hex: '#d6d3d1', number: 300 },
+      { hex: '#a8a29e', number: 400 },
+      { hex: '#78716c', number: 500 },
+      { hex: '#57534e', number: 600 },
+      { hex: '#44403c', number: 700 },
+      { hex: '#292524', number: 800 },
+      { hex: '#1c1917', number: 900 },
+      { hex: '#0c0a09', number: 950 }
+    ]
+  },
+  {
+    name: 'Red',
+    palettes: [
+      { hex: '#fef2f2', number: 50 },
+      { hex: '#fee2e2', number: 100 },
+      { hex: '#fecaca', number: 200 },
+      { hex: '#fca5a5', number: 300 },
+      { hex: '#f87171', number: 400 },
+      { hex: '#ef4444', number: 500 },
+      { hex: '#dc2626', number: 600 },
+      { hex: '#b91c1c', number: 700 },
+      { hex: '#991b1b', number: 800 },
+      { hex: '#7f1d1d', number: 900 },
+      { hex: '#450a0a', number: 950 }
+    ]
+  },
+  {
+    name: 'Orange',
+    palettes: [
+      { hex: '#fff7ed', number: 50 },
+      { hex: '#ffedd5', number: 100 },
+      { hex: '#fed7aa', number: 200 },
+      { hex: '#fdba74', number: 300 },
+      { hex: '#fb923c', number: 400 },
+      { hex: '#f97316', number: 500 },
+      { hex: '#ea580c', number: 600 },
+      { hex: '#c2410c', number: 700 },
+      { hex: '#9a3412', number: 800 },
+      { hex: '#7c2d12', number: 900 },
+      { hex: '#431407', number: 950 }
+    ]
+  },
+  {
+    name: 'Amber',
+    palettes: [
+      { hex: '#fffbeb', number: 50 },
+      { hex: '#fef3c7', number: 100 },
+      { hex: '#fde68a', number: 200 },
+      { hex: '#fcd34d', number: 300 },
+      { hex: '#fbbf24', number: 400 },
+      { hex: '#f59e0b', number: 500 },
+      { hex: '#d97706', number: 600 },
+      { hex: '#b45309', number: 700 },
+      { hex: '#92400e', number: 800 },
+      { hex: '#78350f', number: 900 },
+      { hex: '#451a03', number: 950 }
+    ]
+  },
+  {
+    name: 'Yellow',
+    palettes: [
+      { hex: '#fefce8', number: 50 },
+      { hex: '#fef9c3', number: 100 },
+      { hex: '#fef08a', number: 200 },
+      { hex: '#fde047', number: 300 },
+      { hex: '#facc15', number: 400 },
+      { hex: '#eab308', number: 500 },
+      { hex: '#ca8a04', number: 600 },
+      { hex: '#a16207', number: 700 },
+      { hex: '#854d0e', number: 800 },
+      { hex: '#713f12', number: 900 },
+      { hex: '#422006', number: 950 }
+    ]
+  },
+  {
+    name: 'Lime',
+    palettes: [
+      { hex: '#f7fee7', number: 50 },
+      { hex: '#ecfccb', number: 100 },
+      { hex: '#d9f99d', number: 200 },
+      { hex: '#bef264', number: 300 },
+      { hex: '#a3e635', number: 400 },
+      { hex: '#84cc16', number: 500 },
+      { hex: '#65a30d', number: 600 },
+      { hex: '#4d7c0f', number: 700 },
+      { hex: '#3f6212', number: 800 },
+      { hex: '#365314', number: 900 },
+      { hex: '#1a2e05', number: 950 }
+    ]
+  },
+  {
+    name: 'Green',
+    palettes: [
+      { hex: '#f0fdf4', number: 50 },
+      { hex: '#dcfce7', number: 100 },
+      { hex: '#bbf7d0', number: 200 },
+      { hex: '#86efac', number: 300 },
+      { hex: '#4ade80', number: 400 },
+      { hex: '#22c55e', number: 500 },
+      { hex: '#16a34a', number: 600 },
+      { hex: '#15803d', number: 700 },
+      { hex: '#166534', number: 800 },
+      { hex: '#14532d', number: 900 },
+      { hex: '#052e16', number: 950 }
+    ]
+  },
+  {
+    name: 'Emerald',
+    palettes: [
+      { hex: '#ecfdf5', number: 50 },
+      { hex: '#d1fae5', number: 100 },
+      { hex: '#a7f3d0', number: 200 },
+      { hex: '#6ee7b7', number: 300 },
+      { hex: '#34d399', number: 400 },
+      { hex: '#10b981', number: 500 },
+      { hex: '#059669', number: 600 },
+      { hex: '#047857', number: 700 },
+      { hex: '#065f46', number: 800 },
+      { hex: '#064e3b', number: 900 },
+      { hex: '#022c22', number: 950 }
+    ]
+  },
+  {
+    name: 'Teal',
+    palettes: [
+      { hex: '#f0fdfa', number: 50 },
+      { hex: '#ccfbf1', number: 100 },
+      { hex: '#99f6e4', number: 200 },
+      { hex: '#5eead4', number: 300 },
+      { hex: '#2dd4bf', number: 400 },
+      { hex: '#14b8a6', number: 500 },
+      { hex: '#0d9488', number: 600 },
+      { hex: '#0f766e', number: 700 },
+      { hex: '#115e59', number: 800 },
+      { hex: '#134e4a', number: 900 },
+      { hex: '#042f2e', number: 950 }
+    ]
+  },
+  {
+    name: 'Cyan',
+    palettes: [
+      { hex: '#ecfeff', number: 50 },
+      { hex: '#cffafe', number: 100 },
+      { hex: '#a5f3fc', number: 200 },
+      { hex: '#67e8f9', number: 300 },
+      { hex: '#22d3ee', number: 400 },
+      { hex: '#06b6d4', number: 500 },
+      { hex: '#0891b2', number: 600 },
+      { hex: '#0e7490', number: 700 },
+      { hex: '#155e75', number: 800 },
+      { hex: '#164e63', number: 900 },
+      { hex: '#083344', number: 950 }
+    ]
+  },
+  {
+    name: 'Sky',
+    palettes: [
+      { hex: '#f0f9ff', number: 50 },
+      { hex: '#e0f2fe', number: 100 },
+      { hex: '#bae6fd', number: 200 },
+      { hex: '#7dd3fc', number: 300 },
+      { hex: '#38bdf8', number: 400 },
+      { hex: '#0ea5e9', number: 500 },
+      { hex: '#0284c7', number: 600 },
+      { hex: '#0369a1', number: 700 },
+      { hex: '#075985', number: 800 },
+      { hex: '#0c4a6e', number: 900 },
+      { hex: '#082f49', number: 950 }
+    ]
+  },
+  {
+    name: 'Blue',
+    palettes: [
+      { hex: '#eff6ff', number: 50 },
+      { hex: '#dbeafe', number: 100 },
+      { hex: '#bfdbfe', number: 200 },
+      { hex: '#93c5fd', number: 300 },
+      { hex: '#60a5fa', number: 400 },
+      { hex: '#3b82f6', number: 500 },
+      { hex: '#2563eb', number: 600 },
+      { hex: '#1d4ed8', number: 700 },
+      { hex: '#1e40af', number: 800 },
+      { hex: '#1e3a8a', number: 900 },
+      { hex: '#172554', number: 950 }
+    ]
+  },
+  {
+    name: 'Indigo',
+    palettes: [
+      { hex: '#eef2ff', number: 50 },
+      { hex: '#e0e7ff', number: 100 },
+      { hex: '#c7d2fe', number: 200 },
+      { hex: '#a5b4fc', number: 300 },
+      { hex: '#818cf8', number: 400 },
+      { hex: '#6366f1', number: 500 },
+      { hex: '#4f46e5', number: 600 },
+      { hex: '#4338ca', number: 700 },
+      { hex: '#3730a3', number: 800 },
+      { hex: '#312e81', number: 900 },
+      { hex: '#1e1b4b', number: 950 }
+    ]
+  },
+  {
+    name: 'Violet',
+    palettes: [
+      { hex: '#f5f3ff', number: 50 },
+      { hex: '#ede9fe', number: 100 },
+      { hex: '#ddd6fe', number: 200 },
+      { hex: '#c4b5fd', number: 300 },
+      { hex: '#a78bfa', number: 400 },
+      { hex: '#8b5cf6', number: 500 },
+      { hex: '#7c3aed', number: 600 },
+      { hex: '#6d28d9', number: 700 },
+      { hex: '#5b21b6', number: 800 },
+      { hex: '#4c1d95', number: 900 },
+      { hex: '#2e1065', number: 950 }
+    ]
+  },
+  {
+    name: 'Purple',
+    palettes: [
+      { hex: '#faf5ff', number: 50 },
+      { hex: '#f3e8ff', number: 100 },
+      { hex: '#e9d5ff', number: 200 },
+      { hex: '#d8b4fe', number: 300 },
+      { hex: '#c084fc', number: 400 },
+      { hex: '#a855f7', number: 500 },
+      { hex: '#9333ea', number: 600 },
+      { hex: '#7e22ce', number: 700 },
+      { hex: '#6b21a8', number: 800 },
+      { hex: '#581c87', number: 900 },
+      { hex: '#3b0764', number: 950 }
+    ]
+  },
+  {
+    name: 'Fuchsia',
+    palettes: [
+      { hex: '#fdf4ff', number: 50 },
+      { hex: '#fae8ff', number: 100 },
+      { hex: '#f5d0fe', number: 200 },
+      { hex: '#f0abfc', number: 300 },
+      { hex: '#e879f9', number: 400 },
+      { hex: '#d946ef', number: 500 },
+      { hex: '#c026d3', number: 600 },
+      { hex: '#a21caf', number: 700 },
+      { hex: '#86198f', number: 800 },
+      { hex: '#701a75', number: 900 },
+      { hex: '#4a044e', number: 950 }
+    ]
+  },
+  {
+    name: 'Pink',
+    palettes: [
+      { hex: '#fdf2f8', number: 50 },
+      { hex: '#fce7f3', number: 100 },
+      { hex: '#fbcfe8', number: 200 },
+      { hex: '#f9a8d4', number: 300 },
+      { hex: '#f472b6', number: 400 },
+      { hex: '#ec4899', number: 500 },
+      { hex: '#db2777', number: 600 },
+      { hex: '#be185d', number: 700 },
+      { hex: '#9d174d', number: 800 },
+      { hex: '#831843', number: 900 },
+      { hex: '#500724', number: 950 }
+    ]
+  },
+  {
+    name: 'Rose',
+    palettes: [
+      { hex: '#fff1f2', number: 50 },
+      { hex: '#ffe4e6', number: 100 },
+      { hex: '#fecdd3', number: 200 },
+      { hex: '#fda4af', number: 300 },
+      { hex: '#fb7185', number: 400 },
+      { hex: '#f43f5e', number: 500 },
+      { hex: '#e11d48', number: 600 },
+      { hex: '#be123c', number: 700 },
+      { hex: '#9f1239', number: 800 },
+      { hex: '#881337', number: 900 },
+      { hex: '#4c0519', number: 950 }
+    ]
+  }
+];

+ 7 - 0
packages/color/src/index.ts

@@ -0,0 +1,7 @@
+import { colorPalettes } from './constant';
+
+export * from './palette';
+export * from './shared';
+export { colorPalettes };
+
+export * from './types';

+ 176 - 0
packages/color/src/palette/antd.ts

@@ -0,0 +1,176 @@
+import type { AnyColor, HsvColor } from 'colord';
+import { getHex, getHsv, isValidColor, mixColor } from '../shared';
+import type { ColorIndex } from '../types';
+
+/** Hue step */
+const hueStep = 2;
+/** Saturation step, light color part */
+const saturationStep = 16;
+/** Saturation step, dark color part */
+const saturationStep2 = 5;
+/** Brightness step, light color part */
+const brightnessStep1 = 5;
+/** Brightness step, dark color part */
+const brightnessStep2 = 15;
+/** Light color count, main color up */
+const lightColorCount = 5;
+/** Dark color count, main color down */
+const darkColorCount = 4;
+
+/**
+ * Get AntD palette color by index
+ *
+ * @param color - Color
+ * @param index - The color index of color palette (the main color index is 6)
+ * @returns Hex color
+ */
+export function getAntDPaletteColorByIndex(color: AnyColor, index: ColorIndex): string {
+  if (!isValidColor(color)) {
+    throw new Error('invalid input color value');
+  }
+
+  if (index === 6) {
+    return getHex(color);
+  }
+
+  const isLight = index < 6;
+  const hsv = getHsv(color);
+  const i = isLight ? lightColorCount + 1 - index : index - lightColorCount - 1;
+
+  const newHsv: HsvColor = {
+    h: getHue(hsv, i, isLight),
+    s: getSaturation(hsv, i, isLight),
+    v: getValue(hsv, i, isLight)
+  };
+
+  return getHex(newHsv);
+}
+
+/** Map of dark color index and opacity */
+const darkColorMap = [
+  { index: 7, opacity: 0.15 },
+  { index: 6, opacity: 0.25 },
+  { index: 5, opacity: 0.3 },
+  { index: 5, opacity: 0.45 },
+  { index: 5, opacity: 0.65 },
+  { index: 5, opacity: 0.85 },
+  { index: 5, opacity: 0.9 },
+  { index: 4, opacity: 0.93 },
+  { index: 3, opacity: 0.95 },
+  { index: 2, opacity: 0.97 },
+  { index: 1, opacity: 0.98 }
+];
+
+/**
+ * Get AntD color palette
+ *
+ * @param color - Color
+ * @param darkTheme - Dark theme
+ * @param darkThemeMixColor - Dark theme mix color (default: #141414)
+ */
+export function getAntDColorPalette(color: AnyColor, darkTheme = false, darkThemeMixColor = '#141414'): string[] {
+  const indexes: ColorIndex[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
+
+  const patterns = indexes.map(index => getAntDPaletteColorByIndex(color, index));
+
+  if (darkTheme) {
+    const darkPatterns = darkColorMap.map(({ index, opacity }) => {
+      const darkColor = mixColor(darkThemeMixColor, patterns[index], opacity);
+
+      return darkColor;
+    });
+
+    return darkPatterns.map(item => getHex(item));
+  }
+
+  return patterns;
+}
+
+/**
+ * Get hue
+ *
+ * @param hsv - Hsv format color
+ * @param i - The relative distance from 6
+ * @param isLight - Is light color
+ */
+function getHue(hsv: HsvColor, i: number, isLight: boolean) {
+  let hue: number;
+
+  const hsvH = Math.round(hsv.h);
+
+  if (hsvH >= 60 && hsvH <= 240) {
+    hue = isLight ? hsvH - hueStep * i : hsvH + hueStep * i;
+  } else {
+    hue = isLight ? hsvH + hueStep * i : hsvH - hueStep * i;
+  }
+
+  if (hue < 0) {
+    hue += 360;
+  }
+
+  if (hue >= 360) {
+    hue -= 360;
+  }
+
+  return hue;
+}
+
+/**
+ * Get saturation
+ *
+ * @param hsv - Hsv format color
+ * @param i - The relative distance from 6
+ * @param isLight - Is light color
+ */
+function getSaturation(hsv: HsvColor, i: number, isLight: boolean) {
+  if (hsv.h === 0 && hsv.s === 0) {
+    return hsv.s;
+  }
+
+  let saturation: number;
+
+  if (isLight) {
+    saturation = hsv.s - saturationStep * i;
+  } else if (i === darkColorCount) {
+    saturation = hsv.s + saturationStep;
+  } else {
+    saturation = hsv.s + saturationStep2 * i;
+  }
+
+  if (saturation > 100) {
+    saturation = 100;
+  }
+
+  if (isLight && i === lightColorCount && saturation > 10) {
+    saturation = 10;
+  }
+
+  if (saturation < 6) {
+    saturation = 6;
+  }
+
+  return saturation;
+}
+
+/**
+ * Get value of hsv
+ *
+ * @param hsv - Hsv format color
+ * @param i - The relative distance from 6
+ * @param isLight - Is light color
+ */
+function getValue(hsv: HsvColor, i: number, isLight: boolean) {
+  let value: number;
+
+  if (isLight) {
+    value = hsv.v + brightnessStep1 * i;
+  } else {
+    value = hsv.v - brightnessStep2 * i;
+  }
+
+  if (value > 100) {
+    value = 100;
+  }
+
+  return value;
+}

+ 45 - 0
packages/color/src/palette/index.ts

@@ -0,0 +1,45 @@
+import type { AnyColor } from 'colord';
+import { getHex } from '../shared';
+import type { ColorPaletteNumber } from '../types';
+import { getRecommendedColorPalette } from './recommend';
+import { getAntDColorPalette } from './antd';
+
+/**
+ * get color palette by provided color
+ *
+ * @param color
+ * @param recommended whether to get recommended color palette (the provided color may not be the main color)
+ */
+export function getColorPalette(color: AnyColor, recommended = false) {
+  const colorMap = new Map<ColorPaletteNumber, string>();
+
+  if (recommended) {
+    const colorPalette = getRecommendedColorPalette(getHex(color));
+    colorPalette.palettes.forEach(palette => {
+      colorMap.set(palette.number, palette.hex);
+    });
+  } else {
+    const colors = getAntDColorPalette(color);
+
+    const colorNumbers: ColorPaletteNumber[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
+
+    colorNumbers.forEach((number, index) => {
+      colorMap.set(number, colors[index]);
+    });
+  }
+
+  return colorMap;
+}
+
+/**
+ * get color palette color by number
+ *
+ * @param color the provided color
+ * @param number the color palette number
+ * @param recommended whether to get recommended color palette (the provided color may not be the main color)
+ */
+export function getPaletteColorByNumber(color: AnyColor, number: ColorPaletteNumber, recommended = false) {
+  const colorMap = getColorPalette(color, recommended);
+
+  return colorMap.get(number as ColorPaletteNumber)!;
+}

+ 152 - 0
packages/color/src/palette/recommend.ts

@@ -0,0 +1,152 @@
+import { getColorName, getDeltaE, getHsl, isValidColor, transformHslToHex } from '../shared';
+import { colorPalettes } from '../constant';
+import type {
+  ColorPalette,
+  ColorPaletteFamily,
+  ColorPaletteFamilyWithNearestPalette,
+  ColorPaletteMatch,
+  ColorPaletteNumber
+} from '../types';
+
+/**
+ * get recommended color palette by provided color
+ *
+ * @param color the provided color
+ */
+export function getRecommendedColorPalette(color: string) {
+  const colorPaletteFamily = getRecommendedColorPaletteFamily(color);
+
+  const colorMap = new Map<ColorPaletteNumber, ColorPalette>();
+
+  colorPaletteFamily.palettes.forEach(palette => {
+    colorMap.set(palette.number, palette);
+  });
+
+  const mainColor = colorMap.get(500)!;
+  const matchColor = colorPaletteFamily.palettes.find(palette => palette.hex === color)!;
+
+  const colorPalette: ColorPaletteMatch = {
+    ...colorPaletteFamily,
+    colorMap,
+    main: mainColor,
+    match: matchColor
+  };
+
+  return colorPalette;
+}
+
+/**
+ * get recommended palette color by provided color
+ *
+ * @param color the provided color
+ * @param number the color palette number
+ */
+export function getRecommendedPaletteColorByNumber(color: string, number: ColorPaletteNumber) {
+  const colorPalette = getRecommendedColorPalette(color);
+
+  const { hex } = colorPalette.colorMap.get(number)!;
+
+  return hex;
+}
+
+/**
+ * get color palette family by provided color and color name
+ *
+ * @param color the provided color
+ */
+export function getRecommendedColorPaletteFamily(color: string) {
+  if (!isValidColor(color)) {
+    throw new Error('Invalid color, please check color value!');
+  }
+
+  let colorName = getColorName(color);
+
+  colorName = colorName.toLowerCase().replace(/\s/g, '-');
+
+  const { h: h1, s: s1 } = getHsl(color);
+
+  const { nearestLightnessPalette, palettes } = getNearestColorPaletteFamily(color, colorPalettes);
+
+  const { number, hex } = nearestLightnessPalette;
+
+  const { h: h2, s: s2 } = getHsl(hex);
+
+  const deltaH = h1 - h2;
+
+  const sRatio = s1 / s2;
+
+  const colorPaletteFamily: ColorPaletteFamily = {
+    name: colorName,
+    palettes: palettes.map(palette => {
+      let hexValue = color;
+
+      const isSame = number === palette.number;
+
+      if (!isSame) {
+        const { h: h3, s: s3, l } = getHsl(palette.hex);
+
+        const newH = deltaH < 0 ? h3 + deltaH : h3 - deltaH;
+        const newS = s3 * sRatio;
+
+        hexValue = transformHslToHex({
+          h: newH,
+          s: newS,
+          l
+        });
+      }
+
+      return {
+        hex: hexValue,
+        number: palette.number
+      };
+    })
+  };
+
+  return colorPaletteFamily;
+}
+
+/**
+ * get nearest color palette family
+ *
+ * @param color color
+ * @param families color palette families
+ */
+function getNearestColorPaletteFamily(color: string, families: ColorPaletteFamily[]) {
+  const familyWithConfig = families.map(family => {
+    const palettes = family.palettes.map(palette => {
+      return {
+        ...palette,
+        delta: getDeltaE(color, palette.hex)
+      };
+    });
+
+    const nearestPalette = palettes.reduce((prev, curr) => (prev.delta < curr.delta ? prev : curr));
+
+    return {
+      ...family,
+      palettes,
+      nearestPalette
+    };
+  });
+
+  const nearestPaletteFamily = familyWithConfig.reduce((prev, curr) =>
+    prev.nearestPalette.delta < curr.nearestPalette.delta ? prev : curr
+  );
+
+  const { l } = getHsl(color);
+
+  const paletteFamily: ColorPaletteFamilyWithNearestPalette = {
+    ...nearestPaletteFamily,
+    nearestLightnessPalette: nearestPaletteFamily.palettes.reduce((prev, curr) => {
+      const { l: prevLightness } = getHsl(prev.hex);
+      const { l: currLightness } = getHsl(curr.hex);
+
+      const deltaPrev = Math.abs(prevLightness - l);
+      const deltaCurr = Math.abs(currLightness - l);
+
+      return deltaPrev < deltaCurr ? prev : curr;
+    })
+  };
+
+  return paletteFamily;
+}

+ 93 - 0
packages/color/src/shared/colord.ts

@@ -0,0 +1,93 @@
+import { colord, extend } from 'colord';
+import namesPlugin from 'colord/plugins/names';
+import mixPlugin from 'colord/plugins/mix';
+import labPlugin from 'colord/plugins/lab';
+import type { AnyColor, HslColor, RgbColor } from 'colord';
+
+extend([namesPlugin, mixPlugin, labPlugin]);
+
+export function isValidColor(color: AnyColor) {
+  return colord(color).isValid();
+}
+
+export function getHex(color: AnyColor) {
+  return colord(color).toHex();
+}
+
+export function getRgb(color: AnyColor) {
+  return colord(color).toRgb();
+}
+
+export function getHsl(color: AnyColor) {
+  return colord(color).toHsl();
+}
+
+export function getHsv(color: AnyColor) {
+  return colord(color).toHsv();
+}
+
+export function getDeltaE(color1: AnyColor, color2: AnyColor) {
+  return colord(color1).delta(color2);
+}
+
+export function transformHslToHex(color: HslColor) {
+  return colord(color).toHex();
+}
+
+/**
+ * Add color alpha
+ *
+ * @param color - Color
+ * @param alpha - Alpha (0 - 1)
+ */
+export function addColorAlpha(color: AnyColor, alpha: number) {
+  return colord(color).alpha(alpha).toHex();
+}
+
+/**
+ * Mix color
+ *
+ * @param firstColor - First color
+ * @param secondColor - Second color
+ * @param ratio - The ratio of the second color (0 - 1)
+ */
+export function mixColor(firstColor: AnyColor, secondColor: AnyColor, ratio: number) {
+  return colord(firstColor).mix(secondColor, ratio).toHex();
+}
+
+/**
+ * Transform color with opacity to similar color without opacity
+ *
+ * @param color - Color
+ * @param alpha - Alpha (0 - 1)
+ * @param bgColor Background color (usually white or black)
+ */
+export function transformColorWithOpacity(color: AnyColor, alpha: number, bgColor = '#ffffff') {
+  const originColor = addColorAlpha(color, alpha);
+  const { r: oR, g: oG, b: oB } = colord(originColor).toRgb();
+
+  const { r: bgR, g: bgG, b: bgB } = colord(bgColor).toRgb();
+
+  function calRgb(or: number, bg: number, al: number) {
+    return bg + (or - bg) * al;
+  }
+
+  const resultRgb: RgbColor = {
+    r: calRgb(oR, bgR, alpha),
+    g: calRgb(oG, bgG, alpha),
+    b: calRgb(oB, bgB, alpha)
+  };
+
+  return colord(resultRgb).toHex();
+}
+
+/**
+ * Is white color
+ *
+ * @param color - Color
+ */
+export function isWhiteColor(color: AnyColor) {
+  return colord(color).isEqual('#ffffff');
+}
+
+export { colord };

+ 2 - 0
packages/color/src/shared/index.ts

@@ -0,0 +1,2 @@
+export * from './colord';
+export * from './name';

+ 49 - 0
packages/color/src/shared/name.ts

@@ -0,0 +1,49 @@
+import { colorNames } from '../constant';
+import { getHex, getHsl, getRgb } from './colord';
+
+/**
+ * Get color name
+ *
+ * @param color
+ */
+export function getColorName(color: string) {
+  const hex = getHex(color);
+  const rgb = getRgb(color);
+  const hsl = getHsl(color);
+
+  let ndf = 0;
+  let ndf1 = 0;
+  let ndf2 = 0;
+  let cl = -1;
+  let df = -1;
+
+  let name = '';
+
+  colorNames.some((item, index) => {
+    const [hexValue, colorName] = item;
+
+    const match = hex === hexValue;
+
+    if (match) {
+      name = colorName;
+    } else {
+      const { r, g, b } = getRgb(hexValue);
+      const { h, s, l } = getHsl(hexValue);
+
+      ndf1 = (rgb.r - r) ** 2 + (rgb.g - g) ** 2 + (rgb.b - b) ** 2;
+      ndf2 = (hsl.h - h) ** 2 + (hsl.s - s) ** 2 + (hsl.l - l) ** 2;
+
+      ndf = ndf1 + ndf2 * 2;
+      if (df < 0 || df > ndf) {
+        df = ndf;
+        cl = index;
+      }
+    }
+
+    return match;
+  });
+
+  name = colorNames[cl][1];
+
+  return name;
+}

+ 58 - 0
packages/color/src/types/index.ts

@@ -0,0 +1,58 @@
+/**
+ * the color palette number
+ *
+ * the main color number is 500
+ */
+export type ColorPaletteNumber = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
+
+/** the color palette */
+export type ColorPalette = {
+  /** the color hex value */
+  hex: string;
+  /**
+   * the color number
+   *
+   * - 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950
+   */
+  number: ColorPaletteNumber;
+};
+
+/** the color palette family */
+export type ColorPaletteFamily = {
+  /** the color palette family name */
+  name: string;
+  /** the color palettes */
+  palettes: ColorPalette[];
+};
+
+/** the color palette with delta */
+export type ColorPaletteWithDelta = ColorPalette & {
+  delta: number;
+};
+
+/** the color palette family with nearest palette */
+export type ColorPaletteFamilyWithNearestPalette = ColorPaletteFamily & {
+  nearestPalette: ColorPaletteWithDelta;
+  nearestLightnessPalette: ColorPaletteWithDelta;
+};
+
+/** the color palette match */
+export type ColorPaletteMatch = ColorPaletteFamily & {
+  /** the color map of the palette */
+  colorMap: Map<ColorPaletteNumber, ColorPalette>;
+  /**
+   * the main color of the palette
+   *
+   * which number is 500
+   */
+  main: ColorPalette;
+  /** the match color of the palette */
+  match: ColorPalette;
+};
+
+/**
+ * The color index of color palette
+ *
+ * From left to right, the color is from light to dark, 6 is main color
+ */
+export type ColorIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;

+ 20 - 0
packages/color/tsconfig.json

@@ -0,0 +1,20 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "jsx": "preserve",
+    "lib": ["DOM", "ESNext"],
+    "baseUrl": ".",
+    "module": "ESNext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "types": ["node"],
+    "strict": true,
+    "strictNullChecks": true,
+    "noUnusedLocals": true,
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+    "forceConsistentCasingInFileNames": true
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules", "dist"]
+}

+ 16 - 0
packages/hooks/package.json

@@ -0,0 +1,16 @@
+{
+  "name": "@sa/hooks",
+  "version": "1.3.15",
+  "exports": {
+    ".": "./src/index.ts"
+  },
+  "typesVersions": {
+    "*": {
+      "*": ["./src/*"]
+    }
+  },
+  "dependencies": {
+    "@sa/axios": "workspace:*",
+    "@sa/utils": "workspace:*"
+  }
+}

+ 9 - 0
packages/hooks/src/index.ts

@@ -0,0 +1,9 @@
+import useBoolean from './use-boolean';
+import useLoading from './use-loading';
+import useCountDown from './use-count-down';
+import useContext from './use-context';
+import useSvgIconRender from './use-svg-icon-render';
+import useTable from './use-table';
+
+export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useTable };
+export type * from './use-table';

+ 31 - 0
packages/hooks/src/use-boolean.ts

@@ -0,0 +1,31 @@
+import { ref } from 'vue';
+
+/**
+ * Boolean
+ *
+ * @param initValue Init value
+ */
+export default function useBoolean(initValue = false) {
+  const bool = ref(initValue);
+
+  function setBool(value: boolean) {
+    bool.value = value;
+  }
+  function setTrue() {
+    setBool(true);
+  }
+  function setFalse() {
+    setBool(false);
+  }
+  function toggle() {
+    setBool(!bool.value);
+  }
+
+  return {
+    bool,
+    setBool,
+    setTrue,
+    setFalse,
+    toggle
+  };
+}

+ 96 - 0
packages/hooks/src/use-context.ts

@@ -0,0 +1,96 @@
+import { inject, provide } from 'vue';
+
+/**
+ * Use context
+ *
+ * @example
+ *   ```ts
+ *   // there are three vue files: A.vue, B.vue, C.vue, and A.vue is the parent component of B.vue and C.vue
+ *
+ *   // context.ts
+ *   import { ref } from 'vue';
+ *   import { useContext } from '@sa/hooks';
+ *
+ *   export const [provideDemoContext, useDemoContext] = useContext('demo', () => {
+ *     const count = ref(0);
+ *
+ *     function increment() {
+ *       count.value++;
+ *     }
+ *
+ *     function decrement() {
+ *       count.value--;
+ *     }
+ *
+ *     return {
+ *       count,
+ *       increment,
+ *       decrement
+ *     };
+ *   })
+ *   ``` // A.vue
+ *   ```vue
+ *   <template>
+ *     <div>A</div>
+ *   </template>
+ *   <script setup lang="ts">
+ *   import { provideDemoContext } from './context';
+ *
+ *   provideDemoContext();
+ *   // const { increment } = provideDemoContext(); // also can control the store in the parent component
+ *   </script>
+ *   ``` // B.vue
+ *   ```vue
+ *   <template>
+ *    <div>B</div>
+ *   </template>
+ *   <script setup lang="ts">
+ *   import { useDemoContext } from './context';
+ *
+ *   const { count, increment } = useDemoContext();
+ *   </script>
+ *   ```;
+ *
+ *   // C.vue is same as B.vue
+ *
+ * @param contextName Context name
+ * @param fn Context function
+ */
+export default function useContext<Arguments extends Array<any>, T>(
+  contextName: string,
+  composable: (...args: Arguments) => T
+) {
+  const key = Symbol(contextName);
+
+  /**
+   * Injects the context value.
+   *
+   * @param consumerName - The name of the component that is consuming the context. If provided, the component must be
+   *   used within the context provider.
+   * @param defaultValue - The default value to return if the context is not provided.
+   * @returns The context value.
+   */
+  const useInject = <N extends string | null | undefined = undefined>(
+    consumerName?: N,
+    defaultValue?: T
+  ): N extends null | undefined ? T | null : T => {
+    const value = inject(key, defaultValue);
+
+    if (consumerName && !value) {
+      throw new Error(`\`${consumerName}\` must be used within \`${contextName}\``);
+    }
+
+    // @ts-expect-error - we want to return null if the value is undefined or null
+    return value || null;
+  };
+
+  const useProvide = (...args: Arguments) => {
+    const value = composable(...args);
+
+    provide(key, value);
+
+    return value;
+  };
+
+  return [useProvide, useInject] as const;
+}

+ 68 - 0
packages/hooks/src/use-count-down.ts

@@ -0,0 +1,68 @@
+import { computed, onScopeDispose, ref } from 'vue';
+import { useRafFn } from '@vueuse/core';
+
+/**
+ * A hook for implementing a countdown timer. It uses `requestAnimationFrame` for smooth and accurate timing,
+ * independent of the screen refresh rate.
+ *
+ * @param initialSeconds - The total number of seconds for the countdown.
+ */
+export default function useCountDown(initialSeconds: number) {
+  const remainingSeconds = ref(0);
+
+  const count = computed(() => Math.ceil(remainingSeconds.value));
+
+  const isCounting = computed(() => remainingSeconds.value > 0);
+
+  const { pause, resume } = useRafFn(
+    ({ delta }) => {
+      // delta: milliseconds elapsed since the last frame.
+
+      // If countdown already reached zero or below, ensure it's 0 and stop.
+      if (remainingSeconds.value <= 0) {
+        remainingSeconds.value = 0;
+        pause();
+        return;
+      }
+
+      // Calculate seconds passed since the last frame.
+      const secondsPassed = delta / 1000;
+      remainingSeconds.value -= secondsPassed;
+
+      // If countdown has finished after decrementing.
+      if (remainingSeconds.value <= 0) {
+        remainingSeconds.value = 0;
+        pause();
+      }
+    },
+    { immediate: false } // The timer does not start automatically.
+  );
+
+  /**
+   * Starts the countdown.
+   *
+   * @param [updatedSeconds=initialSeconds] - Optionally, start with a new duration. Default is `initialSeconds`
+   */
+  function start(updatedSeconds: number = initialSeconds) {
+    remainingSeconds.value = updatedSeconds;
+    resume();
+  }
+
+  /** Stops the countdown and resets the remaining time to 0. */
+  function stop() {
+    remainingSeconds.value = 0;
+    pause();
+  }
+
+  // Ensure the rAF loop is cleaned up when the component is unmounted.
+  onScopeDispose(() => {
+    pause();
+  });
+
+  return {
+    count,
+    isCounting,
+    start,
+    stop
+  };
+}

+ 16 - 0
packages/hooks/src/use-loading.ts

@@ -0,0 +1,16 @@
+import useBoolean from './use-boolean';
+
+/**
+ * Loading
+ *
+ * @param initValue Init value
+ */
+export default function useLoading(initValue = false) {
+  const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initValue);
+
+  return {
+    loading,
+    startLoading,
+    endLoading
+  };
+}

+ 79 - 0
packages/hooks/src/use-request.ts

@@ -0,0 +1,79 @@
+import { ref } from 'vue';
+import type { Ref } from 'vue';
+import { createFlatRequest } from '@sa/axios';
+import type {
+  AxiosError,
+  CreateAxiosDefaults,
+  CustomAxiosRequestConfig,
+  MappedType,
+  RequestInstanceCommon,
+  RequestOption,
+  ResponseType
+} from '@sa/axios';
+import useLoading from './use-loading';
+
+export type HookRequestInstanceResponseSuccessData<ApiData> = {
+  data: Ref<ApiData>;
+  error: Ref<null>;
+};
+
+export type HookRequestInstanceResponseFailData<ResponseData> = {
+  data: Ref<null>;
+  error: Ref<AxiosError<ResponseData>>;
+};
+
+export type HookRequestInstanceResponseData<ResponseData, ApiData> = {
+  loading: Ref<boolean>;
+} & (HookRequestInstanceResponseSuccessData<ApiData> | HookRequestInstanceResponseFailData<ResponseData>);
+
+export interface HookRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
+  extends RequestInstanceCommon<State> {
+  <T extends ApiData = ApiData, R extends ResponseType = 'json'>(
+    config: CustomAxiosRequestConfig
+  ): HookRequestInstanceResponseData<ResponseData, MappedType<R, T>>;
+}
+
+/**
+ * create a hook request instance
+ *
+ * @param axiosConfig
+ * @param options
+ */
+export default function createHookRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
+  axiosConfig?: CreateAxiosDefaults,
+  options?: Partial<RequestOption<ResponseData, ApiData, State>>
+) {
+  const request = createFlatRequest<ResponseData, ApiData, State>(axiosConfig, options);
+
+  const hookRequest: HookRequestInstance<ResponseData, ApiData, State> = function hookRequest<
+    T extends ApiData = ApiData,
+    R extends ResponseType = 'json'
+  >(config: CustomAxiosRequestConfig) {
+    const { loading, startLoading, endLoading } = useLoading();
+
+    const data = ref(null) as Ref<MappedType<R, T>>;
+    const error = ref(null) as Ref<AxiosError<ResponseData> | null>;
+
+    startLoading();
+
+    request(config).then(res => {
+      if (res.data) {
+        data.value = res.data as MappedType<R, T>;
+      } else {
+        error.value = res.error;
+      }
+
+      endLoading();
+    });
+
+    return {
+      loading,
+      data,
+      error
+    };
+  } as HookRequestInstance<ResponseData, ApiData, State>;
+
+  hookRequest.cancelAllRequest = request.cancelAllRequest;
+
+  return hookRequest;
+}

+ 144 - 0
packages/hooks/src/use-signal.ts

@@ -0,0 +1,144 @@
+import { computed, ref, shallowRef, triggerRef } from 'vue';
+import type {
+  ComputedGetter,
+  DebuggerOptions,
+  Ref,
+  ShallowRef,
+  WritableComputedOptions,
+  WritableComputedRef
+} from 'vue';
+
+type Updater<T> = (value: T) => T;
+type Mutator<T> = (value: T) => void;
+
+/**
+ * Signal is a reactive value that can be set, updated or mutated
+ *
+ * @example
+ *   ```ts
+ *   const count = useSignal(0);
+ *
+ *   // `watchEffect`
+ *   watchEffect(() => {
+ *   console.log(count());
+ *   });
+ *
+ *   // watch
+ *   watch(count, value => {
+ *   console.log(value);
+ *   });
+ *
+ *   // useComputed
+ *   const double = useComputed(() => count() * 2);
+ *   const writeableDouble = useComputed({
+ *   get: () => count() * 2,
+ *   set: value => count.set(value / 2)
+ *   });
+ *   ```
+ */
+export interface Signal<T> {
+  (): Readonly<T>;
+  /**
+   * Set the value of the signal
+   *
+   * It recommend use `set` for primitive values
+   *
+   * @param value
+   */
+  set(value: T): void;
+  /**
+   * Update the value of the signal using an updater function
+   *
+   * It recommend use `update` for non-primitive values, only the first level of the object will be reactive.
+   *
+   * @param updater
+   */
+  update(updater: Updater<T>): void;
+  /**
+   * Mutate the value of the signal using a mutator function
+   *
+   * this action will call `triggerRef`, so the value will be tracked on `watchEffect`.
+   *
+   * It recommend use `mutate` for non-primitive values, all levels of the object will be reactive.
+   *
+   * @param mutator
+   */
+  mutate(mutator: Mutator<T>): void;
+  /**
+   * Get the reference of the signal
+   *
+   * Sometimes it can be useful to make `v-model` work with the signal
+   *
+   * ```vue
+   * <template>
+   *   <input v-model="model.count" />
+   * </template>;
+   *
+   * <script setup lang="ts">
+   *  const state = useSignal({ count: 0 }, { useRef: true });
+   *
+   *  const model = state.getRef();
+   * </script>
+   * ```
+   */
+  getRef(): Readonly<ShallowRef<Readonly<T>>>;
+}
+
+export interface ReadonlySignal<T> {
+  (): Readonly<T>;
+}
+
+export interface SignalOptions {
+  /**
+   * Whether to use `ref` to store the value
+   *
+   * @default false use `sharedRef` to store the value
+   */
+  useRef?: boolean;
+}
+
+export function useSignal<T>(initialValue: T, options?: SignalOptions): Signal<T> {
+  const { useRef } = options || {};
+
+  const state = useRef ? (ref(initialValue) as Ref<T>) : shallowRef(initialValue);
+
+  return createSignal(state);
+}
+
+export function useComputed<T>(getter: ComputedGetter<T>, debugOptions?: DebuggerOptions): ReadonlySignal<T>;
+export function useComputed<T>(options: WritableComputedOptions<T>, debugOptions?: DebuggerOptions): Signal<T>;
+export function useComputed<T>(
+  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
+  debugOptions?: DebuggerOptions
+) {
+  const isGetter = typeof getterOrOptions === 'function';
+
+  const computedValue = computed(getterOrOptions as any, debugOptions);
+
+  if (isGetter) {
+    return () => computedValue.value as ReadonlySignal<T>;
+  }
+
+  return createSignal(computedValue);
+}
+
+function createSignal<T>(state: ShallowRef<T> | WritableComputedRef<T>): Signal<T> {
+  const signal = () => state.value;
+
+  signal.set = (value: T) => {
+    state.value = value;
+  };
+
+  signal.update = (updater: Updater<T>) => {
+    state.value = updater(state.value);
+  };
+
+  signal.mutate = (mutator: Mutator<T>) => {
+    mutator(state.value);
+    triggerRef(state);
+  };
+
+  signal.getRef = () => state as Readonly<ShallowRef<Readonly<T>>>;
+
+  return signal;
+}

+ 50 - 0
packages/hooks/src/use-svg-icon-render.ts

@@ -0,0 +1,50 @@
+import { h } from 'vue';
+import type { Component } from 'vue';
+
+/**
+ * Svg icon render hook
+ *
+ * @param SvgIcon Svg icon component
+ */
+export default function useSvgIconRender(SvgIcon: Component) {
+  interface IconConfig {
+    /** Iconify icon name */
+    icon?: string;
+    /** Local icon name */
+    localIcon?: string;
+    /** Icon color */
+    color?: string;
+    /** Icon size */
+    fontSize?: number;
+  }
+
+  type IconStyle = Partial<Pick<CSSStyleDeclaration, 'color' | 'fontSize'>>;
+
+  /**
+   * Svg icon VNode
+   *
+   * @param config
+   */
+  const SvgIconVNode = (config: IconConfig) => {
+    const { color, fontSize, icon, localIcon } = config;
+
+    const style: IconStyle = {};
+
+    if (color) {
+      style.color = color;
+    }
+    if (fontSize) {
+      style.fontSize = `${fontSize}px`;
+    }
+
+    if (!icon && !localIcon) {
+      return undefined;
+    }
+
+    return () => h(SvgIcon, { icon, localIcon, style });
+  };
+
+  return {
+    SvgIconVNode
+  };
+}

+ 132 - 0
packages/hooks/src/use-table.ts

@@ -0,0 +1,132 @@
+import { computed, ref } from 'vue';
+import type { Ref, VNodeChild } from 'vue';
+import useBoolean from './use-boolean';
+import useLoading from './use-loading';
+
+export interface PaginationData<T> {
+  data: T[];
+  pageNum: number;
+  pageSize: number;
+  total: number;
+}
+
+type GetApiData<ApiData, Pagination extends boolean> = Pagination extends true ? PaginationData<ApiData> : ApiData[];
+
+type Transform<ResponseData, ApiData, Pagination extends boolean> = (
+  response: ResponseData
+) => GetApiData<ApiData, Pagination>;
+
+export type TableColumnCheckTitle = string | ((...args: any) => VNodeChild);
+
+export type TableColumnCheck = {
+  key: string;
+  title: TableColumnCheckTitle;
+  checked: boolean;
+  visible: boolean;
+};
+
+export interface UseTableOptions<ResponseData, ApiData, Column, Pagination extends boolean> {
+  /**
+   * api function to get table data
+   */
+  api: () => Promise<ResponseData>;
+  /**
+   * whether to enable pagination
+   */
+  pagination?: Pagination;
+  /**
+   * transform api response to table data
+   */
+  transform: Transform<ResponseData, ApiData, Pagination>;
+  /**
+   * columns factory
+   */
+  columns: () => Column[];
+  /**
+   * get column checks
+   */
+  getColumnChecks: (columns: Column[]) => TableColumnCheck[];
+  /**
+   * get columns
+   */
+  getColumns: (columns: Column[], checks: TableColumnCheck[]) => Column[];
+  /**
+   * callback when response fetched
+   */
+  onFetched?: (data: GetApiData<ApiData, Pagination>) => void | Promise<void>;
+  /**
+   * whether to get data immediately
+   *
+   * @default true
+   */
+  immediate?: boolean;
+}
+
+export default function useTable<ResponseData, ApiData, Column, Pagination extends boolean>(
+  options: UseTableOptions<ResponseData, ApiData, Column, Pagination>
+) {
+  const { loading, startLoading, endLoading } = useLoading();
+  const { bool: empty, setBool: setEmpty } = useBoolean();
+
+  const { api, pagination, transform, columns, getColumnChecks, getColumns, onFetched, immediate = true } = options;
+
+  const data = ref([]) as Ref<ApiData[]>;
+
+  const columnChecks = ref(getColumnChecks(columns())) as Ref<TableColumnCheck[]>;
+
+  const $columns = computed(() => getColumns(columns(), columnChecks.value));
+
+  function reloadColumns() {
+    const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
+
+    const defaultChecks = getColumnChecks(columns());
+
+    columnChecks.value = defaultChecks.map(col => ({
+      ...col,
+      checked: checkMap.get(col.key) ?? col.checked
+    }));
+  }
+
+  async function getData() {
+    try {
+      startLoading();
+
+      const response = await api();
+
+      const transformed = transform(response);
+
+      data.value = getTableData(transformed, pagination);
+
+      setEmpty(data.value.length === 0);
+
+      await onFetched?.(transformed);
+    } finally {
+      endLoading();
+    }
+  }
+
+  if (immediate) {
+    getData();
+  }
+
+  return {
+    loading,
+    empty,
+    data,
+    columns: $columns,
+    columnChecks,
+    reloadColumns,
+    getData
+  };
+}
+
+function getTableData<ApiData, Pagination extends boolean>(
+  data: GetApiData<ApiData, Pagination>,
+  pagination?: Pagination
+) {
+  if (pagination) {
+    return (data as PaginationData<ApiData>).data;
+  }
+
+  return data as ApiData[];
+}

+ 20 - 0
packages/hooks/tsconfig.json

@@ -0,0 +1,20 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "jsx": "preserve",
+    "lib": ["DOM", "ESNext"],
+    "baseUrl": ".",
+    "module": "ESNext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "types": ["node"],
+    "strict": true,
+    "strictNullChecks": true,
+    "noUnusedLocals": true,
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+    "forceConsistentCasingInFileNames": true
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules", "dist"]
+}

+ 19 - 0
packages/materials/package.json

@@ -0,0 +1,19 @@
+{
+  "name": "@sa/materials",
+  "version": "1.3.15",
+  "exports": {
+    ".": "./src/index.ts"
+  },
+  "typesVersions": {
+    "*": {
+      "*": ["./src/*"]
+    }
+  },
+  "dependencies": {
+    "@sa/utils": "workspace:*",
+    "simplebar-vue": "2.4.2"
+  },
+  "devDependencies": {
+    "typed-css-modules": "0.9.1"
+  }
+}

+ 6 - 0
packages/materials/src/index.ts

@@ -0,0 +1,6 @@
+import AdminLayout, { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './libs/admin-layout';
+import PageTab from './libs/page-tab';
+import SimpleScrollbar from './libs/simple-scrollbar';
+
+export { AdminLayout, LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX, PageTab, SimpleScrollbar };
+export * from './types';

+ 63 - 0
packages/materials/src/libs/admin-layout/index.module.css

@@ -0,0 +1,63 @@
+/* @type */
+
+.layout-header,
+.layout-header-placement {
+  height: var(--soy-header-height);
+}
+
+.layout-header {
+  z-index: var(--soy-header-z-index);
+}
+
+.layout-tab {
+  top: var(--soy-header-height);
+  height: var(--soy-tab-height);
+  z-index: var(--soy-tab-z-index);
+}
+
+.layout-tab-placement {
+  height: var(--soy-tab-height);
+}
+
+.layout-sider {
+  width: var(--soy-sider-width);
+  z-index: var(--soy-sider-z-index);
+}
+
+.layout-mobile-sider {
+  z-index: var(--soy-sider-z-index);
+}
+
+.layout-mobile-sider-mask {
+  z-index: var(--soy-mobile-sider-z-index);
+}
+
+.layout-sider_collapsed {
+  width: var(--soy-sider-collapsed-width);
+  z-index: var(--soy-sider-z-index);
+}
+
+.layout-footer,
+.layout-footer-placement {
+  height: var(--soy-footer-height);
+}
+
+.layout-footer {
+  z-index: var(--soy-footer-z-index);
+}
+
+.left-gap {
+  padding-left: var(--soy-sider-width);
+}
+
+.left-gap_collapsed {
+  padding-left: var(--soy-sider-collapsed-width);
+}
+
+.sider-padding-top {
+  padding-top: var(--soy-header-height);
+}
+
+.sider-padding-bottom {
+  padding-bottom: var(--soy-footer-height);
+}

+ 18 - 0
packages/materials/src/libs/admin-layout/index.module.css.d.ts

@@ -0,0 +1,18 @@
+declare const styles: {
+  readonly 'layout-header': string;
+  readonly 'layout-header-placement': string;
+  readonly 'layout-tab': string;
+  readonly 'layout-tab-placement': string;
+  readonly 'layout-sider': string;
+  readonly 'layout-mobile-sider': string;
+  readonly 'layout-mobile-sider-mask': string;
+  readonly 'layout-sider_collapsed': string;
+  readonly 'layout-footer': string;
+  readonly 'layout-footer-placement': string;
+  readonly 'left-gap': string;
+  readonly 'left-gap_collapsed': string;
+  readonly 'sider-padding-top': string;
+  readonly 'sider-padding-bottom': string;
+};
+
+export default styles;

+ 5 - 0
packages/materials/src/libs/admin-layout/index.ts

@@ -0,0 +1,5 @@
+import AdminLayout from './index.vue';
+import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './shared';
+
+export default AdminLayout;
+export { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX };

+ 236 - 0
packages/materials/src/libs/admin-layout/index.vue

@@ -0,0 +1,236 @@
+<script setup lang="ts">
+import { computed } from 'vue';
+import type { AdminLayoutProps } from '../../types';
+import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID, createLayoutCssVars } from './shared';
+import style from './index.module.css';
+
+defineOptions({
+  name: 'AdminLayout'
+});
+
+const props = withDefaults(defineProps<AdminLayoutProps>(), {
+  mode: 'vertical',
+  scrollMode: 'content',
+  scrollElId: LAYOUT_SCROLL_EL_ID,
+  commonClass: 'transition-all-300',
+  fixedTop: true,
+  maxZIndex: LAYOUT_MAX_Z_INDEX,
+  headerVisible: true,
+  headerHeight: 56,
+  tabVisible: true,
+  tabHeight: 48,
+  siderVisible: true,
+  siderCollapse: false,
+  siderWidth: 220,
+  siderCollapsedWidth: 64,
+  footerVisible: true,
+  footerHeight: 48,
+  rightFooter: false
+});
+
+interface Emits {
+  /** Update siderCollapse */
+  (e: 'update:siderCollapse', collapse: boolean): void;
+}
+
+const emit = defineEmits<Emits>();
+
+type SlotFn = (props?: Record<string, unknown>) => any;
+
+type Slots = {
+  /** Main */
+  default?: SlotFn;
+  /** Header */
+  header?: SlotFn;
+  /** Tab */
+  tab?: SlotFn;
+  /** Sider */
+  sider?: SlotFn;
+  /** Footer */
+  footer?: SlotFn;
+};
+
+const slots = defineSlots<Slots>();
+
+const cssVars = computed(() => createLayoutCssVars(props));
+
+// config visible
+const showHeader = computed(() => Boolean(slots.header) && props.headerVisible);
+const showTab = computed(() => Boolean(slots.tab) && props.tabVisible);
+const showSider = computed(() => !props.isMobile && Boolean(slots.sider) && props.siderVisible);
+const showMobileSider = computed(() => props.isMobile && Boolean(slots.sider) && props.siderVisible);
+const showFooter = computed(() => Boolean(slots.footer) && props.footerVisible);
+
+// scroll mode
+const isWrapperScroll = computed(() => props.scrollMode === 'wrapper');
+const isContentScroll = computed(() => props.scrollMode === 'content');
+
+// layout direction
+const isVertical = computed(() => props.mode === 'vertical');
+const isHorizontal = computed(() => props.mode === 'horizontal');
+
+const fixedHeaderAndTab = computed(() => props.fixedTop || (isHorizontal.value && isWrapperScroll.value));
+
+// css
+const leftGapClass = computed(() => {
+  if (!props.fullContent && showSider.value) {
+    return props.siderCollapse ? style['left-gap_collapsed'] : style['left-gap'];
+  }
+
+  return '';
+});
+
+const headerLeftGapClass = computed(() => (isVertical.value ? leftGapClass.value : ''));
+
+const footerLeftGapClass = computed(() => {
+  const condition1 = isVertical.value;
+  const condition2 = isHorizontal.value && isWrapperScroll.value && !props.fixedFooter;
+  const condition3 = Boolean(isHorizontal.value && props.rightFooter);
+
+  if (condition1 || condition2 || condition3) {
+    return leftGapClass.value;
+  }
+
+  return '';
+});
+
+const siderPaddingClass = computed(() => {
+  let cls = '';
+
+  if (showHeader.value && !headerLeftGapClass.value) {
+    cls += style['sider-padding-top'];
+  }
+  if (showFooter.value && !footerLeftGapClass.value) {
+    cls += ` ${style['sider-padding-bottom']}`;
+  }
+
+  return cls;
+});
+
+function handleClickMask() {
+  emit('update:siderCollapse', true);
+}
+</script>
+
+<template>
+  <div class="relative h-full" :class="[commonClass]" :style="cssVars">
+    <div
+      :id="isWrapperScroll ? scrollElId : undefined"
+      class="h-full flex flex-col"
+      :class="[commonClass, scrollWrapperClass, { 'overflow-y-auto': isWrapperScroll }]"
+    >
+      <!-- Header -->
+      <template v-if="showHeader">
+        <header
+          v-show="!fullContent"
+          class="flex-shrink-0"
+          :class="[
+            style['layout-header'],
+            commonClass,
+            headerLeftGapClass,
+            { 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
+          ]"
+        >
+          <slot name="header"></slot>
+        </header>
+        <div
+          v-show="!fullContent && fixedHeaderAndTab"
+          class="flex-shrink-0 overflow-hidden"
+          :class="[style['layout-header-placement']]"
+        ></div>
+      </template>
+
+      <!-- Tab -->
+      <template v-if="showTab">
+        <div
+          class="flex-shrink-0"
+          :class="[
+            style['layout-tab'],
+            commonClass,
+            tabClass,
+            { 'top-0!': fullContent || !showHeader },
+            leftGapClass,
+            { 'absolute left-0 w-full': fixedHeaderAndTab }
+          ]"
+        >
+          <slot name="tab"></slot>
+        </div>
+        <div
+          v-show="fullContent || fixedHeaderAndTab"
+          class="flex-shrink-0 overflow-hidden"
+          :class="[style['layout-tab-placement']]"
+        ></div>
+      </template>
+
+      <!-- Sider -->
+      <template v-if="showSider">
+        <aside
+          v-show="!fullContent"
+          class="absolute left-0 top-0 h-full"
+          :class="[
+            commonClass,
+            siderClass,
+            siderPaddingClass,
+            siderCollapse ? style['layout-sider_collapsed'] : style['layout-sider']
+          ]"
+        >
+          <slot name="sider"></slot>
+        </aside>
+      </template>
+
+      <!-- Mobile Sider -->
+      <template v-if="showMobileSider">
+        <aside
+          class="absolute left-0 top-0 h-full w-0 bg-white"
+          :class="[
+            commonClass,
+            mobileSiderClass,
+            style['layout-mobile-sider'],
+            siderCollapse ? 'overflow-hidden' : style['layout-sider']
+          ]"
+        >
+          <slot name="sider"></slot>
+        </aside>
+        <div
+          v-show="!siderCollapse"
+          class="absolute left-0 top-0 h-full w-full bg-[rgba(0,0,0,0.2)]"
+          :class="[style['layout-mobile-sider-mask']]"
+          @click="handleClickMask"
+        ></div>
+      </template>
+
+      <!-- Main Content -->
+      <main
+        :id="isContentScroll ? scrollElId : undefined"
+        class="flex flex-col flex-grow"
+        :class="[commonClass, contentClass, leftGapClass, { 'overflow-y-auto': isContentScroll }]"
+      >
+        <slot></slot>
+      </main>
+
+      <!-- Footer -->
+      <template v-if="showFooter">
+        <footer
+          v-show="!fullContent"
+          class="flex-shrink-0"
+          :class="[
+            style['layout-footer'],
+            commonClass,
+            footerClass,
+            footerLeftGapClass,
+            { 'absolute left-0 bottom-0 w-full': fixedFooter }
+          ]"
+        >
+          <slot name="footer"></slot>
+        </footer>
+        <div
+          v-show="!fullContent && fixedFooter"
+          class="flex-shrink-0 overflow-hidden"
+          :class="[style['layout-footer-placement']]"
+        ></div>
+      </template>
+    </div>
+  </div>
+</template>
+
+<style scoped></style>

+ 68 - 0
packages/materials/src/libs/admin-layout/shared.ts

@@ -0,0 +1,68 @@
+import type { AdminLayoutProps, LayoutCssVars, LayoutCssVarsProps } from '../../types';
+
+/** The id of the scroll element of the layout */
+export const LAYOUT_SCROLL_EL_ID = '__SCROLL_EL_ID__';
+
+/** The max z-index of the layout */
+export const LAYOUT_MAX_Z_INDEX = 100;
+
+/**
+ * Create layout css vars by css vars props
+ *
+ * @param props Css vars props
+ */
+function createLayoutCssVarsByCssVarsProps(props: LayoutCssVarsProps) {
+  const cssVars: LayoutCssVars = {
+    '--soy-header-height': `${props.headerHeight}px`,
+    '--soy-header-z-index': props.headerZIndex,
+    '--soy-tab-height': `${props.tabHeight}px`,
+    '--soy-tab-z-index': props.tabZIndex,
+    '--soy-sider-width': `${props.siderWidth}px`,
+    '--soy-sider-collapsed-width': `${props.siderCollapsedWidth}px`,
+    '--soy-sider-z-index': props.siderZIndex,
+    '--soy-mobile-sider-z-index': props.mobileSiderZIndex,
+    '--soy-footer-height': `${props.footerHeight}px`,
+    '--soy-footer-z-index': props.footerZIndex
+  };
+
+  return cssVars;
+}
+
+/**
+ * Create layout css vars
+ *
+ * @param props
+ */
+export function createLayoutCssVars(props: AdminLayoutProps) {
+  const {
+    mode,
+    isMobile,
+    maxZIndex = LAYOUT_MAX_Z_INDEX,
+    headerHeight,
+    tabHeight,
+    siderWidth,
+    siderCollapsedWidth,
+    footerHeight
+  } = props;
+
+  const headerZIndex = maxZIndex - 3;
+  const tabZIndex = maxZIndex - 5;
+  const siderZIndex = mode === 'vertical' || isMobile ? maxZIndex - 1 : maxZIndex - 4;
+  const mobileSiderZIndex = isMobile ? maxZIndex - 2 : 0;
+  const footerZIndex = maxZIndex - 5;
+
+  const cssProps: LayoutCssVarsProps = {
+    headerHeight,
+    headerZIndex,
+    tabHeight,
+    tabZIndex,
+    siderWidth,
+    siderZIndex,
+    mobileSiderZIndex,
+    siderCollapsedWidth,
+    footerHeight,
+    footerZIndex
+  };
+
+  return createLayoutCssVarsByCssVarsProps(cssProps);
+}

+ 53 - 0
packages/materials/src/libs/page-tab/button-tab.vue

@@ -0,0 +1,53 @@
+<script setup lang="ts">
+import type { PageTabProps } from '../../types';
+import style from './index.module.css';
+
+defineOptions({
+  name: 'ButtonTab'
+});
+
+defineProps<PageTabProps>();
+
+type SlotFn = (props?: Record<string, unknown>) => any;
+
+type Slots = {
+  /**
+   * Slot
+   *
+   * The center content of the tab
+   */
+  default?: SlotFn;
+  /**
+   * Slot
+   *
+   * The left content of the tab
+   */
+  prefix?: SlotFn;
+  /**
+   * Slot
+   *
+   * The right content of the tab
+   */
+  suffix?: SlotFn;
+};
+
+defineSlots<Slots>();
+</script>
+
+<template>
+  <div
+    class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-12px whitespace-nowrap border-(1px solid) rounded-4px px-12px py-4px"
+    :class="[
+      style['button-tab'],
+      { [style['button-tab_dark']]: darkMode },
+      { [style['button-tab_active']]: active },
+      { [style['button-tab_active_dark']]: active && darkMode }
+    ]"
+  >
+    <slot name="prefix"></slot>
+    <slot></slot>
+    <slot name="suffix"></slot>
+  </div>
+</template>
+
+<style scoped></style>

+ 31 - 0
packages/materials/src/libs/page-tab/chrome-tab-bg.vue

@@ -0,0 +1,31 @@
+<script setup lang="ts">
+defineOptions({
+  name: 'ChromeTabBg'
+});
+</script>
+
+<template>
+  <svg class="size-full">
+    <defs>
+      <symbol id="geometry-left" viewBox="0 0 214 36">
+        <path d="M17 0h197v36H0v-2c4.5 0 9-3.5 9-8V8c0-4.5 3.5-8 8-8z" />
+      </symbol>
+      <symbol id="geometry-right" viewBox="0 0 214 36">
+        <use xlink:href="#geometry-left" />
+      </symbol>
+      <clipPath>
+        <rect width="100%" height="100%" x="0" />
+      </clipPath>
+    </defs>
+    <svg width="51%" height="100%">
+      <use xlink:href="#geometry-left" width="214" height="36" fill="currentColor" />
+    </svg>
+    <g transform="scale(-1, 1)">
+      <svg width="51%" height="100%" x="-100%" y="0">
+        <use xlink:href="#geometry-right" width="214" height="36" fill="currentColor" />
+      </svg>
+    </g>
+  </svg>
+</template>
+
+<style scoped></style>

+ 58 - 0
packages/materials/src/libs/page-tab/chrome-tab.vue

@@ -0,0 +1,58 @@
+<script setup lang="ts">
+import type { PageTabProps } from '../../types';
+import ChromeTabBg from './chrome-tab-bg.vue';
+import style from './index.module.css';
+
+defineOptions({
+  name: 'ChromeTab'
+});
+
+defineProps<PageTabProps>();
+
+type SlotFn = (props?: Record<string, unknown>) => any;
+
+type Slots = {
+  /**
+   * Slot
+   *
+   * The center content of the tab
+   */
+  default?: SlotFn;
+  /**
+   * Slot
+   *
+   * The left content of the tab
+   */
+  prefix?: SlotFn;
+  /**
+   * Slot
+   *
+   * The right content of the tab
+   */
+  suffix?: SlotFn;
+};
+
+defineSlots<Slots>();
+</script>
+
+<template>
+  <div
+    class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-16px whitespace-nowrap px-24px py-6px -mr-18px"
+    :class="[
+      style['chrome-tab'],
+      { [style['chrome-tab_dark']]: darkMode },
+      { [style['chrome-tab_active']]: active },
+      { [style['chrome-tab_active_dark']]: active && darkMode }
+    ]"
+  >
+    <div class=":soy: pointer-events-none absolute left-0 top-0 h-full w-full -z-1" :class="[style['chrome-tab__bg']]">
+      <ChromeTabBg />
+    </div>
+    <slot name="prefix"></slot>
+    <slot></slot>
+    <slot name="suffix"></slot>
+    <div class=":soy: absolute right-7px h-16px w-1px bg-#1f2225" :class="[style['chrome-tab-divider']]"></div>
+  </div>
+</template>
+
+<style scoped></style>

+ 97 - 0
packages/materials/src/libs/page-tab/index.module.css

@@ -0,0 +1,97 @@
+/* @type */
+
+.button-tab {
+  border-color: #e5e7eb;
+}
+
+.button-tab_dark {
+  border-color: #ffffff3d;
+}
+
+.button-tab:hover {
+  color: var(--soy-primary-color);
+  border-color: var(--soy-primary-color-opacity3);
+}
+
+.button-tab_active {
+  color: var(--soy-primary-color);
+  border-color: var(--soy-primary-color-opacity3);
+  background-color: var(--soy-primary-color-opacity1);
+}
+
+.button-tab_active_dark {
+  background-color: var(--soy-primary-color-opacity2);
+}
+
+.button-tab .svg-close:hover {
+  font-size: 12px;
+  color: #ffffff;
+  background-color: var(--soy-primary-color);
+}
+
+.button-tab_dark .svg-close:hover {
+  color: #000000;
+}
+
+.chrome-tab:hover {
+  z-index: 9;
+}
+
+.chrome-tab_active {
+  z-index: 10;
+  color: var(--soy-primary-color);
+}
+
+.chrome-tab__bg {
+  color: transparent;
+}
+
+.chrome-tab_active .chrome-tab__bg {
+  color: var(--soy-primary-color1);
+}
+
+.chrome-tab_active_dark .chrome-tab__bg {
+  color: var(--soy-primary-color2);
+}
+
+.chrome-tab:hover .chrome-tab__bg {
+  color: #dee1e6;
+}
+
+.chrome-tab_active:hover .chrome-tab__bg {
+  color: var(--soy-primary-color1);
+}
+
+.chrome-tab_dark:hover .chrome-tab__bg {
+  color: #333333;
+}
+
+.chrome-tab_active_dark:hover .chrome-tab__bg {
+  color: var(--soy-primary-color2);
+}
+
+.chrome-tab .svg-close:hover {
+  font-size: 12px;
+  color: #ffffff;
+  background-color: #9ca3af;
+}
+
+.chrome-tab_active .svg-close:hover {
+  background-color: var(--soy-primary-color);
+}
+
+.chrome-tab_dark .svg-close:hover {
+  color: #000000;
+}
+
+.chrome-tab_active .chrome-tab-divider {
+  opacity: 0;
+}
+
+.chrome-tab:hover .chrome-tab-divider {
+  opacity: 0;
+}
+
+.chrome-tab_dark .chrome-tab-divider {
+  background-color: rgba(255, 255, 255, 0.9);
+}

+ 15 - 0
packages/materials/src/libs/page-tab/index.module.css.d.ts

@@ -0,0 +1,15 @@
+declare const styles: {
+  readonly 'button-tab': string;
+  readonly 'button-tab_dark': string;
+  readonly 'button-tab_active': string;
+  readonly 'button-tab_active_dark': string;
+  readonly 'chrome-tab': string;
+  readonly 'chrome-tab_active': string;
+  readonly 'chrome-tab__bg': string;
+  readonly 'chrome-tab_active_dark': string;
+  readonly 'chrome-tab_dark': string;
+  readonly 'chrome-tab-divider': string;
+  readonly 'svg-close': string;
+};
+
+export default styles;

+ 3 - 0
packages/materials/src/libs/page-tab/index.ts

@@ -0,0 +1,3 @@
+import PageTab from './index.vue';
+
+export default PageTab;

+ 72 - 0
packages/materials/src/libs/page-tab/index.vue

@@ -0,0 +1,72 @@
+<script setup lang="ts">
+import { computed } from 'vue';
+import type { Component } from 'vue';
+import type { PageTabMode, PageTabProps } from '../../types';
+import { ACTIVE_COLOR, createTabCssVars } from './shared';
+import ChromeTab from './chrome-tab.vue';
+import ButtonTab from './button-tab.vue';
+import SvgClose from './svg-close.vue';
+import style from './index.module.css';
+
+defineOptions({
+  name: 'PageTab'
+});
+
+const props = withDefaults(defineProps<PageTabProps>(), {
+  mode: 'chrome',
+  commonClass: 'transition-all-300',
+  activeColor: ACTIVE_COLOR,
+  closable: true
+});
+
+interface Emits {
+  (e: 'close'): void;
+}
+
+const emit = defineEmits<Emits>();
+
+const activeTabComponent = computed(() => {
+  const { mode, chromeClass, buttonClass } = props;
+
+  const tabComponentMap = {
+    chrome: {
+      component: ChromeTab,
+      class: chromeClass
+    },
+    button: {
+      component: ButtonTab,
+      class: buttonClass
+    }
+  } satisfies Record<PageTabMode, { component: Component; class?: string }>;
+
+  return tabComponentMap[mode];
+});
+
+const cssVars = computed(() => createTabCssVars(props.activeColor));
+
+const bindProps = computed(() => {
+  const { chromeClass: _chromeCls, buttonClass: _btnCls, ...rest } = props;
+
+  return rest;
+});
+
+function handleClose() {
+  emit('close');
+}
+</script>
+
+<template>
+  <component :is="activeTabComponent.component" :class="activeTabComponent.class" :style="cssVars" v-bind="bindProps">
+    <template #prefix>
+      <slot name="prefix"></slot>
+    </template>
+    <slot></slot>
+    <template #suffix>
+      <slot name="suffix">
+        <SvgClose v-if="closable" :class="[style['svg-close']]" @pointerdown.stop="handleClose" />
+      </slot>
+    </template>
+  </component>
+</template>
+
+<style scoped></style>

+ 31 - 0
packages/materials/src/libs/page-tab/shared.ts

@@ -0,0 +1,31 @@
+import { addColorAlpha, transformColorWithOpacity } from '@sa/color';
+import type { PageTabCssVars, PageTabCssVarsProps } from '../../types';
+
+/** The active color of the tab */
+export const ACTIVE_COLOR = '#1890ff';
+
+function createCssVars(props: PageTabCssVarsProps) {
+  const cssVars: PageTabCssVars = {
+    '--soy-primary-color': props.primaryColor,
+    '--soy-primary-color1': props.primaryColor1,
+    '--soy-primary-color2': props.primaryColor2,
+    '--soy-primary-color-opacity1': props.primaryColorOpacity1,
+    '--soy-primary-color-opacity2': props.primaryColorOpacity2,
+    '--soy-primary-color-opacity3': props.primaryColorOpacity3
+  };
+
+  return cssVars;
+}
+
+export function createTabCssVars(primaryColor: string) {
+  const cssProps: PageTabCssVarsProps = {
+    primaryColor,
+    primaryColor1: transformColorWithOpacity(primaryColor, 0.1, '#ffffff'),
+    primaryColor2: transformColorWithOpacity(primaryColor, 0.3, '#000000'),
+    primaryColorOpacity1: addColorAlpha(primaryColor, 0.1),
+    primaryColorOpacity2: addColorAlpha(primaryColor, 0.15),
+    primaryColorOpacity3: addColorAlpha(primaryColor, 0.3)
+  };
+
+  return createCssVars(cssProps);
+}

+ 18 - 0
packages/materials/src/libs/page-tab/svg-close.vue

@@ -0,0 +1,18 @@
+<script setup lang="ts">
+defineOptions({
+  name: 'SvgClose'
+});
+</script>
+
+<template>
+  <div class=":soy: relative h-16px w-16px inline-flex items-center justify-center rd-50% text-14px">
+    <svg width="1em" height="1em" viewBox="0 0 1024 1024">
+      <path
+        fill="currentColor"
+        d="m563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8L295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512L196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1l216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
+      />
+    </svg>
+  </div>
+</template>
+
+<style scoped></style>

+ 3 - 0
packages/materials/src/libs/simple-scrollbar/index.ts

@@ -0,0 +1,3 @@
+import SimpleScrollbar from './index.vue';
+
+export default SimpleScrollbar;

+ 18 - 0
packages/materials/src/libs/simple-scrollbar/index.vue

@@ -0,0 +1,18 @@
+<script setup lang="ts">
+import Simplebar from 'simplebar-vue';
+import 'simplebar-vue/dist/simplebar.min.css';
+
+defineOptions({
+  name: 'SimpleScrollbar'
+});
+</script>
+
+<template>
+  <div class="h-full flex-1-hidden">
+    <Simplebar class="h-full">
+      <slot />
+    </Simplebar>
+  </div>
+</template>
+
+<style scoped></style>

+ 288 - 0
packages/materials/src/types/index.ts

@@ -0,0 +1,288 @@
+/** Header config */
+interface AdminLayoutHeaderConfig {
+  /**
+   * Whether header is visible
+   *
+   * @default true
+   */
+  headerVisible?: boolean;
+  /**
+   * Header height
+   *
+   * @default 56px
+   */
+  headerHeight?: number;
+}
+
+/** Tab config */
+interface AdminLayoutTabConfig {
+  /**
+   * Whether tab is visible
+   *
+   * @default true
+   */
+  tabVisible?: boolean;
+  /**
+   * Tab class
+   *
+   * @default ''
+   */
+  tabClass?: string;
+  /**
+   * Tab height
+   *
+   * @default 48px
+   */
+  tabHeight?: number;
+}
+
+/** Sider config */
+interface AdminLayoutSiderConfig {
+  /**
+   * Whether sider is visible
+   *
+   * @default true
+   */
+  siderVisible?: boolean;
+  /**
+   * Sider class
+   *
+   * @default ''
+   */
+  siderClass?: string;
+  /**
+   * Mobile sider class
+   *
+   * @default ''
+   */
+  mobileSiderClass?: string;
+  /**
+   * Sider collapse status
+   *
+   * @default false
+   */
+  siderCollapse?: boolean;
+  /**
+   * Sider width when collapse is false
+   *
+   * @default '220px'
+   */
+  siderWidth?: number;
+  /**
+   * Sider width when collapse is true
+   *
+   * @default '64px'
+   */
+  siderCollapsedWidth?: number;
+}
+
+/** Content config */
+export interface AdminLayoutContentConfig {
+  /**
+   * Content class
+   *
+   * @default ''
+   */
+  contentClass?: string;
+  /**
+   * Whether content is full the page
+   *
+   * If true, other elements will be hidden by `display: none`
+   */
+  fullContent?: boolean;
+}
+
+/** Footer config */
+export interface AdminLayoutFooterConfig {
+  /**
+   * Whether footer is visible
+   *
+   * @default true
+   */
+  footerVisible?: boolean;
+  /**
+   * Whether footer is fixed
+   *
+   * @default true
+   */
+  fixedFooter?: boolean;
+  /**
+   * Footer class
+   *
+   * @default ''
+   */
+  footerClass?: string;
+  /**
+   * Footer height
+   *
+   * @default 48px
+   */
+  footerHeight?: number;
+  /**
+   * Whether footer is on the right side
+   *
+   * When the layout is vertical, the footer is on the right side
+   */
+  rightFooter?: boolean;
+}
+
+/**
+ * Layout mode
+ *
+ * - Horizontal
+ * - Vertical
+ */
+export type LayoutMode = 'horizontal' | 'vertical';
+
+/**
+ * The scroll mode when content overflow
+ *
+ * - Wrapper: the layout component's wrapper element has a scrollbar
+ * - Content: the layout component's content element has a scrollbar
+ *
+ * @default 'wrapper'
+ */
+export type LayoutScrollMode = 'wrapper' | 'content';
+
+/** Admin layout props */
+export interface AdminLayoutProps
+  extends AdminLayoutHeaderConfig,
+    AdminLayoutTabConfig,
+    AdminLayoutSiderConfig,
+    AdminLayoutContentConfig,
+    AdminLayoutFooterConfig {
+  /**
+   * Layout mode
+   *
+   * - {@link LayoutMode}
+   */
+  mode?: LayoutMode;
+  /** Is mobile layout */
+  isMobile?: boolean;
+  /**
+   * Scroll mode
+   *
+   * - {@link ScrollMode}
+   */
+  scrollMode?: LayoutScrollMode;
+  /**
+   * The id of the scroll element of the layout
+   *
+   * It can be used to get the corresponding Dom and scroll it
+   *
+   * @example
+   *   use the default id by import
+   *   ```ts
+   *   import { adminLayoutScrollElId } from '@sa/vue-materials';
+   *   ```
+   *
+   * @default
+   * ```ts
+   * const adminLayoutScrollElId = '__ADMIN_LAYOUT_SCROLL_EL_ID__'
+   * ```
+   */
+  scrollElId?: string;
+  /** The class of the scroll element */
+  scrollElClass?: string;
+  /** The class of the scroll wrapper element */
+  scrollWrapperClass?: string;
+  /**
+   * The common class of the layout
+   *
+   * Is can be used to configure the transition animation
+   *
+   * @default 'transition-all-300'
+   */
+  commonClass?: string;
+  /**
+   * Whether fix the header and tab
+   *
+   * @default true
+   */
+  fixedTop?: boolean;
+  /**
+   * The max z-index of the layout
+   *
+   * The z-index of Header,Tab,Sider and Footer will not exceed this value
+   */
+  maxZIndex?: number;
+}
+
+type Kebab<S extends string> = S extends Uncapitalize<S> ? S : `-${Uncapitalize<S>}`;
+
+type KebabCase<S extends string> = S extends `${infer Start}${infer End}`
+  ? `${Uncapitalize<Start>}${KebabCase<Kebab<End>>}`
+  : S;
+
+type Prefix = '--soy-';
+
+export type LayoutCssVarsProps = Pick<
+  AdminLayoutProps,
+  'headerHeight' | 'tabHeight' | 'siderWidth' | 'siderCollapsedWidth' | 'footerHeight'
+> & {
+  headerZIndex?: number;
+  tabZIndex?: number;
+  siderZIndex?: number;
+  mobileSiderZIndex?: number;
+  footerZIndex?: number;
+};
+
+export type LayoutCssVars = {
+  [K in keyof LayoutCssVarsProps as `${Prefix}${KebabCase<K>}`]: string | number;
+};
+
+/**
+ * The mode of the tab
+ *
+ * - Button: button style
+ * - Chrome: chrome style
+ *
+ * @default chrome
+ */
+export type PageTabMode = 'button' | 'chrome';
+
+export interface PageTabProps {
+  /** Whether is dark mode */
+  darkMode?: boolean;
+  /**
+   * The mode of the tab
+   *
+   * - {@link TabMode}
+   */
+  mode?: PageTabMode;
+  /**
+   * The common class of the layout
+   *
+   * Is can be used to configure the transition animation
+   *
+   * @default 'transition-all-300'
+   */
+  commonClass?: string;
+  /** The class of the button tab */
+  buttonClass?: string;
+  /** The class of the chrome tab */
+  chromeClass?: string;
+  /** Whether the tab is active */
+  active?: boolean;
+  /** The color of the active tab */
+  activeColor?: string;
+  /**
+   * Whether the tab is closable
+   *
+   * Show the close icon when true
+   */
+  closable?: boolean;
+}
+
+export type PageTabCssVarsProps = {
+  primaryColor: string;
+  primaryColor1: string;
+  primaryColor2: string;
+  primaryColorOpacity1: string;
+  primaryColorOpacity2: string;
+  primaryColorOpacity3: string;
+};
+
+export type PageTabCssVars = {
+  [K in keyof PageTabCssVarsProps as `${Prefix}${KebabCase<K>}`]: string | number;
+};

+ 20 - 0
packages/materials/tsconfig.json

@@ -0,0 +1,20 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "jsx": "preserve",
+    "lib": ["DOM", "ESNext"],
+    "baseUrl": ".",
+    "module": "ESNext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "types": ["node"],
+    "strict": true,
+    "strictNullChecks": true,
+    "noUnusedLocals": true,
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+    "forceConsistentCasingInFileNames": true
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules", "dist"]
+}

+ 15 - 0
packages/ofetch/package.json

@@ -0,0 +1,15 @@
+{
+  "name": "@sa/fetch",
+  "version": "1.3.15",
+  "exports": {
+    ".": "./src/index.ts"
+  },
+  "typesVersions": {
+    "*": {
+      "*": ["./src/*"]
+    }
+  },
+  "dependencies": {
+    "ofetch": "1.4.1"
+  }
+}

+ 10 - 0
packages/ofetch/src/index.ts

@@ -0,0 +1,10 @@
+import { ofetch } from 'ofetch';
+import type { FetchOptions } from 'ofetch';
+
+export function createRequest(options: FetchOptions) {
+  const request = ofetch.create(options);
+
+  return request;
+}
+
+export default createRequest;

+ 20 - 0
packages/ofetch/tsconfig.json

@@ -0,0 +1,20 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "jsx": "preserve",
+    "lib": ["DOM", "ESNext"],
+    "baseUrl": ".",
+    "module": "ESNext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "types": ["node"],
+    "strict": true,
+    "strictNullChecks": true,
+    "noUnusedLocals": true,
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+    "forceConsistentCasingInFileNames": true
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules", "dist"]
+}

+ 3 - 0
packages/scripts/bin.ts

@@ -0,0 +1,3 @@
+#!/usr/bin/env tsx
+
+import './src/index.ts';

+ 27 - 0
packages/scripts/package.json

@@ -0,0 +1,27 @@
+{
+  "name": "@sa/scripts",
+  "version": "1.3.15",
+  "bin": {
+    "sa": "./bin.ts"
+  },
+  "exports": {
+    ".": "./src/index.ts"
+  },
+  "typesVersions": {
+    "*": {
+      "*": ["./src/*"]
+    }
+  },
+  "devDependencies": {
+    "@soybeanjs/changelog": "0.3.24",
+    "bumpp": "10.2.0",
+    "c12": "3.1.0",
+    "cac": "6.7.14",
+    "consola": "3.4.2",
+    "enquirer": "2.4.1",
+    "execa": "9.6.0",
+    "kolorist": "1.8.0",
+    "npm-check-updates": "18.0.1",
+    "rimraf": "6.0.1"
+  }
+}

+ 10 - 0
packages/scripts/src/commands/changelog.ts

@@ -0,0 +1,10 @@
+import { generateChangelog, generateTotalChangelog } from '@soybeanjs/changelog';
+import type { ChangelogOption } from '@soybeanjs/changelog';
+
+export async function genChangelog(options?: Partial<ChangelogOption>, total = false) {
+  if (total) {
+    await generateTotalChangelog(options);
+  } else {
+    await generateChangelog(options);
+  }
+}

+ 5 - 0
packages/scripts/src/commands/cleanup.ts

@@ -0,0 +1,5 @@
+import { rimraf } from 'rimraf';
+
+export async function cleanup(paths: string[]) {
+  await rimraf(paths, { glob: true });
+}

+ 84 - 0
packages/scripts/src/commands/git-commit.ts

@@ -0,0 +1,84 @@
+import path from 'node:path';
+import { readFileSync } from 'node:fs';
+import { prompt } from 'enquirer';
+import { execCommand } from '../shared';
+import { locales } from '../locales';
+import type { Lang } from '../locales';
+
+interface PromptObject {
+  types: string;
+  scopes: string;
+  description: string;
+}
+
+/**
+ * Git commit with Conventional Commits standard
+ *
+ * @param lang
+ */
+export async function gitCommit(lang: Lang = 'en-us') {
+  const { gitCommitMessages, gitCommitTypes, gitCommitScopes } = locales[lang];
+
+  const typesChoices = gitCommitTypes.map(([value, msg]) => {
+    const nameWithSuffix = `${value}:`;
+
+    const message = `${nameWithSuffix.padEnd(12)}${msg}`;
+
+    return {
+      name: value,
+      message
+    };
+  });
+
+  const scopesChoices = gitCommitScopes.map(([value, msg]) => ({
+    name: value,
+    message: `${value.padEnd(30)} (${msg})`
+  }));
+
+  const result = await prompt<PromptObject>([
+    {
+      name: 'types',
+      type: 'select',
+      message: gitCommitMessages.types,
+      choices: typesChoices
+    },
+    {
+      name: 'scopes',
+      type: 'select',
+      message: gitCommitMessages.scopes,
+      choices: scopesChoices
+    },
+    {
+      name: 'description',
+      type: 'text',
+      message: gitCommitMessages.description
+    }
+  ]);
+
+  const breaking = result.description.startsWith('!') ? '!' : '';
+
+  const description = result.description.replace(/^!/, '').trim();
+
+  const commitMsg = `${result.types}(${result.scopes})${breaking}: ${description}`;
+
+  await execCommand('git', ['commit', '-m', commitMsg], { stdio: 'inherit' });
+}
+
+/** Git commit message verify */
+export async function gitCommitVerify(lang: Lang = 'en-us', ignores: RegExp[] = []) {
+  const gitPath = await execCommand('git', ['rev-parse', '--show-toplevel']);
+
+  const gitMsgPath = path.join(gitPath, '.git', 'COMMIT_EDITMSG');
+
+  const commitMsg = readFileSync(gitMsgPath, 'utf8').trim();
+
+  if (ignores.some(regExp => regExp.test(commitMsg))) return;
+
+  const REG_EXP = /(?<type>[a-z]+)(?:\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
+
+  if (!REG_EXP.test(commitMsg)) {
+    const errorMsg = locales[lang].gitCommitVerify;
+
+    throw new Error(errorMsg);
+  }
+}

+ 6 - 0
packages/scripts/src/commands/index.ts

@@ -0,0 +1,6 @@
+export * from './git-commit';
+export * from './cleanup';
+export * from './update-pkg';
+export * from './changelog';
+export * from './release';
+export * from './router';

+ 12 - 0
packages/scripts/src/commands/release.ts

@@ -0,0 +1,12 @@
+import { versionBump } from 'bumpp';
+
+export async function release(execute = 'pnpm sa changelog', push = true) {
+  await versionBump({
+    files: ['**/package.json', '!**/node_modules'],
+    execute,
+    all: true,
+    tag: true,
+    commit: 'chore(projects): release v%s',
+    push
+  });
+}

+ 90 - 0
packages/scripts/src/commands/router.ts

@@ -0,0 +1,90 @@
+import process from 'node:process';
+import path from 'node:path';
+import { writeFile } from 'node:fs/promises';
+import { existsSync, mkdirSync } from 'node:fs';
+import { prompt } from 'enquirer';
+import { green, red } from 'kolorist';
+
+interface PromptObject {
+  routeName: string;
+  addRouteParams: boolean;
+  routeParams: string;
+}
+
+/** generate route */
+export async function generateRoute() {
+  const result = await prompt<PromptObject>([
+    {
+      name: 'routeName',
+      type: 'text',
+      message: 'please enter route name',
+      initial: 'demo-route_child'
+    },
+    {
+      name: 'addRouteParams',
+      type: 'confirm',
+      message: 'add route params?',
+      initial: false
+    }
+  ]);
+
+  if (result.addRouteParams) {
+    const answers = await prompt<PromptObject>({
+      name: 'routeParams',
+      type: 'text',
+      message: 'please enter route params',
+      initial: 'id'
+    });
+
+    Object.assign(result, answers);
+  }
+
+  const PAGE_DIR_NAME_PATTERN = /^[\w-]+[0-9a-zA-Z]+$/;
+
+  if (!PAGE_DIR_NAME_PATTERN.test(result.routeName)) {
+    throw new Error(`${red('route name is invalid, it only allow letters, numbers, "-" or "_"')}.
+For example:
+(1) one level route: ${green('demo-route')}
+(2) two level route: ${green('demo-route_child')}
+(3) multi level route: ${green('demo-route_child_child')}
+(4) group route: ${green('_ignore_demo-route')}'
+`);
+  }
+
+  const PARAM_REG = /^\w+$/g;
+
+  if (result.routeParams && !PARAM_REG.test(result.routeParams)) {
+    throw new Error(red('route params is invalid, it only allow letters, numbers or "_".'));
+  }
+
+  const cwd = process.cwd();
+
+  const [dir, ...rest] = result.routeName.split('_') as string[];
+
+  let routeDir = path.join(cwd, 'src', 'views', dir);
+
+  if (rest.length) {
+    routeDir = path.join(routeDir, rest.join('_'));
+  }
+
+  if (!existsSync(routeDir)) {
+    mkdirSync(routeDir, { recursive: true });
+  } else {
+    throw new Error(red('route already exists'));
+  }
+
+  const fileName = result.routeParams ? `[${result.routeParams}].vue` : 'index.vue';
+
+  const vueTemplate = `<script setup lang="ts"></script>
+
+<template>
+  <div>${result.routeName}</div>
+</template>
+
+<style scoped></style>
+`;
+
+  const filePath = path.join(routeDir, fileName);
+
+  await writeFile(filePath, vueTemplate);
+}

+ 5 - 0
packages/scripts/src/commands/update-pkg.ts

@@ -0,0 +1,5 @@
+import { execCommand } from '../shared';
+
+export async function updatePkg(args: string[] = ['--deep', '-u']) {
+  execCommand('npx', ['ncu', ...args], { stdio: 'inherit' });
+}

+ 39 - 0
packages/scripts/src/config/index.ts

@@ -0,0 +1,39 @@
+import process from 'node:process';
+import { loadConfig } from 'c12';
+import type { CliOption } from '../types';
+
+const defaultOptions: CliOption = {
+  cwd: process.cwd(),
+  cleanupDirs: [
+    '**/dist',
+    '**/package-lock.json',
+    '**/yarn.lock',
+    '**/pnpm-lock.yaml',
+    '**/node_modules',
+    '!node_modules/**'
+  ],
+  ncuCommandArgs: ['--deep', '-u'],
+  changelogOptions: {},
+  gitCommitVerifyIgnores: [
+    /^((Merge pull request)|(Merge (.*?) into (.*?)|(Merge branch (.*?)))(?:\r?\n)*$)/m,
+    /^(Merge tag (.*?))(?:\r?\n)*$/m,
+    /^(R|r)evert (.*)/,
+    /^(amend|fixup|squash)!/,
+    /^(Merged (.*?)(in|into) (.*)|Merged PR (.*): (.*))/,
+    /^Merge remote-tracking branch(\s*)(.*)/,
+    /^Automatic merge(.*)/,
+    /^Auto-merged (.*?) into (.*)/
+  ]
+};
+
+export async function loadCliOptions(overrides?: Partial<CliOption>, cwd = process.cwd()) {
+  const { config } = await loadConfig<Partial<CliOption>>({
+    name: 'soybean',
+    defaults: defaultOptions,
+    overrides,
+    cwd,
+    packageJson: true
+  });
+
+  return config as CliOption;
+}

+ 109 - 0
packages/scripts/src/index.ts

@@ -0,0 +1,109 @@
+import cac from 'cac';
+import { blue, lightGreen } from 'kolorist';
+import { version } from '../package.json';
+import { cleanup, genChangelog, generateRoute, gitCommit, gitCommitVerify, release, updatePkg } from './commands';
+import { loadCliOptions } from './config';
+import type { Lang } from './locales';
+
+type Command = 'cleanup' | 'update-pkg' | 'git-commit' | 'git-commit-verify' | 'changelog' | 'release' | 'gen-route';
+
+type CommandAction<A extends object> = (args?: A) => Promise<void> | void;
+
+type CommandWithAction<A extends object = object> = Record<Command, { desc: string; action: CommandAction<A> }>;
+
+interface CommandArg {
+  /** Execute additional command after bumping and before git commit. Defaults to 'pnpm sa changelog' */
+  execute?: string;
+  /** Indicates whether to push the git commit and tag. Defaults to true */
+  push?: boolean;
+  /** Generate changelog by total tags */
+  total?: boolean;
+  /**
+   * The glob pattern of dirs to clean up
+   *
+   * If not set, it will use the default value
+   *
+   * Multiple values use "," to separate them
+   */
+  cleanupDir?: string;
+  /**
+   * display lang of cli
+   *
+   * @default 'en-us'
+   */
+  lang?: Lang;
+}
+
+export async function setupCli() {
+  const cliOptions = await loadCliOptions();
+
+  const cli = cac(blue('soybean-admin'));
+
+  cli
+    .version(lightGreen(version))
+    .option(
+      '-e, --execute [command]',
+      "Execute additional command after bumping and before git commit. Defaults to 'npx soy changelog'"
+    )
+    .option('-p, --push', 'Indicates whether to push the git commit and tag')
+    .option('-t, --total', 'Generate changelog by total tags')
+    .option(
+      '-c, --cleanupDir <dir>',
+      'The glob pattern of dirs to cleanup, If not set, it will use the default value, Multiple values use "," to separate them'
+    )
+    .option('-l, --lang <lang>', 'display lang of cli', { default: 'en-us', type: [String] })
+    .help();
+
+  const commands: CommandWithAction<CommandArg> = {
+    cleanup: {
+      desc: 'delete dirs: node_modules, dist, etc.',
+      action: async () => {
+        await cleanup(cliOptions.cleanupDirs);
+      }
+    },
+    'update-pkg': {
+      desc: 'update package.json dependencies versions',
+      action: async () => {
+        await updatePkg(cliOptions.ncuCommandArgs);
+      }
+    },
+    'git-commit': {
+      desc: 'git commit, generate commit message which match Conventional Commits standard',
+      action: async args => {
+        await gitCommit(args?.lang);
+      }
+    },
+    'git-commit-verify': {
+      desc: 'verify git commit message, make sure it match Conventional Commits standard',
+      action: async args => {
+        await gitCommitVerify(args?.lang, cliOptions.gitCommitVerifyIgnores);
+      }
+    },
+    changelog: {
+      desc: 'generate changelog',
+      action: async args => {
+        await genChangelog(cliOptions.changelogOptions, args?.total);
+      }
+    },
+    release: {
+      desc: 'release: update version, generate changelog, commit code',
+      action: async args => {
+        await release(args?.execute, args?.push);
+      }
+    },
+    'gen-route': {
+      desc: 'generate route',
+      action: async () => {
+        await generateRoute();
+      }
+    }
+  };
+
+  for (const [command, { desc, action }] of Object.entries(commands)) {
+    cli.command(command, lightGreen(desc)).action(action);
+  }
+
+  cli.parse();
+}
+
+setupCli();

+ 82 - 0
packages/scripts/src/locales/index.ts

@@ -0,0 +1,82 @@
+import { bgRed, green, red, yellow } from 'kolorist';
+
+export type Lang = 'zh-cn' | 'en-us';
+
+export const locales = {
+  'zh-cn': {
+    gitCommitMessages: {
+      types: '请选择提交类型',
+      scopes: '请选择提交范围',
+      description: `请输入描述信息(${yellow('!')}开头表示破坏性改动`
+    },
+    gitCommitTypes: [
+      ['feat', '新功能'],
+      ['feat-wip', '开发中的功能,比如某功能的部分代码'],
+      ['fix', '修复Bug'],
+      ['docs', '只涉及文档更新'],
+      ['typo', '代码或文档勘误,比如错误拼写'],
+      ['style', '修改代码风格,不影响代码含义的变更'],
+      ['refactor', '代码重构,既不修复 bug 也不添加功能的代码变更'],
+      ['perf', '可提高性能的代码更改'],
+      ['optimize', '优化代码质量的代码更改'],
+      ['test', '添加缺失的测试或更正现有测试'],
+      ['build', '影响构建系统或外部依赖项的更改'],
+      ['ci', '对 CI 配置文件和脚本的更改'],
+      ['chore', '没有修改src或测试文件的其他变更'],
+      ['revert', '还原先前的提交']
+    ] as [string, string][],
+    gitCommitScopes: [
+      ['projects', '项目'],
+      ['packages', '包'],
+      ['components', '组件'],
+      ['hooks', '钩子函数'],
+      ['utils', '工具函数'],
+      ['types', 'TS类型声明'],
+      ['styles', '代码风格'],
+      ['deps', '项目依赖'],
+      ['release', '发布项目新版本'],
+      ['other', '其他的变更']
+    ] as [string, string][],
+    gitCommitVerify: `${bgRed(' 错误 ')} ${red('git 提交信息必须符合 Conventional Commits 标准!')}\n\n${green(
+      '推荐使用命令 `pnpm commit` 生成符合 Conventional Commits 标准的提交信息。\n获取有关 Conventional Commits 的更多信息,请访问此链接: https://conventionalcommits.org'
+    )}`
+  },
+  'en-us': {
+    gitCommitMessages: {
+      types: 'Please select a type',
+      scopes: 'Please select a scope',
+      description: `Please enter a description (add prefix ${yellow('!')} to indicate breaking change)`
+    },
+    gitCommitTypes: [
+      ['feat', 'A new feature'],
+      ['feat-wip', 'Features in development, such as partial code for a certain feature'],
+      ['fix', 'A bug fix'],
+      ['docs', 'Documentation only changes'],
+      ['typo', 'Code or document corrections, such as spelling errors'],
+      ['style', 'Changes that do not affect the meaning of the code'],
+      ['refactor', 'A code change that neither fixes a bug nor adds a feature'],
+      ['perf', 'A code change that improves performance'],
+      ['optimize', 'A code change that optimizes code quality'],
+      ['test', 'Adding missing tests or correcting existing tests'],
+      ['build', 'Changes that affect the build system or external dependencies'],
+      ['ci', 'Changes to our CI configuration files and scripts'],
+      ['chore', "Other changes that don't modify src or test files"],
+      ['revert', 'Reverts a previous commit']
+    ] as [string, string][],
+    gitCommitScopes: [
+      ['projects', 'project'],
+      ['packages', 'packages'],
+      ['components', 'components'],
+      ['hooks', 'hook functions'],
+      ['utils', 'utils functions'],
+      ['types', 'TS declaration'],
+      ['styles', 'style'],
+      ['deps', 'project dependencies'],
+      ['release', 'release project'],
+      ['other', 'other changes']
+    ] as [string, string][],
+    gitCommitVerify: `${bgRed(' ERROR ')} ${red('git commit message must match the Conventional Commits standard!')}\n\n${green(
+      'Recommended to use the command `pnpm commit` to generate Conventional Commits compliant commit information.\nGet more info about Conventional Commits, follow this link: https://conventionalcommits.org'
+    )}`
+  }
+} satisfies Record<Lang, Record<string, unknown>>;

+ 7 - 0
packages/scripts/src/shared/index.ts

@@ -0,0 +1,7 @@
+import type { Options } from 'execa';
+
+export async function execCommand(cmd: string, args: string[], options?: Options) {
+  const { execa } = await import('execa');
+  const res = await execa(cmd, args, options);
+  return (res?.stdout as string)?.trim() || '';
+}

+ 31 - 0
packages/scripts/src/types/index.ts

@@ -0,0 +1,31 @@
+import type { ChangelogOption } from '@soybeanjs/changelog';
+
+export interface CliOption {
+  /** The project root directory */
+  cwd: string;
+  /**
+   * Cleanup dirs
+   *
+   * Glob pattern syntax {@link https://github.com/isaacs/minimatch}
+   *
+   * @default
+   * ```json
+   * ["** /dist", "** /pnpm-lock.yaml", "** /node_modules", "!node_modules/**"]
+   * ```
+   */
+  cleanupDirs: string[];
+  /**
+   * Npm-check-updates command args
+   *
+   * @default ['--deep', '-u']
+   */
+  ncuCommandArgs: string[];
+  /**
+   * Options of generate changelog
+   *
+   * @link https://github.com/soybeanjs/changelog
+   */
+  changelogOptions: Partial<ChangelogOption>;
+  /** The ignore pattern list of git commit verify */
+  gitCommitVerifyIgnores: RegExp[];
+}

+ 20 - 0
packages/scripts/tsconfig.json

@@ -0,0 +1,20 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "jsx": "preserve",
+    "lib": ["DOM", "ESNext"],
+    "baseUrl": ".",
+    "module": "ESNext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "types": ["node"],
+    "strict": true,
+    "strictNullChecks": true,
+    "noUnusedLocals": true,
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+    "forceConsistentCasingInFileNames": true
+  },
+  "include": ["src/**/*", "typings/**/*"],
+  "exclude": ["node_modules", "dist"]
+}

+ 12 - 0
packages/uno-preset/package.json

@@ -0,0 +1,12 @@
+{
+  "name": "@sa/uno-preset",
+  "version": "1.3.15",
+  "exports": {
+    ".": "./src/index.ts"
+  },
+  "typesVersions": {
+    "*": {
+      "*": ["./src/*"]
+    }
+  }
+}

+ 55 - 0
packages/uno-preset/src/index.ts

@@ -0,0 +1,55 @@
+// @unocss-include
+
+import type { Preset } from '@unocss/core';
+import type { Theme } from '@unocss/preset-uno';
+
+export function presetSoybeanAdmin(): Preset<Theme> {
+  const preset: Preset<Theme> = {
+    name: 'preset-soybean-admin',
+    shortcuts: [
+      {
+        'flex-center': 'flex justify-center items-center',
+        'flex-x-center': 'flex justify-center',
+        'flex-y-center': 'flex items-center',
+        'flex-col': 'flex flex-col',
+        'flex-col-center': 'flex-center flex-col',
+        'flex-col-stretch': 'flex-col items-stretch',
+        'i-flex-center': 'inline-flex justify-center items-center',
+        'i-flex-x-center': 'inline-flex justify-center',
+        'i-flex-y-center': 'inline-flex items-center',
+        'i-flex-col': 'flex-col inline-flex',
+        'i-flex-col-center': 'flex-col i-flex-center',
+        'i-flex-col-stretch': 'i-flex-col items-stretch',
+        'flex-1-hidden': 'flex-1 overflow-hidden'
+      },
+      {
+        'absolute-lt': 'absolute left-0 top-0',
+        'absolute-lb': 'absolute left-0 bottom-0',
+        'absolute-rt': 'absolute right-0 top-0',
+        'absolute-rb': 'absolute right-0 bottom-0',
+        'absolute-tl': 'absolute-lt',
+        'absolute-tr': 'absolute-rt',
+        'absolute-bl': 'absolute-lb',
+        'absolute-br': 'absolute-rb',
+        'absolute-center': 'absolute-lt flex-center size-full',
+        'fixed-lt': 'fixed left-0 top-0',
+        'fixed-lb': 'fixed left-0 bottom-0',
+        'fixed-rt': 'fixed right-0 top-0',
+        'fixed-rb': 'fixed right-0 bottom-0',
+        'fixed-tl': 'fixed-lt',
+        'fixed-tr': 'fixed-rt',
+        'fixed-bl': 'fixed-lb',
+        'fixed-br': 'fixed-rb',
+        'fixed-center': 'fixed-lt flex-center size-full'
+      },
+      {
+        'nowrap-hidden': 'overflow-hidden whitespace-nowrap',
+        'ellipsis-text': 'nowrap-hidden text-ellipsis'
+      }
+    ]
+  };
+
+  return preset;
+}
+
+export default presetSoybeanAdmin;

+ 20 - 0
packages/uno-preset/tsconfig.json

@@ -0,0 +1,20 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "jsx": "preserve",
+    "lib": ["DOM", "ESNext"],
+    "baseUrl": ".",
+    "module": "ESNext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "types": ["node"],
+    "strict": true,
+    "strictNullChecks": true,
+    "noUnusedLocals": true,
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+    "forceConsistentCasingInFileNames": true
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules", "dist"]
+}

+ 22 - 0
packages/utils/package.json

@@ -0,0 +1,22 @@
+{
+  "name": "@sa/utils",
+  "version": "1.3.15",
+  "exports": {
+    ".": "./src/index.ts"
+  },
+  "typesVersions": {
+    "*": {
+      "*": ["./src/*"]
+    }
+  },
+  "dependencies": {
+    "colord": "2.9.3",
+    "crypto-js": "4.2.0",
+    "klona": "2.0.6",
+    "localforage": "1.10.0",
+    "nanoid": "5.1.5"
+  },
+  "devDependencies": {
+    "@types/crypto-js": "4.2.2"
+  }
+}

+ 27 - 0
packages/utils/src/crypto.ts

@@ -0,0 +1,27 @@
+import CryptoJS from 'crypto-js';
+
+export class Crypto<T extends object> {
+  /** Secret */
+  secret: string;
+
+  constructor(secret: string) {
+    this.secret = secret;
+  }
+
+  encrypt(data: T): string {
+    const dataString = JSON.stringify(data);
+    const encrypted = CryptoJS.AES.encrypt(dataString, this.secret);
+    return encrypted.toString();
+  }
+
+  decrypt(encrypted: string) {
+    const decrypted = CryptoJS.AES.decrypt(encrypted, this.secret);
+    const dataString = decrypted.toString(CryptoJS.enc.Utf8);
+    try {
+      return JSON.parse(dataString) as T;
+    } catch {
+      // avoid parse error
+      return null;
+    }
+  }
+}

+ 4 - 0
packages/utils/src/index.ts

@@ -0,0 +1,4 @@
+export * from './crypto';
+export * from './storage';
+export * from './nanoid';
+export * from './klona';

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно