or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

components.mdcomposables.mddata-display.mddirectives.mdfeedback.mdforms.mdframework-core.mdicons.mdindex.mdinternationalization.mdlab-components.mdnavigation.mdtheming.mdtransitions.mdutilities.md

directives.mddocs/

0

# Directives

1

2

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

3

4

## Capabilities

5

6

### ClickOutside

7

8

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

9

10

```typescript { .api }

11

/**

12

* Detects clicks outside the element and calls handler

13

*/

14

const ClickOutside: Directive;

15

16

interface ClickOutsideBinding {

17

/** Handler function called when click occurs outside */

18

handler?: (event: Event) => void;

19

/** Additional options */

20

options?: {

21

/** Elements to exclude from outside detection */

22

exclude?: (string | Element | ComponentPublicInstance)[];

23

/** Include the element itself in outside detection */

24

closeConditional?: (event: Event) => boolean;

25

};

26

}

27

28

// Usage syntax variations

29

type ClickOutsideValue =

30

| ((event: Event) => void)

31

| {

32

handler: (event: Event) => void;

33

exclude?: (string | Element)[];

34

closeConditional?: (event: Event) => boolean;

35

};

36

```

37

38

**Usage Examples:**

39

40

```vue

41

<template>

42

<!-- Basic usage -->

43

<div v-click-outside="closeMenu" class="menu">

44

<button @click="menuOpen = !menuOpen">Toggle Menu</button>

45

<div v-show="menuOpen" class="menu-content">

46

Menu items here...

47

</div>

48

</div>

49

50

<!-- With options -->

51

<div

52

v-click-outside="{

53

handler: closeDropdown,

54

exclude: ['.ignore-outside', $refs.trigger]

55

}"

56

class="dropdown"

57

>

58

<button ref="trigger" @click="dropdownOpen = true">

59

Open Dropdown

60

</button>

61

<div v-show="dropdownOpen" class="dropdown-content">

62

<button class="ignore-outside">Won't trigger close</button>

63

</div>

64

</div>

65

66

<!-- With conditional close -->

67

<div

68

v-click-outside="{

69

handler: closeModal,

70

closeConditional: (e) => !e.target.closest('.modal-persistent')

71

}"

72

class="modal"

73

>

74

<div class="modal-content">

75

<button class="modal-persistent">Safe button</button>

76

<button @click="closeModal">Close</button>

77

</div>

78

</div>

79

</template>

80

81

<script setup>

82

const menuOpen = ref(false);

83

const dropdownOpen = ref(false);

84

85

const closeMenu = () => {

86

menuOpen.value = false;

87

};

88

89

const closeDropdown = () => {

90

dropdownOpen.value = false;

91

};

92

93

const closeModal = () => {

94

console.log('Modal closed');

95

};

96

</script>

97

```

98

99

### Intersect

100

101

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

102

103

```typescript { .api }

104

/**

105

* Observes element intersection with viewport or ancestor

106

*/

107

const Intersect: Directive;

108

109

interface IntersectBinding {

110

/** Handler function called on intersection changes */

111

handler?: (

112

entries: IntersectionObserverEntry[],

113

observer: IntersectionObserver,

114

isIntersecting: boolean

115

) => void;

116

/** Intersection observer options */

117

options?: IntersectionObserverInit & {

118

/** Only trigger once when element enters */

119

once?: boolean;

120

/** Quiet mode - don't trigger on initial observation */

121

quiet?: boolean;

122

};

123

}

124

125

interface IntersectionObserverInit {

126

/** Element to use as viewport */

127

root?: Element | null;

128

/** Margin around root */

129

rootMargin?: string;

130

/** Threshold percentages for triggering */

131

threshold?: number | number[];

132

}

133

134

// Usage syntax variations

135

type IntersectValue =

136

| ((entries: IntersectionObserverEntry[], observer: IntersectionObserver, isIntersecting: boolean) => void)

137

| {

138

handler: (entries: IntersectionObserverEntry[], observer: IntersectionObserver, isIntersecting: boolean) => void;

139

options?: IntersectionObserverInit & { once?: boolean; quiet?: boolean };

140

};

141

```

142

143

**Usage Examples:**

144

145

