chore: move hoppscotch-ui package out of the monorepo (#3620)

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
Anwarul Islam
2023-12-16 17:28:10 +06:00
committed by GitHub
parent d80e6c01c8
commit f93558324f
87 changed files with 1195 additions and 8344 deletions

5
.gitignore vendored
View File

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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"
}
}
}

View File

@@ -1,7 +1,8 @@
const postcssConfig = require("@hoppscotch/ui/postcss.config")
const config = {
...postcssConfig,
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
module.exports = config

View File

@@ -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: {

View File

@@ -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({

View File

@@ -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",

View File

@@ -1,7 +1,8 @@
const postcssConfig = require("@hoppscotch/ui/postcss.config")
const config = {
...postcssConfig,
}
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
module.exports = config
module.exports = config;

View File

@@ -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'];
}
}

View File

@@ -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;

View File

@@ -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({

View File

@@ -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",
},
}

View File

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

View File

@@ -1,10 +0,0 @@
.dependabot
.github
.hoppscotch
.vscode
package-lock.json
node_modules
dist
static
components.d.ts
src/types

View File

@@ -1,3 +0,0 @@
module.exports = {
semi: false,
}

View File

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

View File

@@ -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"],
})

View File

@@ -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() {}

View File

@@ -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"
}

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
/*
* Write hoppscotch-ui related custom global styles in this file.
*/

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

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

View File

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

View File

@@ -1,2 +0,0 @@
export { default as HoppButtonPrimary } from "./Primary.vue"
export { default as HoppButtonSecondary } from "./Secondary.vue"

View File

@@ -1,2 +0,0 @@
export * from "./button"
export * from "./smart"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"
>&#8203;</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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
<template>
<IconLucideLoader class="animate-spin svg-icons" />
</template>
<script setup lang="ts">
import IconLucideLoader from "~icons/lucide/loader"
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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>>
}

View File

@@ -1,2 +0,0 @@
export * from "./components"
export * from "./plugin"

View File

@@ -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)
},
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +0,0 @@
<template>
<Story title="Spinner">
<HoppSmartSpinner />
</Story>
</template>
<script setup lang="ts">
import { HoppSmartSpinner } from "../components/smart"
</script>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
import preset from "./ui-preset"
export default {
content: ["src/**/*.{vue,html}"],
presets: [preset],
}

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import preset from "./packages/hoppscotch-ui/ui-preset"
import preset from "@hoppscotch/ui/ui-preset"
export default {
content: [