Browse Source

chore: 项目初始化

lujialiang 1 week ago
commit
bef5550646
100 changed files with 6343 additions and 0 deletions
  1. 11 0
      .editorconfig
  2. 48 0
      .env
  3. 7 0
      .env.prod
  4. 11 0
      .env.test
  5. 13 0
      .gitattributes
  6. 36 0
      .gitignore
  7. 4 0
      .npmrc
  8. 22 0
      .vscode/extensions.json
  9. 20 0
      .vscode/launch.json
  10. 19 0
      .vscode/settings.json
  11. 21 0
      LICENSE
  12. 52 0
      README.md
  13. 58 0
      README.zh_CN.md
  14. 2 0
      build/config/index.ts
  15. 36 0
      build/config/proxy.ts
  16. 12 0
      build/config/time.ts
  17. 13 0
      build/plugins/html.ts
  18. 28 0
      build/plugins/index.ts
  19. 55 0
      build/plugins/router.ts
  20. 32 0
      build/plugins/unocss.ts
  21. 49 0
      build/plugins/unplugin.ts
  22. 27 0
      eslint.config.js
  23. 14 0
      index.html
  24. 101 0
      package.json
  25. 21 0
      packages/axios/package.json
  26. 5 0
      packages/axios/src/constant.ts
  27. 181 0
      packages/axios/src/index.ts
  28. 48 0
      packages/axios/src/options.ts
  29. 28 0
      packages/axios/src/shared.ts
  30. 101 0
      packages/axios/src/type.ts
  31. 20 0
      packages/axios/tsconfig.json
  32. 16 0
      packages/color/package.json
  33. 2 0
      packages/color/src/constant/index.ts
  34. 1579 0
      packages/color/src/constant/name.ts
  35. 356 0
      packages/color/src/constant/palette.ts
  36. 7 0
      packages/color/src/index.ts
  37. 176 0
      packages/color/src/palette/antd.ts
  38. 45 0
      packages/color/src/palette/index.ts
  39. 152 0
      packages/color/src/palette/recommend.ts
  40. 93 0
      packages/color/src/shared/colord.ts
  41. 2 0
      packages/color/src/shared/index.ts
  42. 49 0
      packages/color/src/shared/name.ts
  43. 58 0
      packages/color/src/types/index.ts
  44. 20 0
      packages/color/tsconfig.json
  45. 15 0
      packages/hooks/package.json
  46. 10 0
      packages/hooks/src/index.ts
  47. 31 0
      packages/hooks/src/use-boolean.ts
  48. 96 0
      packages/hooks/src/use-context.ts
  49. 49 0
      packages/hooks/src/use-count-down.ts
  50. 16 0
      packages/hooks/src/use-loading.ts
  51. 79 0
      packages/hooks/src/use-request.ts
  52. 50 0
      packages/hooks/src/use-svg-icon-render.ts
  53. 158 0
      packages/hooks/src/use-table.ts
  54. 20 0
      packages/hooks/tsconfig.json
  55. 20 0
      packages/materials/package.json
  56. 7 0
      packages/materials/src/index.ts
  57. 63 0
      packages/materials/src/libs/admin-layout/index.module.css
  58. 17 0
      packages/materials/src/libs/admin-layout/index.module.css.d.ts
  59. 5 0
      packages/materials/src/libs/admin-layout/index.ts
  60. 237 0
      packages/materials/src/libs/admin-layout/index.vue
  61. 68 0
      packages/materials/src/libs/admin-layout/shared.ts
  62. 3 0
      packages/materials/src/libs/color-picker/index.ts
  63. 116 0
      packages/materials/src/libs/color-picker/index.vue
  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. 14 0
      packages/materials/src/libs/page-tab/index.module.css.d.ts
  69. 3 0
      packages/materials/src/libs/page-tab/index.ts
  70. 85 0
      packages/materials/src/libs/page-tab/index.vue
  71. 31 0
      packages/materials/src/libs/page-tab/shared.ts
  72. 31 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. 294 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. 86 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. 55 0
      packages/scripts/src/config/index.ts
  90. 101 0
      packages/scripts/src/index.ts
  91. 7 0
      packages/scripts/src/shared/index.ts
  92. 33 0
      packages/scripts/src/types/index.ts
  93. 20 0
      packages/scripts/tsconfig.json
  94. 12 0
      packages/uno-preset/package.json
  95. 54 0
      packages/uno-preset/src/index.ts
  96. 20 0
      packages/uno-preset/tsconfig.json
  97. 21 0
      packages/utils/package.json
  98. 252 0
      packages/utils/src/color.ts
  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

+ 48 - 0
.env

@@ -0,0 +1,48 @@
+VITE_BASE_URL=/
+
+VITE_APP_TITLE=ObserveAdmin
+
+VITE_APP_DESC=ObserveAdmin is a fresh and elegant 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=N
+
+# 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=0000
+VITE_SERVICE_SUCCESS_CODE=200
+
+# 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
+VITE_SERVICE_LOGOUT_CODES=401
+
+# 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
+VITE_SERVICE_MODAL_LOGOUT_CODES=401
+
+# 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
+
+# when the route mode is static, the defined super role
+VITE_STATIC_SUPER_ROLE=R_SUPER
+
+# Used to differentiate storage across different domains
+VITE_STORAGE_PREFIX=SOY_

