CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-vuetify

Vue Material Component Framework implementing Google's Material Design specification with comprehensive UI components, theming system, and accessibility features.

Pending
Overview
Eval results
Files

directives.mddocs/

Directives

Vue directives for adding interactive behaviors and functionality to DOM elements without requiring component wrappers.

Capabilities

ClickOutside

Directive for handling clicks outside of an element, commonly used for closing dropdowns and modals.

/**
 * Detects clicks outside the element and calls handler
 */
const ClickOutside: Directive;

interface ClickOutsideBinding {
  /** Handler function called when click occurs outside */
  handler?: (event: Event) => void;
  /** Additional options */
  options?: {
    /** Elements to exclude from outside detection */
    exclude?: (string | Element | ComponentPublicInstance)[];
    /** Include the element itself in outside detection */
    closeConditional?: (event: Event) => boolean;
  };
}

// Usage syntax variations
type ClickOutsideValue = 
  | ((event: Event) => void)
  | {
      handler: (event: Event) => void;
      exclude?: (string | Element)[];
      closeConditional?: (event: Event) => boolean;
    };

Usage Examples:

<template>
  <!-- Basic usage -->
  <div v-click-outside="closeMenu" class="menu">
    <button @click="menuOpen = !menuOpen">Toggle Menu</button>
    <div v-show="menuOpen" class="menu-content">
      Menu items here...
    </div>
  </div>

  <!-- With options -->
  <div
    v-click-outside="{
      handler: closeDropdown,
      exclude: ['.ignore-outside', $refs.trigger]
    }"
    class="dropdown"
  >
    <button ref="trigger" @click="dropdownOpen = true">
      Open Dropdown
    </button>
    <div v-show="dropdownOpen" class="dropdown-content">
      <button class="ignore-outside">Won't trigger close</button>
    </div>
  </div>

  <!-- With conditional close -->
  <div
    v-click-outside="{
      handler: closeModal,
      closeConditional: (e) => !e.target.closest('.modal-persistent')
    }"
    class="modal"
  >
    <div class="modal-content">
      <button class="modal-persistent">Safe button</button>
      <button @click="closeModal">Close</button>
    </div>
  </div>
</template>

<script setup>
const menuOpen = ref(false);
const dropdownOpen = ref(false);

const closeMenu = () => {
  menuOpen.value = false;
};

const closeDropdown = () => {
  dropdownOpen.value = false;
};

const closeModal = () => {
  console.log('Modal closed');
};
</script>

Intersect

Directive using Intersection Observer API to detect when elements enter or leave the viewport.

/**
 * Observes element intersection with viewport or ancestor
 */
const Intersect: Directive;

interface IntersectBinding {
  /** Handler function called on intersection changes */
  handler?: (
    entries: IntersectionObserverEntry[], 
    observer: IntersectionObserver,
    isIntersecting: boolean
  ) => void;
  /** Intersection observer options */
  options?: IntersectionObserverInit & {
    /** Only trigger once when element enters */
    once?: boolean;
    /** Quiet mode - don't trigger on initial observation */
    quiet?: boolean;
  };
}

interface IntersectionObserverInit {
  /** Element to use as viewport */
  root?: Element | null;
  /** Margin around root */
  rootMargin?: string;
  /** Threshold percentages for triggering */
  threshold?: number | number[];
}

// Usage syntax variations
type IntersectValue = 
  | ((entries: IntersectionObserverEntry[], observer: IntersectionObserver, isIntersecting: boolean) => void)
  | {
      handler: (entries: IntersectionObserverEntry[], observer: IntersectionObserver, isIntersecting: boolean) => void;
      options?: IntersectionObserverInit & { once?: boolean; quiet?: boolean };
    };

Usage Examples:

<template>
  <!-- Basic intersection detection -->
  <div v-intersect="onIntersect" class="observe-me">
    This element is being watched
  </div>

  <!-- Lazy loading images -->
  <img
    v-for="image in images"
    :key="image.id"
    v-intersect="{
      handler: loadImage,
      options: { once: true, threshold: 0.1 }
    }"
    :data-src="image.src"
    :alt="image.alt"
    class="lazy-image"
  />

  <!-- Infinite scroll trigger -->
  <div class="content-list">
    <div v-for="item in items" :key="item.id">
      {{ item.title }}
    </div>
    <div
      v-intersect="{
        handler: loadMore,
        options: { rootMargin: '100px' }
      }"
      class="scroll-trigger"
    >
      Loading more...
    </div>
  </div>

  <!-- Animation on scroll -->
  <div
    v-intersect="{
      handler: animateElement,
      options: { threshold: 0.5, once: true }
    }"
    class="animate-on-scroll"
    :class="{ 'is-visible': elementVisible }"
  >
    Animate when 50% visible
  </div>
