@community-release/nx-ui 0.0.57 → 0.0.59

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/dist/module.d.mts CHANGED
@@ -116,6 +116,10 @@ var defaultComponentsStyle = {
116
116
  },
117
117
  input: {
118
118
  "background-color": "var(--ui-color-bg)"
119
+ },
120
+ "typeahead-input": {
121
+ "list-bg": "var(--ui-color-bg)",
122
+ "list-border-radius": "var(--ui-border-radius-small)"
119
123
  }
120
124
  };
121
125
 
package/dist/module.d.ts CHANGED
@@ -116,6 +116,10 @@ var defaultComponentsStyle = {
116
116
  },
117
117
  input: {
118
118
  "background-color": "var(--ui-color-bg)"
119
+ },
120
+ "typeahead-input": {
121
+ "list-bg": "var(--ui-color-bg)",
122
+ "list-border-radius": "var(--ui-border-radius-small)"
119
123
  }
120
124
  };
121
125
 
package/dist/module.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "ui",
3
3
  "configKey": "ui",
4
- "version": "0.0.57"
4
+ "version": "0.0.59"
5
5
  }
package/dist/module.mjs CHANGED
@@ -116,6 +116,10 @@ const defaultComponentsStyle = {
116
116
  },
117
117
  input: {
118
118
  "background-color": "var(--ui-color-bg)"
119
+ },
120
+ "typeahead-input": {
121
+ "list-bg": "var(--ui-color-bg)",
122
+ "list-border-radius": "var(--ui-border-radius-small)"
119
123
  }
120
124
  };
121
125
 
@@ -44,7 +44,7 @@
44
44
 
45
45
  <script setup>
46
46
  // Imports
47
- import { ref, computed, resolveComponent } from 'vue';
47
+ import { ref, computed } from 'vue';
48
48
  import UiImpulseIndicator from '../impulse-indicator.vue';
49
49
  import UiLoading from '../loading.vue';
50
50
  import comProps from '#build/ui.button.mjs';
@@ -155,16 +155,12 @@
155
155
  width : refCom.value.offsetWidth,
156
156
  height : refCom.value.offsetHeight
157
157
  };
158
-
159
- // Handle navigate
160
- // if (computedType == 'a') {
161
- // if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
162
-
163
- // e.preventDefault();
164
-
165
- // router.push(props.href);
166
- // }
167
158
  }
159
+
160
+ // Expose
161
+ defineExpose({
162
+ refCom
163
+ });
168
164
  </script>
169
165
 
170
166
  <style lang="less">
@@ -378,7 +374,7 @@
378
374
  // Disabled
379
375
  &.tag-disabled {
380
376
  opacity: 0.6;
381
- cursor: default;
377
+ cursor: not-allowed;
382
378
  pointer-events: none;
383
379
  }
384
380
  }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024"><path d="M1006.444 334.973 553.473 768.982c-15.17 12.8-29.393 18.014-41.481 18.014-12.089 0-28.373-5.26-39.324-15.852L17.563 334.973c-22.746-21.57-23.486-59.733-1.707-80.355 21.63-22.814 57.789-23.556 80.378-1.703l415.758 398.498 415.757-398.217c22.518-21.852 58.737-21.11 80.378 1.704 21.784 20.34 21.073 58.503-1.683 80.073z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024"><path d="M1008.882 1008.993c-20.007 20.01-52.377 20.01-72.406 0L511.992 584.366 87.41 1008.993c-20.007 20.01-52.377 20.01-72.406 0-20.007-20.009-20.007-52.382 0-72.414l424.677-424.531L15.069 87.421c-20.007-20.01-20.007-52.383 0-72.414 20.007-20.01 52.377-20.01 72.406 0L511.992 439.73l424.58-424.627c20.007-20.01 52.377-20.01 72.406 0 20.007 20.009 20.007 52.382 0 72.414L584.302 512.048l424.58 424.627c20.157 19.84 20.157 52.479 0 72.318z"/></svg>
@@ -17,13 +17,17 @@
17
17
 
18
18
  @change="updateValue($event.target.value)"
19
19
  @input="updateValue($event.target.value)"
20
- @blur="handleBlur($event.target.value)"
20
+ @focus="handleFocusBlur(true, $event.target.value)"
21
+ @blur="handleFocusBlur(false, $event.target.value)"
21
22
  @keyup.enter="updateValue($event.target.value, true, true)"