+ 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"
+}`

+ 11 - 0
.env.test

@@ -0,0 +1,11 @@
+# backend service base url, test environment
+# VITE_SERVICE_BASE_URL=https://mock.apifox.cn/m1/3109515-0-default
+VITE_SERVICE_BASE_URL=http://observe-server.cestong.com.cn
+
+# other backend service base url, test environment
+VITE_OTHER_SERVICE_BASE_URL= `{
+  "demo": "http://observe-server.cestong.com.cn"
+}`
+
+# whether to enable http proxy when is dev mode
+VITE_HTTP_PROXY=Y

+ 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

+ 36 - 0
.gitignore

@@ -0,0 +1,36 @@
+# 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
+**/.vitepress/cache

+ 4 - 0
.npmrc

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

+ 22 - 0
.vscode/extensions.json

@@ -0,0 +1,22 @@
+{
+  "recommendations": [
+    "afzalsayed96.icones",
+    "antfu.iconify",
+    "antfu.unocss",
+    "dbaeumer.vscode-eslint",
+    "editorconfig.editorconfig",
+    "esbenp.prettier-vscode",
+    "formulahendry.auto-close-tag",
+    "formulahendry.auto-complete-tag",
+    "formulahendry.auto-rename-tag",
+    "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"
+  ]
+}

+ 20 - 0
.vscode/launch.json

@@ -0,0 +1,20 @@
+{
+  "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": "${workspaceRoot}/node_modules/.bin/tsx",
+      "skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"],
+      "program": "${file}"
+    }
+  ]
+}

+ 19 - 0
.vscode/settings.json

@@ -0,0 +1,19 @@
+{
+  "editor.codeActionsOnSave": {
+    "source.fixAll.eslint": "explicit",
+    "source.organizeImports": "never"
+  },
+  "eslint.experimental.useFlatConfig": true,
+  "editor.formatOnSave": false,
+  "eslint.validate": ["html", "css", "scss", "json", "jsonc"],
+  "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"],
+  "prettier.enable": false,
+  "typescript.tsdk": "node_modules/typescript/lib",
+  "unocss.root": ["./"],
+  "vue.server.hybridMode": true
+}

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+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.

+ 52 - 0
README.md

@@ -0,0 +1,52 @@
+## Features
+
+- **Cutting-edge technology application**: using the latest popular technology stack such as Vue3, Vite5, TypeScript, Pinia and UnoCSS.
+- **Clear project architecture**: using pnpm monorepo architecture, clear structure, elegant and easy to understand.
+- **Strict code specifications**: follow the [SoybeanJS specification](https://docs.soybeanjs.cn/standard), integrate eslint, prettier and simple-git-hooks to ensure the code is standardized.
+- **TypeScript**: support strict type checking to improve code maintainability.
+- **Rich theme configuration**: built-in a variety of theme configurations, perfectly integrated with UnoCSS.
+- **Built-in internationalization solution**: easily realize multi-language support.
+- **Automated file routing system**: automatically generate route import, declaration and type. For more details, please refer to [Elegant Router](https://github.com/soybeanjs/elegant-router).
+- **Flexible permission routing**: support both front-end static routing and back-end dynamic routing.
+- **Rich page components**: built-in a variety of pages and components, including 403, 404, 500 pages, as well as layout components, tag components, theme configuration components, etc.
+- **Command line tool**: built-in efficient command line tool, git commit, delete file, release, etc.
+- **Mobile adaptation**: perfectly support mobile terminal to realize adaptive layout.
+
+
+## Usage
+
+**Environment Preparation**
+
+Make sure your environment meets the following requirements:
+
+- **git**: you need git to clone and manage project versions.
+- **NodeJS**: >=18.12.0, recommended 18.19.0 or higher.
+- **pnpm**: >= 8.7.0, recommended 8.14.0 or higher.
+
+
+**Install Dependencies**
+
+```bash
+pnpm i
+```
+> Since this project uses the pnpm monorepo management method, please do not use npm or yarn to install dependencies.
+
+**Start Project**
+
+```bash
+pnpm dev
+```
+
+**Build Project**
+
+```bash
+pnpm build
+```
+
+## OpenSource Author
+
+[Soybean](https://github.com/honghuangdc)
+
+## License
+
+This project is based on the [MIT © 2021 Soybean](./LICENSE) protocol, for learning purposes only, please retain the author's copyright information for commercial use, the author does not guarantee and is not responsible for the software.

+ 58 - 0
README.zh_CN.md

@@ -0,0 +1,58 @@
+## 特性
+
+- **前沿技术应用**:采用 Vue3, Vite5, TypeScript, Pinia 和 UnoCSS 等最新流行的技术栈。
+- **清晰的项目架构**:采用 pnpm monorepo 架构,结构清晰,优雅易懂。
+- **严格的代码规范**:遵循 [SoybeanJS 规范](https://docs.soybeanjs.cn/zh/standard),集成了eslint, prettier 和 simple-git-hooks,保证代码的规范性。
+- **TypeScript**: 支持严格的类型检查,提高代码的可维护性。
+- **丰富的主题配置**:内置多样的主题配置,与 UnoCSS 完美结合。
+- **内置国际化方案**:轻松实现多语言支持。
+- **自动化文件路由系统**:自动生成路由导入、声明和类型。更多细节请查看 [Elegant Router](https://github.com/soybeanjs/elegant-router)。
+- **灵活的权限路由**:同时支持前端静态路由和后端动态路由。
+- **丰富的页面组件**:内置多样页面和组件,包括403、404、500页面,以及布局组件、标签组件、主题配置组件等。
+- **命令行工具**:内置高效的命令行工具,git提交、删除文件、发布等。
+- **移动端适配**:完美支持移动端,实现自适应布局。
+
+
+## 使用
+
+**环境准备**
+
+确保你的环境满足以下要求:
+
+- **git**: 你需要git来克隆和管理项目版本。
+- **NodeJS**: >=18.12.0,推荐 18.19.0 或更高。
+- **pnpm**: >= 8.7.0,推荐 8.14.0 或更高。
+
+
+**安装依赖**
+
+```bash
+pnpm i
+```
+> 由于本项目采用了 pnpm monorepo 的管理方式,因此请不要使用 npm 或 yarn 来安装依赖。
+
+**启动项目**
+
+```bash
+pnpm dev
+```
+
+**构建项目**
+
+```bash
+pnpm build
+```
+
+
+
+
+
+
+TODOList 关键词:“待完善”
+1.token刷新逻辑和目前业务不同,需要后续确认处理(soybean login接口返回token&refreshtoken,咱们目前返回:token)
+
+
+
+
+后端:
+菜单管理列表数据结构和其他列表要统一,数据格式为data{list[],count,pageIndex,pageSize}

+ 2 - 0
build/config/index.ts

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

+ 36 - 0
build/config/proxy.ts

@@ -0,0 +1,36 @@
+import type { ProxyOptions } from 'vite';
+import { createServiceConfig } from '../../src/utils/service';
+
+/**
+ * Set http proxy
+ *
+ * @param env - The current env
+ * @param isDev - Is development environment
+ */
+export function createViteProxy(env: Env.ImportMeta, isDev: boolean) {
+  const isEnableHttpProxy = isDev && env.VITE_HTTP_PROXY === 'Y';
+
+  if (!isEnableHttpProxy) return undefined;
+
+  const { baseURL, proxyPattern, other } = createServiceConfig(env);
+
+  const proxy: Record<string, ProxyOptions> = createProxyItem({ baseURL, proxyPattern });
+
+  other.forEach(item => {
+    Object.assign(proxy, createProxyItem(item));
+  });
+
+  return proxy;
+}
+
+function createProxyItem(item: App.Service.ServiceConfigItem) {
+  const proxy: Record<string, ProxyOptions> = {};
+
+  proxy[item.proxyPattern] = {
+    target: item.baseURL,
+    changeOrigin: true,
+    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;
+}

+ 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;
+}

+ 28 - 0
build/plugins/index.ts

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

+ 55 - 0
build/plugins/router.ts

@@ -0,0 +1,55 @@
+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_vue',
+        'document_vite',
+        'document_unocss',
+        'document_naive',
+        'document_antd'
+      ]
+    },
+    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 Icons from 'unplugin-icons/vite';
+import IconsResolver from 'unplugin-icons/resolver';
+import Components from 'unplugin-vue-components/vite';
+import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
+import { FileSystemIconLoader } from 'unplugin-icons/loaders';
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
+
+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: [
+        AntDesignVueResolver({
+          importStyle: false
+        }),
+        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;
+}

+ 27 - 0
eslint.config.js

@@ -0,0 +1,27 @@
+import { defineConfig } from '@soybeanjs/eslint-config';
+
+export default defineConfig(
+  { vue: true, unocss: true },
+  {
+    rules: {
+      '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'
+    }
+  },
+  {
+    ignores: ['src/views/admin/**']
+  }
+);

+ 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>

+ 101 - 0
package.json

@@ -0,0 +1,101 @@
+{
+  "name": "observe-admin",
+  "type": "module",
+  "version": "1.0.0",
+  "description": "observe admin, based on Vue3,Vite3,TypeScript,AntDesign and UnoCSS.",
+  "author": "cestong.com",
+  "license": "MIT",
+  "homepage": "http://observe-front.cestong.com.cn/#/home",
+  "keywords": ["Vue3 admin ", "vue-admin-template", "Vite5", "TypeScript", "ant-design-vue v4", "UnoCSS"],
+  "engines": {
+    "node": ">=18.12.0",
+    "pnpm": ">=8.7.0"
+  },
+  "scripts": {
+    "build": "vite build --mode prod",
+    "build:test": "vite build --mode test",
+    "cleanup": "sa cleanup",
+    "commit": "sa git-commit",
+    "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": {
+    "@better-scroll/core": "2.5.1",
+    "@iconify/vue": "4.1.2",
+    "@sa/axios": "workspace:*",
+    "@sa/color": "workspace:*",
+    "@sa/fetch": "workspace:*",
+    "@sa/hooks": "workspace:*",
+    "@sa/materials": "workspace:*",
+    "@sa/utils": "workspace:*",
+    "@vueuse/core": "10.11.0",
+    "ant-design-vue": "4.2.3",
+    "clipboard": "2.0.11",
+    "dayjs": "1.11.11",
+    "echarts": "5.5.0",
+    "exceljs": "^4.4.0",
+    "file-saver": "^2.0.5",
+    "jsencrypt": "^3.3.2",
+    "lodash-es": "4.17.21",
+    "nprogress": "0.2.0",
+    "pinia": "2.1.7",
+    "tailwind-merge": "2.3.0",
+    "vue": "3.4.29",
+    "vue-draggable-plus": "0.5.0",
+    "vue-i18n": "9.13.1",
+    "vue-router": "4.3.3"
+  },
+  "devDependencies": {
+    "@elegant-router/vue": "0.3.7",
+    "@iconify/json": "2.2.220",
+    "@sa/scripts": "workspace:*",
+    "@sa/uno-preset": "workspace:*",
+    "@soybeanjs/eslint-config": "1.3.7",
+    "@types/file-saver": "^2.0.7",
+    "@types/lodash-es": "4.17.12",
+    "@types/node": "20.14.6",
+    "@types/nprogress": "0.2.3",
+    "@unocss/eslint-config": "0.61.0",
+    "@unocss/preset-icons": "0.61.0",
+    "@unocss/preset-uno": "0.61.0",
+    "@unocss/transformer-directives": "0.61.0",
+    "@unocss/transformer-variant-group": "0.61.0",
+    "@unocss/vite": "0.61.0",
+    "@vitejs/plugin-vue": "5.0.5",
+    "@vitejs/plugin-vue-jsx": "4.0.0",
+    "eslint": "9.5.0",
+    "eslint-plugin-vue": "9.26.0",
+    "lint-staged": "15.2.7",
+    "sass": "1.77.6",
+    "simple-git-hooks": "2.11.1",
+    "tsx": "4.15.6",
+    "typescript": "5.4.5",
+    "unplugin-icons": "0.19.0",
+    "unplugin-vue-components": "0.27.0",
+    "vite": "5.3.1",
+    "vite-plugin-progress": "0.0.7",
+    "vite-plugin-svg-icons": "2.0.1",
+    "vite-plugin-vue-devtools": "7.3.2",
+    "vue-eslint-parser": "9.4.3",
+    "vue-tsc": "2.0.21"
+  },
+  "simple-git-hooks": {
+    "commit-msg": "pnpm sa git-commit-verify",
+    "pre-commit": "pnpm typecheck && pnpm lint-staged"
+  },
+  "lint-staged": {
+    "*": "eslint --fix"
+  },
+  "volta": {
+    "node": "18.20.5",
+    "pnpm": "8.15.9"
+  },
+  "website": "http://observe-front.cestong.com.cn/#/home"
+}

+ 21 - 0
packages/axios/package.json

@@ -0,0 +1,21 @@
+{
+  "name": "@sa/axios",
+  "version": "1.2.6",
+  "exports": {
+    ".": "./src/index.ts"
+  },
+  "typesVersions": {
+    "*": {
+      "*": ["./src/*"]
+    }
+  },
+  "dependencies": {
+    "@sa/utils": "workspace:*",
+    "axios": "1.7.2",
+    "axios-retry": "4.4.0",
+    "qs": "6.12.1"
+  },
+  "devDependencies": {
+    "@types/qs": "6.9.15"
+  }
+}

+ 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';

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

@@ -0,0 +1,181 @@
+import axios, { AxiosError } from 'axios';
+import type { AxiosResponse, CancelTokenSource, 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 = any>(
+  axiosConfig?: CreateAxiosDefaults,
+  options?: Partial<RequestOption<ResponseData>>
+) {
+  const opts = createDefaultOptions<ResponseData>(options);
+
+  const axiosConf = createAxiosConfig(axiosConfig);
+  const instance = axios.create(axiosConf);
+
+  const cancelTokenSourceMap = new Map<string, CancelTokenSource>();
+
+  // 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 cancel token
+    const cancelTokenSource = axios.CancelToken.source();
+    config.cancelToken = cancelTokenSource.token;
+    cancelTokenSourceMap.set(requestId, cancelTokenSource);
+
+    // 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 cancelRequest(requestId: string) {
+    const cancelTokenSource = cancelTokenSourceMap.get(requestId);
+    if (cancelTokenSource) {
+      cancelTokenSource.cancel();
+      cancelTokenSourceMap.delete(requestId);
+    }
+  }
+
+  function cancelAllRequest() {
+    cancelTokenSourceMap.forEach(cancelTokenSource => {
+      cancelTokenSource.cancel();
+    });
+    cancelTokenSourceMap.clear();
+  }
+
+  return {
+    instance,
+    opts,
+    cancelRequest,
+    cancelAllRequest
+  };
+}
+
+/**
+ * create a request instance
+ *
+ * @param axiosConfig axios config
+ * @param options request options
+ */
+export function createRequest<ResponseData = any, State = Record<string, unknown>>(
+  axiosConfig?: CreateAxiosDefaults,
+  options?: Partial<RequestOption<ResponseData>>
+) {
+  const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
+
+  const request: RequestInstance<State> = async function request<T = any, R extends ResponseType = 'json'>(
+    config: CustomAxiosRequestConfig
+  ) {
+    const response: AxiosResponse<ResponseData> = await instance(config);
+
+    const responseType = response.config?.responseType || 'json';
+
+    if (responseType === 'json') {
+      return opts.transformBackendResponse(response);
+    }
+
+    return response.data as MappedType<R, T>;
+  } as RequestInstance<State>;
+
+  request.cancelRequest = cancelRequest;
+  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 = any, State = Record<string, unknown>>(
+  axiosConfig?: CreateAxiosDefaults,
+  options?: Partial<RequestOption<ResponseData>>
+) {
+  const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
+
+  const flatRequest: FlatRequestInstance<State, ResponseData> = async function flatRequest<
+    T = any,
+    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 = opts.transformBackendResponse(response);
+
+        return { data, error: null };
+      }
+
+      return { data: response.data as MappedType<R, T>, error: null };
+    } catch (error) {
+      return { data: null, error };
+    }
+  } as FlatRequestInstance<State, ResponseData>;
+
+  flatRequest.cancelRequest = cancelRequest;
+  flatRequest.cancelAllRequest = cancelAllRequest;
+  flatRequest.state = {} as State;
+
+  return flatRequest;
+}
+
+export { BACKEND_ERROR_CODE, REQUEST_ID_KEY };
+export type * from './type';
+export type { CreateAxiosDefaults, AxiosError };

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

@@ -0,0 +1,48 @@
+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 = any>(options?: Partial<RequestOption<ResponseData>>) {
+  const opts: RequestOption<ResponseData> = {
+    onRequest: async config => config,
+    isBackendSuccess: _response => true,
+    onBackendFail: async () => {},
+    transformBackendResponse: async response => response.data,
+    onError: async () => {}
+  };
+
+  Object.assign(opts, options);
+
+  return opts;
+}
+
+export function createRetryOptions(config?: Partial<CreateAxiosDefaults>) {
+  const retryConfig: IAxiosRetryConfig = {
+    retries: 3
+  };
+
+  Object.assign(retryConfig, config);
+
+  return retryConfig;
+}
+
+export function createAxiosConfig(config?: Partial<CreateAxiosDefaults>) {
+  const TEN_SECONDS = 10 * 1000;
+
+  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;
+}

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

@@ -0,0 +1,101 @@
+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 interface RequestOption<ResponseData = any> {
+  /**
+   * 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>;
+  /**
+   * transform backend response when the responseType is json
+   *
+   * @param response Axios response
+   */
+  transformBackendResponse(response: AxiosResponse<ResponseData>): any | Promise<any>;
+  /**
+   * 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<T> {
+  cancelRequest: (requestId: string) => void;
+  cancelAllRequest: () => void;
+  /** you can set custom state in the request instance */
+  state: T;
+}
+
+/** The request instance */
+export interface RequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> {
+  <T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
+}
+
+export type FlatResponseSuccessData<T = any> = {
+  data: T;
+  error: null;
+};
+
+export type FlatResponseFailData<ResponseData = any> = {
+  data: null;
+  error: AxiosError<ResponseData>;
+};
+
+export type FlatResponseData<T = any, ResponseData = any> =
+  | FlatResponseSuccessData<T>
+  | FlatResponseFailData<ResponseData>;
+
+export interface FlatRequestInstance<S = Record<string, unknown>, ResponseData = any> extends RequestInstanceCommon<S> {
+  <T = any, R extends ResponseType = 'json'>(
+    config: CustomAxiosRequestConfig<R>
+  ): Promise<FlatResponseData<MappedType<R, T>, ResponseData>>;
+}

+ 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.2.6",
+  "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"]
+}

+ 15 - 0
packages/hooks/package.json

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

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

@@ -0,0 +1,10 @@
+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 useHookTable from './use-table';
+
+export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable };
+
+export * 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';
+import type { InjectionKey } 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 { setupStore, useStore } = 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 { setupStore } from './context';
+ *
+ *   setupStore();
+ *   // const { increment } = setupStore(); // also can control the store in the parent component
+ *   </script>
+ *   ``` // B.vue
+ *   ```vue
+ *   <template>
+ *    <div>B</div>
+ *   </template>
+ *   <script setup lang="ts">
+ *   import { useStore } from './context';
+ *
+ *   const { count, increment } = useStore();
+ *   </script>
+ *   ```;
+ *
+ *   // C.vue is same as B.vue
+ *
+ * @param contextName Context name
+ * @param fn Context function
+ */
+export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) {
+  type Context = ReturnType<T>;
+
+  const { useProvide, useInject: useStore } = createContext<Context>(contextName);
+
+  function setupStore(...args: Parameters<T>) {
+    const context: Context = fn(...args);
+    return useProvide(context);
+  }
+
+  return {
+    /** Setup store in the parent component */
+    setupStore,
+    /** Use store in the child component */
+    useStore
+  };
+}
+
+/** Create context */
+function createContext<T>(contextName: string) {
+  const injectKey: InjectionKey<T> = Symbol(contextName);
+
+  function useProvide(context: T) {
+    provide(injectKey, context);
+
+    return context;
+  }
+
+  function useInject() {
+    return inject(injectKey) as T;
+  }
+
+  return {
+    useProvide,
+    useInject
+  };
+}

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