</template>

<script setup>
const images = ref([]);
const items = ref([]);
const elementVisible = ref(false);

const onIntersect = (entries, observer, isIntersecting) => {
  console.log('Element intersecting:', isIntersecting);
};

const loadImage = (entries, observer, isIntersecting) => {
  if (isIntersecting) {
    const img = entries[0].target;
    img.src = img.dataset.src;
    img.classList.add('loaded');
  }
};

const loadMore = async (entries, observer, isIntersecting) => {
  if (isIntersecting) {
    const newItems = await fetchMoreItems();
    items.value.push(...newItems);
  }
};

const animateElement = (entries, observer, isIntersecting) => {
  elementVisible.value = isIntersecting;
};
</script>

<style>
.lazy-image {
  opacity: 0;
  transition: opacity 0.3s;
}

.lazy-image.loaded {
  opacity: 1;
}

.animate-on-scroll {
  transform: translateY(50px);
  opacity: 0;
  transition: all 0.6s ease;
}

.animate-on-scroll.is-visible {
  transform: translateY(0);
  opacity: 1;
}
</style>

Mutate

Directive using MutationObserver to watch for DOM changes within an element.

/**
 * Observes DOM mutations within element
 */
const Mutate: Directive;

interface MutateBinding {
  /** Handler function called when mutations occur */
  handler?: (mutations: MutationRecord[], observer: MutationObserver) => void;
  /** Mutation observer configuration */
  options?: MutationObserverInit;
}

interface MutationObserverInit {
  /** Watch for child node changes */
  childList?: boolean;
  /** Watch for attribute changes */
  attributes?: boolean;
  /** Watch for character data changes */
  characterData?: boolean;
  /** Watch changes in descendant nodes */
  subtree?: boolean;
  /** Report previous attribute values */
  attributeOldValue?: boolean;
  /** Report previous character data */
  characterDataOldValue?: boolean;
  /** Filter specific attributes to watch */
  attributeFilter?: string[];
}

// Usage syntax variations
type MutateValue = 
  | ((mutations: MutationRecord[], observer: MutationObserver) => void)
  | {
      handler: (mutations: MutationRecord[], observer: MutationObserver) => void;
      options?: MutationObserverInit;
    };

Usage Examples:

<template>
  <!-- Watch for content changes -->
  <div
    v-mutate="onContentChange"
    class="dynamic-content"
    v-html="dynamicHtml"
  />

  <!-- Watch for attribute changes -->
  <div
    v-mutate="{
      handler: onAttributeChange,
      options: {
        attributes: true,
        attributeFilter: ['class', 'style']
      }
    }"
    :class="dynamicClass"
    :style="dynamicStyle"
  >
    Watch my attributes
  </div>

  <!-- Watch for child additions -->
  <ul
    v-mutate="{
      handler: onListChange,
      options: {
        childList: true,
        subtree: true
      }
    }"
    class="dynamic-list"
  >
    <li v-for="item in listItems" :key="item.id">
      {{ item.name }}
      <button @click="removeItem(item.id)">Remove</button>
    </li>
  </ul>

  <!-- Monitor form changes -->
  <form
    v-mutate="{
      handler: onFormChange,
      options: {
        attributes: true,
        childList: true,
        subtree: true,
        attributeFilter: ['disabled', 'required']
      }
    }"
  >
    <input v-model="formData.name" :disabled="formDisabled" />
    <input v-model="formData.email" :required="emailRequired" />
  </form>
</template>

<script setup>
const dynamicHtml = ref('<p>Initial content</p>');
const dynamicClass = ref('initial-class');
const listItems = ref([
  { id: 1, name: 'Item 1' },
  { id: 2, name: 'Item 2' }
]);

const onContentChange = (mutations) => {
  mutations.forEach(mutation => {
    if (mutation.type === 'childList') {
      console.log('Content changed:', mutation.addedNodes, mutation.removedNodes);
    }
  });
};

