or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

browser-apis.mdbrowser-events.mddevice-sensors.mdelement-tracking.mdindex.mdmouse-pointer.mdscroll-resize.mdtheme-preferences.mdutilities-advanced.mdwindow-document.md

utilities-advanced.mddocs/

0

# Utilities and Advanced

1

2

Components for utility functions, time management, virtual scrolling, pagination, and other advanced features.

3

4

## Capabilities

5

6

### UseTimestamp Component

7

8

Provides reactive current timestamp with customizable update intervals.

9

10

```typescript { .api }

11

/**

12

* Component that provides reactive timestamp

13

* @example

14

* <UseTimestamp v-slot="{ timestamp }">

15

* <div>Current time: {{ new Date(timestamp).toLocaleString() }}</div>

16

* </UseTimestamp>

17

*/

18

interface UseTimestampProps {

19

/** Offset to add to timestamp (ms) @default 0 */

20

offset?: number;

21

/** Update interval in milliseconds @default 1000 */

22

interval?: number | 'requestAnimationFrame';

23

/** Start immediately @default true */

24

immediate?: boolean;

25

/** Update on focus changes @default true */

26

controls?: boolean;

27

}

28

29

/** Slot data exposed by UseTimestamp component */

30

interface UseTimestampReturn {

31

/** Current timestamp in milliseconds */

32

timestamp: Ref<number>;

33

/** Pause timestamp updates */

34

pause: () => void;

35

/** Resume timestamp updates */

36

resume: () => void;

37

}

38

```

39

40

**Usage Examples:**

41

42

```vue

43

<template>

44

<!-- Basic timestamp -->

45

<UseTimestamp v-slot="{ timestamp }">

46

<div class="timestamp-display">

47

<h3>Live Clock</h3>

48

<div class="clock">

49

<div class="time">{{ formatTime(timestamp) }}</div>

50

<div class="date">{{ formatDate(timestamp) }}</div>

51

</div>

52

</div>

53

</UseTimestamp>

54

55

<!-- High frequency timestamp -->

56

<UseTimestamp :interval="16" v-slot="{ timestamp }">

57

<div class="high-freq-demo">

58

<h3>High Frequency Timer (60 FPS)</h3>

59

<div class="milliseconds">{{ timestamp % 1000 }}ms</div>

60

<div class="animation-box" :style="{ transform: getAnimationTransform(timestamp) }">

61

🚀

62

</div>

63

</div>

64

</UseTimestamp>

65

66

<!-- Controlled timestamp -->

67

<UseTimestamp

68

:immediate="false"

69

:interval="100"

70

v-slot="{ timestamp, pause, resume }"

71

>

72

<div class="controlled-timestamp">

73

<h3>Controlled Timer</h3>

74

<div class="timer-display">{{ Math.floor((timestamp % 60000) / 1000) }}s</div>

75

<div class="timer-controls">

76

<button @click="resume">Start</button>

77

<button @click="pause">Stop</button>

78

</div>

79

</div>

80

</UseTimestamp>

81

82

<!-- Timezone display -->

83

<UseTimestamp v-slot="{ timestamp }">

84

<div class="timezone-demo">

85

<h3>World Clock</h3>

86

<div class="timezone-grid">

87

<div v-for="tz in timezones" :key="tz.zone" class="timezone-item">

88

<div class="tz-name">{{ tz.name }}</div>

89

<div class="tz-time">{{ formatTimeInTimezone(timestamp, tz.zone) }}</div>

90

</div>

91

</div>

92

</div>

93

</UseTimestamp>

94

</template>

95

96

<script setup>

97

import { ref } from 'vue';

98

import { UseTimestamp } from '@vueuse/components';

99

100

const timezones = [

101

{ name: 'New York', zone: 'America/New_York' },

102

{ name: 'London', zone: 'Europe/London' },

103

{ name: 'Tokyo', zone: 'Asia/Tokyo' },

104

{ name: 'Sydney', zone: 'Australia/Sydney' }

105

];

106

107

function formatTime(timestamp) {

108

return new Date(timestamp).toLocaleTimeString();

109

}

110

111

function formatDate(timestamp) {

112

return new Date(timestamp).toLocaleDateString();

113

}

114

115

function formatTimeInTimezone(timestamp, timezone) {

116

return new Date(timestamp).toLocaleTimeString('en-US', { timeZone: timezone });

117

}

118

119

function getAnimationTransform(timestamp) {

120

const angle = (timestamp / 10) % 360;

121

return `rotate(${angle}deg) translateX(50px)`;

122

}

123

</script>

124

125

<style>

126

.timestamp-display, .high-freq-demo, .controlled-timestamp, .timezone-demo {

127

border: 1px solid #ddd;

128

border-radius: 8px;

129

padding: 20px;

130

margin: 15px 0;

131

text-align: center;

132

}

133

134

.clock {

135

font-family: 'Courier New', monospace;

136

background: #1a1a1a;

137

color: #00ff00;

138

padding: 20px;

139

border-radius: 8px;

140

margin: 15px 0;

141

}

142

143

.time {

144

font-size: 2em;

145

font-weight: bold;

146

}

147

148

.date {

149

font-size: 1.2em;

150

margin-top: 5px;

151

opacity: 0.8;

152

}

153

154

.milliseconds {

155

font-family: 'Courier New', monospace;

156

font-size: 1.5em;

157

font-weight: bold;

158

color: #2196f3;

159

margin: 15px 0;

160

}

161

162

.animation-box {

163

display: inline-block;

164

font-size: 2em;

165

margin: 20px;

166

}

167

168

.timer-display {

169

font-size: 3em;

170

font-weight: bold;

171

color: #4caf50;

172

font-family: 'Courier New', monospace;

173

margin: 15px 0;

174

}

175

176

.timer-controls {

177

display: flex;

178

justify-content: center;

179

gap: 15px;

180

}

181

182

.timer-controls button {

183

padding: 10px 20px;

184

font-size: 16px;

185

border: none;

186

border-radius: 6px;

187

cursor: pointer;

188

background: #2196f3;

189

color: white;

190

}

191

192

.timezone-grid {

193

display: grid;

194

grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));

195

gap: 15px;

196

margin-top: 20px;

197

}

198

199

.timezone-item {

200

padding: 15px;

201

border: 1px solid #ddd;

202

border-radius: 6px;

203

background: #f9f9f9;

204

}

205

206

.tz-name {

207

font-weight: bold;

208

margin-bottom: 8px;

209

color: #666;

210

}

211

212

.tz-time {

213

font-family: 'Courier New', monospace;

214

font-size: 1.1em;

215

color: #333;

216

}

217

</style>

218

```