@@ -0,0 +1,49 @@
+import { computed, onScopeDispose, ref } from 'vue';
+import { useRafFn } from '@vueuse/core';
+
+/**
+ * count down
+ *
+ * @param seconds - count down seconds
+ */
+export default function useCountDown(seconds: number) {
+  const FPS_PER_SECOND = 60;
+
+  const fps = ref(0);
+
+  const count = computed(() => Math.ceil(fps.value / FPS_PER_SECOND));
+
+  const isCounting = computed(() => fps.value > 0);
+
+  const { pause, resume } = useRafFn(
+    () => {
+      if (fps.value > 0) {
+        fps.value -= 1;
+      } else {
+        pause();
+      }
+    },
+    { immediate: false }
+  );
+
+  function start(updateSeconds: number = seconds) {
+    fps.value = FPS_PER_SECOND * updateSeconds;
+    resume();
+  }
+
+  function stop() {
+    fps.value = 0;
+    pause();
+  }
+
+  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,
+  RequestOption,
+  ResponseType
+} from '@sa/axios';
+import useLoading from './use-loading';
+
+export type HookRequestInstanceResponseSuccessData<T = any> = {
+  data: Ref<T>;
+  error: Ref<null>;
+};
+
+export type HookRequestInstanceResponseFailData<ResponseData = any> = {
+  data: Ref<null>;
+  error: Ref<AxiosError<ResponseData>>;
+};
+
+export type HookRequestInstanceResponseData<T = any, ResponseData = any> = {
+  loading: Ref<boolean>;
+} & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<ResponseData>);
+
+export interface HookRequestInstance<ResponseData = any> {
+  <T = any, R extends ResponseType = 'json'>(
+    config: CustomAxiosRequestConfig
+  ): HookRequestInstanceResponseData<MappedType<R, T>, ResponseData>;
+  cancelRequest: (requestId: string) => void;
+  cancelAllRequest: () => void;
+}
+
+/**
+ * create a hook request instance
+ *
+ * @param axiosConfig
+ * @param options
+ */
+export default function createHookRequest<ResponseData = any>(
+  axiosConfig?: CreateAxiosDefaults,
+  options?: Partial<RequestOption<ResponseData>>
+) {
+  const request = createFlatRequest<ResponseData>(axiosConfig, options);
+
+  const hookRequest: HookRequestInstance<ResponseData> = function hookRequest<T = any, R extends ResponseType = 'json'>(
+    config: CustomAxiosRequestConfig
+  ) {
+    const { loading, startLoading, endLoading } = useLoading();
+
+    const data = ref<MappedType<R, T> | null>(null) as Ref<MappedType<R, T>>;
+    const error = ref<AxiosError<ResponseData> | null>(null) as Ref<AxiosError<ResponseData> | null>;
+
+    startLoading();
+
+    request(config).then(res => {
+      if (res.data) {
+        data.value = res.data;
+      } else {
+        error.value = res.error;
+      }
+
+      endLoading();
+    });
+
+    return {
+      loading,
+      data,
+      error
+    };
+  } as HookRequestInstance<ResponseData>;
+
+  hookRequest.cancelRequest = request.cancelRequest;
+  hookRequest.cancelAllRequest = request.cancelAllRequest;
+
+  return hookRequest;
+}

