@tekkare/romulus 0.1.87 → 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.86",
7
+ "version": "0.1.88",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -12,6 +12,12 @@
12
12
  et `ph-file-csv` ne se confondent pas en noir et blanc. Elle vient des tokens
13
13
  du theme et non d'un hexadecimal en dur, pour suivre le mode nuit.
14
14
 
15
+ Elle passe par la prop `color` de RmIcon et non par une classe : le style
16
+ scope de ce fichier n'atteint pas le `<svg>` que RmIcon monte derriere son
17
+ `<ClientOnly>`, et l'icone retombait sur la couleur du texte. Les classes
18
+ posees sur RmIcon ne servent donc qu'a la mise en place, jamais a la
19
+ couleur.
20
+
15
21
  Le composant ne navigue pas et n'ouvre rien lui-meme : il emet `select`, la
16
22
  page decide (un apercu dans un tiroir, un onglet, un telechargement).
17
23
  -->
@@ -46,18 +52,18 @@ const emit = defineEmits<{ select: [] }>()
46
52
  * neutre dit « document » sans se tromper, ce qu'une icone approchante ne
47
53
  * ferait pas.
48
54
  */
49
- const FORMATS: { match: string[]; icon: string; tone: string }[] = [
50
- { match: ['pdf'], icon: 'FilePdf', tone: 'pdf' },
51
- { match: ['csv'], icon: 'FileCsv', tone: 'csv' },
52
- { match: ['sheet', 'excel', 'xls'], icon: 'FileXls', tone: 'sheet' },
53
- { match: ['word', 'doc'], icon: 'FileDoc', tone: 'doc' },
55
+ const FORMATS: { match: string[]; icon: string; color: string }[] = [
56
+ { match: ['pdf'], icon: 'FilePdf', color: 'var(--color-danger)' },
57
+ { match: ['csv'], icon: 'FileCsv', color: 'var(--color-info)' },
58
+ { match: ['sheet', 'excel', 'xls'], icon: 'FileXls', color: 'var(--color-success)' },
59
+ { match: ['word', 'doc'], icon: 'FileDoc', color: 'var(--color-primary)' },
54
60
  ]
55
61
 
56
62
  const format = computed(() => {
57
63
  const value = props.format.toLowerCase()
58
64
  return (
59
65
  FORMATS.find(entry => entry.match.some(word => value.includes(word)))
60
- ?? { icon: 'FileText', tone: 'plain' }
66
+ ?? { icon: 'FileText', color: 'var(--text-muted)' }
61
67
  )
62
68
  })
63
69
 
@@ -70,23 +76,17 @@ const ACTION_ICONS: Record<string, string> = {
70
76
 
71
77
  <template>
72
78
  <button type="button" class="rm-file-row" @click="emit('select')">
73
- <RmIcon
74
- class="rm-file-row__icon"
75
- :class="`rm-file-row__icon--${format.tone}`"
76
- :name="format.icon"
77
- :size="16"
78
- />
79
+ <span class="rm-file-row__icon">
80
+ <RmIcon :name="format.icon" :size="16" :color="format.color" />
81
+ </span>
79
82
  <span class="rm-file-row__name">{{ name }}</span>
80
83
  <span v-if="size" class="rm-file-row__size">{{ size }}</span>
81
- <RmIcon
82
- v-if="action !== 'none'"
83
- class="rm-file-row__action"
84
- :name="ACTION_ICONS[action]"
85
- :size="14"
86
- />
84
+ <span v-if="action !== 'none'" class="rm-file-row__action">
85
+ <RmIcon :name="ACTION_ICONS[action]" :size="14" color="var(--text-muted)" />
86
+ </span>
87
87
  </button>
88
88
  </template>
89
89
 
90
90
  <style scoped>
91
- .rm-file-row{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-primary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-13);gap:8px;line-height:1.4;padding:8px;text-align:left;transition:background-color var(--duration-fast) linear;width:100%}.rm-file-row:hover{background:var(--bg-surface-2)}.rm-file-row__icon{color:var(--text-muted);flex:none}.rm-file-row__icon--pdf{color:var(--color-danger)}.rm-file-row__icon--csv{color:var(--color-info)}.rm-file-row__icon--sheet{color:var(--color-success)}.rm-file-row__icon--doc{color:var(--color-primary)}.rm-file-row__name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-file-row__size{font-size:var(--font-12)}.rm-file-row__action,.rm-file-row__size{color:var(--text-muted);flex:none}
91
+ .rm-file-row{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-primary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-13);gap:8px;line-height:1.4;padding:8px;text-align:left;transition:background-color var(--duration-fast) linear;width:100%}.rm-file-row:hover{background:var(--bg-surface-2)}.rm-file-row__action,.rm-file-row__icon{display:inline-flex;flex:none}.rm-file-row__name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-file-row__size{color:var(--text-muted);flex:none;font-size:var(--font-12)}
92
92
  </style>
@@ -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.87",
3
+ "version": "0.1.89",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",