refactor: hoppscotch ui (#2887)

* feat: hopp ui initialized

* feat: button components added

* feat: windi css integration

* chore: package removed from hopp ui

* feat: storybook added

* feat: move all smart components hoppscotch-ui

* fix: import issue from components/smart

* fix: env input component import

* feat: add hoppui to windicss config

* fix: remove storybook

* feat: move components from hoppscotch-ui

* feat: storybook added

* feat: storybook progress

* feat: themeing storybook

* feat: add stories

* chore: package updated

* chore: stories added

* feat: stories added

* feat: stories added

* feat: icons resolved

* feat: i18n composable resolved

* feat: histoire added

* chore: resolved prettier issue

* feat: radio story added

* feat: story added for all components

* feat: new components added to stories

* fix: resolved issues

* feat: readme.md added

* feat: context/provider added

* chore: removed app component registry

* chore: remove importing of all components in hopp-ui to allow code splitting

* chore: fix vite config errors

* chore: jsdoc added

* chore: any replaced with smart-item

* chore: i18n added to ui components

* chore: clean up - removed a duplicate button

---------

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
Anwarul Islam
2023-01-28 08:57:00 +06:00
committed by GitHub
parent 9d7052c626
commit 0fcda0be1a
65 changed files with 2961 additions and 214 deletions

View File

@@ -31,6 +31,7 @@
"@codemirror/state": "^6.1.0", "@codemirror/state": "^6.1.0",
"@codemirror/view": "^6.0.2", "@codemirror/view": "^6.0.2",
"@hoppscotch/codemirror-lang-graphql": "workspace:^0.2.0", "@hoppscotch/codemirror-lang-graphql": "workspace:^0.2.0",
"@hoppscotch/ui": "workspace:^0.0.1",
"@hoppscotch/data": "workspace:^0.4.4", "@hoppscotch/data": "workspace:^0.4.4",
"@hoppscotch/js-sandbox": "workspace:^2.1.0", "@hoppscotch/js-sandbox": "workspace:^2.1.0",
"@hoppscotch/vue-toasted": "^0.1.0", "@hoppscotch/vue-toasted": "^0.1.0",

View File

@@ -26,8 +26,8 @@ declare module '@vue/runtime-core' {
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default'] AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
AppSidenav: typeof import('./components/app/Sidenav.vue')['default'] AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
AppSupport: typeof import('./components/app/Support.vue')['default'] AppSupport: typeof import('./components/app/Support.vue')['default']
ButtonPrimary: typeof import('./components/button/Primary.vue')['default'] ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
ButtonSecondary: typeof import('./components/button/Secondary.vue')['default'] ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
Collections: typeof import('./components/collections/index.vue')['default'] Collections: typeof import('./components/collections/index.vue')['default']
CollectionsAdd: typeof import('./components/collections/Add.vue')['default'] CollectionsAdd: typeof import('./components/collections/Add.vue')['default']
CollectionsAddFolder: typeof import('./components/collections/AddFolder.vue')['default'] CollectionsAddFolder: typeof import('./components/collections/AddFolder.vue')['default']
@@ -130,30 +130,30 @@ declare module '@vue/runtime-core' {
RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default'] RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default']
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default'] RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default'] SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
SmartAnchor: typeof import('./components/smart/Anchor.vue')['default'] SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
SmartAutoComplete: typeof import('./components/smart/AutoComplete.vue')['default'] SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
SmartChangeLanguage: typeof import('./components/smart/ChangeLanguage.vue')['default'] SmartChangeLanguage: typeof import('./components/smart/ChangeLanguage.vue')['default']
SmartCheckbox: typeof import('./components/smart/Checkbox.vue')['default'] SmartCheckbox: typeof import('./../../hoppscotch-ui/src/components/smart/Checkbox.vue')['default']
SmartColorModePicker: typeof import('./components/smart/ColorModePicker.vue')['default'] SmartColorModePicker: typeof import('./components/smart/ColorModePicker.vue')['default']
SmartConfirmModal: typeof import('./components/smart/ConfirmModal.vue')['default'] SmartConfirmModal: typeof import('./../../hoppscotch-ui/src/components/smart/ConfirmModal.vue')['default']
SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default'] SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
SmartExpand: typeof import('./components/smart/Expand.vue')['default'] SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
SmartFileChip: typeof import('./components/smart/FileChip.vue')['default'] SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default'] SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default']
SmartIntersection: typeof import('./components/smart/Intersection.vue')['default'] SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
SmartItem: typeof import('./components/smart/Item.vue')['default'] SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
SmartLink: typeof import('./components/smart/Link.vue')['default'] SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
SmartModal: typeof import('./components/smart/Modal.vue')['default'] SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
SmartProgressRing: typeof import('./components/smart/ProgressRing.vue')['default'] SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
SmartRadio: typeof import('./components/smart/Radio.vue')['default'] SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
SmartRadioGroup: typeof import('./components/smart/RadioGroup.vue')['default'] SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
SmartSlideOver: typeof import('./components/smart/SlideOver.vue')['default'] SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
SmartSpinner: typeof import('./components/smart/Spinner.vue')['default'] SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
SmartTab: typeof import('./components/smart/Tab.vue')['default'] SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
SmartTabs: typeof import('./components/smart/Tabs.vue')['default'] SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
SmartToggle: typeof import('./components/smart/Toggle.vue')['default'] SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
SmartWindow: typeof import('./components/smart/Window.vue')['default'] SmartWindow: typeof import('./../../hoppscotch-ui/src/components/smart/Window.vue')['default']
SmartWindows: typeof import('./components/smart/Windows.vue')['default'] SmartWindows: typeof import('./../../hoppscotch-ui/src/components/smart/Windows.vue')['default']
TabPrimary: typeof import('./components/tab/Primary.vue')['default'] TabPrimary: typeof import('./components/tab/Primary.vue')['default']
TabSecondary: typeof import('./components/tab/Secondary.vue')['default'] TabSecondary: typeof import('./components/tab/Secondary.vue')['default']
Teams: typeof import('./components/teams/index.vue')['default'] Teams: typeof import('./components/teams/index.vue')['default']

View File