+ 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
+  };
+}

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

@@ -0,0 +1,158 @@
+import { computed, reactive, ref } from 'vue';
+import type { Ref } from 'vue';
+import useBoolean from './use-boolean';
+import useLoading from './use-loading';
+
+export type MaybePromise<T> = T | Promise<T>;
+
+export type ApiFn = (args: any) => Promise<unknown>;
+
+export type TableColumnCheck = {
+  key: string;
+  title: string;
+  checked: boolean;
+};
+
+export type TableDataWithIndex<T> = T & { index: number };
+
+export type TransformedData<T> = {
+  data: TableDataWithIndex<T>[];
+  pageIndex: number;
+  pageSize: number;
+  total: number;
+};
+
+export type Transformer<T, Response> = (response: Response) => TransformedData<T>;
+
+export type TableConfig<A extends ApiFn, T, C> = {
+  /** api function to get table data */
+  apiFn: A;
+  /** api params */
+  apiParams?: Parameters<A>[0];
+  /** transform api response to table data */
+  transformer: Transformer<T, Awaited<ReturnType<A>>>;
+  /** columns factory */
+  columns: () => C[];
+  /**
+   * get column checks
+   *
+   * @param columns
+   */
+  getColumnChecks: (columns: C[]) => TableColumnCheck[];
+  /**
+   * get columns
+   *
+   * @param columns
+   */
+  getColumns: (columns: C[], checks: TableColumnCheck[]) => C[];
+  /**
+   * callback when response fetched
+   *
+   * @param transformed transformed data
+   */
+  onFetched?: (transformed: TransformedData<T>) => MaybePromise<void>;
+  /**
+   * whether to get data immediately
+   *
+   * @default true
+   */
+  immediate?: boolean;
+};
+
+export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
+  const { loading, startLoading, endLoading } = useLoading();
+  const { bool: empty, setBool: setEmpty } = useBoolean();
+
+  const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
+  const searchParams: NonNullable<Parameters<A>[0]> = reactive({ ...apiParams });
+
+  const allColumns = ref(config.columns()) as Ref<C[]>;
+
+  const data: Ref<T[]> = ref([]);
+
+  const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
+
+  const columns = computed(() => getColumns(allColumns.value, columnChecks.value));
+
+  function reloadColumns() {
+    allColumns.value = config.columns();
+
+    const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
+
+    const defaultChecks = getColumnChecks(allColumns.value);
+
+    columnChecks.value = defaultChecks.map(col => ({
+      ...col,
+      checked: checkMap.get(col.key) ?? col.checked
+    }));
+  }
+
+  async function getData() {
+    startLoading();
+    const formattedParams = formatSearchParams(searchParams);
+
+    const response = await apiFn(formattedParams);
+    const transformed:any = transformer(response as Awaited<ReturnType<A>>);
+
+    data.value = transformed.data;
+
+    setEmpty(transformed.data.length === 0);
+    // 删除到当前页面的最后一条数据
+    if ( data.value.length == 0 && transformed.pageIndex >= 2 ) {
+      updateSearchParams({
+        pageIndex: transformed.pageIndex-- < 0 ? 1 : transformed.pageIndex--,
+        pageSize: transformed.pageSize,
+      });
+
+      await getData()
+      endLoading();
+      return
+    }
+    await config.onFetched?.(transformed);
+
+    endLoading();
+  }
+
+  function formatSearchParams(params: Record<string, unknown>) {
+    const formattedParams: Record<string, unknown> = {};
+    Object.entries(params).forEach(([key, value]) => {
+      if (value !== null && value !== undefined) {
+        formattedParams[key] = value;
+      }
+    });
+
+    return formattedParams;
+  }
+
+  /**
+   * update search params
+   *
+   * @param params
+   */
+  function updateSearchParams(params: Partial<Parameters<A>[0]>) {
+    Object.assign(searchParams, params);
+  }
+
+  /** reset search params */
+  function resetSearchParams() {
+    Object.assign(searchParams, apiParams);
+    getData();
+  }
+
+  if (immediate) {
+    getData();
+  }
+
+  return {
+    loading,
+    empty,
+    data,
+    columns,
+    columnChecks,
+    reloadColumns,
+    getData,
+    searchParams,
+    updateSearchParams,
+    resetSearchParams
+  };
+}

