chore: move hoppscotch-ui package out of the monorepo (#3620)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -81,10 +81,7 @@ web_modules/
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
.env.*
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^16.2.3",
|
||||
"@commitlint/config-conventional": "^16.2.1",
|
||||
"@hoppscotch/ui": "^0.1.0",
|
||||
"@types/node": "17.0.27",
|
||||
"cross-env": "^7.0.3",
|
||||
"http-server": "^14.1.1",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
"@hoppscotch/js-sandbox": "workspace:^",
|
||||
"@hoppscotch/ui": "workspace:^",
|
||||
"@hoppscotch/ui": "^0.1.0",
|
||||
"@hoppscotch/vue-toasted": "^0.1.0",
|
||||
"@lezer/highlight": "1.2.0",
|
||||
"@unhead/vue": "^1.8.8",
|
||||
|
||||
@@ -341,10 +341,7 @@ import IconImport from "~icons/lucide/folder-down"
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { computed, PropType, Ref, toRef } from "vue"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import {
|
||||
ChildrenResult,
|
||||
SmartTreeAdapter,
|
||||
} from "@hoppscotch/ui/dist/helpers/treeAdapter"
|
||||
import { ChildrenResult, SmartTreeAdapter } from "@hoppscotch/ui/helpers"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
@@ -366,10 +366,7 @@ import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
||||
import { TeamRequest } from "~/helpers/teams/TeamRequest"
|
||||
import {
|
||||
ChildrenResult,
|
||||
SmartTreeAdapter,
|
||||
} from "@hoppscotch/ui/dist/helpers/treeAdapter"
|
||||
import { ChildrenResult, SmartTreeAdapter } from "@hoppscotch/ui/helpers"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { pipe } from "fp-ts/function"
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"@fontsource-variable/roboto-mono": "^5.0.16",
|
||||
"@hoppscotch/common": "workspace:^",
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
"@hoppscotch/ui": "^0.1.0",
|
||||
"@import-meta-env/unplugin": "^0.4.10",
|
||||
"axios": "^1.6.2",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -49,7 +50,6 @@
|
||||
"@graphql-codegen/typescript-urql-graphcache": "^3.0.0",
|
||||
"@graphql-codegen/urql-introspection": "^3.0.0",
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"@hoppscotch/ui": "workspace:^",
|
||||
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
||||
"@rushstack/eslint-patch": "^1.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||
@@ -81,4 +81,4 @@
|
||||
"vite-plugin-vue-layouts": "^0.8.0",
|
||||
"vue-tsc": "^1.8.22"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const postcssConfig = require("@hoppscotch/ui/postcss.config")
|
||||
|
||||
const config = {
|
||||
...postcssConfig,
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
|
||||
@@ -2,10 +2,7 @@ import { Config } from "tailwindcss"
|
||||
import preset from "@hoppscotch/ui/ui-preset"
|
||||
|
||||
export default {
|
||||
content: [
|
||||
"../hoppscotch-common/src/**/*.{vue,html}",
|
||||
"../hoppscotch-ui/src/**/*.{vue,html}",
|
||||
],
|
||||
content: ["../hoppscotch-common/src/**/*.{vue,html}"],
|
||||
presets: [preset],
|
||||
theme: {
|
||||
extend: {
|
||||
|
||||
@@ -117,10 +117,7 @@ export default defineConfig({
|
||||
}),
|
||||
Components({
|
||||
dts: "../hoppscotch-common/src/components.d.ts",
|
||||
dirs: [
|
||||
"../hoppscotch-common/src/components",
|
||||
"../hoppscotch-ui/src/components",
|
||||
],
|
||||
dirs: ["../hoppscotch-common/src/components"],
|
||||
directoryAsNamespace: true,
|
||||
resolvers: [
|
||||
IconResolver({
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"@fontsource-variable/material-symbols-rounded": "^5.0.5",
|
||||
"@fontsource-variable/roboto-mono": "^5.0.6",
|
||||
"@graphql-typed-document-node/core": "^3.1.1",
|
||||
"@hoppscotch/ui": "workspace:^",
|
||||
"@hoppscotch/ui": "^0.1.0",
|
||||
"@hoppscotch/vue-toasted": "^0.1.0",
|
||||
"@intlify/unplugin-vue-i18n": "^1.2.0",
|
||||
"@types/cors": "^2.8.13",
|
||||
@@ -37,7 +37,7 @@
|
||||
"postcss": "^8.4.23",
|
||||
"prettier-plugin-tailwindcss": "^0.5.6",
|
||||
"rxjs": "^7.8.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"tippy.js": "^6.3.7",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"unplugin-icons": "^0.14.9",
|
||||
@@ -48,6 +48,7 @@
|
||||
"vue-tippy": "6.0.0-alpha.58"
|
||||
},
|
||||
"devDependencies": {
|
||||
"hoppscotch-backend": "workspace:^",
|
||||
"@graphql-codegen/cli": "3.0.0",
|
||||
"@graphql-codegen/client-preset": "^2.0.0",
|
||||
"@graphql-codegen/introspection": "3.0.0",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const postcssConfig = require("@hoppscotch/ui/postcss.config")
|
||||
|
||||
const config = {
|
||||
...postcssConfig,
|
||||
}
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config
|
||||
module.exports = config;
|
||||
|
||||
27
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
27
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
@@ -1,9 +1,9 @@
|
||||
// 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'
|
||||
import '@vue/runtime-core';
|
||||
|
||||
export {}
|
||||
export {};
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
@@ -76,6 +76,27 @@ declare module '@vue/runtime-core' {
|
||||
UsersDetails: typeof import('./components/users/Details.vue')['default']
|
||||
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default']
|
||||
UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default']
|
||||
AppHeader: typeof import('./components/app/Header.vue')['default'];
|
||||
AppLogin: typeof import('./components/app/Login.vue')['default'];
|
||||
AppLogout: typeof import('./components/app/Logout.vue')['default'];
|
||||
AppModal: typeof import('./components/app/Modal.vue')['default'];
|
||||
AppSidebar: typeof import('./components/app/Sidebar.vue')['default'];
|
||||
AppToast: typeof import('./components/app/Toast.vue')['default'];
|
||||
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default'];
|
||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary'];
|
||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor'];
|
||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'];
|
||||
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'];
|
||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'];
|
||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'];
|
||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'];
|
||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['default'];
|
||||
TeamsAdd: typeof import('./components/teams/Add.vue')['default'];
|
||||
TeamsDetails: typeof import('./components/teams/Details.vue')['default'];
|
||||
TeamsInvite: typeof import('./components/teams/Invite.vue')['default'];
|
||||
TeamsMembers: typeof import('./components/teams/Members.vue')['default'];
|
||||
TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default'];
|
||||
Tippy: typeof import('vue-tippy')['Tippy'];
|
||||
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ import { Config } from 'tailwindcss';
|
||||
import preset from '@hoppscotch/ui/ui-preset';
|
||||
|
||||
export default {
|
||||
content: ['src/**/*.{vue,html}', '../hoppscotch-ui/src/**/*.{vue,html}'],
|
||||
content: ['src/**/*.{vue,html}'],
|
||||
presets: [preset],
|
||||
} satisfies Config;
|
||||
|
||||
@@ -41,7 +41,7 @@ export default defineConfig({
|
||||
}),
|
||||
Components({
|
||||
dts: './src/components.d.ts',
|
||||
dirs: ['./src/components', '../hoppscotch-ui/src/components'],
|
||||
dirs: ['./src/components'],
|
||||
directoryAsNamespace: true,
|
||||
resolvers: [
|
||||
IconResolver({
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/* 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
27
packages/hoppscotch-ui/.gitignore
vendored
@@ -1,27 +0,0 @@
|
||||
# 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
|
||||
@@ -1,10 +0,0 @@
|
||||
.dependabot
|
||||
.github
|
||||
.hoppscotch
|
||||
.vscode
|
||||
package-lock.json
|
||||
node_modules
|
||||
dist
|
||||
static
|
||||
components.d.ts
|
||||
src/types
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
semi: false,
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<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 <font size=2><sup>ALPHA</sup></font>
|
||||
|
||||
</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 `<HoppButtonPrimary />`
|
||||
|
||||
## 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>
|
||||
@@ -1,18 +0,0 @@
|
||||
import { HstVue } from "@histoire/plugin-vue"
|
||||
import { defineConfig } from "histoire"
|
||||
|
||||
export default defineConfig({
|
||||
theme: {
|
||||
title: "Hoppscotch Design • Hoppscotch",
|
||||
logo: {
|
||||
square: "/logo.svg",
|
||||
light: "/logo.svg",
|
||||
dark: "/logo.svg",
|
||||
},
|
||||
logoHref: "/",
|
||||
favicon: "favicon.ico",
|
||||
},
|
||||
setupFile: "histoire.setup.ts",
|
||||
plugins: [HstVue()],
|
||||
viteIgnorePlugins: ["vite:dts"],
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
import "./src/assets/scss/histoire.scss"
|
||||
|
||||
import "@fontsource-variable/inter"
|
||||
import "@fontsource-variable/material-symbols-rounded"
|
||||
import "@fontsource-variable/roboto-mono"
|
||||
|
||||
export function setupVue3() {}
|
||||
@@ -1,100 +0,0 @@
|
||||
{
|
||||
"name": "@hoppscotch/ui",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"watch": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"story:dev": "histoire dev",
|
||||
"story:build": "histoire build",
|
||||
"story:preview": "histoire preview",
|
||||
"do-build-prod": "pnpm run build",
|
||||
"postinstall": "pnpm run build",
|
||||
"do-build-ui": "pnpm run story:build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.25"
|
||||
},
|
||||
"dependencies": {
|
||||
"@boringer-avatars/vue3": "^0.2.1",
|
||||
"@fontsource-variable/inter": "^5.0.5",
|
||||
"@fontsource-variable/material-symbols-rounded": "^5.0.5",
|
||||
"@fontsource-variable/roboto-mono": "^5.0.6",
|
||||
"@hoppscotch/vue-toasted": "^0.1.0",
|
||||
"@vitejs/plugin-legacy": "^2.3.0",
|
||||
"@vueuse/core": "^8.7.5",
|
||||
"fp-ts": "^2.12.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"path": "^0.12.7",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vuedraggable-es": "^4.1.1"
|
||||
},
|
||||
"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.109",
|
||||
"@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",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.5.1",
|
||||
"histoire": "^0.12.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.23",
|
||||
"prettier": "^2.8.4",
|
||||
"prettier-plugin-tailwindcss": "^0.5.6",
|
||||
"rollup-plugin-polyfill-node": "^0.10.1",
|
||||
"sass": "^1.53.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^4.5.4",
|
||||
"unplugin-icons": "^0.16.1",
|
||||
"unplugin-fonts": "^1.0.3",
|
||||
"unplugin-vue-components": "^0.21.0",
|
||||
"vite": "^3.2.3",
|
||||
"vite-plugin-checker": "^0.5.1",
|
||||
"vite-plugin-dts": "3.2.0",
|
||||
"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.5",
|
||||
"vite-plugin-pwa": "^0.13.1",
|
||||
"vite-plugin-vue-layouts": "^0.7.0",
|
||||
"vue": "^3.2.25",
|
||||
"vue-loader": "^16.8.3",
|
||||
"vue-router": "^4.0.16",
|
||||
"vue-tsc": "^0.38.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"module": "./dist/index.js",
|
||||
"main": "./dist/index.js",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./style.css": "./dist/style.css",
|
||||
"./themes.scss": "./dist/themes.scss",
|
||||
"./postcss.config": {
|
||||
"import": "./postcss.config.cjs",
|
||||
"require": "./postcss.config.cjs"
|
||||
},
|
||||
"./ui-preset": {
|
||||
"import": "./dist/ui-preset.js",
|
||||
"require": "./dist/ui-preset.js"
|
||||
},
|
||||
"./helpers/treeAdapter.ts": "./src/helpers/treeAdapter.ts"
|
||||
},
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,50 +0,0 @@
|
||||
<svg width="824" height="824" viewBox="0 0 824 824" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="824" height="824" rx="184" fill="#08110F"/>
|
||||
<rect width="824" height="824" rx="184" fill="url(#paint0_radial_0_21)" fill-opacity="0.5"/>
|
||||
<path d="M435.425 463.217C429.441 476.657 411.033 481.515 394.309 474.07C377.585 466.624 368.879 449.693 374.863 436.253C380.846 422.813 399.254 417.954 415.978 425.4C432.702 432.846 441.409 449.777 435.425 463.217Z" fill="url(#paint1_linear_0_21)"/>
|
||||
<path d="M435.425 463.217C429.441 476.657 411.033 481.515 394.309 474.07C377.585 466.624 368.879 449.693 374.863 436.253C380.846 422.813 399.254 417.954 415.978 425.4C432.702 432.846 441.409 449.777 435.425 463.217Z" fill="url(#paint2_radial_0_21)" style="mix-blend-mode:soft-light"/>
|
||||
<path d="M535.563 521.172C553.071 526.191 570.536 518.856 574.571 504.789C578.606 490.722 567.684 475.251 550.175 470.232C532.666 465.213 515.201 472.548 511.166 486.615C507.131 500.682 518.054 516.153 535.563 521.172Z" fill="url(#paint3_linear_0_21)"/>
|
||||
<path d="M535.563 521.172C553.071 526.191 570.536 518.856 574.571 504.789C578.606 490.722 567.684 475.251 550.175 470.232C532.666 465.213 515.201 472.548 511.166 486.615C507.131 500.682 518.054 516.153 535.563 521.172Z" fill="url(#paint4_radial_0_21)" style="mix-blend-mode:soft-light"/>
|
||||
<path d="M292.782 355.633C308.227 365.286 314.462 383.173 306.709 395.584C298.955 407.995 280.149 410.231 264.704 400.578C249.258 390.924 243.023 373.037 250.777 360.626C258.53 348.215 277.337 345.98 292.782 355.633Z" fill="url(#paint5_linear_0_21)"/>
|
||||
<path d="M292.782 355.633C308.227 365.286 314.462 383.173 306.709 395.584C298.955 407.995 280.149 410.231 264.704 400.578C249.258 390.924 243.023 373.037 250.777 360.626C258.53 348.215 277.337 345.98 292.782 355.633Z" fill="url(#paint6_radial_0_21)" style="mix-blend-mode:soft-light"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M502.355 231.325C581.373 266.506 632.095 343.263 634.119 429.03C680.633 465.639 726.858 516.883 705.36 565.168C681.25 619.319 595.382 617.091 497.781 589.689C450.767 615.718 392.444 620.168 339.689 596.68C286.934 573.192 251.229 526.908 239.1 474.517C153.428 420.321 94.3151 357.999 118.425 303.847C139.923 255.562 208.935 255.626 267.265 265.697C332.356 209.81 423.338 196.144 502.355 231.325ZM159.38 322.082C147.667 348.389 210.578 423.052 382.845 499.751C555.111 576.449 652.693 573.241 664.405 546.934C674.099 525.16 634.213 483.308 588.537 450.878C553.009 425.484 504.344 397.494 440.864 369.231C423.586 361.538 416.839 341.008 424.104 324.691C431.369 308.374 447.329 297.463 480.93 295.91C496.747 295.862 498.823 291.476 499.546 287.716C500.442 281.915 492.401 276.002 484.108 272.31C418.17 242.953 337.453 255.265 281.503 314.178C226.84 301.933 169.074 300.309 159.38 322.082Z" fill="url(#paint7_linear_0_21)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M502.355 231.325C581.373 266.506 632.095 343.263 634.119 429.03C680.633 465.639 726.858 516.883 705.36 565.168C681.25 619.319 595.382 617.091 497.781 589.689C450.767 615.718 392.444 620.168 339.689 596.68C286.934 573.192 251.229 526.908 239.1 474.517C153.428 420.321 94.3151 357.999 118.425 303.847C139.923 255.562 208.935 255.626 267.265 265.697C332.356 209.81 423.338 196.144 502.355 231.325ZM159.38 322.082C147.667 348.389 210.578 423.052 382.845 499.751C555.111 576.449 652.693 573.241 664.405 546.934C674.099 525.16 634.213 483.308 588.537 450.878C553.009 425.484 504.344 397.494 440.864 369.231C423.586 361.538 416.839 341.008 424.104 324.691C431.369 308.374 447.329 297.463 480.93 295.91C496.747 295.862 498.823 291.476 499.546 287.716C500.442 281.915 492.401 276.002 484.108 272.31C418.17 242.953 337.453 255.265 281.503 314.178C226.84 301.933 169.074 300.309 159.38 322.082Z" fill="url(#paint8_radial_0_21)" style="mix-blend-mode:soft-light"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(814.524 12.36) rotate(125.613) scale(1089.59 1210.34)">
|
||||
<stop stop-color="#00D196" stop-opacity="0.5"/>
|
||||
<stop offset="0.996771" stop-color="#00D196" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_0_21" x1="411.893" y1="212" x2="411.893" y2="612" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00D196"/>
|
||||
<stop offset="1" stop-color="#00B381"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint2_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(644.721 344.481) rotate(159.984) scale(631.37 385.135)">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint3_linear_0_21" x1="411.893" y1="212" x2="411.893" y2="612" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00D196"/>
|
||||
<stop offset="1" stop-color="#00B381"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint4_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(644.721 344.481) rotate(159.984) scale(631.37 385.135)">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint5_linear_0_21" x1="411.893" y1="212" x2="411.893" y2="612" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00D196"/>
|
||||
<stop offset="1" stop-color="#00B381"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint6_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(644.721 344.481) rotate(159.984) scale(631.37 385.135)">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint7_linear_0_21" x1="411.893" y1="212" x2="411.893" y2="612" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00D196"/>
|
||||
<stop offset="1" stop-color="#00B381"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint8_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(644.721 344.481) rotate(159.984) scale(631.37 385.135)">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.9 KiB |
@@ -1,652 +0,0 @@
|
||||
/*
|
||||
* Write hoppscotch-common related custom styles in this file.
|
||||
* If styles are sharable across all package then write into hoppscotch-ui/assets/scss/styles.scss file.
|
||||
*/
|
||||
|
||||
@mixin base-theme {
|
||||
--font-sans: "Inter Variable", sans-serif;
|
||||
--font-mono: "Roboto Mono Variable", monospace;
|
||||
--font-size-body: 0.75rem;
|
||||
--font-size-tiny: 0.625rem;
|
||||
--line-height-body: 1rem;
|
||||
--upper-primary-sticky-fold: 4rem;
|
||||
--upper-secondary-sticky-fold: 6.188rem;
|
||||
--upper-tertiary-sticky-fold: 8.25rem;
|
||||
--upper-fourth-sticky-fold: 10.2rem;
|
||||
--upper-mobile-primary-sticky-fold: 6.75rem;
|
||||
--upper-mobile-secondary-sticky-fold: 8.813rem;
|
||||
--upper-mobile-sticky-fold: 10.875rem;
|
||||
--upper-mobile-tertiary-sticky-fold: 8.25rem;
|
||||
--lower-primary-sticky-fold: 3rem;
|
||||
--lower-secondary-sticky-fold: 5.063rem;
|
||||
--lower-tertiary-sticky-fold: 7.125rem;
|
||||
--lower-fourth-sticky-fold: 9.188rem;
|
||||
--sidebar-primary-sticky-fold: 2rem;
|
||||
--properties-primary-sticky-fold: 2.063rem;
|
||||
}
|
||||
|
||||
@mixin dark-theme {
|
||||
--primary-color: #181818;
|
||||
--primary-light-color: #1c1c1e;
|
||||
--primary-dark-color: #262626;
|
||||
--primary-contrast-color: #171717;
|
||||
|
||||
--secondary-color: #a3a3a3;
|
||||
--secondary-light-color: #737373;
|
||||
--secondary-dark-color: #fafafa;
|
||||
|
||||
--divider-color: #262626;
|
||||
--divider-light-color: #1f1f1f;
|
||||
--divider-dark-color: #2d2d2d;
|
||||
|
||||
--error-color: #292524;
|
||||
--tooltip-color: #f5f5f5;
|
||||
--popover-color: #1b1b1b;
|
||||
--editor-theme: "merbivore_soft";
|
||||
}
|
||||
|
||||
@mixin green-theme {
|
||||
--accent-color: #10b981;
|
||||
--accent-light-color: #34d399;
|
||||
--accent-dark-color: #059669;
|
||||
--accent-contrast-color: #fff;
|
||||
--gradient-from-color: #a7f3d0;
|
||||
--gradient-via-color: #34d399;
|
||||
--gradient-to-color: #059669;
|
||||
}
|
||||
|
||||
* {
|
||||
backface-visibility: hidden;
|
||||
-moz-backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
|
||||
&::before {
|
||||
backface-visibility: hidden;
|
||||
-moz-backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
&::after {
|
||||
backface-visibility: hidden;
|
||||
-moz-backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
@apply selection:bg-accentDark;
|
||||
@apply selection:text-accentContrast;
|
||||
@apply overscroll-none;
|
||||
}
|
||||
|
||||
:root {
|
||||
@include base-theme;
|
||||
@include dark-theme;
|
||||
@include green-theme;
|
||||
|
||||
@apply antialiased;
|
||||
accent-color: var(--accent-color);
|
||||
font-variant-ligatures: common-ligatures;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-transparent;
|
||||
@apply border-b-0 border-l border-r-0 border-t-0 border-solid border-dividerLight;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-divider bg-clip-content;
|
||||
@apply rounded-full;
|
||||
@apply border-4 border-solid border-transparent;
|
||||
@apply hover:bg-dividerDark;
|
||||
@apply hover:bg-clip-content;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@apply w-4;
|
||||
@apply h-0;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder,
|
||||
.cm-placeholder {
|
||||
@apply text-secondary;
|
||||
@apply opacity-50 #{!important};
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
@apply text-secondaryDark;
|
||||
@apply font-medium;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-primary;
|
||||
@apply text-body text-secondary;
|
||||
@apply font-medium;
|
||||
@apply select-none;
|
||||
@apply overflow-x-hidden;
|
||||
@apply leading-body #{!important};
|
||||
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 px-1 py-0.5;
|
||||
@apply -mx-1 -my-0.5;
|
||||
@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 #{!important};
|
||||
@apply fixed;
|
||||
@apply inline-flex;
|
||||
@apply -mt-7;
|
||||
}
|
||||
}
|
||||
|
||||
.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 px-2 py-1;
|
||||
@apply truncate;
|
||||
@apply leading-body;
|
||||
@apply items-center;
|
||||
|
||||
kbd {
|
||||
@apply hidden;
|
||||
@apply font-sans;
|
||||
background-color: rgba(107, 114, 128, 0.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;
|
||||
@apply max-w-[45vw] #{!important};
|
||||
|
||||
.tippy-content {
|
||||
@apply flex flex-col;
|
||||
@apply max-h-[45vh];
|
||||
@apply items-stretch;
|
||||
@apply overflow-y-auto;
|
||||
@apply text-body text-secondary;
|
||||
@apply p-2;
|
||||
@apply leading-body;
|
||||
@apply focus:outline-none;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
& > span {
|
||||
@apply block #{!important};
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-svg-arrow {
|
||||
svg:first-child {
|
||||
@apply fill-dividerDark;
|
||||
}
|
||||
|
||||
svg:last-child {
|
||||
@apply fill-popover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-v-tippy] {
|
||||
@apply flex flex-1;
|
||||
@apply truncate;
|
||||
}
|
||||
|
||||
[interactive] > div {
|
||||
@apply flex flex-1;
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
hr {
|
||||
@apply border-b border-dividerLight;
|
||||
@apply my-2 #{!important};
|
||||
}
|
||||
|
||||
.heading {
|
||||
@apply font-bold;
|
||||
@apply text-lg text-secondaryDark;
|
||||
@apply tracking-tight;
|
||||
}
|
||||
|
||||
.input,
|
||||
.select,
|
||||
.textarea {
|
||||
@apply flex;
|
||||
@apply w-full;
|
||||
@apply px-4 py-2;
|
||||
@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-y-4 translate-x-1;
|
||||
}
|
||||
|
||||
.floating-input:focus-within ~ label {
|
||||
@apply text-secondaryDark;
|
||||
}
|
||||
|
||||
.floating-input ~ .end-actions {
|
||||
@apply absolute;
|
||||
@apply right-[.05rem];
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
.info-response {
|
||||
color: var(--status-info-color);
|
||||
}
|
||||
|
||||
.success-response {
|
||||
color: var(--status-success-color);
|
||||
}
|
||||
|
||||
.redirect-response {
|
||||
color: var(--status-redirect-color);
|
||||
}
|
||||
|
||||
.critical-error-response {
|
||||
color: var(--status-critical-error-color);
|
||||
}
|
||||
|
||||
.server-error-response {
|
||||
color: var(--status-server-error-color);
|
||||
}
|
||||
|
||||
.missing-data-response {
|
||||
color: var(--status-missing-data-color);
|
||||
}
|
||||
|
||||
.toasted-container {
|
||||
@apply max-w-md;
|
||||
@apply z-[10000];
|
||||
|
||||
.toasted {
|
||||
&.toasted-primary {
|
||||
@apply px-4 py-2;
|
||||
@apply bg-tooltip;
|
||||
@apply border-secondaryDark;
|
||||
@apply text-body text-primary;
|
||||
@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-[''];
|
||||
@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 before:absolute;
|
||||
@apply before:inset-0;
|
||||
@apply before:bg-accentLight;
|
||||
@apply before:opacity-0;
|
||||
@apply before:z-20;
|
||||
@apply before:transition;
|
||||
@apply before:content-[''];
|
||||
@apply hover:before:opacity-100;
|
||||
}
|
||||
|
||||
.no-splitter .splitpanes__splitter {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.smart-splitter.splitpanes--vertical > .splitpanes__splitter {
|
||||
@apply w-0;
|
||||
@apply before:-left-0.5;
|
||||
@apply before:-right-0.5;
|
||||
@apply before:h-full;
|
||||
@apply bg-divider;
|
||||
}
|
||||
|
||||
.smart-splitter.splitpanes--horizontal > .splitpanes__splitter {
|
||||
@apply h-0;
|
||||
@apply before:-top-0.5;
|
||||
@apply before:-bottom-0.5;
|
||||
@apply before:w-full;
|
||||
@apply bg-divider;
|
||||
}
|
||||
|
||||
.no-splitter.splitpanes--vertical > .splitpanes__splitter {
|
||||
@apply w-0;
|
||||
@apply pointer-events-none;
|
||||
@apply bg-dividerLight;
|
||||
}
|
||||
|
||||
.no-splitter.splitpanes--horizontal > .splitpanes__splitter {
|
||||
@apply h-0;
|
||||
@apply pointer-events-none;
|
||||
@apply bg-dividerLight;
|
||||
}
|
||||
|
||||
.splitpanes--horizontal .splitpanes__pane {
|
||||
@apply transition-none;
|
||||
}
|
||||
|
||||
.splitpanes--vertical .splitpanes__pane {
|
||||
@apply transition-none;
|
||||
}
|
||||
|
||||
.cm-focused {
|
||||
@apply select-auto;
|
||||
@apply outline-none #{!important};
|
||||
|
||||
.cm-activeLine {
|
||||
@apply bg-primaryLight;
|
||||
}
|
||||
|
||||
.cm-activeLineGutter {
|
||||
@apply bg-primaryDark;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
@apply overscroll-y-auto;
|
||||
}
|
||||
|
||||
.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-dividerLight;
|
||||
@apply rounded;
|
||||
@apply ml-2;
|
||||
@apply px-0.5;
|
||||
@apply min-w-[1rem];
|
||||
@apply min-h-[1rem];
|
||||
@apply leading-none;
|
||||
@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;
|
||||
|
||||
&.env-found {
|
||||
@apply bg-accentDark;
|
||||
@apply hover:bg-accent;
|
||||
}
|
||||
|
||||
&.env-not-found {
|
||||
@apply bg-red-500;
|
||||
@apply hover:bg-red-600;
|
||||
}
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
.gql-operation-not-highlight {
|
||||
@apply opacity-50;
|
||||
}
|
||||
|
||||
.gql-operation-highlight {
|
||||
@apply opacity-100;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
/*
|
||||
* Write hoppscotch-ui related custom global styles in this file.
|
||||
*/
|
||||
@@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<HoppSmartLink
|
||||
:to="to"
|
||||
:blank="blank"
|
||||
class="relative inline-flex items-center justify-center whitespace-nowrap py-2 font-semibold transition focus:outline-none focus-visible:bg-accentDark"
|
||||
:exact="exact"
|
||||
:class="[
|
||||
color
|
||||
? `text-${color}-800 bg-${color}-200 hover:(text-${color}-900 bg-${color}-300) focus-visible:(text-${color}-900 bg-${color}-300)`
|
||||
: `bg-accent text-accentContrast hover:bg-accentDark focus-visible:bg-accentDark`,
|
||||
label ? 'px-4 py-2' : 'p-2',
|
||||
rounded ? 'rounded-full' : 'rounded',
|
||||
{ 'cursor-not-allowed opacity-75': disabled },
|
||||
{ 'pointer-events-none': loading },
|
||||
{ 'px-6 py-4 text-lg': large },
|
||||
{ 'shadow-lg hover:shadow-xl': shadow },
|
||||
{
|
||||
'bg-gradient-to-tr from-gradientFrom via-gradientVia to-gradientTo text-white':
|
||||
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') : '',
|
||||
]"
|
||||
/>
|
||||
<div class="max-w-[16rem] truncate">
|
||||
{{ label }}
|
||||
</div>
|
||||
<div v-if="shortcut.length" class="<sm:hidden">
|
||||
<kbd
|
||||
v-for="(key, index) in shortcut"
|
||||
:key="`key-${index}`"
|
||||
class="shortcut-key !border-accentLight !bg-accentDark"
|
||||
>
|
||||
{{ key }}
|
||||
</kbd>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
v-if="loading"
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
>
|
||||
<HoppSmartSpinner />
|
||||
</span>
|
||||
</HoppSmartLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppSmartLink, HoppSmartSpinner } from "../smart"
|
||||
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>
|
||||
@@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<HoppSmartLink
|
||||
:to="to"
|
||||
:exact="exact"
|
||||
:blank="blank"
|
||||
class="inline-flex items-center justify-center 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 py-2' : 'p-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') : '',
|
||||
]"
|
||||
/>
|
||||
<div class="truncate max-w-[16rem]">
|
||||
{{ label }}
|
||||
</div>
|
||||
<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>
|
||||
<HoppSmartSpinner v-else />
|
||||
</HoppSmartLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppSmartLink, HoppSmartSpinner } from "../smart"
|
||||
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>
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as HoppButtonPrimary } from "./Primary.vue"
|
||||
export { default as HoppButtonSecondary } from "./Secondary.vue"
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./button"
|
||||
export * from "./smart"
|
||||
@@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<HoppSmartLink
|
||||
: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 }}
|
||||
</HoppSmartLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import HoppSmartLink from "./Link.vue"
|
||||
import type { Component } from "vue"
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
to: string
|
||||
exact: boolean
|
||||
blank: boolean
|
||||
label: string
|
||||
icon: Component | null
|
||||
svg: Component | null
|
||||
color: string
|
||||
disabled: boolean
|
||||
reverse: boolean
|
||||
}>(),
|
||||
{
|
||||
to: "",
|
||||
exact: true,
|
||||
blank: false,
|
||||
label: "",
|
||||
icon: null,
|
||||
svg: null,
|
||||
color: "",
|
||||
disabled: false,
|
||||
reverse: false,
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -1,236 +0,0 @@
|
||||
<template>
|
||||
<div class="autocomplete-wrapper">
|
||||
<input
|
||||
ref="acInput"
|
||||
v-model="text"
|
||||
:type="type"
|
||||
autocomplete="off"
|
||||
:placeholder="placeholder"
|
||||
:spellcheck="spellcheck"
|
||||
:autocapitalize="autocapitalize"
|
||||
:class="styles"
|
||||
@input.stop="onInput"
|
||||
@keyup="updateSuggestions"
|
||||
@click="updateSuggestions"
|
||||
@keydown="handleKeystroke"
|
||||
@change="emit('change', $event)"
|
||||
/>
|
||||
<ul v-if="suggestions.length > 0 && suggestionsVisible" class="suggestions">
|
||||
<li
|
||||
v-for="(suggestion, index) in suggestions"
|
||||
:key="`suggestion-${index}`"
|
||||
:class="{ active: currentSuggestionIndex === index }"
|
||||
@click.prevent="forceSuggestion(suggestion)"
|
||||
>
|
||||
{{ suggestion }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, computed, watch } from "vue"
|
||||
|
||||
const acInput = ref<HTMLInputElement>()
|
||||
|
||||
const props = defineProps({
|
||||
spellcheck: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
required: false,
|
||||
},
|
||||
|
||||
autocapitalize: {
|
||||
type: String,
|
||||
default: "off",
|
||||
required: false,
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
|
||||
source: {
|
||||
type: Array<string>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
|
||||
styles: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
|
||||
type: {
|
||||
type: String,
|
||||
default: "text",
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", v: string): void
|
||||
(e: "change", v: Event): void
|
||||
}>()
|
||||
|
||||
const text = ref(props.value)
|
||||
const selectionStart = ref(0)
|
||||
const currentSuggestionIndex = ref(-1)
|
||||
const suggestionsVisible = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
updateSuggestions({
|
||||
target: acInput,
|
||||
})
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(newValue) => {
|
||||
text.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
const suggestions = computed(() => {
|
||||
const input = text.value.substring(0, selectionStart.value)
|
||||
return (
|
||||
props.source
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.toLowerCase().startsWith(input.toLowerCase()) &&
|
||||
input.toLowerCase() !== entry.toLowerCase()
|
||||
)
|
||||
// We only want the top 10 suggestions.
|
||||
.slice(0, 10)
|
||||
)
|
||||
})
|
||||
|
||||
function updateSuggestions(event: any) {
|
||||
// Hide suggestions if ESC pressed.
|
||||
if (event.code && event.code === "Escape") {
|
||||
event.preventDefault()
|
||||
suggestionsVisible.value = false
|
||||
currentSuggestionIndex.value = -1
|
||||
return
|
||||
}
|
||||
|
||||
// As suggestions is a reactive property, this implicitly
|
||||
// causes suggestions to update.
|
||||
selectionStart.value = acInput.value?.selectionStart ?? -1
|
||||
suggestionsVisible.value = true
|
||||
}
|
||||
|
||||
const onInput = (e: Event) => {
|
||||
emit("input", (e.target as HTMLInputElement).value)
|
||||
updateSuggestions(e)
|
||||
}
|
||||
|
||||
function forceSuggestion(suggestion: string) {
|
||||
text.value = suggestion
|
||||
|
||||
selectionStart.value = text.value.length
|
||||
suggestionsVisible.value = true
|
||||
currentSuggestionIndex.value = -1
|
||||
|
||||
emit("input", text.value)
|
||||
}
|
||||
|
||||
function handleKeystroke(event: any) {
|
||||
switch (event.code) {
|
||||
case "ArrowUp":
|
||||
event.preventDefault()
|
||||
|
||||
currentSuggestionIndex.value =
|
||||
currentSuggestionIndex.value - 1 >= 0
|
||||
? currentSuggestionIndex.value - 1
|
||||
: 0
|
||||
break
|
||||
|
||||
case "ArrowDown":
|
||||
event.preventDefault()
|
||||
|
||||
currentSuggestionIndex.value =
|
||||
currentSuggestionIndex.value < suggestions.value.length - 1
|
||||
? currentSuggestionIndex.value + 1
|
||||
: suggestions.value.length - 1
|
||||
break
|
||||
|
||||
case "Enter":
|
||||
event.preventDefault()
|
||||
|
||||
if (currentSuggestionIndex.value > -1)
|
||||
forceSuggestion(
|
||||
suggestions.value.find(
|
||||
(_item, index) => index === currentSuggestionIndex.value
|
||||
)!
|
||||
)
|
||||
break
|
||||
|
||||
case "Tab": {
|
||||
event.preventDefault()
|
||||
|
||||
const activeSuggestion =
|
||||
suggestions.value[
|
||||
currentSuggestionIndex.value >= 0 ? currentSuggestionIndex.value : 0
|
||||
]
|
||||
|
||||
if (!activeSuggestion) return
|
||||
|
||||
forceSuggestion(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 absolute;
|
||||
@apply hidden;
|
||||
@apply bg-popover;
|
||||
@apply -left-px;
|
||||
@apply z-50;
|
||||
@apply shadow-lg;
|
||||
@apply max-h-46;
|
||||
@apply overflow-y-auto;
|
||||
@apply border-b border-x border-divider;
|
||||
|
||||
top: calc(100% + 1px);
|
||||
border-radius: 0 0 8px 8px;
|
||||
|
||||
li {
|
||||
@apply w-full;
|
||||
@apply block;
|
||||
@apply py-2 px-4;
|
||||
@apply text-secondary;
|
||||
@apply font-semibold;
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 0 8px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
@apply bg-primaryDark;
|
||||
@apply text-secondaryDark;
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,107 +0,0 @@
|
||||
<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="checkboxID"
|
||||
type="checkbox"
|
||||
:name="name"
|
||||
class="checkbox"
|
||||
:checked="on"
|
||||
@change="emit('change')"
|
||||
/>
|
||||
<label
|
||||
:for="checkboxID"
|
||||
class="pl-0 font-semibold truncate align-middle cursor-pointer"
|
||||
>
|
||||
<slot></slot>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/*
|
||||
This checkboxIDCounter is tracked in the global scope in order to ensure that each checkbox has a unique ID.
|
||||
When we use this component multiple times on the same page, we need to ensure that each checkbox has a unique ID.
|
||||
This is because the label's for attribute needs to match the checkbox's id attribute.
|
||||
|
||||
That's why we use a global counter that increments each time we use this component.
|
||||
*/
|
||||
let checkboxIDCounter = 564275
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Unique ID for checkbox
|
||||
const checkboxID = `checkbox-${checkboxIDCounter++}`
|
||||
defineProps({
|
||||
on: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: "checkbox",
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "change"): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.checkbox[type="checkbox"] {
|
||||
@apply relative;
|
||||
@apply appearance-none;
|
||||
@apply hidden;
|
||||
|
||||
& + label {
|
||||
@apply inline-flex items-center justify-center;
|
||||
@apply cursor-pointer;
|
||||
@apply relative;
|
||||
|
||||
&::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 mr-2;
|
||||
@apply transition;
|
||||
@apply empty:mr-0;
|
||||
content: "";
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
border: solid;
|
||||
border-width: 0 1.9px 1.9px 0;
|
||||
height: 0.6rem;
|
||||
width: 0.3rem;
|
||||
left: 0.35rem;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
transform: rotate(45deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:checked + label::before {
|
||||
@apply bg-accent;
|
||||
@apply border-accent;
|
||||
@apply text-accentContrast;
|
||||
}
|
||||
|
||||
&:checked + label::after {
|
||||
@apply text-accentContrast;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="confirm ?? t?.('modal.confirm') ?? 'Confirm'"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="text-center">
|
||||
{{ title }}
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
v-focus
|
||||
:label="yes ?? t?.('action.yes') ?? 'Yes'"
|
||||
:loading="!!loadingState"
|
||||
outline
|
||||
@click="resolve"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="no ?? t?.('action.no') ?? 'No'"
|
||||
filled
|
||||
outline
|
||||
@click="hideModal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from "vue"
|
||||
import { HoppButtonPrimary, HoppButtonSecondary } from "../button"
|
||||
import { HoppSmartModal } from "."
|
||||
import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../plugin"
|
||||
|
||||
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>
|
||||
@@ -1,46 +0,0 @@
|
||||
<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"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
: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 { HoppButtonSecondary } from "../button"
|
||||
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 "./../../plugin"
|
||||
|
||||
const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
|
||||
|
||||
const expand = ref(false)
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
less?: string
|
||||
more?: string
|
||||
}>(),
|
||||
{
|
||||
less: "Less",
|
||||
more: "More",
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -1,12 +0,0 @@
|
||||
<template>
|
||||
<span
|
||||
class="inline-flex items-center space-x-1 justify-center rounded px-2 bg-primaryDark"
|
||||
>
|
||||
<IconLucideFile class="opacity-75 svg-icons" />
|
||||
<span class="truncate max-w-[16rem]"><slot></slot></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconLucideFile from "~icons/lucide/file"
|
||||
</script>
|
||||
@@ -1,89 +0,0 @@
|
||||
<template>
|
||||
<div class="relative flex" :class="styles">
|
||||
<input
|
||||
:id="inputID"
|
||||
class="input"
|
||||
ref="inputRef"
|
||||
:class="inputStyles"
|
||||
v-model="inputText"
|
||||
:placeholder="placeholder"
|
||||
:type="type"
|
||||
autocomplete="off"
|
||||
required
|
||||
:disabled="disabled"
|
||||
/>
|
||||
|
||||
<label v-if="label.length > 0" :for="inputID"> {{ label }} </label>
|
||||
<slot name="button"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/*
|
||||
This inputIDCounter is tracked in the global scope in order to ensure that each input has a unique ID.
|
||||
When we use this component multiple times on the same page, we need to ensure that each input has a unique ID.
|
||||
This is because the label's for attribute needs to match the input's id attribute.
|
||||
|
||||
That's why we use a global counter that increments each time we use this component.
|
||||
*/
|
||||
let inputIDCounter = 564275
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onKeyStroke, useVModel } from "@vueuse/core"
|
||||
import { defineProps, onMounted, ref, nextTick } from "vue"
|
||||
|
||||
// Unique ID for input
|
||||
const inputID = `input-${inputIDCounter++}`
|
||||
|
||||
const inputRef = ref()
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.autofocus) {
|
||||
await nextTick()
|
||||
inputRef.value?.focus()
|
||||
}
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id: string
|
||||
styles: string
|
||||
modelValue: string | null
|
||||
placeholder: string
|
||||
inputStyles: string | (string | false)[]
|
||||
type: string
|
||||
label: string
|
||||
disabled: boolean
|
||||
autofocus: boolean
|
||||
}>(),
|
||||
{
|
||||
id: "",
|
||||
styles: "",
|
||||
modelValue: "",
|
||||
placeholder: "",
|
||||
inputStyles: "input",
|
||||
type: "text",
|
||||
label: "",
|
||||
disabled: false,
|
||||
autofocus: true,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "submit"): void
|
||||
(e: "update:modelValue", v: string): void
|
||||
}>()
|
||||
|
||||
const inputText = useVModel(props, "modelValue", emit)
|
||||
|
||||
onKeyStroke(
|
||||
"Enter",
|
||||
(e) => {
|
||||
if (!e.repeat) {
|
||||
return emit("submit")
|
||||
}
|
||||
},
|
||||
{ target: inputRef, eventName: "keydown" }
|
||||
)
|
||||
</script>
|
||||
@@ -1,38 +0,0 @@
|
||||
<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>
|
||||
@@ -1,150 +0,0 @@
|
||||
<template>
|
||||
<HoppSmartLink
|
||||
: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>
|
||||
<HoppSmartSpinner 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 max-w-[16rem]">
|
||||
{{ 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-6 -mr-2 svg-icons"
|
||||
:class="{ 'text-accent': activeInfoIcon }"
|
||||
/>
|
||||
<div v-if="shortcut.length" class="ml-4 inline-flex <sm:hidden font-medium">
|
||||
<kbd
|
||||
v-for="(key, index) in shortcut"
|
||||
:key="`key-${index}`"
|
||||
class="-mr-2 shortcut-key"
|
||||
>
|
||||
{{ key }}
|
||||
</kbd>
|
||||
</div>
|
||||
</HoppSmartLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import HoppSmartLink from "./Link.vue"
|
||||
import HoppSmartSpinner from "./Spinner.vue"
|
||||
import { defineComponent } from "vue"
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
HoppSmartLink,
|
||||
HoppSmartSpinner,
|
||||
},
|
||||
props: {
|
||||
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>
|
||||
@@ -1,72 +0,0 @@
|
||||
<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">
|
||||
// Do not import RouterLink, for some reason that breaks things ¯\_(ツ)_/¯
|
||||
/**
|
||||
* 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 { 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>
|
||||
@@ -1,215 +0,0 @@
|
||||
<template>
|
||||
<Transition name="fade" appear @leave="onTransitionLeaveStart">
|
||||
<div
|
||||
ref="modal"
|
||||
class="fixed inset-0 z-[1000] overflow-y-auto transition"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="flex min-h-screen items-end justify-center 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 bg-primaryLight opacity-80 focus:outline-none"
|
||||
tabindex="0"
|
||||
@click="!dialog ? close() : null"
|
||||
></div>
|
||||
</div>
|
||||
</Transition>
|
||||
<span
|
||||
v-if="placement === 'center'"
|
||||
class="sm:h-screen sm:align-middle <sm:hidden"
|
||||
aria-hidden="true"
|
||||
>​</span
|
||||
>
|
||||
<Transition name="bounce" appear>
|
||||
<div
|
||||
class="inline-block w-full transform overflow-hidden border-dividerDark bg-primary text-left align-bottom shadow-lg transition-all sm:rounded-xl sm:border sm:align-middle"
|
||||
:class="[{ 'mt-16 md:mb-8': placement === 'top' }, styles]"
|
||||
>
|
||||
<div
|
||||
v-if="title"
|
||||
class="flex items-center justify-between border-b border-dividerLight"
|
||||
:class="{ 'p-4': !fullWidth }"
|
||||
>
|
||||
<div class="flex items-center flex-1 justify-start">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<h3 class="heading">
|
||||
{{ title }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="flex items-center flex-1 justify-end">
|
||||
<HoppButtonSecondary
|
||||
v-if="dimissible"
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
||||
:title="closeText ?? t?.('action.close') ?? 'Close'"
|
||||
:icon="IconX"
|
||||
@click="close"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col overflow-y-auto max-h-[55vh]"
|
||||
:class="{ 'p-4': !fullWidth && !fullWidthBody }"
|
||||
>
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasFooterSlot"
|
||||
class="flex flex-1 items-center justify-between 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 { HoppButtonSecondary } from "../button"
|
||||
import IconX from "~icons/lucide/x"
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
useSlots,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
inject,
|
||||
} from "vue"
|
||||
import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../plugin"
|
||||
|
||||
const { t, onModalOpen, onModalClose } =
|
||||
inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
dialog: boolean
|
||||
title: string
|
||||
dimissible: boolean
|
||||
placement: string
|
||||
fullWidth: boolean
|
||||
fullWidthBody: boolean
|
||||
styles: string
|
||||
closeText: string | null
|
||||
}>(),
|
||||
{
|
||||
dialog: false,
|
||||
title: "",
|
||||
dimissible: true,
|
||||
placement: "top",
|
||||
fullWidth: false,
|
||||
fullWidthBody: false,
|
||||
styles: "sm:max-w-lg",
|
||||
closeText: 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>
|
||||
@@ -1,38 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
tabindex="0"
|
||||
class="relative rounded-full flex items-center justify-center cursor-pointer focus:outline-none focus-visible:ring focus-visible:ring-primaryDark"
|
||||
>
|
||||
<Avatar
|
||||
:size="size"
|
||||
:name="name"
|
||||
:square="false"
|
||||
:colors="['#FFAD08', '#EDD75A', '#73B06F', '#0C8F8F', '#405059']"
|
||||
variant="beam"
|
||||
/>
|
||||
<span
|
||||
v-if="indicator"
|
||||
class="border-primary rounded-full border-2 h-2.5 -top-0.5 -right-0.5 w-2.5 absolute"
|
||||
:class="indicatorStyles"
|
||||
></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Avatar } from "@boringer-avatars/vue3"
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
name: string
|
||||
indicator: boolean
|
||||
indicatorStyles: string
|
||||
size: number
|
||||
}>(),
|
||||
{
|
||||
name: "",
|
||||
indicator: false,
|
||||
indicatorStyles: "bg-green-500",
|
||||
size: 22,
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -1,49 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center p-4">
|
||||
<img
|
||||
v-if="src"
|
||||
:src="src"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center"
|
||||
:class="large ? 'w-32 h-32' : 'w-16 h-16'"
|
||||
:alt="alt"
|
||||
/>
|
||||
<slot name="icon"></slot>
|
||||
<span v-if="heading" class="font-semibold mt-2 text-center">
|
||||
{{ heading }}
|
||||
</span>
|
||||
<span
|
||||
v-if="text"
|
||||
class="max-w-sm mt-2 text-center whitespace-normal text-secondaryLight text-tiny"
|
||||
>
|
||||
{{ text }}
|
||||
</span>
|
||||
<div v-if="hasBody" class="mt-4">
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useSlots } from "vue"
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
src?: string
|
||||
alt?: string
|
||||
heading?: string
|
||||
text?: string
|
||||
large?: boolean
|
||||
}>(),
|
||||
{
|
||||
alt: "",
|
||||
text: "",
|
||||
}
|
||||
)
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const hasBody = computed(() => {
|
||||
return !!slots.body
|
||||
})
|
||||
</script>
|
||||
@@ -1,57 +0,0 @@
|
||||
<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>
|
||||
@@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<HoppSmartItem :label="label" :icon="selected ? IconCircleDot : IconCircle" :active="selected" role="radio"
|
||||
:aria-checked="selected" @click="emit('change', value)" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppSmartItem } from "."
|
||||
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>
|
||||
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<HoppSmartRadio
|
||||
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">
|
||||
import { HoppSmartRadio } from "."
|
||||
|
||||
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>
|
||||
@@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<div class="select-wrapper">
|
||||
<span class="down-icon text-xs">
|
||||
<IconChevronDown />
|
||||
</span>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconChevronDown from '~icons/lucide/chevron-down'
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.select-wrapper {
|
||||
@apply flex flex-1;
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.down-icon {
|
||||
@apply absolute;
|
||||
@apply flex;
|
||||
@apply inset-y-0;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply pointer-events-none;
|
||||
@apply text-current;
|
||||
@apply right-3;
|
||||
}
|
||||
</style>
|
||||
@@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<div class="z-[1000]">
|
||||
<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">
|
||||
<HoppButtonSecondary :icon="IconX" @click="close()" />
|
||||
</span>
|
||||
</div>
|
||||
<slot name="content"></slot>
|
||||
</aside>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppButtonSecondary } from "../button"
|
||||
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>
|
||||
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<IconLucideLoader class="animate-spin svg-icons" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconLucideLoader from "~icons/lucide/loader"
|
||||
</script>
|
||||
@@ -1,80 +0,0 @@
|
||||
<template>
|
||||
<div v-if="shouldRender" v-show="active" class="flex flex-1 flex-col">
|
||||
<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,
|
||||
isUnmounting,
|
||||
} = 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(() => {
|
||||
if (isUnmounting.value) return
|
||||
removeTabEntry(props.id)
|
||||
})
|
||||
</script>
|
||||
@@ -1,69 +0,0 @@
|
||||
<template>
|
||||
<div class="overflow-auto rounded-md border border-dividerDark shadow-md">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<slot name="head">
|
||||
<tr
|
||||
class="text-secondary border-b border-dividerDark text-sm text-left bg-primaryLight"
|
||||
>
|
||||
<th v-for="th in headings" scope="col" class="px-6 py-3">
|
||||
{{ th.label ?? th.key }}
|
||||
</th>
|
||||
</tr>
|
||||
</slot>
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y divide-divider">
|
||||
<!-- We are using slot props for future proofing so that in future, we can implement features like filtering -->
|
||||
<slot name="body" :list="list">
|
||||
<tr
|
||||
v-for="(rowData, rowIndex) in list"
|
||||
:key="rowIndex"
|
||||
class="text-secondaryDark hover:bg-divider hover:cursor-pointer rounded-xl"
|
||||
:class="{ 'divide-x divide-divider': showYBorder }"
|
||||
>
|
||||
<td
|
||||
v-for="cellHeading in headings"
|
||||
:key="cellHeading.key"
|
||||
@click="!cellHeading.preventClick && onRowClicked(rowData)"
|
||||
class="max-w-[10rem] pl-6 py-1"
|
||||
>
|
||||
<!-- Dynamic column slot -->
|
||||
<slot :name="cellHeading.key" :item="rowData">
|
||||
<!-- Generic implementation of the column -->
|
||||
<div class="flex flex-col truncate">
|
||||
<span class="truncate">
|
||||
{{ rowData[cellHeading.key] ?? "-" }}
|
||||
</span>
|
||||
</div>
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
</slot>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup generic="Item extends Record<string, unknown>">
|
||||
export type CellHeading = {
|
||||
key: string
|
||||
label?: string
|
||||
preventClick?: boolean
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
/** Whether to show the vertical border between columns */
|
||||
showYBorder?: boolean
|
||||
/** The list of items to be displayed in the table */
|
||||
list?: Item[]
|
||||
/** The headings of the table */
|
||||
headings?: CellHeading[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "onRowClicked", item: Item): void
|
||||
}>()
|
||||
|
||||
const onRowClicked = (item: Item) => emit("onRowClicked", item)
|
||||
</script>
|
||||
@@ -1,261 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex h-full flex-1 flex-nowrap"
|
||||
:class="{ '!h-auto !flex-col': !vertical }"
|
||||
>
|
||||
<div
|
||||
class="tabs relative border-dividerLight"
|
||||
:class="[vertical ? 'border-r' : 'border-b', styles]"
|
||||
>
|
||||
<div class="flex flex-1">
|
||||
<div
|
||||
class="flex flex-1 justify-between"
|
||||
: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 },
|
||||
{ '!cursor-not-allowed opacity-75': 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"
|
||||
:class="{ 'mr-2': tabMeta.label && !vertical }"
|
||||
/>
|
||||
<span v-if="tabMeta.label && !vertical">{{ tabMeta.label }}</span>
|
||||
<span
|
||||
v-if="tabMeta.info && tabMeta.info !== 'null'"
|
||||
class="tab-info"
|
||||
>
|
||||
{{ tabMeta.info }}
|
||||
</span>
|
||||
<span
|
||||
v-if="tabMeta.indicator"
|
||||
class="ml-2 h-1 w-1 rounded-full bg-accentLight"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="contents h-full w-full"
|
||||
:class="[
|
||||
{
|
||||
'!flex flex-1 flex-col 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, Ref } from "vue"
|
||||
import { ref, ComputedRef, computed, provide, onBeforeUnmount } 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
|
||||
isUnmounting: Ref<boolean>
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
const isUnmounting = ref(false)
|
||||
|
||||
provide<TabProvider>("tabs-system", {
|
||||
renderInactive: computed(() => props.renderInactiveTabs),
|
||||
activeTabID: computed(() => props.modelValue),
|
||||
addTabEntry,
|
||||
updateTabEntry,
|
||||
removeTabEntry,
|
||||
isUnmounting,
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
isUnmounting.value = true
|
||||
})
|
||||
|
||||
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 px-4 py-2;
|
||||
@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-[''];
|
||||
@apply focus:after:bg-divider;
|
||||
|
||||
.tab-info {
|
||||
@apply inline-flex;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply px-1;
|
||||
@apply min-w-[1rem];
|
||||
@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>
|
||||
@@ -1,89 +0,0 @@
|
||||
<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>
|
||||
@@ -1,72 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div
|
||||
v-if="rootNodes.status === 'loaded' && rootNodes.data.length > 0"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<div
|
||||
v-for="rootNode in rootNodes.data"
|
||||
:key="rootNode.id"
|
||||
class="flex flex-col flex-1"
|
||||
>
|
||||
<SmartTreeBranch
|
||||
:root-nodes-length="rootNodes.data.length"
|
||||
:node-item="rootNode"
|
||||
:adapter="adapter as SmartTreeAdapter<T>"
|
||||
>
|
||||
<template
|
||||
#default="{ node, toggleChildren, isOpen, highlightChildren }"
|
||||
>
|
||||
<slot
|
||||
name="content"
|
||||
:node="node as TreeNode<T>"
|
||||
:toggle-children="toggleChildren as () => void"
|
||||
:is-open="isOpen as boolean"
|
||||
:highlight-children="(id: string | null) => highlightChildren(id)"
|
||||
></slot>
|
||||
</template>
|
||||
|
||||
<template #emptyNode="{ node }">
|
||||
<slot name="emptyNode" :node="node"></slot>
|
||||
</template>
|
||||
</SmartTreeBranch>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="rootNodes.status === 'loading'"
|
||||
class="flex flex-col items-center justify-center flex-1 p-4"
|
||||
>
|
||||
<SmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t?.("state.loading") }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="rootNodes.status === 'loaded' && rootNodes.data.length === 0"
|
||||
class="flex flex-col flex-1"
|
||||
>
|
||||
<!-- eslint-disable-next-line vue/no-deprecated-filter -->
|
||||
<slot name="emptyNode" :node="null as TreeNode<T> | null"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T extends any">
|
||||
import { computed, inject } from "vue"
|
||||
import SmartTreeBranch from "./TreeBranch.vue"
|
||||
import SmartSpinner from "./Spinner.vue"
|
||||
import { SmartTreeAdapter, TreeNode } from "~/helpers/treeAdapter"
|
||||
import { HOPP_UI_OPTIONS, HoppUIPluginOptions } from "./../../plugin"
|
||||
const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* The adapter that will be used to fetch the tree data
|
||||
* @template T The type of the data that will be stored in the tree
|
||||
*/
|
||||
adapter: SmartTreeAdapter<T>
|
||||
}>()
|
||||
|
||||
/**
|
||||
* Fetch the root nodes from the adapter by passing the node id as null
|
||||
*/
|
||||
const rootNodes = computed(() => props.adapter.getChildren(null).value)
|
||||
</script>
|
||||
@@ -1,135 +0,0 @@
|
||||
<template>
|
||||
<slot
|
||||
:node="nodeItem"
|
||||
:toggle-children="toggleNodeChildren"
|
||||
:is-open="isNodeOpen"
|
||||
:highlight-children="(id: string | null) => highlightNodeChildren(id)"
|
||||
></slot>
|
||||
|
||||
<!-- This is a performance optimization trick -->
|
||||
<!-- Once expanded, Vue will traverse through the children and expand the tree up
|
||||
but when we collapse, the tree and the components are disposed. This is wasteful
|
||||
and comes with performance issues if the children list is expensive to render.
|
||||
Hence, here we render children only when first expanded, and after that, even if collapsed,
|
||||
we just hide the children.
|
||||
-->
|
||||
<div v-if="childrenRendered" v-show="showChildren" class="flex">
|
||||
<div
|
||||
class="ml-[1.375rem] flex w-0.5 transform cursor-nsResize bg-dividerLight transition hover:scale-x-125 hover:bg-dividerDark"
|
||||
@click="toggleNodeChildren"
|
||||
></div>
|
||||
<div
|
||||
v-if="childNodes.status === 'loaded' && childNodes.data.length > 0"
|
||||
class="flex flex-1 flex-col truncate"
|
||||
:class="{
|
||||
'bg-divider': highlightNode,
|
||||
}"
|
||||
>
|
||||
<SmartTreeBranch
|
||||
v-for="childNode in childNodes.data"
|
||||
:key="childNode.id"
|
||||
:node-item="childNode"
|
||||
:adapter="adapter"
|
||||
>
|
||||
<!-- The child slot is given a dynamic name in order to not break Volar -->
|
||||
<template
|
||||
#[CHILD_SLOT_NAME]="{
|
||||
node,
|
||||
toggleChildren,
|
||||
isOpen,
|
||||
highlightChildren,
|
||||
}"
|
||||
>
|
||||
<!-- Casting to help with type checking -->
|
||||
<slot
|
||||
:node="node as TreeNode<T>"
|
||||
:toggle-children="toggleChildren as () => void"
|
||||
:is-open="isOpen as boolean"
|
||||
:highlight-children="
|
||||
(id: string | null) => highlightChildren(id) as void
|
||||
"
|
||||
></slot>
|
||||
</template>
|
||||
<template #emptyNode="{ node }">
|
||||
<slot name="emptyNode" :node="node"></slot>
|
||||
</template>
|
||||
</SmartTreeBranch>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="childNodes.status === 'loading'"
|
||||
class="flex flex-1 flex-col items-center justify-center p-4"
|
||||
>
|
||||
<SmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t?.("state.loading") }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="childNodes.status === 'loaded' && childNodes.data.length === 0"
|
||||
class="flex flex-1 flex-col"
|
||||
>
|
||||
<slot name="emptyNode" :node="nodeItem"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T extends any">
|
||||
import { computed, inject, ref } from "vue"
|
||||
import SmartTreeBranch from "./TreeBranch.vue"
|
||||
import SmartSpinner from "./Spinner.vue"
|
||||
import { SmartTreeAdapter, TreeNode } from "~/helpers/treeAdapter"
|
||||
import { HOPP_UI_OPTIONS, HoppUIPluginOptions } from "./../../plugin"
|
||||
const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* The node item that will be used to render the tree branch
|
||||
* @template T The type of the data passed to the tree branch
|
||||
*/
|
||||
adapter: SmartTreeAdapter<T>
|
||||
/**
|
||||
* The node item that will be used to render the tree branch content
|
||||
*/
|
||||
nodeItem: TreeNode<T>
|
||||
/**
|
||||
* Total number of rootNode
|
||||
*/
|
||||
rootNodesLength?: number
|
||||
}>()
|
||||
|
||||
const CHILD_SLOT_NAME = "default"
|
||||
|
||||
const isOnlyRootChild = computed(() => props.rootNodesLength === 1)
|
||||
|
||||
/**
|
||||
* Marks whether the children on this branch were ever rendered
|
||||
* See the usage inside '<template>' for more info
|
||||
*/
|
||||
const childrenRendered = ref(isOnlyRootChild.value)
|
||||
|
||||
const showChildren = ref(isOnlyRootChild.value)
|
||||
const isNodeOpen = ref(isOnlyRootChild.value)
|
||||
|
||||
const highlightNode = ref(false)
|
||||
|
||||
/**
|
||||
* Fetch the child nodes from the adapter by passing the node id of the current node
|
||||
*/
|
||||
const childNodes = computed(
|
||||
() => props.adapter.getChildren(props.nodeItem.id).value
|
||||
)
|
||||
|
||||
const toggleNodeChildren = () => {
|
||||
if (!childrenRendered.value) childrenRendered.value = true
|
||||
|
||||
showChildren.value = !showChildren.value
|
||||
isNodeOpen.value = !isNodeOpen.value
|
||||
}
|
||||
|
||||
const highlightNodeChildren = (id: string | null) => {
|
||||
if (id) {
|
||||
highlightNode.value = true
|
||||
} else {
|
||||
highlightNode.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="shouldRender"
|
||||
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 = withDefaults(
|
||||
defineProps<{
|
||||
label: string | null
|
||||
info: string | null
|
||||
id: string
|
||||
isRemovable: boolean
|
||||
closeVisibility: "hover" | "always" | "never"
|
||||
selected: boolean
|
||||
}>(),
|
||||
{
|
||||
label: null,
|
||||
info: null,
|
||||
isRemovable: true,
|
||||
closeVisibility: "always",
|
||||
selected: false,
|
||||
}
|
||||
)
|
||||
|
||||
const tabMeta = computed<TabMeta>(() => ({
|
||||
info: props.info,
|
||||
label: props.label,
|
||||
isRemovable: props.isRemovable,
|
||||
icon: slots.icon,
|
||||
suffix: slots.suffix,
|
||||
tabhead: slots.tabhead,
|
||||
closeVisibility: props.closeVisibility,
|
||||
}))
|
||||
|
||||
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>
|
||||
@@ -1,480 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 h-auto overflow-y-hidden flex-nowrap">
|
||||
<div
|
||||
class="sticky top-0 z-10 flex-shrink-0 overflow-x-auto divide-x divide-dividerLight bg-primaryLight tabs group-tabs"
|
||||
>
|
||||
<div
|
||||
class="flex flex-1 flex-shrink-0 w-0 overflow-hidden"
|
||||
ref="scrollContainer"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between divide-x divide-dividerLight"
|
||||
@wheel.prevent="scroll"
|
||||
>
|
||||
<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}`"
|
||||
:id="`removable-tab-${tabID}`"
|
||||
class="tab group"
|
||||
:class="[{ active: modelValue === tabID }]"
|
||||
:aria-label="tabMeta.label || ''"
|
||||
role="button"
|
||||
@keyup.enter="selectTab(tabID)"
|
||||
@click="selectTab(tabID)"
|
||||
>
|
||||
<span
|
||||
v-if="tabMeta.icon"
|
||||
class="flex items-center justify-center cursor-pointer"
|
||||
>
|
||||
<component :is="tabMeta.icon" class="w-4 h-4 svg-icons" />
|
||||
</span>
|
||||
|
||||
<div
|
||||
v-if="!tabMeta.tabhead"
|
||||
class="w-full px-2 text-left truncate"
|
||||
>
|
||||
<span class="truncate">
|
||||
{{ tabMeta.label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="w-full text-left truncate">
|
||||
<component :is="tabMeta.tabhead" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="tabMeta.suffix"
|
||||
class="flex items-center justify-center"
|
||||
>
|
||||
<component :is="tabMeta.suffix" />
|
||||
</div>
|
||||
|
||||
<HoppButtonSecondary
|
||||
v-if="tabMeta.isRemovable"
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
||||
:icon="IconX"
|
||||
:title="closeText ?? t?.('action.close') ?? 'Close'"
|
||||
:class="[
|
||||
{ active: modelValue === tabID },
|
||||
{
|
||||
flex: tabMeta.closeVisibility === 'always',
|
||||
'group-hover:flex hidden':
|
||||
tabMeta.closeVisibility === 'hover',
|
||||
hidden: tabMeta.closeVisibility === 'never',
|
||||
},
|
||||
'close',
|
||||
]"
|
||||
class="rounded !p-0.25"
|
||||
@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-20"
|
||||
>
|
||||
<span
|
||||
v-if="canAddNewTab"
|
||||
class="flex items-center justify-center h-full px-3 bg-primaryLight z-[8]"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="newText ?? t?.('action.new') ?? 'New'"
|
||||
:icon="IconPlus"
|
||||
class="rounded create-new-tab !text-secondaryDark !p-1"
|
||||
filled
|
||||
@click="addTab"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasActions" :class="mdAndLarger ? 'w-64' : 'w-28'">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
:max="MAX_SCROLL_VALUE"
|
||||
v-model="thumbPosition"
|
||||
class="absolute bottom-0 left-0 hidden slider"
|
||||
:class="{
|
||||
'!block': scrollThumb.show,
|
||||
}"
|
||||
:style="[
|
||||
`--thumb-width: ${scrollThumb.width}px`,
|
||||
`width: calc(100% - ${
|
||||
hasActions ? (mdAndLarger ? '19rem' : '10rem') : '3rem'
|
||||
})`,
|
||||
]"
|
||||
id="myRange"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full h-full contents">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppButtonSecondary } from "../button"
|
||||
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,
|
||||
watch,
|
||||
nextTick,
|
||||
useSlots,
|
||||
} from "vue"
|
||||
import {
|
||||
breakpointsTailwind,
|
||||
useBreakpoints,
|
||||
useElementSize,
|
||||
} from "@vueuse/core"
|
||||
import type { Slot } from "vue"
|
||||
import draggable from "vuedraggable-es"
|
||||
import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../index"
|
||||
|
||||
export type TabMeta = {
|
||||
label: string | null
|
||||
icon: Slot | undefined
|
||||
suffix: Slot | undefined
|
||||
tabhead: Slot | undefined
|
||||
info: string | null
|
||||
isRemovable: boolean
|
||||
closeVisibility: "hover" | "always" | "never"
|
||||
}
|
||||
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 breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const mdAndLarger = breakpoints.greater("md")
|
||||
|
||||
const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
styles: string
|
||||
modelValue: string
|
||||
renderInactiveTabs: boolean
|
||||
canAddNewTab: boolean
|
||||
newText: string | null
|
||||
closeText: string | null
|
||||
}>(),
|
||||
{
|
||||
styles: "",
|
||||
renderInactiveTabs: false,
|
||||
canAddNewTab: true,
|
||||
newText: null,
|
||||
closeText: 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 slots = useSlots()
|
||||
|
||||
const hasActions = computed(() => {
|
||||
return !!slots.actions
|
||||
})
|
||||
|
||||
const throwError = (message: string): never => {
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
const TAB_WIDTH = 184
|
||||
const tabEntries = ref<Array<[string, TabMeta]>>([])
|
||||
const tabStyles = computed(() => ({
|
||||
maxWidth: `${tabEntries.value.length * TAB_WIDTH}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}`))
|
||||
)
|
||||
}
|
||||
const sortTabs = (e: {
|
||||
oldDraggableIndex: number
|
||||
newDraggableIndex: number
|
||||
}) => {
|
||||
emit("sort", {
|
||||
oldIndex: e.oldDraggableIndex,
|
||||
newIndex: e.newDraggableIndex,
|
||||
})
|
||||
}
|
||||
provide<TabProvider>("tabs-system", {
|
||||
renderInactive: computed(() => props.renderInactiveTabs),
|
||||
activeTabID: computed(() => props.modelValue),
|
||||
addTabEntry,
|
||||
updateTabEntry,
|
||||
removeTabEntry,
|
||||
})
|
||||
const selectTab = (id: string) => {
|
||||
emit("update:modelValue", id)
|
||||
}
|
||||
const addTab = () => {
|
||||
emit("addTab")
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll related properties
|
||||
*/
|
||||
|
||||
const MAX_SCROLL_VALUE = 500
|
||||
const scrollContainer = ref<HTMLElement>()
|
||||
const { width: scrollContainerWidth } = useElementSize(scrollContainer)
|
||||
const thumbPosition = ref(0)
|
||||
|
||||
const scrollThumb = computed(() => {
|
||||
const clientWidth = scrollContainerWidth.value ?? 0
|
||||
const scrollWidth = tabEntries.value.length * 184
|
||||
|
||||
return {
|
||||
width: (clientWidth / scrollWidth) * clientWidth || 300,
|
||||
show: clientWidth ? scrollWidth > clientWidth : false,
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
* Scroll with mouse wheel
|
||||
*/
|
||||
const scroll = (e: WheelEvent) => {
|
||||
scrollContainer.value!.scrollLeft += e.deltaY
|
||||
scrollContainer.value!.scrollLeft += e.deltaX
|
||||
|
||||
const { scrollWidth, clientWidth, scrollLeft } = scrollContainer.value!
|
||||
const maxScroll = scrollWidth - clientWidth
|
||||
thumbPosition.value = (scrollLeft / maxScroll) * MAX_SCROLL_VALUE
|
||||
}
|
||||
|
||||
/*
|
||||
* Scroll with scrollbar/slider
|
||||
* when scroll thumb is dragged or clicked on the scrollbar
|
||||
*/
|
||||
watch(thumbPosition, (newVal) => {
|
||||
const { scrollWidth, clientWidth } = scrollContainer.value!
|
||||
const maxScroll = scrollWidth - clientWidth
|
||||
scrollContainer.value!.scrollLeft = maxScroll * (newVal / MAX_SCROLL_VALUE)
|
||||
})
|
||||
|
||||
/*
|
||||
* Watch TabID changes
|
||||
* and scroll to the tab if it's not visible
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(tabID) => {
|
||||
nextTick(() => {
|
||||
const element = document.getElementById(`removable-tab-${tabID}`)
|
||||
|
||||
const changeThumbPosition: IntersectionObserverCallback = (
|
||||
entries,
|
||||
observer
|
||||
) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.target === element && entry.intersectionRatio >= 1.0) {
|
||||
// Element is visible now. Stop listening for intersection changes
|
||||
observer.disconnect()
|
||||
|
||||
// We still need setTimeout here because the element might not be fully in position yet
|
||||
setTimeout(() => {
|
||||
const { scrollWidth, clientWidth, scrollLeft } =
|
||||
scrollContainer.value!
|
||||
const maxScroll = scrollWidth - clientWidth
|
||||
thumbPosition.value = (scrollLeft / maxScroll) * MAX_SCROLL_VALUE
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let observer = new IntersectionObserver(changeThumbPosition, {
|
||||
root: null,
|
||||
rootMargin: "0px",
|
||||
threshold: 1.0,
|
||||
})
|
||||
|
||||
if (element) observer.observe(element)
|
||||
element?.scrollIntoView({ behavior: "smooth", inline: "center" })
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</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-[""];
|
||||
|
||||
.tab {
|
||||
@apply relative;
|
||||
@apply flex;
|
||||
@apply p-2;
|
||||
@apply font-semibold;
|
||||
@apply w-46;
|
||||
@apply h-12;
|
||||
@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-[""];
|
||||
@apply focus:before:bg-divider;
|
||||
|
||||
&.active {
|
||||
@apply text-secondaryDark;
|
||||
@apply bg-primary;
|
||||
@apply before:bg-accent;
|
||||
@apply after:absolute;
|
||||
@apply after:inset-x-0;
|
||||
@apply after:bottom-0;
|
||||
@apply after:bg-primary;
|
||||
@apply after:z-[12];
|
||||
@apply after:h-0.25;
|
||||
@apply after:content-[""];
|
||||
}
|
||||
|
||||
.close {
|
||||
@apply opacity-50;
|
||||
|
||||
&.active {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.create-new-tab {
|
||||
@apply after:absolute;
|
||||
@apply after:inset-x-0;
|
||||
@apply after:bottom-0;
|
||||
@apply after:bg-dividerLight;
|
||||
@apply after:z-[14];
|
||||
@apply after:h-0.25;
|
||||
@apply after:content-[""];
|
||||
}
|
||||
|
||||
$slider-height: 4px;
|
||||
|
||||
.slider {
|
||||
--thumb-width: 0;
|
||||
|
||||
height: $slider-height;
|
||||
|
||||
@apply appearance-none;
|
||||
@apply w-full;
|
||||
@apply bg-transparent;
|
||||
@apply outline-none;
|
||||
@apply opacity-0;
|
||||
@apply transition;
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
@apply appearance-none;
|
||||
@apply min-w-0;
|
||||
@apply bg-dividerDark;
|
||||
@apply hover:bg-secondaryLight;
|
||||
@apply active:bg-secondaryLight;
|
||||
|
||||
width: var(--thumb-width);
|
||||
height: $slider-height;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
@apply appearance-none;
|
||||
@apply min-w-0;
|
||||
@apply bg-dividerDark;
|
||||
@apply hover:bg-secondaryLight;
|
||||
@apply active:bg-secondaryLight;
|
||||
|
||||
width: var(--thumb-width);
|
||||
height: $slider-height;
|
||||
}
|
||||
}
|
||||
|
||||
.group-tabs:hover .slider {
|
||||
@apply opacity-100;
|
||||
}
|
||||
</style>
|
||||
@@ -1,27 +0,0 @@
|
||||
export { default as HoppSmartAnchor } from "./Anchor.vue"
|
||||
export { default as HoppSmartAutoComplete } from "./AutoComplete.vue"
|
||||
export { default as HoppSmartCheckbox } from "./Checkbox.vue"
|
||||
export { default as HoppSmartConfirmModal } from "./ConfirmModal.vue"
|
||||
export { default as HoppSmartExpand } from "./Expand.vue"
|
||||
export { default as HoppSmartFileChip } from "./FileChip.vue"
|
||||
export { default as HoppSmartInput } from "./Input.vue"
|
||||
export { default as HoppSmartIntersection } from "./Intersection.vue"
|
||||
export { default as HoppSmartItem } from "./Item.vue"
|
||||
export { default as HoppSmartLink } from "./Link.vue"
|
||||
export { default as HoppSmartModal } from "./Modal.vue"
|
||||
export { default as HoppSmartProgressRing } from "./ProgressRing.vue"
|
||||
export { default as HoppSmartRadio } from "./Radio.vue"
|
||||
export { default as HoppSmartRadioGroup } from "./RadioGroup.vue"
|
||||
export { default as HoppSmartSlideOver } from "./SlideOver.vue"
|
||||
export { default as HoppSmartSpinner } from "./Spinner.vue"
|
||||
export { default as HoppSmartTab } from "./Tab.vue"
|
||||
export { default as HoppSmartTabs } from "./Tabs.vue"
|
||||
export { default as HoppSmartTable } from "./Table.vue"
|
||||
export { default as HoppSmartToggle } from "./Toggle.vue"
|
||||
export { default as HoppSmartWindow } from "./Window.vue"
|
||||
export { default as HoppSmartWindows } from "./Windows.vue"
|
||||
export { default as HoppSmartPicture } from "./Picture.vue"
|
||||
export { default as HoppSmartPlaceholder } from "./Placeholder.vue"
|
||||
export { default as HoppSmartTree } from "./Tree.vue"
|
||||
export { default as HoppSmartTreeBranch } from "./TreeBranch.vue"
|
||||
export { default as HoppSmartSelectWrapper } from "./SelectWrapper.vue"
|
||||
8
packages/hoppscotch-ui/src/env.d.ts
vendored
8
packages/hoppscotch-ui/src/env.d.ts
vendored
@@ -1,8 +0,0 @@
|
||||
/// <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
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Ref } from "vue"
|
||||
|
||||
/**
|
||||
* Representation of a tree node in the SmartTreeAdapter.
|
||||
*/
|
||||
export type TreeNode<T> = {
|
||||
id: string
|
||||
data: T
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of children result from a tree node when there will be a loading state.
|
||||
*/
|
||||
export type ChildrenResult<T> =
|
||||
| {
|
||||
status: "loading"
|
||||
}
|
||||
| {
|
||||
status: "loaded"
|
||||
data: Array<TreeNode<T>>
|
||||
}
|
||||
|
||||
/**
|
||||
* A tree adapter that can be used with the SmartTree component.
|
||||
* @template T The type of data that is stored in the tree.
|
||||
*/
|
||||
export interface SmartTreeAdapter<T> {
|
||||
/**
|
||||
*
|
||||
* @param nodeID - id of the node to get children for
|
||||
* @returns - Ref that contains the children of the node. It is reactive and will be updated when the children are changed.
|
||||
*/
|
||||
getChildren: (nodeID: string | null) => Ref<ChildrenResult<T>>
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./components"
|
||||
export * from "./plugin"
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { Plugin, App } from "vue"
|
||||
|
||||
import "./assets/scss/styles.scss"
|
||||
import "./assets/scss/tailwind.scss"
|
||||
|
||||
/**
|
||||
@constant HOPP_UI_OPTIONS
|
||||
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 [t] - A function for handling translations for the plugin.
|
||||
@property [onModalOpen] - A callback function that is called when a modal is opened.
|
||||
@property [onModalClose] - A callback function that is called when a modal is closed.
|
||||
*/
|
||||
|
||||
export type HoppUIPluginOptions = {
|
||||
t?: (key: string) => string
|
||||
onModalOpen?: () => void
|
||||
onModalClose?: () => void
|
||||
}
|
||||
|
||||
export const plugin: Plugin = {
|
||||
install(app: App, options: HoppUIPluginOptions = {}) {
|
||||
app.provide(HOPP_UI_OPTIONS, options)
|
||||
},
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<template>
|
||||
<Story title="Anchor">
|
||||
<div class="text-secondaryLight text-tiny">
|
||||
By signing in, you are agreeing to our
|
||||
<HoppSmartAnchor
|
||||
class="text-red-800 link"
|
||||
to="https://docs.hoppscotch.io/support/terms"
|
||||
blank
|
||||
label="Terms of Service"
|
||||
/>
|
||||
and
|
||||
<HoppSmartAnchor
|
||||
class="text-red-600 link"
|
||||
to="https://docs.hoppscotch.io/support/privacy"
|
||||
blank
|
||||
label="Privacy Policy"
|
||||
/>
|
||||
</div>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppSmartAnchor } from "../components/smart"
|
||||
</script>
|
||||
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<Story title="Auto Complete">
|
||||
<div class="h-[50vh]">
|
||||
<HoppSmartAutoComplete 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 { HoppSmartAutoComplete } from "../components/smart"
|
||||
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>
|
||||
@@ -1,14 +0,0 @@
|
||||
<template>
|
||||
<Story title="Button">
|
||||
<Variant title="Primary">
|
||||
<HoppButtonPrimary label="Button" />
|
||||
</Variant>
|
||||
<Variant title="Secondary">
|
||||
<HoppButtonSecondary label="Button" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppButtonPrimary, HoppButtonSecondary } from "../components/button"
|
||||
</script>
|
||||
@@ -1,15 +0,0 @@
|
||||
<template>
|
||||
<Story title="Checkbox">
|
||||
<Variant title="Single">
|
||||
<HoppSmartCheckbox :on="on" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppSmartCheckbox } from "../components/smart"
|
||||
|
||||
import { ref } from "vue"
|
||||
|
||||
const on = ref(true)
|
||||
</script>
|
||||
@@ -1,18 +0,0 @@
|
||||
<template>
|
||||
<Story title="Confirm Modal">
|
||||
<HoppSmartConfirmModal :show="show" :title="'Confirm Modal'" @hide-modal="show = false"
|
||||
@resolve="resolveConfirmModal" />
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue"
|
||||
import { HoppSmartConfirmModal } from "../components/smart"
|
||||
|
||||
const show = ref(true)
|
||||
|
||||
const resolveConfirmModal = (resolve: string | null) => {
|
||||
alert("resolved: " + resolve)
|
||||
show.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -1,20 +0,0 @@
|
||||
<template>
|
||||
<Story title="Expand">
|
||||
<HoppSmartExpand>
|
||||
<template #body>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia quas culpa ea nesciunt, magni natus ex numquam perspiciatis,
|
||||
reprehenderit reiciendis necessitatibus nostrum laudantium illum tempora ducimus! Dignissimos officiis sed nisi.
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia quas culpa ea nesciunt, magni natus ex numquam perspiciatis,
|
||||
reprehenderit reiciendis necessitatibus nostrum laudantium illum tempora ducimus! Dignissimos officiis sed nisi.
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia quas culpa ea nesciunt, magni natus ex numquam perspiciatis,
|
||||
reprehenderit reiciendis necessitatibus nostrum laudantium illum tempora ducimus! Dignissimos officiis sed nisi.
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia quas culpa ea nesciunt, magni natus ex numquam perspiciatis,
|
||||
reprehenderit reiciendis necessitatibus nostrum laudantium illum tempora ducimus! Dignissimos officiis sed nisi.
|
||||
</template>
|
||||
</HoppSmartExpand>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppSmartExpand } from "../components/smart"
|
||||
</script>
|
||||
@@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<Story title="Item">
|
||||
<Variant title="Single">
|
||||
<HoppSmartItem :label="'Item'" :active-info-icon="false" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppSmartItem } from "../components/smart"
|
||||
</script>
|
||||
@@ -1,20 +0,0 @@
|
||||
<template>
|
||||
<Story title="Link">
|
||||
<Variant title="Text Link">
|
||||
<HoppSmartLink :to="link" :blank="true"> Click here </HoppSmartLink>
|
||||
</Variant>
|
||||
<Variant title="Button Link">
|
||||
<HoppSmartLink :to="link" :blank="true">
|
||||
<HoppButtonPrimary label="Click here" />
|
||||
</HoppSmartLink>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppSmartLink } from "../components/smart"
|
||||
import { HoppButtonPrimary } from "../components/button"
|
||||
import { ref } from "vue"
|
||||
|
||||
const link = ref("/")
|
||||
</script>
|
||||
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<Story title="Modal">
|
||||
<HoppSmartModal :show="show" :title="'Modal Title'" @hide-modal="show = false" @resolve="resolveConfirmModal" />
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { HoppSmartModal } from "../components/smart"
|
||||
import { ref } from "vue"
|
||||
|
||||
const show = ref(true)
|
||||
|
||||
const resolveConfirmModal = (resolve: string | null) => {
|
||||
alert("resolved: " + resolve)
|
||||
show.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -1,12 +0,0 @@
|
||||
<template>
|
||||
<Story title="Progress Ring">
|
||||
<HoppSmartProgressRing class="mr-2 text-red-500" :radius="8" :stroke="1.5"
|
||||
:progress="(failedTests / totalTests) * 100" />
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppSmartProgressRing } from "../components/smart"
|
||||
const totalTests = 10
|
||||
const failedTests = 2
|
||||
</script>
|
||||
@@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<Story title="Radio">
|
||||
<Variant title="Single">
|
||||
<HoppSmartRadio />
|
||||
</Variant>
|
||||
<Variant title="Group">
|
||||
<HoppSmartRadioGroup :radios="radios" :model-value="selected" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { HoppSmartRadio, HoppSmartRadioGroup } from "../components/smart"
|
||||
|
||||
const selected = ref("option1")
|
||||
const radios = [
|
||||
{ label: "Option 1", value: "option1" },
|
||||
{ label: "Option 2", value: "option2" },
|
||||
{ label: "Option 3", value: "option3" },
|
||||
]
|
||||
</script>
|
||||
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<Story title="Slider Over">
|
||||
<HoppSmartSlideOver :show="show" :title="'Title'" @close="show = false">
|
||||
<template #content>
|
||||
<h1>Content</h1>
|
||||
</template>
|
||||
</HoppSmartSlideOver>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppSmartSlideOver } from "../components/smart"
|
||||
import { ref } from "vue"
|
||||
|
||||
const show = ref(true)
|
||||
</script>
|
||||
@@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<Story title="Spinner">
|
||||
<HoppSmartSpinner />
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppSmartSpinner } from "../components/smart"
|
||||
</script>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<Story title="Tab">
|
||||
<Variant title="Single">
|
||||
<HoppSmartTabs id="my-tab" v-model="selectedTab" render-inactive-tabs>
|
||||
<HoppSmartTab id="tab1" label="Tab 1">
|
||||
<h1>Tab 1 content</h1>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab id="tab2" label="Tab 2">
|
||||
<h1>Tab 2 content</h1>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppSmartTabs, HoppSmartTab } from "../components/smart"
|
||||
import { ref } from "vue"
|
||||
|
||||
const selectedTab = ref("tab1")
|
||||
</script>
|
||||
@@ -1,69 +0,0 @@
|
||||
<template>
|
||||
<Story title="Table">
|
||||
<Variant title="General">
|
||||
<HoppSmartTable :list="list" :headings="headings" />
|
||||
</Variant>
|
||||
<Variant title="Custom">
|
||||
<HoppSmartTable>
|
||||
<template #head>
|
||||
<tr
|
||||
class="text-secondary border-b border-dividerDark text-sm text-left bg-primaryLight"
|
||||
>
|
||||
<th
|
||||
v-for="heading in headings"
|
||||
:key="heading.key"
|
||||
scope="col"
|
||||
class="px-6 py-3"
|
||||
>
|
||||
{{ heading.label }}
|
||||
</th>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<tr
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="text-secondaryDark hover:bg-divider hover:cursor-pointer rounded-xl"
|
||||
>
|
||||
<td
|
||||
v-for="cellHeading in headings"
|
||||
:key="cellHeading.key"
|
||||
class="max-w-[10rem] pl-6 py-1"
|
||||
>
|
||||
{{ item[cellHeading.key] ?? "-" }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</HoppSmartTable>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppSmartTable } from "../components/smart"
|
||||
import { CellHeading } from "~/components/smart/Table.vue"
|
||||
|
||||
// Table Headings
|
||||
const headings: CellHeading[] = [
|
||||
{ key: "id", label: "ID" },
|
||||
{ key: "name", label: "Name" },
|
||||
{ key: "members", label: "Members" },
|
||||
{ key: "role", label: "Role" },
|
||||
]
|
||||
|
||||
const list: Record<string, string | number>[] = [
|
||||
{
|
||||
id: "123455",
|
||||
name: "Joel",
|
||||
members: 10,
|
||||
role: "Frontend Engineer",
|
||||
},
|
||||
{
|
||||
id: "123456",
|
||||
name: "Anwar",
|
||||
members: 12,
|
||||
role: "Frontend Engineer",
|
||||
},
|
||||
]
|
||||
</script>
|
||||
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<Story title="Toggle">
|
||||
<HoppSmartToggle :on="on" @change="change"> Turn on </HoppSmartToggle>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppSmartToggle } from "../components/smart"
|
||||
import { ref } from "vue"
|
||||
|
||||
const on = ref(true)
|
||||
|
||||
const change = () => {
|
||||
alert("changed to: " + on.value)
|
||||
}
|
||||
</script>
|
||||
@@ -1,84 +0,0 @@
|
||||
<template>
|
||||
<Story title="Window">
|
||||
<Variant title="Single">
|
||||
<HoppSmartWindows :id="'my-window'" v-model="selectedWindow" @add-tab="openNewTab" @remove-tab="removeTab"
|
||||
@sort="sortTabs">
|
||||
<HoppSmartWindow v-for="window in tabs" :id="window.id" :key="'tab_' + window.id" :label="window.name"
|
||||
:is-removable="window.removable">
|
||||
</HoppSmartWindow>
|
||||
</HoppSmartWindows>
|
||||
</Variant>
|
||||
<Variant title="Custom Tab Heads">
|
||||
<HoppSmartWindows
|
||||
v-model="selectedWindow"
|
||||
@add-tab="openNewTab"
|
||||
@remove-tab="removeTab"
|
||||
@sort="sortTabs"
|
||||
>
|
||||
<HoppSmartWindow
|
||||
v-for="window in tabs"
|
||||
:id="window.id"
|
||||
:key="'tab_' + window.id"
|
||||
:label="window.name"
|
||||
:is-removable="window.removable"
|
||||
>
|
||||
<template #tabhead>
|
||||
<icon-lucide-train class="svg-icons" /> <span class="truncate w-2"> - Lorem ipsum dolor sit amet</span>
|
||||
</template>
|
||||
</HoppSmartWindow>
|
||||
</HoppSmartWindows>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppSmartWindows, HoppSmartWindow } from "../components/smart"
|
||||
import IconLucideTrain from "~icons/lucide/train"
|
||||
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>
|
||||
@@ -1,6 +0,0 @@
|
||||
import preset from "./ui-preset"
|
||||
|
||||
export default {
|
||||
content: ["src/**/*.{vue,html}"],
|
||||
presets: [preset],
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"allowJs": true,
|
||||
"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"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import { Config } from "tailwindcss"
|
||||
import { theme } from "tailwindcss/defaultConfig"
|
||||
|
||||
export default {
|
||||
content: [],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
},
|
||||
extend: {
|
||||
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)",
|
||||
bannerInfo: "var(--banner-info-color)",
|
||||
bannerWarning: "var(--banner-warning-color)",
|
||||
bannerError: "var(--banner-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)",
|
||||
dark: {
|
||||
50: "#4a4a4a",
|
||||
100: "#3c3c3c",
|
||||
200: "#323232",
|
||||
300: "#2d2d2d",
|
||||
400: "#222222",
|
||||
500: "#1f1f1f",
|
||||
600: "#1c1c1e",
|
||||
700: "#1b1b1b",
|
||||
800: "#181818",
|
||||
900: "#0f0f0f",
|
||||
},
|
||||
light: {
|
||||
50: "#fdfdfd",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: "var(--font-sans)",
|
||||
mono: "var(--font-mono)",
|
||||
},
|
||||
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",
|
||||
},
|
||||
spacing: {
|
||||
0.25: "0.0625rem",
|
||||
0.75: "0.1875rem",
|
||||
20: "5rem",
|
||||
26: "6.5rem",
|
||||
46: "11.5rem",
|
||||
},
|
||||
minWidth: {
|
||||
4: "1rem",
|
||||
5: "1.25rem",
|
||||
20: "5rem",
|
||||
46: "11.5rem",
|
||||
},
|
||||
minHeight: {
|
||||
5: "1.25rem",
|
||||
46: "11.5rem",
|
||||
},
|
||||
maxWidth: {
|
||||
"1/2": "50%",
|
||||
"1/3": "33%",
|
||||
"3/4": "75%",
|
||||
46: "11.5rem",
|
||||
},
|
||||
maxHeight: {
|
||||
46: "11.5rem",
|
||||
sm: "24rem",
|
||||
md: "28rem",
|
||||
lg: "32rem",
|
||||
},
|
||||
backgroundOpacity: {
|
||||
15: "0.15",
|
||||
},
|
||||
screens: {
|
||||
"<sm": { max: "640px" },
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Config
|
||||
@@ -1,56 +0,0 @@
|
||||
import vue from "@vitejs/plugin-vue"
|
||||
import Icons from "unplugin-icons/vite"
|
||||
import { defineConfig } from "vite"
|
||||
import dts from "vite-plugin-dts"
|
||||
import Unfonts from "unplugin-fonts/vite"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
dts({
|
||||
insertTypesEntry: true,
|
||||
skipDiagnostics: true,
|
||||
outputDir: ["dist"],
|
||||
}),
|
||||
Icons({
|
||||
compiler: "vue3",
|
||||
}),
|
||||
Unfonts({
|
||||
fontsource: {
|
||||
families: [
|
||||
{
|
||||
name: "Inter Variable",
|
||||
variables: ["variable-full"],
|
||||
},
|
||||
{
|
||||
name: "Material Symbols Rounded Variable",
|
||||
variables: ["variable-full"],
|
||||
},
|
||||
{
|
||||
name: "Roboto Mono Variable",
|
||||
variables: ["variable-full"],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
], // to process SFC
|
||||
build: {
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
lib: {
|
||||
entry: {
|
||||
index: "./src/index.ts",
|
||||
"ui-preset": "./ui-preset.ts",
|
||||
"postcss.config": "./postcss.config.cjs",
|
||||
},
|
||||
formats: ["es"],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ["vue"],
|
||||
output: {
|
||||
exports: "named",
|
||||
},
|
||||
},
|
||||
emptyOutDir: true,
|
||||
},
|
||||
})
|
||||
4874
pnpm-lock.yaml
generated
4874
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import preset from "./packages/hoppscotch-ui/ui-preset"
|
||||
import preset from "@hoppscotch/ui/ui-preset"
|
||||
|
||||
export default {
|
||||
content: [
|
||||
|
||||
Reference in New Issue
Block a user