@todovue/tv-search 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Cristhian Daza
3
+ Copyright (c) 2026 Cristhian Daza
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -5,7 +5,6 @@
5
5
  A fast, accessible, and fully customizable search interface component for Vue 3 applications. Provides an elegant modal search experience with keyboard shortcuts, real-time filtering, and complete style customization. Works seamlessly in Single Page Apps or Server-Side Rendered (SSR) environments (e.g. Nuxt 3).
6
6
 
7
7
  [![npm](https://img.shields.io/npm/v/@todovue/tv-search.svg)](https://www.npmjs.com/package/@todovue/tv-search)
8
- [![Netlify Status](https://api.netlify.com/api/v1/badges/c6992bf1-ed06-4d9b-8b77-752254880951/deploy-status)](https://app.netlify.com/projects/tv-search-demo/deploys)
9
8
  [![npm downloads](https://img.shields.io/npm/dm/@todovue/tv-search.svg)](https://www.npmjs.com/package/@todovue/tv-search)
10
9
  [![npm total downloads](https://img.shields.io/npm/dt/@todovue/tv-search.svg)](https://www.npmjs.com/package/@todovue/tv-search)
11
10
  ![License](https://img.shields.io/github/license/TODOvue/tv-search)
@@ -15,14 +14,13 @@ A fast, accessible, and fully customizable search interface component for Vue 3
15
14
  ![Last Commit](https://img.shields.io/github/last-commit/TODOvue/tv-search)
16
15
  ![Stars](https://img.shields.io/github/stars/TODOvue/tv-search?style=social)
17
16
 
18
- > Demo: https://tv-search-demo.netlify.app/
17
+ > Demo: https://ui.todovue.blog/search
19
18
 
20
- ---
21
19
  ## Table of Contents
22
20
  - [Features](#features)
23
21
  - [Installation](#installation)
24
22
  - [Quick Start (SPA)](#quick-start-spa)
25
- - [Nuxt 3 / SSR Usage](#nuxt-3--ssr-usage)
23
+ - [Nuxt 4 / SSR Usage](#nuxt-4--ssr-usage)
26
24
  - [Component Registration Options](#component-registration-options)
27
25
  - [Props](#props)
28
26
  - [Events](#events)
@@ -31,13 +29,11 @@ A fast, accessible, and fully customizable search interface component for Vue 3
31
29
  - [Results Data Structure](#results-data-structure)
32
30
  - [Accessibility](#accessibility)
33
31
  - [SSR Notes](#ssr-notes)
34
- - [Roadmap](#roadmap)
35
32
  - [Development](#development)
36
33
  - [Contributing](#contributing)
37
34
  - [Changelog](#changelog)
38
35
  - [License](#license)
39
36
 
40
- ---
41
37
  ## Features
42
38
  - **Keyboard-first UX**: Open with `Ctrl+K` / `Cmd+K`, close with `Esc`
43
39
  - **Real-time filtering**: Search as you type with instant results
@@ -46,11 +42,10 @@ A fast, accessible, and fully customizable search interface component for Vue 3
46
42
  - **Accessible**: Built with semantic HTML and keyboard navigation
47
43
  - **Lightweight**: Minimal dependencies, Vue 3 marked as peer dependency
48
44
  - **SSR compatible**: Works in Nuxt 3 and other SSR frameworks
49
- - **Auto-focus**: Input field receives focus automatically when opened
45
+ - **Autofocus**: Input field receives focus automatically when opened
50
46
  - **Click-away close**: Modal closes when clicking outside the content area
51
47
  - **Flexible results**: Pass any array of searchable items with custom properties
52
48
 
53
- ---
54
49
  ## Installation
55
50
  Using npm:
56
51
  ```bash
@@ -65,7 +60,6 @@ Using pnpm:
65
60
  pnpm add @todovue/tv-search
66
61
  ```
67
62
 
68
- ---
69
63
  ## Quick Start (SPA)
70
64
  Global registration (main.js / main.ts):
71
65
  ```js
@@ -125,8 +119,7 @@ function handleSearch(query) {
125
119
  </template>
126
120
  ```
127
121
 
128
- ---
129
- ## Nuxt 3 / SSR Usage
122
+ ## Nuxt 4 / SSR Usage
130
123
  Create a plugin file: `plugins/tv-search.client.ts` (client-only is recommended since it uses keyboard events):
131
124
  ```ts
132
125
  // nuxt.config.ts
@@ -166,7 +159,6 @@ import { TvSearch } from '@todovue/tv-search'
166
159
  </script>
167
160
  ```
168
161
 
169
- ---
170
162
  ## Component Registration Options
171
163
  | Approach | When to use |
172
164
  |------------------------------------------------------|----------------------------------------------------|
@@ -175,14 +167,15 @@ import { TvSearch } from '@todovue/tv-search'
175
167
  | Local named import `import TvSearch from '...'` | Single page usage / code splitting |
176
168
  | Nuxt plugin `.client.ts` | SSR apps with client-side interactions |
177
169
 
178
- ---
179
170
  ## Props
180
- | Prop | Type | Default | Description | Required |
181
- |--------------|--------|---------|---------------------------------------------------------------------------------------|----------|
182
- | placeholder | String | `""` | Placeholder text for the search input field | `true` |
183
- | titleButton | String | `""` | Text displayed on the search button | `true` |
184
- | results | Array | `[]` | Array of searchable items (see [Results Data Structure](#results-data-structure)) | `true` |
185
- | customStyles | Object | `{}` | Custom color scheme for theming (see [Customization](#customization-styles--theming)) | `false` |
171
+ | Prop | Type | Default | Description | Required |
172
+ |---------------|--------|--------------------------|---------------------------------------------------------------------------------------|----------|
173
+ | placeholder | String | `""` | Placeholder text for the search input field | `true` |
174
+ | titleButton | String | `""` | Text displayed on the search button | `true` |
175
+ | results | Array | `[]` | Array of searchable items (see [Results Data Structure](#results-data-structure)) | `true` |
176
+ | customStyles | Object | `{}` | Custom color scheme for theming (see [Customization](#customization-styles--theming)) | `false` |
177
+ | searchKeys | Array | `['title']` | Array of keys in result objects to search against | `false` |
178
+ | noResultsText | String | `"No results found for"` | Text to display when no results match the query | `false` |
186
179
 
187
180
  ### customStyles Object
188
181
  Customize the appearance by passing a `customStyles` object with any of these properties:
@@ -194,11 +187,10 @@ Customize the appearance by passing a `customStyles` object with any of these pr
194
187
  | bgButton | String | `"#EF233C"` | Background color of the search button |
195
188
  | colorButton | String | `"#F4FAFF"` | Text color of the search button |
196
189
 
197
- ---
198
190
  ## Events
199
- | Event | Payload Type | Description |
200
- |--------|--------------|-------------------------------------------------------------------------------------------------|
201
- | search | String | Emitted when search is triggered (Enter key or button click). Returns the trimmed search query. |
191
+ | Event | Payload Type | Description |
192
+ |--------|-----------------|-------------------------------------------------------------------------------------------------|
193
+ | search | String / Object | Emitted when search is triggered (Enter key or button click). Returns the trimmed search query. |
202
194
 
203
195
  Example:
204
196
  ```vue
@@ -217,7 +209,33 @@ function handleSearch(query) {
217
209
  </script>
218
210
  ```
219
211
 
220
- ---
212
+ ## Slots
213
+ | Slot Name | Props | Description |
214
+ |------------|--------------|---------------------------------------------------|
215
+ | item | `{ result }` | Custom rendering for each result item in the list |
216
+ | no-results | - | Custom content when no results are found |
217
+
218
+ ### Custom Slot Example
219
+ ```vue
220
+ <tv-search
221
+ :results="items"
222
+ :searchKeys="['title', 'description']"
223
+ >
224
+ <template #item="{ result }">
225
+ <div class="my-custom-item">
226
+ <h3>{{ result.title }}</h3>
227
+ <p>{{ result.description }}</p>
228
+ </div>
229
+ </template>
230
+
231
+ <template #no-results>
232
+ <div class="empty-state">
233
+ <p>No matches found.</p>
234
+ </div>
235
+ </template>
236
+ </tv-search>
237
+ ```
238
+
221
239
  ## Keyboard Shortcuts
222
240
  | Shortcut | Action |
223
241
  |------------------------|-----------------------------------|
@@ -226,7 +244,6 @@ function handleSearch(query) {
226
244
  | `Enter` | Execute search with current input |
227
245
  | Click outside modal | Close the search modal |
228
246
 
229
- ---
230
247
  ## Customization (Styles / Theming)
231
248
  You can override the default color scheme by passing a `customStyles` object:
232
249
 
@@ -289,7 +306,6 @@ const brandTheme = {
289
306
  }
290
307
  ```
291
308
 
292
- ---
293
309
  ## Results Data Structure
294
310
  The `results` prop expects an array of objects with the following structure:
295
311
 
@@ -325,7 +341,6 @@ const results = [
325
341
 
326
342
  **Note**: The component currently filters results based on the `title` property matching the user input (case-insensitive). You can handle the `@search` event to implement custom search logic or navigation.
327
343
 
328
- ---
329
344
  ## Accessibility
330
345
  - **Keyboard navigation**: Full support for `Ctrl+K`/`Cmd+K` to open, `Esc` to close, and `Enter` to search
331
346
  - **Focus management**: Input automatically receives focus when modal opens and is selected for immediate typing
@@ -338,7 +353,6 @@ const results = [
338
353
  - Ensure sufficient color contrast when using `customStyles`
339
354
  - Consider adding `aria-label` attributes for screen reader support in future versions
340
355
 
341
- ---
342
356
  ## SSR Notes
343
357
  - **Safe for SSR**: No direct DOM access (`window` / `document`) during module initialization
344
358
  - **Event listeners**: Keyboard event listeners are registered in `onMounted` and cleaned up in `onBeforeUnmount`
@@ -348,25 +362,6 @@ const results = [
348
362
  - For Vue/Vite SPA: `import '@todovue/tv-search/style.css'` in `main.ts`
349
363
  - For Nuxt 3/4: Add `'@todovue/tv-search/style.css'` to the `css` array in `nuxt.config.ts`
350
364
 
351
- ---
352
- ## Roadmap
353
- | Feature | Status |
354
- |------------------------------------------|-------------|
355
- | Support for result `url` navigation | Planned |
356
- | Display `description` in results | Planned |
357
- | Customizable search icon | Planned |
358
- | Multiple keyboard shortcut options | Considering |
359
- | Result categorization / grouping | Considering |
360
- | Highlight matching text in results | Considering |
361
- | Recent searches history | Considering |
362
- | Loading state indicator | Considering |
363
- | Pagination for large result sets | Considering |
364
- | Fuzzy search / advanced filtering | Considering |
365
- | Theming via CSS variables | Considering |
366
- | TypeScript type definitions improvement | Planned |
367
- | ARIA attributes enhancement | Planned |
368
-
369
- ---
370
365
  ## Development
371
366
  Clone the repository and install dependencies:
372
367
  ```bash
@@ -392,7 +387,6 @@ yarn build:demo
392
387
 
393
388
  The demo is served from Vite using `index.html` + `src/demo` examples.
394
389
 
395
- ---
396
390
  ## Contributing
397
391
  Contributions are welcome! Please read our [Contributing Guidelines](https://github.com/TODOvue/tv-search/blob/main/CONTRIBUTING.md) and [Code of Conduct](https://github.com/TODOvue/tv-search/blob/main/CODE_OF_CONDUCT.md) before submitting PRs.
398
392
 
@@ -403,14 +397,11 @@ Contributions are welcome! Please read our [Contributing Guidelines](https://git
403
397
  4. Push to the branch (`git push origin feature/amazing-feature`)
404
398
  5. Open a Pull Request
405
399
 
406
- ---
407
400
  ## Changelog
408
401
  See [CHANGELOG.md](https://github.com/TODOvue/tv-search/blob/main/CHANGELOG.md) for release history and version changes.
409
402
 
410
- ---
411
403
  ## License
412
404
  [MIT](https://github.com/TODOvue/tv-search/blob/main/LICENSE) © TODOvue
413
405
 
414
- ---
415
406
  ### Attributions
416
407
  Crafted for the TODOvue component ecosystem
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),S=require("@todovue/tv-button"),C=`<svg class="tv-icon-svg" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 129 129" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 129 129">
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),C=require("@todovue/tv-button"),w=`<svg class="tv-icon-svg" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 129 129" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 129 129">
2
2
  <g>
3
3
  <path d="M51.6,96.7c11,0,21-3.9,28.8-10.5l35,35c0.8,0.8,1.8,1.2,2.9,1.2s2.1-0.4,2.9-1.2c1.6-1.6,1.6-4.2,0-5.8l-35-35
4
4
  c6.5-7.8,10.5-17.9,10.5-28.8c0-24.9-20.2-45.1-45.1-45.1C26.8,6.5,6.5,26.8,6.5,51.6C6.5,76.5,26.8,96.7,51.6,96.7z
@@ -6,4 +6,4 @@
6
6
  fill="currentColor"/>
7
7
  </g>
8
8
  </svg>
9
- `,w=(l,v)=>{const r=e.ref(""),u=e.ref(!1),c=e.ref(),g=Object.assign({"../assets/icons/search.svg":C}),h=e.computed(()=>g["../assets/icons/search.svg"]||""),i=()=>{s(!0),d()},p=(t=null)=>{if(t&&(t instanceof Event||t?.target&&t?.preventDefault)&&(t=null),!(!r.value.trim()&&!t)){if(t){v("search",t),s(!1),r.value="";return}v("search",r.value.trim()),s(!1),r.value=""}},a=()=>{s(!1)},s=t=>{u.value=t},d=()=>{e.nextTick(()=>{c.value?.select()})},m=t=>{(t.ctrlKey||t.metaKey)&&t.key==="k"&&(t.preventDefault(),i()),t.key==="Escape"&&u.value&&a()};e.onMounted(()=>{document.addEventListener("keydown",m)}),e.onBeforeUnmount(()=>{document.removeEventListener("keydown",m)});const y=e.computed(()=>r.value.length<1?[]:l.results?.filter(t=>t.title.toLowerCase().includes(r.value.toLowerCase()))||[]),o=t=>{if(!t||t[0]!=="#")return t;const k=parseInt(t.slice(1,3),16),b=parseInt(t.slice(3,5),16),B=parseInt(t.slice(5,7),16);return`${k}, ${b}, ${B}`},n=e.computed(()=>{const{customStyles:t}=l;return t?{bgBody:{backgroundColor:`rgba(${o(t.bgBody)}, 0.9)`},bgInput:{backgroundColor:t.bgInput,boxShadow:`0 0 15px 0 ${t.bgInput}`},customButton:{backgroundColor:t.bgButton||"#ef233c",color:t.colorButton||"#f4faff"}}:{}});return{inputValue:r,inputSearch:c,openedModal:u,closeModal:a,openModal:i,search:p,filterResults:y,custom:n,iconContent:h}},_={class:"tv-search"},M=["innerHTML"],E={class:"tv-search-modal-content-input"},T=["placeholder"],V={key:0,class:"tv-search-results"},x=["onClick"],N={__name:"TvSearch",props:{placeholder:{type:String,default:""},titleButton:{type:String,default:""},results:{type:Array,default:()=>[]},customStyles:{type:Object,default:()=>({})}},emits:["search"],setup(l,{emit:v}){const r=l,u=v,{inputValue:c,inputSearch:g,openedModal:h,closeModal:i,openModal:p,search:a,filterResults:s,custom:d,iconContent:m}=w(r,u);return(y,o)=>(e.openBlock(),e.createElementBlock(e.Fragment,null,[e.createElementVNode("div",_,[e.createElementVNode("i",{class:"tv-cursor-pointer tv-search-icon",innerHTML:e.unref(m),onClick:o[0]||(o[0]=(...n)=>e.unref(p)&&e.unref(p)(...n))},null,8,M)]),e.unref(h)?(e.openBlock(),e.createElementBlock("div",{key:0,class:"tv-search-modal",onClick:o[4]||(o[4]=e.withModifiers((...n)=>e.unref(i)&&e.unref(i)(...n),["self"])),style:e.normalizeStyle(e.unref(d).bgBody)},[e.createElementVNode("div",{class:"tv-search-modal-content",style:e.normalizeStyle(e.unref(d).bgInput)},[e.createElementVNode("div",E,[e.withDirectives(e.createElementVNode("input",{type:"text","onUpdate:modelValue":o[1]||(o[1]=n=>e.isRef(c)?c.value=n:null),onKeyup:o[2]||(o[2]=e.withKeys(n=>e.unref(a)(),["enter"])),placeholder:l.placeholder,class:e.normalizeClass(["tv-search-input",{"tv-radius-none-bl":e.unref(s).length>=1}]),ref_key:"inputSearch",ref:g},null,42,T),[[e.vModelText,e.unref(c)]]),e.createVNode(e.unref(S.TvButton),{runded:"",icon:"search","icon-position":"left",onClick:o[3]||(o[3]=n=>e.unref(a)()),class:e.normalizeClass({"tv-radius-none-br":e.unref(s).length>=1}),customStyle:e.unref(d).customButton},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(l.titleButton),1)]),_:1},8,["class","customStyle"])]),e.unref(s).length>=1?(e.openBlock(),e.createElementBlock("div",V,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(e.unref(s),n=>(e.openBlock(),e.createElementBlock("p",{key:n.id,class:"tv-search-results-title tv-cursor-pointer",onClick:t=>e.unref(a)(n)},e.toDisplayString(n.title),9,x))),128))])):e.createCommentVNode("",!0)],4)],4)):e.createCommentVNode("",!0)],64))}},f=N;f.install=l=>{l.component("TvSearch",f)};const I={install:f.install};exports.TvSearch=f;exports.TvSearchPlugin=I;exports.default=f;
9
+ `,_=(r,v)=>{const s=e.ref(""),u=e.ref(!1),c=e.ref(),y=Object.assign({"../assets/icons/search.svg":w}),h=e.computed(()=>y["../assets/icons/search.svg"]||""),i=()=>{l(!0),d()},m=(t=null)=>{if(t&&(t instanceof Event||t?.target&&t?.preventDefault)&&(t=null),!(!s.value.trim()&&!t)){if(t){v("search",t),l(!1),s.value="";return}v("search",s.value.trim()),l(!1),s.value=""}},a=()=>{l(!1)},l=t=>{u.value=t},d=()=>{e.nextTick(()=>{c.value?.select()})},p=t=>{(t.ctrlKey||t.metaKey)&&t.key==="k"&&(t.preventDefault(),i()),t.key==="Escape"&&u.value&&a()};e.onMounted(()=>{document.addEventListener("keydown",p)}),e.onBeforeUnmount(()=>{document.removeEventListener("keydown",p)});const g=e.computed(()=>{if(s.value.length<1)return[];const t=r.searchKeys||["title"],k=s.value.toLowerCase();return r.results?.filter(S=>t.some(B=>{const b=S[B];return b&&String(b).toLowerCase().includes(k)}))||[]}),o=t=>{if(!t||t[0]!=="#")return t;const k=parseInt(t.slice(1,3),16),S=parseInt(t.slice(3,5),16),B=parseInt(t.slice(5,7),16);return`${k}, ${S}, ${B}`},n=e.computed(()=>{const{customStyles:t}=r;return t?{bgBody:{backgroundColor:`rgba(${o(t.bgBody)}, 0.9)`},bgInput:{backgroundColor:t.bgInput,boxShadow:`0 0 15px 0 ${t.bgInput}`},customButton:{backgroundColor:t.bgButton||"#ef233c",color:t.colorButton||"#f4faff"}}:{}});return{inputValue:s,inputSearch:c,openedModal:u,closeModal:a,openModal:i,search:m,filterResults:g,custom:n,iconContent:h}},E={class:"tv-search"},M=["innerHTML"],T={class:"tv-search-modal-content-input"},V=["placeholder"],N={key:0,class:"tv-search-results"},$=["onClick"],x={class:"tv-search-results-title"},I={key:1,class:"tv-search-no-results"},D={__name:"TvSearch",props:{placeholder:{type:String,default:""},titleButton:{type:String,default:""},results:{type:Array,default:()=>[]},customStyles:{type:Object,default:()=>({})},searchKeys:{type:Array,default:()=>["title"]},noResultsText:{type:String,default:"No results found for"}},emits:["search"],setup(r,{emit:v}){const s=r,u=v,{inputValue:c,inputSearch:y,openedModal:h,closeModal:i,openModal:m,search:a,filterResults:l,custom:d,iconContent:p}=_(s,u);return(g,o)=>(e.openBlock(),e.createElementBlock(e.Fragment,null,[e.createElementVNode("div",E,[e.createElementVNode("i",{class:"tv-cursor-pointer tv-search-icon",innerHTML:e.unref(p),onClick:o[0]||(o[0]=(...n)=>e.unref(m)&&e.unref(m)(...n))},null,8,M)]),e.unref(h)?(e.openBlock(),e.createElementBlock("div",{key:0,class:"tv-search-modal",onClick:o[4]||(o[4]=e.withModifiers((...n)=>e.unref(i)&&e.unref(i)(...n),["self"])),style:e.normalizeStyle(e.unref(d).bgBody)},[e.createElementVNode("div",{class:"tv-search-modal-content",style:e.normalizeStyle(e.unref(d).bgInput)},[e.createElementVNode("div",T,[e.withDirectives(e.createElementVNode("input",{type:"text","onUpdate:modelValue":o[1]||(o[1]=n=>e.isRef(c)?c.value=n:null),onKeyup:o[2]||(o[2]=e.withKeys(n=>e.unref(a)(),["enter"])),placeholder:r.placeholder,class:e.normalizeClass(["tv-search-input",{"tv-radius-none-bl":e.unref(l).length>=1}]),ref_key:"inputSearch",ref:y},null,42,V),[[e.vModelText,e.unref(c)]]),e.createVNode(e.unref(C.TvButton),{runded:"",icon:"search","icon-position":"left",onClick:o[3]||(o[3]=n=>e.unref(a)()),class:e.normalizeClass({"tv-radius-none-br":e.unref(l).length>=1}),customStyle:e.unref(d).customButton},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(r.titleButton),1)]),_:1},8,["class","customStyle"])]),e.unref(l).length>=1?(e.openBlock(),e.createElementBlock("div",N,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(e.unref(l),n=>(e.openBlock(),e.createElementBlock("div",{key:n.id,onClick:t=>e.unref(a)(n),class:"tv-cursor-pointer"},[e.renderSlot(g.$slots,"item",{result:n},()=>[e.createElementVNode("p",x,e.toDisplayString(n.title),1)])],8,$))),128))])):e.unref(c)?(e.openBlock(),e.createElementBlock("div",I,[e.renderSlot(g.$slots,"no-results",{},()=>[e.createElementVNode("p",null,e.toDisplayString(r.noResultsText)+' "'+e.toDisplayString(e.unref(c))+'"',1)])])):e.createCommentVNode("",!0)],4)],4)):e.createCommentVNode("",!0)],64))}},f=D;f.install=r=>{r.component("TvSearch",f)};const K={install:f.install};exports.TvSearch=f;exports.TvSearchPlugin=K;exports.default=f;
@@ -1 +1 @@
1
- @charset "UTF-8";.tv-cursor-pointer{cursor:pointer}.tv-radius-none-bl{border-bottom-left-radius:0!important}.tv-radius-none-br{border-bottom-right-radius:0!important}.tv-search-icon{border:2px solid #B9C4DF}.tv-search-modal{background-color:#f8fafcb3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.tv-search-modal .tv-search-modal-content{background-color:#b9c4df;box-shadow:0 0 15px #b9c4df}.tv-search-icon{display:inline-block;height:32px;padding:2px;width:32px}.tv-search-modal{align-items:flex-start;display:flex;height:100%;justify-content:center;left:0;padding-top:100px;position:fixed;top:0;width:100%;z-index:1000}.tv-search-modal .tv-search-modal-content{border-radius:5px;padding:20px;width:550px}.tv-search-modal .tv-search-modal-content .tv-search-input{border-radius:10px 0 0 10px;border:none;font-size:16px;height:44px;outline:none;padding:10px;width:100%}.tv-search-modal .tv-search-modal-content .tv-btn{border-radius:0 10px 10px 0}.tv-search-modal .tv-search-modal-content .tv-search-modal-content-input{display:flex}.tv-search-modal .tv-search-modal-content .tv-search-results{border-radius:0 0 10px 10px;background:#f8fafc;color:#1e293b;display:inline-block;max-height:200px;min-width:300px;overflow-y:auto;transition:all .3s ease;width:100%}.tv-search-modal .tv-search-modal-content .tv-search-results-title{font-size:1rem;font-weight:600;padding:.7rem 1rem}.tv-search-modal .tv-search-modal-content .tv-search-results-title:hover{background:#0e131f33}.tv-search-modal .tv-search-modal-content .tv-search-results-title:active{background:#0e131f66}.dark-mode .tv-search-icon{border:2px solid #0E131F}.dark-mode .tv-search-modal{background-color:#161e31b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.dark-mode .tv-search-modal .tv-search-modal-content{background-color:#0e131f;box-shadow:0 0 15px #0e131f}.dark-mode .tv-search-modal .tv-search-modal-content .tv-search-results{background:#161e31;color:#cbd5e1}
1
+ @charset "UTF-8";.tv-cursor-pointer{cursor:pointer}.tv-radius-none-bl{border-bottom-left-radius:0!important}.tv-radius-none-br{border-bottom-right-radius:0!important}.tv-search-icon{border:2px solid #B9C4DF}.tv-search-modal{background-color:#f8fafcb3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.tv-search-modal .tv-search-modal-content{background-color:#b9c4df;box-shadow:0 0 15px #b9c4df}.tv-search-icon{display:inline-block;height:32px;padding:2px;width:32px}.tv-search-modal{align-items:flex-start;display:flex;height:100%;justify-content:center;left:0;padding-top:100px;position:fixed;top:0;width:100%;z-index:1000}.tv-search-modal .tv-search-modal-content{border-radius:5px;padding:20px;width:550px;max-width:90%}.tv-search-modal .tv-search-modal-content .tv-search-input{border-radius:10px 0 0 10px;border:none;font-size:16px;height:44px;outline:none;padding:10px;width:100%}.tv-search-modal .tv-search-modal-content .tv-btn{border-radius:0 10px 10px 0}.tv-search-modal .tv-search-modal-content .tv-search-modal-content-input{display:flex}.tv-search-modal .tv-search-modal-content .tv-search-results{border-radius:0 0 10px 10px;background:#f8fafc;color:#1e293b;display:inline-block;max-height:200px;overflow-y:auto;transition:all .3s ease;width:100%}.tv-search-modal .tv-search-modal-content .tv-search-results-title{font-size:1rem;font-weight:600;padding:.7rem 1rem}.tv-search-modal .tv-search-modal-content .tv-search-results-title:hover{background:#0e131f33}.tv-search-modal .tv-search-modal-content .tv-search-results-title:active{background:#0e131f66}.dark-mode .tv-search-icon{border:2px solid #0E131F}.dark-mode .tv-search-modal{background-color:#161e31b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.dark-mode .tv-search-modal .tv-search-modal-content{background-color:#0e131f;box-shadow:0 0 15px #0e131f}.dark-mode .tv-search-modal .tv-search-modal-content .tv-search-results{background:#161e31;color:#cbd5e1}@media(max-width:768px){.tv-search-modal{padding-top:50px}.tv-search-modal .tv-search-modal-content{padding:15px;width:95%;max-width:95%}.tv-search-modal .tv-search-modal-content .tv-search-results{max-height:150px}}@media(max-width:480px){.tv-search-modal{padding-top:30px;align-items:flex-start}.tv-search-modal .tv-search-modal-content{padding:10px}.tv-search-modal .tv-search-modal-content .tv-search-input{font-size:14px;height:40px;padding:8px}.tv-search-modal .tv-search-modal-content .tv-search-results{max-height:120px}.tv-search-modal .tv-search-modal-content .tv-search-results-title{font-size:.9rem;padding:.5rem .8rem}}
@@ -1,6 +1,6 @@
1
- import { ref as b, computed as C, onMounted as V, onBeforeUnmount as E, nextTick as z, createElementBlock as v, openBlock as p, Fragment as _, createElementVNode as f, createCommentVNode as M, unref as e, normalizeStyle as B, withModifiers as D, withDirectives as K, createVNode as N, normalizeClass as T, withKeys as H, isRef as O, vModelText as j, withCtx as U, createTextVNode as A, toDisplayString as x, renderList as F } from "vue";
2
- import { TvButton as P } from "@todovue/tv-button";
3
- const q = `<svg class="tv-icon-svg" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 129 129" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 129 129">
1
+ import { ref as M, computed as B, onMounted as E, onBeforeUnmount as N, nextTick as z, createElementBlock as u, openBlock as d, Fragment as $, createElementVNode as a, createCommentVNode as I, unref as e, normalizeStyle as K, withModifiers as D, withDirectives as H, createVNode as O, normalizeClass as L, withKeys as j, isRef as A, vModelText as U, withCtx as q, createTextVNode as F, toDisplayString as k, renderList as P, renderSlot as V } from "vue";
2
+ import { TvButton as G } from "@todovue/tv-button";
3
+ const J = `<svg class="tv-icon-svg" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 129 129" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 129 129">
4
4
  <g>
5
5
  <path d="M51.6,96.7c11,0,21-3.9,28.8-10.5l35,35c0.8,0.8,1.8,1.2,2.9,1.2s2.1-0.4,2.9-1.2c1.6-1.6,1.6-4.2,0-5.8l-35-35
6
6
  c6.5-7.8,10.5-17.9,10.5-28.8c0-24.9-20.2-45.1-45.1-45.1C26.8,6.5,6.5,26.8,6.5,51.6C6.5,76.5,26.8,96.7,51.6,96.7z
@@ -8,41 +8,46 @@ const q = `<svg class="tv-icon-svg" version="1.1" xmlns="http://www.w3.org/2000/
8
8
  fill="currentColor"/>
9
9
  </g>
10
10
  </svg>
11
- `, G = (l, g) => {
12
- const s = b(""), i = b(!1), c = b(), y = /* @__PURE__ */ Object.assign({ "../assets/icons/search.svg": q }), k = C(() => y["../assets/icons/search.svg"] || ""), u = () => {
13
- r(!0), d();
11
+ `, Q = (s, g) => {
12
+ const r = M(""), v = M(!1), c = M(), b = /* @__PURE__ */ Object.assign({ "../assets/icons/search.svg": J }), C = B(() => b["../assets/icons/search.svg"] || ""), p = () => {
13
+ l(!0), f();
14
14
  }, m = (t = null) => {
15
- if (t && (t instanceof Event || t?.target && t?.preventDefault) && (t = null), !(!s.value.trim() && !t)) {
15
+ if (t && (t instanceof Event || t?.target && t?.preventDefault) && (t = null), !(!r.value.trim() && !t)) {
16
16
  if (t) {
17
- g("search", t), r(!1), s.value = "";
17
+ g("search", t), l(!1), r.value = "";
18
18
  return;
19
19
  }
20
- g("search", s.value.trim()), r(!1), s.value = "";
20
+ g("search", r.value.trim()), l(!1), r.value = "";
21
21
  }
22
- }, a = () => {
23
- r(!1);
24
- }, r = (t) => {
25
- i.value = t;
26
- }, d = () => {
22
+ }, i = () => {
23
+ l(!1);
24
+ }, l = (t) => {
25
+ v.value = t;
26
+ }, f = () => {
27
27
  z(() => {
28
28
  c.value?.select();
29
29
  });
30
30
  }, h = (t) => {
31
- (t.ctrlKey || t.metaKey) && t.key === "k" && (t.preventDefault(), u()), t.key === "Escape" && i.value && a();
31
+ (t.ctrlKey || t.metaKey) && t.key === "k" && (t.preventDefault(), p()), t.key === "Escape" && v.value && i();
32
32
  };
33
- V(() => {
33
+ E(() => {
34
34
  document.addEventListener("keydown", h);
35
- }), E(() => {
35
+ }), N(() => {
36
36
  document.removeEventListener("keydown", h);
37
37
  });
38
- const S = C(() => s.value.length < 1 ? [] : l.results?.filter(
39
- (t) => t.title.toLowerCase().includes(s.value.toLowerCase())
40
- ) || []), o = (t) => {
38
+ const y = B(() => {
39
+ if (r.value.length < 1) return [];
40
+ const t = s.searchKeys || ["title"], S = r.value.toLowerCase();
41
+ return s.results?.filter((w) => t.some((_) => {
42
+ const x = w[_];
43
+ return x && String(x).toLowerCase().includes(S);
44
+ })) || [];
45
+ }), o = (t) => {
41
46
  if (!t || t[0] !== "#") return t;
42
- const I = parseInt(t.slice(1, 3), 16), $ = parseInt(t.slice(3, 5), 16), L = parseInt(t.slice(5, 7), 16);
43
- return `${I}, ${$}, ${L}`;
44
- }, n = C(() => {
45
- const { customStyles: t } = l;
47
+ const S = parseInt(t.slice(1, 3), 16), w = parseInt(t.slice(3, 5), 16), _ = parseInt(t.slice(5, 7), 16);
48
+ return `${S}, ${w}, ${_}`;
49
+ }, n = B(() => {
50
+ const { customStyles: t } = s;
46
51
  return t ? {
47
52
  bgBody: { backgroundColor: `rgba(${o(t.bgBody)}, 0.9)` },
48
53
  bgInput: {
@@ -56,20 +61,23 @@ const q = `<svg class="tv-icon-svg" version="1.1" xmlns="http://www.w3.org/2000/
56
61
  } : {};
57
62
  });
58
63
  return {
59
- inputValue: s,
64
+ inputValue: r,
60
65
  inputSearch: c,
61
- openedModal: i,
62
- closeModal: a,
63
- openModal: u,
66
+ openedModal: v,
67
+ closeModal: i,
68
+ openModal: p,
64
69
  search: m,
65
- filterResults: S,
70
+ filterResults: y,
66
71
  custom: n,
67
- iconContent: k
72
+ iconContent: C
68
73
  };
69
- }, J = { class: "tv-search" }, Q = ["innerHTML"], W = { class: "tv-search-modal-content-input" }, X = ["placeholder"], Y = {
74
+ }, W = { class: "tv-search" }, X = ["innerHTML"], Y = { class: "tv-search-modal-content-input" }, Z = ["placeholder"], R = {
70
75
  key: 0,
71
76
  class: "tv-search-results"
72
- }, Z = ["onClick"], R = {
77
+ }, tt = ["onClick"], et = { class: "tv-search-results-title" }, nt = {
78
+ key: 1,
79
+ class: "tv-search-no-results"
80
+ }, ot = {
73
81
  __name: "TvSearch",
74
82
  props: {
75
83
  placeholder: {
@@ -87,85 +95,101 @@ const q = `<svg class="tv-icon-svg" version="1.1" xmlns="http://www.w3.org/2000/
87
95
  customStyles: {
88
96
  type: Object,
89
97
  default: () => ({})
98
+ },
99
+ searchKeys: {
100
+ type: Array,
101
+ default: () => ["title"]
102
+ },
103
+ noResultsText: {
104
+ type: String,
105
+ default: "No results found for"
90
106
  }
91
107
  },
92
108
  emits: ["search"],
93
- setup(l, { emit: g }) {
94
- const s = l, i = g, {
109
+ setup(s, { emit: g }) {
110
+ const r = s, v = g, {
95
111
  inputValue: c,
96
- inputSearch: y,
97
- openedModal: k,
98
- closeModal: u,
112
+ inputSearch: b,
113
+ openedModal: C,
114
+ closeModal: p,
99
115
  openModal: m,
100
- search: a,
101
- filterResults: r,
102
- custom: d,
116
+ search: i,
117
+ filterResults: l,
118
+ custom: f,
103
119
  iconContent: h
104
- } = G(s, i);
105
- return (S, o) => (p(), v(_, null, [
106
- f("div", J, [
107
- f("i", {
120
+ } = Q(r, v);
121
+ return (y, o) => (d(), u($, null, [
122
+ a("div", W, [
123
+ a("i", {
108
124
  class: "tv-cursor-pointer tv-search-icon",
109
125
  innerHTML: e(h),
110
126
  onClick: o[0] || (o[0] = (...n) => e(m) && e(m)(...n))
111
- }, null, 8, Q)
127
+ }, null, 8, X)
112
128
  ]),
113
- e(k) ? (p(), v("div", {
129
+ e(C) ? (d(), u("div", {
114
130
  key: 0,
115
131
  class: "tv-search-modal",
116
- onClick: o[4] || (o[4] = D((...n) => e(u) && e(u)(...n), ["self"])),
117
- style: B(e(d).bgBody)
132
+ onClick: o[4] || (o[4] = D((...n) => e(p) && e(p)(...n), ["self"])),
133
+ style: K(e(f).bgBody)
118
134
  }, [
119
- f("div", {
135
+ a("div", {
120
136
  class: "tv-search-modal-content",
121
- style: B(e(d).bgInput)
137
+ style: K(e(f).bgInput)
122
138
  }, [
123
- f("div", W, [
124
- K(f("input", {
139
+ a("div", Y, [
140
+ H(a("input", {
125
141
  type: "text",
126
- "onUpdate:modelValue": o[1] || (o[1] = (n) => O(c) ? c.value = n : null),
127
- onKeyup: o[2] || (o[2] = H((n) => e(a)(), ["enter"])),
128
- placeholder: l.placeholder,
129
- class: T(["tv-search-input", { "tv-radius-none-bl": e(r).length >= 1 }]),
142
+ "onUpdate:modelValue": o[1] || (o[1] = (n) => A(c) ? c.value = n : null),
143
+ onKeyup: o[2] || (o[2] = j((n) => e(i)(), ["enter"])),
144
+ placeholder: s.placeholder,
145
+ class: L(["tv-search-input", { "tv-radius-none-bl": e(l).length >= 1 }]),
130
146
  ref_key: "inputSearch",
131
- ref: y
132
- }, null, 42, X), [
133
- [j, e(c)]
147
+ ref: b
148
+ }, null, 42, Z), [
149
+ [U, e(c)]
134
150
  ]),
135
- N(e(P), {
151
+ O(e(G), {
136
152
  runded: "",
137
153
  icon: "search",
138
154
  "icon-position": "left",
139
- onClick: o[3] || (o[3] = (n) => e(a)()),
140
- class: T({ "tv-radius-none-br": e(r).length >= 1 }),
141
- customStyle: e(d).customButton
155
+ onClick: o[3] || (o[3] = (n) => e(i)()),
156
+ class: L({ "tv-radius-none-br": e(l).length >= 1 }),
157
+ customStyle: e(f).customButton
142
158
  }, {
143
- default: U(() => [
144
- A(x(l.titleButton), 1)
159
+ default: q(() => [
160
+ F(k(s.titleButton), 1)
145
161
  ]),
146
162
  _: 1
147
163
  }, 8, ["class", "customStyle"])
148
164
  ]),
149
- e(r).length >= 1 ? (p(), v("div", Y, [
150
- (p(!0), v(_, null, F(e(r), (n) => (p(), v("p", {
165
+ e(l).length >= 1 ? (d(), u("div", R, [
166
+ (d(!0), u($, null, P(e(l), (n) => (d(), u("div", {
151
167
  key: n.id,
152
- class: "tv-search-results-title tv-cursor-pointer",
153
- onClick: (t) => e(a)(n)
154
- }, x(n.title), 9, Z))), 128))
155
- ])) : M("", !0)
168
+ onClick: (t) => e(i)(n),
169
+ class: "tv-cursor-pointer"
170
+ }, [
171
+ V(y.$slots, "item", { result: n }, () => [
172
+ a("p", et, k(n.title), 1)
173
+ ])
174
+ ], 8, tt))), 128))
175
+ ])) : e(c) ? (d(), u("div", nt, [
176
+ V(y.$slots, "no-results", {}, () => [
177
+ a("p", null, k(s.noResultsText) + ' "' + k(e(c)) + '"', 1)
178
+ ])
179
+ ])) : I("", !0)
156
180
  ], 4)
157
- ], 4)) : M("", !0)
181
+ ], 4)) : I("", !0)
158
182
  ], 64));
159
183
  }
160
- }, w = R;
161
- w.install = (l) => {
162
- l.component("TvSearch", w);
184
+ }, T = ot;
185
+ T.install = (s) => {
186
+ s.component("TvSearch", T);
163
187
  };
164
- const nt = {
165
- install: w.install
188
+ const lt = {
189
+ install: T.install
166
190
  };
167
191
  export {
168
- w as TvSearch,
169
- nt as TvSearchPlugin,
170
- w as default
192
+ T as TvSearch,
193
+ lt as TvSearchPlugin,
194
+ T as default
171
195
  };
package/package.json CHANGED
@@ -4,11 +4,12 @@
4
4
  "author": "Cristhian Daza",
5
5
  "description": "TvSearch provides a fast, accessible, and fully customizable search interface for Vue 3 apps.",
6
6
  "license": "MIT",
7
- "version": "1.1.1",
7
+ "version": "1.2.0",
8
8
  "type": "module",
9
+ "homepage": "https://ui.todovue.blog/search",
9
10
  "repository": {
10
11
  "type": "git",
11
- "url": "https://github.com/TODOvue/tv-search.git"
12
+ "url": "git+https://github.com/TODOvue/tv-search.git"
12
13
  },
13
14
  "bugs": {
14
15
  "url": "https://github.com/TODOvue/tv-search/issues"
@@ -56,16 +57,16 @@
56
57
  "build:demo": "cp README.md public/ && cp CHANGELOG.md public/ && VITE_BUILD_TARGET=demo vite build"
57
58
  },
58
59
  "peerDependencies": {
59
- "vue": "^3.0.0"
60
+ "vue": "^3.5.26"
60
61
  },
61
62
  "dependencies": {
62
- "@todovue/tv-button": "^1.2.2"
63
+ "@todovue/tv-button": "^1.2.4"
63
64
  },
64
65
  "devDependencies": {
65
- "@todovue/tv-demo": "^1.2.2",
66
- "@vitejs/plugin-vue": "^6.0.0",
67
- "sass": "^1.0.0",
68
- "vite": "^7.0.0",
69
- "vite-plugin-dts": "^4.0.0"
66
+ "@todovue/tv-demo": "^1.4.4",
67
+ "@vitejs/plugin-vue": "^6.0.3",
68
+ "sass": "^1.97.2",
69
+ "vite": "^7.3.1",
70
+ "vite-plugin-dts": "^4.5.4"
70
71
  }
71
72
  }