```vue

146

<template>

147

<!-- Basic intersection detection -->

148

<div v-intersect="onIntersect" class="observe-me">

149

This element is being watched

150

</div>

151

152

<!-- Lazy loading images -->

153

<img

154

v-for="image in images"

155

:key="image.id"

156

v-intersect="{

157

handler: loadImage,

158

options: { once: true, threshold: 0.1 }

159

}"

160

:data-src="image.src"

161

:alt="image.alt"

162

class="lazy-image"

163

/>

164

165

<!-- Infinite scroll trigger -->

166

<div class="content-list">

167

<div v-for="item in items" :key="item.id">

168

{{ item.title }}

169

</div>

170

<div

171

v-intersect="{

172

handler: loadMore,

173

options: { rootMargin: '100px' }

174

}"

175

class="scroll-trigger"

176

>

177

Loading more...

178

</div>

179

</div>

180

181

<!-- Animation on scroll -->

182

<div

183

v-intersect="{

184

handler: animateElement,

185

options: { threshold: 0.5, once: true }

186

}"

187

class="animate-on-scroll"

188

:class="{ 'is-visible': elementVisible }"

189

>

190

Animate when 50% visible

191

</div>

192

</template>

193

194

<script setup>

195

const images = ref([]);

196

const items = ref([]);

197

const elementVisible = ref(false);

198

199

const onIntersect = (entries, observer, isIntersecting) => {

200

console.log('Element intersecting:', isIntersecting);

201

};

202

203

const loadImage = (entries, observer, isIntersecting) => {

204

if (isIntersecting) {

205

const img = entries[0].target;

206

img.src = img.dataset.src;

207

img.classList.add('loaded');

208

}

209

};

210

211

const loadMore = async (entries, observer, isIntersecting) => {

212

if (isIntersecting) {

213

const newItems = await fetchMoreItems();

214

items.value.push(...newItems);

215

}

216

};

217

218

const animateElement = (entries, observer, isIntersecting) => {

219

elementVisible.value = isIntersecting;

220

};

221

</script>

222

223

<style>

224

.lazy-image {

225

opacity: 0;

226

transition: opacity 0.3s;

227

}

228

229

.lazy-image.loaded {

230

opacity: 1;

231

}

232

233

.animate-on-scroll {

234

transform: translateY(50px);

235

opacity: 0;

236

transition: all 0.6s ease;

237

}

238

239

.animate-on-scroll.is-visible {

240

transform: translateY(0);

241

opacity: 1;

242

}

243

</style>

244

```

245

246

### Mutate

247

248

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

249

250

```typescript { .api }

251

/**

252

* Observes DOM mutations within element

253

*/

254

const Mutate: Directive;

255

256

interface MutateBinding {

257

/** Handler function called when mutations occur */

258

handler?: (mutations: MutationRecord[], observer: MutationObserver) => void;

259

/** Mutation observer configuration */

260

options?: MutationObserverInit;

261

}

262

263

interface MutationObserverInit {

264

/** Watch for child node changes */

265

childList?: boolean;

266

/** Watch for attribute changes */

267

attributes?: boolean;

268

/** Watch for character data changes */

269

characterData?: boolean;

270

/** Watch changes in descendant nodes */

271

subtree?: boolean;

272

/** Report previous attribute values */

273

attributeOldValue?: boolean;

274

/** Report previous character data */

275

characterDataOldValue?: boolean;

276

/** Filter specific attributes to watch */

277

attributeFilter?: string[];

278

}

279

280

// Usage syntax variations

281

type MutateValue =

282

| ((mutations: MutationRecord[], observer: MutationObserver) => void)

283

| {

284

handler: (mutations: MutationRecord[], observer: MutationObserver) => void;

285

options?: MutationObserverInit;

286

};

287

```

288

289

**Usage Examples:**

290

291

