@tekkare/romulus 0.1.137 → 0.1.139
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
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
-->
|
|
12
12
|
<script setup lang="ts">
|
|
13
13
|
import { computed } from 'vue'
|
|
14
|
-
import type { RmAlertEvent } from '../utils/alerts'
|
|
14
|
+
import type { RmAlertChange, RmAlertEvent } from '../utils/alerts'
|
|
15
15
|
|
|
16
16
|
const props = withDefaults(
|
|
17
17
|
defineProps<{
|
|
@@ -57,6 +57,29 @@ function shown(event: RmAlertEvent) {
|
|
|
57
57
|
return (event.changes ?? []).slice(0, MAX_LINES)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Les changements ranges par ce qu'ils touchent.
|
|
62
|
+
*
|
|
63
|
+
* Un historique de prix qui bouge produit une ligne par champ, toutes
|
|
64
|
+
* prefixees du meme « Historique du prix · 15 sept. 2026 ». Lu en colonne dans
|
|
65
|
+
* le rail, le prefixe occupait les deux tiers de chaque ligne et ce qui avait
|
|
66
|
+
* change se cherchait a droite : il passe en tete, une fois, et les lignes ne
|
|
67
|
+
* portent plus que leur champ.
|
|
68
|
+
*
|
|
69
|
+
* Groupes CONSECUTIFS, jamais rassembles : l'ordre des changements est celui
|
|
70
|
+
* du moteur, et le bousculer ferait mentir « et 3 autres champs ».
|
|
71
|
+
*/
|
|
72
|
+
function blocks(event: RmAlertEvent) {
|
|
73
|
+
const out: Array<{ group: string | null; changes: RmAlertChange[] }> = []
|
|
74
|
+
for (const change of shown(event)) {
|
|
75
|
+
const group = change.group || null
|
|
76
|
+
const last = out[out.length - 1]
|
|
77
|
+
if (last && last.group === group) last.changes.push(change)
|
|
78
|
+
else out.push({ group, changes: [change] })
|
|
79
|
+
}
|
|
80
|
+
return out
|
|
81
|
+
}
|
|
82
|
+
|
|
60
83
|
function hidden(event: RmAlertEvent) {
|
|
61
84
|
return Math.max((event.changes ?? []).length - MAX_LINES, 0)
|
|
62
85
|
}
|
|
@@ -76,18 +99,36 @@ function hidden(event: RmAlertEvent) {
|
|
|
76
99
|
</span>
|
|
77
100
|
<time class="rm-alert-events__date">{{ event.dateLabel }}</time>
|
|
78
101
|
</header>
|
|
102
|
+
<!-- Sur sa ligne, et non a cote du type : le rail fait 260px, ou
|
|
103
|
+
« Sorties Hospitalieres (PHMEV) » a cote de « Sorti du perimetre »
|
|
104
|
+
se coupait en chemin. -->
|
|
105
|
+
<p v-if="event.source" class="rm-alert-events__source">{{ event.source }}</p>
|
|
79
106
|
<p class="rm-alert-events__title">{{ event.title }}</p>
|
|
80
107
|
<p v-if="event.subtitle" class="rm-alert-events__sub">{{ event.subtitle }}</p>
|
|
81
|
-
<
|
|
82
|
-
v-for="(
|
|
108
|
+
<div
|
|
109
|
+
v-for="(block, index) in blocks(event)"
|
|
83
110
|
:key="index"
|
|
84
|
-
class="rm-alert-
|
|
111
|
+
class="rm-alert-events__block"
|
|
112
|
+
:class="{ 'rm-alert-events__block--grouped': block.group }"
|
|
85
113
|
>
|
|
86
|
-
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
114
|
+
<p v-if="block.group" class="rm-alert-events__group">{{ block.group }}</p>
|
|
115
|
+
<p
|
|
116
|
+
v-for="(change, line) in block.changes"
|
|
117
|
+
:key="line"
|
|
118
|
+
class="rm-alert-events__change"
|
|
119
|
+
>
|
|
120
|
+
<span class="rm-alert-events__field">{{ change.label || change.field }} :</span>
|
|
121
|
+
<!-- Une entree qui entre ou qui sort n'a qu'un cote : annoncer
|
|
122
|
+
« (vide) devient X » remplissait la ligne de ce qui n'existe pas. -->
|
|
123
|
+
<template v-if="change.before && change.after">
|
|
124
|
+
<s>{{ change.before }}</s>
|
|
125
|
+
{{ T.becomes }}
|
|
126
|
+
<strong>{{ change.after }}</strong>
|
|
127
|
+
</template>
|
|
128
|
+
<s v-else-if="change.before">{{ change.before }}</s>
|
|
129
|
+
<strong v-else>{{ change.after || T.empty }}</strong>
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
91
132
|
<p v-if="hidden(event)" class="rm-alert-events__more">
|
|
92
133
|
{{ T.more(hidden(event)) }}
|
|
93
134
|
</p>
|
|
@@ -99,5 +140,5 @@ function hidden(event: RmAlertEvent) {
|
|
|
99
140
|
</template>
|
|
100
141
|
|
|
101
142
|
<style scoped>
|
|
102
|
-
.rm-alert-events{display:flex;flex-direction:column;gap:10px}.rm-alert-events__item{border:1px solid var(--border-default);border-radius:9px;display:flex;flex-direction:column;gap:3px;padding:10px 12px}.rm-alert-events__item--unread{border-left:3px solid var(--color-primary)}.rm-alert-events__head{align-items:center;display:flex;gap:8px;justify-content:space-between}.rm-alert-events__kind{color:var(--text-secondary);font-size:10.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.rm-alert-events__kind--appeared{color:var(--color-success)}.rm-alert-events__kind--changed{color:var(--color-info)}.rm-alert-events__kind--disappeared{color:var(--color-warning)}.rm-alert-events__date{color:var(--text-muted);font-size:11px}.rm-alert-events__title{color:var(--text-primary);font-size:13.5px;line-height:1.4;margin:0}.rm-alert-events__from,.rm-alert-events__sub{color:var(--text-muted);font-size:11.5px;margin:0}.rm-alert-events__change{color:var(--text-secondary);font-size:12.5px;margin:2px 0 0}.rm-alert-events__change s{opacity:.65}.rm-alert-events__more{color:var(--text-muted);font-size:12px;font-style:italic;margin:2px 0 0}
|
|
143
|
+
.rm-alert-events{display:flex;flex-direction:column;gap:10px}.rm-alert-events__item{border:1px solid var(--border-default);border-radius:9px;display:flex;flex-direction:column;gap:3px;padding:10px 12px}.rm-alert-events__item--unread{border-left:3px solid var(--color-primary)}.rm-alert-events__head{align-items:center;display:flex;gap:8px;justify-content:space-between}.rm-alert-events__source{color:var(--text-muted);font-size:11px;font-weight:600;margin:0}.rm-alert-events__kind{color:var(--text-secondary);font-size:10.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.rm-alert-events__kind--appeared{color:var(--color-success)}.rm-alert-events__kind--changed{color:var(--color-info)}.rm-alert-events__kind--disappeared{color:var(--color-warning)}.rm-alert-events__date{color:var(--text-muted);font-size:11px}.rm-alert-events__title{color:var(--text-primary);font-size:13.5px;line-height:1.4;margin:0}.rm-alert-events__from,.rm-alert-events__sub{color:var(--text-muted);font-size:11.5px;margin:0}.rm-alert-events__block{margin-top:4px}.rm-alert-events__block--grouped{border-left:2px solid var(--border-default);padding-left:8px}.rm-alert-events__group{color:var(--text-primary);font-size:11.5px;font-weight:600;margin:0}.rm-alert-events__change{color:var(--text-secondary);font-size:12.5px;margin:2px 0 0}.rm-alert-events__field{color:var(--text-muted)}.rm-alert-events__change s{opacity:.65}.rm-alert-events__more{color:var(--text-muted);font-size:12px;font-style:italic;margin:2px 0 0}
|
|
103
144
|
</style>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmClampText - un texte long coupe a N lignes, avec « Lire plus ».
|
|
3
|
+
|
|
4
|
+
Les fiches produit portent des paragraphes de longueur imprevisible : une
|
|
5
|
+
indication tient parfois en deux lignes, parfois en quarante. Les laisser
|
|
6
|
+
couler pousse tout le reste de la carte hors de l'ecran ; les couper sans
|
|
7
|
+
rien dire fait perdre le texte.
|
|
8
|
+
|
|
9
|
+
Le lien n'apparait que si le texte deborde vraiment. La mesure se fait dans
|
|
10
|
+
l'etat coupe (`scrollHeight > clientHeight`) et se refait au
|
|
11
|
+
redimensionnement : une carte elargie peut faire tenir le texte, le lien
|
|
12
|
+
doit alors disparaitre. Une fois deplie, on ne remesure pas — l'element
|
|
13
|
+
n'est plus coupe, la comparaison rendrait toujours faux et le lien
|
|
14
|
+
« Reduire » s'effacerait sous le doigt.
|
|
15
|
+
-->
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(defineProps<{
|
|
20
|
+
/** Le texte a afficher. Vide, le composant ne rend rien. */
|
|
21
|
+
text?: string | null
|
|
22
|
+
/** Nombre de lignes visibles avant la coupe. */
|
|
23
|
+
lines?: number
|
|
24
|
+
lang?: 'fr' | 'en'
|
|
25
|
+
}>(), {
|
|
26
|
+
text: '',
|
|
27
|
+
lines: 6,
|
|
28
|
+
lang: 'fr',
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const WORDS = {
|
|
32
|
+
fr: { more: 'Lire plus', less: 'Réduire' },
|
|
33
|
+
en: { more: 'Read more', less: 'Show less' },
|
|
34
|
+
} as const
|
|
35
|
+
|
|
36
|
+
const words = computed(() => WORDS[props.lang])
|
|
37
|
+
|
|
38
|
+
const bodyRef = ref<HTMLElement>()
|
|
39
|
+
const expanded = ref(false)
|
|
40
|
+
const overflows = ref(false)
|
|
41
|
+
|
|
42
|
+
async function measure() {
|
|
43
|
+
if (expanded.value) return
|
|
44
|
+
await nextTick()
|
|
45
|
+
const el = bodyRef.value
|
|
46
|
+
if (!el) return
|
|
47
|
+
overflows.value = el.scrollHeight > el.clientHeight
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let observer: ResizeObserver | undefined
|
|
51
|
+
|
|
52
|
+
onMounted(() => {
|
|
53
|
+
measure()
|
|
54
|
+
if (typeof ResizeObserver !== 'undefined' && bodyRef.value) {
|
|
55
|
+
observer = new ResizeObserver(() => measure())
|
|
56
|
+
observer.observe(bodyRef.value)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
onBeforeUnmount(() => observer?.disconnect())
|
|
60
|
+
|
|
61
|
+
// Un changement de texte remet la coupe : le paragraphe suivant n'a aucune
|
|
62
|
+
// raison d'heriter du « deplie » demande sur le precedent.
|
|
63
|
+
watch(() => props.text, () => {
|
|
64
|
+
expanded.value = false
|
|
65
|
+
measure()
|
|
66
|
+
})
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template>
|
|
70
|
+
<div v-if="text" class="rm-clamp">
|
|
71
|
+
<p
|
|
72
|
+
ref="bodyRef"
|
|
73
|
+
class="rm-clamp__body"
|
|
74
|
+
:class="{ 'rm-clamp__body--cut': !expanded }"
|
|
75
|
+
:style="{ '--rm-clamp-lines': lines }"
|
|
76
|
+
>
|
|
77
|
+
{{ text }}
|
|
78
|
+
</p>
|
|
79
|
+
<button
|
|
80
|
+
v-if="overflows"
|
|
81
|
+
type="button"
|
|
82
|
+
class="rm-clamp__toggle"
|
|
83
|
+
:aria-expanded="expanded"
|
|
84
|
+
@click="expanded = !expanded"
|
|
85
|
+
>
|
|
86
|
+
{{ expanded ? words.less : words.more }}
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<style scoped>
|
|
92
|
+
.rm-clamp{max-width:100%;min-width:0;overflow:hidden}.rm-clamp__body{color:var(--text-secondary);font-size:var(--font-14);line-height:1.6;overflow-wrap:break-word;word-break:break-word}.rm-clamp__body--cut{display:-webkit-box;-webkit-line-clamp:var(--rm-clamp-lines);line-clamp:var(--rm-clamp-lines);-webkit-box-orient:vertical;overflow:hidden}.rm-clamp__toggle{background:none;border:none;color:var(--color-healthcare);cursor:pointer;display:inline-block;font-family:inherit;font-size:var(--font-13);font-weight:var(--weight-semibold);margin-top:var(--space-4);padding:0}.rm-clamp__toggle:hover{text-decoration:underline}
|
|
93
|
+
</style>
|
|
@@ -82,6 +82,12 @@ export interface RmAlertChange {
|
|
|
82
82
|
field: string;
|
|
83
83
|
/** Libelle du champ tel que la page le nomme. */
|
|
84
84
|
label?: string | null;
|
|
85
|
+
/**
|
|
86
|
+
* Ce que le champ touche : l'historique, l'entree de cet historique. Les
|
|
87
|
+
* changements qui le partagent sont montres sous un seul en-tete, et leur
|
|
88
|
+
* libelle ne porte plus que le champ. Absent, la ligne se suffit a elle-meme.
|
|
89
|
+
*/
|
|
90
|
+
group?: string | null;
|
|
85
91
|
before: string;
|
|
86
92
|
after: string;
|
|
87
93
|
}
|
|
@@ -93,6 +99,13 @@ export interface RmAlertEvent {
|
|
|
93
99
|
changes?: RmAlertChange[] | null;
|
|
94
100
|
/** Date deja mise en forme par l'appelant. */
|
|
95
101
|
dateLabel: string;
|
|
102
|
+
/**
|
|
103
|
+
* La partie de l'app d'ou vient l'evenement, nommee par l'appelant.
|
|
104
|
+
*
|
|
105
|
+
* Une alerte qui suit une marque partout rapporte d'un index a l'autre : la
|
|
106
|
+
* carte le dit, sinon elle annonce un changement sans dire ou le lire.
|
|
107
|
+
*/
|
|
108
|
+
source?: string | null;
|
|
96
109
|
alertTitle?: string | null;
|
|
97
110
|
read?: boolean;
|
|
98
111
|
}
|