@tekkare/romulus 0.1.174 → 0.1.176

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.173",
7
+ "version": "0.1.175",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -0,0 +1,145 @@
1
+ <!--
2
+ RmPopover - un petit panneau accroche a son bouton.
3
+
4
+ A preferer a une modale quand le choix est court et se fait dans la foulee
5
+ du geste : une modale arrete tout, voile la page et reclame une fermeture,
6
+ la ou l'utilisateur voulait juste designer quelque chose et continuer. La
7
+ modale reste la bonne forme pour une question qui doit etre tranchee, ou un
8
+ formulaire de plusieurs champs.
9
+
10
+ Il est teleporte dans `<body>` et place en coordonnees de fenetre : dans le
11
+ flux, il serait coupe par le `overflow` des cartes et des colonnes, et un
12
+ `position: absolute` le placerait par rapport au premier ancetre positionne,
13
+ qui n'est pas le bouton. Il bascule au-dessus de l'ancre quand le bas de la
14
+ fenetre manque, et se recale au defilement comme au redimensionnement.
15
+
16
+ Il se ferme sur Echap, sur un clic au-dehors, et sur `update:modelValue`.
17
+ L'ancre est passee par la page (`anchor`), pas devinee : c'est elle qui sait
18
+ quel bouton a ouvert le panneau.
19
+ -->
20
+ <script setup lang="ts">
21
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
22
+
23
+ const props = withDefaults(
24
+ defineProps<{
25
+ /** Ouvert ou ferme. */
26
+ modelValue: boolean
27
+ /** Element sous lequel le panneau se place. */
28
+ anchor?: HTMLElement | null
29
+ /** Largeur du panneau. Par defaut il s'ajuste a son contenu, sans depasser. */
30
+ width?: string
31
+ /** Nom accessible : le panneau est un `dialog`, il en faut un. */
32
+ label?: string
33
+ /**
34
+ * Cote de l'ancre sur lequel le panneau s'aligne. `end` colle son bord
35
+ * droit a celui du bouton (le cas des boutons en fin de ligne), `start`
36
+ * son bord gauche.
37
+ */
38
+ align?: 'start' | 'end'
39
+ }>(),
40
+ { anchor: null, width: '', label: undefined, align: 'end' },
41
+ )
42
+
43
+ const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
44
+
45
+ const panel = ref<HTMLElement | null>(null)
46
+ const style = ref<Record<string, string>>({})
47
+
48
+ /** Marge minimale entre le panneau et un bord de la fenetre. */
49
+ const EDGE = 8
50
+
51
+ const panelStyle = computed(() => ({
52
+ ...style.value,
53
+ ...(props.width ? { width: props.width } : {}),
54
+ }))
55
+
56
+ function place() {
57
+ const box = props.anchor?.getBoundingClientRect()
58
+ const size = panel.value?.getBoundingClientRect()
59
+ if (!box || !size) return
60
+
61
+ const wanted = props.align === 'start' ? box.left : box.right - size.width
62
+ const left = Math.max(EDGE, Math.min(wanted, window.innerWidth - size.width - EDGE))
63
+
64
+ // Le cote le plus large l'emporte : sous le bouton tant que ca tient, au
65
+ // dessus sinon. Une liste plus haute que la fenetre ne peut tenir nulle
66
+ // part : elle prend alors la place disponible et defile, plutot que de
67
+ // sortir par le bas comme le faisait un simple basculement.
68
+ const room = { below: window.innerHeight - box.bottom - 6 - EDGE, above: box.top - 6 - EDGE }
69
+ const below = room.below >= size.height || room.below >= room.above
70
+ const max = Math.max(120, Math.round(below ? room.below : room.above))
71
+ const height = Math.min(size.height, max)
72
+ const top = below ? box.bottom + 6 : box.top - 6 - height
73
+
74
+ style.value = {
75
+ position: 'fixed',
76
+ left: `${Math.round(left)}px`,
77
+ top: `${Math.round(Math.max(EDGE, top))}px`,
78
+ maxHeight: `${max}px`,
79
+ }
80
+ }
81
+
82
+ function close() {
83
+ emit('update:modelValue', false)
84
+ }
85
+
86
+ function onDocumentClick(event: MouseEvent) {
87
+ if (!props.modelValue) return
88
+ const target = event.target as Node
89
+ if (panel.value?.contains(target)) return
90
+ // Le bouton qui ouvre est souvent une bascule : le laisser gerer son clic,
91
+ // sinon le panneau se ferme ici et se rouvre juste apres.
92
+ if (props.anchor?.contains(target)) return
93
+ close()
94
+ }
95
+
96
+ function onEscape(event: KeyboardEvent) {
97
+ if (event.key === 'Escape' && props.modelValue) close()
98
+ }
99
+
100
+ watch(
101
+ () => props.modelValue,
102
+ async (open) => {
103
+ if (!open) return
104
+ // Le panneau doit etre rendu pour etre mesure : sa hauteur depend de son
105
+ // contenu, et c'est elle qui decide du basculement au-dessus.
106
+ await nextTick()
107
+ place()
108
+ },
109
+ )
110
+
111
+ onMounted(() => {
112
+ document.addEventListener('click', onDocumentClick)
113
+ document.addEventListener('keydown', onEscape)
114
+ window.addEventListener('scroll', place, true)
115
+ window.addEventListener('resize', place)
116
+ })
117
+
118
+ onBeforeUnmount(() => {
119
+ document.removeEventListener('click', onDocumentClick)
120
+ document.removeEventListener('keydown', onEscape)
121
+ window.removeEventListener('scroll', place, true)
122
+ window.removeEventListener('resize', place)
123
+ })
124
+ </script>
125
+
126
+ <template>
127
+ <Teleport to="body">
128
+ <Transition name="rm-pop">
129
+ <div
130
+ v-if="modelValue"
131
+ ref="panel"
132
+ class="rm-pop"
133
+ role="dialog"
134
+ :aria-label="label"
135
+ :style="panelStyle"
136
+ >
137
+ <slot />
138
+ </div>
139
+ </Transition>
140
+ </Teleport>
141
+ </template>
142
+
143
+ <style scoped>
144
+ .rm-pop{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);box-sizing:border-box;max-width:min(92vw,360px);min-width:220px;overflow-y:auto;padding:var(--space-12);z-index:var(--z-drawer)}.rm-pop-enter-active,.rm-pop-leave-active{transition:transform var(--duration-fast) var(--ease-out),opacity var(--duration-fast) linear}.rm-pop-enter-from,.rm-pop-leave-to{opacity:0;transform:translateY(-4px)}@media (prefers-reduced-motion:reduce){.rm-pop-enter-active,.rm-pop-leave-active{transition:opacity var(--duration-fast) linear}.rm-pop-enter-from,.rm-pop-leave-to{transform:none}}
145
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.174",
3
+ "version": "0.1.176",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",