```vue

292

<template>

293

<!-- Watch for content changes -->

294

<div

295

v-mutate="onContentChange"

296

class="dynamic-content"

297

v-html="dynamicHtml"

298

/>

299

300

<!-- Watch for attribute changes -->

301

<div

302

v-mutate="{

303

handler: onAttributeChange,

304

options: {

305

attributes: true,

306

attributeFilter: ['class', 'style']

307

}

308

}"

309

:class="dynamicClass"

310

:style="dynamicStyle"

311

>

312

Watch my attributes

313

</div>

314

315

<!-- Watch for child additions -->

316

<ul

317

v-mutate="{

318

handler: onListChange,

319

options: {

320

childList: true,

321

subtree: true

322

}

323

}"

324

class="dynamic-list"

325

>

326

<li v-for="item in listItems" :key="item.id">

327

{{ item.name }}

328

<button @click="removeItem(item.id)">Remove</button>

329

</li>

330

</ul>

331

332

<!-- Monitor form changes -->

333

<form

334

v-mutate="{

335

handler: onFormChange,

336

options: {

337

attributes: true,

338

childList: true,

339

subtree: true,

340

attributeFilter: ['disabled', 'required']

341

}

342

}"

343

>

344

<input v-model="formData.name" :disabled="formDisabled" />

345

<input v-model="formData.email" :required="emailRequired" />

346

</form>

347

</template>

348

349

<script setup>

350

const dynamicHtml = ref('<p>Initial content</p>');

351

const dynamicClass = ref('initial-class');

352

const listItems = ref([

353

{ id: 1, name: 'Item 1' },

354

{ id: 2, name: 'Item 2' }

355

]);

356

357

const onContentChange = (mutations) => {

358

mutations.forEach(mutation => {

359

if (mutation.type === 'childList') {

360

console.log('Content changed:', mutation.addedNodes, mutation.removedNodes);

361

}

362

});

363

};

364

365

const onAttributeChange = (mutations) => {

366

mutations.forEach(mutation => {

367

if (mutation.type === 'attributes') {

368

console.log(`Attribute ${mutation.attributeName} changed from`,

369

mutation.oldValue, 'to', mutation.target[mutation.attributeName]);

370

}

371

});

372

};

373

374

const onListChange = (mutations) => {

375

console.log('List structure changed:', mutations.length, 'mutations');

376

};

377

378

const onFormChange = (mutations) => {

379

console.log('Form changed:', mutations);

380

// Handle form validation or auto-save

381

};

382

</script>

383

```

384

385

### Resize

386

387

Directive using ResizeObserver to detect element size changes.

388

389

```typescript { .api }

390

/**

391

* Observes element resize events

392

*/

393

const Resize: Directive;

394

395

interface ResizeBinding {

396

/** Handler function called when element resizes */

397

handler?: (entries: ResizeObserverEntry[], observer: ResizeObserver) => void;

398

/** Resize observer options */

399

options?: {

400

/** Debounce resize events (ms) */

401

debounce?: number;

402

/** Only trigger on width changes */

403

watchWidth?: boolean;

404

/** Only trigger on height changes */

405

watchHeight?: boolean;

406

};

407

}

408

409

// Usage syntax variations

410

type ResizeValue =

411

| ((entries: ResizeObserverEntry[], observer: ResizeObserver) => void)

412

| {

413

handler: (entries: ResizeObserverEntry[], observer: ResizeObserver) => void;

414

options?: { debounce?: number; watchWidth?: boolean; watchHeight?: boolean };

415

};

416

```

417

418

**Usage Examples:**

419

420

```vue

421

<template>

422

<!-- Basic resize detection -->

423

<div

424

v-resize="onResize"

425

class="resizable-container"

426

:style="{ width: containerWidth + 'px', height: containerHeight + 'px' }"

427

>

428

<p>Size: {{ currentSize.width }} x {{ currentSize.height }}</p>

429

<button @click="resizeContainer">Change Size</button>

430

</div>

431

432

<!-- Debounced resize handling -->

433

<textarea

434

v-resize="{

435

handler: onTextareaResize,

436

options: { debounce: 250 }

437

}"

438

v-model="textContent"

439

class="auto-resize-textarea"

440

/>

441

442

<!-- Chart responsive to container -->

443

<div

444

v-resize="{

445

handler: updateChart,

446

options: { debounce: 100 }

447

}"

448

class="chart-container"

449

>

450

<canvas ref="chartCanvas" />

451

</div>

452

453

<!-- Responsive grid -->

454

<div

455

v-resize="{

456

handler: updateGridColumns,

457

options: { watchWidth: true, debounce: 150 }

458

}"

459

class="responsive-grid"

460

:style="{ gridTemplateColumns: gridColumns }"

461

>

462

<div v-for="item in gridItems" :key="item.id" class="grid-item">

463

{{ item.name }}

464

</div>

465

</div>

466

</template>

467

468

<script setup>

469

const containerWidth = ref(400);

470

const containerHeight = ref(300);

471

const currentSize = ref({ width: 0, height: 0 });

472

const textContent = ref('');

473

const gridColumns = ref('1fr 1fr 1fr');

474

const chartCanvas = ref();

475

476

const onResize = (entries) => {

477

const { width, height } = entries[0].contentRect;

478

currentSize.value = { width: Math.round(width), height: Math.round(height) };

479

};

480

481

const onTextareaResize = (entries) => {

482

const { width, height } = entries[0].contentRect;

483

console.log(`Textarea resized to ${width}x${height}`);

484

// Auto-save or adjust UI based on size

485

};

486

487

const updateChart = (entries) => {

488

const { width, height } = entries[0].contentRect;

489

if (chartCanvas.value) {

490

chartCanvas.value.width = width;

491

chartCanvas.value.height = height;

492

// Redraw chart with new dimensions

493

redrawChart();

494

}

495

};

496

497

const updateGridColumns = (entries) => {

498

const { width } = entries[0].contentRect;

499

if (width < 600) {

500

gridColumns.value = '1fr';

501

} else if (width < 900) {

502

gridColumns.value = '1fr 1fr';

503

} else {

504

gridColumns.value = '1fr 1fr 1fr';

505

}

506

};

507

508

const resizeContainer = () => {

509

containerWidth.value = Math.random() * 400 + 200;

510

containerHeight.value = Math.random() * 300 + 150;

511

};

512

</script>

513

```