const onAttributeChange = (mutations) => {
  mutations.forEach(mutation => {
    if (mutation.type === 'attributes') {
      console.log(`Attribute ${mutation.attributeName} changed from`, 
        mutation.oldValue, 'to', mutation.target[mutation.attributeName]);
    }
  });
};

const onListChange = (mutations) => {
  console.log('List structure changed:', mutations.length, 'mutations');
};

const onFormChange = (mutations) => {
  console.log('Form changed:', mutations);
  // Handle form validation or auto-save
};
</script>

Resize

Directive using ResizeObserver to detect element size changes.

/**
 * Observes element resize events
 */
const Resize: Directive;

interface ResizeBinding {
  /** Handler function called when element resizes */
  handler?: (entries: ResizeObserverEntry[], observer: ResizeObserver) => void;
  /** Resize observer options */
  options?: {
    /** Debounce resize events (ms) */
    debounce?: number;
    /** Only trigger on width changes */
    watchWidth?: boolean;
    /** Only trigger on height changes */
    watchHeight?: boolean;
  };
}

// Usage syntax variations
type ResizeValue = 
  | ((entries: ResizeObserverEntry[], observer: ResizeObserver) => void)
  | {
      handler: (entries: ResizeObserverEntry[], observer: ResizeObserver) => void;
      options?: { debounce?: number; watchWidth?: boolean; watchHeight?: boolean };
    };

Usage Examples:

<template>
  <!-- Basic resize detection -->
  <div
    v-resize="onResize"
    class="resizable-container"
    :style="{ width: containerWidth + 'px', height: containerHeight + 'px' }"
  >
    <p>Size: {{ currentSize.width }} x {{ currentSize.height }}</p>
    <button @click="resizeContainer">Change Size</button>
  </div>

  <!-- Debounced resize handling -->
  <textarea
    v-resize="{
      handler: onTextareaResize,
      options: { debounce: 250 }
    }"
    v-model="textContent"
    class="auto-resize-textarea"
  />

  <!-- Chart responsive to container -->
  <div
    v-resize="{
      handler: updateChart,
      options: { debounce: 100 }
    }"
    class="chart-container"
  >
    <canvas ref="chartCanvas" />
  </div>

  <!-- Responsive grid -->
  <div
    v-resize="{
      handler: updateGridColumns,
      options: { watchWidth: true, debounce: 150 }
    }"
    class="responsive-grid"
    :style="{ gridTemplateColumns: gridColumns }"
  >
    <div v-for="item in gridItems" :key="item.id" class="grid-item">
      {{ item.name }}
    </div>
  </div>
</template>

<script setup>
const containerWidth = ref(400);
const containerHeight = ref(300);
const currentSize = ref({ width: 0, height: 0 });
const textContent = ref('');
const gridColumns = ref('1fr 1fr 1fr');
const chartCanvas = ref();

const onResize = (entries) => {
  const { width, height } = entries[0].contentRect;
  currentSize.value = { width: Math.round(width), height: Math.round(height) };
};

const onTextareaResize = (entries) => {
  const { width, height } = entries[0].contentRect;
  console.log(`Textarea resized to ${width}x${height}`);
  // Auto-save or adjust UI based on size
};

const updateChart = (entries) => {
  const { width, height } = entries[0].contentRect;
  if (chartCanvas.value) {
    chartCanvas.value.width = width;
    chartCanvas.value.height = height;
    // Redraw chart with new dimensions
    redrawChart();
  }
};

const updateGridColumns = (entries) => {
  const { width } = entries[0].contentRect;
  if (width < 600) {
    gridColumns.value = '1fr';
  } else if (width < 900) {
    gridColumns.value = '1fr 1fr';
  } else {
    gridColumns.value = '1fr 1fr 1fr';
  }
};

const resizeContainer = () => {
  containerWidth.value = Math.random() * 400 + 200;
  containerHeight.value = Math.random() * 300 + 150;
};
</script>

Ripple

Directive for adding Material Design ripple effect to elements on interaction.

/**
 * Adds Material Design ripple effect
 */
const Ripple: Directive;

interface RippleBinding {
  /** Ripple configuration */
  value?: boolean | {
    /** Ripple color */
    color?: string;
    /** Center the ripple effect */
    center?: boolean;
    /** Custom CSS class */
    class?: string;
  };
}

// Usage syntax variations
type RippleValue = 
  | boolean
  | {
      color?: string;
      center?: boolean;
      class?: string;
    };

Usage Examples:

<template>
  <!-- Basic ripple effect -->
  <button v-ripple class="custom-button">
    Click for ripple
  </button>

  <!-- Disabled ripple -->
  <button v-ripple="false" class="no-ripple-button">
    No ripple effect
  </button>

  <!-- Custom color ripple -->
  <div
    v-ripple="{ color: 'red' }"
    class="ripple-card"
    @click="handleClick"
  >
    Custom red ripple
  </div>

  <!-- Centered ripple -->
  <v-icon
    v-ripple="{ center: true }"
    size="large"
    @click="toggleFavorite"
  >
    {{ isFavorite ? 'mdi-heart' : 'mdi-heart-outline' }}
  </v-icon>

  <!-- Conditional ripple -->
  <button
    v-ripple="!disabled"
    :disabled="disabled"
    class="conditional-ripple"
  >
    {{ disabled ? 'Disabled' : 'Enabled' }}
  </button>

  <!-- Custom ripple class -->
  <div
    v-ripple="{ class: 'custom-ripple-class', color: 'purple' }"
    class="fancy-card"
  >
    Custom styled ripple
  </div>
</template>

<script setup>
const isFavorite = ref(false);
const disabled = ref(false);

const handleClick = () => {
  console.log('Card clicked with ripple');
};

const toggleFavorite = () => {
  isFavorite.value = !isFavorite.value;
};
</script>

<style>
.custom-button {
  padding: 12px 24px;
  background: #1976d2;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}

.ripple-card {
  padding: 20px;
  background: #f5f5f5;
  border-radius: 8px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}

.custom-ripple-class {
  animation-duration: 0.8s !important;
  opacity: 0.3 !important;
}
</style>

Scroll

Directive for handling scroll events with customizable options and performance optimizations.

/**
 * Handles scroll events with options
 */
const Scroll: Directive;

interface ScrollBinding {
  /** Scroll handler function */
  handler?: (event: Event) => void;
  /** Scroll configuration options */
  options?: {
    /** Throttle scroll events (ms) */
    throttle?: number;
    /** Target element to watch for scroll */
    target?: string | Element | Window;
    /** Passive event listener */
    passive?: boolean;
  };
}

// Usage syntax variations
type ScrollValue = 
  | ((event: Event) => void)
  | {
      handler: (event: Event) => void;
      options?: {
        throttle?: number;
        target?: string | Element | Window;
        passive?: boolean;
      };
    };

Usage Examples:

<template>
  <!-- Basic scroll handling -->
  <div
    v-scroll="onScroll"
    class="scrollable-content"
    style="height: 400px; overflow-y: auto;"
  >
    <div v-for="n in 100" :key="n" class="scroll-item">
      Item {{ n }}
    </div>
  </div>

  <!-- Throttled scroll -->
  <div
    v-scroll="{
      handler: onThrottledScroll,
      options: { throttle: 100 }
    }"
    class="performance-scroll"
    style="height: 300px; overflow-y: auto;"
  >
    <div style="height: 2000px; background: linear-gradient(to bottom, #f0f0f0, #d0d0d0);">
      Large scrollable content
    </div>
  </div>

  <!-- Window scroll (global) -->
  <div
    v-scroll="{
      handler: onWindowScroll,
      options: { 
        target: 'window',
        throttle: 50,
        passive: true
      }
    }"
  >
    <!-- This element watches window scroll -->
    <div class="scroll-indicator" :style="{ width: scrollProgress + '%' }"></div>
  </div>

  <!-- Scroll to top button -->
  <div class="page-content" style="height: 200vh; padding: 20px;">
    <div
      v-scroll="{
        handler: updateScrollTop,
        options: { target: 'window', throttle: 100 }
      }"
    >
      Long page content...
    </div>
    
    <v-fab
      v-show="showScrollTop"
      location="bottom right"
      icon="mdi-chevron-up"
      @click="scrollToTop"
    />
  </div>

  <!-- Infinite scroll implementation -->
  <div
    v-scroll="{
      handler: checkInfiniteScroll,
      options: { throttle: 200 }
    }"
    class="infinite-list"
    style="height: 400px; overflow-y: auto;"
  >
    <div v-for="item in infiniteItems" :key="item.id" class="list-item">
      {{ item.name }}
    </div>
    <div v-if="loading" class="loading">Loading more...</div>
  </div>
</template>

<script setup>
const scrollProgress = ref(0);
const showScrollTop = ref(false);
const infiniteItems = ref([]);
const loading = ref(false);

const onScroll = (event) => {
  const target = event.target;
  console.log('Scroll position:', target.scrollTop);
};

const onThrottledScroll = (event) => {
  const target = event.target;
  const scrollPercent = (target.scrollTop / (target.scrollHeight - target.clientHeight)) * 100;
  console.log('Scroll percent:', scrollPercent.toFixed(1));
};

const onWindowScroll = () => {
  const scrolled = window.scrollY;
  const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
  scrollProgress.value = (scrolled / maxScroll) * 100;
};

const updateScrollTop = () => {
  showScrollTop.value = window.scrollY > 300;
};

const scrollToTop = () => {
  window.scrollTo({
    top: 0,
    behavior: 'smooth'
  });
};

const checkInfiniteScroll = (event) => {
  const target = event.target;
  const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
  
  if (scrollBottom < 100 && !loading.value) {
    loadMoreItems();
  }
};

const loadMoreItems = async () => {
  loading.value = true;
  // Simulate API call
  await new Promise(resolve => setTimeout(resolve, 1000));
  
  const newItems = Array.from({ length: 20 }, (_, i) => ({
    id: infiniteItems.value.length + i + 1,
    name: `Item ${infiniteItems.value.length + i + 1}`
  }));
  
  infiniteItems.value.push(...newItems);
  loading.value = false;
};
</script>

<style>
.scroll-indicator {
  height: 4px;
  background: linear-gradient(to right, #2196f3, #21cbf3);
  transition: width 0.1s ease;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 9999;
}

.scroll-item,
.list-item {
  padding: 16px;
  border-bottom: 1px solid #eee;
}

.loading {
  padding: 20px;
  text-align: center;
  color: #666;
}
</style>

Touch

Directive for handling touch gestures and swipe interactions on mobile devices.

/**
 * Handles touch gestures and swipes
 */
const Touch: Directive;

interface TouchBinding {
  /** Touch handler functions */
  handlers?: {
    /** Start touch handler */
    start?: (wrapper: TouchWrapper) => void;
    /** End touch handler */
    end?: (wrapper: TouchWrapper) => void;
    /** Move touch handler */
    move?: (wrapper: TouchWrapper) => void;
    /** Left swipe handler */
    left?: (wrapper: TouchWrapper) => void;
    /** Right swipe handler */
    right?: (wrapper: TouchWrapper) => void;
    /** Up swipe handler */
    up?: (wrapper: TouchWrapper) => void;
    /** Down swipe handler */
    down?: (wrapper: TouchWrapper) => void;
  };
  /** Touch configuration options */
  options?: {
    /** Minimum distance for swipe */
    threshold?: number;
    /** Prevent default touch behavior */
    passive?: boolean;
  };
}

interface TouchWrapper {
  /** Original touch event */
  touchstartX: number;
  touchstartY: number;
  touchmoveX: number;
  touchmoveY: number;
  touchendX: number;
  touchendY: number;
  offsetX: number;
  offsetY: number;
}

// Usage syntax variations
type TouchValue = {
  [K in 'start' | 'end' | 'move' | 'left' | 'right' | 'up' | 'down']?: (wrapper: TouchWrapper) => void;
} & {
  options?: {
    threshold?: number;
    passive?: boolean;
  };
};

Usage Examples:

<template>
  <!-- Basic swipe detection -->
  <div
    v-touch="{
      left: () => nextSlide(),
      right: () => previousSlide()
    }"
    class="swipe-container"
  >
    <div class="slide" :style="{ transform: `translateX(-${currentSlide * 100}%)` }">
      <div v-for="(slide, index) in slides" :key="index" class="slide-item">
        Slide {{ index + 1 }}
      </div>
    </div>
  </div>

  <!-- Full gesture handling -->
  <div
    v-touch="{
      start: onTouchStart,
      move: onTouchMove,
      end: onTouchEnd,
      left: onSwipeLeft,
      right: onSwipeRight,
      up: onSwipeUp,
      down: onSwipeDown,
      options: { threshold: 50 }
    }"
    class="gesture-area"
    :style="{
      transform: `translate(${position.x}px, ${position.y}px)`,
      background: touchActive ? '#e3f2fd' : '#f5f5f5'
    }"
  >
    <p>Touch and swipe me!</p>
    <p>Position: {{ position.x }}, {{ position.y }}</p>
    <p>Last gesture: {{ lastGesture }}</p>
  </div>

  <!-- Swipeable cards -->
  <div class="card-stack">
    <div
      v-for="(card, index) in cards"
      v-show="index >= currentCardIndex"
      :key="card.id"
      v-touch="{
        left: () => swipeCard('left', index),
        right: () => swipeCard('right', index),
        options: { threshold: 100 }
      }"
      class="swipe-card"
      :style="{
        zIndex: cards.length - index,
        transform: `scale(${1 - (index - currentCardIndex) * 0.05})`
      }"
    >
      <h3>{{ card.title }}</h3>
      <p>{{ card.content }}</p>
    </div>
  </div>

  <!-- Mobile drawer -->
  <div
    v-touch="{
      right: openDrawer,
      options: { threshold: 30 }
    }"
    class="drawer-edge"
  >
    <!-- Edge area for drawer gesture -->
  </div>

  <div
    v-show="drawerOpen"
    v-touch="{
      left: closeDrawer,
      options: { threshold: 50 }
    }"
    class="drawer"
    :class="{ open: drawerOpen }"
  >
    <div class="drawer-content">
      <h3>Mobile Drawer</h3>
      <p>Swipe left to close</p>
    </div>
  </div>