219

220

### UseTimeAgo Component

221

222

Formats timestamps as relative time strings with automatic updates.

223

224

```typescript { .api }

225

/**

226

* Component that formats dates as relative time

227

* @example

228

* <UseTimeAgo :time="date" v-slot="{ timeAgo }">

229

* <div>{{ timeAgo }}</div>

230

* </UseTimeAgo>

231

*/

232

interface UseTimeAgoProps {

233

/** The date/time to format */

234

time: MaybeRefOrGetter<Date | number | string>;

235

/** Update interval in milliseconds @default 30000 */

236

updateInterval?: number;

237

/** Maximum unit to display @default 'month' */

238

max?: TimeAgoUnit;

239

/** Full date format when max exceeded @default 'YYYY-MM-DD' */

240

fullDateFormatter?: (date: Date) => string;

241

/** Custom messages for different time units */

242

messages?: TimeAgoMessages;

243

/** Show just now instead of 0 seconds @default false */

244

showSecond?: boolean;

245

/** Rounding method @default 'round' */

246

rounding?: 'floor' | 'ceil' | 'round';

247

}

248

249

/** Slot data exposed by UseTimeAgo component */

250

interface UseTimeAgoReturn {

251

/** Formatted relative time string */

252

timeAgo: Ref<string>;

253

}

254

255

type TimeAgoUnit = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';

256

257

interface TimeAgoMessages {

258

justNow?: string;

259

past?: string | ((n: string) => string);

260

future?: string | ((n: string) => string);

261

month?: string | string[];

262

year?: string | string[];

263

day?: string | string[];

264

week?: string | string[];

265

hour?: string | string[];

266

minute?: string | string[];

267

second?: string | string[];

268

}

269

```

270

271

**Usage Examples:**

272

273

```vue

274

<template>

275

<!-- Basic time ago -->

276

<div class="time-ago-demo">

277

<h3>Time Ago Examples</h3>

278

<div class="time-examples">

279

<div v-for="example in timeExamples" :key="example.label" class="time-example">

280

<div class="example-label">{{ example.label }}:</div>

281

<UseTimeAgo :time="example.time" v-slot="{ timeAgo }">

282

<div class="example-time">{{ timeAgo }}</div>

283

</UseTimeAgo>

284

</div>

285

</div>

286

</div>

287

288

<!-- Custom messages -->

289

<UseTimeAgo

290

:time="customTime"

291

:messages="customMessages"

292

v-slot="{ timeAgo }"

293

>

294

<div class="custom-time-ago">

295

<h3>Custom Messages</h3>

296

<p>{{ timeAgo }}</p>

297

</div>

298

</UseTimeAgo>

299

300

<!-- Activity feed -->

301

<div class="activity-feed">

302

<h3>Activity Feed</h3>

303

<div class="activities">

304

<div v-for="activity in activities" :key="activity.id" class="activity-item">

305

<div class="activity-icon">{{ activity.icon }}</div>

306

<div class="activity-content">

307

<div class="activity-text">{{ activity.text }}</div>

308

<UseTimeAgo :time="activity.timestamp" v-slot="{ timeAgo }">

309

<div class="activity-time">{{ timeAgo }}</div>

310

</UseTimeAgo>

311

</div>

312

</div>

313

</div>

314

</div>

315

316

<!-- Blog posts with different formats -->

317

<div class="blog-demo">

318

<h3>Blog Posts</h3>

319

<div class="posts">

320

<div v-for="post in blogPosts" :key="post.id" class="post-item">

321

<h4>{{ post.title }}</h4>

322

<p>{{ post.excerpt }}</p>

323

<div class="post-meta">

324

<span>By {{ post.author }}</span>

325

<UseTimeAgo

326

:time="post.publishedAt"

327

:max="'week'"

328

:full-date-formatter="formatFullDate"

329

v-slot="{ timeAgo }"

330

>

331

<span class="post-time">{{ timeAgo }}</span>

332

</UseTimeAgo>

333

</div>

334

</div>

335

</div>

336

</div>

337

</template>

338

339

<script setup>

340

import { ref, computed } from 'vue';

341

import { UseTimeAgo } from '@vueuse/components';

342

343

const now = Date.now();

344

345

const timeExamples = [

346

{ label: 'Just now', time: now - 5000 },

347

{ label: '2 minutes ago', time: now - 2 * 60 * 1000 },

348

{ label: '1 hour ago', time: now - 60 * 60 * 1000 },

349

{ label: 'Yesterday', time: now - 25 * 60 * 60 * 1000 },

350

{ label: 'Last week', time: now - 8 * 24 * 60 * 60 * 1000 },

351

{ label: 'Last month', time: now - 35 * 24 * 60 * 60 * 1000 }

352

];

353

354

const customTime = ref(now - 30 * 60 * 1000);

355

356

const customMessages = {

357

justNow: 'right now',

358

past: n => `${n} ago`,

359

future: n => `in ${n}`,

360

second: ['second', 'seconds'],

361

minute: ['minute', 'minutes'],

362

hour: ['hour', 'hours'],

363

day: ['day', 'days'],

364

week: ['week', 'weeks'],

365

month: ['month', 'months'],

366

year: ['year', 'years']

367

};

368

369

const activities = ref([

370

{

371

id: 1,

372

icon: '📝',

373

text: 'John created a new document',

374

timestamp: now - 5 * 60 * 1000

375

},

376

{

377

id: 2,

378

icon: '💬',

379

text: 'Sarah commented on your post',

380

timestamp: now - 15 * 60 * 1000

381

},

382

{

383

id: 3,

384

icon: '❤️',

385

text: 'Mike liked your photo',

386

timestamp: now - 2 * 60 * 60 * 1000

387

},

388

{

389

id: 4,

390

icon: '🔗',

391

text: 'Anna shared your article',

392

timestamp: now - 6 * 60 * 60 * 1000

393

},

394

{

395

id: 5,

396

icon: '🎉',

397

text: 'You reached 100 followers!',

398

timestamp: now - 2 * 24 * 60 * 60 * 1000

399

}

400

]);

401

402

const blogPosts = ref([

403

{

404

id: 1,

405

title: 'Getting Started with Vue 3',

406

excerpt: 'Learn the basics of Vue 3 composition API...',

407

author: 'Alex Chen',

408

publishedAt: now - 3 * 24 * 60 * 60 * 1000

409

},

410

{

411

id: 2,

412

title: 'Advanced TypeScript Tips',

413

excerpt: 'Discover advanced TypeScript patterns...',

414

author: 'Maria Garcia',

415

publishedAt: now - 10 * 24 * 60 * 60 * 1000

416

},

417

{

418

id: 3,

419

title: 'Building Scalable Applications',

420

excerpt: 'Best practices for large-scale development...',

421

author: 'David Kim',

422

publishedAt: now - 45 * 24 * 60 * 60 * 1000

423

}

424

]);

425

426

function formatFullDate(date) {

427

return date.toLocaleDateString('en-US', {

428

year: 'numeric',

429

month: 'long',

430

day: 'numeric'

431

});

432

}

433

</script>

434

435

<style>

436

.time-ago-demo, .custom-time-ago, .activity-feed, .blog-demo {

437

border: 1px solid #ddd;

438

border-radius: 8px;

439

padding: 20px;

440

margin: 15px 0;

441

}

442

443

.time-examples {

444

display: grid;

445

grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));

446

gap: 15px;

447

margin-top: 15px;

448

}

449

450

.time-example {

451

padding: 15px;

452

border: 1px solid #eee;

453

border-radius: 6px;

454

background: #f9f9f9;

455

}

456

457

.example-label {

458

font-weight: bold;

459

color: #666;

460

margin-bottom: 8px;

461

}

462

463

.example-time {

464

color: #2196f3;

465

font-size: 1.1em;

466

}

467

468

.custom-time-ago {

469

text-align: center;

470

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

471

color: white;

472

}

473

474

.activities {

475

display: flex;

476

flex-direction: column;

477

gap: 12px;

478

margin-top: 15px;

479

}

480

481

.activity-item {

482

display: flex;

483

align-items: flex-start;

484

gap: 12px;

485

padding: 15px;

486

border: 1px solid #eee;

487

border-radius: 8px;

488

background: #fafafa;

489

}

490

491

.activity-icon {

492

font-size: 1.5em;

493

flex-shrink: 0;

494

}

495

496

.activity-content {

497

flex: 1;

498

}

499

500

.activity-text {

501

font-weight: 500;

502

margin-bottom: 4px;

503

}

504

505

.activity-time {

506

font-size: 0.9em;

507

color: #666;

508

}

509

510

.posts {

511

display: flex;

512

flex-direction: column;

513

gap: 20px;

514

margin-top: 15px;

515

}

516

517

.post-item {

518

padding: 20px;

519

border: 1px solid #ddd;

520

border-radius: 8px;

521

background: white;

522

}

523

524

.post-item h4 {

525

margin: 0 0 10px 0;

526

color: #333;

527

}

528

529

.post-item p {

530

margin: 0 0 15px 0;

531

color: #666;

532

line-height: 1.5;

533

}

534

535

.post-meta {

536

display: flex;

537

justify-content: space-between;

538

align-items: center;

539

font-size: 0.9em;

540

color: #888;

541

border-top: 1px solid #eee;

542

padding-top: 15px;

543

}

544

545

.post-time {

546

font-weight: 500;

547

}

548

</style>

549

```

550

551

### UseVirtualList Component

552

553

Implements virtual scrolling for large datasets with optimal performance.

554

555

```typescript { .api }

556

/**

557

* Component that implements virtual scrolling for large lists

558

* @example

559

* <UseVirtualList :list="items" :options="{ itemHeight: 50 }" v-slot="{ list, containerProps, wrapperProps }">

560

* <div v-bind="containerProps">

561

* <div v-bind="wrapperProps">

562

* <div v-for="{ data, index } in list" :key="index">{{ data.name }}</div>

563

* </div>

564

* </div>

565

* </UseVirtualList>

566

*/

567

interface UseVirtualListProps<T = any> {

568

/** Array of items to virtualize */

569

list: T[];

570

/** Virtual list options */

571

options: UseVirtualListOptions;

572

/** Container height @default 300 */

573

height?: number;

574

}

575

576

/** Slot data exposed by UseVirtualList component */

577

interface UseVirtualListReturn<T = any> {

578

/** Visible list items with their data and indices */

579

list: ComputedRef<UseVirtualListItem<T>[]>;

580

/** Props to bind to the scrollable container */

581

containerProps: ComputedRef<UseVirtualListContainerProps>;

582

/** Props to bind to the wrapper element */

583

wrapperProps: ComputedRef<UseVirtualListWrapperProps>;

584

/** Scroll to specific index */

585

scrollTo: (index: number) => void;

586

}

587

588

interface UseVirtualListOptions {

589

/** Height of each item in pixels @default 50 */

590

itemHeight: number | ((index: number) => number);

591

/** Number of extra items to render outside viewport @default 5 */

592

overscan?: number;

593

}

594

595

interface UseVirtualListItem<T = any> {

596

data: T;

597

index: number;

598

}

599

600

interface UseVirtualListContainerProps {

601

ref: Ref<HTMLElement | undefined>;

602

onScroll: (e: Event) => void;

603

style: CSSProperties;

604

}

605

606

interface UseVirtualListWrapperProps {

607

style: ComputedRef<CSSProperties>;

608

}

609

```

610

611

**Usage Examples:**

612

613

```vue

614

<template>

615

<!-- Basic virtual list -->

616

<div class="virtual-list-demo">

617

<h3>Virtual List - {{ largeList.length.toLocaleString() }} Items</h3>

618

<UseVirtualList

619

:list="largeList"

620

:options="{ itemHeight: 50, overscan: 10 }"

621

:height="400"

622

v-slot="{ list, containerProps, wrapperProps, scrollTo }"

623

>

624

<div class="virtual-controls">

625

<button @click="scrollTo(0)">Go to Top</button>

626

<button @click="scrollTo(Math.floor(largeList.length / 2))">Go to Middle</button>

627

<button @click="scrollTo(largeList.length - 1)">Go to Bottom</button>

628

<input

629

type="number"

630

:max="largeList.length - 1"

631

placeholder="Jump to index"

632

@keyup.enter="scrollTo(parseInt($event.target.value))"

633

/>

634

</div>

635

<div v-bind="containerProps" class="virtual-container">

636

<div v-bind="wrapperProps" class="virtual-wrapper">

637

<div

638

v-for="{ data, index } in list"

639

:key="index"

640

class="virtual-item"

641

>

642

<div class="item-index">#{{ index }}</div>

643

<div class="item-name">{{ data.name }}</div>

644

<div class="item-value">{{ data.value }}</div>

645

</div>

646

</div>

647

</div>

648

</UseVirtualList>

649

</div>

650

651

<!-- Variable height virtual list -->

652

<div class="variable-height-demo">

653

<h3>Variable Height Virtual List</h3>

654

<UseVirtualList

655

:list="variableHeightList"

656

:options="{ itemHeight: getItemHeight, overscan: 3 }"

657

:height="350"

658

v-slot="{ list, containerProps, wrapperProps }"

659

>

660

<div v-bind="containerProps" class="variable-container">

661

<div v-bind="wrapperProps" class="variable-wrapper">

662

<div

663

v-for="{ data, index } in list"

664

:key="index"

665

class="variable-item"

666

:class="data.type"

667

>

668

<div class="item-header">

669

<span class="item-type">{{ data.type }}</span>

670

<span class="item-id">#{{ index }}</span>

671

</div>

672

<div class="item-content">{{ data.content }}</div>

673

<div v-if="data.details" class="item-details">

674

{{ data.details }}

675

</div>

676

</div>

677

</div>

678

</div>

679

</UseVirtualList>

680

</div>

681

682

<!-- Chat message virtual list -->

683

<div class="chat-demo">

684

<h3>Virtual Chat Messages</h3>

685

<UseVirtualList

686

:list="chatMessages"

687

:options="{ itemHeight: getChatMessageHeight, overscan: 5 }"

688

:height="300"

689

v-slot="{ list, containerProps, wrapperProps, scrollTo }"

690

>

691

<div class="chat-controls">

692

<button @click="addMessage">Add Message</button>

693

<button @click="scrollTo(chatMessages.length - 1)">Scroll to Latest</button>

694

</div>

695

<div v-bind="containerProps" class="chat-container">

696

<div v-bind="wrapperProps" class="chat-wrapper">

697

<div

698

v-for="{ data, index } in list"

699

:key="index"

700

class="chat-message"

701

:class="{ own: data.isOwn }"

702

>

703

<div class="message-info">

704

<span class="message-author">{{ data.author }}</span>

705

<span class="message-time">{{ formatChatTime(data.timestamp) }}</span>

706

</div>

707

<div class="message-text">{{ data.text }}</div>

708

</div>

709

</div>

710

</div>

711

</UseVirtualList>

712

</div>

713

714

<!-- Performance comparison -->

715

<div class="performance-demo">

716

<h3>Performance Comparison</h3>

717

<div class="performance-controls">

718

<button @click="showVirtual = !showVirtual">

719

{{ showVirtual ? 'Show Regular List' : 'Show Virtual List' }}

720

</button>

721

<span class="performance-info">

722

Rendering {{ showVirtual ? 'virtualized' : 'all' }} items

723

</span>

724

</div>

725

726

<div v-if="!showVirtual" class="regular-list">

727

<div

728

v-for="(item, index) in performanceList.slice(0, 1000)"

729

:key="index"

730

class="performance-item"

731

>

732

{{ item.name }} - {{ item.description }}

733

</div>

734

</div>

735

736

<UseVirtualList

737

v-else

738

:list="performanceList"

739

:options="{ itemHeight: 60 }"

740

:height="400"

741

v-slot="{ list, containerProps, wrapperProps }"

742

>

743

<div v-bind="containerProps" class="performance-container">

744

<div v-bind="wrapperProps" class="performance-wrapper">

745

<div

746

v-for="{ data, index } in list"

747

:key="index"

748

class="performance-item"

749

>

750

{{ data.name }} - {{ data.description }}

751

</div>

752

</div>

753

</div>

754

</UseVirtualList>

755

</div>

756

</template>

757

758

<script setup>

759

import { ref, computed } from 'vue';

760

import { UseVirtualList } from '@vueuse/components';

761

762

// Generate large list

763

const largeList = ref([]);

764

for (let i = 0; i < 100000; i++) {

765

largeList.value.push({

766

name: `Item ${i + 1}`,

767

value: Math.floor(Math.random() * 1000)

768

});

769

}

770

771

// Variable height list

772

const variableHeightList = ref([]);

773

const itemTypes = ['header', 'content', 'footer'];

774

for (let i = 0; i < 1000; i++) {

775

const type = itemTypes[Math.floor(Math.random() * itemTypes.length)];

776

variableHeightList.value.push({

777

type,

778

content: `This is ${type} item ${i + 1}`,

779

details: type === 'content' ? `Additional details for item ${i + 1}` : null

780

});

781

}

782

783

// Chat messages

784

const chatMessages = ref([]);

785

const authors = ['Alice', 'Bob', 'Charlie', 'Diana'];

786

for (let i = 0; i < 500; i++) {

787

const author = authors[Math.floor(Math.random() * authors.length)];

788

chatMessages.value.push({

789

author,

790

text: `Message ${i + 1}: ${generateRandomMessage()}`,

791

timestamp: Date.now() - (500 - i) * 60000,

792

isOwn: author === 'Alice'

793

});

794

}

795

796

// Performance list

797

const performanceList = ref([]);

798

for (let i = 0; i < 50000; i++) {

799

performanceList.value.push({

800

name: `Performance Item ${i + 1}`,

801

description: `This is a description for performance testing item number ${i + 1}`

802

});

803

}

804

805

const showVirtual = ref(true);

806

807

function getItemHeight(index) {

808

const item = variableHeightList.value[index];

809

if (!item) return 50;

810

811

switch (item.type) {

812

case 'header': return 80;

813

case 'content': return item.details ? 120 : 80;

814

case 'footer': return 60;

815

default: return 50;

816

}

817

}

818

819

function getChatMessageHeight(index) {

820

const message = chatMessages.value[index];

821

if (!message) return 60;

822

823

// Estimate height based on message length

824

const baseHeight = 60;

825

const extraHeight = Math.ceil(message.text.length / 50) * 20;

826

return Math.min(baseHeight + extraHeight, 150);

827

}

828

829

function addMessage() {

830

const newMessage = {

831

author: 'Alice',

832

text: `New message: ${generateRandomMessage()}`,

833

timestamp: Date.now(),

834

isOwn: true

835

};

836

chatMessages.value.push(newMessage);

837

}

838

839

function generateRandomMessage() {

840

const messages = [

841

"Hello everyone!",

842

"How are you doing today?",

843

"This is a test message.",

844

"Virtual scrolling is amazing!",

845

"Performance is great with large lists.",

846

"Check out this new feature.",

847

"What do you think about this implementation?",

848

"Looking forward to your feedback!"

849

];

850

return messages[Math.floor(Math.random() * messages.length)];

851

}

852

853

function formatChatTime(timestamp) {

854

return new Date(timestamp).toLocaleTimeString();

855

}

856

</script>

857

858

<style>

859

.virtual-list-demo, .variable-height-demo, .chat-demo, .performance-demo {

860

border: 1px solid #ddd;

861

border-radius: 8px;

862

padding: 20px;

863

margin: 15px 0;

864

}

865

866

.virtual-controls, .chat-controls, .performance-controls {

867

display: flex;

868

gap: 10px;

869

align-items: center;

870

margin-bottom: 15px;

871

flex-wrap: wrap;

872

}

873

874

.virtual-controls button, .chat-controls button, .performance-controls button {

875

padding: 6px 12px;

876

border: 1px solid #ddd;

877

border-radius: 4px;

878

cursor: pointer;

879

background: #f5f5f5;

880

}

881

882

.virtual-controls input {

883

padding: 6px 8px;

884

border: 1px solid #ddd;

885

border-radius: 4px;

886

width: 120px;

887

}

888

889

.virtual-container, .variable-container, .chat-container, .performance-container {

890

border: 2px solid #eee;

891

border-radius: 6px;

892

background: #fafafa;

893

}

894

895

.virtual-item, .variable-item, .performance-item {

896

display: flex;

897

align-items: center;

898

gap: 15px;

899

padding: 15px;

900

border-bottom: 1px solid #eee;

901

background: white;

902

}

903

904

.virtual-item:hover, .variable-item:hover, .performance-item:hover {

905

background: #f0f8ff;

906

}

907

908

.item-index {

909

font-weight: bold;

910

color: #666;

911

min-width: 60px;

912

}

913

914

.item-name {

915

font-weight: bold;

916

flex: 1;

917

}

918

919

.item-value {

920

color: #2196f3;

921

font-family: monospace;

922

}

923

924

.variable-item.header {

925

background: #e3f2fd;

926

font-weight: bold;

927

}

928

929

.variable-item.footer {

930

background: #f3e5f5;

931

font-style: italic;

932

}

933

934

.item-header {

935

display: flex;

936

justify-content: space-between;

937

width: 100%;

938

align-items: center;

939

}

940

941

.item-type {

942

text-transform: uppercase;

943

font-size: 0.8em;

944

font-weight: bold;

945

padding: 2px 8px;

946

border-radius: 4px;

947

background: #ddd;

948

}

949

950

.item-content {

951

margin-top: 8px;

952

}

953

954

.item-details {

955

margin-top: 5px;

956

font-size: 0.9em;

957

color: #666;

958

}

959

960

.chat-message {

961

padding: 12px 15px;

962

border-bottom: 1px solid #eee;

963

background: white;

964

}

965

966

.chat-message.own {

967

background: #e8f5e8;

968

}

969

970

.message-info {

971

display: flex;

972

justify-content: space-between;

973

margin-bottom: 5px;

974

font-size: 0.9em;

975

}

976

977

.message-author {

978

font-weight: bold;

979

color: #333;

980

}

981

982

.message-time {

983

color: #666;

984

}

985

986

.message-text {

987

line-height: 1.4;

988

}

989

990

.regular-list {

991

height: 400px;

992

border: 2px solid #eee;

993

border-radius: 6px;

994

overflow-y: auto;

995

background: #fafafa;

996

}

997

998

.performance-info {

999

font-size: 0.9em;

1000

color: #666;

1001

font-style: italic;

1002

}

1003

</style>

1004

```

1005

1006

### UseOffsetPagination Component

1007

1008

Manages offset-based pagination with reactive state and event emissions.

1009

1010

```typescript { .api }

1011

/**

1012

* Component that manages offset-based pagination

1013

* @example

1014

* <UseOffsetPagination

1015

* v-slot="{ currentPage, pageSize, pageCount, isFirstPage, isLastPage, prev, next }"

1016

* :total="1000"

1017

* :page-size="20"

1018

* >

1019

* <div>Page {{ currentPage }} of {{ pageCount }}</div>

1020

* </UseOffsetPagination>

1021

*/

1022

interface UseOffsetPaginationProps {

1023

/** Total number of items */

1024

total: MaybeRefOrGetter<number>;

1025

/** Number of items per page @default 10 */

1026

pageSize?: MaybeRefOrGetter<number>;

1027

/** Current page number @default 1 */

1028

page?: MaybeRefOrGetter<number>;

1029

/** Callback when page changes */

1030

onPageChange?: (returnValue: UseOffsetPaginationReturn) => unknown;

1031

/** Callback when page size changes */

1032

onPageSizeChange?: (returnValue: UseOffsetPaginationReturn) => unknown;

1033

/** Callback when page count changes */

1034

onPageCountChange?: (returnValue: UseOffsetPaginationReturn) => unknown;

1035

}

1036

1037

/** Slot data exposed by UseOffsetPagination component */

1038

interface UseOffsetPaginationReturn {

1039

/** Current page number */

1040

currentPage: Ref<number>;

1041

/** Current page size */

1042

pageSize: Ref<number>;

1043

/** Total number of pages */

1044

pageCount: ComputedRef<number>;

1045

/** Whether on first page */

1046

isFirstPage: ComputedRef<boolean>;

1047

/** Whether on last page */

1048

isLastPage: ComputedRef<boolean>;

1049

/** Go to previous page */

1050

prev: () => void;

1051

/** Go to next page */

1052

next: () => void;

1053

/** Go to specific page */

1054

goToPage: (page: number) => void;

1055

}

1056

1057

/** Component events */

1058

interface UseOffsetPaginationEmits {

1059

/** Emitted when page changes */

1060

'page-change': (returnValue: UseOffsetPaginationReturn) => void;

1061

/** Emitted when page size changes */

1062

'page-size-change': (returnValue: UseOffsetPaginationReturn) => void;

1063

/** Emitted when page count changes */

1064

'page-count-change': (returnValue: UseOffsetPaginationReturn) => void;

1065

}

1066

```

1067

1068

**Usage Examples:**

1069

1070