514

515

### Ripple

516

517

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

518

519

```typescript { .api }

520

/**

521

* Adds Material Design ripple effect

522

*/

523

const Ripple: Directive;

524

525

interface RippleBinding {

526

/** Ripple configuration */

527

value?: boolean | {

528

/** Ripple color */

529

color?: string;

530

/** Center the ripple effect */

531

center?: boolean;

532

/** Custom CSS class */

533

class?: string;

534

};

535

}

536

537

// Usage syntax variations

538

type RippleValue =

539

| boolean

540

| {

541

color?: string;

542

center?: boolean;

543

class?: string;

544

};

545

```

546

547

**Usage Examples:**

548

549

```vue

550

<template>

551

<!-- Basic ripple effect -->

552

<button v-ripple class="custom-button">

553

Click for ripple

554

</button>

555

556

<!-- Disabled ripple -->

557

<button v-ripple="false" class="no-ripple-button">

558

No ripple effect

559

</button>

560

561

<!-- Custom color ripple -->

562

<div

563

v-ripple="{ color: 'red' }"

564

class="ripple-card"

565

@click="handleClick"

566

>

567

Custom red ripple

568

</div>

569

570

<!-- Centered ripple -->

571

<v-icon

572

v-ripple="{ center: true }"

573

size="large"

574

@click="toggleFavorite"

575

>

576

{{ isFavorite ? 'mdi-heart' : 'mdi-heart-outline' }}

577

</v-icon>

578

579

<!-- Conditional ripple -->

580

<button

581

v-ripple="!disabled"

582

:disabled="disabled"

583

class="conditional-ripple"

584

>

585

{{ disabled ? 'Disabled' : 'Enabled' }}

586

</button>

587

588

<!-- Custom ripple class -->

589

<div

590

v-ripple="{ class: 'custom-ripple-class', color: 'purple' }"

591

class="fancy-card"

592

>

593

Custom styled ripple

594

</div>

595

</template>

596

597

<script setup>

598

const isFavorite = ref(false);

599

const disabled = ref(false);

600

601

const handleClick = () => {

602

console.log('Card clicked with ripple');

603

};

604

605

const toggleFavorite = () => {

606

isFavorite.value = !isFavorite.value;

607

};

608

</script>

609

610

<style>

611

.custom-button {

612

padding: 12px 24px;

613

background: #1976d2;

614

color: white;

615

border: none;

616

border-radius: 4px;

617

cursor: pointer;

618

position: relative;

619

overflow: hidden;

620

}

621

622

.ripple-card {

623

padding: 20px;

624

background: #f5f5f5;

625

border-radius: 8px;

626

cursor: pointer;

627

position: relative;

628

overflow: hidden;

629

}

630

631

.custom-ripple-class {

632

animation-duration: 0.8s !important;

633

opacity: 0.3 !important;

634

}

635

</style>

636

```

637

638

### Scroll

639

640

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

641

642

```typescript { .api }

643

/**

644

* Handles scroll events with options

645

*/

646

const Scroll: Directive;

647

648

interface ScrollBinding {

649

/** Scroll handler function */

650

handler?: (event: Event) => void;

651

/** Scroll configuration options */

652

options?: {

653

/** Throttle scroll events (ms) */

654

throttle?: number;

655

/** Target element to watch for scroll */

656

target?: string | Element | Window;

657

/** Passive event listener */

658

passive?: boolean;

659

};

660

}

661

662

// Usage syntax variations

663

type ScrollValue =

664

| ((event: Event) => void)

665

| {

666

handler: (event: Event) => void;

667

options?: {

668

throttle?: number;

669

target?: string | Element | Window;

670

passive?: boolean;

671

};

672

};

673

```

674

675

**Usage Examples:**

676

677