</template>

<script setup>
const currentSlide = ref(0);
const slides = ref(['Slide 1', 'Slide 2', 'Slide 3']);
const position = ref({ x: 0, y: 0 });
const touchActive = ref(false);
const lastGesture = ref('none');
const currentCardIndex = ref(0);
const drawerOpen = ref(false);

const cards = ref([
  { id: 1, title: 'Card 1', content: 'Swipe left or right' },
  { id: 2, title: 'Card 2', content: 'Another card to swipe' },
  { id: 3, title: 'Card 3', content: 'Last card in stack' },
]);

const nextSlide = () => {
  if (currentSlide.value < slides.value.length - 1) {
    currentSlide.value++;
  }
};

const previousSlide = () => {
  if (currentSlide.value > 0) {
    currentSlide.value--;
  }
};

const onTouchStart = (wrapper) => {
  touchActive.value = true;
  console.log('Touch started at:', wrapper.touchstartX, wrapper.touchstartY);
};

const onTouchMove = (wrapper) => {
  position.value = {
    x: wrapper.touchmoveX - wrapper.touchstartX,
    y: wrapper.touchmoveY - wrapper.touchstartY
  };
};

const onTouchEnd = (wrapper) => {
  touchActive.value = false;
  // Reset position with animation
  setTimeout(() => {
    position.value = { x: 0, y: 0 };
  }, 100);
};

const onSwipeLeft = () => {
  lastGesture.value = 'swipe left';
};

const onSwipeRight = () => {
  lastGesture.value = 'swipe right';
};

const onSwipeUp = () => {
  lastGesture.value = 'swipe up';
};

const onSwipeDown = () => {
  lastGesture.value = 'swipe down';
};

const swipeCard = (direction, index) => {
  if (index === currentCardIndex.value) {
    console.log(`Card swiped ${direction}`);
    currentCardIndex.value++;
  }
};

const openDrawer = () => {
  drawerOpen.value = true;
};

const closeDrawer = () => {
  drawerOpen.value = false;
};
</script>

<style>
.swipe-container {
  width: 300px;
  height: 200px;
  overflow: hidden;
  border-radius: 8px;
  background: #f0f0f0;
}

.slide {
  display: flex;
  transition: transform 0.3s ease;
}

.slide-item {
  min-width: 300px;
  height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
}

.gesture-area {
  width: 300px;
  height: 200px;
  border-radius: 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  transition: transform 0.2s ease, background 0.2s ease;
  cursor: grab;
  user-select: none;
}

.card-stack {
  position: relative;
  width: 280px;
  height: 180px;
}

.swipe-card {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
  padding: 20px;
  transition: transform 0.2s ease;
  cursor: grab;
}

.drawer-edge {
  position: fixed;
  left: 0;
  top: 0;
  width: 20px;
  height: 100vh;
  z-index: 1000;
}

.drawer {
  position: fixed;
  left: -300px;
  top: 0;
  width: 300px;
  height: 100vh;
  background: white;
  box-shadow: 2px 0 10px rgba(0,0,0,0.1);
  transition: left 0.3s ease;
  z-index: 999;
}

.drawer.open {
  left: 0;
}