```vue

1071

<template>

1072

<!-- Basic pagination -->

1073

<div class="pagination-demo">

1074

<h3>Basic Pagination</h3>

1075

<UseOffsetPagination

1076

:total="500"

1077

:page-size="20"

1078

v-slot="{ currentPage, pageSize, pageCount, isFirstPage, isLastPage, prev, next, goToPage }"

1079

@page-change="handlePageChange"

1080

>

1081

<div class="data-section">

1082

<div class="data-info">

1083

Showing {{ (currentPage - 1) * pageSize + 1 }}-{{ Math.min(currentPage * pageSize, 500) }} of 500 items

1084

</div>

1085

<div class="data-grid">

1086

<div v-for="item in getCurrentPageData(currentPage, pageSize)" :key="item.id" class="data-item">

1087

{{ item.name }} - {{ item.description }}

1088

</div>

1089

</div>

1090

</div>

1091

1092

<div class="pagination-controls">

1093

<button @click="prev" :disabled="isFirstPage" class="nav-btn">

1094

← Previous

1095

</button>

1096

1097

<div class="page-numbers">

1098

<button

1099

v-for="page in getVisiblePages(currentPage, pageCount)"

1100

:key="page"

1101

@click="goToPage(page)"

1102

:class="{ active: page === currentPage, ellipsis: page === '...' }"

1103

:disabled="page === '...'"

1104

class="page-btn"

1105

>

1106

{{ page }}

1107

</button>

1108

</div>

1109

1110

<button @click="next" :disabled="isLastPage" class="nav-btn">

1111

Next →

1112

</button>

1113

</div>

1114

1115

<div class="pagination-info">

1116

Page {{ currentPage }} of {{ pageCount }}

1117

</div>

1118

</UseOffsetPagination>

1119

</div>

1120

1121

<!-- Advanced pagination with size selector -->

1122

<div class="advanced-pagination">

1123

<h3>Advanced Pagination</h3>

1124

<UseOffsetPagination

1125

:total="advancedTotal"

1126

:page-size="advancedPageSize"

1127

:page="advancedPage"

1128

v-slot="pagination"

1129

@page-change="handleAdvancedPageChange"

1130

@page-size-change="handlePageSizeChange"

1131

>

1132

<div class="advanced-controls">

1133

<label class="page-size-selector">

1134

Items per page:

1135

<select v-model="advancedPageSize">

1136

<option :value="10">10</option>

1137

<option :value="25">25</option>

1138

<option :value="50">50</option>

1139

<option :value="100">100</option>

1140

</select>

1141

</label>

1142

1143

<div class="jump-to-page">

1144

<label>

1145

Jump to page:

1146

<input

1147

type="number"

1148

:min="1"

1149

:max="pagination.pageCount"

1150

@keyup.enter="pagination.goToPage(parseInt($event.target.value))"

1151

class="page-input"

1152

/>

1153

</label>

1154

</div>

1155

</div>

1156

1157

<div class="advanced-data">

1158

<div class="results-summary">

1159

Results {{ (pagination.currentPage - 1) * pagination.pageSize + 1 }}-{{

1160

Math.min(pagination.currentPage * pagination.pageSize, advancedTotal)

1161

}} of {{ advancedTotal.toLocaleString() }}

1162

</div>

1163

1164

<table class="data-table">

1165

<thead>

1166

<tr>

1167

<th>ID</th>

1168

<th>Name</th>

1169

<th>Category</th>

1170

<th>Status</th>

1171

<th>Created</th>

1172

</tr>

1173

</thead>

1174

<tbody>

1175

<tr v-for="item in getAdvancedPageData(pagination.currentPage, pagination.pageSize)" :key="item.id">

1176

<td>{{ item.id }}</td>

1177

<td>{{ item.name }}</td>

1178

<td>{{ item.category }}</td>

1179

<td>

1180

<span :class="`status-${item.status}`" class="status-badge">

1181

{{ item.status }}

1182

</span>

1183

</td>

1184

<td>{{ formatDate(item.created) }}</td>

1185

</tr>

1186

</tbody>

1187

</table>

1188

1189

<div class="table-pagination">

1190

<button @click="pagination.prev" :disabled="pagination.isFirstPage">

1191

← Prev

1192

</button>

1193

<span class="page-info">

1194

{{ pagination.currentPage }} / {{ pagination.pageCount }}

1195

</span>

1196

<button @click="pagination.next" :disabled="pagination.isLastPage">

1197

Next →

1198

</button>

1199

</div>

1200

</div>

1201

</UseOffsetPagination>

1202

</div>

1203

1204

<!-- Search with pagination -->

1205

<div class="search-pagination">

1206

<h3>Search with Pagination</h3>

1207

<div class="search-controls">

1208

<input

1209

v-model="searchQuery"

1210

placeholder="Search items..."

1211

class="search-input"

1212

@input="handleSearch"

1213

/>

1214

<button @click="clearSearch" class="clear-btn">Clear</button>

1215

</div>

1216

1217

<UseOffsetPagination

1218

:total="filteredTotal"

1219

:page-size="10"

1220

:page="searchPage"

1221

v-slot="pagination"

1222

@page-change="handleSearchPageChange"

1223

>

1224

<div v-if="searchQuery" class="search-results-info">

1225

Found {{ filteredTotal }} results for "{{ searchQuery }}"

1226

</div>

1227

1228

<div class="search-results">

1229

<div v-if="filteredTotal === 0" class="no-results">

1230

No results found for "{{ searchQuery }}"

1231

</div>

1232

<div v-else class="results-list">

1233

<div

1234

v-for="item in getFilteredPageData(pagination.currentPage, pagination.pageSize)"

1235

:key="item.id"

1236

class="result-item"

1237

>

1238

<div class="result-title">{{ highlightSearchTerm(item.name, searchQuery) }}</div>

1239

<div class="result-description">{{ highlightSearchTerm(item.description, searchQuery) }}</div>

1240

</div>

1241

</div>

1242

</div>

1243

1244

<div v-if="filteredTotal > 0" class="search-pagination-controls">

1245

<button @click="pagination.prev" :disabled="pagination.isFirstPage">

1246

← Previous

1247

</button>

1248

<span>{{ pagination.currentPage }} of {{ pagination.pageCount }}</span>

1249

<button @click="pagination.next" :disabled="pagination.isLastPage">

1250

Next →

1251

</button>

1252

</div>

1253

</UseOffsetPagination>

1254

</div>

1255

</template>

1256

1257

<script setup>

1258

import { ref, computed } from 'vue';

1259

import { UseOffsetPagination } from '@vueuse/components';

1260

1261

// Basic pagination data

1262

const basicData = ref([]);

1263

for (let i = 1; i <= 500; i++) {

1264

basicData.value.push({

1265

id: i,

1266

name: `Item ${i}`,

1267

description: `Description for item ${i}`

1268

});

1269

}

1270

1271

// Advanced pagination data

1272

const advancedPageSize = ref(25);

1273

const advancedPage = ref(1);

1274

const advancedTotal = 2547;

1275

1276

const advancedData = ref([]);

1277

const categories = ['Electronics', 'Clothing', 'Books', 'Home & Garden', 'Sports'];

1278

const statuses = ['active', 'inactive', 'pending'];

1279

1280

for (let i = 1; i <= advancedTotal; i++) {

1281

advancedData.value.push({

1282

id: i,

1283

name: `Product ${i}`,

1284

category: categories[Math.floor(Math.random() * categories.length)],

1285

status: statuses[Math.floor(Math.random() * statuses.length)],

1286

created: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000)

1287

});

1288

}

1289

1290

// Search pagination

1291

const searchQuery = ref('');

1292

const searchPage = ref(1);

1293

const searchData = ref([]);

1294

1295

// Initialize search data

1296

for (let i = 1; i <= 1000; i++) {

1297

searchData.value.push({

1298

id: i,

1299

name: `Item ${i} - ${generateRandomWords()}`,

1300

description: `This is a detailed description for item ${i}. ${generateRandomDescription()}`

1301

});

1302

}

1303

1304

const filteredData = computed(() => {

1305

if (!searchQuery.value) return searchData.value;

1306

1307

const query = searchQuery.value.toLowerCase();

1308

return searchData.value.filter(item =>

1309

item.name.toLowerCase().includes(query) ||

1310

item.description.toLowerCase().includes(query)

1311

);

1312

});

1313

1314

const filteredTotal = computed(() => filteredData.value.length);

1315

1316

function getCurrentPageData(page, pageSize) {

1317

const start = (page - 1) * pageSize;

1318

const end = start + pageSize;

1319

return basicData.value.slice(start, end);

1320

}

1321

1322

function getAdvancedPageData(page, pageSize) {

1323

const start = (page - 1) * pageSize;

1324

const end = start + pageSize;

1325

return advancedData.value.slice(start, end);

1326

}

1327

1328

function getFilteredPageData(page, pageSize) {

1329

const start = (page - 1) * pageSize;

1330

const end = start + pageSize;

1331

return filteredData.value.slice(start, end);

1332

}

1333

1334

function getVisiblePages(currentPage, totalPages) {

1335

const pages = [];

1336

const maxVisible = 7;

1337

1338

if (totalPages <= maxVisible) {

1339

for (let i = 1; i <= totalPages; i++) {

1340

pages.push(i);

1341

}

1342

} else {

1343

pages.push(1);

1344

1345

if (currentPage > 4) {

1346

pages.push('...');

1347

}

1348

1349

const start = Math.max(2, currentPage - 1);

1350

const end = Math.min(totalPages - 1, currentPage + 1);

1351

1352

for (let i = start; i <= end; i++) {

1353

pages.push(i);

1354

}

1355

1356

if (currentPage < totalPages - 3) {

1357

pages.push('...');

1358

}

1359

1360

if (totalPages > 1) {

1361

pages.push(totalPages);

1362

}

1363

}

1364

1365

return pages;

1366

}

1367

1368

function handlePageChange(pagination) {

1369

console.log('Page changed:', pagination.currentPage);

1370

}

1371

1372

function handleAdvancedPageChange(pagination) {

1373

advancedPage.value = pagination.currentPage;

1374

}

1375

1376

function handlePageSizeChange(pagination) {

1377

advancedPageSize.value = pagination.pageSize;

1378

advancedPage.value = 1; // Reset to first page

1379

}

1380

1381

function handleSearch() {

1382

searchPage.value = 1; // Reset to first page on search

1383

}

1384

1385

function handleSearchPageChange(pagination) {

1386

searchPage.value = pagination.currentPage;

1387

}

1388

1389

function clearSearch() {

1390

searchQuery.value = '';

1391

searchPage.value = 1;

1392

}

1393

1394

function highlightSearchTerm(text, query) {

1395

if (!query) return text;

1396

1397

const regex = new RegExp(`(${query})`, 'gi');

1398

return text.replace(regex, '<mark>$1</mark>');

1399

}

1400

1401

function formatDate(date) {

1402

return date.toLocaleDateString();

1403

}

1404

1405

function generateRandomWords() {

1406

const words = ['Amazing', 'Fantastic', 'Great', 'Awesome', 'Cool', 'Nice', 'Super', 'Mega'];

1407

return words[Math.floor(Math.random() * words.length)];

1408

}

1409

1410

function generateRandomDescription() {

1411

const descriptions = [

1412

'High quality product with excellent features.',

1413

'Perfect for everyday use and special occasions.',

1414

'Durable construction with modern design.',

1415

'Affordable pricing with premium quality.',

1416

'Innovative technology meets practical design.'

1417

];

1418

return descriptions[Math.floor(Math.random() * descriptions.length)];

1419

}

1420

</script>

1421

1422

<style>

1423

.pagination-demo, .advanced-pagination, .search-pagination {

1424

border: 1px solid #ddd;

1425

border-radius: 8px;

1426

padding: 20px;

1427

margin: 15px 0;

1428

}

1429

1430

.data-section {

1431

margin-bottom: 20px;

1432

}

1433

1434

.data-info {

1435

margin-bottom: 15px;

1436

font-weight: 500;

1437

color: #666;

1438

}

1439

1440

.data-grid {

1441

display: grid;

1442

grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));

1443

gap: 10px;

1444

margin-bottom: 20px;

1445

}

1446

1447

.data-item, .result-item {

1448

padding: 10px;

1449

border: 1px solid #ddd;

1450

border-radius: 4px;

1451

background: #f9f9f9;

1452

}

1453

1454

.pagination-controls, .table-pagination, .search-pagination-controls {

1455

display: flex;

1456

align-items: center;

1457

justify-content: center;

1458

gap: 10px;

1459

margin: 20px 0;

1460

}

1461

1462

.nav-btn, .page-btn {

1463

padding: 8px 12px;

1464

border: 1px solid #ddd;

1465

border-radius: 4px;

1466

cursor: pointer;

1467

background: white;

1468

}

1469

1470

.nav-btn:disabled {

1471

opacity: 0.5;

1472

cursor: not-allowed;

1473

}

1474

1475

.page-numbers {

1476

display: flex;

1477

gap: 2px;

1478

}

1479

1480

.page-btn.active {

1481

background: #2196f3;

1482

color: white;

1483

border-color: #2196f3;

1484

}

1485

1486

.page-btn.ellipsis {

1487

cursor: default;

1488

background: transparent;

1489

border: none;

1490

}

1491

1492

.pagination-info {

1493

text-align: center;

1494

color: #666;

1495

font-size: 0.9em;

1496

}

1497

1498

.advanced-controls {

1499

display: flex;

1500

gap: 20px;

1501

align-items: center;

1502

margin-bottom: 20px;

1503

flex-wrap: wrap;

1504

}

1505

1506

.page-size-selector select, .page-input {

1507

margin-left: 8px;

1508

padding: 4px 8px;

1509

border: 1px solid #ddd;

1510

border-radius: 4px;

1511

}

1512

1513

.page-input {

1514

width: 80px;

1515

}

1516

1517

.data-table {

1518

width: 100%;

1519

border-collapse: collapse;

1520

margin: 15px 0;

1521

}

1522

1523

.data-table th, .data-table td {

1524

padding: 12px;

1525

text-align: left;

1526

border-bottom: 1px solid #ddd;

1527

}

1528

1529

.data-table th {

1530

background: #f5f5f5;

1531

font-weight: 600;

1532

}

1533

1534

.status-badge {

1535

padding: 4px 8px;

1536

border-radius: 4px;

1537

font-size: 0.8em;

1538

font-weight: 500;

1539

text-transform: uppercase;

1540

}

1541

1542

.status-active {

1543

background: #e8f5e8;

1544

color: #2e7d32;

1545

}

1546

1547

.status-inactive {

1548

background: #ffebee;

1549

color: #c62828;

1550

}

1551

1552

.status-pending {

1553

background: #fff3e0;

1554

color: #f57c00;

1555

}

1556

1557

.search-controls {

1558

display: flex;

1559

gap: 10px;

1560

margin-bottom: 20px;

1561

}

1562

1563

.search-input {

1564

flex: 1;

1565

padding: 10px;

1566

border: 1px solid #ddd;

1567

border-radius: 4px;

1568

font-size: 16px;

1569

}

1570

1571

.clear-btn {

1572

padding: 10px 15px;

1573

border: 1px solid #ddd;

1574

border-radius: 4px;

1575

cursor: pointer;

1576

background: #f5f5f5;

1577

}

1578

1579

.search-results-info {

1580

margin-bottom: 15px;

1581

padding: 10px;

1582

background: #e3f2fd;

1583

border-radius: 4px;

1584

color: #1976d2;

1585

}

1586

1587

.no-results {

1588

text-align: center;

1589

padding: 40px;

1590

color: #666;

1591

font-style: italic;

1592

}

1593

1594

.results-list {

1595

display: flex;

1596

flex-direction: column;

1597

gap: 10px;

1598

}

1599

1600

.result-title {

1601

font-weight: bold;

1602

margin-bottom: 5px;

1603

}

1604

1605

.result-description {

1606

color: #666;

1607

font-size: 0.9em;

1608

}

1609

1610

:global(mark) {

1611

background: #ffeb3b;

1612

padding: 0 2px;

1613

border-radius: 2px;

1614

}

1615

</style>

1616

```