```vue

678

<template>

679

<!-- Basic scroll handling -->

680

<div

681

v-scroll="onScroll"

682

class="scrollable-content"

683

style="height: 400px; overflow-y: auto;"

684

>

685

<div v-for="n in 100" :key="n" class="scroll-item">

686

Item {{ n }}

687

</div>

688

</div>

689

690

<!-- Throttled scroll -->

691

<div

692

v-scroll="{

693

handler: onThrottledScroll,

694

options: { throttle: 100 }

695

}"

696

class="performance-scroll"

697

style="height: 300px; overflow-y: auto;"

698

>

699

<div style="height: 2000px; background: linear-gradient(to bottom, #f0f0f0, #d0d0d0);">

700

Large scrollable content

701

</div>

702

</div>

703

704

<!-- Window scroll (global) -->

705

<div

706

v-scroll="{

707

handler: onWindowScroll,

708

options: {

709

target: 'window',

710

throttle: 50,

711

passive: true

712

}

713

}"

714

>

715

<!-- This element watches window scroll -->

716

<div class="scroll-indicator" :style="{ width: scrollProgress + '%' }"></div>

717

</div>

718

719

<!-- Scroll to top button -->

720

<div class="page-content" style="height: 200vh; padding: 20px;">

721

<div

722

v-scroll="{

723

handler: updateScrollTop,

724

options: { target: 'window', throttle: 100 }

725

}"

726

>

727

Long page content...

728

</div>

729

730

<v-fab

731

v-show="showScrollTop"

732

location="bottom right"

733

icon="mdi-chevron-up"

734

@click="scrollToTop"

735

/>

736

</div>

737

738

<!-- Infinite scroll implementation -->

739

<div

740

v-scroll="{

741

handler: checkInfiniteScroll,

742

options: { throttle: 200 }

743

}"

744

class="infinite-list"

745

style="height: 400px; overflow-y: auto;"

746

>

747

<div v-for="item in infiniteItems" :key="item.id" class="list-item">

748

{{ item.name }}

749

</div>

750

<div v-if="loading" class="loading">Loading more...</div>

751

</div>

752

</template>

753

754

<script setup>

755

const scrollProgress = ref(0);

756

const showScrollTop = ref(false);

757

const infiniteItems = ref([]);

758

const loading = ref(false);

759

760

const onScroll = (event) => {

761

const target = event.target;

762

console.log('Scroll position:', target.scrollTop);

763

};

764

765

const onThrottledScroll = (event) => {

766

const target = event.target;

767

const scrollPercent = (target.scrollTop / (target.scrollHeight - target.clientHeight)) * 100;

768

console.log('Scroll percent:', scrollPercent.toFixed(1));

769

};

770

771

const onWindowScroll = () => {

772

const scrolled = window.scrollY;

773

const maxScroll = document.documentElement.scrollHeight - window.innerHeight;

774

scrollProgress.value = (scrolled / maxScroll) * 100;

775

};

776

777

const updateScrollTop = () => {

778

showScrollTop.value = window.scrollY > 300;

779

};

780

781

const scrollToTop = () => {

782

window.scrollTo({

783

top: 0,

784

behavior: 'smooth'

785

});

786

};

787

788

const checkInfiniteScroll = (event) => {

789

const target = event.target;

790

const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight;

791

792

if (scrollBottom < 100 && !loading.value) {

793

loadMoreItems();

794

}

795

};

796

797

const loadMoreItems = async () => {

798

loading.value = true;

799

// Simulate API call

800

await new Promise(resolve => setTimeout(resolve, 1000));

801

802

const newItems = Array.from({ length: 20 }, (_, i) => ({

803

id: infiniteItems.value.length + i + 1,

804

name: `Item ${infiniteItems.value.length + i + 1}`

805

}));

806

807

infiniteItems.value.push(...newItems);

808

loading.value = false;

809

};

810

</script>

811

812

<style>

813

.scroll-indicator {

814

height: 4px;

815

background: linear-gradient(to right, #2196f3, #21cbf3);

816

transition: width 0.1s ease;

817

position: fixed;

818

top: 0;

819

left: 0;

820

z-index: 9999;

821

}

822

823

.scroll-item,

824

.list-item {

825

padding: 16px;

826

border-bottom: 1px solid #eee;

827

}

828

829

.loading {

830

padding: 20px;

831

text-align: center;

832

color: #666;

833

}

834

</style>

835

```

836

837

### Touch

838

839

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

840

841