@@ -50,9 +50,9 @@
ref="tippyActions" ref="tippyActions"
class="flex flex-col focus:outline-none" class="flex flex-col focus:outline-none"
tabindex="0" tabindex="0"
@keyup.d="documentation.$el.click()" @keyup.d="documentation!.$el.click()"
@keyup.s="shortcuts.$el.click()" @keyup.s="shortcuts!.$el.click()"
@keyup.c="chat.$el.click()" @keyup.c="chat!.$el.click()"
@keyup.escape="hide()" @keyup.escape="hide()"
> >
<SmartItem <SmartItem
@@ -221,9 +221,9 @@ import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { currentUser$ } from "~/helpers/fb/auth" import { currentUser$ } from "~/helpers/fb/auth"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import SmartItem from "@components/smart/Item.vue"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils" import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { invokeAction } from "@helpers/actions" import { invokeAction } from "@helpers/actions"
import SmartItem from "@hoppscotch/ui/src/components/smart/Item.vue"
const t = useI18n() const t = useI18n()
const showDeveloperOptions = ref(false) const showDeveloperOptions = ref(false)
@@ -271,7 +271,7 @@ const showDeveloperOptionModal = () => {
// Template refs // Template refs
const tippyActions = ref<TippyComponent | null>(null) const tippyActions = ref<TippyComponent | null>(null)
const documentation = ref<typeof SmartItem | null>(null) const documentation = ref<typeof SmartItem>()
const shortcuts = ref<typeof SmartItem | null>(null) const shortcuts = ref<typeof SmartItem>()
const chat = ref<typeof SmartItem | null>(null) const chat = ref<typeof SmartItem>()
</script> </script>

View File

@@ -122,7 +122,7 @@ import {
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import SmartItem from "@components/smart/Item.vue" import SmartItem from "@hoppscotch/ui/src/components/smart/Item.vue"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -140,9 +140,9 @@ const confirmRemove = ref(false)
const tippyActions = ref<TippyComponent | null>(null) const tippyActions = ref<TippyComponent | null>(null)
const options = ref<TippyComponent | null>(null) const options = ref<TippyComponent | null>(null)
const edit = ref<typeof SmartItem | null>(null) const edit = ref<typeof SmartItem>()
const duplicate = ref<typeof SmartItem | null>(null) const duplicate = ref<typeof SmartItem>()
const deleteAction = ref<typeof SmartItem | null>(null) const deleteAction = ref<typeof SmartItem>()
const removeEnvironment = () => { const removeEnvironment = () => {
if (props.environmentIndex === null) return if (props.environmentIndex === null) return

View File

@@ -108,7 +108,7 @@ import IconCopy from "~icons/lucide/copy"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import IconMoreVertical from "~icons/lucide/more-vertical" import IconMoreVertical from "~icons/lucide/more-vertical"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import SmartItem from "@components/smart/Item.vue" import SmartItem from "@hoppscotch/ui/src/components/smart/Item.vue"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -126,9 +126,9 @@ const confirmRemove = ref(false)
const tippyActions = ref<TippyComponent | null>(null) const tippyActions = ref<TippyComponent | null>(null)
const options = ref<TippyComponent | null>(null) const options = ref<TippyComponent | null>(null)
const edit = ref<typeof SmartItem | null>(null) const edit = ref<typeof SmartItem>()
const duplicate = ref<typeof SmartItem | null>(null) const duplicate = ref<typeof SmartItem>()
const deleteAction = ref<typeof SmartItem | null>(null) const deleteAction = ref<typeof SmartItem>()
const removeEnvironment = () => { const removeEnvironment = () => {
pipe( pipe(

View File

@@ -0,0 +1,21 @@
import { HoppModule } from "."
import HoppUI from "./../../../hoppscotch-ui/src"
import { HoppUIPluginOptions } from "@hoppscotch/ui/src"
import { useKeybindingDisabler } from "~/helpers/keybindings"
import { useI18n } from "vue-i18n"
const { disableKeybindings, enableKeybindings } = useKeybindingDisabler()
const HoppUIOptions: HoppUIPluginOptions = {
t: (key: string) => useI18n().t(key).toString(),
onModalOpen: disableKeybindings,
onModalClose: enableKeybindings,
}
export default <HoppModule>{
onVueAppInit(app) {
// disable eslint for this line. it's a hack because there's some unknown type error
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
app.use(HoppUI, HoppUIOptions)
},
}

View File

@@ -1,6 +1,9 @@
import { defineConfig } from "windicss/helpers" import { defineConfig } from "windicss/helpers"
export default defineConfig({ export default defineConfig({
extract: {
include: ["src/**/*.{vue,html}", "../hoppscotch-ui/src/**/*.{vue,html}"],
},
theme: { theme: {
container: { container: {
center: true, center: true,

View File

@@ -34,7 +34,7 @@
"devDependencies": { "devDependencies": {
"@types/lodash": "^4.14.181", "@types/lodash": "^4.14.181",
"typescript": "^4.6.3", "typescript": "^4.6.3",
"vite": "^3.1.0" "vite": "^3.2.3"
}, },
"dependencies": { "dependencies": {
"fp-ts": "^2.11.10", "fp-ts": "^2.11.10",

View File

@@ -9,9 +9,7 @@ export default defineConfig({
lib: { lib: {
entry: resolve(__dirname, "src/index.ts"), entry: resolve(__dirname, "src/index.ts"),
fileName: "hoppscotch-data", fileName: "hoppscotch-data",
formats: [ formats: ["es", "cjs"],
"es", "cjs" },
], },
}
}
}) })

View File

@@ -0,0 +1,47 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution")
module.exports = {
root: true,
env: {
browser: true,
node: true,
jest: true,
},
parserOptions: {
sourceType: "module",
requireConfigFile: false,
},
extends: [
"@vue/typescript/recommended",
"plugin:vue/vue3-recommended",
"plugin:prettier/recommended",
"plugin:storybook/recommended",
],
ignorePatterns: [
"static/**/*",
"./helpers/backend/graphql.ts",
"**/*.d.ts",
"types/**/*",
],
plugins: ["vue", "prettier"],
// add your custom rules here
rules: {
semi: [2, "never"],
"import/named": "off",
// because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
"no-console": "off",
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"prettier/prettier":
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"vue/multi-word-component-names": "off",
"vue/no-side-effects-in-computed-properties": "off",
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "off",
"@typescript-eslint/no-unused-vars":
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"import/default": "off",
"no-undef": "off",
},
}

27
packages/hoppscotch-ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment Variables
.env

View File

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

View File

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

View File

@@ -0,0 +1,57 @@
<div align="center">
<a href="https://hoppscotch.io">
<img
src="https://avatars.githubusercontent.com/u/56705483"
alt="Hoppscotch Logo"
height="64"
/>
</a>
</div>
<div align="center">
# Hoppscotch UI
</div>
Welcome to hoppscotch-ui, a collection of presentational components for our web applications. This library is part of the hoppscotch monorepo and contains components such as buttons, spinners, modals, tabs, windows, etc.
## Usage
To use the components in project, simply name the component with `directory` name as alias:
For example `Primary Button` component is in `button` directory and the file name is `Primary.vue`. So, use that you have to write `<ButtonPrimary />`
## Histoire
We've included Histoire in this library which is similar to Storybook, to make it easy to play with the components in the browser. You can run Histoire in the browser with command
`pnpm run story:dev`
You can also use [Histoire](https://histoire.dev/) to create stories for your components and test them in different scenarios.
## Versioning
This project follows [Semantic Versioning](https://semver.org/) but as the project is still pre-1.0. The code and the public exposed API should not be considered to be fixed and stable. Things can change at any time!
## License
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT) - see [`LICENSE`](https://github.com/hoppscotch/hoppscotch/blob/main/LICENSE) for more details.
<div align="center">
<br />
<br />
###### built with ❤︎ by the [Hoppscotch Team](https://github.com/hoppscotch) and [contributors](https://github.com/hoppscotch/hoppscotch/graphs/contributors).
</div>

View File

@@ -0,0 +1,7 @@
import { HstVue } from "@histoire/plugin-vue"
import { defineConfig } from "histoire"
export default defineConfig({
setupFile: "histoire.setup.ts",
plugins: [HstVue()],
})

View File

@@ -0,0 +1,5 @@
import "./src/assets/scss/styles.scss"
import "./src/assets/scss/themes.scss"
import "virtual:windi.css"
export function setupVue3() {}

View File

@@ -0,0 +1,82 @@
{
"name": "@hoppscotch/ui",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "vite build",
"story:dev": "histoire dev",
"story:build": "histoire build",
"story:preview": "histoire preview"
},
"dependencies": {
"@hoppscotch/vue-toasted": "^0.1.0",
"@lezer/highlight": "^1.0.0",
"@vitejs/plugin-legacy": "^2.3.0",
"@vueuse/core": "^8.7.5",
"@vueuse/head": "^0.7.9",
"acorn-walk": "^8.2.0",
"esprima": "^4.0.1",
"events": "^3.3.0",
"fp-ts": "^2.12.1",
"globalthis": "^1.0.3",
"lodash-es": "^4.17.21",
"path": "^0.12.7",
"rxjs": "^7.5.5",
"splitpanes": "^3.1.1",
"tern": "^0.24.3",
"timers": "^0.1.1",
"tippy.js": "^6.3.7",
"url": "^0.11.0",
"util": "^0.12.4",
"vite-plugin-eslint": "^1.8.1",
"vue": "^3.2.25",
"vue-github-button": "^3.0.3",
"vue-router": "^4.0.16",
"vue-tippy": "6.0.0-alpha.58",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
"@histoire/plugin-vue": "^0.12.4",
"@iconify-json/lucide": "^1.1.40",
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@rushstack/eslint-patch": "^1.1.4",
"@types/lodash-es": "^4.17.6",
"@types/splitpanes": "^2.2.1",
"@typescript-eslint/eslint-plugin": "^5.19.0",
"@typescript-eslint/parser": "^5.19.0",
"@vitejs/plugin-vue": "^3.1.0",
"@vue/compiler-sfc": "^3.2.39",
"@vue/eslint-config-typescript": "^11.0.1",
"@vue/runtime-core": "^3.2.39",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-plugin-prettier": "^4.2.0",
"eslint-plugin-vue": "^9.5.1",
"histoire": "^0.12.4",
"npm-run-all": "^4.1.5",
"rollup-plugin-polyfill-node": "^0.10.1",
"sass": "^1.53.0",
"typescript": "^4.5.4",
"unplugin-icons": "^0.14.9",
"unplugin-vue-components": "^0.21.0",
"vite": "^3.2.3",
"vite-plugin-checker": "^0.5.1",
"vite-plugin-fonts": "^0.6.0",
"vite-plugin-html-config": "^1.0.10",
"vite-plugin-inspect": "^0.7.4",
"vite-plugin-pages": "^0.26.0",
"vite-plugin-pages-sitemap": "^1.4.0",
"vite-plugin-pwa": "^0.13.1",
"vite-plugin-vue-layouts": "^0.7.0",
"vite-plugin-windicss": "^1.8.8",
"vue-loader": "^16.8.3",
"vue-tsc": "^0.38.2",
"windicss": "^3.5.6"
},
"files": [
"src",
"dist"
]
}

View File

@@ -0,0 +1,562 @@
* {
@apply backface-hidden;
@apply before:backface-hidden;
@apply after:backface-hidden;
@apply selection:bg-accentDark;
@apply selection:text-accentContrast;
}
:root {
@apply antialiased;
accent-color: var(--accent-color);
font-variant-ligatures: common-ligatures;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
@apply border-solid border-l border-dividerLight border-t-0 border-b-0 border-r-0;
}
::-webkit-scrollbar-thumb {
@apply bg-divider bg-clip-content;
@apply rounded-full;
@apply border-solid border-transparent border-4;
@apply hover:bg-dividerDark;
@apply hover:bg-clip-content;
}
::-webkit-scrollbar {
@apply w-4;
@apply h-0;
}
input::placeholder,
textarea::placeholder,
.cm-placeholder {
@apply text-secondary;
@apply opacity-35;
}
input,
textarea {
@apply text-secondaryDark;
@apply font-medium;
}
html {
scroll-behavior: smooth;
}
body {
@apply bg-primary;
@apply text-secondary text-body;
@apply font-medium;
@apply select-none;
@apply overflow-x-hidden;
@apply leading-body;
animation: fade 300ms forwards;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
}
@keyframes fade {
0% {
@apply opacity-0;
}
100% {
@apply opacity-100;
}
}
.fade-enter-active,
.fade-leave-active {
@apply transition-opacity;
}
.fade-enter-from,
.fade-leave-to {
@apply opacity-0;
}
.slide-enter-active,
.slide-leave-active {
@apply transition;
@apply duration-300;
}
.slide-enter-from,
.slide-leave-to {
@apply transform;
@apply translate-x-full;
}
.bounce-enter-active,
.bounce-leave-active {
@apply transition;
}
.bounce-enter-from,
.bounce-leave-to {
@apply transform;
@apply scale-95;
}
.svg-icons {
@apply flex-shrink-0;
@apply overflow-hidden;
height: var(--line-height-body);
width: var(--line-height-body);
}
a {
@apply inline-flex;
@apply text-current;
@apply no-underline;
@apply transition;
@apply leading-body;
@apply focus:outline-none;
&.link {
@apply items-center;
@apply py-0.5 px-1;
@apply -my-0.5 -mx-1;
@apply text-accent;
@apply rounded;
@apply hover:text-accentDark;
@apply focus-visible:ring;
@apply focus-visible:ring-accent;
@apply focus-visible:text-accentDark;
}
}
.cm-tooltip {
.tippy-box {
@apply shadow-none;
@apply fixed;
@apply inline-flex;
@apply -mt-6;
}
}
.tippy-box[data-theme~="tooltip"] {
@apply bg-tooltip;
@apply border-solid border-tooltip;
@apply rounded;
@apply shadow;
.tippy-content {
@apply flex;
@apply text-tiny text-primary;
@apply font-semibold;
@apply py-1 px-2;
@apply truncate;
@apply leading-normal;
@apply items-center;
kbd {
@apply hidden;
@apply font-sans;
@apply bg-gray-500/45;
@apply text-primaryLight;
@apply rounded-sm;
@apply px-1;
@apply my-0 ml-1;
@apply truncate;
@apply sm:inline-flex;
}
.env-icon {
@apply transition;
@apply inline-flex;
@apply items-center;
}
}
.tippy-svg-arrow {
svg:first-child {
@apply fill-tooltip;
}
svg:last-child {
@apply fill-tooltip;
}
}
}
.tippy-box[data-theme~="popover"] {
@apply bg-popover;
@apply border-solid border-dividerDark;
@apply rounded;
@apply shadow-lg;
.tippy-content {
@apply flex flex-col;
@apply max-h-56;
@apply items-stretch;
@apply overflow-y-auto;
@apply text-secondary text-body;
@apply p-2;
@apply leading-normal;
@apply focus:outline-none;
scroll-behavior: smooth;
}
.tippy-svg-arrow {
svg:first-child {
@apply fill-dividerDark;
}
svg:last-child {
@apply fill-popover;
}
}
}
[data-v-tippy] {
@apply flex flex-1;
}
[interactive] > div {
@apply flex flex-1;
@apply h-full;
}
hr {
@apply border-b border-dividerLight;
@apply my-2;
}
.heading {
@apply font-bold;
@apply text-secondaryDark text-lg;
@apply tracking-tight;
}
.input,
.select,
.textarea {
@apply flex;
@apply w-full;
@apply py-2 px-4;
@apply bg-transparent;
@apply rounded;
@apply text-secondaryDark;
@apply border border-divider;
@apply focus-visible:border-dividerDark;
}
input,
select,
textarea,
button {
@apply truncate;
@apply transition;
@apply text-body;
@apply leading-body;
@apply focus:outline-none;
@apply disabled:cursor-not-allowed;
}
.input[type="file"],
.input[type="radio"],
#installPWA {
@apply hidden;
}
.floating-input ~ label {
@apply absolute;
@apply px-2 py-0.5;
@apply m-2;
@apply rounded;
@apply transition;
@apply origin-top-left;
}
.floating-input:focus-within ~ label,
.floating-input:not(:placeholder-shown) ~ label {
@apply bg-primary;
@apply transform;
@apply origin-top-left;
@apply scale-75;
@apply translate-x-1 -translate-y-4;
}
.floating-input:focus-within ~ label {
@apply text-secondaryDark;
}
.floating-input ~ .end-actions {
@apply absolute;
@apply right-0.2;
@apply inset-y-0;
@apply flex;
@apply items-center;
}
.floating-input:has(~ .end-actions) {
@apply pr-12;
}
pre.ace_editor {
@apply font-mono;
@apply resize-none;
@apply z-0;
}
.select {
@apply appearance-none;
@apply cursor-pointer;
&::-ms-expand {
@apply hidden;
}
}
.select-wrapper {
@apply flex flex-1;
@apply relative;
@apply after:absolute;
@apply after:flex;
@apply after:inset-y-0;
@apply after:items-center;
@apply after:justify-center;
@apply after:pointer-events-none;
@apply after:font-icon;
@apply after:text-secondaryLight;
@apply after:right-3;
@apply after:content-["\e313"];
}
.info-response {
@apply text-pink-500;
}
.success-response {
@apply text-green-500;
}
.redir-response {
@apply text-yellow-500;
}
.cl-error-response {
@apply text-red-500;
}
.sv-error-response {
@apply text-red-600;
}
.missing-data-response {
@apply text-secondaryLight;
}
.toasted-container {
@apply max-w-md;
.toasted {
&.toasted-primary {
@apply px-4 py-2;
@apply bg-tooltip;
@apply border-secondaryDark;
@apply text-primary text-body;
@apply justify-between;
@apply shadow-lg;
@apply font-semibold;
@apply transition;
@apply leading-body;
@apply sm:rounded;
@apply sm:border;
.action {
@apply relative;
@apply flex flex-shrink-0;
@apply text-body;
@apply px-4;
@apply my-1;
@apply ml-auto;
@apply normal-case;
@apply font-semibold;
@apply leading-body;
@apply tracking-normal;
@apply rounded;
@apply last:ml-4;
@apply sm:ml-8;
@apply before:absolute;
@apply before:bg-current;
@apply before:opacity-10;
@apply before:inset-0;
@apply before:transition;
@apply before:content-DEFAULT;
@apply hover:no-underline;
@apply hover:before:opacity-20;
}
}
&.info {
@apply bg-accent;
@apply text-accentContrast;
@apply border-accentDark;
}
&.error {
@apply bg-red-200;
@apply text-red-800;
@apply border-red-400;
}
&.success {
@apply bg-green-200;
@apply text-green-800;
@apply border-green-400;
}
}
}
.smart-splitter .splitpanes__splitter {
@apply relative;
@apply bg-primaryLight;
@apply before:absolute;
@apply before:inset-0;
@apply before:bg-accentLight;
@apply before:opacity-0;
@apply before:z-20;
@apply before:transition;
@apply before:content-DEFAULT;
@apply after:absolute;
@apply after:inset-0;
@apply after:z-20;
@apply after:transition;
@apply after:flex;
@apply after:items-center;
@apply after:justify-center;
@apply after:text-dividerDark;
@apply after:font-icon;
@apply hover:before:opacity-100;
@apply hover:after:text-accentDark;
}
.no-splitter .splitpanes__splitter {
@apply relative;
@apply bg-primaryLight;
}
.smart-splitter.splitpanes--vertical > .splitpanes__splitter {
@apply w-1;
@apply before:-left-0.5;
@apply before:-right-0.5;
@apply before:h-full;
@apply after:content-["\e5d4"];
}
.smart-splitter.splitpanes--horizontal > .splitpanes__splitter {
@apply h-1;
@apply before:-top-0.5;
@apply before:-bottom-0.5;
@apply before:w-full;
@apply after:content-["\e5d3"];
}
.no-splitter.splitpanes--vertical > .splitpanes__splitter {
@apply w-0.5;
@apply pointer-events-none;
}
.no-splitter.splitpanes--horizontal > .splitpanes__splitter {
@apply h-0.5;
@apply pointer-events-none;
}
.cm-focused {
@apply select-auto;
@apply outline-none #{!important};
.cm-activeLine {
@apply bg-primaryLight;
}
.cm-activeLineGutter {
@apply bg-primaryDark;
}
}
.cm-editor {
.cm-line::selection {
@apply bg-accentDark #{!important};
@apply text-accentContrast #{!important};
}
.cm-line ::selection {
@apply bg-accentDark #{!important};
@apply text-accentContrast #{!important};
}
}
.shortcut-key {
@apply inline-flex;
@apply font-sans;
@apply text-tiny;
@apply bg-divider;
@apply rounded;
@apply ml-2;
@apply px-1;
@apply min-w-5;
@apply min-h-5;
@apply items-center;
@apply justify-center;
@apply border border-dividerDark;
@apply shadow-sm;
@apply <sm:hidden;
}
.capitalize-first {
@apply first-letter:capitalize;
}
details {
@apply select-none;
}
details summary::-webkit-details-marker {
@apply hidden;
}
details summary .indicator {
@apply transition;
}
details[open] summary .indicator {
@apply transform;
@apply rotate-90;
}
@media (max-width: 767px) {
main {
margin-bottom: env(safe-area-inset-bottom);
}
}
.env-highlight {
* {
@apply text-accentContrast;
}
}
#nprogress .bar {
@apply bg-accent #{!important};
}
.color-picker[type="color"] {
@apply appearance-none;
}
.color-picker[type="color"]::-webkit-color-swatch-wrapper {
@apply rounded;
@apply p-0;
}
.color-picker[type="color"]::-webkit-color-swatch {
@apply rounded;
@apply border-0;
}

View File

@@ -0,0 +1,311 @@
@mixin base-theme {
--font-sans: "Inter", sans-serif;
--font-mono: "Roboto Mono", monospace;
--font-icon: "Material Icons";
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
}
@mixin dark-theme {
--primary-color: theme("colors.neutral.900");
--primary-light-color: theme("colors.dark.600");
--primary-dark-color: theme("colors.neutral.800");
--primary-contrast-color: #161616;
--secondary-color: theme("colors.neutral.400");
--secondary-light-color: theme("colors.neutral.500");
--secondary-dark-color: theme("colors.neutral.100");
--divider-color: theme("colors.neutral.800");
--divider-light-color: theme("colors.dark.500");
--divider-dark-color: theme("colors.dark.300");
--error-color: theme("colors.stone.800");
--tooltip-color: theme("colors.neutral.100");
--popover-color: theme("colors.dark.700");
--editor-theme: "merbivore_soft";
}
@mixin light-theme {
--primary-color: theme("colors.white");
--primary-light-color: theme("colors.neutral.50");
--primary-dark-color: theme("colors.neutral.100");
--primary-contrast-color: #fefefe;
--secondary-color: theme("colors.neutral.500");
--secondary-light-color: theme("colors.neutral.400");
--secondary-dark-color: theme("colors.neutral.900");
--divider-color: theme("colors.gray.100");
--divider-light-color: theme("colors.neutral.100");
--divider-dark-color: theme("colors.neutral.300");
--error-color: theme("colors.yellow.100");
--tooltip-color: theme("colors.neutral.800");
--popover-color: theme("colors.white");
--editor-theme: "textmate";
}
@mixin black-theme {
--primary-color: theme("colors.dark.900");
--primary-light-color: theme("colors.neutral.900");
--primary-dark-color: theme("colors.dark.800");
--primary-contrast-color: #0e0e0e;
--secondary-color: theme("colors.neutral.400");
--secondary-light-color: theme("colors.neutral.500");
--secondary-dark-color: theme("colors.neutral.100");
--divider-color: theme("colors.neutral.800");
--divider-light-color: theme("colors.dark.800");
--divider-dark-color: theme("colors.dark.300");
--error-color: theme("colors.stone.900");
--tooltip-color: theme("colors.neutral.100");
--popover-color: theme("colors.dark.600");
--editor-theme: "twilight";
}
@mixin dark-editor-theme {
--editor-type-color: theme("colors.purple.400");
--editor-name-color: theme("colors.blue.400");
--editor-operator-color: theme("colors.indigo.400");
--editor-invalid-color: theme("colors.red.400");
--editor-separator-color: theme("colors.gray.400");
--editor-meta-color: theme("colors.gray.400");
--editor-variable-color: theme("colors.green.400");
--editor-link-color: theme("colors.cyan.400");
--editor-process-color: theme("colors.fuchsia.400");
--editor-constant-color: theme("colors.violet.400");
--editor-keyword-color: theme("colors.pink.400");
}
@mixin light-editor-theme {
--editor-type-color: theme("colors.purple.600");
--editor-name-color: theme("colors.red.600");
--editor-operator-color: theme("colors.indigo.600");
--editor-invalid-color: theme("colors.red.600");
--editor-separator-color: theme("colors.gray.600");
--editor-meta-color: theme("colors.gray.600");
--editor-variable-color: theme("colors.green.600");
--editor-link-color: theme("colors.cyan.600");
--editor-process-color: theme("colors.blue.600");
--editor-constant-color: theme("colors.fuchsia.600");
--editor-keyword-color: theme("colors.pink.600");
}
@mixin black-editor-theme {
--editor-type-color: theme("colors.purple.400");
--editor-name-color: theme("colors.fuchsia.400");
--editor-operator-color: theme("colors.indigo.400");
--editor-invalid-color: theme("colors.red.400");
--editor-separator-color: theme("colors.gray.400");
--editor-meta-color: theme("colors.gray.400");
--editor-variable-color: theme("colors.green.400");
--editor-link-color: theme("colors.cyan.400");
--editor-process-color: theme("colors.violet.400");
--editor-constant-color: theme("colors.blue.400");
--editor-keyword-color: theme("colors.pink.400");
}
@mixin green-theme {
--accent-color: theme("colors.green.500");
--accent-light-color: theme("colors.green.400");
--accent-dark-color: theme("colors.green.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.green.200");
--gradient-via-color: theme("colors.green.400");
--gradient-to-color: theme("colors.green.600");
}
@mixin teal-theme {
--accent-color: theme("colors.teal.500");
--accent-light-color: theme("colors.teal.400");
--accent-dark-color: theme("colors.teal.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.teal.200");
--gradient-via-color: theme("colors.teal.400");
--gradient-to-color: theme("colors.teal.600");
}
@mixin blue-theme {
--accent-color: theme("colors.blue.500");
--accent-light-color: theme("colors.blue.400");
--accent-dark-color: theme("colors.blue.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.blue.200");
--gradient-via-color: theme("colors.blue.400");
--gradient-to-color: theme("colors.blue.600");
}
@mixin indigo-theme {
--accent-color: theme("colors.indigo.500");
--accent-light-color: theme("colors.indigo.400");
--accent-dark-color: theme("colors.indigo.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.indigo.200");
--gradient-via-color: theme("colors.indigo.400");
--gradient-to-color: theme("colors.indigo.600");
}
@mixin purple-theme {
--accent-color: theme("colors.purple.500");
--accent-light-color: theme("colors.purple.400");
--accent-dark-color: theme("colors.purple.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.purple.200");
--gradient-via-color: theme("colors.purple.400");
--gradient-to-color: theme("colors.purple.600");
}
@mixin yellow-theme {
--accent-color: theme("colors.yellow.500");
--accent-light-color: theme("colors.yellow.400");
--accent-dark-color: theme("colors.yellow.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.yellow.200");
--gradient-via-color: theme("colors.yellow.400");
--gradient-to-color: theme("colors.yellow.600");
}
@mixin orange-theme {
--accent-color: theme("colors.orange.500");
--accent-light-color: theme("colors.orange.400");
--accent-dark-color: theme("colors.orange.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.orange.200");
--gradient-via-color: theme("colors.orange.400");
--gradient-to-color: theme("colors.orange.600");
}
@mixin red-theme {
--accent-color: theme("colors.red.500");
--accent-light-color: theme("colors.red.400");
--accent-dark-color: theme("colors.red.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.red.200");
--gradient-via-color: theme("colors.red.400");
--gradient-to-color: theme("colors.red.600");
}
@mixin pink-theme {
--accent-color: theme("colors.pink.500");
--accent-light-color: theme("colors.pink.400");
--accent-dark-color: theme("colors.pink.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.pink.200");
--gradient-via-color: theme("colors.pink.400");
--gradient-to-color: theme("colors.pink.600");
}
:root {
@include base-theme;
@include dark-theme;
@include green-theme;
@include dark-editor-theme;
}
:root.light {
@include light-theme;
@include light-editor-theme;
}
:root.dark {
@include dark-theme;
@include dark-editor-theme;
}
:root.black {
@include black-theme;
@include black-editor-theme;
}
:root[data-accent="blue"] {
@include blue-theme;
}
:root[data-accent="green"] {
@include green-theme;
}
:root[data-accent="teal"] {
@include teal-theme;
}
:root[data-accent="indigo"] {
@include indigo-theme;
}
:root[data-accent="purple"] {
@include purple-theme;
}
:root[data-accent="orange"] {
@include orange-theme;
}
:root[data-accent="pink"] {
@include pink-theme;
}
:root[data-accent="red"] {
@include red-theme;
}
:root[data-accent="yellow"] {
@include yellow-theme;
}
@mixin font-small {
--font-size-body: 0.75rem;
--line-height-body: 1rem;
--upper-primary-sticky-fold: 4.125rem;
--upper-secondary-sticky-fold: 6.188rem;
--upper-tertiary-sticky-fold: 8.25rem;
--upper-mobile-primary-sticky-fold: 6.625rem;
--upper-mobile-secondary-sticky-fold: 8.688rem;
--upper-mobile-sticky-fold: 10.75rem;
--upper-mobile-tertiary-sticky-fold: 8.25rem;
--lower-primary-sticky-fold: 3rem;
--lower-secondary-sticky-fold: 5.063rem;
--lower-tertiary-sticky-fold: 7.125rem;
--sidebar-primary-sticky-fold: 2rem;
}
@mixin font-medium {
--font-size-body: 0.875rem;
--line-height-body: 1.25rem;
--upper-primary-sticky-fold: 4.375rem;
--upper-secondary-sticky-fold: 6.688rem;
--upper-tertiary-sticky-fold: 9rem;
--upper-mobile-primary-sticky-fold: 7.125rem;
--upper-mobile-secondary-sticky-fold: 9.438rem;
--upper-mobile-sticky-fold: 11.75rem;
--upper-mobile-tertiary-sticky-fold: 9rem;
--lower-primary-sticky-fold: 3.25rem;
--lower-secondary-sticky-fold: 5.563rem;
--lower-tertiary-sticky-fold: 7.875rem;
--sidebar-primary-sticky-fold: 2.25rem;
}
@mixin font-large {
--font-size-body: 1rem;
--line-height-body: 1.5rem;
--upper-primary-sticky-fold: 4.625rem;
--upper-secondary-sticky-fold: 7.188rem;
--upper-tertiary-sticky-fold: 9.75rem;
--upper-mobile-primary-sticky-fold: 7.625rem;
--upper-mobile-secondary-sticky-fold: 10.188rem;
--upper-mobile-sticky-fold: 12.75rem;
--upper-mobile-tertiary-sticky-fold: 9.75rem;
--lower-primary-sticky-fold: 3.5rem;
--lower-secondary-sticky-fold: 6.063rem;
--lower-tertiary-sticky-fold: 8.625rem;
--sidebar-primary-sticky-fold: 2.5rem;
}
:root[data-font-size="small"] {
@include font-small;
}
:root[data-font-size="medium"] {
@include font-medium;
}
:root[data-font-size="large"] {
@include font-large;
}
.generic {
@apply text-primary text-primaryLight bg-primary;
}

View File

@@ -0,0 +1,37 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
ButtonPrimary: typeof import('./components/button/Primary.vue')['default']
ButtonSecondary: typeof import('./components/button/Secondary.vue')['default']
IconLucideLoader: typeof import('~icons/lucide/loader')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SmartAnchor: typeof import('./components/smart/Anchor.vue')['default']
SmartAutoComplete: typeof import('./components/smart/AutoComplete.vue')['default']
SmartCheckbox: typeof import('./components/smart/Checkbox.vue')['default']
SmartConfirmModal: typeof import('./components/smart/ConfirmModal.vue')['default']
SmartExpand: typeof import('./components/smart/Expand.vue')['default']
SmartFileChip: typeof import('./components/smart/FileChip.vue')['default']
SmartIntersection: typeof import('./components/smart/Intersection.vue')['default']
SmartItem: typeof import('./components/smart/Item.vue')['default']
SmartLink: typeof import('./components/smart/Link.vue')['default']
SmartModal: typeof import('./components/smart/Modal.vue')['default']
SmartProgressRing: typeof import('./components/smart/ProgressRing.vue')['default']
SmartRadio: typeof import('./components/smart/Radio.vue')['default']
SmartRadioGroup: typeof import('./components/smart/RadioGroup.vue')['default']
SmartSlideOver: typeof import('./components/smart/SlideOver.vue')['default']
SmartSpinner: typeof import('./components/smart/Spinner.vue')['default']
SmartTab: typeof import('./components/smart/Tab.vue')['default']
SmartTabs: typeof import('./components/smart/Tabs.vue')['default']
SmartToggle: typeof import('./components/smart/Toggle.vue')['default']
SmartWindow: typeof import('./components/smart/Window.vue')['default']
SmartWindows: typeof import('./components/smart/Windows.vue')['default']
}
}

View File

@@ -0,0 +1,47 @@
import Primary from "./button/Primary.vue"
import Secondary from "./button/Secondary.vue"
import Anchor from "./smart/Anchor.vue"
import AutoComplete from "./smart/AutoComplete.vue"
import Checkbox from "./smart/Checkbox.vue"
import ConfirmModal from "./smart/ConfirmModal.vue"
import Expand from "./smart/Expand.vue"
import FileChip from "./smart/FileChip.vue"
import Intersection from "./smart/Intersection.vue"
import Item from "./smart/Item.vue"
import Link from "./smart/Link.vue"
import Modal from "./smart/Modal.vue"
import ProgressRing from "./smart/ProgressRing.vue"
import Radio from "./smart/Radio.vue"
import RadioGroup from "./smart/RadioGroup.vue"
import SlideOver from "./smart/SlideOver.vue"
import Spinner from "./smart/Spinner.vue"
import Tab from "./smart/Tab.vue"
import Tabs from "./smart/Tabs.vue"
import Toggle from "./smart/Toggle.vue"
import Window from "./smart/Window.vue"
import Windows from "./smart/Windows.vue"
export default {
Primary,
Secondary,
Anchor,
AutoComplete,
Checkbox,
ConfirmModal,
Expand,
FileChip,
Intersection,
Item,
Link,
Modal,
ProgressRing,
Radio,
RadioGroup,
SlideOver,
Spinner,
Tab,
Tabs,
Toggle,
Window,
Windows,
}

View File

@@ -2,7 +2,7 @@
<SmartModal <SmartModal
v-if="show" v-if="show"
dialog dialog
:title="t('modal.confirm')" :title="confirm ?? t?.('modal.confirm') ?? 'Confirm'"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
@close="hideModal" @close="hideModal"
@@ -16,13 +16,13 @@
<span class="flex space-x-2"> <span class="flex space-x-2">
<ButtonPrimary <ButtonPrimary
v-focus v-focus
:label="yes ?? t('action.yes')" :label="yes ?? t?.('action.yes') ?? 'Yes'"
:loading="!!loadingState" :loading="!!loadingState"
outline outline
@click="resolve" @click="resolve"
/> />
<ButtonSecondary <ButtonSecondary
:label="no ?? t('action.no')" :label="no ?? t?.('action.no') ?? 'No'"
filled filled
outline outline
@click="hideModal" @click="hideModal"
@@ -33,20 +33,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from "@composables/i18n" import { inject } from "vue"
import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../index"
const t = useI18n() const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
show: boolean show: boolean
title?: string | null title?: string | null
confirm?: string | null
yes?: string | null yes?: string | null
no?: string | null no?: string | null
loadingState?: boolean | null loadingState?: boolean | null
}>(), }>(),
{ {
title: null, title: null,
confirm: null,
yes: null, yes: null,
no: null, no: null,
loadingState: null, loadingState: null,

View File

@@ -9,7 +9,11 @@
> >
<ButtonSecondary <ButtonSecondary
:icon="expand ? IconChevronUp : IconChevronDown" :icon="expand ? IconChevronUp : IconChevronDown"
:label="expand ? t('action.less') : t('action.more')" :label="
expand
? less ?? t?.('action.less') ?? 'Less'
: more ?? t?.('action.more') ?? 'More'
"
filled filled
rounded rounded
@click="expand = !expand" @click="expand = !expand"
@@ -21,10 +25,21 @@
<script setup lang="ts"> <script setup lang="ts">
import IconChevronUp from "~icons/lucide/chevron-up" import IconChevronUp from "~icons/lucide/chevron-up"
import IconChevronDown from "~icons/lucide/chevron-down" import IconChevronDown from "~icons/lucide/chevron-down"
import { ref } from "vue" import { inject, ref } from "vue"
import { useI18n } from "@composables/i18n" import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../index"
const t = useI18n() const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
const expand = ref(false) const expand = ref(false)
withDefaults(
defineProps<{
less?: string
more?: string
}>(),
{
less: "Less",
more: "More",
}
)
</script> </script>

View File

@@ -48,7 +48,7 @@
<ButtonSecondary <ButtonSecondary
v-if="dimissible" v-if="dimissible"
v-tippy="{ theme: 'tooltip', delay: [500, 20] }" v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
:title="t('action.close')" :title="closeText ?? t?.('action.close') ?? 'Close'"
:icon="IconX" :icon="IconX"
@click="close" @click="close"
/> />
@@ -93,11 +93,18 @@ const stack = (() => {
<script setup lang="ts"> <script setup lang="ts">
import IconX from "~icons/lucide/x" import IconX from "~icons/lucide/x"
import { ref, computed, useSlots, onMounted, onBeforeUnmount } from "vue" import {
import { useKeybindingDisabler } from "~/helpers/keybindings" ref,
import { useI18n } from "@composables/i18n" computed,
useSlots,
onMounted,
onBeforeUnmount,
inject,
} from "vue"
import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../index"
const t = useI18n() const { t, onModalOpen, onModalClose } =
inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
defineProps({ defineProps({
dialog: { dialog: {
@@ -124,16 +131,18 @@ defineProps({
type: String, type: String,
default: "sm:max-w-lg", default: "sm:max-w-lg",
}, },
closeText: {
type: String,
default: null,
},
}) })
const emit = defineEmits<{ const emit = defineEmits<{
(e: "close"): void (e: "close"): void
}>() }>()
const { disableKeybindings, enableKeybindings } = useKeybindingDisabler()
onBeforeUnmount(() => { onBeforeUnmount(() => {
enableKeybindings() onModalClose?.()
}) })
const stackId = ref(stackIDTicker++) const stackId = ref(stackIDTicker++)
@@ -153,7 +162,7 @@ onMounted(() => {
stack.push(stackId.value) stack.push(stackId.value)
document.addEventListener("keydown", onKeyDown) document.addEventListener("keydown", onKeyDown)
disableKeybindings() onModalOpen?.()
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@@ -78,7 +78,6 @@ import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import type { Component } from "vue" import type { Component } from "vue"
import { ref, ComputedRef, computed, provide } from "vue" import { ref, ComputedRef, computed, provide } from "vue"
import { throwError } from "~/helpers/functional/error"
export type TabMeta = { export type TabMeta = {
label: string | null label: string | null
@@ -124,6 +123,10 @@ const emit = defineEmits<{
(e: "update:modelValue", newTabID: string): void (e: "update:modelValue", newTabID: string): void
}>() }>()
const throwError = (message: string): never => {
throw new Error(message)
}
const tabEntries = ref<Array<[string, TabMeta]>>([]) const tabEntries = ref<Array<[string, TabMeta]>>([])
const addTabEntry = (tabID: string, meta: TabMeta) => { const addTabEntry = (tabID: string, meta: TabMeta) => {

View File

@@ -41,7 +41,7 @@
:style="{ :style="{
visibility: tabMeta.isRemovable ? 'visible' : 'hidden', visibility: tabMeta.isRemovable ? 'visible' : 'hidden',
}" }"
:title="t('action.close')" :title="closeText ?? t?.('action.close') ?? 'Close'"
:class="[{ active: modelValue === tabID }, 'close']" :class="[{ active: modelValue === tabID }, 'close']"
class="mx-2 !p-0.5" class="mx-2 !p-0.5"
@click.stop="emit('removeTab', tabID)" @click.stop="emit('removeTab', tabID)"
@@ -60,7 +60,7 @@
> >
<ButtonSecondary <ButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('action.new')" :title="newText ?? t?.('action.new') ?? 'New'"
:icon="IconPlus" :icon="IconPlus"
class="rounded !p-1" class="rounded !p-1"
filled filled
@@ -85,11 +85,10 @@ import { pipe } from "fp-ts/function"
import { not } from "fp-ts/Predicate" import { not } from "fp-ts/Predicate"
import * as A from "fp-ts/Array" import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import { ref, ComputedRef, computed, provide } from "vue" import { ref, ComputedRef, computed, provide, inject } from "vue"
import type { Slot } from "vue" import type { Slot } from "vue"
import draggable from "vuedraggable" import draggable from "vuedraggable"
import { throwError } from "~/helpers/functional/error" import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../index"
import { useI18n } from "~/composables/i18n"
export type TabMeta = { export type TabMeta = {
label: string | null label: string | null
@@ -104,7 +103,7 @@ export type TabProvider = {
removeTabEntry: (tabID: string) => void removeTabEntry: (tabID: string) => void
} }
const t = useI18n() const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
const props = defineProps({ const props = defineProps({
styles: { styles: {
@@ -119,6 +118,14 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
newText: {
type: String,
default: null,
},
closeText: {
type: String,
default: null,
},
}) })
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", newTabID: string): void (e: "update:modelValue", newTabID: string): void
@@ -126,6 +133,11 @@ const emit = defineEmits<{
(e: "removeTab", tabID: string): void (e: "removeTab", tabID: string): void
(e: "addTab"): void (e: "addTab"): void
}>() }>()
const throwError = (message: string): never => {
throw new Error(message)
}
const tabEntries = ref<Array<[string, TabMeta]>>([]) const tabEntries = ref<Array<[string, TabMeta]>>([])
const tabStyles = computed(() => ({ const tabStyles = computed(() => ({
maxWidth: `${tabEntries.value.length * 184}px`, maxWidth: `${tabEntries.value.length * 184}px`,

8
packages/hoppscotch-ui/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module "*.vue" {
import { DefineComponent } from "vue"
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}

View File

@@ -0,0 +1,33 @@
import { Plugin } from "vue"
import "./assets/scss/styles.scss"
import "virtual:windi.css"
/**
@constant HOPP_UI_OPTIONS
@type {string}
A constant representing the key for storing HoppUI plugin options in the global context.
*/
export const HOPP_UI_OPTIONS = "HOPP_UI_OPTIONS"
/**
@typedef {Object} HoppUIPluginOptions
@property {Function} [t] - A function for handling translations for the plugin.
@property {Function} [onModalOpen] - A callback function that is called when a modal is opened.
@property {Function} [onModalClose] - A callback function that is called when a modal is closed.
*/
export type HoppUIPluginOptions = {
t?: (key: string) => string
onModalOpen?: () => void
onModalClose?: () => void
}
const plugin: Plugin = {
install(app, options: HoppUIPluginOptions = {}) {
app.provide(HOPP_UI_OPTIONS, options)
},
}
export default plugin

View File

@@ -0,0 +1,20 @@
<template>
<Story title="Anchor">
<div class="text-secondaryLight text-tiny">
By signing in, you are agreeing to our
<SmartAnchor
class="link text-red-800"
to="https://docs.hoppscotch.io/terms"
blank
label="Terms of Service"
/>
and
<SmartAnchor
class="link text-red-600"
to="https://docs.hoppscotch.io/privacy"
blank
label="Privacy Policy"
/>
</div>
</Story>
</template>

View File

@@ -0,0 +1,55 @@
<template>
<Story title="Auto Complete">
<div class="h-[50vh]">
<SmartAutoComplete
placeholder="Select a header"
:source="commonHeaders"
:spellcheck="false"
:value="header[0].key"
autofocus
styles="
bg-transparent
flex
flex-1
py-1
px-4
truncate
"
class="flex-1 !flex"
@input="updateHeader()"
/>
</div>
</Story>
</template>
<script setup lang="ts">
import { ref } from "vue"
type GQLHeader = {
key: string
value: string
active: boolean
}
const header = ref<GQLHeader[]>([
{
key: "Content-Type",
value: "application/json",
active: true,
},
])
const commonHeaders = [
"Push-Policy",
"Retry-After",
"Signature",
"Signed-Headers",
"Server-Timing",
"SourceMap",
"Upgrade",
]
const updateHeader = () => {
// alert("updated")
}
</script>

View File

@@ -0,0 +1,10 @@
<template>
<Story title="Button">
<Variant title="Primary">
<ButtonPrimary label="Button" />
</Variant>
<Variant title="Secondary">
<ButtonSecondary label="Button" />
</Variant>
</Story>
</template>

View File

@@ -0,0 +1,13 @@
<template>
<Story title="Checkbox">
<Variant title="Single">
<SmartCheckbox :on="on" />
</Variant>
</Story>
</template>
<script setup lang="ts">
import { ref } from "vue"
const on = ref(true)
</script>

View File

@@ -0,0 +1,21 @@
<template>
<Story title="Confirm Modal">
<SmartConfirmModal
:show="show"
:title="'Confirm Modal'"
@hide-modal="show = false"
@resolve="resolveConfirmModal"
/>
</Story>
</template>
<script lang="ts" setup>
import { ref } from "vue"
const show = ref(true)
const resolveConfirmModal = (resolve: string | null) => {
alert("resolved: " + resolve)
show.value = false
}
</script>

View File

@@ -0,0 +1,7 @@
<template>
<Story title="Item">
<Variant title="Single">
<SmartItem :label="'Item'" :active-info-icon="false" />
</Variant>
</Story>
</template>

View File

@@ -0,0 +1,18 @@
<template>
<Story title="Link">
<Variant title="Text Link">
<SmartLink :to="link" :blank="true"> Click here </SmartLink>
</Variant>
<Variant title="Button Link">
<SmartLink :to="link" :blank="true">
<ButtonPrimary label="Click here" />
</SmartLink>
</Variant>
</Story>
</template>
<script setup lang="ts">
import { ref } from "vue"
const link = ref("/graphql")
</script>

View File

@@ -0,0 +1,21 @@
<template>
<Story title="Modal">
<SmartModal
:show="show"
:title="'Modal Title'"
@hide-modal="show = false"
@resolve="resolveConfirmModal"
/>
</Story>
</template>
<script lang="ts" setup>
import { ref } from "vue"
const show = ref(true)
const resolveConfirmModal = (resolve: string | null) => {
alert("resolved: " + resolve)
show.value = false
}
</script>

View File

@@ -0,0 +1,15 @@
<template>
<Story title="Progress Ring">
<SmartProgressRing
class="mr-2 text-red-500"
:radius="8"
:stroke="1.5"
:progress="(failedTests / totalTests) * 100"
/>
</Story>
</template>
<script setup lang="ts">
const totalTests = 10
const failedTests = 2
</script>

View File

@@ -0,0 +1,21 @@
<template>
<Story title="Radio">
<Variant title="Single">
<SmartRadio />
</Variant>
<Variant title="Group">
<SmartRadioGroup :radios="radios" :model-value="selected" />
</Variant>
</Story>
</template>
<script setup lang="ts">
import { ref } from "vue"
const selected = ref("option1")
const radios = [
{ label: "Option 1", value: "option1" },
{ label: "Option 2", value: "option2" },
{ label: "Option 3", value: "option3" },
]
</script>

View File

@@ -0,0 +1,15 @@
<template>
<Story title="Slider Over">
<SmartSlideOver :show="show" :title="'Title'" @close="show = false">
<template #content>
<h1>Content</h1>
</template>
</SmartSlideOver>
</Story>
</template>
<script lang="ts" setup>
import { ref } from "vue"
const show = ref(true)
</script>

View File

@@ -0,0 +1,5 @@
<template>
<Story title="Spinner">
<SmartSpinner />
</Story>
</template>

View File

@@ -0,0 +1,20 @@
<template>
<Story title="Tab">
<Variant title="Single">
<SmartTabs id="my-tab" v-model="selectedTab" render-inactive-tabs>
<SmartTab id="tab1" label="Tab 1">
<h1>Tab 1 content</h1>
</SmartTab>
<SmartTab id="tab2" label="Tab 2">
<h1>Tab 2 content</h1>
</SmartTab>
</SmartTabs>
</Variant>
</Story>
</template>
<script setup lang="ts">
import { ref } from "vue"
const selectedTab = ref("tab1")
</script>

View File

@@ -0,0 +1,15 @@
<template>
<Story title="Toggle">
<SmartToggle :on="on" @change="change"> Turn on </SmartToggle>
</Story>
</template>
<script setup lang="ts">
import { ref } from "vue"
const on = ref(true)
const change = () => {
alert("changed to: " + on.value)
}
</script>

View File

@@ -0,0 +1,72 @@
<template>
<Story title="Window">
<Variant title="Single">
<SmartWindows
:id="'my-window'"
v-model="selectedWindow"
@add-tab="openNewTab"
@remove-tab="removeTab"
@sort="sortTabs"
>
<SmartWindow
v-for="window in tabs"
:id="window.id"
:key="'tab_' + window.id"
:label="window.name"
:is-removable="window.removable"
>
</SmartWindow>
</SmartWindows>
</Variant>
</Story>
</template>
<script setup lang="ts">
import { ref } from "vue"
const selectedWindow = ref("window1")
type Tab = {
id: string
name: string
removable: boolean
}
const tabs = ref<Tab[]>([
{
id: "1",
name: "window1",
removable: false,
},
{
id: "2",
name: "window2",
removable: true,
},
{
id: "3",
name: "window3",
removable: true,
},
])
const openNewTab = () => {
const newTab = {
id: Date.now().toString(),
name: "New Window",
removable: true,
}
tabs.value = [...tabs.value, { ...newTab }]
selectedWindow.value = newTab.id
}
const removeTab = (tabID: string) => {
tabs.value = tabs.value.filter((tab) => tab.id !== tabID)
}
const sortTabs = (e: { oldIndex: number; newIndex: number }) => {
const newTabs = [...tabs.value]
newTabs.splice(e.newIndex, 0, newTabs.splice(e.oldIndex, 1)[0])
tabs.value = newTabs
}
</script>

View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noUnusedLocals": true,
"paths": {
"~/*": ["./src/*"],
"@composables/*": ["./src/composables/*"],
"@components/*": ["./src/components/*"],
"@helpers/*": ["./src/helpers/*"],
"@modules/*": ["./src/modules/*"],
"@workers/*": ["./src/workers/*"],
"@functional/*": ["./src/helpers/functional/*"]
},
"types": [
"vite/client",
"unplugin-icons/types/vue",
"vite-plugin-pages/client",
"vite-plugin-vue-layouts/client",
"vite-plugin-pwa/client"
]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

View File

@@ -0,0 +1,60 @@
import vue from "@vitejs/plugin-vue"
import path from "path"
import { FileSystemIconLoader } from "unplugin-icons/loaders"
import IconResolver from "unplugin-icons/resolver"
import Icons from "unplugin-icons/vite"
import Components from "unplugin-vue-components/vite"
import { defineConfig } from "vite"
import WindiCSS from "vite-plugin-windicss"
module.exports = defineConfig({
plugins: [
vue(),
WindiCSS({
root: path.resolve(__dirname),
}),
Components({
dts: "./src/components.d.ts",
dirs: ["./src/components"],
directoryAsNamespace: true,
resolvers: [
IconResolver({
prefix: "icon",
customCollections: ["hopp", "auth", "brands"],
}),
],
}),
Icons({
compiler: "vue3",
customCollections: {
hopp: FileSystemIconLoader("../hoppscotch-common/assets/icons"),
auth: FileSystemIconLoader("../hoppscotch-common/assets/icons/auth"),
brands: FileSystemIconLoader(
"../hoppscotch-common/assets/icons/brands"
),
},
}),
], // to process SFC
build: {
lib: {
entry: path.resolve(__dirname, "src/index.ts"),
name: "hopp-ui",
formats: ["es"], // adding 'umd' requires globals set to every external module
fileName: (format) => `hopp-ui.${format}.js`,
},
rollupOptions: {
// external modules won't be bundled into HoppUI library
external: ["vue"], // not every external has a global
output: {
// disable warning on src/index.ts using both default and named export
exports: "named",
// Provide global variables to use in the UMD build
// for externalized deps (not useful if 'umd' is not in lib.formats)
globals: {
vue: "Vue",
},
},
},
emptyOutDir: false, // to retain the types folder generated by tsc
},
})

View File

@@ -0,0 +1,65 @@
import { defineConfig } from "windicss/helpers"
export default defineConfig({
theme: {
container: {
center: true,
},
extend: {
inset: {
upperPrimaryStickyFold: "var(--upper-primary-sticky-fold)",
upperSecondaryStickyFold: "var(--upper-secondary-sticky-fold)",
upperTertiaryStickyFold: "var(--upper-tertiary-sticky-fold)",
upperMobilePrimaryStickyFold: "var(--upper-mobile-primary-sticky-fold)",
upperMobileSecondaryStickyFold:
"var(--upper-mobile-secondary-sticky-fold)",
upperMobileStickyFold: "var(--upper-mobile-sticky-fold)",
upperMobileTertiaryStickyFold:
"var(--upper-mobile-tertiary-sticky-fold)",
lowerPrimaryStickyFold: "var(--lower-primary-sticky-fold)",
lowerSecondaryStickyFold: "var(--lower-secondary-sticky-fold)",
lowerTertiaryStickyFold: "var(--lower-tertiary-sticky-fold)",
sidebarPrimaryStickyFold: "var(--sidebar-primary-sticky-fold)",
},
colors: {
primary: "var(--primary-color)",
primaryLight: "var(--primary-light-color)",
primaryDark: "var(--primary-dark-color)",
primaryContrast: "var(--primary-contrast-color)",
secondary: "var(--secondary-color)",
secondaryLight: "var(--secondary-light-color)",
secondaryDark: "var(--secondary-dark-color)",
accent: "var(--accent-color)",
accentLight: "var(--accent-light-color)",
accentDark: "var(--accent-dark-color)",
accentContrast: "var(--accent-contrast-color)",
divider: "var(--divider-color)",
dividerLight: "var(--divider-light-color)",
dividerDark: "var(--divider-dark-color)",
error: "var(--error-color)",
tooltip: "var(--tooltip-color)",
popover: "var(--popover-color)",
gradientFrom: "var(--gradient-from-color)",
gradientVia: "var(--gradient-via-color)",
gradientTo: "var(--gradient-to-color)",
},
fontFamily: {
sans: "var(--font-sans)",
mono: "var(--font-mono)",
icon: "var(--font-icon)",
},
fontSize: {
tiny: "var(--font-size-tiny)",
body: "var(--font-size-body)",
},
lineHeight: {
body: "var(--line-height-body)",
},
cursor: {
nsResize: "ns-resize",
grab: "grab",
grabbing: "grabbing",
},
},
},
})

View File

@@ -107,7 +107,10 @@ export default defineConfig({
}), }),
Components({ Components({
dts: "../hoppscotch-common/src/components.d.ts", dts: "../hoppscotch-common/src/components.d.ts",
dirs: ["../hoppscotch-common/src/components"], dirs: [
"../hoppscotch-common/src/components",
"../hoppscotch-ui/src/components",
],
directoryAsNamespace: true, directoryAsNamespace: true,
resolvers: [ resolvers: [
IconResolver({ IconResolver({

1246
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff