@tekkare/romulus 0.1.223 → 0.1.225

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.222",
7
+ "version": "0.1.224",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
3
- import { PhCaretDown, PhCheck, PhMagnifyingGlass } from '@phosphor-icons/vue'
3
+ import { PhCaretDown, PhCheck, PhMagnifyingGlass, PhPlus } from '@phosphor-icons/vue'
4
4
  import type { Component } from 'vue'
5
5
  import { rmPanelOpened, rmPanelRegister } from '../utils/panels'
6
6
 
@@ -37,8 +37,23 @@ const props = withDefaults(defineProps<{
37
37
  searchPlaceholder?: string
38
38
  /** Langue des textes propres au composant (tout selectionner, aucun resultat). */
39
39
  lang?: 'fr' | 'en'
40
+ /**
41
+ * La saisie de recherche peut devenir une valeur nouvelle (des tags) : une
42
+ * ligne « Creer « … » » apparait quand aucune option ne porte exactement ce
43
+ * libelle. La valeur est ajoutee a la selection et `create` previent
44
+ * l'appelant, qui l'ajoute a ses options. Implique la recherche.
45
+ */
46
+ creatable?: boolean
47
+ /**
48
+ * `chip` : la puce de la barre de filtres. `add` : un simple « + » a la
49
+ * hauteur d'une RmTag, pose au bout d'une rangee d'etiquettes (les tags
50
+ * d'une page) ; `label` devient son intitule accessible.
51
+ */
52
+ trigger?: 'chip' | 'add'
40
53
  }>(), {
41
54
  icon: undefined,
55
+ creatable: false,
56
+ trigger: 'chip',
42
57
  multiple: true,
43
58
  showSelectAll: false,
44
59
  searchable: 'auto',
@@ -47,8 +62,10 @@ const props = withDefaults(defineProps<{
47
62
  })
48
63
 
49
64
  const texts = computed(() => props.lang === 'en'
50
- ? { selectAll: 'Select all', deselectAll: 'Deselect all', noMatch: 'No match' }
51
- : { selectAll: 'Tout sélectionner', deselectAll: 'Tout désélectionner', noMatch: 'Aucun résultat' })
65
+ ? { selectAll: 'Select all', deselectAll: 'Deselect all', noMatch: 'No match', create: (q: string) => `Create « ${q} »` }
66
+ : { selectAll: 'Tout sélectionner', deselectAll: 'Tout désélectionner', noMatch: 'Aucun résultat', create: (q: string) => `Créer « ${q} »` })
67
+
68
+ const emit = defineEmits<{ create: [value: string] }>()
52
69
 
53
70
  const selected = defineModel<string[]>({ default: () => [] })
54
71
 
@@ -80,9 +97,27 @@ const displayLabel = computed(() => {
80
97
  })
81
98
 
82
99
  const hasSearch = computed(() =>
83
- props.searchable === 'auto' ? props.options.length > SEARCH_THRESHOLD : props.searchable,
100
+ props.creatable || (props.searchable === 'auto' ? props.options.length > SEARCH_THRESHOLD : props.searchable),
84
101
  )
85
102
 
103
+ /** La saisie, quand elle peut devenir une option nouvelle. */
104
+ const creatableValue = computed(() => {
105
+ const q = search.value.trim()
106
+ if (!props.creatable || !q) return ''
107
+ const exists = props.options.some(o => o.label.toLowerCase() === q.toLowerCase())
108
+ return exists ? '' : q
109
+ })
110
+
111
+ function createOption() {
112
+ const value = creatableValue.value
113
+ if (!value) return
114
+ hasUserChanged.value = true
115
+ emit('create', value)
116
+ selected.value = props.multiple ? [...selected.value, value] : [value]
117
+ search.value = ''
118
+ if (!props.multiple) isOpen.value = false
119
+ }
120
+
86
121
  const filteredOptions = computed(() => {
87
122
  if (!hasSearch.value || !search.value.trim()) return props.options
88
123
  const needle = search.value.trim().toLowerCase()
@@ -176,8 +211,21 @@ onUnmounted(() => {
176
211
  </script>
177
212
 
178
213
  <template>
179
- <div ref="chipRef" class="filter-chip-wrapper">
214
+ <div ref="chipRef" class="filter-chip-wrapper" :class="{ 'filter-chip-wrapper--add': trigger === 'add' }">
180
215
  <button
216
+ v-if="trigger === 'add'"
217
+ type="button"
218
+ class="filter-chip__add"
219
+ :class="{ 'filter-chip__add--open': isOpen }"
220
+ :aria-label="label"
221
+ :title="label"
222
+ :aria-expanded="isOpen"
223
+ @click="toggle"
224
+ >
225
+ <PhPlus :size="12" weight="bold" />
226
+ </button>
227
+ <button
228
+ v-else
181
229
  class="filter-chip"
182
230
  :class="{ 'filter-chip--active': isActive }"
183
231
  @click="toggle"
@@ -204,6 +252,7 @@ onUnmounted(() => {
204
252
  type="search"
205
253
  :placeholder="searchPlaceholder"
206
254
  @click.stop
255
+ @keydown.enter.prevent="createOption"
207
256
  />
208
257
  </label>
209
258
  <button
@@ -217,7 +266,16 @@ onUnmounted(() => {
217
266
  <span>{{ allSelected ? texts.deselectAll : texts.selectAll }}</span>
218
267
  </button>
219
268
  <div :class="{ 'filter-chip__options-group': showSelectAll && multiple && options.length > 0 }">
220
- <p v-if="hasSearch && filteredOptions.length === 0" class="filter-chip__no-match">
269
+ <button
270
+ v-if="creatableValue"
271
+ type="button"
272
+ class="filter-chip__option filter-chip__create"
273
+ @click.stop="createOption"
274
+ >
275
+ <PhPlus :size="12" weight="bold" />
276
+ <span>{{ texts.create(creatableValue) }}</span>
277
+ </button>
278
+ <p v-if="hasSearch && filteredOptions.length === 0 && !creatableValue" class="filter-chip__no-match">
221
279
  {{ texts.noMatch }}
222
280
  </p>
223
281
  <template v-for="row in rows" :key="row.key">
@@ -248,5 +306,5 @@ onUnmounted(() => {
248
306
  </template>
249
307
 
250
308
  <style scoped>
251
- .filter-chip-wrapper{position:relative}.filter-chip{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:6px;box-sizing:border-box;color:var(--text-secondary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);font-weight:var(--weight-medium);gap:var(--space-8);height:32px;padding:var(--space-8);transition:all var(--duration-fast) var(--ease-in-out);white-space:nowrap;width:193px}.filter-chip:hover{border-color:var(--border-default)}.filter-chip__icon{color:var(--color-healthcare);flex-shrink:0}.filter-chip--active{border-color:var(--color-healthcare);box-shadow:0 0 8px 0 rgba(var(--healthcare-rgb),.1)}.filter-chip__label{flex:1;min-width:0;overflow:hidden;text-align:left;text-overflow:ellipsis;white-space:nowrap}.filter-chip__badge{align-items:center;background-color:var(--color-healthcare);border-radius:100px;color:var(--color-text-inverse);display:inline-flex;flex-shrink:0;font-size:var(--font-11);font-weight:600;height:18px;justify-content:center;line-height:1;min-width:18px;padding:0 5px}.filter-chip__caret{color:var(--text-muted);flex-shrink:0;transition:transform var(--duration-fast) var(--ease-in-out)}.filter-chip__caret--open{transform:rotate(180deg)}.filter-chip__dropdown{background-color:var(--bg-surface);border:1px solid var(--border-default);border-radius:6px;box-shadow:0 4px 12px rgba(var(--shadow-rgb),.08),0 2px 4px rgba(var(--shadow-rgb),.04);left:0;max-height:300px;max-width:420px;min-width:380px;overflow-y:auto;padding:var(--space-4);position:absolute;top:calc(100% + 4px);width:-moz-max-content;width:max-content;z-index:50}.filter-chip__search{align-items:center;background-color:var(--bg-surface);border-bottom:1px solid var(--border-soft);color:var(--text-muted);display:flex;gap:var(--space-6);margin:-4px -4px 4px;padding:var(--space-8);position:sticky;top:-4px;z-index:1}.filter-chip__search input{background:none;border:none;color:var(--text-primary);flex:1;font-family:var(--font-sans);font-size:var(--font-14);min-width:0;outline:none}.filter-chip__search input::-webkit-search-cancel-button{cursor:pointer}.filter-chip__no-match{font-size:var(--font-14);margin:0;padding:var(--space-10) var(--space-8)}.filter-chip__no-match,.filter-chip__section{color:var(--text-muted);font-family:var(--font-sans)}.filter-chip__section{border-top:1px solid var(--border-default);font-size:var(--font-12);font-weight:var(--weight-bold);letter-spacing:.04em;margin:8px 0 2px;padding:var(--space-8) var(--space-8) 0;text-transform:uppercase}.filter-chip__section:first-child{border-top:none;margin-top:0;padding-top:0}.filter-chip__option{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:var(--space-8);padding:var(--space-6) var(--space-8);text-align:left;transition:background-color var(--duration-fast) var(--ease-in-out);width:100%}.filter-chip__option:hover{background-color:var(--bg-surface-2)}.filter-chip__option--selected{color:var(--text-primary);font-weight:var(--weight-medium)}.filter-chip__checkbox{align-items:center;border:1.5px solid var(--border-default);border-radius:3px;display:flex;flex-shrink:0;height:16px;justify-content:center;transition:all var(--duration-fast) var(--ease-in-out);width:16px}.filter-chip__checkbox--checked{background-color:var(--color-healthcare);border-color:var(--color-healthcare);color:var(--color-text-inverse)}.filter-chip__radio{align-items:center;border:1.5px solid var(--border-default);border-radius:100%;display:flex;flex-shrink:0;height:16px;justify-content:center;transition:all var(--duration-fast) var(--ease-in-out);width:16px}.filter-chip__radio--checked{border-color:var(--color-healthcare)}.filter-chip__radio--checked:after{background-color:var(--color-healthcare);border-radius:100%;content:"";height:8px;width:8px}.filter-chip__select-all{align-items:center;background:none;border:none;border-radius:4px;color:var(--color-healthcare);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);font-weight:500;gap:var(--space-8);padding:var(--space-6) var(--space-8);text-align:left;width:100%}.filter-chip__select-all:hover{background-color:rgba(var(--healthcare-rgb),.04)}.filter-chip__options-group{border-left:1px solid var(--border-default);margin-left:14px;padding-left:var(--space-6)}.dropdown-enter-active,.dropdown-leave-active{transition:opacity var(--duration-fast) var(--ease-in-out),transform var(--duration-fast) var(--ease-in-out)}.dropdown-enter-from,.dropdown-leave-to{opacity:0;transform:translateY(-4px)}
309
+ .filter-chip-wrapper{position:relative}.filter-chip-wrapper--add{display:inline-flex}.filter-chip{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:6px;box-sizing:border-box;color:var(--text-secondary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);font-weight:var(--weight-medium);gap:var(--space-8);height:32px;padding:var(--space-8);transition:all var(--duration-fast) var(--ease-in-out);white-space:nowrap;width:193px}.filter-chip:hover{border-color:var(--border-default)}.filter-chip__icon{color:var(--color-healthcare);flex-shrink:0}.filter-chip--active{border-color:var(--color-healthcare);box-shadow:0 0 8px 0 rgba(var(--healthcare-rgb),.1)}.filter-chip__label{flex:1;min-width:0;overflow:hidden;text-align:left;text-overflow:ellipsis;white-space:nowrap}.filter-chip__badge{align-items:center;background-color:var(--color-healthcare);border-radius:100px;color:var(--color-text-inverse);display:inline-flex;flex-shrink:0;font-size:var(--font-11);font-weight:600;height:18px;justify-content:center;line-height:1;min-width:18px;padding:0 5px}.filter-chip__caret{color:var(--text-muted);flex-shrink:0;transition:transform var(--duration-fast) var(--ease-in-out)}.filter-chip__caret--open{transform:rotate(180deg)}.filter-chip__dropdown{background-color:var(--bg-surface);border:1px solid var(--border-default);border-radius:6px;box-shadow:0 4px 12px rgba(var(--shadow-rgb),.08),0 2px 4px rgba(var(--shadow-rgb),.04);left:0;max-height:300px;max-width:420px;min-width:380px;overflow-y:auto;padding:var(--space-4);position:absolute;top:calc(100% + 4px);width:-moz-max-content;width:max-content;z-index:50}.filter-chip__search{align-items:center;background-color:var(--bg-surface);border-bottom:1px solid var(--border-soft);color:var(--text-muted);display:flex;gap:var(--space-6);margin:-4px -4px 4px;padding:var(--space-8);position:sticky;top:-4px;z-index:1}.filter-chip__search input{background:none;border:none;color:var(--text-primary);flex:1;font-family:var(--font-sans);font-size:var(--font-14);min-width:0;outline:none}.filter-chip__search input::-webkit-search-cancel-button{cursor:pointer}.filter-chip__no-match{font-size:var(--font-14);margin:0;padding:var(--space-10) var(--space-8)}.filter-chip__no-match,.filter-chip__section{color:var(--text-muted);font-family:var(--font-sans)}.filter-chip__section{border-top:1px solid var(--border-default);font-size:var(--font-12);font-weight:var(--weight-bold);letter-spacing:.04em;margin:8px 0 2px;padding:var(--space-8) var(--space-8) 0;text-transform:uppercase}.filter-chip__section:first-child{border-top:none;margin-top:0;padding-top:0}.filter-chip__add{align-items:center;background:transparent;border:1px dashed var(--border-default);border-radius:var(--radius-sm);color:var(--text-secondary);cursor:pointer;display:inline-flex;font-size:var(--font-12);justify-content:center;line-height:normal;padding:calc(var(--space-4) - 1px) calc(var(--space-6) - 1px)}.filter-chip__add svg{height:1lh}.filter-chip__add--open,.filter-chip__add:hover{border-color:var(--color-primary);border-style:solid;color:var(--color-primary)}.filter-chip__add:focus-visible{box-shadow:var(--shadow-focus);outline:none}.filter-chip__create{color:var(--color-primary);font-weight:var(--weight-medium)}.filter-chip__option{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:var(--space-8);padding:var(--space-6) var(--space-8);text-align:left;transition:background-color var(--duration-fast) var(--ease-in-out);width:100%}.filter-chip__option:hover{background-color:var(--bg-surface-2)}.filter-chip__option--selected{color:var(--text-primary);font-weight:var(--weight-medium)}.filter-chip__checkbox{align-items:center;border:1.5px solid var(--border-default);border-radius:3px;display:flex;flex-shrink:0;height:16px;justify-content:center;transition:all var(--duration-fast) var(--ease-in-out);width:16px}.filter-chip__checkbox--checked{background-color:var(--color-healthcare);border-color:var(--color-healthcare);color:var(--color-text-inverse)}.filter-chip__radio{align-items:center;border:1.5px solid var(--border-default);border-radius:100%;display:flex;flex-shrink:0;height:16px;justify-content:center;transition:all var(--duration-fast) var(--ease-in-out);width:16px}.filter-chip__radio--checked{border-color:var(--color-healthcare)}.filter-chip__radio--checked:after{background-color:var(--color-healthcare);border-radius:100%;content:"";height:8px;width:8px}.filter-chip__select-all{align-items:center;background:none;border:none;border-radius:4px;color:var(--color-healthcare);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);font-weight:500;gap:var(--space-8);padding:var(--space-6) var(--space-8);text-align:left;width:100%}.filter-chip__select-all:hover{background-color:rgba(var(--healthcare-rgb),.04)}.filter-chip__options-group{border-left:1px solid var(--border-default);margin-left:14px;padding-left:var(--space-6)}.dropdown-enter-active,.dropdown-leave-active{transition:opacity var(--duration-fast) var(--ease-in-out),transform var(--duration-fast) var(--ease-in-out)}.dropdown-enter-from,.dropdown-leave-to{opacity:0;transform:translateY(-4px)}
252
310
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.223",
3
+ "version": "0.1.225",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",