@tekkare/romulus 0.1.200 → 0.1.202
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 +1 -1
- package/dist/runtime/components/AlertCard.vue +13 -4
- package/dist/runtime/components/DropdownSelect.vue +22 -36
- package/dist/runtime/components/MultipleSelect.vue +26 -3
- package/dist/runtime/components/SavedViewCard.vue +2 -1
- package/dist/runtime/components/Tooltip.vue +72 -13
- package/dist/runtime/composables/useRmFloating.d.ts +43 -0
- package/dist/runtime/composables/useRmFloating.js +61 -0
- package/dist/runtime/utils/alerts.d.ts +10 -3
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -53,6 +53,7 @@ const emit = defineEmits<{
|
|
|
53
53
|
const FR = {
|
|
54
54
|
view: 'Vue',
|
|
55
55
|
brands: 'Marques',
|
|
56
|
+
project: 'Projet',
|
|
56
57
|
channel: 'Canaux',
|
|
57
58
|
cadence: 'Cadence',
|
|
58
59
|
tracked: 'éléments suivis',
|
|
@@ -112,6 +113,7 @@ const FR = {
|
|
|
112
113
|
const EN = {
|
|
113
114
|
view: 'View',
|
|
114
115
|
brands: 'Brands',
|
|
116
|
+
project: 'Project',
|
|
115
117
|
channel: 'Channels',
|
|
116
118
|
cadence: 'Cadence',
|
|
117
119
|
tracked: 'items tracked',
|
|
@@ -166,6 +168,13 @@ const EN = {
|
|
|
166
168
|
|
|
167
169
|
const T = computed(() => (props.lang === 'en' ? EN : FR))
|
|
168
170
|
|
|
171
|
+
/** Ce que la carte annonce sous le titre, selon le genre de l'alerte. */
|
|
172
|
+
const scope = computed(() => {
|
|
173
|
+
if (props.alert.kind === 'product') return { icon: 'Tag', label: T.value.brands }
|
|
174
|
+
if (props.alert.kind === 'project') return { icon: 'FolderSimple', label: T.value.project }
|
|
175
|
+
return { icon: 'BookmarkSimple', label: T.value.view }
|
|
176
|
+
})
|
|
177
|
+
|
|
169
178
|
/** Nombres ecrits comme on les lit : 19 751, pas 19751. */
|
|
170
179
|
const number = (value: number) =>
|
|
171
180
|
new Intl.NumberFormat(props.lang === 'en' ? 'en-GB' : 'fr-FR').format(value)
|
|
@@ -298,10 +307,10 @@ function pick(item: RmAlertMenuItem) {
|
|
|
298
307
|
</div>
|
|
299
308
|
|
|
300
309
|
<p v-if="alert.viewTitle" class="rm-alert__view">
|
|
301
|
-
<!-- Une alerte de produit ne surveille pas une vue :
|
|
302
|
-
telle ferait chercher une vue qui n'existe pas. -->
|
|
303
|
-
<RmIcon :name="
|
|
304
|
-
{{
|
|
310
|
+
<!-- Une alerte de produit ou de projet ne surveille pas une vue :
|
|
311
|
+
l'annoncer comme telle ferait chercher une vue qui n'existe pas. -->
|
|
312
|
+
<RmIcon :name="scope.icon" :size="13" />
|
|
313
|
+
{{ scope.label }} : {{ alert.viewTitle }}
|
|
305
314
|
<span v-if="alert.shared" class="rm-alert__shared">{{ T.shared }}</span>
|
|
306
315
|
</p>
|
|
307
316
|
|
|
@@ -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
|
-
|
|
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
|
-
:
|
|
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{
|
|
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
|
-
|
|
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
|
-
<
|
|
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;
|
|
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>
|
|
@@ -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-
|
|
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
|
|
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
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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:
|
|
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
|
+
}
|
|
@@ -30,9 +30,16 @@ export interface RmAlertMenuItem {
|
|
|
30
30
|
export interface RmAlert {
|
|
31
31
|
id: string;
|
|
32
32
|
title: string;
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Ce que l'alerte surveille : une vue, des marques, ou les documents
|
|
35
|
+
* ranges dans un projet. Le projet ne se cree pas depuis le formulaire
|
|
36
|
+
* (il s'active depuis la page du projet), d'ou un genre a part.
|
|
37
|
+
*/
|
|
38
|
+
kind?: RmAlertKind | 'project';
|
|
39
|
+
/**
|
|
40
|
+
* Nom de la vue surveillee, les marques d'une alerte de produit, ou le
|
|
41
|
+
* titre du projet.
|
|
42
|
+
*/
|
|
36
43
|
viewTitle?: string | null;
|
|
37
44
|
status: RmAlertStatus;
|
|
38
45
|
triggers: RmAlertTrigger[];
|