```typescript { .api }

842

/**

843

* Handles touch gestures and swipes

844

*/

845

const Touch: Directive;

846

847

interface TouchBinding {

848

/** Touch handler functions */

849

handlers?: {

850

/** Start touch handler */

851

start?: (wrapper: TouchWrapper) => void;

852

/** End touch handler */

853

end?: (wrapper: TouchWrapper) => void;

854

/** Move touch handler */

855

move?: (wrapper: TouchWrapper) => void;

856

/** Left swipe handler */

857

left?: (wrapper: TouchWrapper) => void;

858

/** Right swipe handler */

859

right?: (wrapper: TouchWrapper) => void;

860

/** Up swipe handler */

861

up?: (wrapper: TouchWrapper) => void;

862

/** Down swipe handler */

863

down?: (wrapper: TouchWrapper) => void;

864

};

865

/** Touch configuration options */

866

options?: {

867

/** Minimum distance for swipe */

868

threshold?: number;

869

/** Prevent default touch behavior */

870

passive?: boolean;

871

};

872

}

873

874

interface TouchWrapper {

875

/** Original touch event */

876

touchstartX: number;

877

touchstartY: number;

878

touchmoveX: number;

879

touchmoveY: number;

880

touchendX: number;

881

touchendY: number;

882

offsetX: number;

883

offsetY: number;

884

}

885

886

// Usage syntax variations

887

type TouchValue = {

888

[K in 'start' | 'end' | 'move' | 'left' | 'right' | 'up' | 'down']?: (wrapper: TouchWrapper) => void;

889

} & {

890

options?: {

891

threshold?: number;

892

passive?: boolean;

893

};

894

};

895

```

896

897

**Usage Examples:**

898

899