+ 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"]
+}

+ 20 - 0
packages/materials/package.json

@@ -0,0 +1,20 @@
+{
+  "name": "@sa/materials",
+  "version": "1.2.6",
+  "exports": {
+    ".": "./src/index.ts"
+  },
+  "typesVersions": {
+    "*": {
+      "*": ["./src/*"]
+    }
+  },
+  "dependencies": {
+    "@sa/utils": "workspace:*",
+    "@simonwep/pickr": "1.9.1",
+    "simplebar-vue": "2.3.5"
+  },
+  "devDependencies": {
+    "typed-css-modules": "0.9.1"
+  }
+}

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

@@ -0,0 +1,7 @@
+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';
+import ColorPicker from './libs/color-picker';
+
+export { AdminLayout, LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX, PageTab, SimpleScrollbar, ColorPicker };
+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);
+}

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

@@ -0,0 +1,17 @@
+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 };

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

@@ -0,0 +1,237 @@
+<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,
+            headerClass,
+            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);
+}

+ 3 - 0
packages/materials/src/libs/color-picker/index.ts

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

+ 116 - 0
packages/materials/src/libs/color-picker/index.vue

@@ -0,0 +1,116 @@
+<script setup lang="ts">
+import { onMounted, ref, watch } from 'vue';
+import ColorPicker from '@simonwep/pickr';
+import '@simonwep/pickr/dist/themes/nano.min.css';
+
+defineOptions({
+  name: 'ColorPicker'
+});
+
+interface Props {
+  color: string;
+  palettes?: string[];
+  disabled?: boolean;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  palettes: () => [
+    '#3b82f6',
+    '#6366f1',
+    '#8b5cf6',
+    '#a855f7',
+    '#0ea5e9',
+    '#06b6d4',
+    '#f43f5e',
+    '#ef4444',
+    '#ec4899',
+    '#d946ef',
+    '#f97316',
+    '#f59e0b',
+    '#eab308',
+    '#84cc16',
+    '#22c55e',
+    '#10b981',
+    '#14b8a6'
+  ]
+});
+
+interface Emits {
+  (e: 'update:color', value: string): void;
+}
+
+const emit = defineEmits<Emits>();
+
+const domRef = ref<HTMLElement | null>(null);
+const instance = ref<ColorPicker | null>(null);
+
+function handleColorChange(hsva: ColorPicker.HSVaColor) {
+  const color = hsva.toHEXA().toString();
+  emit('update:color', color);
+}
+
+function initColorPicker() {
+  if (!domRef.value) return;
+
+  instance.value = ColorPicker.create({
+    el: domRef.value,
+    theme: 'nano',
+    swatches: props.palettes,
+    lockOpacity: true,
+    default: props.color,
+    disabled: props.disabled,
+    components: {
+      preview: true,
+      opacity: false,
+      hue: true,
+      interaction: {
+        hex: true,
+        rgba: true,
+        input: true
+      }
+    }
+  });
+
+  instance.value.on('change', handleColorChange);
+}
+
+function updateColor(color: string) {
+  if (!instance.value) return;
+
+  instance.value.setColor(color);
+}
+
+function updateDisabled(disabled: boolean) {
+  if (!instance.value) return;
+
+  if (disabled) {
+    instance.value.disable();
+  } else {
+    instance.value.enable();
+  }
+}
+
+watch(
+  () => props.color,
+  value => {
+    updateColor(value);
+  }
+);
+
+watch(
+  () => props.disabled,
+  value => {
+    updateDisabled(value);
+  }
+);
+
+onMounted(() => {
+  initColorPicker();
+});
+</script>
+
+<template>
+  <div ref="domRef"></div>
+</template>
+
+<style scoped></style>

