@tekkare/romulus 0.1.159 → 0.1.161

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.158",
7
+ "version": "0.1.160",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -162,22 +162,97 @@ const t = computed(() => WORDS[props.lang])
162
162
  const isOpen = ref(false)
163
163
  const removeSearch = ref('')
164
164
  const activePreset = ref<string>()
165
- const alignRight = ref(false)
166
165
  const rootRef = ref<HTMLElement>()
166
+ const panelRef = ref<HTMLElement>()
167
167
  const addRef = ref<HTMLInputElement>()
168
168
 
169
169
  /** Largeur du panneau, tenue en phase avec la regle `.rm-csel__panel`. */
170
170
  const PANEL_WIDTH = 820
171
171
 
172
+ /** Marge gardee contre les bords de la fenetre. */
173
+ const PANEL_GAP = 8
174
+
175
+ /**
176
+ * Largeur en dessous de laquelle un conteneur n'est pas pris pour cadre : une
177
+ * carte etroite qui cache son debordement reduirait le panneau a un couloir,
178
+ * alors qu'elle ne le decoupe plus (il est pose sur la fenetre).
179
+ */
180
+ const MIN_BOUNDS = 360
181
+
182
+ /**
183
+ * Le cadre dans lequel le panneau doit tenir.
184
+ *
185
+ * La fenetre ne suffit pas. Un filtre pose dans un tiroir colle au bord droit
186
+ * de l'ecran tient contre ce bord : cale a gauche le panneau sort du tiroir
187
+ * par la droite, cale a droite il en sort par la gauche et couvre la page.
188
+ * Dans les deux cas il flotte hors de la surface qui le porte, et le lecteur
189
+ * lit un panneau qui n'appartient visiblement plus a rien.
190
+ *
191
+ * On remonte donc jusqu'au premier ancetre qui borne son contenu (un tiroir,
192
+ * une modale, un panneau du cadre : tout ce qui n'a pas `overflow: visible`),
193
+ * et le panneau se range dedans. Sans ancetre de ce genre — le cas courant
194
+ * d'un filtre pose en pleine page — le cadre reste la fenetre, et le placement
195
+ * ne change pas.
196
+ */
197
+ function panelBounds(): { left: number, right: number } {
198
+ const viewport = { left: PANEL_GAP, right: window.innerWidth - PANEL_GAP }
199
+ let node = rootRef.value?.parentElement
200
+
201
+ while (node && node !== document.body) {
202
+ const style = getComputedStyle(node)
203
+ const clips = `${style.overflowX}${style.overflowY}`.replace(/visible/g, '') !== ''
204
+ if (clips) {
205
+ const box = node.getBoundingClientRect()
206
+ if (box.width >= MIN_BOUNDS) {
207
+ return {
208
+ left: Math.max(viewport.left, box.left + PANEL_GAP),
209
+ right: Math.min(viewport.right, box.right - PANEL_GAP),
210
+ }
211
+ }
212
+ }
213
+ node = node.parentElement
214
+ }
215
+
216
+ return viewport
217
+ }
218
+
172
219
  /**
173
- * Le panneau sort a gauche du champ par defaut. Pose sur un filtre de bout de
174
- * rangee, il depasserait de l'ecran : il se cale alors sur le bord droit du
175
- * champ.
220
+ * Le panneau est pose sur la fenetre, pas dans le champ : depose dans le flux,
221
+ * il etait coupe des que le filtre vivait dans un conteneur qui defile (un
222
+ * tiroir, une carte a debordement cache), et aucun reglage de `overflow` chez
223
+ * l'appelant ne le rattrapait.
224
+ *
225
+ * Il sort donc par un `Teleport` et se positionne a la main, sous le champ,
226
+ * cale a gauche du champ et ramene dans son cadre quand il en depasserait.
176
227
  */