22
-
23
- @focus="haveFocus = true"
23
+ @keydown.down="emit('keydown.down', $event)"
24
+ @keydown.tab="emit('keydown.tab', $event)"
25
+ @keydown.esc.prevent="emit('keydown.esc', $event)"
24
26
 
25
27
  formnovalidate
26
28
  spellcheck="false"
29
+
30
+ v-bind="inputAttrs"
27
31
  />
28
32
  </div>
29
33
  <div class="slot-append" v-if="hasSlot('append')"><slot name="append"></slot></div>
@@ -33,13 +37,17 @@
33
37
 
34
38
  <script setup>
35
39
  // Import
36
- import { ref, computed, useSlots } from 'vue';
40
+ import { ref, computed, useSlots, useAttrs } from 'vue';
37
41
  import UiImpulseIndicator from '../impulse-indicator.vue';
38
42
  import comProps from '#build/ui.input.mjs';
39
43
 
40
44
  // Misc
41
- const emit = defineEmits(['input', 'enter', 'blur', 'update:modelValue']);
45
+ const emit = defineEmits([
46
+ 'input', 'enter', 'focus', 'blur', 'update:modelValue',
47
+ 'keydown.down', 'keydown.tab', 'keydown.esc',
48
+ ]);
42
49
  const slots = useSlots();
50
+ const attrs = useAttrs();
43
51
 
44
52
  // Data
45
53
  const props = defineProps({
@@ -97,6 +105,15 @@
97
105
  return ar;
98
106
  });
99
107
 
108
+ const inputAttrs = {};
109
+ for (const key in attrs) {
110
+ if (key.startsWith('input.')) {
111
+ const k = key.slice(6) // All after "input."
112
+
113
+ inputAttrs[k] = attrs[key];
114
+ }
115
+ }
116
+
100
117
  // Methods
101
118
  function updateValue(value, doTrim = false, submit = false) {
102
119
  const validValue = doTrim ? value.trim() : value;
@@ -104,21 +121,29 @@
104
121
  if (value !== validValue)
105
122
  refInput.value.value = validValue;
106
123
 
107
- emit('update:modelValue', validValue);
124
+ if (props.modelValue !== validValue) {
125
+ emit('update:modelValue', validValue);
126
+ emit('input', validValue);
127
+ }
108
128
 
109
129
  if (submit) emit('enter', validValue);
110
130
  }
111
131
 