1617

1618

## Additional Utility Components

1619

1620

### UseNetwork Component

1621

1622

```typescript { .api }

1623

/**

1624

* Component that provides network connectivity information

1625

*/

1626

interface UseNetworkReturn {

1627

isOnline: Ref<boolean>;

1628

downlink: Ref<number | undefined>;

1629

downlinkMax: Ref<number | undefined>;

1630

effectiveType: Ref<string | undefined>;

1631

saveData: Ref<boolean | undefined>;

1632

type: Ref<string | undefined>;

1633

}

1634

```

1635

1636

### UseOnline Component

1637

1638

```typescript { .api }

1639

/**

1640

* Component that tracks online/offline status

1641

*/

1642

interface UseOnlineReturn {

1643

isOnline: Ref<boolean>;

1644

}

1645

```

1646

1647

### UseIdle Component

1648

1649

```typescript { .api }

1650

/**

1651

* Component that tracks user idle state

1652

*/

1653

interface UseIdleReturn {

1654

idle: Ref<boolean>;

1655

lastActive: Ref<number>;

1656

reset: () => void;

1657

}

1658

```

1659

1660

### UseImage Component

1661

1662

```typescript { .api }

1663

/**

1664

* Component that manages image loading state

1665

*/

1666

interface UseImageReturn {

1667

isLoading: Ref<boolean>;

1668

error: Ref<Event | null>;

1669

isReady: Ref<boolean>;

1670

}

1671

```