228
+ const panelStyle = ref<Record<string, string>>({})
229
+
177
230
  function placePanel() {
178
231
  const rect = rootRef.value?.getBoundingClientRect()
179
232
  if (!rect) return
180
- alignRight.value = rect.left + PANEL_WIDTH > window.innerWidth - 8
233
+
234
+ const bounds = panelBounds()
235
+ const width = Math.min(PANEL_WIDTH, bounds.right - bounds.left)
236
+ // Cale a gauche du champ, puis ramene dans le cadre : le bord droit d'abord,
237
+ // le gauche ensuite, pour qu'un cadre plus etroit que le panneau laisse voir
238
+ // son debut plutot que sa fin.
239
+ const left = Math.max(bounds.left, Math.min(rect.left, bounds.right - width))
240
+
241
+ // Sous le champ, sauf quand il ne reste rien dessous : le panneau se pose
242
+ // alors au-dessus, et sa hauteur se borne a la place reellement libre.
243
+ const below = window.innerHeight - rect.bottom - PANEL_GAP * 2
244
+ const above = rect.top - PANEL_GAP * 2
245
+ const flip = below < 320 && above > below
246
+
247
+ panelStyle.value = {
248
+ position: 'fixed',
249
+ left: `${left}px`,
250
+ width: `${width}px`,
251
+ maxHeight: `${Math.max(240, flip ? above : below)}px`,
252
+ ...(flip
253
+ ? { bottom: `${window.innerHeight - rect.top + PANEL_GAP}px` }
254
+ : { top: `${rect.bottom + PANEL_GAP}px` }),
255
+ }
181
256
  }
182
257
 
183
258
  /** Les chaines nues valent libelle et valeur a la fois. */
@@ -227,13 +302,31 @@ watch(addInput, (value) => {
227
302
  }, 500)
228
303
  })
229
304
 
305
+ /**
306
+ * Le panneau suit le champ tant qu'il est ouvert : pose sur la fenetre, il
307
+ * resterait sinon la ou le champ etait avant que la page (ou le tiroir) ne
308
+ * defile.
309
+ */
310
+ function trackPanel() {
311
+ window.addEventListener('scroll', placePanel, true)
312
+ window.addEventListener('resize', placePanel)
313
+ }
314
+
315
+ function untrackPanel() {
316
+ window.removeEventListener('scroll', placePanel, true)
317
+ window.removeEventListener('resize', placePanel)
318
+ }
319
+
230
320
  function close() {
231
321
  isOpen.value = false
232
322
  document.removeEventListener('click', onDocumentClick)
323
+ untrackPanel()
233
324
  }
234
325
 
235
326
  function onDocumentClick(event: MouseEvent) {
236
- if (!rootRef.value?.contains(event.target as Node)) close()
327
+ const target = event.target as Node
328
+ if (rootRef.value?.contains(target) || panelRef.value?.contains(target)) return
329
+ close()
237
330
  }
238
331
 