```vue

900

<template>

901

<!-- Basic swipe detection -->

902

<div

903

v-touch="{

904

left: () => nextSlide(),

905

right: () => previousSlide()

906

}"

907

class="swipe-container"

908

>

909

<div class="slide" :style="{ transform: `translateX(-${currentSlide * 100}%)` }">

910

<div v-for="(slide, index) in slides" :key="index" class="slide-item">

911

Slide {{ index + 1 }}

912

</div>

913

</div>

914

</div>

915

916

<!-- Full gesture handling -->

917

<div

918

v-touch="{

919

start: onTouchStart,

920

move: onTouchMove,

921

end: onTouchEnd,

922

left: onSwipeLeft,

923

right: onSwipeRight,

924

up: onSwipeUp,

925

down: onSwipeDown,

926

options: { threshold: 50 }

927

}"

928

class="gesture-area"

929

:style="{

930

transform: `translate(${position.x}px, ${position.y}px)`,

931

background: touchActive ? '#e3f2fd' : '#f5f5f5'

932

}"

933

>

934

<p>Touch and swipe me!</p>

935

<p>Position: {{ position.x }}, {{ position.y }}</p>

936

<p>Last gesture: {{ lastGesture }}</p>

937

</div>

938

939

<!-- Swipeable cards -->

940

<div class="card-stack">

941

<div

942

v-for="(card, index) in cards"

943

v-show="index >= currentCardIndex"

944

:key="card.id"

945

v-touch="{

946

left: () => swipeCard('left', index),

947

right: () => swipeCard('right', index),

948

options: { threshold: 100 }

949

}"

950

class="swipe-card"

951

:style="{

952

zIndex: cards.length - index,

953

transform: `scale(${1 - (index - currentCardIndex) * 0.05})`

954

}"

955

>

956

<h3>{{ card.title }}</h3>

957

<p>{{ card.content }}</p>

958

</div>

959

</div>

960

961

<!-- Mobile drawer -->

962

<div

963

v-touch="{

964

right: openDrawer,

965

options: { threshold: 30 }

966

}"

967

class="drawer-edge"

968

>

969

<!-- Edge area for drawer gesture -->

970

</div>

971

972

<div

973

v-show="drawerOpen"

974

v-touch="{

975

left: closeDrawer,

976

options: { threshold: 50 }

977

}"

978

class="drawer"

979

:class="{ open: drawerOpen }"

980

>

981

<div class="drawer-content">

982

<h3>Mobile Drawer</h3>

983

<p>Swipe left to close</p>

984

</div>

985

</div>

986

</template>

987

988

<script setup>

989

const currentSlide = ref(0);

990

const slides = ref(['Slide 1', 'Slide 2', 'Slide 3']);

991

const position = ref({ x: 0, y: 0 });

992

const touchActive = ref(false);

993

const lastGesture = ref('none');

994

const currentCardIndex = ref(0);

995

const drawerOpen = ref(false);

996

997

const cards = ref([

998

{ id: 1, title: 'Card 1', content: 'Swipe left or right' },

999

{ id: 2, title: 'Card 2', content: 'Another card to swipe' },

1000

{ id: 3, title: 'Card 3', content: 'Last card in stack' },

1001

]);

1002

1003

const nextSlide = () => {

1004

if (currentSlide.value < slides.value.length - 1) {

1005

currentSlide.value++;

1006

}

1007

};

1008

1009

const previousSlide = () => {

1010

if (currentSlide.value > 0) {

1011

currentSlide.value--;

1012

}

1013

};

1014

1015

const onTouchStart = (wrapper) => {

1016

touchActive.value = true;

1017

console.log('Touch started at:', wrapper.touchstartX, wrapper.touchstartY);

1018

};

1019

1020

const onTouchMove = (wrapper) => {

1021

position.value = {

1022

x: wrapper.touchmoveX - wrapper.touchstartX,

1023

y: wrapper.touchmoveY - wrapper.touchstartY

1024

};

1025

};

1026

1027

const onTouchEnd = (wrapper) => {

1028

touchActive.value = false;

1029

// Reset position with animation

1030

setTimeout(() => {

1031

position.value = { x: 0, y: 0 };

1032

}, 100);

1033

};

1034

1035

const onSwipeLeft = () => {

1036

lastGesture.value = 'swipe left';

1037

};

1038

1039

const onSwipeRight = () => {

1040

lastGesture.value = 'swipe right';

1041

};

1042

1043

const onSwipeUp = () => {

1044

lastGesture.value = 'swipe up';

1045

};

1046

1047

const onSwipeDown = () => {

1048

lastGesture.value = 'swipe down';

1049

};

1050

1051

const swipeCard = (direction, index) => {

1052

if (index === currentCardIndex.value) {

1053

console.log(`Card swiped ${direction}`);

1054

currentCardIndex.value++;

1055

}

1056

};

1057

1058

const openDrawer = () => {

1059

drawerOpen.value = true;

1060

};

1061

1062

const closeDrawer = () => {

1063

drawerOpen.value = false;

1064

};

1065

</script>

1066

1067

<style>

1068

.swipe-container {

1069

width: 300px;

1070

height: 200px;

1071

overflow: hidden;

1072

border-radius: 8px;

1073

background: #f0f0f0;

1074

}

1075

1076

.slide {

1077

display: flex;

1078

transition: transform 0.3s ease;

1079

}

1080

1081

.slide-item {

1082

min-width: 300px;

1083

height: 200px;

1084

display: flex;

1085

align-items: center;

1086

justify-content: center;

1087

font-size: 24px;

1088

background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);

1089

color: white;

1090

}

1091

1092

.gesture-area {

1093

width: 300px;

1094

height: 200px;

1095

border-radius: 12px;

1096

display: flex;

1097

flex-direction: column;

1098

align-items: center;

1099

justify-content: center;

1100

transition: transform 0.2s ease, background 0.2s ease;

1101

cursor: grab;

1102

user-select: none;

1103

}

1104

1105

.card-stack {

1106

position: relative;

1107

width: 280px;

1108

height: 180px;

1109

}

1110

1111

.swipe-card {

1112

position: absolute;

1113

top: 0;

1114

left: 0;

1115

width: 100%;

1116

height: 100%;

1117

background: white;

1118

border-radius: 12px;

1119

box-shadow: 0 4px 12px rgba(0,0,0,0.1);

1120

padding: 20px;

1121

transition: transform 0.2s ease;

1122

cursor: grab;

1123

}

1124

1125

.drawer-edge {

1126

position: fixed;

1127

left: 0;

1128

top: 0;

1129

width: 20px;

1130

height: 100vh;

1131

z-index: 1000;

1132

}

1133

1134

.drawer {

1135

position: fixed;

1136

left: -300px;

1137

top: 0;

1138

width: 300px;

1139

height: 100vh;

1140

background: white;

1141

box-shadow: 2px 0 10px rgba(0,0,0,0.1);

1142

transition: left 0.3s ease;

1143

z-index: 999;

1144

}

1145

1146

.drawer.open {

1147

left: 0;

1148

}

1149

1150

.drawer-content {

1151

padding: 20px;

1152

}

1153

</style>

1154

```

1155

1156

### Tooltip (Directive)

1157

1158

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

1159

1160

```typescript { .api }

1161

/**

1162

* Simple tooltip directive

1163

*/

1164

const Tooltip: Directive;

1165

1166

interface TooltipBinding {

1167

/** Tooltip text or configuration */

1168

value?: string | {

1169

/** Tooltip text */

1170

text: string;

1171

/** Tooltip position */

1172

location?: 'top' | 'bottom' | 'left' | 'right';

1173

/** Show delay in ms */

1174

delay?: number;

1175

/** Disabled state */

1176

disabled?: boolean;

1177

};

1178

}

1179

1180

// Usage syntax variations

1181

type TooltipValue =

1182

| string

1183

| {

1184

text: string;

1185

location?: 'top' | 'bottom' | 'left' | 'right';

1186

delay?: number;

1187

disabled?: boolean;

1188

};

1189

```