1672

1673

### UseObjectUrl Component

1674

1675

```typescript { .api }

1676

/**

1677

* Component that creates and manages object URLs

1678

*/

1679

interface UseObjectUrlReturn {

1680

url: Ref<string | undefined>;

1681

release: () => void;

1682

}

1683

```

1684

1685

## Type Definitions

1686

1687

```typescript { .api }

1688

/** Common types used across utility and advanced components */

1689

type MaybeRefOrGetter<T> = T | Ref<T> | (() => T);

1690

1691

interface RenderableComponent {

1692

/** The element that the component should be rendered as @default 'div' */

1693

as?: object | string;

1694

}

1695

1696

/** Time and timestamp types */

1697

interface UseTimestampOptions {

1698

offset?: number;

1699

interval?: number | 'requestAnimationFrame';

1700

immediate?: boolean;

1701

controls?: boolean;

1702

}

1703

1704

/** Pagination types */

1705

interface UseOffsetPaginationOptions {

1706

total: MaybeRefOrGetter<number>;

1707

pageSize?: MaybeRefOrGetter<number>;

1708

page?: MaybeRefOrGetter<number>;

1709

}

1710

1711

/** Virtual list types */

1712

interface UseVirtualListItem<T = any> {

1713

data: T;

1714

index: number;

1715

}

1716

1717

/** Network information types */

1718

interface NetworkInformation extends EventTarget {

1719

readonly connection?: NetworkConnection;

1720

readonly onLine: boolean;

1721

}

1722

1723

interface NetworkConnection {

1724

readonly downlink: number;

1725

readonly downlinkMax: number;

1726

readonly effectiveType: '2g' | '3g' | '4g' | 'slow-2g';

1727

readonly rtt: number;

1728

readonly saveData: boolean;

1729

readonly type: ConnectionType;

1730

}

1731

1732

type ConnectionType = 'bluetooth' | 'cellular' | 'ethernet' | 'mixed' | 'none' | 'other' | 'unknown' | 'wifi' | 'wimax';

1733

```