+ 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);
+}

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

@@ -0,0 +1,14 @@
+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;

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

@@ -0,0 +1,85 @@
+<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');
+}
+
+function handleMouseup(e: MouseEvent) {
+  // close tab by mouse wheel button click
+  if (e.button === 1) {
+    handleClose();
+  }
+}
+</script>
+
+<template>
+  <component
+    :is="activeTabComponent.component"
+    :class="activeTabComponent.class"
+    :style="cssVars"
+    v-bind="bindProps"
+    @mouseup="handleMouseup"
+  >
+    <template #prefix>
+      <slot name="prefix"></slot>
+    </template>
+    <slot></slot>
+    <template #suffix>
+      <slot name="suffix">
+        <SvgClose v-if="closable" :class="[style['svg-close']]" @click="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/utils';
+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);
+}

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

@@ -0,0 +1,31 @@
+<script setup lang="ts">
+defineOptions({
+  name: 'SvgClose'
+});
+
+const emit = defineEmits<Emits>();
+
+interface Emits {
+  (e: 'click'): void;
+}
+
+function handleClick() {
+  emit('click');
+}
+</script>
+
+<template>
+  <div
+    class=":soy: relative h-16px w-16px inline-flex items-center justify-center rd-50% text-14px"
+    @click.stop="handleClick"
+  >
+    <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>

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