1190

1191

**Usage Examples:**

1192

1193

```vue

1194

<template>

1195

<!-- Simple text tooltip -->

1196

<button v-tooltip="'This is a simple tooltip'">

1197

Hover for tooltip

1198

</button>

1199

1200

<!-- Positioned tooltip -->

1201

<v-icon

1202

v-tooltip="{

1203

text: 'Save your work',

1204

location: 'bottom'

1205

}"

1206

@click="saveWork"

1207

>

1208

mdi-content-save

1209

</v-icon>

1210

1211

<!-- Tooltip with delay -->

1212

<v-chip

1213

v-tooltip="{

1214

text: 'Click to remove',

1215

location: 'top',

1216

delay: 1000

1217

}"

1218

closable

1219

@click:close="removeChip"

1220

>

1221

Delayed tooltip

1222

</v-chip>

1223

1224

<!-- Conditional tooltip -->

1225

<v-btn

1226

v-tooltip="{

1227

text: 'Please fill all required fields',

1228

disabled: formValid

1229

}"

1230

:disabled="!formValid"

1231

@click="submitForm"

1232

>

1233

Submit

1234

</v-btn>

1235

1236

<!-- Dynamic tooltip content -->

1237

<div

1238

v-for="item in items"

1239

:key="item.id"

1240

v-tooltip="getTooltipText(item)"

1241

class="item"

1242

>

1243

{{ item.name }}

1244

</div>

1245

</template>

1246

1247

<script setup>

1248

const formValid = ref(false);

1249

const items = ref([

1250

{ id: 1, name: 'Item 1', description: 'First item description' },

1251

{ id: 2, name: 'Item 2', description: 'Second item description' },

1252

]);

1253

1254

const saveWork = () => {

1255

console.log('Work saved');

1256

};

1257

1258

const removeChip = () => {

1259

console.log('Chip removed');

1260

};

1261

1262

const submitForm = () => {

1263

if (formValid.value) {

1264

console.log('Form submitted');

1265

}

1266

};

1267

1268

const getTooltipText = (item) => {

1269

return {

1270

text: item.description,

1271

location: 'right',

1272

delay: 500

1273

};

1274

};

1275

</script>

1276

1277

<style>

1278

.item {

1279

padding: 8px 16px;

1280

margin: 4px;

1281

background: #f5f5f5;

1282

border-radius: 4px;

1283

cursor: pointer;

1284

}

1285

1286

.item:hover {

1287

background: #e0e0e0;

1288

}

1289

</style>

1290

```

1291

1292

## Types

1293

1294

```typescript { .api }

1295

// Common directive binding interface

1296

interface DirectiveBinding<T = any> {

1297

value: T;

1298

oldValue: T;

1299

arg?: string;

1300

modifiers: Record<string, boolean>;

1301

instance: ComponentPublicInstance | null;

1302

dir: ObjectDirective<any, T>;

1303

}

1304

1305

// Event types

1306

interface TouchWrapper {

1307

touchstartX: number;

1308

touchstartY: number;

1309

touchmoveX: number;

1310

touchmoveY: number;

1311

touchendX: number;

1312

touchendY: number;

1313

offsetX: number;

1314

offsetY: number;

1315

}

1316

1317

// Observer types

1318

interface IntersectionObserverEntry {

1319

boundingClientRect: DOMRectReadOnly;

1320

intersectionRatio: number;

1321

intersectionRect: DOMRectReadOnly;

1322

isIntersecting: boolean;

1323

rootBounds: DOMRectReadOnly | null;

1324

target: Element;

1325

time: number;

1326

}

1327

1328

interface ResizeObserverEntry {

1329

borderBoxSize: ResizeObserverSize[];

1330

contentBoxSize: ResizeObserverSize[];

1331

contentRect: DOMRectReadOnly;

1332

target: Element;

1333

}

1334

1335

interface MutationRecord {

1336

type: 'childList' | 'attributes' | 'characterData';

1337

target: Node;

1338

addedNodes: NodeList;

1339

removedNodes: NodeList;

1340

previousSibling: Node | null;

1341

nextSibling: Node | null;

1342

attributeName: string | null;

1343

attributeNamespace: string | null;

1344

oldValue: string | null;

1345

}

1346

1347

// Location types for tooltips and positioning

1348

type DirectiveLocation = 'top' | 'bottom' | 'left' | 'right';

1349

```