239
332
  async function toggle() {
@@ -242,6 +335,7 @@ async function toggle() {
242
335
 
243
336
  isOpen.value = true
244
337
  placePanel()
338
+ trackPanel()
245
339
  rmPanelOpened(close)
246
340
  document.addEventListener('click', onDocumentClick)
247
341
  await nextTick()
@@ -292,6 +386,7 @@ onUnmounted(() => {
292
386
  unregister?.()
293
387
  clearTimeout(debounceId)
294
388
  document.removeEventListener('click', onDocumentClick)
389
+ untrackPanel()
295
390
  })
296
391
  </script>
297
392
 
@@ -324,11 +419,14 @@ onUnmounted(() => {
324
419
  <PhCaretDown :size="16" />
325
420
  </button>
326
421
 
422
+ <Teleport to="body">
327
423
  <div
328
424
  v-show="isOpen"
425
+ ref="panelRef"
329
426
  class="rm-csel__panel"
330
- :class="{ 'rm-csel__panel--right': alignRight }"
427
+ :style="panelStyle"
331
428
  @click.stop
429
+ @keydown.escape="close"
332
430
  >
333
431
  <div class="rm-csel__column">
334
432
  <div v-if="presets && presets.length > 0" class="rm-csel__presets">
@@ -437,9 +535,10 @@ onUnmounted(() => {
437
535
  </div>
438
536
  </div>
439
537
  </div>
538
+ </Teleport>
440
539
  </div>
441
540
  </template>
442
541
 
443
542
  <style scoped>
444
- .rm-csel{align-items:stretch;flex-direction:column;position:relative;width:196px}.rm-csel,.rm-csel__label{display:flex;gap:var(--space-4)}.rm-csel__label{align-items:center;color:var(--text-secondary);font-size:var(--font-12,12px);line-height:14px}.rm-csel__info{background:none;border:none;color:var(--text-muted);cursor:pointer;display:flex;padding:0}.rm-csel__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-csel__field:hover:not(.is-disabled){border-color:rgba(var(--brand-rgb),.3)}.rm-csel__field.is-open{border-color:var(--color-primary)}.rm-csel__field.is-disabled{background:var(--bg-surface-2);color:var(--text-muted);cursor:not-allowed}.rm-csel__field:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.rm-csel__value-slot{display:block;flex:1;min-width:0}.rm-csel__value{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-csel__value.is-empty{color:var(--text-muted);font-style:italic;font-weight:400}.rm-csel__field :deep(svg){color:var(--text-secondary);flex-shrink:0}.rm-csel__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-csel__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;left:0;margin-top:8px;max-width:calc(100vw - 32px);position:absolute;right:auto;top:100%;width:820px;z-index:12}.rm-csel__panel--right{left:auto;right:0}.rm-csel__column{display:flex;flex:1;flex-direction:column;gap:var(--space-8);min-width:0;padding:var(--space-16)}.rm-csel__column--right{border-left:1px solid var(--border-soft);flex:0 0 300px}.rm-csel__presets{border-bottom:1px solid var(--border-soft);display:flex;flex-direction:column;gap:var(--space-8);padding-bottom:var(--space-8)}.rm-csel__pills{display:flex;flex-wrap:wrap;gap:var(--space-4)}.rm-csel__pill{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:9999px;color:var(--text-secondary);cursor:pointer;font-family:inherit;font-size:var(--font-12,12px);padding:var(--space-4) var(--space-10)}.rm-csel__pill.is-active,.rm-csel__pill:hover{border-color:var(--color-primary);color:var(--color-primary)}.rm-csel__pill.is-active{background:rgba(var(--brand-rgb),.1);font-weight:700}.rm-csel__head{align-items:center;display:flex;gap:var(--space-8);justify-content:space-between}.rm-csel__section{color:var(--text-secondary);font-size:var(--font-12,12px);font-weight:700;margin:0}.rm-csel__action{background:none;border:none;color:var(--color-primary);cursor:pointer;font-family:inherit;font-size:var(--font-12,12px);font-weight:600;padding:0}.rm-csel__action:disabled{color:var(--text-muted);cursor:not-allowed}.rm-csel__action:hover:not(:disabled){text-decoration:underline}.rm-csel__action--clear:not(:disabled){color:var(--color-danger)}.rm-csel__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-6) var(--space-10)}.rm-csel__search :deep(svg){color:var(--text-muted);flex-shrink:0}.rm-csel__search-input{background:none;border:none;color:var(--text-primary);font-family:inherit;font-size:var(--font-13,13px);width:100%}.rm-csel__search-input::-moz-placeholder{color:var(--text-muted);font-size:var(--font-12,12px)}.rm-csel__search-input::placeholder{color:var(--text-muted);font-size:var(--font-12,12px)}.rm-csel__search-input:focus-visible{outline:none}.rm-csel__state{color:var(--text-secondary);font-size:var(--font-13,13px);font-style:italic;padding-top:var(--space-8)}.rm-csel__loading{display:flex;flex-direction:column;gap:var(--space-10);padding-top:var(--space-8)}.rm-csel__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-csel__list{display:flex;flex-direction:column;gap:var(--space-4);height:260px;overflow-x:hidden;overflow-y:auto;scrollbar-width:thin}.rm-csel__option{align-items:flex-start;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-13,13px);gap:var(--space-8);line-height:150%;min-width:0;padding:var(--space-2) var(--space-4);text-align:left}.rm-csel__option span{min-width:0;overflow-wrap:anywhere}.rm-csel__option :deep(svg){background:var(--bg-surface);border-radius:50%;flex-shrink:0;margin-top:3px}.rm-csel__hint{color:var(--text-muted);flex-shrink:0;font-size:var(--font-11,11px);white-space:nowrap}.rm-csel__option:hover{background:rgba(var(--brand-rgb),.05)}.rm-csel__option:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px}.rm-csel__option--add :deep(svg){color:var(--color-primary)}.rm-csel__option--remove :deep(svg){color:var(--color-danger)}@media (max-width:767px){.rm-csel__panel{flex-direction:column;width:calc(100vw - 32px)}.rm-csel__column--right{border-left:none;border-top:1px solid var(--border-soft)}.rm-csel__list{height:180px}}
543
+ .rm-csel{align-items:stretch;flex-direction:column;position:relative;width:196px}.rm-csel,.rm-csel__label{display:flex;gap:var(--space-4)}.rm-csel__label{align-items:center;color:var(--text-secondary);font-size:var(--font-12,12px);line-height:14px}.rm-csel__info{background:none;border:none;color:var(--text-muted);cursor:pointer;display:flex;padding:0}.rm-csel__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-csel__field:hover:not(.is-disabled){border-color:rgba(var(--brand-rgb),.3)}.rm-csel__field.is-open{border-color:var(--color-primary)}.rm-csel__field.is-disabled{background:var(--bg-surface-2);color:var(--text-muted);cursor:not-allowed}.rm-csel__field:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.rm-csel__value-slot{display:block;flex:1;min-width:0}.rm-csel__value{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-csel__value.is-empty{color:var(--text-muted);font-style:italic;font-weight:400}.rm-csel__field :deep(svg){color:var(--text-secondary);flex-shrink:0}.rm-csel__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-csel__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;max-height:calc(100vh - 32px);max-width:calc(100vw - 16px);overflow:hidden;position:fixed;z-index:var(--z-popover,1000)}.rm-csel__column{display:flex;flex:1;flex-direction:column;gap:var(--space-8);min-width:0;padding:var(--space-16)}.rm-csel__column--right{border-left:1px solid var(--border-soft);flex:0 0 300px}.rm-csel__presets{border-bottom:1px solid var(--border-soft);display:flex;flex-direction:column;gap:var(--space-8);padding-bottom:var(--space-8)}.rm-csel__pills{display:flex;flex-wrap:wrap;gap:var(--space-4)}.rm-csel__pill{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:9999px;color:var(--text-secondary);cursor:pointer;font-family:inherit;font-size:var(--font-12,12px);padding:var(--space-4) var(--space-10)}.rm-csel__pill.is-active,.rm-csel__pill:hover{border-color:var(--color-primary);color:var(--color-primary)}.rm-csel__pill.is-active{background:rgba(var(--brand-rgb),.1);font-weight:700}.rm-csel__head{align-items:center;display:flex;gap:var(--space-8);justify-content:space-between}.rm-csel__section{color:var(--text-secondary);font-size:var(--font-12,12px);font-weight:700;margin:0}.rm-csel__action{background:none;border:none;color:var(--color-primary);cursor:pointer;font-family:inherit;font-size:var(--font-12,12px);font-weight:600;padding:0}.rm-csel__action:disabled{color:var(--text-muted);cursor:not-allowed}.rm-csel__action:hover:not(:disabled){text-decoration:underline}.rm-csel__action--clear:not(:disabled){color:var(--color-danger)}.rm-csel__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-6) var(--space-10)}.rm-csel__search :deep(svg){color:var(--text-muted);flex-shrink:0}.rm-csel__search-input{background:none;border:none;color:var(--text-primary);font-family:inherit;font-size:var(--font-13,13px);width:100%}.rm-csel__search-input::-moz-placeholder{color:var(--text-muted);font-size:var(--font-12,12px)}.rm-csel__search-input::placeholder{color:var(--text-muted);font-size:var(--font-12,12px)}.rm-csel__search-input:focus-visible{outline:none}.rm-csel__state{color:var(--text-secondary);font-size:var(--font-13,13px);font-style:italic;padding-top:var(--space-8)}.rm-csel__loading{display:flex;flex-direction:column;gap:var(--space-10);padding-top:var(--space-8)}.rm-csel__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-csel__list{display:flex;flex-direction:column;gap:var(--space-4);height:260px;overflow-x:hidden;overflow-y:auto;scrollbar-width:thin}.rm-csel__option{align-items:flex-start;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-13,13px);gap:var(--space-8);line-height:150%;min-width:0;padding:var(--space-2) var(--space-4);text-align:left}.rm-csel__option span{min-width:0;overflow-wrap:anywhere}.rm-csel__option :deep(svg){background:var(--bg-surface);border-radius:50%;flex-shrink:0;margin-top:3px}.rm-csel__hint{color:var(--text-muted);flex-shrink:0;font-size:var(--font-11,11px);white-space:nowrap}.rm-csel__option:hover{background:rgba(var(--brand-rgb),.05)}.rm-csel__option:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px}.rm-csel__option--add :deep(svg){color:var(--color-primary)}.rm-csel__option--remove :deep(svg){color:var(--color-danger)}@media (max-width:767px){.rm-csel__panel{flex-direction:column}.rm-csel__column--right{border-left:none;border-top:1px solid var(--border-soft)}.rm-csel__list{height:180px}}
445
544
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.159",
3
+ "version": "0.1.161",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",