@todovue/tv-search 1.1.3 → 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/README.md CHANGED
@@ -16,12 +16,11 @@ A fast, accessible, and fully customizable search interface component for Vue 3
16
16
 
17
17
  > Demo: https://ui.todovue.blog/search
18
18
 
19
- ---
20
19
  ## Table of Contents
21
20
  - [Features](#features)
22
21
  - [Installation](#installation)
23
22
  - [Quick Start (SPA)](#quick-start-spa)
24
- - [Nuxt 3 / SSR Usage](#nuxt-3--ssr-usage)
23
+ - [Nuxt 4 / SSR Usage](#nuxt-4--ssr-usage)
25
24
  - [Component Registration Options](#component-registration-options)
26
25
  - [Props](#props)
27
26
  - [Events](#events)
@@ -30,13 +29,11 @@ A fast, accessible, and fully customizable search interface component for Vue 3
30
29
  - [Results Data Structure](#results-data-structure)
31
30
  - [Accessibility](#accessibility)
32
31
  - [SSR Notes](#ssr-notes)
33
- - [Roadmap](#roadmap)
34
32
  - [Development](#development)
35
33
  - [Contributing](#contributing)
36
34
  - [Changelog](#changelog)
37
35
  - [License](#license)
38
36
 
39
- ---
40
37
  ## Features
41
38
  - **Keyboard-first UX**: Open with `Ctrl+K` / `Cmd+K`, close with `Esc`
42
39
  - **Real-time filtering**: Search as you type with instant results
@@ -45,11 +42,10 @@ A fast, accessible, and fully customizable search interface component for Vue 3
45
42
  - **Accessible**: Built with semantic HTML and keyboard navigation
46
43
  - **Lightweight**: Minimal dependencies, Vue 3 marked as peer dependency
47
44
  - **SSR compatible**: Works in Nuxt 3 and other SSR frameworks
48
- - **Auto-focus**: Input field receives focus automatically when opened
45
+ - **Autofocus**: Input field receives focus automatically when opened
49
46
  - **Click-away close**: Modal closes when clicking outside the content area
50
47
  - **Flexible results**: Pass any array of searchable items with custom properties
51
48
 
52
- ---
53
49
  ## Installation
54
50
  Using npm:
55
51
  ```bash
@@ -64,7 +60,6 @@ Using pnpm:
64
60
  pnpm add @todovue/tv-search
65
61
  ```
66
62
 
67
- ---
68
63
  ## Quick Start (SPA)
69
64
  Global registration (main.js / main.ts):
70
65
  ```js
@@ -124,8 +119,7 @@ function handleSearch(query) {
124
119
  </template>
125
120
  ```
126
121
 
127
- ---
128
- ## Nuxt 3 / SSR Usage
122
+ ## Nuxt 4 / SSR Usage
129
123
  Create a plugin file: `plugins/tv-search.client.ts` (client-only is recommended since it uses keyboard events):
130
124
  ```ts
131
125
  // nuxt.config.ts
@@ -165,7 +159,6 @@ import { TvSearch } from '@todovue/tv-search'
165
159
  </script>
166
160
  ```
167
161
 
168
- ---
169
162
  ## Component Registration Options
170
163
  | Approach | When to use |
171
164
  |------------------------------------------------------|----------------------------------------------------|
@@ -174,14 +167,15 @@ import { TvSearch } from '@todovue/tv-search'
174
167
  | Local named import `import TvSearch from '...'` | Single page usage / code splitting |
175
168
  | Nuxt plugin `.client.ts` | SSR apps with client-side interactions |
176
169
 
177
- ---
178
170
  ## Props
179
- | Prop | Type | Default | Description | Required |
180
- |--------------|--------|---------|---------------------------------------------------------------------------------------|----------|
181
- | placeholder | String | `""` | Placeholder text for the search input field | `true` |
182
- | titleButton | String | `""` | Text displayed on the search button | `true` |
183
- | results | Array | `[]` | Array of searchable items (see [Results Data Structure](#results-data-structure)) | `true` |
184
- | 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` |
185
179
 
186
180
  ### customStyles Object
187
181
  Customize the appearance by passing a `customStyles` object with any of these properties:
@@ -193,11 +187,10 @@ Customize the appearance by passing a `customStyles` object with any of these pr
193
187
  | bgButton | String | `"#EF233C"` | Background color of the search button |
194
188
  | colorButton | String | `"#F4FAFF"` | Text color of the search button |
195
189
 
196
- ---
197
190
  ## Events
198
- | Event | Payload Type | Description |
199
- |--------|--------------|-------------------------------------------------------------------------------------------------|
200
- | 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. |
201
194
 
202
195
  Example:
203
196
  ```vue
@@ -216,7 +209,33 @@ function handleSearch(query) {
216
209
  </script>
217
210
  ```
218
211
 
219
- ---
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
+
220
239
  ## Keyboard Shortcuts
221
240
  | Shortcut | Action |
222
241
  |------------------------|-----------------------------------|
@@ -225,7 +244,6 @@ function handleSearch(query) {
225
244
  | `Enter` | Execute search with current input |
226
245
  | Click outside modal | Close the search modal |
227
246
 
228
- ---
229
247
  ## Customization (Styles / Theming)
230
248
  You can override the default color scheme by passing a `customStyles` object:
231
249
 
@@ -288,7 +306,6 @@ const brandTheme = {
288
306
  }
289
307
  ```
290
308
 
291
- ---
292
309
  ## Results Data Structure
293
310
  The `results` prop expects an array of objects with the following structure:
294
311
 
@@ -324,7 +341,6 @@ const results = [
324
341
 
325
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.
326
343
 
327
- ---
328
344
  ## Accessibility
329
345
  - **Keyboard navigation**: Full support for `Ctrl+K`/`Cmd+K` to open, `Esc` to close, and `Enter` to search
330
346
  - **Focus management**: Input automatically receives focus when modal opens and is selected for immediate typing
@@ -337,7 +353,6 @@ const results = [
337
353
  - Ensure sufficient color contrast when using `customStyles`
338
354
  - Consider adding `aria-label` attributes for screen reader support in future versions
339
355
 
340
- ---
341
356
  ## SSR Notes
342
357
  - **Safe for SSR**: No direct DOM access (`window` / `document`) during module initialization
343
358
  - **Event listeners**: Keyboard event listeners are registered in `onMounted` and cleaned up in `onBeforeUnmount`
@@ -347,25 +362,6 @@ const results = [
347
362
  - For Vue/Vite SPA: `import '@todovue/tv-search/style.css'` in `main.ts`
348
363
  - For Nuxt 3/4: Add `'@todovue/tv-search/style.css'` to the `css` array in `nuxt.config.ts`
349
364
 
350
- ---
351
- ## Roadmap
352
- | Feature | Status |
353
- |------------------------------------------|-------------|
354
- | Support for result `url` navigation | Planned |
355
- | Display `description` in results | Planned |
356
- | Customizable search icon | Planned |
357
- | Multiple keyboard shortcut options | Considering |
358
- | Result categorization / grouping | Considering |
359
- | Highlight matching text in results | Considering |
360
- | Recent searches history | Considering |
361
- | Loading state indicator | Considering |
362
- | Pagination for large result sets | Considering |
363
- | Fuzzy search / advanced filtering | Considering |
364
- | Theming via CSS variables | Considering |
365
- | TypeScript type definitions improvement | Planned |
366
- | ARIA attributes enhancement | Planned |
367
-
368
- ---
369
365
  ## Development
370
366
  Clone the repository and install dependencies:
371
367
  ```bash
@@ -391,7 +387,6 @@ yarn build:demo
391
387
 
392
388
  The demo is served from Vite using `index.html` + `src/demo` examples.
393
389
 
394
- ---
395
390
  ## Contributing
396
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.
397
392
 
@@ -402,14 +397,11 @@ Contributions are welcome! Please read our [Contributing Guidelines](https://git
402
397
  4. Push to the branch (`git push origin feature/amazing-feature`)
403
398
  5. Open a Pull Request
404
399
 
405
- ---
406
400
  ## Changelog
407
401
  See [CHANGELOG.md](https://github.com/TODOvue/tv-search/blob/main/CHANGELOG.md) for release history and version changes.
408
402
 
409
- ---
410
403
  ## License
411
404
  [MIT](https://github.com/TODOvue/tv-search/blob/main/LICENSE) © TODOvue
412
405
 
413
- ---
414
406
  ### Attributions
415
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,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,7 +4,7 @@
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.3",
7
+ "version": "1.2.0",
8
8
  "type": "module",
9
9
  "homepage": "https://ui.todovue.blog/search",
10
10
  "repository": {
@@ -60,13 +60,13 @@
60
60
  "vue": "^3.5.26"
61
61
  },
62
62
  "dependencies": {
63
- "@todovue/tv-button": "^1.2.3"
63
+ "@todovue/tv-button": "^1.2.4"
64
64
  },
65
65
  "devDependencies": {
66
- "@todovue/tv-demo": "^1.2.7",
66
+ "@todovue/tv-demo": "^1.4.4",
67
67
  "@vitejs/plugin-vue": "^6.0.3",
68
- "sass": "^1.97.1",
69
- "vite": "^7.3.0",
68
+ "sass": "^1.97.2",
69
+ "vite": "^7.3.1",
70
70
  "vite-plugin-dts": "^4.5.4"
71
71
  }
72
72
  }