.drawer-content {
  padding: 20px;
}
</style>

Tooltip (Directive)

Directive version of tooltip for simple text tooltips without component overhead.

/**
 * Simple tooltip directive
 */
const Tooltip: Directive;

interface TooltipBinding {
  /** Tooltip text or configuration */
  value?: string | {
    /** Tooltip text */
    text: string;
    /** Tooltip position */
    location?: 'top' | 'bottom' | 'left' | 'right';
    /** Show delay in ms */
    delay?: number;
    /** Disabled state */
    disabled?: boolean;
  };
}

// Usage syntax variations
type TooltipValue = 
  | string
  | {
      text: string;
      location?: 'top' | 'bottom' | 'left' | 'right';
      delay?: number;
      disabled?: boolean;
    };

Usage Examples:

<template>
  <!-- Simple text tooltip -->
  <button v-tooltip="'This is a simple tooltip'">
    Hover for tooltip
  </button>

  <!-- Positioned tooltip -->
  <v-icon
    v-tooltip="{
      text: 'Save your work',
      location: 'bottom'
    }"
    @click="saveWork"
  >
    mdi-content-save
  </v-icon>

  <!-- Tooltip with delay -->
  <v-chip
    v-tooltip="{
      text: 'Click to remove',
      location: 'top',
      delay: 1000
    }"
    closable
    @click:close="removeChip"
  >
    Delayed tooltip
  </v-chip>

  <!-- Conditional tooltip -->
  <v-btn
    v-tooltip="{
      text: 'Please fill all required fields',
      disabled: formValid
    }"
    :disabled="!formValid"
    @click="submitForm"
  >
    Submit
  </v-btn>

  <!-- Dynamic tooltip content -->
  <div
    v-for="item in items"
    :key="item.id"
    v-tooltip="getTooltipText(item)"
    class="item"
  >
    {{ item.name }}
  </div>
</template>

<script setup>
const formValid = ref(false);
const items = ref([
  { id: 1, name: 'Item 1', description: 'First item description' },
  { id: 2, name: 'Item 2', description: 'Second item description' },
]);

const saveWork = () => {
  console.log('Work saved');
};

const removeChip = () => {
  console.log('Chip removed');
};

const submitForm = () => {
  if (formValid.value) {
    console.log('Form submitted');
  }
};

const getTooltipText = (item) => {
  return {
    text: item.description,
    location: 'right',
    delay: 500
  };
};
</script>

<style>
.item {
  padding: 8px 16px;
  margin: 4px;
  background: #f5f5f5;
  border-radius: 4px;
  cursor: pointer;
}

.item:hover {
  background: #e0e0e0;
}
</style>

Types

// Common directive binding interface
interface DirectiveBinding<T = any> {
  value: T;
  oldValue: T;
  arg?: string;
  modifiers: Record<string, boolean>;
  instance: ComponentPublicInstance | null;
  dir: ObjectDirective<any, T>;
}

// Event types
interface TouchWrapper {
  touchstartX: number;
  touchstartY: number;
  touchmoveX: number;
  touchmoveY: number;
  touchendX: number;
  touchendY: number;
  offsetX: number;
  offsetY: number;
}

// Observer types
interface IntersectionObserverEntry {
  boundingClientRect: DOMRectReadOnly;
  intersectionRatio: number;
  intersectionRect: DOMRectReadOnly;
  isIntersecting: boolean;
  rootBounds: DOMRectReadOnly | null;
  target: Element;
  time: number;
}

interface ResizeObserverEntry {
  borderBoxSize: ResizeObserverSize[];
  contentBoxSize: ResizeObserverSize[];
  contentRect: DOMRectReadOnly;
  target: Element;
}

interface MutationRecord {
  type: 'childList' | 'attributes' | 'characterData';
  target: Node;
  addedNodes: NodeList;
  removedNodes: NodeList;
  previousSibling: Node | null;
  nextSibling: Node | null;
  attributeName: string | null;
  attributeNamespace: string | null;
  oldValue: string | null;
}

// Location types for tooltips and positioning
type DirectiveLocation = 'top' | 'bottom' | 'left' | 'right';

Install with Tessl CLI

npx tessl i tessl/npm-vuetify

docs

components.md

composables.md

data-display.md

directives.md

feedback.md

forms.md

framework-core.md

icons.md

index.md

internationalization.md

lab-components.md

navigation.md

theming.md

transitions.md

utilities.md

tile.json