112
- function handleBlur(v) {
113
- haveFocus.value = false;
114
-
115
- emit('blur', v);
116
- updateValue(v);
132
+ function handleFocusBlur(focus, value) {
133
+ haveFocus.value = focus;
134
+ emit(focus ? 'focus' : 'blur', focus);
135
+
136
+ if (!focus) updateValue(value);
117
137
  }
118
138
 
119
139
  const hasSlot = (name) => {
120
140
  return !!slots[name];
121
141
  };
142
+
143
+ // Expose
144
+ defineExpose({
145
+ refInput
146
+ });
122
147
  </script>
123
148
 
124
149
  <style lang="less">
@@ -5,3 +5,5 @@
5
5
  @ui-select-value-font-weight: var(--ui-font-weight-medium);
6
6
  @ui-select-background-color: var(--ui-color-surface);
7
7
  @ui-input-background-color: var(--ui-color-bg);
8
+ @ui-typeahead-input-list-bg: var(--ui-color-bg);
9
+ @ui-typeahead-input-list-border-radius: var(--ui-border-radius-small);
@@ -0,0 +1,558 @@
1
+ <template>
2
+ <div class="component-ui-typeahead-input" ref="refEl" :class="classes">
3
+ <div class="component-ui-typeahead-input-grid">
4
+ <ui-input
5
+ ref="refComInput"
6
+
7
+ :modelValue="modelValue"
8
+ @update:modelValue="v => emit('update:modelValue', v)"
9
+
10
+ :input-id="inputId"
11
+ :error="error"
12
+ :name="name"
13
+ :required="required"
14
+ :disabled="disabled"
15
+ :placeholder="placeholder"
16
+ @input="handleInputTyping"
17
+ @focus="handleInputFocus(true)"
18
+ @blur="handleInputFocus(false)"
19
+ @keydown.down="selectFirstListItem"
20
+ @keydown.esc="handleInputEsc"
21
+ @enter="handleInputFocus(false)"
22
+
23
+ v-bind="inputAttrs"
24
+ >
25
+ <template #prepend>
26
+ <ui-button
27
+ variant="flat"
28
+ shape="square"
29
+ :color="color"
30
+
31
+ @click.stop="toggleList(true)"
32
+ @keydown.down.prevent="selectFirstListItem"
33
+
34
+ aria-hidden="true"
35
+ tabindex="-1"
36
+
37
+ :disabled="disabled"
38
+ >
39
+ <svg class="list-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="16" height="16"><path fill="currentColor" d="M1006.444 334.973 553.473 768.982c-15.17 12.8-29.393 18.014-41.481 18.014-12.089 0-28.373-5.26-39.324-15.852L17.563 334.973c-22.746-21.57-23.486-59.733-1.707-80.355 21.63-22.814 57.789-23.556 80.378-1.703l415.758 398.498 415.757-398.217c22.518-21.852 58.737-21.11 80.378 1.704 21.784 20.34 21.073 58.503-1.683 80.073z"/></svg>
40
+ </ui-button>
41
+ </template>
42
+ <template #append>
43
+ <ui-button
44
+ variant="flat"
45
+ shape="square"
46
+ :color="color"
47
+
48
+ @click.stop="emit('update:modelValue', '')"
49
+
50
+ :aria-label="dictionary['clear']"
51
+ :disabled="disabled"
52
+ >
53
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="14" height="14"><path fill="currentColor" d="M1008.882 1008.993c-20.007 20.01-52.377 20.01-72.406 0L511.992 584.366 87.41 1008.993c-20.007 20.01-52.377 20.01-72.406 0-20.007-20.009-20.007-52.382 0-72.414l424.677-424.531L15.069 87.421c-20.007-20.01-20.007-52.383 0-72.414 20.007-20.01 52.377-20.01 72.406 0L511.992 439.73l424.58-424.627c20.007-20.01 52.377-20.01 72.406 0 20.007 20.009 20.007 52.382 0 72.414L584.302 512.048l424.58 424.627c20.157 19.84 20.157 52.479 0 72.318z"/></svg>
54
+ </ui-button>
55
+ </template>
56
+ </ui-input>
57
+ <ui-button shape="default" :disabled="disabled" :color="color">{{ dictionary['submit'] }}</ui-button>
58
+ </div>
59
+
60
+ <Teleport to="body">
61
+ <div v-if="listIsShown" class="component-ui-typeahead-input-list" :style="listStyle" @click="handleListClick">
62
+ <!-- ♿ Element for screen reader users, to annotate the number of results found -->
63
+ <div
64
+ class="component-ui-typeahead-input-list--header"
65
+ :role="a11yAnnotation.length ? 'alert' : ''"
66
+ :key="a11yAnnotation"
67
+ >
68
+ {{ a11yAnnotation }}
69
+ </div>
70
+
71
+ <ul ref="refList" role="listbox">
72
+ <li
73
+ v-for="(item, index) of filteredList"
74
+ @click="handleListItemClick(item)"
75
+ role="option"
76
+ tabindex="0"
77
+ @keydown.down.prevent="focusNext(index)"
78
+ @keydown.up.prevent="focusPrev(index)"
79
+ @keydown.enter.prevent="handleListItemClick(item)"
80
+ @keydown.tab="handleListTab($event, index)"
81
+ @keydown.esc="handleListEsc"
82
+ >
83
+ {{ item.name }}
84
+ </li>
85
+ </ul>
86
+
87
+ <div class="component-ui-typeahead-input-list--footer">
88
+ <ui-button ref="refListBtnClose" @keydown.tab="handleListButtonTab" @click="listHide" variant="flat" block>{{ dictionary['close'] }}</ui-button>
89
+ </div>
90
+ </div>
91
+ </Teleport>
92
+ </div>
93
+ </template>
94
+
95
+ <script setup>
96
+ // Imports
97
+ import { ref, computed, watch, onMounted, onBeforeUnmount, useAttrs } from 'vue';
98
+ import normalizeString from './normalizeString';
99
+ import similarText from './similarText';
100
+
101
+ // Misc
102
+ const attrs = useAttrs();
103
+
104
+ // Data
105
+ const emit = defineEmits(['update:modelValue']);
106
+
107
+ const props = defineProps({
108
+ modelValue: {
109
+ required: true
110
+ },
111
+ inputId: {
112
+ default: 'typeahead',
113
+ },
114
+ name: {
115
+ default: 'typeahead',
116
+ },
117
+ placeholder: {
118
+ default: '',
119
+ },
120
+ disabled: {
121
+ type: Boolean,
122
+ default: false,
123
+ },
124
+ required: {
125
+ type: Boolean,
126
+ default: false,
127
+ },
128
+ error: {
129
+ type: Boolean,
130
+ default: false,
131
+ },
132
+ impulse: {
133
+ default: false
134
+ },
135
+ size: {
136
+ type: String,
137
+ default: 'medium'
138
+ },
139
+ dictionary: {
140
+ type: Object,
141
+ default() {
142
+ return {
143
+ list: 'View list',
144
+ clear: 'Clear input',
145
+ close: 'Close',
146
+ submit: 'Ok',
147
+ itemsFound: `Found {n} options`,
148
+ noItemsFound: `No items found`,
149
+ };
150
+ }
151
+ },
152
+
153
+ color: {
154
+ type: String,
155
+ default: 'primary'
156
+ },
157
+
158
+ options: {
159
+ type: Array,
160
+ required: true
161
+ }
162
+ });
163
+
164
+ const refComInput = ref(null);
165
+ const refEl = ref(null);
166
+ const refList = ref(null);
167
+ const refListBtnClose = ref(null);
168
+
169
+ let inputBlurTimeout;
170
+
171
+ const classes = computed(() => {
172
+ const result = [];
173
+
174
+ if (listIsShown.value) result.push('tag-list-active');
175
+
176
+ return result;
177
+ });
178
+
179
+ // List
180
+ const listIsShown = ref(false);
181
+ const listStyle = ref({});
182
+ const optionsPrepared = false;
183
+ const preparedOptions = ref([]);
184
+ const showFullList = ref(false);
185
+
186
+ const filteredList = computed(() => {
187
+ let result = [];
188
+ let query = showFullList.value ? '' : props.modelValue;
189
+ let options = preparedOptions.value;
190
+
191
+ query = normalizeString(query);
192
+
193
+ for (let item of options) {
194
+ // Looking for a query entry
195
+ const position = item.n.indexOf( query );
196
+
197
+ if (position > -1) {
198
+ result.push({
199
+ likenesses: 100, // Match percentage
200
+ position, // Word position in string
201
+ item
202
+ });
203
+ } else {
204
+ // We compare the query for similarity with each individual word
205
+ for (let j=0, position=0, likenesses; j < item.w.length; j++) {
206
+ likenesses = similarText(item.w[j], query, true);
207
+
208
+ // likenesses > {minimum likeness percent}
209
+ if (likenesses > 67) {
210
+ result.push({
211
+ likenesses,
212
+ position,
213
+ item
214
+ });
215
+ break;
216
+ }
217
+
218
+ position += item.w.length + 1; // 1 = space
219
+ }
220
+ }
221
+
222
+ }
223
+
224
+ // Sort by likenesses (more is better)
225
+ result.sort(function(a, b) { return b.likenesses - a.likenesses; });
226
+
227
+ // Sort by position (less is better)
228
+ result.sort(function(a, b) { return a.position - b.position; });
229
+
230
+ // Format result to match vue-select
231
+ return result.map(v => {
232
+ return v.item;
233
+ });
234
+ });
235
+
236
+
237
+ // 💬 Input data
238
+ const inputAttrs = {};
239
+ for (const key in attrs) {
240
+ if (key.startsWith('input.')) {
241
+ inputAttrs[key] = attrs[key];
242
+ }
243
+ }
244
+
245
+
246
+ // 💬 Input methods
247
+ function selectInput() {
248
+ refComInput.value.refInput.focus();
249
+ }
250
+
251
+ /** Handle input typing */
252
+ function handleInputTyping(v) {
253
+ if (v?.length) listShow();
254
+ }
255
+
256
+ /**
257
+ * Handle input focus
258
+ * @param {Boolean} haveFocus
259
+ */
260
+ function handleInputFocus(haveFocus) {
261
+ if (haveFocus) {
262
+ listShow();
263
+ } else {
264
+ clearTimeout(inputBlurTimeout);
265
+ inputBlurTimeout = setTimeout(listHide, 125);
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Handle input escape press
271
+ * - prevent default action
272
+ * - hide the list
273
+ * @param {Event} e
274
+ */
275
+ function handleInputEsc(e) {
276
+ e.preventDefault();
277
+ listHide();
278
+ }
279
+
280
+
281
+ // 📝 List methods
282
+
283
+ /**
284
+ * Toggle list
285
+ * @param {Boolean} full - show full list
286
+ */
287
+ function toggleList(full = false) {
288
+ if (listIsShown.value)
289
+ listHide();
290
+ else
291
+ listShow(full);
292
+ }
293
+
294
+ /**
295
+ * Select first list item
296
+ * @param {Event} e
297
+ */
298
+ function selectFirstListItem(e) {
299
+ // If list is active
300
+ if (refList.value) {
301
+ const li = refList.value.querySelector('li');
302
+
303
+ // If items exists
304
+ if (li) {
305
+ e.preventDefault();
306
+ li.focus();
307
+ preventListClose();
308
+ }
309
+ }
310
+ }
311
+
312
+ /** Focus prev list element, or focus input */
313
+ function focusPrev(index) {
314
+ if (index > 0) {
315
+ const li = refList.value.querySelectorAll('li');
316
+
317
+ li[index - 1].focus();
318
+ } else {
319
+ // If on first then return to <input />
320
+ selectInput();
321
+ }
322
+ }
323
+
324
+ /** Focus next list element */
325
+ function focusNext(index) {
326
+ const li = refList.value.querySelectorAll('li');
327
+
328
+ if (index < li.length - 1) li[index + 1].focus();
329
+ }
330
+
331
+ /**
332
+ * Handle list item click
333
+ * - update modelValue
334
+ * - hide the list
335
+ * - focus <input />
336
+ */
337
+ function handleListItemClick(item) {
338
+ emit('update:modelValue', `${item.name} (${item.value})`);
339
+ listHide();
340
+
341
+ selectInput();
342
+ }
343
+
344
+ function handleListClick(e) {
345
+ preventListClose();
346
+ }
347
+
348
+ /**
349
+ * Handle list tab press
350
+ * Shift + Tab on first -> focus <input />
351
+ * @param {Event} e
352
+ * @param {Number} index
353
+ */
354
+ function handleListTab(e, index) {
355
+ if (e.shiftKey && index === 0) {
356
+ e.preventDefault();
357
+ selectInput();
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Handle list escape press
363
+ * - select input
364
+ * - hide the list
365
+ */
366
+ function handleListEsc() {
367
+ selectInput();
368
+ listHide();
369
+ }
370
+
371
+ /**
372
+ * Show the list
373
+ * @param {boolean} full - show full list
374
+ */
375
+ function listShow(full = false) {
376
+ if (listIsShown.value && showFullList.value == full) return;
377
+
378
+ preventListClose();
379
+
380
+ if (!optionsPrepared) prepareOptions();
381
+
382
+ // Position list
383
+ const rect = refEl.value.getBoundingClientRect();
384
+
385
+ const top = rect.top + window.scrollY;
386
+ const left = rect.left + window.scrollX;
387
+ const width = rect.width;
388
+ const height = rect.height;
389
+
390
+ listStyle.value = {
391
+ top: top + height + 'px',
392
+ left: left + 'px',
393
+ width: width + 'px'
394
+ };
395
+
396
+ showFullList.value = full;
397
+ listIsShown.value = true;
398
+ }
399
+
400
+ /** Hide the list */
401
+ function listHide() {
402
+ if (listIsShown.value) listIsShown.value = false;
403
+ }
404
+
405
+ /** Prevent list close */
406
+ function preventListClose() {
407
+ clearTimeout(inputBlurTimeout);
408
+ }
409
+
410
+ /**
411
+ * Handle tab click while focus list close button
412
+ * @param {Event} e
413
+ */
414
+ function handleListButtonTab(e) {
415
+ if (!e.shiftKey) e.preventDefault();
416
+ }
417
+
418
+ /** Prepare list options */
419
+ function prepareOptions() {
420
+ const result = [];
421
+
422
+ for (let item of props.options) {
423
+ const n = normalizeString(item.name);
424
+ const o = {
425
+ value: item.value,
426
+ name: item.name,
427
+ n,
428
+ w: n.replace(',', '').split(' ')
429
+ };
430
+
431
+ result.push(o);
432
+ }
433
+
434
+ preparedOptions.value = result;
435
+ }
436
+
437
+ // ♿ Annotation for screen reader users, to annotate the number of results found
438
+ const a11yAnnotation = ref('');
439
+ let tA11y;
440
+
441
+ watch(filteredList, (v) => {
442
+ clearTimeout(tA11y);
443
+ tA11y = setTimeout(() => {
444
+ a11yAnnotation.value = v.length ? props.dictionary['itemsFound'].replace('{n}', v.length) : props.dictionary['noItemsFound'];
445
+ }, 500);
446
+ });
447
+
448
+ // 🖱️ Handle click outside
449
+ function handleClickOutside(e) {
450
+ const el1 = e.target.closest('.component-ui-typeahead-input-list');
451
+ const el2 = e.target.closest('.component-ui-typeahead-input');
452
+
453
+ if (!el1 && !el2) listHide();
454
+ }
455
+ onMounted(() => {
456
+ document.addEventListener('click', handleClickOutside);
457
+ });
458
+ onBeforeUnmount(() => {
459
+ document.removeEventListener('click', handleClickOutside);
460
+ });
461
+ </script>
462
+
463
+ <style lang="less">
464
+ @import '../styles/components.less';
465
+
466
+ @com-text-small: var(--ui-text-small);
467
+
468
+ @com-input-height-default: var(--ui-input-height-default);
469
+
470
+ @com-color-primary: var(--ui-color-primary);
471
+ @com-color-text-on-primary: var(--ui-color-text-on-primary);
472
+ @com-color-surface: @ui-typeahead-input-list-bg;
473
+ @com-color-border: var(--ui-color-border);
474
+
475
+ @com-space-mini: var(--ui-space-mini);
476
+ @com-space-micro: var(--ui-space-micro);
477
+
478
+ @com-border-radius: @ui-typeahead-input-list-border-radius;
479
+
480
+ .component-ui-typeahead-input {
481
+ .list-icon {
482
+ transition: all var(--ui-ani-time) var(--ui-ani-ease);
483
+ }
484
+
485
+ &.tag-list-active {
486
+ .list-icon {
487
+ transform: rotate(180deg);
488
+ }
489
+ }
490
+ }
491
+
492
+ .component-ui-typeahead-input-grid {
493
+
494
+
495
+ display: grid;
496
+ grid-template-columns: auto 50px;
497
+ gap: @com-space-micro;
498
+
499
+
500
+ input:focus-visible {
501
+ outline: unset;
502
+ }
503
+
504
+ .component-ui-input .component-ui-button {
505
+ padding: 0;
506
+ width: @com-input-height-default;
507
+ height: 100%;
508
+ font-size: @com-text-small;
509
+
510
+ .slot-default {
511
+ position: relative;
512
+ display: grid;
513
+ place-items: center;
514
+ height: 100%;
515
+ }
516
+
517
+ &:focus-visible {
518
+ outline-offset: -8px;
519
+ }
520
+ }
521
+ }
522
+
523
+ .component-ui-typeahead-input-list {
524
+ z-index: 999;
525
+
526
+ position: absolute;
527
+ margin-top: 5px;
528
+ background: @com-color-surface;
529
+ border: 1px solid @com-color-border;
530
+ border-radius: @com-border-radius;
531
+
532
+ ul {
533
+ overflow-x: hidden;
534
+ overflow-y: auto;
535
+ max-height: 400px;
536
+ }
537
+
538
+ li {
539
+ padding: @com-space-micro @com-space-mini;
540
+ cursor: pointer;
541
+
542
+ &:hover,
543
+ &:focus-visible {
544
+ color: @com-color-text-on-primary;
545
+ background: @com-color-primary;
546
+ }
547
+ }
548
+
549
+ .component-ui-typeahead-input-list--header {
550
+ text-align: center;
551
+ padding: @com-space-mini;
552
+ }
553
+
554
+ .component-ui-typeahead-input-list--footer {
555
+ padding: @com-space-mini;
556
+ }
557
+ }
558
+ </style>
@@ -0,0 +1,2 @@
1
+ declare function _default(s: string): string;
2
+ export default _default;
@@ -0,0 +1,32 @@
1
+ /** Normalize string, replace letters with extra symbols, convert to lower case
2
+ * @param {string} s
3
+ * @returns {string}
4
+ */
5
+ export default (s) => {
6
+ if (!s) return '';
7
+
8
+ const arChars = [
9
+ [['ā', 'à', 'á', 'â', 'ã', 'ä', 'å'], 'a'],
10
+ [['č'], 'c'],
11
+ [['ē'], 'e'],
12
+ [['ģ'], 'g'],
13
+ [['ī'], 'i'],
14
+ [['ķ'], 'k'],
15
+ [['ļ'], 'l'],
16
+ [['ņ'], 'n'],
17
+ [['š'], 's'],
18
+ [['ū'], 'u'],
19
+ [['ž'], 'z'],
20
+ [['!', '\\(', '\\)', '\\[', '\\]', '/'], ''],
21
+ ];
22
+
23
+ s = s.toLocaleLowerCase();
24
+
25
+ for (let item of arChars) {
26
+ for (let letterToReplace of item[0]) {
27
+ s = s.replace( new RegExp(letterToReplace, 'g'), item[1] );
28
+ }
29
+ }
30
+
31
+ return s.replace(/\s{2,}/g, '').trim();
32
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Compares two strings for similarity (PHP's similar_text algorithm).
3
+ *
4
+ * The algorithm finds the longest common substring between the two strings
5
+ * and then recursively compares the parts to the left and right of the match.
6
+ *
7
+ * ⚠️ Complexity is O(n³), which may be slow for very long strings, but accurate.
8
+ *
9
+ * @param {string} first - The first string to compare.
10
+ * @param {string} second - The second string to compare.
11
+ * @param {boolean} [percent=false] - If true, returns the similarity as a percentage (0–100).
12
+ * If false or omitted, returns the number of matching characters.
13
+ * @returns {number} The count of matching characters or the similarity percentage.
14
+ *
15
+ * @example
16
+ * // Returns the number of matching characters
17
+ * similarText("hello", "hallo"); // 4
18
+ *
19
+ * @example
20
+ * // Returns the similarity percentage
21
+ * similarText("hello", "hallo", true); // ~80
22
+ */
23
+ export default function similarText(first: string, second: string, returnPercent?: boolean): number;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Compares two strings for similarity (PHP's similar_text algorithm).
3
+ *
4
+ * The algorithm finds the longest common substring between the two strings
5
+ * and then recursively compares the parts to the left and right of the match.
6
+ *
7
+ * ⚠️ Complexity is O(n³), which may be slow for very long strings, but accurate.
8
+ *
9
+ * @param {string} first - The first string to compare.
10
+ * @param {string} second - The second string to compare.
11
+ * @param {boolean} [percent=false] - If true, returns the similarity as a percentage (0–100).
12
+ * If false or omitted, returns the number of matching characters.
13
+ * @returns {number} The count of matching characters or the similarity percentage.
14
+ *
15
+ * @example
16
+ * // Returns the number of matching characters
17
+ * similarText("hello", "hallo"); // 4
18
+ *
19
+ * @example
20
+ * // Returns the similarity percentage
21
+ * similarText("hello", "hallo", true); // ~80
22
+ */
23
+ export default function similarText(first, second, returnPercent = false) {
24
+ if (first == null || second == null) {
25
+ return 0;
26
+ }
27
+
28
+ first = String(first);
29
+ second = String(second);
30
+
31
+ let pos1 = 0, pos2 = 0, max = 0;
32
+ const firstLength = first.length;
33
+ const secondLength = second.length;
34
+
35
+ for (let p = 0; p < firstLength; p++) {
36
+ for (let q = 0; q < secondLength; q++) {
37
+ let l = 0;
38
+ while (
39
+ p + l < firstLength &&
40
+ q + l < secondLength &&
41
+ first.charAt(p + l) === second.charAt(q + l)
42
+ ) {
43
+ l++;
44
+ }
45
+ if (l > max) {
46
+ max = l;
47
+ pos1 = p;
48
+ pos2 = q;
49
+ }
50
+ }
51
+ }
52
+
53
+ let sum = max;
54
+
55
+ if (sum > 0) {
56
+ if (pos1 > 0 && pos2 > 0) {
57
+ sum += similarText(first.slice(0, pos1), second.slice(0, pos2));
58
+ }
59
+ if (pos1 + max < firstLength && pos2 + max < secondLength) {
60
+ sum += similarText(
61
+ first.slice(pos1 + max),
62
+ second.slice(pos2 + max)
63
+ );
64
+ }
65
+ }
66
+
67
+ return returnPercent ? (sum * 200) / (firstLength + secondLength) : sum;
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@community-release/nx-ui",
3
- "version": "0.0.57",
3
+ "version": "0.0.59",
4
4
  "packageManager": "pnpm@10.14.0",
5
5
  "description": "nx-ui - Nuxt UI library",
6
6
  "repository": {