@@ -0,0 +1,294 @@
+/** Header config */
+interface AdminLayoutHeaderConfig {
+  /**
+   * Whether header is visible
+   *
+   * @default true
+   */
+  headerVisible?: boolean;
+  /**
+   * Header class
+   *
+   * @default ''
+   */
+  headerClass?: string;
+  /**
+   * 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.2.6",
+  "exports": {
+    ".": "./src/index.ts"
+  },
+  "typesVersions": {
+    "*": {
+      "*": ["./src/*"]
+    }
+  },
+  "dependencies": {
+    "ofetch": "1.3.4"
+  }
+}

+ 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.2.6",
+  "bin": {
+    "sa": "./bin.ts"
+  },
+  "exports": {
+    ".": "./src/index.ts"
+  },
+  "typesVersions": {
+    "*": {
+      "*": ["./src/*"]
+    }
+  },
+  "devDependencies": {
+    "@soybeanjs/changelog": "0.3.24",
+    "bumpp": "9.4.1",
+    "c12": "1.11.1",
+    "cac": "6.7.14",
+    "consola": "3.2.3",
+    "enquirer": "2.4.1",
+    "execa": "9.2.0",
+    "kolorist": "1.8.0",
+    "npm-check-updates": "16.14.20",
+    "rimraf": "5.0.7"
+  }
+}

+ 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 });
+}

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

@@ -0,0 +1,86 @@
+import path from 'node:path';
+import { readFileSync } from 'node:fs';
+import { prompt } from 'enquirer';
+import { bgRed, green, red, yellow } from 'kolorist';
+import { execCommand } from '../shared';
+import type { CliOption } from '../types';
+
+interface PromptObject {
+  types: string;
+  scopes: string;
+  description: string;
+}
+
+/**
+ * Git commit with Conventional Commits standard
+ *
+ * @param gitCommitTypes
+ * @param gitCommitScopes
+ */
+export async function gitCommit(
+  gitCommitTypes: CliOption['gitCommitTypes'],
+  gitCommitScopes: CliOption['gitCommitScopes']
+) {
+  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: 'Please select a type',
+      choices: typesChoices
+    },
+    {
+      name: 'scopes',
+      type: 'select',
+      message: 'Please select a scope',
+      choices: scopesChoices
+    },
+    {
+      name: 'description',
+      type: 'text',
+      message: `Please enter a description (add prefix ${yellow('!')} to indicate breaking change)`
+    }
+  ]);
+
+  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() {
+  const gitPath = await execCommand('git', ['rev-parse', '--show-toplevel']);
+
+  const gitMsgPath = path.join(gitPath, '.git', 'COMMIT_EDITMSG');
+
+  const commitMsg = readFileSync(gitMsgPath, 'utf8').trim();
+
+  const REG_EXP = /(?<type>[a-z]+)(?:\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
+
+  if (!REG_EXP.test(commitMsg)) {
+    throw new Error(
+      `${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'
+      )}`
+    );
+  }
+}

+ 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' });
+}

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

