@tekkare/romulus 0.1.174 → 0.1.175

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.174",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -0,0 +1,137 @@
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
+ const below = box.bottom + 6
64
+ const above = box.top - size.height - 6
65
+ const top = below + size.height > window.innerHeight - EDGE && above > EDGE ? above : below
66
+
67
+ style.value = {
68
+ position: 'fixed',
69
+ left: `${Math.round(left)}px`,
70
+ top: `${Math.round(top)}px`,
71
+ }
72
+ }
73
+
74
+ function close() {
75
+ emit('update:modelValue', false)
76
+ }
77
+
78
+ function onDocumentClick(event: MouseEvent) {
79
+ if (!props.modelValue) return
80
+ const target = event.target as Node
81
+ if (panel.value?.contains(target)) return
82
+ // Le bouton qui ouvre est souvent une bascule : le laisser gerer son clic,
83
+ // sinon le panneau se ferme ici et se rouvre juste apres.
84
+ if (props.anchor?.contains(target)) return
85
+ close()
86
+ }
87
+
88
+ function onEscape(event: KeyboardEvent) {
89
+ if (event.key === 'Escape' && props.modelValue) close()
90
+ }
91
+
92
+ watch(
93
+ () => props.modelValue,
94
+ async (open) => {
95
+ if (!open) return
96
+ // Le panneau doit etre rendu pour etre mesure : sa hauteur depend de son
97
+ // contenu, et c'est elle qui decide du basculement au-dessus.
98
+ await nextTick()
99
+ place()
100
+ },
101
+ )
102
+
103
+ onMounted(() => {
104
+ document.addEventListener('click', onDocumentClick)
105
+ document.addEventListener('keydown', onEscape)
106
+ window.addEventListener('scroll', place, true)
107
+ window.addEventListener('resize', place)
108
+ })
109
+
110
+ onBeforeUnmount(() => {
111
+ document.removeEventListener('click', onDocumentClick)
112
+ document.removeEventListener('keydown', onEscape)
113
+ window.removeEventListener('scroll', place, true)
114
+ window.removeEventListener('resize', place)
115
+ })
116
+ </script>
117
+
118
+ <template>
119
+ <Teleport to="body">
120
+ <Transition name="rm-pop">
121
+ <div
122
+ v-if="modelValue"
123
+ ref="panel"
124
+ class="rm-pop"
125
+ role="dialog"
126
+ :aria-label="label"
127
+ :style="panelStyle"
128
+ >
129
+ <slot />
130
+ </div>
131
+ </Transition>
132
+ </Teleport>
133
+ </template>
134
+
135
+ <style scoped>
136
+ .rm-pop{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);max-width:min(92vw,360px);min-width:220px;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}}
137
+ </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.175",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",