refactor: hoppscotch ui (#2887)
* feat: hopp ui initialized * feat: button components added * feat: windi css integration * chore: package removed from hopp ui * feat: storybook added * feat: move all smart components hoppscotch-ui * fix: import issue from components/smart * fix: env input component import * feat: add hoppui to windicss config * fix: remove storybook * feat: move components from hoppscotch-ui * feat: storybook added * feat: storybook progress * feat: themeing storybook * feat: add stories * chore: package updated * chore: stories added * feat: stories added * feat: stories added * feat: icons resolved * feat: i18n composable resolved * feat: histoire added * chore: resolved prettier issue * feat: radio story added * feat: story added for all components * feat: new components added to stories * fix: resolved issues * feat: readme.md added * feat: context/provider added * chore: removed app component registry * chore: remove importing of all components in hopp-ui to allow code splitting * chore: fix vite config errors * chore: jsdoc added * chore: any replaced with smart-item * chore: i18n added to ui components * chore: clean up - removed a duplicate button --------- Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com> Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
47
packages/hoppscotch-ui/.eslintrc.js
Normal file
47
packages/hoppscotch-ui/.eslintrc.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/* eslint-env node */
|
||||
require("@rushstack/eslint-patch/modern-module-resolution")
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
requireConfigFile: false,
|
||||
},
|
||||
extends: [
|
||||
"@vue/typescript/recommended",
|
||||
"plugin:vue/vue3-recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:storybook/recommended",
|
||||
],
|
||||
ignorePatterns: [
|
||||
"static/**/*",
|
||||
"./helpers/backend/graphql.ts",
|
||||
"**/*.d.ts",
|
||||
"types/**/*",
|
||||
],
|
||||
plugins: ["vue", "prettier"],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
semi: [2, "never"],
|
||||
"import/named": "off",
|
||||
// because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
|
||||
"no-console": "off",
|
||||
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
||||
"prettier/prettier":
|
||||
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/no-side-effects-in-computed-properties": "off",
|
||||
"import/no-named-as-default": "off",
|
||||
"import/no-named-as-default-member": "off",
|
||||
"@typescript-eslint/no-unused-vars":
|
||||
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"import/default": "off",
|
||||
"no-undef": "off",
|
||||
},
|
||||
}
|
||||
27
packages/hoppscotch-ui/.gitignore
vendored
Normal file
27
packages/hoppscotch-ui/.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Environment Variables
|
||||
.env
|
||||
10
packages/hoppscotch-ui/.prettierignore
Normal file
10
packages/hoppscotch-ui/.prettierignore
Normal file
@@ -0,0 +1,10 @@
|
||||
.dependabot
|
||||
.github
|
||||
.hoppscotch
|
||||
.vscode
|
||||
package-lock.json
|
||||
node_modules
|
||||
dist
|
||||
static
|
||||
components.d.ts
|
||||
src/types
|
||||
3
packages/hoppscotch-ui/.prettierrc.js
Normal file
3
packages/hoppscotch-ui/.prettierrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
semi: false
|
||||
}
|
||||
57
packages/hoppscotch-ui/README.md
Normal file
57
packages/hoppscotch-ui/README.md
Normal file
@@ -0,0 +1,57 @@
|
||||
<div align="center">
|
||||
|
||||
<a href="https://hoppscotch.io">
|
||||
|
||||
<img
|
||||
|
||||
src="https://avatars.githubusercontent.com/u/56705483"
|
||||
|
||||
alt="Hoppscotch Logo"
|
||||
|
||||
height="64"
|
||||
|
||||
/>
|
||||
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
# Hoppscotch UI
|
||||
|
||||
</div>
|
||||
|
||||
Welcome to hoppscotch-ui, a collection of presentational components for our web applications. This library is part of the hoppscotch monorepo and contains components such as buttons, spinners, modals, tabs, windows, etc.
|
||||
|
||||
## Usage
|
||||
|
||||
To use the components in project, simply name the component with `directory` name as alias:
|
||||
|
||||
For example `Primary Button` component is in `button` directory and the file name is `Primary.vue`. So, use that you have to write `<ButtonPrimary />`
|
||||
|
||||
## Histoire
|
||||
|
||||
We've included Histoire in this library which is similar to Storybook, to make it easy to play with the components in the browser. You can run Histoire in the browser with command
|
||||
|
||||
`pnpm run story:dev`
|
||||
|
||||
You can also use [Histoire](https://histoire.dev/) to create stories for your components and test them in different scenarios.
|
||||
|
||||
## Versioning
|
||||
|
||||
This project follows [Semantic Versioning](https://semver.org/) but as the project is still pre-1.0. The code and the public exposed API should not be considered to be fixed and stable. Things can change at any time!
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT) - see [`LICENSE`](https://github.com/hoppscotch/hoppscotch/blob/main/LICENSE) for more details.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<br />
|
||||
|
||||
###### built with ❤︎ by the [Hoppscotch Team](https://github.com/hoppscotch) and [contributors](https://github.com/hoppscotch/hoppscotch/graphs/contributors).
|
||||
|
||||
</div>
|
||||
7
packages/hoppscotch-ui/histoire.config.ts
Normal file
7
packages/hoppscotch-ui/histoire.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { HstVue } from "@histoire/plugin-vue"
|
||||
import { defineConfig } from "histoire"
|
||||
|
||||
export default defineConfig({
|
||||
setupFile: "histoire.setup.ts",
|
||||
plugins: [HstVue()],
|
||||
})
|
||||
5
packages/hoppscotch-ui/histoire.setup.ts
Normal file
5
packages/hoppscotch-ui/histoire.setup.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import "./src/assets/scss/styles.scss"
|
||||
import "./src/assets/scss/themes.scss"
|
||||
import "virtual:windi.css"
|
||||
|
||||
export function setupVue3() {}
|
||||
82
packages/hoppscotch-ui/package.json
Normal file
82
packages/hoppscotch-ui/package.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"name": "@hoppscotch/ui",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"story:dev": "histoire dev",
|
||||
"story:build": "histoire build",
|
||||
"story:preview": "histoire preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hoppscotch/vue-toasted": "^0.1.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@vitejs/plugin-legacy": "^2.3.0",
|
||||
"@vueuse/core": "^8.7.5",
|
||||
"@vueuse/head": "^0.7.9",
|
||||
"acorn-walk": "^8.2.0",
|
||||
"esprima": "^4.0.1",
|
||||
"events": "^3.3.0",
|
||||
"fp-ts": "^2.12.1",
|
||||
"globalthis": "^1.0.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"path": "^0.12.7",
|
||||
"rxjs": "^7.5.5",
|
||||
"splitpanes": "^3.1.1",
|
||||
"tern": "^0.24.3",
|
||||
"timers": "^0.1.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"url": "^0.11.0",
|
||||
"util": "^0.12.4",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vue": "^3.2.25",
|
||||
"vue-github-button": "^3.0.3",
|
||||
"vue-router": "^4.0.16",
|
||||
"vue-tippy": "6.0.0-alpha.58",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
|
||||
"@histoire/plugin-vue": "^0.12.4",
|
||||
"@iconify-json/lucide": "^1.1.40",
|
||||
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/splitpanes": "^2.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.19.0",
|
||||
"@typescript-eslint/parser": "^5.19.0",
|
||||
"@vitejs/plugin-vue": "^3.1.0",
|
||||
"@vue/compiler-sfc": "^3.2.39",
|
||||
"@vue/eslint-config-typescript": "^11.0.1",
|
||||
"@vue/runtime-core": "^3.2.39",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-plugin-prettier": "^4.2.0",
|
||||
"eslint-plugin-vue": "^9.5.1",
|
||||
"histoire": "^0.12.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rollup-plugin-polyfill-node": "^0.10.1",
|
||||
"sass": "^1.53.0",
|
||||
"typescript": "^4.5.4",
|
||||
"unplugin-icons": "^0.14.9",
|
||||
"unplugin-vue-components": "^0.21.0",
|
||||
"vite": "^3.2.3",
|
||||
"vite-plugin-checker": "^0.5.1",
|
||||
"vite-plugin-fonts": "^0.6.0",
|
||||
"vite-plugin-html-config": "^1.0.10",
|
||||
"vite-plugin-inspect": "^0.7.4",
|
||||
"vite-plugin-pages": "^0.26.0",
|
||||
"vite-plugin-pages-sitemap": "^1.4.0",
|
||||
"vite-plugin-pwa": "^0.13.1",
|
||||
"vite-plugin-vue-layouts": "^0.7.0",
|
||||
"vite-plugin-windicss": "^1.8.8",
|
||||
"vue-loader": "^16.8.3",
|
||||
"vue-tsc": "^0.38.2",
|
||||
"windicss": "^3.5.6"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
562
packages/hoppscotch-ui/src/assets/scss/styles.scss
Normal file
562
packages/hoppscotch-ui/src/assets/scss/styles.scss
Normal file
@@ -0,0 +1,562 @@
|
||||
* {
|
||||
@apply backface-hidden;
|
||||
@apply before:backface-hidden;
|
||||
@apply after:backface-hidden;
|
||||
@apply selection:bg-accentDark;
|
||||
@apply selection:text-accentContrast;
|
||||
}
|
||||
|
||||
:root {
|
||||
@apply antialiased;
|
||||
accent-color: var(--accent-color);
|
||||
font-variant-ligatures: common-ligatures;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-transparent;
|
||||
@apply border-solid border-l border-dividerLight border-t-0 border-b-0 border-r-0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-divider bg-clip-content;
|
||||
@apply rounded-full;
|
||||
@apply border-solid border-transparent border-4;
|
||||
@apply hover:bg-dividerDark;
|
||||
@apply hover:bg-clip-content;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@apply w-4;
|
||||
@apply h-0;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder,
|
||||
.cm-placeholder {
|
||||
@apply text-secondary;
|
||||
@apply opacity-35;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
@apply text-secondaryDark;
|
||||
@apply font-medium;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-primary;
|
||||
@apply text-secondary text-body;
|
||||
@apply font-medium;
|
||||
@apply select-none;
|
||||
@apply overflow-x-hidden;
|
||||
@apply leading-body;
|
||||
animation: fade 300ms forwards;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
0% {
|
||||
@apply opacity-0;
|
||||
}
|
||||
|
||||
100% {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
@apply transition-opacity;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
@apply opacity-0;
|
||||
}
|
||||
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
@apply transition;
|
||||
@apply duration-300;
|
||||
}
|
||||
|
||||
.slide-enter-from,
|
||||
.slide-leave-to {
|
||||
@apply transform;
|
||||
@apply translate-x-full;
|
||||
}
|
||||
|
||||
.bounce-enter-active,
|
||||
.bounce-leave-active {
|
||||
@apply transition;
|
||||
}
|
||||
|
||||
.bounce-enter-from,
|
||||
.bounce-leave-to {
|
||||
@apply transform;
|
||||
@apply scale-95;
|
||||
}
|
||||
|
||||
.svg-icons {
|
||||
@apply flex-shrink-0;
|
||||
@apply overflow-hidden;
|
||||
height: var(--line-height-body);
|
||||
width: var(--line-height-body);
|
||||
}
|
||||
|
||||
a {
|
||||
@apply inline-flex;
|
||||
@apply text-current;
|
||||
@apply no-underline;
|
||||
@apply transition;
|
||||
@apply leading-body;
|
||||
@apply focus:outline-none;
|
||||
|
||||
&.link {
|
||||
@apply items-center;
|
||||
@apply py-0.5 px-1;
|
||||
@apply -my-0.5 -mx-1;
|
||||
@apply text-accent;
|
||||
@apply rounded;
|
||||
@apply hover:text-accentDark;
|
||||
@apply focus-visible:ring;
|
||||
@apply focus-visible:ring-accent;
|
||||
@apply focus-visible:text-accentDark;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-tooltip {
|
||||
.tippy-box {
|
||||
@apply shadow-none;
|
||||
@apply fixed;
|
||||
@apply inline-flex;
|
||||
@apply -mt-6;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~="tooltip"] {
|
||||
@apply bg-tooltip;
|
||||
@apply border-solid border-tooltip;
|
||||
@apply rounded;
|
||||
@apply shadow;
|
||||
|
||||
.tippy-content {
|
||||
@apply flex;
|
||||
@apply text-tiny text-primary;
|
||||
@apply font-semibold;
|
||||
@apply py-1 px-2;
|
||||
@apply truncate;
|
||||
@apply leading-normal;
|
||||
@apply items-center;
|
||||
|
||||
kbd {
|
||||
@apply hidden;
|
||||
@apply font-sans;
|
||||
@apply bg-gray-500/45;
|
||||
@apply text-primaryLight;
|
||||
@apply rounded-sm;
|
||||
@apply px-1;
|
||||
@apply my-0 ml-1;
|
||||
@apply truncate;
|
||||
@apply sm:inline-flex;
|
||||
}
|
||||
|
||||
.env-icon {
|
||||
@apply transition;
|
||||
@apply inline-flex;
|
||||
@apply items-center;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-svg-arrow {
|
||||
svg:first-child {
|
||||
@apply fill-tooltip;
|
||||
}
|
||||
|
||||
svg:last-child {
|
||||
@apply fill-tooltip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~="popover"] {
|
||||
@apply bg-popover;
|
||||
@apply border-solid border-dividerDark;
|
||||
@apply rounded;
|
||||
@apply shadow-lg;
|
||||
|
||||
.tippy-content {
|
||||
@apply flex flex-col;
|
||||
@apply max-h-56;
|
||||
@apply items-stretch;
|
||||
@apply overflow-y-auto;
|
||||
@apply text-secondary text-body;
|
||||
@apply p-2;
|
||||
@apply leading-normal;
|
||||
@apply focus:outline-none;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.tippy-svg-arrow {
|
||||
svg:first-child {
|
||||
@apply fill-dividerDark;
|
||||
}
|
||||
|
||||
svg:last-child {
|
||||
@apply fill-popover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-v-tippy] {
|
||||
@apply flex flex-1;
|
||||
}
|
||||
|
||||
[interactive] > div {
|
||||
@apply flex flex-1;
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
hr {
|
||||
@apply border-b border-dividerLight;
|
||||
@apply my-2;
|
||||
}
|
||||
|
||||
.heading {
|
||||
@apply font-bold;
|
||||
@apply text-secondaryDark text-lg;
|
||||
@apply tracking-tight;
|
||||
}
|
||||
|
||||
.input,
|
||||
.select,
|
||||
.textarea {
|
||||
@apply flex;
|
||||
@apply w-full;
|
||||
@apply py-2 px-4;
|
||||
@apply bg-transparent;
|
||||
@apply rounded;
|
||||
@apply text-secondaryDark;
|
||||
@apply border border-divider;
|
||||
@apply focus-visible:border-dividerDark;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
button {
|
||||
@apply truncate;
|
||||
@apply transition;
|
||||
@apply text-body;
|
||||
@apply leading-body;
|
||||
@apply focus:outline-none;
|
||||
@apply disabled:cursor-not-allowed;
|
||||
}
|
||||
|
||||
.input[type="file"],
|
||||
.input[type="radio"],
|
||||
#installPWA {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.floating-input ~ label {
|
||||
@apply absolute;
|
||||
@apply px-2 py-0.5;
|
||||
@apply m-2;
|
||||
@apply rounded;
|
||||
@apply transition;
|
||||
@apply origin-top-left;
|
||||
}
|
||||
|
||||
.floating-input:focus-within ~ label,
|
||||
.floating-input:not(:placeholder-shown) ~ label {
|
||||
@apply bg-primary;
|
||||
@apply transform;
|
||||
@apply origin-top-left;
|
||||
@apply scale-75;
|
||||
@apply translate-x-1 -translate-y-4;
|
||||
}
|
||||
|
||||
.floating-input:focus-within ~ label {
|
||||
@apply text-secondaryDark;
|
||||
}
|
||||
|
||||
.floating-input ~ .end-actions {
|
||||
@apply absolute;
|
||||
@apply right-0.2;
|
||||
@apply inset-y-0;
|
||||
@apply flex;
|
||||
@apply items-center;
|
||||
}
|
||||
|
||||
.floating-input:has(~ .end-actions) {
|
||||
@apply pr-12;
|
||||
}
|
||||
|
||||
pre.ace_editor {
|
||||
@apply font-mono;
|
||||
@apply resize-none;
|
||||
@apply z-0;
|
||||
}
|
||||
|
||||
.select {
|
||||
@apply appearance-none;
|
||||
@apply cursor-pointer;
|
||||
|
||||
&::-ms-expand {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
@apply flex flex-1;
|
||||
@apply relative;
|
||||
@apply after:absolute;
|
||||
@apply after:flex;
|
||||
@apply after:inset-y-0;
|
||||
@apply after:items-center;
|
||||
@apply after:justify-center;
|
||||
@apply after:pointer-events-none;
|
||||
@apply after:font-icon;
|
||||
@apply after:text-secondaryLight;
|
||||
@apply after:right-3;
|
||||
@apply after:content-["\e313"];
|
||||
}
|
||||
|
||||
.info-response {
|
||||
@apply text-pink-500;
|
||||
}
|
||||
|
||||
.success-response {
|
||||
@apply text-green-500;
|
||||
}
|
||||
|
||||
.redir-response {
|
||||
@apply text-yellow-500;
|
||||
}
|
||||
|
||||
.cl-error-response {
|
||||
@apply text-red-500;
|
||||
}
|
||||
|
||||
.sv-error-response {
|
||||
@apply text-red-600;
|
||||
}
|
||||
|
||||
.missing-data-response {
|
||||
@apply text-secondaryLight;
|
||||
}
|
||||
|
||||
.toasted-container {
|
||||
@apply max-w-md;
|
||||
|
||||
.toasted {
|
||||
&.toasted-primary {
|
||||
@apply px-4 py-2;
|
||||
@apply bg-tooltip;
|
||||
@apply border-secondaryDark;
|
||||
@apply text-primary text-body;
|
||||
@apply justify-between;
|
||||
@apply shadow-lg;
|
||||
@apply font-semibold;
|
||||
@apply transition;
|
||||
@apply leading-body;
|
||||
@apply sm:rounded;
|
||||
@apply sm:border;
|
||||
|
||||
.action {
|
||||
@apply relative;
|
||||
@apply flex flex-shrink-0;
|
||||
@apply text-body;
|
||||
@apply px-4;
|
||||
@apply my-1;
|
||||
@apply ml-auto;
|
||||
@apply normal-case;
|
||||
@apply font-semibold;
|
||||
@apply leading-body;
|
||||
@apply tracking-normal;
|
||||
@apply rounded;
|
||||
@apply last:ml-4;
|
||||
@apply sm:ml-8;
|
||||
@apply before:absolute;
|
||||
@apply before:bg-current;
|
||||
@apply before:opacity-10;
|
||||
@apply before:inset-0;
|
||||
@apply before:transition;
|
||||
@apply before:content-DEFAULT;
|
||||
@apply hover:no-underline;
|
||||
@apply hover:before:opacity-20;
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
@apply bg-accent;
|
||||
@apply text-accentContrast;
|
||||
@apply border-accentDark;
|
||||
}
|
||||
|
||||
&.error {
|
||||
@apply bg-red-200;
|
||||
@apply text-red-800;
|
||||
@apply border-red-400;
|
||||
}
|
||||
|
||||
&.success {
|
||||
@apply bg-green-200;
|
||||
@apply text-green-800;
|
||||
@apply border-green-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.smart-splitter .splitpanes__splitter {
|
||||
@apply relative;
|
||||
@apply bg-primaryLight;
|
||||
@apply before:absolute;
|
||||
@apply before:inset-0;
|
||||
@apply before:bg-accentLight;
|
||||
@apply before:opacity-0;
|
||||
@apply before:z-20;
|
||||
@apply before:transition;
|
||||
@apply before:content-DEFAULT;
|
||||
@apply after:absolute;
|
||||
@apply after:inset-0;
|
||||
@apply after:z-20;
|
||||
@apply after:transition;
|
||||
@apply after:flex;
|
||||
@apply after:items-center;
|
||||
@apply after:justify-center;
|
||||
@apply after:text-dividerDark;
|
||||
@apply after:font-icon;
|
||||
@apply hover:before:opacity-100;
|
||||
@apply hover:after:text-accentDark;
|
||||
}
|
||||
|
||||
.no-splitter .splitpanes__splitter {
|
||||
@apply relative;
|
||||
@apply bg-primaryLight;
|
||||
}
|
||||
|
||||
.smart-splitter.splitpanes--vertical > .splitpanes__splitter {
|
||||
@apply w-1;
|
||||
@apply before:-left-0.5;
|
||||
@apply before:-right-0.5;
|
||||
@apply before:h-full;
|
||||
@apply after:content-["\e5d4"];
|
||||
}
|
||||
|
||||
.smart-splitter.splitpanes--horizontal > .splitpanes__splitter {
|
||||
@apply h-1;
|
||||
@apply before:-top-0.5;
|
||||
@apply before:-bottom-0.5;
|
||||
@apply before:w-full;
|
||||
@apply after:content-["\e5d3"];
|
||||
}
|
||||
|
||||
.no-splitter.splitpanes--vertical > .splitpanes__splitter {
|
||||
@apply w-0.5;
|
||||
@apply pointer-events-none;
|
||||
}
|
||||
|
||||
.no-splitter.splitpanes--horizontal > .splitpanes__splitter {
|
||||
@apply h-0.5;
|
||||
@apply pointer-events-none;
|
||||
}
|
||||
|
||||
.cm-focused {
|
||||
@apply select-auto;
|
||||
@apply outline-none #{!important};
|
||||
|
||||
.cm-activeLine {
|
||||
@apply bg-primaryLight;
|
||||
}
|
||||
|
||||
.cm-activeLineGutter {
|
||||
@apply bg-primaryDark;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
.cm-line::selection {
|
||||
@apply bg-accentDark #{!important};
|
||||
@apply text-accentContrast #{!important};
|
||||
}
|
||||
|
||||
.cm-line ::selection {
|
||||
@apply bg-accentDark #{!important};
|
||||
@apply text-accentContrast #{!important};
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-key {
|
||||
@apply inline-flex;
|
||||
@apply font-sans;
|
||||
@apply text-tiny;
|
||||
@apply bg-divider;
|
||||
@apply rounded;
|
||||
@apply ml-2;
|
||||
@apply px-1;
|
||||
@apply min-w-5;
|
||||
@apply min-h-5;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply border border-dividerDark;
|
||||
@apply shadow-sm;
|
||||
@apply <sm:hidden;
|
||||
}
|
||||
|
||||
.capitalize-first {
|
||||
@apply first-letter:capitalize;
|
||||
}
|
||||
|
||||
details {
|
||||
@apply select-none;
|
||||
}
|
||||
|
||||
details summary::-webkit-details-marker {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
details summary .indicator {
|
||||
@apply transition;
|
||||
}
|
||||
|
||||
details[open] summary .indicator {
|
||||
@apply transform;
|
||||
@apply rotate-90;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
main {
|
||||
margin-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
}
|
||||
|
||||
.env-highlight {
|
||||
* {
|
||||
@apply text-accentContrast;
|
||||
}
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
@apply bg-accent #{!important};
|
||||
}
|
||||
|
||||
.color-picker[type="color"] {
|
||||
@apply appearance-none;
|
||||
}
|
||||
|
||||
.color-picker[type="color"]::-webkit-color-swatch-wrapper {
|
||||
@apply rounded;
|
||||
@apply p-0;
|
||||
}
|
||||
|
||||
.color-picker[type="color"]::-webkit-color-swatch {
|
||||
@apply rounded;
|
||||
@apply border-0;
|
||||
}
|
||||
311
packages/hoppscotch-ui/src/assets/scss/themes.scss
Normal file
311
packages/hoppscotch-ui/src/assets/scss/themes.scss
Normal file
@@ -0,0 +1,311 @@
|
||||
@mixin base-theme {
|
||||
--font-sans: "Inter", sans-serif;
|
||||
--font-mono: "Roboto Mono", monospace;
|
||||
--font-icon: "Material Icons";
|
||||
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
|
||||
}
|
||||
|
||||
@mixin dark-theme {
|
||||
--primary-color: theme("colors.neutral.900");
|
||||
--primary-light-color: theme("colors.dark.600");
|
||||
--primary-dark-color: theme("colors.neutral.800");
|
||||
--primary-contrast-color: #161616;
|
||||
--secondary-color: theme("colors.neutral.400");
|
||||
--secondary-light-color: theme("colors.neutral.500");
|
||||
--secondary-dark-color: theme("colors.neutral.100");
|
||||
--divider-color: theme("colors.neutral.800");
|
||||
--divider-light-color: theme("colors.dark.500");
|
||||
--divider-dark-color: theme("colors.dark.300");
|
||||
--error-color: theme("colors.stone.800");
|
||||
--tooltip-color: theme("colors.neutral.100");
|
||||
--popover-color: theme("colors.dark.700");
|
||||
--editor-theme: "merbivore_soft";
|
||||
}
|
||||
|
||||
@mixin light-theme {
|
||||
--primary-color: theme("colors.white");
|
||||
--primary-light-color: theme("colors.neutral.50");
|
||||
--primary-dark-color: theme("colors.neutral.100");
|
||||
--primary-contrast-color: #fefefe;
|
||||
--secondary-color: theme("colors.neutral.500");
|
||||
--secondary-light-color: theme("colors.neutral.400");
|
||||
--secondary-dark-color: theme("colors.neutral.900");
|
||||
--divider-color: theme("colors.gray.100");
|
||||
--divider-light-color: theme("colors.neutral.100");
|
||||
--divider-dark-color: theme("colors.neutral.300");
|
||||
--error-color: theme("colors.yellow.100");
|
||||
--tooltip-color: theme("colors.neutral.800");
|
||||
--popover-color: theme("colors.white");
|
||||
--editor-theme: "textmate";
|
||||
}
|
||||
|
||||
@mixin black-theme {
|
||||
--primary-color: theme("colors.dark.900");
|
||||
--primary-light-color: theme("colors.neutral.900");
|
||||
--primary-dark-color: theme("colors.dark.800");
|
||||
--primary-contrast-color: #0e0e0e;
|
||||
--secondary-color: theme("colors.neutral.400");
|
||||
--secondary-light-color: theme("colors.neutral.500");
|
||||
--secondary-dark-color: theme("colors.neutral.100");
|
||||
--divider-color: theme("colors.neutral.800");
|
||||
--divider-light-color: theme("colors.dark.800");
|
||||
--divider-dark-color: theme("colors.dark.300");
|
||||
--error-color: theme("colors.stone.900");
|
||||
--tooltip-color: theme("colors.neutral.100");
|
||||
--popover-color: theme("colors.dark.600");
|
||||
--editor-theme: "twilight";
|
||||
}
|
||||
|
||||
@mixin dark-editor-theme {
|
||||
--editor-type-color: theme("colors.purple.400");
|
||||
--editor-name-color: theme("colors.blue.400");
|
||||
--editor-operator-color: theme("colors.indigo.400");
|
||||
--editor-invalid-color: theme("colors.red.400");
|
||||
--editor-separator-color: theme("colors.gray.400");
|
||||
--editor-meta-color: theme("colors.gray.400");
|
||||
--editor-variable-color: theme("colors.green.400");
|
||||
--editor-link-color: theme("colors.cyan.400");
|
||||
--editor-process-color: theme("colors.fuchsia.400");
|
||||
--editor-constant-color: theme("colors.violet.400");
|
||||
--editor-keyword-color: theme("colors.pink.400");
|
||||
}
|
||||
|
||||
@mixin light-editor-theme {
|
||||
--editor-type-color: theme("colors.purple.600");
|
||||
--editor-name-color: theme("colors.red.600");
|
||||
--editor-operator-color: theme("colors.indigo.600");
|
||||
--editor-invalid-color: theme("colors.red.600");
|
||||
--editor-separator-color: theme("colors.gray.600");
|
||||
--editor-meta-color: theme("colors.gray.600");
|
||||
--editor-variable-color: theme("colors.green.600");
|
||||
--editor-link-color: theme("colors.cyan.600");
|
||||
--editor-process-color: theme("colors.blue.600");
|
||||
--editor-constant-color: theme("colors.fuchsia.600");
|
||||
--editor-keyword-color: theme("colors.pink.600");
|
||||
}
|
||||
|
||||
@mixin black-editor-theme {
|
||||
--editor-type-color: theme("colors.purple.400");
|
||||
--editor-name-color: theme("colors.fuchsia.400");
|
||||
--editor-operator-color: theme("colors.indigo.400");
|
||||
--editor-invalid-color: theme("colors.red.400");
|
||||
--editor-separator-color: theme("colors.gray.400");
|
||||
--editor-meta-color: theme("colors.gray.400");
|
||||
--editor-variable-color: theme("colors.green.400");
|
||||
--editor-link-color: theme("colors.cyan.400");
|
||||
--editor-process-color: theme("colors.violet.400");
|
||||
--editor-constant-color: theme("colors.blue.400");
|
||||
--editor-keyword-color: theme("colors.pink.400");
|
||||
}
|
||||
|
||||
@mixin green-theme {
|
||||
--accent-color: theme("colors.green.500");
|
||||
--accent-light-color: theme("colors.green.400");
|
||||
--accent-dark-color: theme("colors.green.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.green.200");
|
||||
--gradient-via-color: theme("colors.green.400");
|
||||
--gradient-to-color: theme("colors.green.600");
|
||||
}
|
||||
|
||||
@mixin teal-theme {
|
||||
--accent-color: theme("colors.teal.500");
|
||||
--accent-light-color: theme("colors.teal.400");
|
||||
--accent-dark-color: theme("colors.teal.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.teal.200");
|
||||
--gradient-via-color: theme("colors.teal.400");
|
||||
--gradient-to-color: theme("colors.teal.600");
|
||||
}
|
||||
|
||||
@mixin blue-theme {
|
||||
--accent-color: theme("colors.blue.500");
|
||||
--accent-light-color: theme("colors.blue.400");
|
||||
--accent-dark-color: theme("colors.blue.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.blue.200");
|
||||
--gradient-via-color: theme("colors.blue.400");
|
||||
--gradient-to-color: theme("colors.blue.600");
|
||||
}
|
||||
|
||||
@mixin indigo-theme {
|
||||
--accent-color: theme("colors.indigo.500");
|
||||
--accent-light-color: theme("colors.indigo.400");
|
||||
--accent-dark-color: theme("colors.indigo.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.indigo.200");
|
||||
--gradient-via-color: theme("colors.indigo.400");
|
||||
--gradient-to-color: theme("colors.indigo.600");
|
||||
}
|
||||
|
||||
@mixin purple-theme {
|
||||
--accent-color: theme("colors.purple.500");
|
||||
--accent-light-color: theme("colors.purple.400");
|
||||
--accent-dark-color: theme("colors.purple.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.purple.200");
|
||||
--gradient-via-color: theme("colors.purple.400");
|
||||
--gradient-to-color: theme("colors.purple.600");
|
||||
}
|
||||
|
||||
@mixin yellow-theme {
|
||||
--accent-color: theme("colors.yellow.500");
|
||||
--accent-light-color: theme("colors.yellow.400");
|
||||
--accent-dark-color: theme("colors.yellow.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.yellow.200");
|
||||
--gradient-via-color: theme("colors.yellow.400");
|
||||
--gradient-to-color: theme("colors.yellow.600");
|
||||
}
|
||||
|
||||
@mixin orange-theme {
|
||||
--accent-color: theme("colors.orange.500");
|
||||
--accent-light-color: theme("colors.orange.400");
|
||||
--accent-dark-color: theme("colors.orange.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.orange.200");
|
||||
--gradient-via-color: theme("colors.orange.400");
|
||||
--gradient-to-color: theme("colors.orange.600");
|
||||
}
|
||||
|
||||
@mixin red-theme {
|
||||
--accent-color: theme("colors.red.500");
|
||||
--accent-light-color: theme("colors.red.400");
|
||||
--accent-dark-color: theme("colors.red.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.red.200");
|
||||
--gradient-via-color: theme("colors.red.400");
|
||||
--gradient-to-color: theme("colors.red.600");
|
||||
}
|
||||
|
||||
@mixin pink-theme {
|
||||
--accent-color: theme("colors.pink.500");
|
||||
--accent-light-color: theme("colors.pink.400");
|
||||
--accent-dark-color: theme("colors.pink.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.pink.200");
|
||||
--gradient-via-color: theme("colors.pink.400");
|
||||
--gradient-to-color: theme("colors.pink.600");
|
||||
}
|
||||
|
||||
:root {
|
||||
@include base-theme;
|
||||
@include dark-theme;
|
||||
@include green-theme;
|
||||
@include dark-editor-theme;
|
||||
}
|
||||
|
||||
:root.light {
|
||||
@include light-theme;
|
||||
@include light-editor-theme;
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
@include dark-theme;
|
||||
@include dark-editor-theme;
|
||||
}
|
||||
|
||||
:root.black {
|
||||
@include black-theme;
|
||||
@include black-editor-theme;
|
||||
}
|
||||
|
||||
:root[data-accent="blue"] {
|
||||
@include blue-theme;
|
||||
}
|
||||
|
||||
:root[data-accent="green"] {
|
||||
@include green-theme;
|
||||
}
|
||||
|
||||
:root[data-accent="teal"] {
|
||||
@include teal-theme;
|
||||
}
|
||||
|
||||
:root[data-accent="indigo"] {
|
||||
@include indigo-theme;
|
||||
}
|
||||
|
||||
:root[data-accent="purple"] {
|
||||
@include purple-theme;
|
||||
}
|
||||
|
||||
:root[data-accent="orange"] {
|
||||
@include orange-theme;
|
||||
}
|
||||
|
||||
:root[data-accent="pink"] {
|
||||
@include pink-theme;
|
||||
}
|
||||
|
||||
:root[data-accent="red"] {
|
||||
@include red-theme;
|
||||
}
|
||||
|
||||
:root[data-accent="yellow"] {
|
||||
@include yellow-theme;
|
||||
}
|
||||
|
||||
@mixin font-small {
|
||||
--font-size-body: 0.75rem;
|
||||
--line-height-body: 1rem;
|
||||
--upper-primary-sticky-fold: 4.125rem;
|
||||
--upper-secondary-sticky-fold: 6.188rem;
|
||||
--upper-tertiary-sticky-fold: 8.25rem;
|
||||
--upper-mobile-primary-sticky-fold: 6.625rem;
|
||||
--upper-mobile-secondary-sticky-fold: 8.688rem;
|
||||
--upper-mobile-sticky-fold: 10.75rem;
|
||||
--upper-mobile-tertiary-sticky-fold: 8.25rem;
|
||||
--lower-primary-sticky-fold: 3rem;
|
||||
--lower-secondary-sticky-fold: 5.063rem;
|
||||
--lower-tertiary-sticky-fold: 7.125rem;
|
||||
--sidebar-primary-sticky-fold: 2rem;
|
||||
}
|
||||
|
||||
@mixin font-medium {
|
||||
--font-size-body: 0.875rem;
|
||||
--line-height-body: 1.25rem;
|
||||
--upper-primary-sticky-fold: 4.375rem;
|
||||
--upper-secondary-sticky-fold: 6.688rem;
|
||||
--upper-tertiary-sticky-fold: 9rem;
|
||||
--upper-mobile-primary-sticky-fold: 7.125rem;
|
||||
--upper-mobile-secondary-sticky-fold: 9.438rem;
|
||||
--upper-mobile-sticky-fold: 11.75rem;
|
||||
--upper-mobile-tertiary-sticky-fold: 9rem;
|
||||
--lower-primary-sticky-fold: 3.25rem;
|
||||
--lower-secondary-sticky-fold: 5.563rem;
|
||||
--lower-tertiary-sticky-fold: 7.875rem;
|
||||
--sidebar-primary-sticky-fold: 2.25rem;
|
||||
}
|
||||
|
||||
@mixin font-large {
|
||||
--font-size-body: 1rem;
|
||||
--line-height-body: 1.5rem;
|
||||
--upper-primary-sticky-fold: 4.625rem;
|
||||
--upper-secondary-sticky-fold: 7.188rem;
|
||||
--upper-tertiary-sticky-fold: 9.75rem;
|
||||
--upper-mobile-primary-sticky-fold: 7.625rem;
|
||||
--upper-mobile-secondary-sticky-fold: 10.188rem;
|
||||
--upper-mobile-sticky-fold: 12.75rem;
|
||||
--upper-mobile-tertiary-sticky-fold: 9.75rem;
|
||||
--lower-primary-sticky-fold: 3.5rem;
|
||||
--lower-secondary-sticky-fold: 6.063rem;
|
||||
--lower-tertiary-sticky-fold: 8.625rem;
|
||||
--sidebar-primary-sticky-fold: 2.5rem;
|
||||
}
|
||||
|
||||
:root[data-font-size="small"] {
|
||||
@include font-small;
|
||||
}
|
||||
|
||||
:root[data-font-size="medium"] {
|
||||
@include font-medium;
|
||||
}
|
||||
|
||||
:root[data-font-size="large"] {
|
||||
@include font-large;
|
||||
}
|
||||
|
||||
.generic {
|
||||
@apply text-primary text-primaryLight bg-primary;
|
||||
}
|
||||
37
packages/hoppscotch-ui/src/components.d.ts
vendored
Normal file
37
packages/hoppscotch-ui/src/components.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
export {}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
ButtonPrimary: typeof import('./components/button/Primary.vue')['default']
|
||||
ButtonSecondary: typeof import('./components/button/Secondary.vue')['default']
|
||||
IconLucideLoader: typeof import('~icons/lucide/loader')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SmartAnchor: typeof import('./components/smart/Anchor.vue')['default']
|
||||
SmartAutoComplete: typeof import('./components/smart/AutoComplete.vue')['default']
|
||||
SmartCheckbox: typeof import('./components/smart/Checkbox.vue')['default']
|
||||
SmartConfirmModal: typeof import('./components/smart/ConfirmModal.vue')['default']
|
||||
SmartExpand: typeof import('./components/smart/Expand.vue')['default']
|
||||
SmartFileChip: typeof import('./components/smart/FileChip.vue')['default']
|
||||
SmartIntersection: typeof import('./components/smart/Intersection.vue')['default']
|
||||
SmartItem: typeof import('./components/smart/Item.vue')['default']
|
||||
SmartLink: typeof import('./components/smart/Link.vue')['default']
|
||||
SmartModal: typeof import('./components/smart/Modal.vue')['default']
|
||||
SmartProgressRing: typeof import('./components/smart/ProgressRing.vue')['default']
|
||||
SmartRadio: typeof import('./components/smart/Radio.vue')['default']
|
||||
SmartRadioGroup: typeof import('./components/smart/RadioGroup.vue')['default']
|
||||
SmartSlideOver: typeof import('./components/smart/SlideOver.vue')['default']
|
||||
SmartSpinner: typeof import('./components/smart/Spinner.vue')['default']
|
||||
SmartTab: typeof import('./components/smart/Tab.vue')['default']
|
||||
SmartTabs: typeof import('./components/smart/Tabs.vue')['default']
|
||||
SmartToggle: typeof import('./components/smart/Toggle.vue')['default']
|
||||
SmartWindow: typeof import('./components/smart/Window.vue')['default']
|
||||
SmartWindows: typeof import('./components/smart/Windows.vue')['default']
|
||||
}
|
||||
|
||||
}
|
||||
101
packages/hoppscotch-ui/src/components/button/Primary.vue
Normal file
101
packages/hoppscotch-ui/src/components/button/Primary.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<SmartLink
|
||||
:to="to"
|
||||
:blank="blank"
|
||||
class="relative inline-flex items-center justify-center py-2 font-bold transition focus:outline-none focus-visible:bg-accentDark"
|
||||
:class="[
|
||||
color
|
||||
? `text-${color}-800 bg-${color}-200 hover:(text-${color}-900 bg-${color}-300) focus-visible:(text-${color}-900 bg-${color}-300)`
|
||||
: `text-accentContrast bg-accent hover:bg-accentDark focus-visible:bg-accentDark`,
|
||||
label ? 'px-4' : 'px-2',
|
||||
rounded ? 'rounded-full' : 'rounded',
|
||||
{ 'opacity-75 cursor-not-allowed': disabled },
|
||||
{ 'pointer-events-none': loading },
|
||||
{ 'px-6 py-4 text-lg': large },
|
||||
{ 'shadow-lg hover:shadow-xl': shadow },
|
||||
{
|
||||
'text-white bg-gradient-to-tr from-gradientFrom via-gradientVia to-gradientTo':
|
||||
gradient,
|
||||
},
|
||||
{
|
||||
'border border-accent hover:border-accentDark focus-visible:border-accentDark':
|
||||
outline,
|
||||
},
|
||||
]"
|
||||
:disabled="disabled"
|
||||
:tabindex="loading ? '-1' : '0'"
|
||||
role="button"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center justify-center whitespace-nowrap"
|
||||
:class="[{ 'flex-row-reverse': reverse }, { 'opacity-50': loading }]"
|
||||
>
|
||||
<component
|
||||
:is="icon"
|
||||
v-if="icon"
|
||||
class="svg-icons"
|
||||
:class="[
|
||||
{ '!text-2xl': large },
|
||||
label ? (reverse ? 'ml-2' : 'mr-2') : '',
|
||||
]"
|
||||
/>
|
||||
{{ label }}
|
||||
<div v-if="shortcut.length" class="<sm:hidden">
|
||||
<kbd
|
||||
v-for="(key, index) in shortcut"
|
||||
:key="`key-${index}`"
|
||||
class="shortcut-key !bg-accentDark !border-accentLight"
|
||||
>
|
||||
{{ key }}
|
||||
</kbd>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
v-if="loading"
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
>
|
||||
<SmartSpinner />
|
||||
</span>
|
||||
</SmartLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from "vue"
|
||||
|
||||
interface Props {
|
||||
to?: string
|
||||
exact?: boolean
|
||||
blank?: boolean
|
||||
label?: string
|
||||
icon?: object | null | Component // It is a component!
|
||||
svg?: object | null | Component // It is a component!
|
||||
color?: string
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
large?: boolean
|
||||
shadow?: boolean
|
||||
reverse?: boolean
|
||||
rounded?: boolean
|
||||
gradient?: boolean
|
||||
outline?: boolean
|
||||
shortcut?: string[]
|
||||
}
|
||||
withDefaults(defineProps<Props>(), {
|
||||
to: "",
|
||||
exact: true,
|
||||
blank: false,
|
||||
label: "",
|
||||
icon: null,
|
||||
svg: null,
|
||||
color: "",
|
||||
disabled: false,
|
||||
loading: false,
|
||||
large: false,
|
||||
shadow: false,
|
||||
reverse: false,
|
||||
rounded: false,
|
||||
gradient: false,
|
||||
outline: false,
|
||||
shortcut: () => [],
|
||||
})
|
||||
</script>
|
||||
96
packages/hoppscotch-ui/src/components/button/Secondary.vue
Normal file
96
packages/hoppscotch-ui/src/components/button/Secondary.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<SmartLink
|
||||
:to="to"
|
||||
:exact="exact"
|
||||
:blank="blank"
|
||||
class="inline-flex items-center justify-center py-2 font-semibold transition whitespace-nowrap focus:outline-none"
|
||||
:class="[
|
||||
color
|
||||
? `text-${color}-500 hover:text-${color}-600 focus-visible:text-${color}-600`
|
||||
: 'text-secondary hover:text-secondaryDark focus-visible:text-secondaryDark',
|
||||
{ 'pointer-events-none': loading },
|
||||
label ? 'rounded px-4' : 'px-2',
|
||||
{ 'rounded-full': rounded },
|
||||
{ 'opacity-75 cursor-not-allowed': disabled },
|
||||
{ 'flex-row-reverse': reverse },
|
||||
{ 'px-6 py-4 text-lg': large },
|
||||
{
|
||||
'border border-divider hover:border-dividerDark focus-visible:border-dividerDark':
|
||||
outline,
|
||||
},
|
||||
{
|
||||
'bg-primaryLight hover:bg-primaryDark focus-visible:bg-primaryDark':
|
||||
filled,
|
||||
},
|
||||
]"
|
||||
:disabled="disabled"
|
||||
:tabindex="loading ? '-1' : '0'"
|
||||
role="button"
|
||||
>
|
||||
<span
|
||||
v-if="!loading"
|
||||
class="inline-flex items-center justify-center whitespace-nowrap"
|
||||
:class="{ 'flex-row-reverse': reverse }"
|
||||
>
|
||||
<component
|
||||
:is="icon"
|
||||
v-if="icon"
|
||||
class="svg-icons"
|
||||
:class="[
|
||||
{ '!text-2xl': large },
|
||||
label ? (reverse ? 'ml-2' : 'mr-2') : '',
|
||||
]"
|
||||
/>
|
||||
{{ label }}
|
||||
<div v-if="shortcut.length" class="<sm:hidden">
|
||||
<kbd
|
||||
v-for="(key, index) in shortcut"
|
||||
:key="`key-${index}`"
|
||||
class="shortcut-key !bg-inherit"
|
||||
>
|
||||
{{ key }}
|
||||
</kbd>
|
||||
</div>
|
||||
</span>
|
||||
<SmartSpinner v-else />
|
||||
</SmartLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from "vue"
|
||||
|
||||
interface Props {
|
||||
to?: string
|
||||
exact?: boolean
|
||||
blank?: boolean
|
||||
label?: string
|
||||
icon?: object | null | Component // It is a component!
|
||||
svg?: object | null | Component // It is a component!
|
||||
color?: string
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
reverse?: boolean
|
||||
rounded?: boolean
|
||||
large?: boolean
|
||||
outline?: boolean
|
||||
shortcut?: string[]
|
||||
filled?: boolean
|
||||
}
|
||||
withDefaults(defineProps<Props>(), {
|
||||
to: "",
|
||||
exact: true,
|
||||
blank: false,
|
||||
label: "",
|
||||
icon: null,
|
||||
svg: null,
|
||||
color: "",
|
||||
disabled: false,
|
||||
loading: false,
|
||||
reverse: false,
|
||||
rounded: false,
|
||||
large: false,
|
||||
outline: false,
|
||||
shortcut: () => [],
|
||||
filled: false,
|
||||
})
|
||||
</script>
|
||||
47
packages/hoppscotch-ui/src/components/index.ts
Normal file
47
packages/hoppscotch-ui/src/components/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import Primary from "./button/Primary.vue"
|
||||
import Secondary from "./button/Secondary.vue"
|
||||
import Anchor from "./smart/Anchor.vue"
|
||||
import AutoComplete from "./smart/AutoComplete.vue"
|
||||
import Checkbox from "./smart/Checkbox.vue"
|
||||
import ConfirmModal from "./smart/ConfirmModal.vue"
|
||||
import Expand from "./smart/Expand.vue"
|
||||
import FileChip from "./smart/FileChip.vue"
|
||||
import Intersection from "./smart/Intersection.vue"
|
||||
import Item from "./smart/Item.vue"
|
||||
import Link from "./smart/Link.vue"
|
||||
import Modal from "./smart/Modal.vue"
|
||||
import ProgressRing from "./smart/ProgressRing.vue"
|
||||
import Radio from "./smart/Radio.vue"
|
||||
import RadioGroup from "./smart/RadioGroup.vue"
|
||||
import SlideOver from "./smart/SlideOver.vue"
|
||||
import Spinner from "./smart/Spinner.vue"
|
||||
import Tab from "./smart/Tab.vue"
|
||||
import Tabs from "./smart/Tabs.vue"
|
||||
import Toggle from "./smart/Toggle.vue"
|
||||
import Window from "./smart/Window.vue"
|
||||
import Windows from "./smart/Windows.vue"
|
||||
|
||||
export default {
|
||||
Primary,
|
||||
Secondary,
|
||||
Anchor,
|
||||
AutoComplete,
|
||||
Checkbox,
|
||||
ConfirmModal,
|
||||
Expand,
|
||||
FileChip,
|
||||
Intersection,
|
||||
Item,
|
||||
Link,
|
||||
Modal,
|
||||
ProgressRing,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
SlideOver,
|
||||
Spinner,
|
||||
Tab,
|
||||
Tabs,
|
||||
Toggle,
|
||||
Window,
|
||||
Windows,
|
||||
}
|
||||
70
packages/hoppscotch-ui/src/components/smart/Anchor.vue
Normal file
70
packages/hoppscotch-ui/src/components/smart/Anchor.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<SmartLink
|
||||
:to="to"
|
||||
:exact="exact"
|
||||
:blank="blank"
|
||||
class="inline-flex items-center justify-center focus:outline-none"
|
||||
:class="[
|
||||
color
|
||||
? `text-${color}-500 hover:text-${color}-600 focus-visible:text-${color}-600`
|
||||
: 'hover:text-secondaryDark focus-visible:text-secondaryDark',
|
||||
{ 'opacity-75 cursor-not-allowed': disabled },
|
||||
{ 'flex-row-reverse': reverse },
|
||||
]"
|
||||
:disabled="disabled"
|
||||
tabindex="0"
|
||||
>
|
||||
<component
|
||||
:is="icon"
|
||||
v-if="icon"
|
||||
class="svg-icons"
|
||||
:class="label ? (reverse ? 'ml-2' : 'mr-2') : ''"
|
||||
/>
|
||||
{{ label }}
|
||||
</SmartLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, defineComponent, PropType } from "vue"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
exact: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
blank: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
icon: {
|
||||
type: Object as PropType<Component | null>,
|
||||
default: null,
|
||||
},
|
||||
svg: {
|
||||
type: Object as PropType<Component | null>,
|
||||
default: null,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
reverse: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
245
packages/hoppscotch-ui/src/components/smart/AutoComplete.vue
Normal file
245
packages/hoppscotch-ui/src/components/smart/AutoComplete.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="autocomplete-wrapper">
|
||||
<input
|
||||
ref="acInput"
|
||||
:value="text"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:placeholder="placeholder"
|
||||
:spellcheck="spellcheck"
|
||||
:autocapitalize="autocapitalize"
|
||||
:class="styles"
|
||||
@input.stop="
|
||||
(e) => {
|
||||
$emit('input', e.target.value)
|
||||
updateSuggestions(e)
|
||||
}
|
||||
"
|
||||
@keyup="updateSuggestions"
|
||||
@click="updateSuggestions"
|
||||
@keydown="handleKeystroke"
|
||||
@change="$emit('change', $event)"
|
||||
/>
|
||||
<ul
|
||||
v-if="suggestions.length > 0 && suggestionsVisible"
|
||||
class="suggestions"
|
||||
:style="{ transform: `translate(${suggestionsOffsetLeft}px, 0)` }"
|
||||
>
|
||||
<li
|
||||
v-for="(suggestion, index) in suggestions"
|
||||
:key="`suggestion-${index}`"
|
||||
:class="{ active: currentSuggestionIndex === index }"
|
||||
@click.prevent="forceSuggestion(suggestion)"
|
||||
>
|
||||
{{ suggestion }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
spellcheck: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
required: false,
|
||||
},
|
||||
|
||||
autocapitalize: {
|
||||
type: String,
|
||||
default: "off",
|
||||
required: false,
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
|
||||
source: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
|
||||
styles: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
emits: ["input", "change"],
|
||||
data() {
|
||||
return {
|
||||
text: this.value,
|
||||
selectionStart: 0,
|
||||
suggestionsOffsetLeft: 0,
|
||||
currentSuggestionIndex: -1,
|
||||
suggestionsVisible: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Gets the suggestions list to be displayed under the input box.
|
||||
*
|
||||
* @returns {default.props.source|{type, required}}
|
||||
*/
|
||||
suggestions() {
|
||||
const input = this.text.substring(0, this.selectionStart)
|
||||
|
||||
return (
|
||||
this.source
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.toLowerCase().startsWith(input.toLowerCase()) &&
|
||||
input.toLowerCase() !== entry.toLowerCase()
|
||||
)
|
||||
// Cut off the part that's already been typed.
|
||||
.map((entry) => entry.substring(this.selectionStart))
|
||||
// We only want the top 10 suggestions.
|
||||
.slice(0, 10)
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(newValue) {
|
||||
this.text = newValue
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateSuggestions({
|
||||
target: this.$refs.acInput,
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateSuggestions(event) {
|
||||
// Hide suggestions if ESC pressed.
|
||||
if (event.code && event.code === "Escape") {
|
||||
event.preventDefault()
|
||||
this.suggestionsVisible = false
|
||||
this.currentSuggestionIndex = -1
|
||||
return
|
||||
}
|
||||
|
||||
// As suggestions is a reactive property, this implicitly
|
||||
// causes suggestions to update.
|
||||
this.selectionStart = this.$refs.acInput.selectionStart
|
||||
this.suggestionsOffsetLeft = 12 * this.selectionStart
|
||||
this.suggestionsVisible = true
|
||||
},
|
||||
|
||||
forceSuggestion(text) {
|
||||
const input = this.text.substring(0, this.selectionStart)
|
||||
this.text = input + text
|
||||
|
||||
this.selectionStart = this.text.length
|
||||
this.suggestionsVisible = true
|
||||
this.currentSuggestionIndex = -1
|
||||
|
||||
this.$emit("input", this.text)
|
||||
},
|
||||
|
||||
handleKeystroke(event) {
|
||||
switch (event.code) {
|
||||
case "Enter":
|
||||
event.preventDefault()
|
||||
if (this.currentSuggestionIndex > -1)
|
||||
this.forceSuggestion(
|
||||
this.suggestions.find(
|
||||
(_item, index) => index === this.currentSuggestionIndex
|
||||
)
|
||||
)
|
||||
break
|
||||
|
||||
case "ArrowUp":
|
||||
event.preventDefault()
|
||||
this.currentSuggestionIndex =
|
||||
this.currentSuggestionIndex - 1 >= 0
|
||||
? this.currentSuggestionIndex - 1
|
||||
: 0
|
||||
break
|
||||
|
||||
case "ArrowDown":
|
||||
event.preventDefault()
|
||||
this.currentSuggestionIndex =
|
||||
this.currentSuggestionIndex < this.suggestions.length - 1
|
||||
? this.currentSuggestionIndex + 1
|
||||
: this.suggestions.length - 1
|
||||
break
|
||||
|
||||
case "Tab": {
|
||||
const activeSuggestion =
|
||||
this.suggestions[
|
||||
this.currentSuggestionIndex >= 0 ? this.currentSuggestionIndex : 0
|
||||
]
|
||||
|
||||
if (!activeSuggestion) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
const input = this.text.substring(0, this.selectionStart)
|
||||
this.text = input + activeSuggestion
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.autocomplete-wrapper {
|
||||
@apply relative;
|
||||
@apply contents;
|
||||
|
||||
input:focus + ul.suggestions,
|
||||
ul.suggestions:hover {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
ul.suggestions {
|
||||
@apply hidden;
|
||||
@apply bg-popover;
|
||||
@apply absolute;
|
||||
@apply mx-2;
|
||||
@apply left-0;
|
||||
@apply z-50;
|
||||
@apply shadow-lg;
|
||||
@apply max-h-46;
|
||||
@apply overflow-y-auto;
|
||||
top: calc(100% - 4px);
|
||||
border-radius: 0 0 8px 8px;
|
||||
|
||||
li {
|
||||
@apply w-full;
|
||||
@apply block;
|
||||
@apply py-2 px-4;
|
||||
@apply text-secondary;
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
@apply bg-accentDark;
|
||||
@apply text-accentContrast;
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
70
packages/hoppscotch-ui/src/components/smart/Checkbox.vue
Normal file
70
packages/hoppscotch-ui/src/components/smart/Checkbox.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div
|
||||
class="inline-flex items-center justify-center transition cursor-pointer flex-nowrap group hover:text-secondaryDark"
|
||||
role="checkbox"
|
||||
:aria-checked="on"
|
||||
@click="emit('change')"
|
||||
>
|
||||
<input
|
||||
id="checkbox"
|
||||
type="checkbox"
|
||||
name="checkbox"
|
||||
class="checkbox"
|
||||
:checked="on"
|
||||
@change="emit('change')"
|
||||
/>
|
||||
<label
|
||||
for="checkbox"
|
||||
class="pl-0 font-semibold truncate align-middle cursor-pointer"
|
||||
>
|
||||
<slot></slot>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
on: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "change"): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.checkbox[type="checkbox"] {
|
||||
@apply appearance-none;
|
||||
@apply hidden;
|
||||
|
||||
& + label {
|
||||
@apply inline-flex items-center justify-center;
|
||||
@apply cursor-pointer;
|
||||
|
||||
&::before {
|
||||
@apply border-2 border-divider;
|
||||
@apply rounded;
|
||||
@apply group-hover: border-accentDark;
|
||||
@apply inline-flex;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply text-transparent;
|
||||
@apply h-4;
|
||||
@apply w-4;
|
||||
@apply font-icon;
|
||||
@apply mr-2;
|
||||
@apply transition;
|
||||
@apply content-["\e876"];
|
||||
}
|
||||
}
|
||||
|
||||
&:checked + label::before {
|
||||
@apply bg-accent;
|
||||
@apply border-accent;
|
||||
@apply text-accentContrast;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
72
packages/hoppscotch-ui/src/components/smart/ConfirmModal.vue
Normal file
72
packages/hoppscotch-ui/src/components/smart/ConfirmModal.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<SmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="confirm ?? t?.('modal.confirm') ?? 'Confirm'"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
{{ title }}
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<ButtonPrimary
|
||||
v-focus
|
||||
:label="yes ?? t?.('action.yes') ?? 'Yes'"
|
||||
:loading="!!loadingState"
|
||||
outline
|
||||
@click="resolve"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
:label="no ?? t?.('action.no') ?? 'No'"
|
||||
filled
|
||||
outline
|
||||
@click="hideModal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from "vue"
|
||||
import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../index"
|
||||
|
||||
const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
show: boolean
|
||||
title?: string | null
|
||||
confirm?: string | null
|
||||
yes?: string | null
|
||||
no?: string | null
|
||||
loadingState?: boolean | null
|
||||
}>(),
|
||||
{
|
||||
title: null,
|
||||
confirm: null,
|
||||
yes: null,
|
||||
no: null,
|
||||
loadingState: null,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
(e: "resolve", title: string | null): void
|
||||
}>()
|
||||
|
||||
const hideModal = () => {
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
const resolve = () => {
|
||||
emit("resolve", props.title)
|
||||
if (props.loadingState === null) emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
45
packages/hoppscotch-ui/src/components/smart/Expand.vue
Normal file
45
packages/hoppscotch-ui/src/components/smart/Expand.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative flex flex-col space-y-2 overflow-hidden"
|
||||
:class="expand ? 'h-full' : 'max-h-32'"
|
||||
>
|
||||
<slot name="body"></slot>
|
||||
<div
|
||||
class="sticky inset-x-0 bottom-0 flex items-center justify-center flex-shrink-0 overflow-x-auto"
|
||||
>
|
||||
<ButtonSecondary
|
||||
:icon="expand ? IconChevronUp : IconChevronDown"
|
||||
:label="
|
||||
expand
|
||||
? less ?? t?.('action.less') ?? 'Less'
|
||||
: more ?? t?.('action.more') ?? 'More'
|
||||
"
|
||||
filled
|
||||
rounded
|
||||
@click="expand = !expand"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconChevronUp from "~icons/lucide/chevron-up"
|
||||
import IconChevronDown from "~icons/lucide/chevron-down"
|
||||
import { inject, ref } from "vue"
|
||||
import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../index"
|
||||
|
||||
const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
|
||||
|
||||
const expand = ref(false)
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
less?: string
|
||||
more?: string
|
||||
}>(),
|
||||
{
|
||||
less: "Less",
|
||||
more: "More",
|
||||
}
|
||||
)
|
||||
</script>
|
||||
22
packages/hoppscotch-ui/src/components/smart/FileChip.vue
Normal file
22
packages/hoppscotch-ui/src/components/smart/FileChip.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<span class="chip">
|
||||
<component :is="IconFile" class="opacity-75 svg-icons" />
|
||||
<span class="px-2 truncate max-w-32"><slot></slot></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconFile from "~icons/lucide/file"
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chip {
|
||||
@apply inline-flex;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply rounded;
|
||||
@apply pl-2;
|
||||
@apply pr-0.5;
|
||||
@apply bg-primaryDark;
|
||||
}
|
||||
</style>
|
||||
38
packages/hoppscotch-ui/src/components/smart/Intersection.vue
Normal file
38
packages/hoppscotch-ui/src/components/smart/Intersection.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div ref="container">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from "vue"
|
||||
|
||||
/*
|
||||
Implements a wrapper listening to viewport intersections via
|
||||
IntersectionObserver API
|
||||
|
||||
Events
|
||||
------
|
||||
intersecting (entry: IntersectionObserverEntry) -> When the component is intersecting the viewport
|
||||
*/
|
||||
const observer = ref<IntersectionObserver>()
|
||||
const container = ref<Element>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "intersecting", entry: IntersectionObserverEntry): void
|
||||
}>()
|
||||
|
||||
onMounted(() => {
|
||||
observer.value = new IntersectionObserver(([entry]) => {
|
||||
if (entry && entry.isIntersecting) {
|
||||
emit("intersecting", entry)
|
||||
}
|
||||
})
|
||||
|
||||
observer.value.observe(container.value!)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
observer.value!.disconnect()
|
||||
})
|
||||
</script>
|
||||
140
packages/hoppscotch-ui/src/components/smart/Item.vue
Normal file
140
packages/hoppscotch-ui/src/components/smart/Item.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<SmartLink
|
||||
:to="to"
|
||||
:exact="exact"
|
||||
:blank="blank"
|
||||
class="inline-flex items-center flex-shrink-0 px-4 py-2 rounded transition hover:bg-primaryDark hover:text-secondaryDark focus:outline-none focus-visible:bg-primaryDark focus-visible:text-secondaryDark"
|
||||
:class="[
|
||||
{ 'opacity-75 cursor-not-allowed': disabled },
|
||||
{ 'pointer-events-none': loading },
|
||||
{ 'flex-1': label },
|
||||
{ 'flex-row-reverse justify-end': reverse },
|
||||
{
|
||||
'border border-divider hover:border-dividerDark focus-visible:border-dividerDark':
|
||||
outline,
|
||||
},
|
||||
]"
|
||||
:disabled="disabled"
|
||||
:tabindex="loading ? '-1' : '0'"
|
||||
role="menuitem"
|
||||
>
|
||||
<span
|
||||
v-if="!loading"
|
||||
class="inline-flex items-center"
|
||||
:class="{ 'self-start': !!infoIcon }"
|
||||
>
|
||||
<component
|
||||
:is="icon"
|
||||
v-if="icon"
|
||||
class="opacity-75 svg-icons"
|
||||
:class="[
|
||||
label ? (reverse ? 'ml-4' : 'mr-4') : '',
|
||||
{ 'text-accent': active },
|
||||
]"
|
||||
/>
|
||||
</span>
|
||||
<SmartSpinner v-else class="mr-4 text-secondaryDark" />
|
||||
<div
|
||||
class="inline-flex items-start flex-1 truncate"
|
||||
:class="{ 'flex-col': description }"
|
||||
>
|
||||
<div class="font-semibold truncate">
|
||||
{{ label }}
|
||||
</div>
|
||||
<p v-if="description" class="my-2 text-left text-secondaryLight">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
<component
|
||||
:is="infoIcon"
|
||||
v-if="infoIcon"
|
||||
class="items-center self-center ml-4 svg-icons"
|
||||
:class="{ 'text-accent': activeInfoIcon }"
|
||||
/>
|
||||
<div v-if="shortcut.length" class="ml-4 <sm:hidden font-medium">
|
||||
<kbd
|
||||
v-for="(key, index) in shortcut"
|
||||
:key="`key-${index}`"
|
||||
class="-mr-2 shortcut-key"
|
||||
>
|
||||
{{ key }}
|
||||
</kbd>
|
||||
</div>
|
||||
</SmartLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
to: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
exact: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
blank: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* This will be a component!
|
||||
*/
|
||||
icon: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
/**
|
||||
* This will be a component!
|
||||
*/
|
||||
svg: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
reverse: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
outline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
shortcut: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
activeInfoIcon: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* This will be a component!
|
||||
*/
|
||||
infoIcon: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
90
packages/hoppscotch-ui/src/components/smart/Link.vue
Normal file
90
packages/hoppscotch-ui/src/components/smart/Link.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<button
|
||||
v-if="renderedTag === 'BUTTON'"
|
||||
aria-label="button"
|
||||
role="button"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot></slot>
|
||||
</button>
|
||||
<a
|
||||
v-else-if="renderedTag === 'ANCHOR' && !blank"
|
||||
aria-label="Link"
|
||||
:href="to"
|
||||
role="link"
|
||||
v-bind="updatedAttrs"
|
||||
>
|
||||
<slot></slot>
|
||||
</a>
|
||||
<a
|
||||
v-else-if="renderedTag === 'ANCHOR' && blank"
|
||||
aria-label="Link"
|
||||
:href="to"
|
||||
role="link"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
v-bind="updatedAttrs"
|
||||
>
|
||||
<slot></slot>
|
||||
</a>
|
||||
<RouterLink v-else :to="to" v-bind="updatedAttrs">
|
||||
<slot></slot>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/**
|
||||
* for preventing the automatic binding of $attrs.
|
||||
* we are manually binding $attrs or updatedAttrs.
|
||||
* if this is not set to false, along with manually binded updatedAttrs, it will also bind $attrs.
|
||||
*/
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useAttrs } from "vue"
|
||||
import { RouterLink } from "vue-router"
|
||||
import { omit } from "lodash-es"
|
||||
|
||||
const props = defineProps({
|
||||
to: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
blank: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const renderedTag = computed(() => {
|
||||
if (!props.to) {
|
||||
return "BUTTON" as const
|
||||
} else if (props.blank) {
|
||||
return "ANCHOR" as const
|
||||
} else if (/^\/(?!\/).*$/.test(props.to)) {
|
||||
// regex101.com/r/LU1iFL/1
|
||||
return "FRAMEWORK" as const
|
||||
} else {
|
||||
return "ANCHOR" as const
|
||||
}
|
||||
})
|
||||
|
||||
const $attrs = useAttrs()
|
||||
|
||||
/**
|
||||
* tippy checks if the disabled attribute exists on the anchor tag, if it exists it won't show the tooltip.
|
||||
* and when directly binding the disabled attribute using v-bind="attrs",
|
||||
* vue renders the disabled attribute as disabled="false" ("false" being a string),
|
||||
* which causes tippy to think the disabled attribute is present, ( it does a targetElement.hasAttribute("disabled") check ) and it won't show the tooltip.
|
||||
*
|
||||
* here we are just omiting disabled if it is false.
|
||||
*/
|
||||
const updatedAttrs = computed(() =>
|
||||
renderedTag.value === "ANCHOR" && !$attrs.disabled
|
||||
? omit($attrs, "disabled")
|
||||
: $attrs
|
||||
)
|
||||
</script>
|
||||
219
packages/hoppscotch-ui/src/components/smart/Modal.vue
Normal file
219
packages/hoppscotch-ui/src/components/smart/Modal.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<Transition name="fade" appear @leave="onTransitionLeaveStart">
|
||||
<div
|
||||
ref="modal"
|
||||
class="fixed inset-0 z-50 overflow-y-auto transition"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="flex items-end justify-center min-h-screen text-center sm:block"
|
||||
>
|
||||
<Transition name="fade" appear>
|
||||
<div
|
||||
class="fixed inset-0 transition-opacity"
|
||||
@touchstart="!dialog ? close() : null"
|
||||
@touchend="!dialog ? close() : null"
|
||||
@mouseup="!dialog ? close() : null"
|
||||
@mousedown="!dialog ? close() : null"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 opacity-80 bg-primaryLight focus:outline-none"
|
||||
tabindex="0"
|
||||
@click="!dialog ? close() : null"
|
||||
></div>
|
||||
</div>
|
||||
</Transition>
|
||||
<span
|
||||
v-if="placement === 'center'"
|
||||
class="sm:h-screen <sm:hidden sm:align-middle"
|
||||
aria-hidden="true"
|
||||
>​</span
|
||||
>
|
||||
<Transition name="bounce" appear>
|
||||
<div
|
||||
class="inline-block w-full overflow-hidden text-left align-bottom transition-all transform shadow-lg sm:border border-dividerDark bg-primary sm:rounded-xl sm:align-middle"
|
||||
:class="[{ 'mt-24 md:mb-8': placement === 'top' }, styles]"
|
||||
>
|
||||
<div
|
||||
v-if="title"
|
||||
class="flex items-center justify-between border-b border-dividerLight"
|
||||
:class="{ 'p-4': !fullWidth }"
|
||||
>
|
||||
<h3 class="heading" :class="{ 'ml-4': !fullWidth }">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<span class="flex items-center">
|
||||
<slot name="actions"></slot>
|
||||
<kbd class="mr-2 shortcut-key">ESC</kbd>
|
||||
<ButtonSecondary
|
||||
v-if="dimissible"
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
||||
:title="closeText ?? t?.('action.close') ?? 'Close'"
|
||||
:icon="IconX"
|
||||
@click="close"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col overflow-y-auto max-h-lg"
|
||||
:class="{ 'p-4': !fullWidth }"
|
||||
>
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasFooterSlot"
|
||||
class="flex items-center justify-between flex-1 border-t border-dividerLight bg-primaryContrast"
|
||||
:class="{ 'p-4': !fullWidth }"
|
||||
>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
const PORTAL_DOM_ID = "hoppscotch-modal-portal"
|
||||
|
||||
// An ID ticker for generating consistently unique modal IDs
|
||||
let stackIDTicker = 0
|
||||
|
||||
// Why ?
|
||||
const stack = (() => {
|
||||
const stack: number[] = []
|
||||
return {
|
||||
push: stack.push.bind(stack),
|
||||
pop: stack.pop.bind(stack),
|
||||
peek: () => (stack.length === 0 ? undefined : stack[stack.length - 1]),
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconX from "~icons/lucide/x"
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
useSlots,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
inject,
|
||||
} from "vue"
|
||||
import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../index"
|
||||
|
||||
const { t, onModalOpen, onModalClose } =
|
||||
inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
|
||||
|
||||
defineProps({
|
||||
dialog: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
dimissible: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
default: "top",
|
||||
},
|
||||
fullWidth: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
styles: {
|
||||
type: String,
|
||||
default: "sm:max-w-lg",
|
||||
},
|
||||
closeText: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "close"): void
|
||||
}>()
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
onModalClose?.()
|
||||
})
|
||||
|
||||
const stackId = ref(stackIDTicker++)
|
||||
const shouldCleanupDomOnUnmount = ref(true)
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const hasFooterSlot = computed(() => {
|
||||
return !!slots.footer
|
||||
})
|
||||
|
||||
const modal = ref<Element>()
|
||||
|
||||
onMounted(() => {
|
||||
const portal = getPortal()
|
||||
portal.appendChild(modal.value!)
|
||||
stack.push(stackId.value)
|
||||
document.addEventListener("keydown", onKeyDown)
|
||||
|
||||
onModalOpen?.()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (shouldCleanupDomOnUnmount.value && modal.value) {
|
||||
getPortal().removeChild(modal.value)
|
||||
}
|
||||
stack.pop()
|
||||
document.removeEventListener("keydown", onKeyDown)
|
||||
})
|
||||
|
||||
const close = () => {
|
||||
emit("close")
|
||||
}
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && stackId.value === stack.peek()) {
|
||||
e.preventDefault()
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
const onTransitionLeaveStart = () => {
|
||||
close()
|
||||
shouldCleanupDomOnUnmount.value = false
|
||||
}
|
||||
|
||||
const getPortal = () => {
|
||||
let el = document.querySelector("#" + PORTAL_DOM_ID)
|
||||
if (el) {
|
||||
return el
|
||||
}
|
||||
el = document.createElement("DIV")
|
||||
el.id = PORTAL_DOM_ID
|
||||
document.body.appendChild(el)
|
||||
return el
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bounce-enter-active {
|
||||
@apply transition;
|
||||
animation: bounce-in 150ms;
|
||||
}
|
||||
|
||||
@keyframes bounce-in {
|
||||
0% {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
57
packages/hoppscotch-ui/src/components/smart/ProgressRing.vue
Normal file
57
packages/hoppscotch-ui/src/components/smart/ProgressRing.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<svg :height="radius * 2" :width="radius * 2">
|
||||
<circle
|
||||
:stroke-width="stroke"
|
||||
class="stroke-green-500"
|
||||
fill="transparent"
|
||||
:r="normalizedRadius"
|
||||
:cx="radius"
|
||||
:cy="radius"
|
||||
/>
|
||||
<circle
|
||||
:stroke-width="stroke"
|
||||
stroke="currentColor"
|
||||
fill="transparent"
|
||||
:r="normalizedRadius"
|
||||
:cx="radius"
|
||||
:cy="radius"
|
||||
:style="{ strokeDashoffset: strokeDashoffset }"
|
||||
:stroke-dasharray="circumference + ' ' + circumference"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
radius: {
|
||||
type: Number,
|
||||
default: 12,
|
||||
},
|
||||
progress: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
},
|
||||
stroke: {
|
||||
type: Number,
|
||||
default: 4,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const normalizedRadius = this.radius - this.stroke * 2
|
||||
const circumference = normalizedRadius * 2 * Math.PI
|
||||
|
||||
return {
|
||||
normalizedRadius,
|
||||
circumference,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
strokeDashoffset() {
|
||||
return this.circumference - (this.progress / 100) * this.circumference
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
34
packages/hoppscotch-ui/src/components/smart/Radio.vue
Normal file
34
packages/hoppscotch-ui/src/components/smart/Radio.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<SmartItem
|
||||
:label="label"
|
||||
:icon="selected ? IconCircleDot : IconCircle"
|
||||
:active="selected"
|
||||
role="radio"
|
||||
:aria-checked="selected"
|
||||
@click="emit('change', value)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconCircleDot from "~icons/lucide/circle-dot"
|
||||
import IconCircle from "~icons/lucide/circle"
|
||||
|
||||
defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "change", value: string): void
|
||||
}>()
|
||||
</script>
|
||||
26
packages/hoppscotch-ui/src/components/smart/RadioGroup.vue
Normal file
26
packages/hoppscotch-ui/src/components/smart/RadioGroup.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<SmartRadio
|
||||
v-for="(radio, index) in radios"
|
||||
:key="`radio-${index}`"
|
||||
:value="radio.value"
|
||||
:label="radio.label"
|
||||
:selected="modelValue === radio.value"
|
||||
@change="emit('update:modelValue', radio.value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: string): void
|
||||
}>()
|
||||
|
||||
defineProps<{
|
||||
radios: Array<{
|
||||
value: string // The key of the radio option
|
||||
label: string
|
||||
}>
|
||||
modelValue: string // Should be a radio key given in the radios array
|
||||
}>()
|
||||
</script>
|
||||
66
packages/hoppscotch-ui/src/components/smart/SlideOver.vue
Normal file
66
packages/hoppscotch-ui/src/components/smart/SlideOver.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div>
|
||||
<Transition name="fade" appear>
|
||||
<div
|
||||
v-if="show"
|
||||
class="fixed inset-0 z-20 transition-opacity"
|
||||
@keydown.esc="close()"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-primaryLight opacity-90 focus:outline-none"
|
||||
tabindex="0"
|
||||
@click="close()"
|
||||
></div>
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition name="slide" appear>
|
||||
<aside
|
||||
v-if="show"
|
||||
class="fixed top-0 right-0 z-30 flex flex-col h-full max-w-full overflow-auto border-l shadow-xl border-dividerDark bg-primary w-96"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between p-2 border-b border-dividerLight"
|
||||
>
|
||||
<h3 class="ml-4 heading">{{ title }}</h3>
|
||||
<span class="flex items-center">
|
||||
<kbd class="mr-2 shortcut-key">ESC</kbd>
|
||||
<ButtonSecondary :icon="IconX" @click="close()" />
|
||||
</span>
|
||||
</div>
|
||||
<slot name="content"></slot>
|
||||
</aside>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch } from "vue"
|
||||
import IconX from "~icons/lucide/x"
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
title: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "close"): void
|
||||
}>()
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) document.body.style.setProperty("overflow", "hidden")
|
||||
else document.body.style.removeProperty("overflow")
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.keyCode === 27 && props.show) close()
|
||||
})
|
||||
})
|
||||
|
||||
const close = () => {
|
||||
emit("close")
|
||||
}
|
||||
</script>
|
||||
3
packages/hoppscotch-ui/src/components/smart/Spinner.vue
Normal file
3
packages/hoppscotch-ui/src/components/smart/Spinner.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<icon-lucide-loader class="animate-spin svg-icons" />
|
||||
</template>
|
||||
78
packages/hoppscotch-ui/src/components/smart/Tab.vue
Normal file
78
packages/hoppscotch-ui/src/components/smart/Tab.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div v-if="shouldRender" v-show="active" class="flex flex-col flex-1">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
inject,
|
||||
computed,
|
||||
watch,
|
||||
Component,
|
||||
markRaw,
|
||||
} from "vue"
|
||||
import { TabMeta, TabProvider } from "./Tabs.vue"
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id: string
|
||||
label: string
|
||||
icon?: Component | object | string | null
|
||||
info?: string | null
|
||||
indicator?: boolean
|
||||
disabled?: boolean
|
||||
}>(),
|
||||
{
|
||||
icon: null,
|
||||
indicator: false,
|
||||
info: null,
|
||||
disabled: false,
|
||||
}
|
||||
)
|
||||
|
||||
const tabMeta = computed<TabMeta>(() => ({
|
||||
// props.icon can store a component, which should not be made deeply reactive
|
||||
icon:
|
||||
props.icon && typeof props.icon === "object"
|
||||
? markRaw(props.icon)
|
||||
: props.icon,
|
||||
|
||||
indicator: props.indicator,
|
||||
info: props.info,
|
||||
label: props.label,
|
||||
disabled: props.disabled,
|
||||
}))
|
||||
|
||||
const {
|
||||
activeTabID,
|
||||
renderInactive,
|
||||
addTabEntry,
|
||||
updateTabEntry,
|
||||
removeTabEntry,
|
||||
} = inject<TabProvider>("tabs-system")!
|
||||
|
||||
const active = computed(() => activeTabID.value === props.id)
|
||||
|
||||
const shouldRender = computed(() => {
|
||||
// If render inactive is true, then it should be rendered nonetheless
|
||||
if (renderInactive.value) return true
|
||||
|
||||
// Else, return whatever is the active state
|
||||
return active.value
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
addTabEntry(props.id, tabMeta.value)
|
||||
})
|
||||
|
||||
watch(tabMeta, (newMeta) => {
|
||||
updateTabEntry(props.id, newMeta)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
removeTabEntry(props.id)
|
||||
})
|
||||
</script>
|
||||
251
packages/hoppscotch-ui/src/components/smart/Tabs.vue
Normal file
251
packages/hoppscotch-ui/src/components/smart/Tabs.vue
Normal file
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-1 h-full flex-nowrap"
|
||||
:class="{ 'flex-col h-auto': !vertical }"
|
||||
>
|
||||
<div
|
||||
class="relative tabs border-dividerLight"
|
||||
:class="[vertical ? 'border-r' : 'border-b', styles]"
|
||||
>
|
||||
<div class="flex flex-1">
|
||||
<div
|
||||
class="flex justify-between flex-1"
|
||||
:class="{ 'flex-col': vertical }"
|
||||
>
|
||||
<div class="flex" :class="{ 'flex-col space-y-2 p-2': vertical }">
|
||||
<button
|
||||
v-for="([tabID, tabMeta], index) in tabEntries"
|
||||
:key="`tab-${index}`"
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
placement: 'left',
|
||||
content: vertical ? tabMeta.label : null,
|
||||
}"
|
||||
class="tab"
|
||||
:class="[
|
||||
{ active: modelValue === tabID },
|
||||
{ vertical: vertical },
|
||||
{ 'opacity-75 !cursor-not-allowed': tabMeta.disabled },
|
||||
]"
|
||||
:aria-label="tabMeta.label || ''"
|
||||
:disabled="tabMeta.disabled"
|
||||
role="button"
|
||||
@keyup.enter="selectTab(tabID)"
|
||||
@click="selectTab(tabID)"
|
||||
>
|
||||
<component
|
||||
:is="tabMeta.icon"
|
||||
v-if="tabMeta.icon"
|
||||
class="svg-icons"
|
||||
/>
|
||||
<span v-else-if="tabMeta.label">{{ tabMeta.label }}</span>
|
||||
<span
|
||||
v-if="tabMeta.info && tabMeta.info !== 'null'"
|
||||
class="tab-info"
|
||||
>
|
||||
{{ tabMeta.info }}
|
||||
</span>
|
||||
<span
|
||||
v-if="tabMeta.indicator"
|
||||
class="w-1 h-1 ml-2 rounded-full bg-accentLight"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="w-full h-full contents"
|
||||
:class="[
|
||||
{
|
||||
'!flex flex-col flex-1 overflow-y-auto': vertical,
|
||||
},
|
||||
contentStyles,
|
||||
]"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { not } from "fp-ts/Predicate"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as O from "fp-ts/Option"
|
||||
import type { Component } from "vue"
|
||||
import { ref, ComputedRef, computed, provide } from "vue"
|
||||
|
||||
export type TabMeta = {
|
||||
label: string | null
|
||||
icon: string | Component | null
|
||||
indicator: boolean
|
||||
info: string | null
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
export type TabProvider = {
|
||||
// Whether inactive tabs should remain rendered
|
||||
renderInactive: ComputedRef<boolean>
|
||||
activeTabID: ComputedRef<string>
|
||||
addTabEntry: (tabID: string, meta: TabMeta) => void
|
||||
updateTabEntry: (tabID: string, newMeta: TabMeta) => void
|
||||
removeTabEntry: (tabID: string) => void
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
styles: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
renderInactiveTabs: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
contentStyles: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", newTabID: string): void
|
||||
}>()
|
||||
|
||||
const throwError = (message: string): never => {
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
const tabEntries = ref<Array<[string, TabMeta]>>([])
|
||||
|
||||
const addTabEntry = (tabID: string, meta: TabMeta) => {
|
||||
tabEntries.value = pipe(
|
||||
tabEntries.value,
|
||||
O.fromPredicate(not(A.exists(([id]) => id === tabID))),
|
||||
O.map(A.append([tabID, meta] as [string, TabMeta])),
|
||||
O.getOrElseW(() => throwError(`Tab with duplicate ID created: '${tabID}'`))
|
||||
)
|
||||
}
|
||||
|
||||
const updateTabEntry = (tabID: string, newMeta: TabMeta) => {
|
||||
tabEntries.value = pipe(
|
||||
tabEntries.value,
|
||||
A.findIndex(([id]) => id === tabID),
|
||||
O.chain((index) =>
|
||||
pipe(
|
||||
tabEntries.value,
|
||||
A.updateAt(index, [tabID, newMeta] as [string, TabMeta])
|
||||
)
|
||||
),
|
||||
O.getOrElseW(() => throwError(`Failed to update tab entry: ${tabID}`))
|
||||
)
|
||||
}
|
||||
|
||||
const removeTabEntry = (tabID: string) => {
|
||||
tabEntries.value = pipe(
|
||||
tabEntries.value,
|
||||
A.findIndex(([id]) => id === tabID),
|
||||
O.chain((index) => pipe(tabEntries.value, A.deleteAt(index))),
|
||||
O.getOrElseW(() => throwError(`Failed to remove tab entry: ${tabID}`))
|
||||
)
|
||||
|
||||
// If we tried to remove the active tabEntries, switch to first tab entry
|
||||
if (props.modelValue === tabID)
|
||||
if (tabEntries.value.length > 0) selectTab(tabEntries.value[0][0])
|
||||
}
|
||||
|
||||
provide<TabProvider>("tabs-system", {
|
||||
renderInactive: computed(() => props.renderInactiveTabs),
|
||||
activeTabID: computed(() => props.modelValue),
|
||||
addTabEntry,
|
||||
updateTabEntry,
|
||||
removeTabEntry,
|
||||
})
|
||||
|
||||
const selectTab = (id: string) => {
|
||||
emit("update:modelValue", id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tabs {
|
||||
@apply flex;
|
||||
@apply whitespace-nowrap;
|
||||
@apply overflow-auto;
|
||||
@apply flex-shrink-0;
|
||||
|
||||
.tab {
|
||||
@apply relative;
|
||||
@apply flex;
|
||||
@apply flex-shrink-0;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply py-2 px-4;
|
||||
@apply text-secondary;
|
||||
@apply font-semibold;
|
||||
@apply cursor-pointer;
|
||||
@apply hover: text-secondaryDark;
|
||||
@apply focus: outline-none;
|
||||
@apply focus-visible: text-secondaryDark;
|
||||
@apply after:absolute;
|
||||
@apply after:left-4;
|
||||
@apply after:right-4;
|
||||
@apply after:bottom-0;
|
||||
@apply after:bg-transparent;
|
||||
@apply after:z-2;
|
||||
@apply after:h-0.5;
|
||||
@apply after:content-DEFAULT;
|
||||
@apply focus: after: bg-divider;
|
||||
|
||||
.tab-info {
|
||||
@apply inline-flex;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply w-5;
|
||||
@apply h-4;
|
||||
@apply ml-2;
|
||||
@apply text-8px;
|
||||
@apply border border-divider;
|
||||
@apply rounded;
|
||||
@apply text-secondaryLight;
|
||||
}
|
||||
|
||||
&.active {
|
||||
@apply text-secondaryDark;
|
||||
@apply after:bg-accent;
|
||||
|
||||
.tab-info {
|
||||
@apply text-secondary;
|
||||
@apply border-dividerDark;
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
@apply p-2;
|
||||
@apply rounded;
|
||||
@apply focus: after: hidden;
|
||||
|
||||
&.active {
|
||||
@apply text-accent;
|
||||
@apply after:hidden;
|
||||
|
||||
.tab-info {
|
||||
@apply text-secondary;
|
||||
@apply border-dividerDark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
89
packages/hoppscotch-ui/src/components/smart/Toggle.vue
Normal file
89
packages/hoppscotch-ui/src/components/smart/Toggle.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div
|
||||
class="inline-flex items-center justify-center cursor-pointer transition flex-nowrap group hover:text-secondaryDark rounded py-0.5 px-1 -my-0.5 -mx-1 focus:outline-none focus-visible:ring focus-visible:ring-accent focus-visible:text-secondaryDark"
|
||||
tabindex="0"
|
||||
@click="emit('change')"
|
||||
@keyup.enter="emit('change')"
|
||||
>
|
||||
<span ref="toggle" class="toggle" :class="{ on: on }">
|
||||
<span class="handle"></span>
|
||||
</span>
|
||||
<span class="pl-0 font-semibold truncate align-middle cursor-pointer">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
on: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "change"): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$useBorder: true;
|
||||
$borderColor: var(--divider-color);
|
||||
$activeColor: var(--divider-dark-color);
|
||||
$inactiveColor: var(--divider-color);
|
||||
$inactiveHandleColor: var(--secondary-light-color);
|
||||
$activeHandleColor: var(--accent-color);
|
||||
$width: 1.6rem;
|
||||
$height: 0.6rem;
|
||||
$indicatorHeight: 0.4rem;
|
||||
$indicatorWidth: 0.4rem;
|
||||
$handleSpacing: 0.1rem;
|
||||
$transition: all 0.2s ease-in-out;
|
||||
|
||||
.toggle {
|
||||
@apply relative;
|
||||
@apply flex;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply rounded-full;
|
||||
@apply p-0;
|
||||
@apply mr-4;
|
||||
@apply cursor-pointer;
|
||||
@apply flex-shrink-0;
|
||||
@apply transition;
|
||||
@apply group-hover: border-accentDark;
|
||||
@apply focus: outline-none;
|
||||
@apply focus-visible: border-accentDark;
|
||||
width: $width;
|
||||
height: $height;
|
||||
border: if($useBorder, 2px solid $borderColor, none);
|
||||
background-color: if($useBorder, transparent, $inactiveColor);
|
||||
box-sizing: initial;
|
||||
|
||||
.handle {
|
||||
@apply absolute;
|
||||
@apply flex;
|
||||
@apply flex-shrink-0;
|
||||
@apply inset-0;
|
||||
@apply rounded-full;
|
||||
@apply pointer-events-none;
|
||||
transition: $transition;
|
||||
margin: $handleSpacing;
|
||||
background-color: $inactiveHandleColor;
|
||||
width: $indicatorWidth;
|
||||
height: $indicatorHeight;
|
||||
}
|
||||
|
||||
&.on {
|
||||
// background-color: $activeColor;
|
||||
border-color: $activeColor;
|
||||
@apply focus-visible: border-accentDark;
|
||||
|
||||
.handle {
|
||||
background-color: $activeHandleColor;
|
||||
left: #{$width - $height};
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
49
packages/hoppscotch-ui/src/components/smart/Window.vue
Normal file
49
packages/hoppscotch-ui/src/components/smart/Window.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div v-show="active" class="flex flex-col flex-1 overflow-y-auto">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
inject,
|
||||
computed,
|
||||
watch,
|
||||
useSlots,
|
||||
} from "vue"
|
||||
import { TabMeta, TabProvider } from "./Windows.vue"
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const props = defineProps({
|
||||
label: { type: String, default: null },
|
||||
info: { type: String, default: null },
|
||||
id: { type: String, default: null, required: true },
|
||||
isRemovable: { type: Boolean, default: true },
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
const tabMeta = computed<TabMeta>(() => ({
|
||||
info: props.info,
|
||||
label: props.label,
|
||||
isRemovable: props.isRemovable,
|
||||
icon: slots.icon,
|
||||
}))
|
||||
const { activeTabID, addTabEntry, updateTabEntry, removeTabEntry } =
|
||||
inject<TabProvider>("tabs-system")!
|
||||
const active = computed(() => activeTabID.value === props.id)
|
||||
|
||||
onMounted(() => {
|
||||
addTabEntry(props.id, tabMeta.value)
|
||||
})
|
||||
watch(tabMeta, (newMeta) => {
|
||||
updateTabEntry(props.id, newMeta)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
removeTabEntry(props.id)
|
||||
})
|
||||
</script>
|
||||
263
packages/hoppscotch-ui/src/components/smart/Windows.vue
Normal file
263
packages/hoppscotch-ui/src/components/smart/Windows.vue
Normal file
@@ -0,0 +1,263 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 h-auto overflow-y-hidden flex-nowrap">
|
||||
<div
|
||||
class="relative sticky top-0 z-10 flex-shrink-0 overflow-x-auto tabs bg-primaryLight"
|
||||
>
|
||||
<div class="flex flex-1 flex-shrink-0 w-0 overflow-x-auto">
|
||||
<div class="flex justify-between divide-x divide-dividerLight">
|
||||
<div class="flex">
|
||||
<draggable
|
||||
v-bind="dragOptions"
|
||||
:list="tabEntries"
|
||||
:style="tabStyles"
|
||||
:item-key="'window-'"
|
||||
class="flex flex-shrink-0 overflow-x-auto transition divide-x divide-dividerLight"
|
||||
@sort="sortTabs"
|
||||
>
|
||||
<template #item="{ element: [tabID, tabMeta] }">
|
||||
<button
|
||||
:key="`removable-tab-${tabID}`"
|
||||
class="tab"
|
||||
:class="[{ active: modelValue === tabID }]"
|
||||
:aria-label="tabMeta.label || ''"
|
||||
role="button"
|
||||
@keyup.enter="selectTab(tabID)"
|
||||
@click="selectTab(tabID)"
|
||||
>
|
||||
<div class="flex items-stretch truncate">
|
||||
<span
|
||||
v-if="tabMeta.icon"
|
||||
class="flex items-center justify-center mx-4 cursor-pointer"
|
||||
>
|
||||
<component :is="tabMeta.icon" class="w-4 h-4 svg-icons" />
|
||||
</span>
|
||||
<span class="truncate">
|
||||
{{ tabMeta.label }}
|
||||
</span>
|
||||
</div>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
||||
:icon="IconX"
|
||||
:style="{
|
||||
visibility: tabMeta.isRemovable ? 'visible' : 'hidden',
|
||||
}"
|
||||
:title="closeText ?? t?.('action.close') ?? 'Close'"
|
||||
:class="[{ active: modelValue === tabID }, 'close']"
|
||||
class="mx-2 !p-0.5"
|
||||
@click.stop="emit('removeTab', tabID)"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<div
|
||||
class="sticky right-0 flex items-center justify-center flex-shrink-0 overflow-x-auto z-8"
|
||||
>
|
||||
<slot name="actions">
|
||||
<span
|
||||
v-if="canAddNewTab"
|
||||
class="flex items-center justify-center px-2 py-1.5 bg-primaryLight z-8"
|
||||
>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="newText ?? t?.('action.new') ?? 'New'"
|
||||
:icon="IconPlus"
|
||||
class="rounded !p-1"
|
||||
filled
|
||||
@click="addTab"
|
||||
/>
|
||||
</span>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-full contents">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconX from "~icons/lucide/x"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { not } from "fp-ts/Predicate"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { ref, ComputedRef, computed, provide, inject } from "vue"
|
||||
import type { Slot } from "vue"
|
||||
import draggable from "vuedraggable"
|
||||
import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../index"
|
||||
|
||||
export type TabMeta = {
|
||||
label: string | null
|
||||
icon: Slot | undefined
|
||||
info: string | null
|
||||
isRemovable: boolean
|
||||
}
|
||||
export type TabProvider = {
|
||||
activeTabID: ComputedRef<string>
|
||||
addTabEntry: (tabID: string, meta: TabMeta) => void
|
||||
updateTabEntry: (tabID: string, newMeta: TabMeta) => void
|
||||
removeTabEntry: (tabID: string) => void
|
||||
}
|
||||
|
||||
const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
|
||||
|
||||
const props = defineProps({
|
||||
styles: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
canAddNewTab: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
newText: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
closeText: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", newTabID: string): void
|
||||
(e: "sort", body: { oldIndex: number; newIndex: number }): void
|
||||
(e: "removeTab", tabID: string): void
|
||||
(e: "addTab"): void
|
||||
}>()
|
||||
|
||||
const throwError = (message: string): never => {
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
const tabEntries = ref<Array<[string, TabMeta]>>([])
|
||||
const tabStyles = computed(() => ({
|
||||
maxWidth: `${tabEntries.value.length * 184}px`,
|
||||
width: "100%",
|
||||
minWidth: "0px",
|
||||
// transition: "max-width 0.2s",
|
||||
}))
|
||||
const dragOptions = {
|
||||
group: "tabs",
|
||||
animation: 250,
|
||||
handle: ".tab",
|
||||
draggable: ".tab",
|
||||
ghostClass: "cursor-move",
|
||||
}
|
||||
const addTabEntry = (tabID: string, meta: TabMeta) => {
|
||||
tabEntries.value = pipe(
|
||||
tabEntries.value,
|
||||
O.fromPredicate(not(A.exists(([id]) => id === tabID))),
|
||||
O.map(A.append([tabID, meta] as [string, TabMeta])),
|
||||
O.getOrElseW(() => throwError(`Tab with duplicate ID created: '${tabID}'`))
|
||||
)
|
||||
}
|
||||
const updateTabEntry = (tabID: string, newMeta: TabMeta) => {
|
||||
tabEntries.value = pipe(
|
||||
tabEntries.value,
|
||||
A.findIndex(([id]) => id === tabID),
|
||||
O.chain((index) =>
|
||||
pipe(
|
||||
tabEntries.value,
|
||||
A.updateAt(index, [tabID, newMeta] as [string, TabMeta])
|
||||
)
|
||||
),
|
||||
O.getOrElseW(() => throwError(`Failed to update tab entry: ${tabID}`))
|
||||
)
|
||||
}
|
||||
const removeTabEntry = (tabID: string) => {
|
||||
tabEntries.value = pipe(
|
||||
tabEntries.value,
|
||||
A.findIndex(([id]) => id === tabID),
|
||||
O.chain((index) => pipe(tabEntries.value, A.deleteAt(index))),
|
||||
O.getOrElseW(() => throwError(`Failed to remove tab entry: ${tabID}`))
|
||||
)
|
||||
// If we tried to remove the active tabEntries, switch to first tab entry
|
||||
if (props.modelValue === tabID)
|
||||
if (tabEntries.value.length > 0) selectTab(tabEntries.value[0][0])
|
||||
}
|
||||
const sortTabs = (e: {
|
||||
oldDraggableIndex: number
|
||||
newDraggableIndex: number
|
||||
}) => {
|
||||
emit("sort", {
|
||||
oldIndex: e.oldDraggableIndex,
|
||||
newIndex: e.newDraggableIndex,
|
||||
})
|
||||
}
|
||||
provide<TabProvider>("tabs-system", {
|
||||
activeTabID: computed(() => props.modelValue),
|
||||
addTabEntry,
|
||||
updateTabEntry,
|
||||
removeTabEntry,
|
||||
})
|
||||
const selectTab = (id: string) => {
|
||||
emit("update:modelValue", id)
|
||||
}
|
||||
const addTab = () => {
|
||||
emit("addTab")
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tabs {
|
||||
@apply flex;
|
||||
@apply whitespace-nowrap;
|
||||
@apply overflow-auto;
|
||||
@apply flex-shrink-0;
|
||||
@apply after:absolute;
|
||||
@apply after:inset-x-0;
|
||||
@apply after:bottom-0;
|
||||
@apply after:bg-dividerLight;
|
||||
@apply after:z-10;
|
||||
@apply after:h-0.25;
|
||||
@apply after:content-DEFAULT;
|
||||
|
||||
.tab {
|
||||
@apply relative;
|
||||
@apply flex;
|
||||
@apply py-2;
|
||||
@apply font-semibold;
|
||||
@apply w-46;
|
||||
@apply transition;
|
||||
@apply flex-1;
|
||||
@apply items-center;
|
||||
@apply justify-between;
|
||||
@apply text-secondaryLight;
|
||||
@apply hover:bg-primaryDark;
|
||||
@apply hover:text-secondary;
|
||||
@apply focus-visible:text-secondaryDark;
|
||||
@apply before:absolute;
|
||||
@apply before:left-0;
|
||||
@apply before:right-0;
|
||||
@apply before:top-0;
|
||||
@apply before:bg-transparent;
|
||||
@apply before:z-2;
|
||||
@apply before:h-0.5;
|
||||
@apply before:content-DEFAULT;
|
||||
@apply focus: before: bg-divider;
|
||||
|
||||
&.active {
|
||||
@apply text-secondaryDark;
|
||||
@apply bg-primary;
|
||||
@apply before:bg-accent;
|
||||
}
|
||||
|
||||
.close {
|
||||
@apply opacity-50;
|
||||
|
||||
&.active {
|
||||
@apply opacity-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
8
packages/hoppscotch-ui/src/env.d.ts
vendored
Normal file
8
packages/hoppscotch-ui/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue" {
|
||||
import { DefineComponent } from "vue"
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
33
packages/hoppscotch-ui/src/index.ts
Normal file
33
packages/hoppscotch-ui/src/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Plugin } from "vue"
|
||||
|
||||
import "./assets/scss/styles.scss"
|
||||
import "virtual:windi.css"
|
||||
|
||||
/**
|
||||
@constant HOPP_UI_OPTIONS
|
||||
@type {string}
|
||||
A constant representing the key for storing HoppUI plugin options in the global context.
|
||||
*/
|
||||
|
||||
export const HOPP_UI_OPTIONS = "HOPP_UI_OPTIONS"
|
||||
|
||||
/**
|
||||
@typedef {Object} HoppUIPluginOptions
|
||||
@property {Function} [t] - A function for handling translations for the plugin.
|
||||
@property {Function} [onModalOpen] - A callback function that is called when a modal is opened.
|
||||
@property {Function} [onModalClose] - A callback function that is called when a modal is closed.
|
||||
*/
|
||||
|
||||
export type HoppUIPluginOptions = {
|
||||
t?: (key: string) => string
|
||||
onModalOpen?: () => void
|
||||
onModalClose?: () => void
|
||||
}
|
||||
|
||||
const plugin: Plugin = {
|
||||
install(app, options: HoppUIPluginOptions = {}) {
|
||||
app.provide(HOPP_UI_OPTIONS, options)
|
||||
},
|
||||
}
|
||||
|
||||
export default plugin
|
||||
20
packages/hoppscotch-ui/src/stories/Anchor.story.vue
Normal file
20
packages/hoppscotch-ui/src/stories/Anchor.story.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<Story title="Anchor">
|
||||
<div class="text-secondaryLight text-tiny">
|
||||
By signing in, you are agreeing to our
|
||||
<SmartAnchor
|
||||
class="link text-red-800"
|
||||
to="https://docs.hoppscotch.io/terms"
|
||||
blank
|
||||
label="Terms of Service"
|
||||
/>
|
||||
and
|
||||
<SmartAnchor
|
||||
class="link text-red-600"
|
||||
to="https://docs.hoppscotch.io/privacy"
|
||||
blank
|
||||
label="Privacy Policy"
|
||||
/>
|
||||
</div>
|
||||
</Story>
|
||||
</template>
|
||||
55
packages/hoppscotch-ui/src/stories/AutoComplete.story.vue
Normal file
55
packages/hoppscotch-ui/src/stories/AutoComplete.story.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<Story title="Auto Complete">
|
||||
<div class="h-[50vh]">
|
||||
<SmartAutoComplete
|
||||
placeholder="Select a header"
|
||||
:source="commonHeaders"
|
||||
:spellcheck="false"
|
||||
:value="header[0].key"
|
||||
autofocus
|
||||
styles="
|
||||
bg-transparent
|
||||
flex
|
||||
flex-1
|
||||
py-1
|
||||
px-4
|
||||
truncate
|
||||
"
|
||||
class="flex-1 !flex"
|
||||
@input="updateHeader()"
|
||||
/>
|
||||
</div>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
|
||||
type GQLHeader = {
|
||||
key: string
|
||||
value: string
|
||||
active: boolean
|
||||
}
|
||||
|
||||
const header = ref<GQLHeader[]>([
|
||||
{
|
||||
key: "Content-Type",
|
||||
value: "application/json",
|
||||
active: true,
|
||||
},
|
||||
])
|
||||
|
||||
const commonHeaders = [
|
||||
"Push-Policy",
|
||||
"Retry-After",
|
||||
"Signature",
|
||||
"Signed-Headers",
|
||||
"Server-Timing",
|
||||
"SourceMap",
|
||||
"Upgrade",
|
||||
]
|
||||
|
||||
const updateHeader = () => {
|
||||
// alert("updated")
|
||||
}
|
||||
</script>
|
||||
10
packages/hoppscotch-ui/src/stories/Button.story.vue
Normal file
10
packages/hoppscotch-ui/src/stories/Button.story.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<Story title="Button">
|
||||
<Variant title="Primary">
|
||||
<ButtonPrimary label="Button" />
|
||||
</Variant>
|
||||
<Variant title="Secondary">
|
||||
<ButtonSecondary label="Button" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
13
packages/hoppscotch-ui/src/stories/Checkbox.story.vue
Normal file
13
packages/hoppscotch-ui/src/stories/Checkbox.story.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<Story title="Checkbox">
|
||||
<Variant title="Single">
|
||||
<SmartCheckbox :on="on" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
|
||||
const on = ref(true)
|
||||
</script>
|
||||
21
packages/hoppscotch-ui/src/stories/ConfirmModal.story.vue
Normal file
21
packages/hoppscotch-ui/src/stories/ConfirmModal.story.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<Story title="Confirm Modal">
|
||||
<SmartConfirmModal
|
||||
:show="show"
|
||||
:title="'Confirm Modal'"
|
||||
@hide-modal="show = false"
|
||||
@resolve="resolveConfirmModal"
|
||||
/>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue"
|
||||
|
||||
const show = ref(true)
|
||||
|
||||
const resolveConfirmModal = (resolve: string | null) => {
|
||||
alert("resolved: " + resolve)
|
||||
show.value = false
|
||||
}
|
||||
</script>
|
||||
7
packages/hoppscotch-ui/src/stories/Item.story.vue
Normal file
7
packages/hoppscotch-ui/src/stories/Item.story.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<Story title="Item">
|
||||
<Variant title="Single">
|
||||
<SmartItem :label="'Item'" :active-info-icon="false" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
18
packages/hoppscotch-ui/src/stories/Link.story.vue
Normal file
18
packages/hoppscotch-ui/src/stories/Link.story.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<Story title="Link">
|
||||
<Variant title="Text Link">
|
||||
<SmartLink :to="link" :blank="true"> Click here </SmartLink>
|
||||
</Variant>
|
||||
<Variant title="Button Link">
|
||||
<SmartLink :to="link" :blank="true">
|
||||
<ButtonPrimary label="Click here" />
|
||||
</SmartLink>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
|
||||
const link = ref("/graphql")
|
||||
</script>
|
||||
21
packages/hoppscotch-ui/src/stories/Modal.story.vue
Normal file
21
packages/hoppscotch-ui/src/stories/Modal.story.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<Story title="Modal">
|
||||
<SmartModal
|
||||
:show="show"
|
||||
:title="'Modal Title'"
|
||||
@hide-modal="show = false"
|
||||
@resolve="resolveConfirmModal"
|
||||
/>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue"
|
||||
|
||||
const show = ref(true)
|
||||
|
||||
const resolveConfirmModal = (resolve: string | null) => {
|
||||
alert("resolved: " + resolve)
|
||||
show.value = false
|
||||
}
|
||||
</script>
|
||||
15
packages/hoppscotch-ui/src/stories/ProgressRing.story.vue
Normal file
15
packages/hoppscotch-ui/src/stories/ProgressRing.story.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<Story title="Progress Ring">
|
||||
<SmartProgressRing
|
||||
class="mr-2 text-red-500"
|
||||
:radius="8"
|
||||
:stroke="1.5"
|
||||
:progress="(failedTests / totalTests) * 100"
|
||||
/>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const totalTests = 10
|
||||
const failedTests = 2
|
||||
</script>
|
||||
21
packages/hoppscotch-ui/src/stories/Radio.story.vue
Normal file
21
packages/hoppscotch-ui/src/stories/Radio.story.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<Story title="Radio">
|
||||
<Variant title="Single">
|
||||
<SmartRadio />
|
||||
</Variant>
|
||||
<Variant title="Group">
|
||||
<SmartRadioGroup :radios="radios" :model-value="selected" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
|
||||
const selected = ref("option1")
|
||||
const radios = [
|
||||
{ label: "Option 1", value: "option1" },
|
||||
{ label: "Option 2", value: "option2" },
|
||||
{ label: "Option 3", value: "option3" },
|
||||
]
|
||||
</script>
|
||||
15
packages/hoppscotch-ui/src/stories/SlideOver.story.vue
Normal file
15
packages/hoppscotch-ui/src/stories/SlideOver.story.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<Story title="Slider Over">
|
||||
<SmartSlideOver :show="show" :title="'Title'" @close="show = false">
|
||||
<template #content>
|
||||
<h1>Content</h1>
|
||||
</template>
|
||||
</SmartSlideOver>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue"
|
||||
|
||||
const show = ref(true)
|
||||
</script>
|
||||
5
packages/hoppscotch-ui/src/stories/Spinner.story.vue
Normal file
5
packages/hoppscotch-ui/src/stories/Spinner.story.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<Story title="Spinner">
|
||||
<SmartSpinner />
|
||||
</Story>
|
||||
</template>
|
||||
20
packages/hoppscotch-ui/src/stories/Tab.story.vue
Normal file
20
packages/hoppscotch-ui/src/stories/Tab.story.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<Story title="Tab">
|
||||
<Variant title="Single">
|
||||
<SmartTabs id="my-tab" v-model="selectedTab" render-inactive-tabs>
|
||||
<SmartTab id="tab1" label="Tab 1">
|
||||
<h1>Tab 1 content</h1>
|
||||
</SmartTab>
|
||||
<SmartTab id="tab2" label="Tab 2">
|
||||
<h1>Tab 2 content</h1>
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
|
||||
const selectedTab = ref("tab1")
|
||||
</script>
|
||||
15
packages/hoppscotch-ui/src/stories/Toggle.story.vue
Normal file
15
packages/hoppscotch-ui/src/stories/Toggle.story.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<Story title="Toggle">
|
||||
<SmartToggle :on="on" @change="change"> Turn on </SmartToggle>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
|
||||
const on = ref(true)
|
||||
|
||||
const change = () => {
|
||||
alert("changed to: " + on.value)
|
||||
}
|
||||
</script>
|
||||
72
packages/hoppscotch-ui/src/stories/Window.story.vue
Normal file
72
packages/hoppscotch-ui/src/stories/Window.story.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<Story title="Window">
|
||||
<Variant title="Single">
|
||||
<SmartWindows
|
||||
:id="'my-window'"
|
||||
v-model="selectedWindow"
|
||||
@add-tab="openNewTab"
|
||||
@remove-tab="removeTab"
|
||||
@sort="sortTabs"
|
||||
>
|
||||
<SmartWindow
|
||||
v-for="window in tabs"
|
||||
:id="window.id"
|
||||
:key="'tab_' + window.id"
|
||||
:label="window.name"
|
||||
:is-removable="window.removable"
|
||||
>
|
||||
</SmartWindow>
|
||||
</SmartWindows>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
|
||||
const selectedWindow = ref("window1")
|
||||
|
||||
type Tab = {
|
||||
id: string
|
||||
name: string
|
||||
removable: boolean
|
||||
}
|
||||
|
||||
const tabs = ref<Tab[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "window1",
|
||||
removable: false,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "window2",
|
||||
removable: true,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "window3",
|
||||
removable: true,
|
||||
},
|
||||
])
|
||||
|
||||
const openNewTab = () => {
|
||||
const newTab = {
|
||||
id: Date.now().toString(),
|
||||
name: "New Window",
|
||||
removable: true,
|
||||
}
|
||||
tabs.value = [...tabs.value, { ...newTab }]
|
||||
selectedWindow.value = newTab.id
|
||||
}
|
||||
|
||||
const removeTab = (tabID: string) => {
|
||||
tabs.value = tabs.value.filter((tab) => tab.id !== tabID)
|
||||
}
|
||||
|
||||
const sortTabs = (e: { oldIndex: number; newIndex: number }) => {
|
||||
const newTabs = [...tabs.value]
|
||||
newTabs.splice(e.newIndex, 0, newTabs.splice(e.oldIndex, 1)[0])
|
||||
tabs.value = newTabs
|
||||
}
|
||||
</script>
|
||||
34
packages/hoppscotch-ui/tsconfig.json
Normal file
34
packages/hoppscotch-ui/tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"noUnusedLocals": true,
|
||||
"paths": {
|
||||
"~/*": ["./src/*"],
|
||||
"@composables/*": ["./src/composables/*"],
|
||||
"@components/*": ["./src/components/*"],
|
||||
"@helpers/*": ["./src/helpers/*"],
|
||||
"@modules/*": ["./src/modules/*"],
|
||||
"@workers/*": ["./src/workers/*"],
|
||||
"@functional/*": ["./src/helpers/functional/*"]
|
||||
},
|
||||
"types": [
|
||||
"vite/client",
|
||||
"unplugin-icons/types/vue",
|
||||
"vite-plugin-pages/client",
|
||||
"vite-plugin-vue-layouts/client",
|
||||
"vite-plugin-pwa/client"
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
60
packages/hoppscotch-ui/vite.config.ts
Normal file
60
packages/hoppscotch-ui/vite.config.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import vue from "@vitejs/plugin-vue"
|
||||
import path from "path"
|
||||
import { FileSystemIconLoader } from "unplugin-icons/loaders"
|
||||
import IconResolver from "unplugin-icons/resolver"
|
||||
import Icons from "unplugin-icons/vite"
|
||||
import Components from "unplugin-vue-components/vite"
|
||||
import { defineConfig } from "vite"
|
||||
import WindiCSS from "vite-plugin-windicss"
|
||||
|
||||
module.exports = defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
WindiCSS({
|
||||
root: path.resolve(__dirname),
|
||||
}),
|
||||
Components({
|
||||
dts: "./src/components.d.ts",
|
||||
dirs: ["./src/components"],
|
||||
directoryAsNamespace: true,
|
||||
resolvers: [
|
||||
IconResolver({
|
||||
prefix: "icon",
|
||||
customCollections: ["hopp", "auth", "brands"],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Icons({
|
||||
compiler: "vue3",
|
||||
customCollections: {
|
||||
hopp: FileSystemIconLoader("../hoppscotch-common/assets/icons"),
|
||||
auth: FileSystemIconLoader("../hoppscotch-common/assets/icons/auth"),
|
||||
brands: FileSystemIconLoader(
|
||||
"../hoppscotch-common/assets/icons/brands"
|
||||
),
|
||||
},
|
||||
}),
|
||||
], // to process SFC
|
||||
build: {
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, "src/index.ts"),
|
||||
name: "hopp-ui",
|
||||
formats: ["es"], // adding 'umd' requires globals set to every external module
|
||||
fileName: (format) => `hopp-ui.${format}.js`,
|
||||
},
|
||||
rollupOptions: {
|
||||
// external modules won't be bundled into HoppUI library
|
||||
external: ["vue"], // not every external has a global
|
||||
output: {
|
||||
// disable warning on src/index.ts using both default and named export
|
||||
exports: "named",
|
||||
// Provide global variables to use in the UMD build
|
||||
// for externalized deps (not useful if 'umd' is not in lib.formats)
|
||||
globals: {
|
||||
vue: "Vue",
|
||||
},
|
||||
},
|
||||
},
|
||||
emptyOutDir: false, // to retain the types folder generated by tsc
|
||||
},
|
||||
})
|
||||
65
packages/hoppscotch-ui/windi.config.ts
Normal file
65
packages/hoppscotch-ui/windi.config.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { defineConfig } from "windicss/helpers"
|
||||
|
||||
export default defineConfig({
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
},
|
||||
extend: {
|
||||
inset: {
|
||||
upperPrimaryStickyFold: "var(--upper-primary-sticky-fold)",
|
||||
upperSecondaryStickyFold: "var(--upper-secondary-sticky-fold)",
|
||||
upperTertiaryStickyFold: "var(--upper-tertiary-sticky-fold)",
|
||||
upperMobilePrimaryStickyFold: "var(--upper-mobile-primary-sticky-fold)",
|
||||
upperMobileSecondaryStickyFold:
|
||||
"var(--upper-mobile-secondary-sticky-fold)",
|
||||
upperMobileStickyFold: "var(--upper-mobile-sticky-fold)",
|
||||
upperMobileTertiaryStickyFold:
|
||||
"var(--upper-mobile-tertiary-sticky-fold)",
|
||||
lowerPrimaryStickyFold: "var(--lower-primary-sticky-fold)",
|
||||
lowerSecondaryStickyFold: "var(--lower-secondary-sticky-fold)",
|
||||
lowerTertiaryStickyFold: "var(--lower-tertiary-sticky-fold)",
|
||||
sidebarPrimaryStickyFold: "var(--sidebar-primary-sticky-fold)",
|
||||
},
|
||||
colors: {
|
||||
primary: "var(--primary-color)",
|
||||
primaryLight: "var(--primary-light-color)",
|
||||
primaryDark: "var(--primary-dark-color)",
|
||||
primaryContrast: "var(--primary-contrast-color)",
|
||||
secondary: "var(--secondary-color)",
|
||||
secondaryLight: "var(--secondary-light-color)",
|
||||
secondaryDark: "var(--secondary-dark-color)",
|
||||
accent: "var(--accent-color)",
|
||||
accentLight: "var(--accent-light-color)",
|
||||
accentDark: "var(--accent-dark-color)",
|
||||
accentContrast: "var(--accent-contrast-color)",
|
||||
divider: "var(--divider-color)",
|
||||
dividerLight: "var(--divider-light-color)",
|
||||
dividerDark: "var(--divider-dark-color)",
|
||||
error: "var(--error-color)",
|
||||
tooltip: "var(--tooltip-color)",
|
||||
popover: "var(--popover-color)",
|
||||
gradientFrom: "var(--gradient-from-color)",
|
||||
gradientVia: "var(--gradient-via-color)",
|
||||
gradientTo: "var(--gradient-to-color)",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: "var(--font-sans)",
|
||||
mono: "var(--font-mono)",
|
||||
icon: "var(--font-icon)",
|
||||
},
|
||||
fontSize: {
|
||||
tiny: "var(--font-size-tiny)",
|
||||
body: "var(--font-size-body)",
|
||||
},
|
||||
lineHeight: {
|
||||
body: "var(--line-height-body)",
|
||||
},
|
||||
cursor: {
|
||||
nsResize: "ns-resize",
|
||||
grab: "grab",
|
||||
grabbing: "grabbing",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user