@@ -0,0 +1,55 @@
+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/**'
+  ],
+  gitCommitTypes: [
+    ['feat', 'A new feature'],
+    ['fix', 'A bug fix'],
+    ['docs', 'Documentation only changes'],
+    ['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']
+  ],
+  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']
+  ],
+  ncuCommandArgs: ['--deep', '-u'],
+  changelogOptions: {}
+};
+
+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;
+}

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

@@ -0,0 +1,101 @@
+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';
+
+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 cleanup
+   *
+   * If not set, it will use the default value
+   *
+   * Multiple values use "," to separate them
+   */
+  cleanupDir?: string;
+}
+
+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'
+    )
+    .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 () => {
+        await gitCommit(cliOptions.gitCommitTypes, cliOptions.gitCommitScopes);
+      }
+    },
+    'git-commit-verify': {
+      desc: 'verify git commit message, make sure it match Conventional Commits standard',
+      action: async () => {
+        await gitCommitVerify();
+      }
+    },
+    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();

+ 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() || '';
+}

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

@@ -0,0 +1,33 @@
+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[];
+  /** Git commit types */
+  gitCommitTypes: [string, string][];
+  /** Git commit scopes */
+  gitCommitScopes: [string, string][];
+  /**
+   * Npm-check-updates command args
+   *
+   * @default ['--deep', '-u']
+   */
+  ncuCommandArgs: string[];
+  /**
+   * Options of generate changelog
+   *
+   * @link https://github.com/soybeanjs/changelog
+   */
+  changelogOptions: Partial<ChangelogOption>;
+}

+ 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.2.6",
+  "exports": {
+    ".": "./src/index.ts"
+  },
+  "typesVersions": {
+    "*": {
+      "*": ["./src/*"]
+    }
+  }
+}

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

@@ -0,0 +1,54 @@
+// @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-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"]
+}

+ 21 - 0
packages/utils/package.json

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

+ 252 - 0
packages/utils/src/color.ts

@@ -0,0 +1,252 @@
+import { colord, extend } from 'colord';
+import namesPlugin from 'colord/plugins/names';
+import mixPlugin from 'colord/plugins/mix';
+import type { AnyColor, HsvColor, RgbColor } from 'colord';
+
+extend([namesPlugin, mixPlugin]);
+
+/**
+ * Add color alpha
+ *
+ * @param color - Color
+ * @param alpha - Alpha (0 - 1)
+ */
+export function addColorAlpha(color: string, 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: string, secondColor: string, 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: string, 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: string) {
+  return colord(color).isEqual('#ffffff');
+}
+
+/**
+ * Get rgb of color
+ *
+ * @param color Color
+ */
+export function getRgbOfColor(color: string) {
+  return colord(color).toRgb();
+}
+
+/** 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;
+
+/**
+ * The color index of color palette
+ *
+ * From left to right, the color is from light to dark, 6 is main color
+ */
+type ColorIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
+
+/**
+ * Get color palette (from left to right, the color is from light to dark, 6 is main color)
+ *
+ * @param color - Color
+ * @param index - The color index of color palette (the main color index is 6)
+ * @returns Hex color
+ */
+export function getColorPalette(color: AnyColor, index: ColorIndex): string {
+  const transformColor = colord(color);
+
+  if (!transformColor.isValid()) {
+    throw new Error('invalid input color value');
+  }
+
+  if (index === 6) {
+    return colord(transformColor).toHex();
+  }
+
+  const isLight = index < 6;
+  const hsv = transformColor.toHsv();
+  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 colord(newHsv).toHex();
+}
+
+/** 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: 4, opacity: 0.9 },
+  { index: 3, opacity: 0.95 },
+  { index: 2, opacity: 0.97 },
+  { index: 1, opacity: 0.98 }
+];
+
+/**
+ * Get color palettes
+ *
+ * @param color - Color
+ * @param darkTheme - Dark theme
+ * @param darkThemeMixColor - Dark theme mix color (default: #141414)
+ */
+export function getColorPalettes(color: AnyColor, darkTheme = false, darkThemeMixColor = '#141414'): string[] {
+  const indexes: ColorIndex[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+
+  const patterns = indexes.map(index => getColorPalette(color, index));
+
+  if (darkTheme) {
+    const darkPatterns = darkColorMap.map(({ index, opacity }) => {
+      const darkColor = colord(darkThemeMixColor).mix(patterns[index], opacity);
+
+      return darkColor;
+    });
+
+    return darkPatterns.map(item => colord(item).toHex());
+  }
+
+  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;
+}

+ 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 './color';
+export * from './crypto';
+export * from './storage';
+export * from './nanoid';

Some files were not shown because too many files changed in this diff