@tekkare/romulus 0.1.88 → 0.1.89

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.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "0.1.87",
7
+ "version": "0.1.88",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -5,6 +5,12 @@ import { PhMagnifyingGlass, PhX } from '@phosphor-icons/vue'
5
5
  export interface SearchSuggestion {
6
6
  label: string
7
7
  field?: string
8
+ /**
9
+ * Icone Phosphor posee devant la suggestion, a la place de la loupe. Elle
10
+ * dit de quel champ la valeur vient (une marque, une molecule, un
11
+ * laboratoire) : sans elle, dix suggestions se ressemblent toutes.
12
+ */
13
+ icon?: string
8
14
  }
9
15
 
10
16
  const model = defineModel<string>({ default: '' })
@@ -22,6 +28,22 @@ const props = withDefaults(defineProps<{
22
28
  haveAdvanced?: boolean
23
29
  /** Libelle du lien, pour les pages qui ne sont pas en anglais. */
24
30
  advancedLabel?: string
31
+ /**
32
+ * Met en evidence la part saisie dans chaque suggestion. Le composant la
33
+ * decoupe lui-meme a partir du texte tape : la page n'a pas a fabriquer du
34
+ * HTML, et la couleur reste celle du theme.
35
+ */
36
+ highlightMatch?: boolean
37
+ /**
38
+ * Les suggestions sont en cours de chargement. La liste s'ouvre sur une
39
+ * ligne d'attente plutot que de rester fermee : une recherche qui met une
40
+ * seconde a repondre ne doit pas se lire comme une recherche sans resultat.
41
+ */
42
+ loading?: boolean
43
+ /** Ligne affichee pendant le chargement des suggestions. */
44
+ loadingText?: string
45
+ /** Ligne affichee quand la recherche ne ramene aucune suggestion. */
46
+ emptyText?: string
25
47
  }>(), {
26
48
  advancedActive: false,
27
49
  advancedCount: 0,
@@ -29,6 +51,10 @@ const props = withDefaults(defineProps<{
29
51
  placeholder: 'Search...',
30
52
  haveAdvanced: true,
31
53
  advancedLabel: 'Advanced search',
54
+ highlightMatch: false,
55
+ loading: false,
56
+ loadingText: 'Searching…',
57
+ emptyText: '',
32
58
  })
33
59
 
34
60
  const emit = defineEmits<{
@@ -72,6 +98,23 @@ function clearSearch() {
72
98
  emit('search', '')
73
99
  }
74
100
 
101
+ /**
102
+ * Le libelle decoupe autour de la part saisie : avant, la correspondance,
103
+ * apres. Un `v-html` ferait le meme rendu, mais poserait du HTML fabrique par
104
+ * la page dans la liste ; trois morceaux de texte ne le posent pas.
105
+ */
106
+ function parts(label: string) {
107
+ const query = model.value.trim()
108
+ if (!props.highlightMatch || !query) return [{ text: label, match: false }]
109
+ const at = label.toLowerCase().indexOf(query.toLowerCase())
110
+ if (at < 0) return [{ text: label, match: false }]
111
+ return [
112
+ { text: label.slice(0, at), match: false },
113
+ { text: label.slice(at, at + query.length), match: true },
114
+ { text: label.slice(at + query.length), match: false },
115
+ ].filter(part => part.text)
116
+ }
117
+
75
118
  function onClickOutside(e: Event) {
76
119
  if (barRef.value && !barRef.value.contains(e.target as Node)) {
77
120
  showSuggestions.value = false
@@ -113,22 +156,39 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
113
156
  </div>
114
157
 
115
158
  <Transition name="suggestions">
116
- <div v-if="showSuggestions && suggestions.length > 0" class="search-bar__suggestions">
117
- <button
118
- v-for="(sugg, i) in suggestions"
119
- :key="i"
120
- class="search-bar__suggestion"
121
- @click="selectSuggestion(sugg)"
122
- >
123
- <PhMagnifyingGlass :size="14" class="search-bar__suggestion-icon" />
124
- <span class="search-bar__suggestion-label">{{ sugg.label }}</span>
125
- <span v-if="sugg.field" class="search-bar__suggestion-field">{{ sugg.field.split('.').pop() }}</span>
126
- </button>
159
+ <div v-if="showSuggestions && (suggestions.length > 0 || loading || emptyText)" class="search-bar__suggestions">
160
+ <p v-if="loading" class="search-bar__suggestion-note">{{ loadingText }}</p>
161
+ <template v-else-if="suggestions.length > 0">
162
+ <button
163
+ v-for="(sugg, i) in suggestions"
164
+ :key="i"
165
+ class="search-bar__suggestion"
166
+ @click="selectSuggestion(sugg)"
167
+ >
168
+ <RmIcon
169
+ v-if="sugg.icon"
170
+ class="search-bar__suggestion-icon"
171
+ :name="sugg.icon"
172
+ :size="14"
173
+ color="var(--text-muted)"
174
+ />
175
+ <PhMagnifyingGlass v-else :size="14" class="search-bar__suggestion-icon" />
176
+ <span class="search-bar__suggestion-label">
177
+ <span
178
+ v-for="(part, index) in parts(sugg.label)"
179
+ :key="index"
180
+ :class="{ 'search-bar__suggestion-match': part.match }"
181
+ >{{ part.text }}</span>
182
+ </span>
183
+ <span v-if="sugg.field" class="search-bar__suggestion-field">{{ sugg.field.split('.').pop() }}</span>
184
+ </button>
185
+ </template>
186
+ <p v-else class="search-bar__suggestion-note">{{ emptyText }}</p>
127
187
  </div>
128
188
  </Transition>
129
189
  </div>
130
190
  </template>
131
191
 
132
192
  <style scoped>
133
- .search-bar-wrapper{position:relative}.search-bar{align-items:center;border:1px solid var(--border-default);border-radius:var(--radius-pill);display:inline-flex;gap:var(--space-8);height:32px;min-width:200px;padding:var(--space-4) var(--space-12);transition:border-color var(--duration-fast) var(--ease-in-out)}.search-bar:focus-within{border-color:var(--color-secondary);box-shadow:var(--shadow-focus)}.search-bar__icon{color:var(--text-muted);flex-shrink:0}.search-bar__input{background:transparent;border:none;color:var(--text-secondary);font-family:var(--font-sans);font-size:var(--font-14);outline:none;width:100%}.search-bar__input::-moz-placeholder{color:var(--text-muted)}.search-bar__input::placeholder{color:var(--text-muted)}.search-bar__clear{align-items:center;border:none;border-radius:50%;cursor:pointer;display:flex;flex-shrink:0;height:18px;justify-content:center;transition:all var(--duration-fast) var(--ease-in-out);width:18px}.search-bar__clear,.search-bar__clear:hover{background:var(--border-default);color:var(--text-secondary)}.search-bar__advanced{background:none;border:none;border-left:1px solid var(--border-default);color:var(--text-secondary);cursor:pointer;flex-shrink:0;font-family:var(--font-sans);font-size:var(--font-14);padding:0 0 0 var(--space-8);text-decoration:underline;transition:color var(--duration-fast) var(--ease-in-out);white-space:nowrap}.search-bar__advanced:hover{color:var(--color-healthcare)}.search-bar--advanced-active{border-color:var(--color-healthcare)}.search-bar__advanced--active{color:var(--color-healthcare);font-weight:500}.search-bar__advanced-badge{align-items:center;background-color:var(--color-healthcare);border-radius:100px;color:var(--color-text-inverse);display:inline-flex;font-size:10px;font-weight:600;height:16px;justify-content:center;line-height:1;min-width:16px;padding:0 4px}.search-bar__suggestions{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:8px;box-shadow:0 4px 12px rgba(var(--shadow-rgb),.08);left:0;max-height:280px;overflow-y:auto;padding:4px;position:absolute;right:0;top:calc(100% + 4px);z-index:50}.search-bar__suggestion{align-items:center;background:none;border:none;border-radius:4px;color:var(--text-secondary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);gap:8px;padding:8px 10px;text-align:left;transition:background-color var(--duration-fast) var(--ease-in-out);width:100%}.search-bar__suggestion:hover{background-color:var(--bg-surface-2)}.search-bar__suggestion-icon{color:var(--text-muted);flex-shrink:0}.search-bar__suggestion-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.search-bar__suggestion-field{background:var(--bg-surface-2);border-radius:3px;color:var(--text-muted);flex-shrink:0;font-size:11px;padding:1px 6px}.suggestions-enter-active,.suggestions-leave-active{transition:opacity var(--duration-fast) var(--ease-in-out),transform var(--duration-fast) var(--ease-in-out)}.suggestions-enter-from,.suggestions-leave-to{opacity:0;transform:translateY(-4px)}
193
+ .search-bar__suggestion-match{color:var(--color-secondary);font-weight:var(--weight-bold)}.search-bar__suggestion-note{color:var(--text-muted);font-size:var(--font-13);margin:0;padding:var(--space-8) var(--space-12)}.search-bar-wrapper{position:relative}.search-bar{align-items:center;border:1px solid var(--border-default);border-radius:var(--radius-pill);display:inline-flex;gap:var(--space-8);height:32px;min-width:200px;padding:var(--space-4) var(--space-12);transition:border-color var(--duration-fast) var(--ease-in-out)}.search-bar:focus-within{border-color:var(--color-secondary);box-shadow:var(--shadow-focus)}.search-bar__icon{color:var(--text-muted);flex-shrink:0}.search-bar__input{background:transparent;border:none;color:var(--text-secondary);font-family:var(--font-sans);font-size:var(--font-14);outline:none;width:100%}.search-bar__input::-moz-placeholder{color:var(--text-muted)}.search-bar__input::placeholder{color:var(--text-muted)}.search-bar__clear{align-items:center;border:none;border-radius:50%;cursor:pointer;display:flex;flex-shrink:0;height:18px;justify-content:center;transition:all var(--duration-fast) var(--ease-in-out);width:18px}.search-bar__clear,.search-bar__clear:hover{background:var(--border-default);color:var(--text-secondary)}.search-bar__advanced{background:none;border:none;border-left:1px solid var(--border-default);color:var(--text-secondary);cursor:pointer;flex-shrink:0;font-family:var(--font-sans);font-size:var(--font-14);padding:0 0 0 var(--space-8);text-decoration:underline;transition:color var(--duration-fast) var(--ease-in-out);white-space:nowrap}.search-bar__advanced:hover{color:var(--color-healthcare)}.search-bar--advanced-active{border-color:var(--color-healthcare)}.search-bar__advanced--active{color:var(--color-healthcare);font-weight:500}.search-bar__advanced-badge{align-items:center;background-color:var(--color-healthcare);border-radius:100px;color:var(--color-text-inverse);display:inline-flex;font-size:10px;font-weight:600;height:16px;justify-content:center;line-height:1;min-width:16px;padding:0 4px}.search-bar__suggestions{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:8px;box-shadow:0 4px 12px rgba(var(--shadow-rgb),.08);left:0;max-height:280px;overflow-y:auto;padding:4px;position:absolute;right:0;top:calc(100% + 4px);z-index:50}.search-bar__suggestion{align-items:center;background:none;border:none;border-radius:4px;color:var(--text-secondary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);gap:8px;padding:8px 10px;text-align:left;transition:background-color var(--duration-fast) var(--ease-in-out);width:100%}.search-bar__suggestion:hover{background-color:var(--bg-surface-2)}.search-bar__suggestion-icon{color:var(--text-muted);flex-shrink:0}.search-bar__suggestion-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.search-bar__suggestion-field{background:var(--bg-surface-2);border-radius:3px;color:var(--text-muted);flex-shrink:0;font-size:11px;padding:1px 6px}.suggestions-enter-active,.suggestions-leave-active{transition:opacity var(--duration-fast) var(--ease-in-out),transform var(--duration-fast) var(--ease-in-out)}.suggestions-enter-from,.suggestions-leave-to{opacity:0;transform:translateY(-4px)}
134
194
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.88",
3
+ "version": "0.1.89",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",