@tekkare/romulus 0.1.201 → 0.1.203

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.200",
7
+ "version": "0.1.202",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -29,6 +29,7 @@
29
29
  import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
30
30
  import { PhCaretDown, PhCaretUpDown, PhCheckCircle, PhCircle, PhMagnifyingGlass } from '@phosphor-icons/vue'
31
31
  import { rmPanelOpened, rmPanelRegister } from '../utils/panels'
32
+ import { useRmFloating } from '../composables/useRmFloating'
32
33
 
33
34
  const props = withDefaults(defineProps<{
34
35
  /** Intitule pose au-dessus du champ. */
@@ -98,6 +99,16 @@ const searchTerm = ref('')
98
99
  const activeIndex = ref(-1)
99
100
  const rootRef = ref<HTMLElement>()
100
101
  const searchRef = ref<HTMLInputElement>()
102
+ const panelRef = ref<HTMLElement>()
103
+
104
+ /**
105
+ * Le panneau sort par un `Teleport` et se place sur la fenetre (cf.
106
+ * `useRmFloating`) : pose dans le champ, il etait coupe par toute carte a
107
+ * debordement cache. Il bascule au-dessus du champ quand la place manque
108
+ * dessous, ce qui couvre aussi le selecteur pose en bas d'une modale.
109
+ */
110
+ const floating = useRmFloating(rootRef, panelRef, { matchWidth: true })
111
+ const panelStyle = floating.style
101
112
 
102
113
  /** Cette option est-elle montree sans etre prenable ? */
103
114
  function isDisabled(option: string): boolean {
@@ -124,50 +135,21 @@ function close() {
124
135
  isOpen.value = false
125
136
  activeIndex.value = -1
126
137
  document.removeEventListener('click', onDocumentClick)
138
+ floating.close()
127
139
  }
128
140
 
129
141
  function onDocumentClick(event: MouseEvent) {
130
- if (!rootRef.value?.contains(event.target as Node)) close()
131
- }
132
-
133
- /**
134
- * Le panneau s'ouvre vers le haut quand il n'y a pas la place en dessous.
135
- *
136
- * Dans une modale, un selecteur pose en bas de formulaire deroulait sa liste
137
- * hors du cadre visible et forcait un defilement pour lire ses propres
138
- * options. La place se mesure sur le cadre defilant le plus proche, pas sur
139
- * la fenetre : c'est lui qui coupe.
140
- */
141
- const dropUp = ref(false)
142
-
143
- function scrollParent(node: HTMLElement | null): HTMLElement | null {
144
- let current = node?.parentElement ?? null
145
- while (current) {
146
- const overflow = getComputedStyle(current).overflowY
147
- if (overflow === 'auto' || overflow === 'scroll') return current
148
- current = current.parentElement
149
- }
150
- return null
151
- }
152
-
153
- function placePanel() {
154
- const field = rootRef.value
155
- if (!field) return
156
- const rect = field.getBoundingClientRect()
157
- const frame = scrollParent(field)?.getBoundingClientRect()
158
- const bottom = frame ? frame.bottom : window.innerHeight
159
- const top = frame ? frame.top : 0
160
- // 320 : la hauteur qu'une liste de quelques options occupe en pratique.
161
- const needed = 320
162
- dropUp.value = rect.bottom + needed > bottom && rect.top - top > needed
142
+ const target = event.target as Node
143
+ if (rootRef.value?.contains(target) || panelRef.value?.contains(target)) return
144
+ close()
163
145
  }
164
146
 
165
147
  async function toggle() {
166
148
  if (props.disabled) return
167
149
  if (isOpen.value) return close()
168
150
 
169
- placePanel()
170
151
  isOpen.value = true
152
+ floating.open()
171
153
  rmPanelOpened(close)
172
154
  searchTerm.value = ''
173
155
  activeIndex.value = props.options.indexOf(selected.value ?? '')
@@ -238,11 +220,14 @@ onUnmounted(() => {
238
220
  <component :is="sortIcon ? PhCaretUpDown : PhCaretDown" :size="16" />
239
221
  </button>
240
222
 
223
+ <Teleport to="body" :disabled="!isOpen">
241
224
  <div
242
225
  v-show="isOpen"
226
+ ref="panelRef"
243
227
  class="rm-dsel__panel"
244
- :class="{ 'rm-dsel__panel--up': dropUp }"
228
+ :style="panelStyle"
245
229
  @click.stop
230
+ @keydown="onKeydown"
246
231
  >
247
232
  <div v-if="search" class="rm-dsel__search">
248
233
  <PhMagnifyingGlass :size="16" />
@@ -289,9 +274,10 @@ onUnmounted(() => {
289
274
  </li>
290
275
  </ul>
291
276
  </div>
277
+ </Teleport>
292
278
  </div>
293
279
  </template>
294
280
 
295
281
  <style scoped>
296
- .rm-dsel{align-items:stretch;display:flex;flex-direction:column;gap:var(--space-4);position:relative;width:196px}.rm-dsel__label{color:var(--text-secondary);font-size:var(--font-12,12px);line-height:14px}.rm-dsel__field{align-items:center;background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:var(--radius-md,8px);box-sizing:border-box;color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-14,14px);font-weight:700;gap:var(--space-4);line-height:16px;padding:var(--space-12);text-align:left;width:100%}.rm-dsel__field:hover:not(.is-disabled){border-color:rgba(var(--brand-rgb),.3)}.rm-dsel__field.is-open{border-color:var(--color-primary)}.rm-dsel__field.is-disabled{background:var(--bg-surface-2);color:var(--text-muted);cursor:not-allowed}.rm-dsel__field:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.rm-dsel__value-slot{display:block;flex:1;min-width:0}.rm-dsel__value{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-dsel__value.is-empty{color:var(--text-muted);font-style:italic;font-weight:400}.rm-dsel__field :deep(svg){color:var(--text-secondary);flex-shrink:0}.rm-dsel__panel{background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:var(--radius-lg,12px);box-shadow:var(--r-shadow-lg,0 8px 24px rgba(0,0,0,.12));box-sizing:border-box;display:flex;flex-direction:column;left:0;margin-top:8px;max-height:500px;max-width:min(480px,calc(100vw - 32px));min-width:max(230px,100%);padding:var(--space-16);position:absolute;top:100%;width:-moz-max-content;width:max-content;z-index:12}.rm-dsel__panel--up{bottom:100%;margin-bottom:8px;margin-top:0;top:auto}.rm-dsel__search{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md,8px);display:flex;gap:var(--space-8);padding:var(--space-8) var(--space-12)}.rm-dsel__search :deep(svg){color:var(--text-muted);flex-shrink:0}.rm-dsel__search-input{background:none;border:none;color:var(--text-primary);font-family:inherit;font-size:var(--font-14,14px);width:100%}.rm-dsel__search-input::-moz-placeholder{color:var(--text-muted);font-size:var(--font-12,12px)}.rm-dsel__search-input::placeholder{color:var(--text-muted);font-size:var(--font-12,12px)}.rm-dsel__search-input:focus-visible{outline:none}.rm-dsel__loading{display:flex;flex-direction:column;gap:var(--space-10);padding-top:var(--space-8)}.rm-dsel__sr{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;white-space:nowrap}.rm-dsel__state{color:var(--text-secondary);font-size:var(--font-14,14px);font-style:italic;padding-top:var(--space-16);text-align:center}.rm-dsel__list{display:flex;flex-direction:column;list-style:none;margin:0;min-height:10px;overflow-y:auto;padding:0}.rm-dsel__list:not(:only-child){margin-top:16px}.rm-dsel__option.is-disabled{color:var(--text-muted);cursor:not-allowed}.rm-dsel__option{align-items:flex-start;border-radius:var(--radius-sm,4px);cursor:pointer;display:flex;flex-shrink:0;font-size:var(--font-14,14px);gap:var(--space-8);line-height:160%;margin:4px 0;min-height:24px;padding:var(--space-2) var(--space-4)}.rm-dsel__option :deep(svg){color:var(--text-muted);flex-shrink:0;margin-top:3px}.rm-dsel__option.is-active{background:rgba(var(--brand-rgb),.05)}.rm-dsel__option.is-selected{font-weight:700}.rm-dsel__option.is-active :deep(svg),.rm-dsel__option.is-selected :deep(svg){color:var(--color-primary)}
282
+ .rm-dsel{align-items:stretch;display:flex;flex-direction:column;gap:var(--space-4);position:relative;width:196px}.rm-dsel__label{color:var(--text-secondary);font-size:var(--font-12,12px);line-height:14px}.rm-dsel__field{align-items:center;background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:var(--radius-md,8px);box-sizing:border-box;color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-14,14px);font-weight:700;gap:var(--space-4);line-height:16px;padding:var(--space-12);text-align:left;width:100%}.rm-dsel__field:hover:not(.is-disabled){border-color:rgba(var(--brand-rgb),.3)}.rm-dsel__field.is-open{border-color:var(--color-primary)}.rm-dsel__field.is-disabled{background:var(--bg-surface-2);color:var(--text-muted);cursor:not-allowed}.rm-dsel__field:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.rm-dsel__value-slot{display:block;flex:1;min-width:0}.rm-dsel__value{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-dsel__value.is-empty{color:var(--text-muted);font-style:italic;font-weight:400}.rm-dsel__field :deep(svg){color:var(--text-secondary);flex-shrink:0}.rm-dsel__panel{border:1.5px solid var(--border-default);border-radius:var(--radius-lg,12px);box-shadow:var(--r-shadow-lg,0 8px 24px rgba(0,0,0,.12));box-sizing:border-box;flex-direction:column;max-height:500px;max-width:min(480px,calc(100vw - 32px));min-width:230px;padding:var(--space-16);position:fixed;width:-moz-max-content;width:max-content;z-index:var(--z-popover,1000)}.rm-dsel__panel,.rm-dsel__search{background:var(--bg-surface);display:flex}.rm-dsel__search{align-items:center;border:1px solid var(--border-default);border-radius:var(--radius-md,8px);gap:var(--space-8);padding:var(--space-8) var(--space-12)}.rm-dsel__search :deep(svg){color:var(--text-muted);flex-shrink:0}.rm-dsel__search-input{background:none;border:none;color:var(--text-primary);font-family:inherit;font-size:var(--font-14,14px);width:100%}.rm-dsel__search-input::-moz-placeholder{color:var(--text-muted);font-size:var(--font-12,12px)}.rm-dsel__search-input::placeholder{color:var(--text-muted);font-size:var(--font-12,12px)}.rm-dsel__search-input:focus-visible{outline:none}.rm-dsel__loading{display:flex;flex-direction:column;gap:var(--space-10);padding-top:var(--space-8)}.rm-dsel__sr{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;white-space:nowrap}.rm-dsel__state{color:var(--text-secondary);font-size:var(--font-14,14px);font-style:italic;padding-top:var(--space-16);text-align:center}.rm-dsel__list{display:flex;flex-direction:column;list-style:none;margin:0;min-height:10px;overflow-y:auto;padding:0}.rm-dsel__list:not(:only-child){margin-top:16px}.rm-dsel__option.is-disabled{color:var(--text-muted);cursor:not-allowed}.rm-dsel__option{align-items:flex-start;border-radius:var(--radius-sm,4px);cursor:pointer;display:flex;flex-shrink:0;font-size:var(--font-14,14px);gap:var(--space-8);line-height:160%;margin:4px 0;min-height:24px;padding:var(--space-2) var(--space-4)}.rm-dsel__option :deep(svg){color:var(--text-muted);flex-shrink:0;margin-top:3px}.rm-dsel__option.is-active{background:rgba(var(--brand-rgb),.05)}.rm-dsel__option.is-selected{font-weight:700}.rm-dsel__option.is-active :deep(svg),.rm-dsel__option.is-selected :deep(svg){color:var(--color-primary)}
297
283
  </style>
@@ -27,6 +27,7 @@
27
27
  import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
28
28
  import { PhCaretDown, PhCheckSquare, PhInfo, PhMagnifyingGlass, PhSquare } from '@phosphor-icons/vue'
29
29
  import { rmPanelOpened, rmPanelRegister } from '../utils/panels'
30
+ import { useRmFloating } from '../composables/useRmFloating'
30
31
 
31
32
  const props = withDefaults(defineProps<{
32
33
  /** Intitule pose au-dessus du champ. */
@@ -89,6 +90,15 @@ const t = computed(() => WORDS[props.lang])
89
90
  const isOpen = ref(false)
90
91
  const searchTerm = ref('')
91
92
  const rootRef = ref<HTMLElement>()
93
+ const panelRef = ref<HTMLElement>()
94
+
95
+ /**
96
+ * Le panneau sort par un `Teleport` et se place sur la fenetre (cf.
97
+ * `useRmFloating`) : pose dans le champ, il etait coupe par toute carte a
98
+ * debordement cache.
99
+ */
100
+ const floating = useRmFloating(rootRef, panelRef, { matchWidth: true })
101
+ const panelStyle = floating.style
92
102
  const searchRef = ref<HTMLInputElement>()
93
103
 
94
104
  // Tout cocher d'entree, une fois : refaire la selection a chaque arrivee
@@ -124,11 +134,14 @@ function close() {
124
134
  if (!isOpen.value) return
125
135
  isOpen.value = false
126
136
  document.removeEventListener('click', onDocumentClick)
137
+ floating.close()
127
138
  emit('close')
128
139
  }
129
140
 
130
141
  function onDocumentClick(event: MouseEvent) {
131
- if (!rootRef.value?.contains(event.target as Node)) close()
142
+ const target = event.target as Node
143
+ if (rootRef.value?.contains(target) || panelRef.value?.contains(target)) return
144
+ close()
132
145
  }
133
146
 
134
147
  async function toggle() {
@@ -136,6 +149,7 @@ async function toggle() {
136
149
  if (isOpen.value) return close()
137
150
 
138
151
  isOpen.value = true
152
+ floating.open()
139
153
  rmPanelOpened(close)
140
154
  searchTerm.value = ''
141
155
  document.addEventListener('click', onDocumentClick)
@@ -200,7 +214,15 @@ onUnmounted(() => {
200
214
  <PhCaretDown :size="16" />
201
215
  </button>
202
216
 
203
- <div v-show="isOpen" class="rm-msel__panel" @click.stop>
217
+ <Teleport to="body" :disabled="!isOpen">
218
+ <div
219
+ v-show="isOpen"
220
+ ref="panelRef"
221
+ class="rm-msel__panel"
222
+ :style="panelStyle"
223
+ @click.stop
224
+ @keydown.escape="close"
225
+ >
204
226
  <div v-if="search" class="rm-msel__search">
205
227
  <PhMagnifyingGlass :size="16" />
206
228
  <input
@@ -260,9 +282,10 @@ onUnmounted(() => {
260
282
  </div>
261
283
  </div>
262
284
  </div>
285
+ </Teleport>
263
286
  </div>
264
287
  </template>
265
288
 
266
289
  <style scoped>
267
- .rm-msel{align-items:stretch;flex-direction:column;position:relative;width:196px}.rm-msel,.rm-msel__label{display:flex;gap:var(--space-4)}.rm-msel__label{align-items:center;color:var(--text-secondary);font-size:var(--font-12,12px);line-height:14px}.rm-msel__info{background:none;border:none;color:var(--text-muted);cursor:pointer;display:flex;padding:0}.rm-msel__field{align-items:center;background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:var(--radius-md,8px);box-sizing:border-box;color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-14,14px);font-weight:700;gap:var(--space-4);line-height:16px;padding:var(--space-12);text-align:left;width:100%}.rm-msel__field:hover:not(.is-disabled){border-color:rgba(var(--brand-rgb),.3)}.rm-msel__field.is-open{border-color:var(--color-primary)}.rm-msel__field.is-disabled{background:var(--bg-surface-2);color:var(--text-muted);cursor:not-allowed}.rm-msel__field:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.rm-msel__value-slot{display:block;flex:1;min-width:0}.rm-msel__value{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-msel__value.is-empty{color:var(--text-muted);font-style:italic;font-weight:400}.rm-msel__field :deep(svg){color:var(--text-secondary);flex-shrink:0}.rm-msel__count{align-items:center;background:rgba(var(--brand-rgb),.1);border-radius:50%;color:var(--color-primary);display:flex;flex-shrink:0;font-size:var(--font-12,12px);font-weight:600;height:20px;justify-content:center;width:20px}.rm-msel__panel{border:1.5px solid var(--border-default);border-radius:var(--radius-lg,12px);box-shadow:var(--r-shadow-lg,0 8px 24px rgba(0,0,0,.12));box-sizing:border-box;flex-direction:column;left:0;margin-top:8px;max-height:500px;max-width:320px;min-width:230px;padding:var(--space-16);position:absolute;top:100%;z-index:12}.rm-msel__panel,.rm-msel__search{background:var(--bg-surface);display:flex}.rm-msel__search{align-items:center;border:1px solid var(--border-default);border-radius:var(--radius-md,8px);gap:var(--space-8);padding:var(--space-8) var(--space-12)}.rm-msel__search :deep(svg){color:var(--text-muted);flex-shrink:0}.rm-msel__search-input{background:none;border:none;color:var(--text-primary);font-family:inherit;font-size:var(--font-14,14px);width:100%}.rm-msel__search-input::-moz-placeholder{color:var(--text-muted);font-size:var(--font-12,12px)}.rm-msel__search-input::placeholder{color:var(--text-muted);font-size:var(--font-12,12px)}.rm-msel__search-input:focus-visible{outline:none}.rm-msel__loading{display:flex;flex-direction:column;gap:var(--space-10);padding-top:var(--space-8)}.rm-msel__sr{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;white-space:nowrap}.rm-msel__state{color:var(--text-secondary);font-size:var(--font-14,14px);font-style:italic;padding-top:var(--space-16);text-align:center}.rm-msel__list{display:flex;flex-direction:column;min-height:10px;overflow-y:auto}.rm-msel__list:not(:only-child){margin-top:16px}.rm-msel__indent{margin-left:16px}.rm-msel__option{align-items:center;background:none;border:none;border-radius:var(--radius-sm,4px);color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-14,14px);gap:var(--space-8);line-height:160%;margin:4px 0;min-height:24px;padding:var(--space-2) var(--space-4);text-align:left;width:100%}.rm-msel__option :deep(svg){color:var(--text-muted);flex-shrink:0}.rm-msel__option:hover{background:rgba(var(--brand-rgb),.05)}.rm-msel__option:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px}.rm-msel__option.is-checked{font-weight:700}.rm-msel__option.is-checked :deep(svg),.rm-msel__option:hover :deep(svg){color:var(--color-primary)}
290
+ .rm-msel{align-items:stretch;flex-direction:column;position:relative;width:196px}.rm-msel,.rm-msel__label{display:flex;gap:var(--space-4)}.rm-msel__label{align-items:center;color:var(--text-secondary);font-size:var(--font-12,12px);line-height:14px}.rm-msel__info{background:none;border:none;color:var(--text-muted);cursor:pointer;display:flex;padding:0}.rm-msel__field{align-items:center;background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:var(--radius-md,8px);box-sizing:border-box;color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-14,14px);font-weight:700;gap:var(--space-4);line-height:16px;padding:var(--space-12);text-align:left;width:100%}.rm-msel__field:hover:not(.is-disabled){border-color:rgba(var(--brand-rgb),.3)}.rm-msel__field.is-open{border-color:var(--color-primary)}.rm-msel__field.is-disabled{background:var(--bg-surface-2);color:var(--text-muted);cursor:not-allowed}.rm-msel__field:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.rm-msel__value-slot{display:block;flex:1;min-width:0}.rm-msel__value{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-msel__value.is-empty{color:var(--text-muted);font-style:italic;font-weight:400}.rm-msel__field :deep(svg){color:var(--text-secondary);flex-shrink:0}.rm-msel__count{align-items:center;background:rgba(var(--brand-rgb),.1);border-radius:50%;color:var(--color-primary);display:flex;flex-shrink:0;font-size:var(--font-12,12px);font-weight:600;height:20px;justify-content:center;width:20px}.rm-msel__panel{border:1.5px solid var(--border-default);border-radius:var(--radius-lg,12px);box-shadow:var(--r-shadow-lg,0 8px 24px rgba(0,0,0,.12));box-sizing:border-box;flex-direction:column;max-height:500px;max-width:320px;min-width:230px;padding:var(--space-16);position:fixed;z-index:var(--z-popover,1000)}.rm-msel__panel,.rm-msel__search{background:var(--bg-surface);display:flex}.rm-msel__search{align-items:center;border:1px solid var(--border-default);border-radius:var(--radius-md,8px);gap:var(--space-8);padding:var(--space-8) var(--space-12)}.rm-msel__search :deep(svg){color:var(--text-muted);flex-shrink:0}.rm-msel__search-input{background:none;border:none;color:var(--text-primary);font-family:inherit;font-size:var(--font-14,14px);width:100%}.rm-msel__search-input::-moz-placeholder{color:var(--text-muted);font-size:var(--font-12,12px)}.rm-msel__search-input::placeholder{color:var(--text-muted);font-size:var(--font-12,12px)}.rm-msel__search-input:focus-visible{outline:none}.rm-msel__loading{display:flex;flex-direction:column;gap:var(--space-10);padding-top:var(--space-8)}.rm-msel__sr{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;white-space:nowrap}.rm-msel__state{color:var(--text-secondary);font-size:var(--font-14,14px);font-style:italic;padding-top:var(--space-16);text-align:center}.rm-msel__list{display:flex;flex-direction:column;min-height:10px;overflow-y:auto}.rm-msel__list:not(:only-child){margin-top:16px}.rm-msel__indent{margin-left:16px}.rm-msel__option{align-items:center;background:none;border:none;border-radius:var(--radius-sm,4px);color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-14,14px);gap:var(--space-8);line-height:160%;margin:4px 0;min-height:24px;padding:var(--space-2) var(--space-4);text-align:left;width:100%}.rm-msel__option :deep(svg){color:var(--text-muted);flex-shrink:0}.rm-msel__option:hover{background:rgba(var(--brand-rgb),.05)}.rm-msel__option:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px}.rm-msel__option.is-checked{font-weight:700}.rm-msel__option.is-checked :deep(svg),.rm-msel__option:hover :deep(svg){color:var(--color-primary)}
268
291
  </style>
@@ -38,8 +38,11 @@ withDefaults(
38
38
  count?: number | string | null
39
39
  /** Libelle sous le chiffre (« avis », « produits »). */
40
40
  countLabel?: string
41
- /** Precision sous le libelle (« dont 2 acces precoce »). */
42
- countExtra?: string
41
+ /**
42
+ * Precision sous le libelle (« dont 2 acces precoce »). Un tableau pose
43
+ * une ligne par entree, les vides sont sautees.
44
+ */
45
+ countExtra?: string | string[]
43
46
  /** Rappel en bas de la zone de droite (« Voir les 5 avis »). Une fleche est ajoutee. */
44
47
  moreLabel?: string
45
48
  /** Curseur main et surbrillance au survol : la carte est un lien. */
@@ -113,7 +116,11 @@ withDefaults(
113
116
  <div v-if="count !== null && count !== undefined" class="rm-rcard__count">
114
117
  <p class="rm-rcard__count-value">{{ count }}</p>
115
118
  <p v-if="countLabel" class="rm-rcard__count-label">{{ countLabel }}</p>
116
- <p v-if="countExtra" class="rm-rcard__count-extra">{{ countExtra }}</p>
119
+ <p
120
+ v-for="(line, i) in [countExtra].flat().filter(Boolean)"
121
+ :key="i"
122
+ class="rm-rcard__count-extra"
123
+ >{{ line }}</p>
117
124
  </div>
118
125
 
119
126
  <div class="rm-rcard__main">
@@ -128,5 +135,5 @@ withDefaults(
128
135
  </template>
129
136
 
130
137
  <style scoped>
131
- .rm-rcard{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-2xl);color:var(--text-primary);display:block;font-family:var(--font-sans);overflow:hidden;transition:border-color var(--duration-fast) linear,box-shadow var(--duration-fast) linear;width:100%}.rm-rcard--clickable{cursor:pointer}.rm-rcard--clickable:hover{border-color:color-mix(in srgb,var(--color-primary) 35%,var(--border-default));box-shadow:var(--shadow-md)}.rm-rcard__head{align-items:flex-start;display:flex;gap:var(--space-16);padding:var(--space-20) var(--space-24) 0}.rm-rcard__identity{flex:1 1 auto;min-width:0}.rm-rcard__title{font-size:var(--font-18);font-weight:800;letter-spacing:.01em;margin:0}.rm-rcard__subtitle{color:var(--text-muted);font-size:var(--font-14);margin-top:4px;overflow-wrap:anywhere}.rm-rcard__actions{align-items:center;display:flex;flex:0 0 auto;gap:var(--space-8)}.rm-rcard__body{align-items:stretch;display:flex;gap:var(--space-24);padding:var(--space-16) var(--space-24) var(--space-20)}.rm-rcard__count{align-items:center;border-right:1px solid var(--border-default);display:flex;flex:0 0 128px;flex-direction:column;gap:var(--space-2);justify-content:center;padding-right:var(--space-24);text-align:center}.rm-rcard__count-value{color:var(--color-primary);font-size:var(--font-32);font-weight:var(--weight-bold);line-height:1.1;margin:0}.rm-rcard__count-label{color:var(--text-muted);font-size:var(--font-12);font-weight:var(--weight-semibold);letter-spacing:.08em;margin:0;text-transform:uppercase}.rm-rcard__count-extra{color:var(--color-primary);font-size:var(--font-13);line-height:1.3;margin:6px 0 0}.rm-rcard__main{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--space-6);min-width:0}.rm-rcard__meta{align-items:center;display:flex;flex-wrap:wrap;gap:var(--space-8)}.rm-rcard__more{color:var(--color-primary);font-size:var(--font-14);font-weight:var(--weight-semibold);margin:4px 0 0}.rm-rcard__footer{background:var(--bg-surface-2);border-top:1px solid var(--border-default);font-size:var(--font-13);padding:var(--space-12) var(--space-24)}.rm-rcard :deep(.rm-rcard__label){align-items:center;color:var(--text-secondary);display:inline-flex;font-size:var(--font-12);font-weight:var(--weight-bold);gap:var(--space-4);letter-spacing:.06em;text-transform:uppercase}.rm-rcard :deep(.rm-rcard__ref){background:var(--bg-surface-3);border-radius:var(--radius-sm);color:var(--text-secondary);font-family:Courier New,monospace;font-size:var(--font-12);padding:var(--space-2) var(--space-8)}.rm-rcard :deep(.rm-rcard__lead){font-size:15px;font-weight:var(--weight-bold);margin:0}.rm-rcard :deep(.rm-rcard__text){font-size:15px;margin:0}.rm-rcard :deep(.rm-rcard__desc){color:var(--text-secondary);font-size:var(--font-14);line-height:1.5;margin:0}.rm-rcard :deep(.rm-rcard__clamp){display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.rm-rcard--dense{border-radius:var(--radius-xl)}.rm-rcard--dense .rm-rcard__head{gap:var(--space-12);padding:var(--space-12) var(--space-16) 0}.rm-rcard--dense .rm-rcard__title{font-size:15px}.rm-rcard--dense .rm-rcard__subtitle{font-size:var(--font-13);margin-top:2px}.rm-rcard--dense .rm-rcard__body{gap:var(--space-16);padding:var(--space-8) var(--space-16) var(--space-12)}.rm-rcard--dense .rm-rcard__count{flex-basis:104px;padding-right:var(--space-16)}.rm-rcard--dense .rm-rcard__count-value{font-size:var(--font-24)}.rm-rcard--dense .rm-rcard__count-label{font-size:var(--font-11)}.rm-rcard--dense .rm-rcard__main{gap:var(--space-4)}.rm-rcard--dense .rm-rcard__count-extra,.rm-rcard--dense .rm-rcard__footer,.rm-rcard--dense .rm-rcard__main>:not(.rm-rcard__meta,.rm-rcard__lead){display:none}.rm-rcard--dense :deep(.rm-rcard__lead){display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}.rm-rcard--dense .rm-rcard__body{align-items:center}.rm-rcard--dense :deep(.rm-rcard__lead),.rm-rcard--dense :deep(.rm-rcard__text){font-size:var(--font-14)}.rm-rcard--dense :deep(.rm-rcard__desc){font-size:var(--font-13)}
138
+ .rm-rcard{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-2xl);color:var(--text-primary);display:block;font-family:var(--font-sans);overflow:hidden;transition:border-color var(--duration-fast) linear,box-shadow var(--duration-fast) linear;width:100%}.rm-rcard--clickable{cursor:pointer}.rm-rcard--clickable:hover{border-color:color-mix(in srgb,var(--color-primary) 35%,var(--border-default));box-shadow:var(--shadow-md)}.rm-rcard__head{align-items:flex-start;display:flex;gap:var(--space-16);padding:var(--space-20) var(--space-24) 0}.rm-rcard__identity{flex:1 1 auto;min-width:0}.rm-rcard__title{font-size:var(--font-18);font-weight:800;letter-spacing:.01em;margin:0}.rm-rcard__subtitle{color:var(--text-muted);font-size:var(--font-14);margin-top:4px;overflow-wrap:anywhere}.rm-rcard__actions{align-items:center;display:flex;flex:0 0 auto;gap:var(--space-8)}.rm-rcard__body{align-items:stretch;display:flex;gap:var(--space-24);padding:var(--space-16) var(--space-24) var(--space-20)}.rm-rcard__count{align-items:center;border-right:1px solid var(--border-default);display:flex;flex:0 0 128px;flex-direction:column;gap:var(--space-2);justify-content:center;padding-right:var(--space-24);text-align:center}.rm-rcard__count-value{color:var(--color-primary);font-size:var(--font-32);font-weight:var(--weight-bold);line-height:1.1;margin:0}.rm-rcard__count-label{color:var(--text-muted);font-size:var(--font-12);font-weight:var(--weight-semibold);letter-spacing:.08em;margin:0;text-transform:uppercase}.rm-rcard__count-extra{color:var(--color-primary);font-size:var(--font-13);line-height:1.3;margin:6px 0 0}.rm-rcard__count-extra+.rm-rcard__count-extra{margin-top:2px}.rm-rcard__main{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--space-6);min-width:0}.rm-rcard__meta{align-items:center;display:flex;flex-wrap:wrap;gap:var(--space-8)}.rm-rcard__more{color:var(--color-primary);font-size:var(--font-14);font-weight:var(--weight-semibold);margin:4px 0 0}.rm-rcard__footer{background:var(--bg-surface-2);border-top:1px solid var(--border-default);font-size:var(--font-13);padding:var(--space-12) var(--space-24)}.rm-rcard :deep(.rm-rcard__label){align-items:center;color:var(--text-secondary);display:inline-flex;font-size:var(--font-12);font-weight:var(--weight-bold);gap:var(--space-4);letter-spacing:.06em;text-transform:uppercase}.rm-rcard :deep(.rm-rcard__ref){background:var(--bg-surface-3);border-radius:var(--radius-sm);color:var(--text-secondary);font-family:Courier New,monospace;font-size:var(--font-12);padding:var(--space-2) var(--space-8)}.rm-rcard :deep(.rm-rcard__lead){font-size:15px;font-weight:var(--weight-bold);margin:0}.rm-rcard :deep(.rm-rcard__text){font-size:15px;margin:0}.rm-rcard :deep(.rm-rcard__desc){color:var(--text-secondary);font-size:var(--font-14);line-height:1.5;margin:0}.rm-rcard :deep(.rm-rcard__clamp){display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.rm-rcard--dense{border-radius:var(--radius-xl)}.rm-rcard--dense .rm-rcard__head{gap:var(--space-12);padding:var(--space-12) var(--space-16) 0}.rm-rcard--dense .rm-rcard__title{font-size:15px}.rm-rcard--dense .rm-rcard__subtitle{font-size:var(--font-13);margin-top:2px}.rm-rcard--dense .rm-rcard__body{gap:var(--space-16);padding:var(--space-8) var(--space-16) var(--space-12)}.rm-rcard--dense .rm-rcard__count{flex-basis:104px;padding-right:var(--space-16)}.rm-rcard--dense .rm-rcard__count-value{font-size:var(--font-24)}.rm-rcard--dense .rm-rcard__count-label{font-size:var(--font-11)}.rm-rcard--dense .rm-rcard__main{gap:var(--space-4)}.rm-rcard--dense .rm-rcard__count-extra,.rm-rcard--dense .rm-rcard__footer,.rm-rcard--dense .rm-rcard__main>:not(.rm-rcard__meta,.rm-rcard__lead){display:none}.rm-rcard--dense :deep(.rm-rcard__lead){display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}.rm-rcard--dense .rm-rcard__body{align-items:center}.rm-rcard--dense :deep(.rm-rcard__lead),.rm-rcard--dense :deep(.rm-rcard__text){font-size:var(--font-14)}.rm-rcard--dense :deep(.rm-rcard__desc){font-size:var(--font-13)}
132
139
  </style>
@@ -380,6 +380,7 @@ function onCardClick() {
380
380
  class="rm-view__pill-tip"
381
381
  dir="top"
382
382
  full
383
+ max-width="320px"
383
384
  :disabled="!clipped[`${index}-${pillIndex}`]"
384
385
  >
385
386
  <span class="rm-view__pill-v" :data-pill="`${index}-${pillIndex}`">
@@ -419,5 +420,5 @@ function onCardClick() {
419
420
  </template>
420
421
 
421
422
  <style scoped>
422
- .rm-view{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);color:var(--text-primary);cursor:pointer;font-family:var(--font-sans);padding:14px 18px;transition:border-color var(--duration-fast) var(--ease-out),box-shadow var(--duration-fast) var(--ease-out),background var(--duration-fast) var(--ease-out)}.rm-view:not(.rm-view--active):hover{background:var(--bg-hover);border-color:var(--color-accent-soft)}.rm-view--active{border-color:var(--color-secondary);box-shadow:0 0 0 3px rgba(var(--brand-rgb),.08);cursor:default}.rm-view__strip{align-items:center;background:var(--bg-hover);border-bottom:1px solid var(--border-soft);border-radius:7px 7px 0 0;color:var(--color-secondary);display:flex;flex-wrap:wrap;font-size:var(--font-11);font-weight:var(--weight-bold);gap:var(--space-8);letter-spacing:.06em;margin:-14px -18px var(--space-12);padding:var(--space-8) var(--space-16);text-transform:uppercase}.rm-view__strip-mark{align-items:center;display:inline-flex;gap:var(--space-6);white-space:nowrap}.rm-view__head{align-items:center;display:flex;gap:var(--space-10)}.rm-view__icon{align-items:center;border-radius:var(--radius-md);display:inline-flex;flex:0 0 auto;height:34px;justify-content:center;width:34px}.rm-view__icon--primary{background:var(--bg-hover);color:var(--color-secondary)}.rm-view__icon--success{background:var(--color-success-bg);color:var(--color-success)}.rm-view__icon--warning{background:var(--color-warning-bg);color:var(--color-warning)}.rm-view__icon--danger{background:var(--color-danger-bg);color:var(--color-danger)}.rm-view__icon--info{background:var(--color-info-bg);color:var(--color-info)}.rm-view__star{align-items:center;background:transparent;border:1px solid transparent;border-radius:var(--radius-md);color:var(--text-muted);cursor:pointer;display:inline-flex;flex:0 0 auto;height:28px;justify-content:center;padding:0;width:28px}.rm-view__star:hover{background:var(--bg-hover)}.rm-view__star--on,.rm-view__star:hover{color:var(--color-warning)}.rm-view__title,.rm-view__title-edit{flex:1;font-size:var(--font-16);font-weight:var(--weight-bold);min-width:0}.rm-view__title-edit{background:var(--bg-surface);border:1px solid var(--color-secondary);border-radius:var(--radius-md);color:var(--text-primary);font-family:inherit;outline:none;padding:var(--space-2) var(--space-8)}.rm-view__title-edit:focus{box-shadow:0 0 0 3px rgba(var(--brand-rgb),.12)}.rm-view__actions{align-items:center;display:flex;gap:var(--space-8)}.rm-view__badge{align-items:center;background:var(--color-success-bg);border-radius:var(--radius-pill);color:var(--color-success);display:inline-flex;font-size:var(--font-10);font-weight:var(--weight-bold);gap:var(--space-4);letter-spacing:.06em;padding:3px var(--space-8);text-transform:uppercase}.rm-view__menu-wrap{position:relative}.rm-view__more{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-muted);cursor:pointer;display:inline-flex;height:28px;justify-content:center;padding:0;width:28px}.rm-view__more:hover{background:var(--bg-hover);color:var(--text-primary)}.rm-view__menu-shade{inset:0;position:fixed;z-index:60}.rm-view__menu{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);display:flex;flex-direction:column;min-width:200px;padding:var(--space-4);position:absolute;right:0;top:calc(100% + 6px);z-index:61}.rm-view__menu-item{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-13);gap:var(--space-8);padding:var(--space-8) var(--space-12);text-align:left;width:100%}.rm-view__menu-item:hover{background:var(--bg-hover);color:var(--color-secondary)}.rm-view__menu-item--danger{color:var(--color-danger)}.rm-view__menu-item--danger:hover{background:var(--color-danger-bg);color:var(--color-danger)}.rm-view__desc{color:var(--text-secondary);font-size:var(--font-13);margin:var(--space-8) 0 var(--space-12)}.rm-view__config{background:var(--bg-surface-2);border:1px solid var(--border-soft);border-radius:var(--radius-md);display:flex;flex-direction:column;gap:var(--space-6);margin:10px 0 var(--space-12);padding:var(--space-10) 14px}.rm-view__row{align-items:flex-start;display:flex;flex-wrap:wrap;font-size:var(--font-12);gap:14px}.rm-view__row-key{color:var(--text-muted);display:inline-flex;flex-shrink:0;font-size:var(--font-10);font-weight:var(--weight-semibold);letter-spacing:.04em;min-width:96px;padding-top:var(--space-4);text-transform:uppercase}.rm-view__row-key,.rm-view__row-val{align-items:center;gap:var(--space-6)}.rm-view__row-val{color:var(--text-primary);display:flex;flex:1 1 200px;flex-wrap:wrap;padding:var(--space-2) 0}.rm-view__pill{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);display:inline-flex;font-size:var(--font-12);gap:var(--space-4);padding:5px var(--space-10)}.rm-view__pill-k{color:var(--color-primary);font-weight:var(--weight-semibold)}.rm-view__pill-tip{min-width:0}.rm-view__pill-tip :deep(.rm-tooltip__bubble){max-width:320px}.rm-view__pill-v{display:-webkit-box;-webkit-box-orient:vertical;color:var(--text-primary);font-weight:var(--weight-medium);-webkit-line-clamp:3;overflow:hidden}.rm-view__foot{border-top:1px solid var(--border-soft);color:var(--text-muted);display:flex;flex-wrap:wrap;font-size:var(--font-11);gap:var(--space-16);padding-top:var(--space-10)}.rm-view__foot span{align-items:center;display:inline-flex;gap:var(--space-4)}.rm-view__confirm{align-items:center;background:var(--color-danger-bg);border:1px solid var(--color-danger);border-radius:var(--radius-md);color:var(--color-danger);display:flex;flex-wrap:wrap;font-size:var(--font-13);gap:var(--space-12);margin-top:var(--space-12);padding:var(--space-8) var(--space-12)}.rm-view__confirm>span{align-items:center;display:inline-flex;flex:1 1 220px;gap:var(--space-8)}.rm-view__confirm-actions{display:flex;flex-shrink:0;gap:var(--space-8)}
423
+ .rm-view{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);color:var(--text-primary);cursor:pointer;font-family:var(--font-sans);padding:14px 18px;transition:border-color var(--duration-fast) var(--ease-out),box-shadow var(--duration-fast) var(--ease-out),background var(--duration-fast) var(--ease-out)}.rm-view:not(.rm-view--active):hover{background:var(--bg-hover);border-color:var(--color-accent-soft)}.rm-view--active{border-color:var(--color-secondary);box-shadow:0 0 0 3px rgba(var(--brand-rgb),.08);cursor:default}.rm-view__strip{align-items:center;background:var(--bg-hover);border-bottom:1px solid var(--border-soft);border-radius:7px 7px 0 0;color:var(--color-secondary);display:flex;flex-wrap:wrap;font-size:var(--font-11);font-weight:var(--weight-bold);gap:var(--space-8);letter-spacing:.06em;margin:-14px -18px var(--space-12);padding:var(--space-8) var(--space-16);text-transform:uppercase}.rm-view__strip-mark{align-items:center;display:inline-flex;gap:var(--space-6);white-space:nowrap}.rm-view__head{align-items:center;display:flex;gap:var(--space-10)}.rm-view__icon{align-items:center;border-radius:var(--radius-md);display:inline-flex;flex:0 0 auto;height:34px;justify-content:center;width:34px}.rm-view__icon--primary{background:var(--bg-hover);color:var(--color-secondary)}.rm-view__icon--success{background:var(--color-success-bg);color:var(--color-success)}.rm-view__icon--warning{background:var(--color-warning-bg);color:var(--color-warning)}.rm-view__icon--danger{background:var(--color-danger-bg);color:var(--color-danger)}.rm-view__icon--info{background:var(--color-info-bg);color:var(--color-info)}.rm-view__star{align-items:center;background:transparent;border:1px solid transparent;border-radius:var(--radius-md);color:var(--text-muted);cursor:pointer;display:inline-flex;flex:0 0 auto;height:28px;justify-content:center;padding:0;width:28px}.rm-view__star:hover{background:var(--bg-hover)}.rm-view__star--on,.rm-view__star:hover{color:var(--color-warning)}.rm-view__title,.rm-view__title-edit{flex:1;font-size:var(--font-16);font-weight:var(--weight-bold);min-width:0}.rm-view__title-edit{background:var(--bg-surface);border:1px solid var(--color-secondary);border-radius:var(--radius-md);color:var(--text-primary);font-family:inherit;outline:none;padding:var(--space-2) var(--space-8)}.rm-view__title-edit:focus{box-shadow:0 0 0 3px rgba(var(--brand-rgb),.12)}.rm-view__actions{align-items:center;display:flex;gap:var(--space-8)}.rm-view__badge{align-items:center;background:var(--color-success-bg);border-radius:var(--radius-pill);color:var(--color-success);display:inline-flex;font-size:var(--font-10);font-weight:var(--weight-bold);gap:var(--space-4);letter-spacing:.06em;padding:3px var(--space-8);text-transform:uppercase}.rm-view__menu-wrap{position:relative}.rm-view__more{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-muted);cursor:pointer;display:inline-flex;height:28px;justify-content:center;padding:0;width:28px}.rm-view__more:hover{background:var(--bg-hover);color:var(--text-primary)}.rm-view__menu-shade{inset:0;position:fixed;z-index:60}.rm-view__menu{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);display:flex;flex-direction:column;min-width:200px;padding:var(--space-4);position:absolute;right:0;top:calc(100% + 6px);z-index:61}.rm-view__menu-item{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-13);gap:var(--space-8);padding:var(--space-8) var(--space-12);text-align:left;width:100%}.rm-view__menu-item:hover{background:var(--bg-hover);color:var(--color-secondary)}.rm-view__menu-item--danger{color:var(--color-danger)}.rm-view__menu-item--danger:hover{background:var(--color-danger-bg);color:var(--color-danger)}.rm-view__desc{color:var(--text-secondary);font-size:var(--font-13);margin:var(--space-8) 0 var(--space-12)}.rm-view__config{background:var(--bg-surface-2);border:1px solid var(--border-soft);border-radius:var(--radius-md);display:flex;flex-direction:column;gap:var(--space-6);margin:10px 0 var(--space-12);padding:var(--space-10) 14px}.rm-view__row{align-items:flex-start;display:flex;flex-wrap:wrap;font-size:var(--font-12);gap:14px}.rm-view__row-key{color:var(--text-muted);display:inline-flex;flex-shrink:0;font-size:var(--font-10);font-weight:var(--weight-semibold);letter-spacing:.04em;min-width:96px;padding-top:var(--space-4);text-transform:uppercase}.rm-view__row-key,.rm-view__row-val{align-items:center;gap:var(--space-6)}.rm-view__row-val{color:var(--text-primary);display:flex;flex:1 1 200px;flex-wrap:wrap;padding:var(--space-2) 0}.rm-view__pill{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);display:inline-flex;font-size:var(--font-12);gap:var(--space-4);padding:5px var(--space-10)}.rm-view__pill-k{color:var(--color-primary);font-weight:var(--weight-semibold)}.rm-view__pill-tip{min-width:0}.rm-view__pill-v{display:-webkit-box;-webkit-box-orient:vertical;color:var(--text-primary);font-weight:var(--weight-medium);-webkit-line-clamp:3;overflow:hidden}.rm-view__foot{border-top:1px solid var(--border-soft);color:var(--text-muted);display:flex;flex-wrap:wrap;font-size:var(--font-11);gap:var(--space-16);padding-top:var(--space-10)}.rm-view__foot span{align-items:center;display:inline-flex;gap:var(--space-4)}.rm-view__confirm{align-items:center;background:var(--color-danger-bg);border:1px solid var(--color-danger);border-radius:var(--radius-md);color:var(--color-danger);display:flex;flex-wrap:wrap;font-size:var(--font-13);gap:var(--space-12);margin-top:var(--space-12);padding:var(--space-8) var(--space-12)}.rm-view__confirm>span{align-items:center;display:inline-flex;flex:1 1 220px;gap:var(--space-8)}.rm-view__confirm-actions{display:flex;flex-shrink:0;gap:var(--space-8)}
423
424
  </style>
@@ -14,11 +14,20 @@
14
14
 
15
15
  Le texte passe par un slot nomme plutot que par une prop : un lien ou une
16
16
  mise en forme dans une bulle n'obligent plus a sortir du composant.
17
+
18
+ La bulle sort par un `Teleport` et se place sur la fenetre (cf.
19
+ `useRmFloating`) : posee dans l'element, elle etait coupee par toute carte a
20
+ debordement cache, un tableau a hauteur bornee ou un tiroir. Le `Teleport`
21
+ ne s'active que pendant qu'elle se montre. Le survol passe donc par des
22
+ evenements plutot que par `:hover` : la bulle n'est plus une enfant de
23
+ l'element dans le DOM. Elle reste ouverte quand le pointeur passe de
24
+ l'element a elle, pour qu'un lien pose dedans reste cliquable.
17
25
  -->
18
26
  <script setup lang="ts">
19
- import { useId } from 'vue'
27
+ import { onBeforeUnmount, ref, useId } from 'vue'
28
+ import { useRmFloating } from '../composables/useRmFloating'
20
29
 
21
- withDefaults(defineProps<{
30
+ const props = withDefaults(defineProps<{
22
31
  /** Texte de la bulle. Ignore si le slot `content` est fourni. */
23
32
  text?: string
24
33
  /** Cote ou la bulle sort, par rapport a l'element survole. */
@@ -34,34 +43,84 @@ withDefaults(defineProps<{
34
43
  * lire.
35
44
  */
36
45
  full?: boolean
46
+ /**
47
+ * Largeur maximale de la bulle (220px par defaut). Une prop et non un
48
+ * `:deep()` chez l'appelant : sortie par le `Teleport`, la bulle n'est plus
49
+ * sous l'element dans le DOM et aucun selecteur descendant ne l'atteint.
50
+ */
51
+ maxWidth?: string
37
52
  }>(), {
38
53
  text: '',
39
54
  dir: 'top',
40
55
  disabled: false,
41
56
  full: false,
57
+ maxWidth: undefined,
42
58
  })
43
59
 
44
60
  const id = `rm-tooltip-${useId()}`
61
+
62
+ const rootRef = ref<HTMLElement>()
63
+ const bubbleRef = ref<HTMLElement>()
64
+ const visible = ref(false)
65
+ const floating = useRmFloating(rootRef, bubbleRef, {
66
+ kind: 'bubble',
67
+ side: props.dir,
68
+ offset: props.dir === 'top' || props.dir === 'bottom' ? 6 : 8,
69
+ })
70
+
71
+ let hideId: ReturnType<typeof setTimeout> | undefined
72
+
73
+ function show() {
74
+ if (props.disabled) return
75
+ clearTimeout(hideId)
76
+ visible.value = true
77
+ floating.open()
78
+ }
79
+
80
+ /** Un court delai laisse au pointeur le temps de passer de l'element a la bulle. */
81
+ function hide() {
82
+ clearTimeout(hideId)
83
+ hideId = setTimeout(() => {
84
+ visible.value = false
85
+ floating.close()
86
+ }, 80)
87
+ }
88
+
89
+ onBeforeUnmount(() => clearTimeout(hideId))
45
90
  </script>
46
91
 
47
92
  <template>
48
93
  <!-- `aria-describedby` est pose en permanence : un lecteur d'ecran lit la
49
94
  bulle a l'arrivee sur l'element, sans attendre un survol qu'il ne fait
50
95
  pas. C'est le CSS seul qui la montre a l'oeil. -->
51
- <span class="rm-tooltip" :aria-describedby="disabled ? undefined : id">
96
+ <span
97
+ ref="rootRef"
98
+ class="rm-tooltip"
99
+ :aria-describedby="disabled ? undefined : id"
100
+ @mouseenter="show"
101
+ @mouseleave="hide"
102
+ @focusin="show"
103
+ @focusout="hide"
104
+ >
52
105
  <slot />
53
- <span
54
- v-if="!disabled"
55
- :id="id"
56
- role="tooltip"
57
- class="rm-tooltip__bubble"
58
- :class="[`rm-tooltip__bubble--${dir}`, { 'rm-tooltip__bubble--full': full }]"
59
- >
60
- <slot name="content">{{ text }}</slot>
61
- </span>
106
+ <Teleport to="body" :disabled="!visible">
107
+ <span
108
+ v-if="!disabled"
109
+ :id="id"
110
+ ref="bubbleRef"
111
+ role="tooltip"
112
+ class="rm-tooltip__bubble"
113
+ :class="{ 'rm-tooltip__bubble--full': full, 'is-visible': visible }"
114
+ :style="[visible ? floating.style.value : {}, maxWidth ? { maxWidth } : {}]"
115
+ @mouseenter="show"
116
+ @mouseleave="hide"
117
+ >
118
+ <slot name="content">{{ text }}</slot>
119
+ </span>
120
+ </Teleport>
62
121
  </span>
63
122
  </template>
64
123
 
65
124
  <style scoped>
66
- .rm-tooltip{display:inline-flex;position:relative;width:-moz-fit-content;width:fit-content}.rm-tooltip__bubble{background:var(--r-color-gray-900);border-radius:var(--radius-sm,4px);color:var(--r-color-white);display:-webkit-box;font-size:var(--font-13,13px);font-weight:400;-webkit-line-clamp:4;line-height:1.6;max-width:220px;padding:var(--space-4) var(--space-12);position:absolute;text-align:left;white-space:pre-line;width:-moz-max-content;width:max-content;z-index:60;-webkit-box-orient:vertical;opacity:0;overflow:hidden;transition:opacity var(--duration-fast,.15s) var(--ease-out,ease);visibility:hidden}.rm-tooltip__bubble--full{display:block;overflow:visible}.rm-tooltip:focus-within .rm-tooltip__bubble,.rm-tooltip:hover .rm-tooltip__bubble{opacity:1;visibility:visible}.rm-tooltip__bubble--top{bottom:calc(100% + 6px);left:50%;transform:translateX(-50%)}.rm-tooltip__bubble--bottom{left:50%;top:calc(100% + 6px);transform:translateX(-50%)}.rm-tooltip__bubble--right{left:calc(100% + 8px);top:50%;transform:translateY(-50%)}.rm-tooltip__bubble--left{right:calc(100% + 8px);top:50%;transform:translateY(-50%)}
125
+ .rm-tooltip{display:inline-flex;position:relative;width:-moz-fit-content;width:fit-content}.rm-tooltip__bubble{background:var(--r-color-gray-900);border-radius:var(--radius-sm,4px);color:var(--r-color-white);display:-webkit-box;font-size:var(--font-13,13px);font-weight:400;-webkit-line-clamp:4;line-height:1.6;max-width:220px;padding:var(--space-4) var(--space-12);position:fixed;text-align:left;white-space:pre-line;width:-moz-max-content;width:max-content;z-index:calc(var(--z-popover, 1000) + 1);-webkit-box-orient:vertical;opacity:0;overflow:hidden;transition:opacity var(--duration-fast,.15s) var(--ease-out,ease);visibility:hidden}.rm-tooltip__bubble--full{display:block;overflow:visible}.rm-tooltip__bubble.is-visible{opacity:1;visibility:visible}
67
126
  </style>
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Placement d'une couche flottante (panneau de select, bulle d'aide) sur la
3
+ * fenetre plutot que dans son conteneur.
4
+ *
5
+ * Posee en `position: absolute` a cote de son champ, une couche flottante est
6
+ * coupee par le premier ancetre qui masque son debordement : une `RmCard` ou
7
+ * une `RmTableCard` (qui cachent le leur pour que la table suive les coins
8
+ * arrondis), un tableau a hauteur bornee, un tiroir. Aucun reglage d'`overflow`
9
+ * chez l'appelant ne la rattrape sans casser autre chose.
10
+ *
11
+ * La regle du design system est donc la suivante : TOUTE couche flottante sort
12
+ * par un `<Teleport to="body">` et se place en `position: fixed` par ce
13
+ * composable. Le `Teleport` se desactive tant que la couche est fermee
14
+ * (`:disabled="!open"`) : une page de cinq cents bulles d'aide ne pose pas
15
+ * cinq cents noeuds dans `body`, seule celle qu'on lit y passe.
16
+ *
17
+ * Le composable suit le champ tant que la couche est ouverte : posee sur la
18
+ * fenetre, elle resterait sinon la ou le champ etait avant que la page (ou un
19
+ * tiroir) ne defile.
20
+ */
21
+ import { type Ref } from 'vue';
22
+ export interface RmFloatingOptions {
23
+ /**
24
+ * `panel` : sous le champ, calee a gauche, et bascule au-dessus quand la
25
+ * place manque dessous (un panneau de select). `bubble` : centree sur le
26
+ * champ, du cote `side` (une bulle d'aide).
27
+ */
28
+ kind?: 'panel' | 'bubble';
29
+ /** Cote ou sort une bulle. Ignore pour un panneau. */
30
+ side?: 'bottom' | 'top' | 'left' | 'right';
31
+ /** Ecart entre le champ et la couche, en pixels. */
32
+ offset?: number;
33
+ /** La couche fait au moins la largeur du champ (panneau de select). */
34
+ matchWidth?: boolean;
35
+ /** Hauteur qu'une liste occupe en pratique : en dessous, la couche bascule. */
36
+ needed?: number;
37
+ }
38
+ export declare function useRmFloating(anchor: Ref<HTMLElement | undefined>, floating: Ref<HTMLElement | undefined>, options?: RmFloatingOptions): {
39
+ style: Ref<Record<string, string>, Record<string, string>>;
40
+ place: () => void;
41
+ open: () => Promise<void>;
42
+ close: () => void;
43
+ };
@@ -0,0 +1,61 @@
1
+ import { nextTick, onBeforeUnmount, ref } from "vue";
2
+ const GAP = 8;
3
+ export function useRmFloating(anchor, floating, options = {}) {
4
+ const { kind = "panel", side = "top", offset = GAP, matchWidth = false, needed = 320 } = options;
5
+ const style = ref({});
6
+ let tracking = false;
7
+ function place() {
8
+ const rect = anchor.value?.getBoundingClientRect();
9
+ if (!rect) return;
10
+ const vw = window.innerWidth;
11
+ const vh = window.innerHeight;
12
+ const width = floating.value?.offsetWidth ?? 0;
13
+ if (kind === "panel") {
14
+ const below = vh - rect.bottom - offset - GAP;
15
+ const above = rect.top - offset - GAP;
16
+ const flip = below < needed && above > below;
17
+ const left = Math.max(GAP, Math.min(rect.left, vw - width - GAP));
18
+ style.value = {
19
+ position: "fixed",
20
+ left: `${left}px`,
21
+ maxHeight: `${Math.max(160, Math.min(500, flip ? above : below))}px`,
22
+ ...matchWidth ? { minWidth: `max(230px, ${rect.width}px)` } : {},
23
+ ...flip ? { bottom: `${vh - rect.top + offset}px` } : { top: `${rect.bottom + offset}px` }
24
+ };
25
+ return;
26
+ }
27
+ const cx = rect.left + rect.width / 2;
28
+ const cy = rect.top + rect.height / 2;
29
+ const clampX = (x) => Math.max(GAP + width / 2, Math.min(x, vw - GAP - width / 2));
30
+ const placements = {
31
+ top: { left: clampX(cx), top: rect.top - offset, transform: "translate(-50%, -100%)" },
32
+ bottom: { left: clampX(cx), top: rect.bottom + offset, transform: "translateX(-50%)" },
33
+ right: { left: rect.right + offset, top: cy, transform: "translateY(-50%)" },
34
+ left: { left: rect.left - offset, top: cy, transform: "translate(-100%, -50%)" }
35
+ };
36
+ const p = placements[side];
37
+ style.value = {
38
+ position: "fixed",
39
+ left: `${p.left}px`,
40
+ top: `${p.top}px`,
41
+ transform: p.transform
42
+ };
43
+ }
44
+ async function open() {
45
+ place();
46
+ await nextTick();
47
+ place();
48
+ if (tracking) return;
49
+ tracking = true;
50
+ window.addEventListener("scroll", place, true);
51
+ window.addEventListener("resize", place);
52
+ }
53
+ function close() {
54
+ if (!tracking) return;
55
+ tracking = false;
56
+ window.removeEventListener("scroll", place, true);
57
+ window.removeEventListener("resize", place);
58
+ }
59
+ onBeforeUnmount(close);
60
+ return { style, place, open, close };
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.201",
3
+ "version": "0.1.203",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",