Skip to main content
Glama
VButton.vue15.4 kB
<!-- THIS COMPONENT IS DEPRECATED, DO NOT USE IT ANYMORE!--> <template> <component :is="htmlTagOrComponentType" v-bind="dynamicAttrs" ref="component" v-tooltip="truncateRef?.tooltipActive ? truncateRef.tooltip : undefined" class="vbutton" :tabindex="tabIndex" :class="clsx(computedClasses, computedTextSize)" @click="clickHandler($event)" > <div class="vbutton__inner"> <template v-if="computedLoading"> <Icon class="vbutton__icon" :name="loadingIcon" /> <span class="vbutton__text"> {{ loadingText }}</span> </template> <template v-else-if="showSuccess"> <Icon class="vbutton__icon" :name="successIcon" /> <span class="vbutton__text"> <slot name="success">{{ successText }}</slot> </span> </template> <template v-else> <slot name="icon"> <Icon v-if="icon" :class=" clsx('vbutton__icon', props.iconClass ? props.iconClass : '') " :name="icon" /> </slot> <TruncateWithTooltip v-if="truncateText" ref="truncateRef" class="py-xs" > <slot v-if="confirmClick && confirmFirstClickAt" name="confirm-click"> | {{ confirmClick === true ? "You sure? Click again to confirm" : confirmClick }} </slot> <slot v-else>{{ label }}</slot> </TruncateWithTooltip> <span v-else class="vbutton__text"> <slot v-if="confirmClick && confirmFirstClickAt" name="confirm-click"> | {{ confirmClick === true ? "You sure? Click again to confirm" : confirmClick }} </slot> <slot v-else>{{ label }}</slot> </span> <slot name="iconRight"> <Icon v-if="iconRight" class="vbutton__icon" :name="iconRight" /> </slot> <slot name="pill"> <TextPill v-if="pill" class="ml-2xs" mono>{{ pill }}</TextPill> </slot> </template> </div> </component> </template> <script lang="ts" setup> import { ref, computed, onBeforeUnmount, watch, PropType } from "vue"; import { RouterLink } from "vue-router"; import * as _ from "lodash-es"; import clsx from "clsx"; import { ApiRequestStatus } from "../../pinia"; import Icon from "../icons/Icon.vue"; import { IconNames } from "../icons/icon_set"; import { Tones } from "../utils/color_utils"; import { useTheme } from "../utils/theme_tools"; import TextPill from "./TextPill.vue"; import TruncateWithTooltip from "./TruncateWithTooltip.vue"; import { tw } from "../../utils/tw-utils"; const SHOW_SUCCESS_DELAY = 2000; type ButtonSizes = "2xs" | "xs" | "sm" | "md" | "lg" | "xl"; type ButtonVariants = "solid" | "ghost" | "soft" | "transparent"; type ButtonTones = Tones; const props = defineProps({ size: { type: String as PropType<ButtonSizes>, default: "md" }, textSize: { type: String as PropType<ButtonSizes> }, iconClass: { type: String }, variant: { type: String as PropType<ButtonVariants>, default: "solid" }, tone: { type: String as PropType<ButtonTones>, default: "action" }, label: { type: String }, icon: String as PropType<IconNames>, iconRight: String as PropType<IconNames>, href: String, linkToNamedRoute: String, linkTo: [String, Object], target: String, disabled: Boolean, loading: Boolean, loadingText: { type: String, default: "Loading..." }, loadingIcon: { type: String as PropType<IconNames>, default: "loader" }, requestStatus: { type: [Boolean, Object] as PropType<false | ApiRequestStatus>, // can be false if passing 'someCondition && status' }, clickSuccess: { type: Boolean }, successText: { type: String, default: "Success!" }, iconSuccess: { type: String as PropType<IconNames> }, confirmClick: { type: [Boolean, String] }, submit: { type: Boolean }, square: { type: Boolean }, rounded: { type: Boolean }, pill: { type: String, required: false }, truncateText: { type: Boolean }, hoverGlow: Boolean, tabIndex: Number, }); const truncateRef = ref<InstanceType<typeof TruncateWithTooltip>>(); const emit = defineEmits(["click"]); const successIcon = computed(() => { if (props.iconSuccess) return props.iconSuccess; return "check"; }); const htmlTagOrComponentType = computed(() => { if (props.href) return "a"; if (props.linkTo || props.linkToNamedRoute) return RouterLink; return "button"; }); const htmlButtonType = computed(() => { if (htmlTagOrComponentType.value !== "button") return undefined; if (props.submit) return "submit"; return "button"; }); // loading status can be passed in via loading prop or from requestStatus const computedLoading = computed( () => props.loading || !!_.get(props, "requestStatus.isPending"), ); // we use an object to do some dynamic bindings so we don't attach props that are not needed const dynamicAttrs = computed(() => { return { // set the "to" prop if we are in router link mode ...(htmlTagOrComponentType.value === RouterLink && { to: props.linkToNamedRoute ? { name: props.linkToNamedRoute } : props.linkTo, }), // if we set href to undefined when in RouterLink mode, it doesn't set it properly ...(htmlTagOrComponentType.value === "a" && { href: props.href, }), // set the target when its a link/router link ...((htmlTagOrComponentType.value === RouterLink || (htmlTagOrComponentType.value === "a" && props.target)) && { target: props.target, }), ...(htmlButtonType.value && { type: htmlButtonType.value, }), }; }); // watch request status and show a success message for a short time when request goes through const showSuccess = ref(false); watch( () => props.requestStatus, (newStatus, oldStatus, onInvalidate) => { // TODO: look over the reactivity / types here... // status object can change without values changing if using a keyed request status with a /* // that returns an object of keyed statuses if (_.isEqual(newStatus, oldStatus)) return; if (!newStatus) return; // toggle button to show a success message for a short period if (newStatus.isSuccess && props.successText) { showSuccess.value = true; const timeout = setTimeout(() => { showSuccess.value = false; }, SHOW_SUCCESS_DELAY); onInvalidate(() => clearTimeout(timeout)); } }, { deep: true }, ); // get the right type of timeout (some weirdness around NodeJS.Timeout) type Timeout = ReturnType<typeof setTimeout>; // confirm click functionality -- requires the user to click twice to confirm // can be a nicer lightweight alternative to a modal const confirmFirstClickAt = ref<Date | null>(null); let confirmClickTimeout: Timeout; let successClickTimeout: Timeout; function clickHandler(e: MouseEvent) { if (props.confirmClick) { if (confirmFirstClickAt.value) { // check if the 2 clicks are super close together and ignore if they are // this is to help ignore some users who always double click everything if (+new Date() - +confirmFirstClickAt.value < 300) { return; } confirmFirstClickAt.value = null; clearTimeout(confirmClickTimeout); emit("click"); } else { confirmFirstClickAt.value = new Date(); confirmClickTimeout = setTimeout(() => { confirmFirstClickAt.value = null; }, 3000); } } else { if (props.clickSuccess) { showSuccess.value = true; successClickTimeout = setTimeout(() => { showSuccess.value = false; }, SHOW_SUCCESS_DELAY); } emit("click", e); } } const component = ref<InstanceType<typeof HTMLElement>>(); const focus = () => { component.value?.focus(); }; defineExpose({ focus, }); onBeforeUnmount(() => { if (successClickTimeout) clearTimeout(successClickTimeout); }); const containerTheme = useTheme(); const computedTextSize = computed(() => { if (props.textSize) { return { "2xs": tw`text-2xs`, xs: tw`text-xs`, sm: tw`text-sm`, md: tw`text-md`, lg: tw`text-lg`, xl: tw`text-xl`, }[props.textSize]; } else { return { "2xs": tw`text-[8px]`, xs: tw`text-[12px]`, sm: tw`text-[14px]`, md: tw`text-[14px]`, lg: tw`text-[18px]`, xl: tw`text-[20px]`, }[props.size]; } }); const computedClasses = computed(() => ({ "--disabled": !!props.disabled, "--loading": !!computedLoading.value, ...(props.variant && { [`--variant-${props.variant}`]: true }), ...(props.size && { [`--size-${props.size}`]: true }), ...(props.textSize && { [`--text-size-${props.textSize}`]: true }), ...(props.tone && { [`--tone-${props.tone}`]: true }), "--rounded": !!props.rounded, "--hover-glow": !!props.hoverGlow, "--within-dark": containerTheme.theme.value === "dark", "--within-light": containerTheme.theme.value === "light", ...(props.square ? { "rounded-none": true } : { rounded: true }), })); </script> <style lang="less"> .vbutton:focus-visible { outline: none; } .vbutton { display: inline-block; vertical-align: middle; border-style: solid; border-width: 1px; border-color: rgba(0, 0, 0, 0); // margin-right: 4px; // margin-bottom: 1px; // font-size: 14px; // font-family: @fancy-font; // text-transform: uppercase; text-decoration: none; font-weight: bold; text-align: center; text-shadow: none; white-space: nowrap; cursor: pointer; user-select: none; transition: all 0.25s; transition-property: color, border-color, background-color; position: relative; z-index: 2; // border-color: rgba(0,0,0,0) !important; // background-color: rgba(0,0,0,0) !important; > * { pointer-events: none; } .vbutton__inner { display: flex; align-items: center; justify-content: center; position: relative; z-index: 2; gap: 8px; } .vbutton__text { line-height: 24px; &:empty { display: none; } } .vbutton__icon { flex-grow: 0; flex-shrink: 0; pointer-events: none; } // &:focus { // } // &:hover { // } // Size options (medium is default) &.--size-2xs { // font-size: 8px; padding: 1px 1px; // border-radius: 8px; .vbutton__icon { padding: 0px; } .vbutton__inner { gap: 1px; padding: 0 0px; } .vbutton__text { padding: 0 1px; } } &.--size-xs { // font-size: 12px; padding: 2px 2px; // border-radius: 8px; .vbutton__icon { padding: 0px; } .vbutton__inner { gap: 2px; padding: 0 0px; } .vbutton__text { padding: 0 2px; } } &.--size-sm { // font-size: 14px; padding: 2px 4px; .vbutton__inner { gap: 2px; } .vbutton__icon { padding: 4px; } } &.--size-md { padding: 6px 8px; .vbutton__inner { gap: 4px; } .vbutton__icon { width: 24px; height: 24px; } } &.--size-lg { padding: 14px 24px; border-width: 2px; // font-size: 18px; .vbutton__inner { gap: 8px; } .vbutton__icon { width: 32px; height: 32px; margin-top: -4px; // to keep the icon size 24x24 margin-bottom: -4px; } } &.--size-xl { max-width: 100%; // font-size: 20px; padding: 22px 36px; border-width: 2px; .vbutton__inner { gap: 16px; } .vbutton__icon { width: 40px; height: 40px; margin-top: -8px; // to keep the icon size 24x24 margin-bottom: -8px; } } &.--disabled, &.--loading { pointer-events: none; opacity: 0.4; // cannot change cursor since pointer events are disabled } &:active { transform: scale3d(0.95, 0.95, 1); } // Set up theme helpers so we can quickly add new color themes .button-theme-generator(@color) { --button-glow-color: @color; --button-glow-color-darker: darken(@color, 20%); &.--variant-solid { background-color: @color; color: if( @color = @colors-action-300, // an exception to the rule for action 300 "white", // sets legible text color based on the background color contrast(@color) ); // set hover to either lighten or darken depending on if color is bright or dark &:hover when (lightness(@color) > 50%) { background: darken(@color, 10%); } &:hover when (lightness(@color) < 50%) { background: lighten(@color, 10%); } } &.--variant-ghost { color: @color; border-color: @color; background-color: fade(@color, 0); &:hover { background-color: fade(@color, 15); } &:active { background-color: fade(@color, 20); } } &.--variant-soft { color: @color; background: fade(@color, 15); &:hover { background: fade(@color, 20); } &:active { background: fade(@color, 25); } } &.--variant-transparent { color: @color; background: fade(@color, 0); &:hover { background: fade(@color, 15); } &:active { background: fade(@color, 25); } } } .create-transparent-button-theme(@color, @alpha: 0) { border-color: @color; background: fade(@color, @alpha); color: @color !important; &:hover { background: fade(@color, 10); border-color: @color; } } &.--tone-action { &.--within-dark { .button-theme-generator(@colors-action-300); } &.--within-light { .button-theme-generator(@colors-action-500); } } &.--tone-destructive { .button-theme-generator(@colors-destructive-500); } &.--tone-success { .button-theme-generator(@colors-success-500); } &.--tone-warning { .button-theme-generator(@colors-warning-500); } &.--tone-neutral { .button-theme-generator(@colors-neutral-500); } &.--tone-shade { &.--within-dark { .button-theme-generator(@colors-white); } &.--within-light { .button-theme-generator(@colors-black); } } &.--rounded { border-radius: 9999px; } &.--hover-glow:hover { animation: 1s glow_start, 1s glow 1s ease-in-out infinite alternate; } @keyframes glow_start { from { box-shadow: none; } to { box-shadow: 0 0 2px 1px rgba(0, 0, 0, 60%), 0 0 15px var(--button-glow-color), 0 0 25px var(--button-glow-color), 0 0 35px var(--button-glow-color), 0 0 45px var(--button-glow-color), 0 0 55px var(--button-glow-color); } } @keyframes glow { from { box-shadow: 0 0 2px 1px rgba(0, 0, 0, 60%), 0 0 15px var(--button-glow-color), 0 0 25px var(--button-glow-color), 0 0 35px var(--button-glow-color), 0 0 45px var(--button-glow-color), 0 0 55px var(--button-glow-color); } to { box-shadow: 0 0 2px 1px rgba(0, 0, 0, 60%), 0 0 5px var(--button-glow-color), 0 0 10px var(--button-glow-color), 0 0 20px var(--button-glow-color), 0 0 30px var(--button-glow-color-darker), 0 0 40px var(--button-glow-color-darker); } } } </style>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server