@tekkare/romulus 0.1.151 → 0.1.153
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
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmTextHighlight - un texte ou les termes cherches ressortent.
|
|
3
|
+
|
|
4
|
+
Les listes de resultats doivent montrer POURQUOI une ligne est la : sans le
|
|
5
|
+
surlignage, une indication de quarante mots trouvee sur un synonyme se lit
|
|
6
|
+
comme un faux positif. C'est la meme regle que les extraits du moteur, mais
|
|
7
|
+
rendue cote app, sur un texte que la page tient deja.
|
|
8
|
+
|
|
9
|
+
ORIGINE AURIGA
|
|
10
|
+
--------------
|
|
11
|
+
Remplace `argTextHighlight`, un composant que les apps recopiaient chez
|
|
12
|
+
elles (fr_realifedata_app2 en portait une copie a 90 appels). Le surlignage
|
|
13
|
+
y passait par `v-html` : le texte venait pourtant d'Elasticsearch, donc
|
|
14
|
+
d'une donnee, et tout `<` qu'il portait entrait tel quel dans le DOM. Ici le
|
|
15
|
+
texte n'est jamais interprete, il est decoupe en segments et seuls les
|
|
16
|
+
segments retenus passent dans un `<mark>`.
|
|
17
|
+
|
|
18
|
+
| Auriga | Romulus | Note |
|
|
19
|
+
|-------------------|------------|-------------------------------------------|
|
|
20
|
+
| `text` | `text` | |
|
|
21
|
+
| `queries` | `queries` | une chaine ou une liste ; les vides sont ignorees |
|
|
22
|
+
| `crop` + `lines` + `have-read-more` | `clamp` | un seul nombre : la coupe et le lien vont ensemble, et le lien ne sort que si le texte deborde vraiment |
|
|
23
|
+
| `id` | - | Auriga retrouvait son propre noeud par `document.getElementById` au montage, ce qui demandait un id unique a chaque appel et ne mesurait rien sans lui |
|
|
24
|
+
| `custom-read-more`| - | les libelles viennent de `lang`, comme partout ailleurs |
|
|
25
|
+
| `lang: en/fr/es` | `lang: fr/en` | l'espagnol n'existe dans aucune app du portail |
|
|
26
|
+
| `@onShowMore` / `@onShowLess` | `@toggle` | un evenement qui porte l'etat plutot que deux |
|
|
27
|
+
|
|
28
|
+
La correspondance est une SOUS-CHAINE, insensible a la casse et aux
|
|
29
|
+
accents. Auriga bornait chaque terme par `\b`, ce qui laissait « myel » sans
|
|
30
|
+
surlignage dans « myelome » alors que c'est bien lui qui a ramene la ligne,
|
|
31
|
+
et ratait tout terme accentue selon la graphie du corpus.
|
|
32
|
+
-->
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
35
|
+
|
|
36
|
+
const props = withDefaults(defineProps<{
|
|
37
|
+
/** Le texte a afficher. Vide, le composant ne rend rien. */
|
|
38
|
+
text?: string | null
|
|
39
|
+
/** Termes a surligner. Une chaine vaut une liste d'un terme. */
|
|
40
|
+
queries?: string | readonly string[] | null
|
|
41
|
+
/**
|
|
42
|
+
* Nombre de lignes visibles avant la coupe. A zero, le texte coule et
|
|
43
|
+
* aucun lien n'est pose.
|
|
44
|
+
*/
|
|
45
|
+
clamp?: number
|
|
46
|
+
lang?: 'fr' | 'en'
|
|
47
|
+
}>(), {
|
|
48
|
+
text: '',
|
|
49
|
+
queries: null,
|
|
50
|
+
clamp: 0,
|
|
51
|
+
lang: 'fr',
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const emit = defineEmits<{ toggle: [expanded: boolean] }>()
|
|
55
|
+
|
|
56
|
+
const WORDS = {
|
|
57
|
+
fr: { more: 'Lire plus', less: 'Réduire' },
|
|
58
|
+
en: { more: 'Read more', less: 'Show less' },
|
|
59
|
+
} as const
|
|
60
|
+
|
|
61
|
+
const words = computed(() => WORDS[props.lang])
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Minuscule et sans accent, SANS changer la longueur : c'est ce qui permet de
|
|
65
|
+
* chercher sur la forme repliee et de decouper le texte d'origine aux memes
|
|
66
|
+
* indices. Un `normalize('NFD')` sur toute la chaine, lui, ajoute les signes
|
|
67
|
+
* combinants et decale tout.
|
|
68
|
+
*/
|
|
69
|
+
function fold(value: string): string {
|
|
70
|
+
let out = ''
|
|
71
|
+
for (const char of value) {
|
|
72
|
+
out += char.normalize('NFD')[0]!.toLowerCase()
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type Segment = { text: string, hit: boolean }
|
|
78
|
+
|
|
79
|
+
const segments = computed<Segment[]>(() => {
|
|
80
|
+
const text = props.text ?? ''
|
|
81
|
+
if (!text) return []
|
|
82
|
+
|
|
83
|
+
const terms = (Array.isArray(props.queries) ? props.queries : props.queries ? [props.queries] : [])
|
|
84
|
+
.map(term => String(term ?? '').trim())
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
if (!terms.length) return [{ text, hit: false }]
|
|
87
|
+
|
|
88
|
+
const hay = fold(text)
|
|
89
|
+
const spans: [number, number][] = []
|
|
90
|
+
for (const term of terms) {
|
|
91
|
+
const needle = fold(term)
|
|
92
|
+
if (!needle) continue
|
|
93
|
+
let from = 0
|
|
94
|
+
for (;;) {
|
|
95
|
+
const at = hay.indexOf(needle, from)
|
|
96
|
+
if (at === -1) break
|
|
97
|
+
spans.push([at, at + needle.length])
|
|
98
|
+
from = at + needle.length
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!spans.length) return [{ text, hit: false }]
|
|
102
|
+
|
|
103
|
+
// Deux termes qui se chevauchent ne font qu'un surlignage : sans la fusion,
|
|
104
|
+
// le second couperait le premier en deux `<mark>` colles, que le lecteur
|
|
105
|
+
// voit comme deux mots.
|
|
106
|
+
spans.sort((a, b) => a[0] - b[0])
|
|
107
|
+
const merged: [number, number][] = [spans[0]!]
|
|
108
|
+
for (const [start, end] of spans.slice(1)) {
|
|
109
|
+
const last = merged[merged.length - 1]!
|
|
110
|
+
if (start <= last[1]) last[1] = Math.max(last[1], end)
|
|
111
|
+
else merged.push([start, end])
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const out: Segment[] = []
|
|
115
|
+
let cursor = 0
|
|
116
|
+
for (const [start, end] of merged) {
|
|
117
|
+
if (start > cursor) out.push({ text: text.slice(cursor, start), hit: false })
|
|
118
|
+
out.push({ text: text.slice(start, end), hit: true })
|
|
119
|
+
cursor = end
|
|
120
|
+
}
|
|
121
|
+
if (cursor < text.length) out.push({ text: text.slice(cursor), hit: false })
|
|
122
|
+
return out
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const bodyRef = ref<HTMLElement>()
|
|
126
|
+
const expanded = ref(false)
|
|
127
|
+
const overflows = ref(false)
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* La mesure se fait dans l'etat coupe et se refait au redimensionnement : une
|
|
131
|
+
* carte elargie peut faire tenir le texte, le lien doit alors disparaitre.
|
|
132
|
+
* Une fois deplie, on ne remesure pas : l'element n'est plus coupe, la
|
|
133
|
+
* comparaison rendrait toujours faux et le lien « Reduire » s'effacerait sous
|
|
134
|
+
* le doigt.
|
|
135
|
+
*/
|
|
136
|
+
async function measure() {
|
|
137
|
+
if (!props.clamp || expanded.value) return
|
|
138
|
+
await nextTick()
|
|
139
|
+
const el = bodyRef.value
|
|
140
|
+
if (!el) return
|
|
141
|
+
overflows.value = el.scrollHeight > el.clientHeight
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let observer: ResizeObserver | undefined
|
|
145
|
+
|
|
146
|
+
onMounted(() => {
|
|
147
|
+
measure()
|
|
148
|
+
if (typeof ResizeObserver !== 'undefined' && bodyRef.value) {
|
|
149
|
+
observer = new ResizeObserver(() => measure())
|
|
150
|
+
observer.observe(bodyRef.value)
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
onBeforeUnmount(() => observer?.disconnect())
|
|
154
|
+
|
|
155
|
+
// Un changement de texte remet la coupe : le paragraphe suivant n'a aucune
|
|
156
|
+
// raison d'heriter du « deplie » demande sur le precedent.
|
|
157
|
+
watch(() => props.text, () => {
|
|
158
|
+
expanded.value = false
|
|
159
|
+
measure()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
function toggle() {
|
|
163
|
+
expanded.value = !expanded.value
|
|
164
|
+
emit('toggle', expanded.value)
|
|
165
|
+
}
|
|
166
|
+
</script>
|
|
167
|
+
|
|
168
|
+
<template>
|
|
169
|
+
<div v-if="text" class="rm-thl">
|
|
170
|
+
<p
|
|
171
|
+
ref="bodyRef"
|
|
172
|
+
class="rm-thl__body"
|
|
173
|
+
:class="{ 'rm-thl__body--cut': clamp && !expanded }"
|
|
174
|
+
:style="clamp ? { '--rm-thl-lines': clamp } : undefined"
|
|
175
|
+
><template v-for="(segment, index) in segments" :key="index"><mark
|
|
176
|
+
v-if="segment.hit"
|
|
177
|
+
class="rm-thl__hit"
|
|
178
|
+
>{{ segment.text }}</mark><template v-else>{{ segment.text }}</template></template></p>
|
|
179
|
+
<button
|
|
180
|
+
v-if="clamp && overflows"
|
|
181
|
+
type="button"
|
|
182
|
+
class="rm-thl__toggle"
|
|
183
|
+
:aria-expanded="expanded"
|
|
184
|
+
@click="toggle"
|
|
185
|
+
>
|
|
186
|
+
{{ expanded ? words.less : words.more }}
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
</template>
|
|
190
|
+
|
|
191
|
+
<style scoped>
|
|
192
|
+
.rm-thl{max-width:100%;min-width:0}.rm-thl__body{color:inherit;font-size:inherit;line-height:inherit;margin:0;overflow-wrap:break-word;word-break:break-word}.rm-thl__body--cut{display:-webkit-box;-webkit-line-clamp:var(--rm-thl-lines);line-clamp:var(--rm-thl-lines);-webkit-box-orient:vertical;overflow:hidden}.rm-thl__hit{background:color-mix(in srgb,var(--color-primary) 18%,transparent);border-radius:var(--radius-sm,4px);color:inherit;font-weight:var(--weight-semibold,600);padding:0 2px}.rm-thl__toggle{background:none;border:none;color:var(--color-primary);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-thl__toggle:hover{text-decoration:underline}
|
|
193
|
+
</style>
|
|
@@ -118,6 +118,11 @@ const isDone = (id: string) => props.done.includes(id)
|
|
|
118
118
|
</div>
|
|
119
119
|
|
|
120
120
|
<p v-if="item.disabledReason" class="rm-tuto__reason">{{ item.disabledReason }}</p>
|
|
121
|
+
<!-- La note ne refuse rien : elle dit ce que « Démarrer » va faire. -->
|
|
122
|
+
<p v-else-if="item.note" class="rm-tuto__reason rm-tuto__reason--note">
|
|
123
|
+
<RmIcon name="ArrowRight" :size="12" />
|
|
124
|
+
{{ item.note }}
|
|
125
|
+
</p>
|
|
121
126
|
</article>
|
|
122
127
|
</div>
|
|
123
128
|
|
|
@@ -126,5 +131,5 @@ const isDone = (id: string) => props.done.includes(id)
|
|
|
126
131
|
</template>
|
|
127
132
|
|
|
128
133
|
<style scoped>
|
|
129
|
-
.rm-tuto{display:flex;flex-direction:column;gap:var(--space-12);width:100%}.rm-tuto__head{align-items:center;display:flex;gap:var(--space-8);justify-content:space-between}.rm-tuto__title{color:var(--text-primary);font-size:var(--font-16)}.rm-tuto__badge,.rm-tuto__title{font-weight:var(--weight-semibold)}.rm-tuto__badge{background:var(--bg-hover);border-radius:var(--radius-pill);color:var(--color-primary);font-size:var(--font-10);letter-spacing:.06em;padding:2px var(--space-8);text-transform:uppercase}.rm-tuto__desc{color:var(--text-secondary);font-size:var(--font-12);line-height:1.55}.rm-tuto__card,.rm-tuto__list{display:flex;flex-direction:column;gap:var(--space-8)}.rm-tuto__card{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl);padding:var(--space-16);transition:border-color var(--duration-fast) var(--ease-out)}.rm-tuto__card:hover{border-color:var(--color-violet-pale)}.rm-tuto__card--off{opacity:.72}.rm-tuto__row{align-items:flex-start;display:flex;gap:var(--space-12)}.rm-tuto__icon{align-items:center;background:var(--bg-hover);border-radius:var(--radius-lg);color:var(--color-secondary);display:flex;flex-shrink:0;height:36px;justify-content:center;width:36px}.rm-tuto__body{display:flex;flex:1;flex-direction:column;gap:3px;min-width:0}.rm-tuto__line{align-items:center;display:flex;flex-wrap:wrap;gap:var(--space-6)}.rm-tuto__name{color:var(--text-primary);font-size:var(--font-13);font-weight:var(--weight-semibold)}.rm-tuto__tag{background:var(--bg-surface-3);border-radius:var(--radius-pill);color:var(--text-muted);font-size:var(--font-10);padding:1px var(--space-6)}.rm-tuto__done{align-items:center;color:var(--color-success);display:inline-flex;font-size:var(--font-10);font-weight:var(--weight-semibold);gap:3px}.rm-tuto__summary{color:var(--text-secondary);font-size:var(--font-12);line-height:1.5}.rm-tuto__meta{color:var(--text-muted);font-size:var(--font-11);font-variant-numeric:tabular-nums}.rm-tuto__reason{color:var(--text-muted);font-size:var(--font-11);line-height:1.5;padding-left:48px}
|
|
134
|
+
.rm-tuto{display:flex;flex-direction:column;gap:var(--space-12);width:100%}.rm-tuto__head{align-items:center;display:flex;gap:var(--space-8);justify-content:space-between}.rm-tuto__title{color:var(--text-primary);font-size:var(--font-16)}.rm-tuto__badge,.rm-tuto__title{font-weight:var(--weight-semibold)}.rm-tuto__badge{background:var(--bg-hover);border-radius:var(--radius-pill);color:var(--color-primary);font-size:var(--font-10);letter-spacing:.06em;padding:2px var(--space-8);text-transform:uppercase}.rm-tuto__desc{color:var(--text-secondary);font-size:var(--font-12);line-height:1.55}.rm-tuto__card,.rm-tuto__list{display:flex;flex-direction:column;gap:var(--space-8)}.rm-tuto__card{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl);padding:var(--space-16);transition:border-color var(--duration-fast) var(--ease-out)}.rm-tuto__card:hover{border-color:var(--color-violet-pale)}.rm-tuto__card--off{opacity:.72}.rm-tuto__row{align-items:flex-start;display:flex;gap:var(--space-12)}.rm-tuto__icon{align-items:center;background:var(--bg-hover);border-radius:var(--radius-lg);color:var(--color-secondary);display:flex;flex-shrink:0;height:36px;justify-content:center;width:36px}.rm-tuto__body{display:flex;flex:1;flex-direction:column;gap:3px;min-width:0}.rm-tuto__line{align-items:center;display:flex;flex-wrap:wrap;gap:var(--space-6)}.rm-tuto__name{color:var(--text-primary);font-size:var(--font-13);font-weight:var(--weight-semibold)}.rm-tuto__tag{background:var(--bg-surface-3);border-radius:var(--radius-pill);color:var(--text-muted);font-size:var(--font-10);padding:1px var(--space-6)}.rm-tuto__done{align-items:center;color:var(--color-success);display:inline-flex;font-size:var(--font-10);font-weight:var(--weight-semibold);gap:3px}.rm-tuto__summary{color:var(--text-secondary);font-size:var(--font-12);line-height:1.5}.rm-tuto__meta{color:var(--text-muted);font-size:var(--font-11);font-variant-numeric:tabular-nums}.rm-tuto__reason{color:var(--text-muted);font-size:var(--font-11);line-height:1.5;padding-left:48px}.rm-tuto__reason--note{align-items:center;color:var(--color-secondary);display:flex;gap:4px}
|
|
130
135
|
</style>
|
|
@@ -42,4 +42,10 @@ export interface RmTutorial {
|
|
|
42
42
|
* desactive le bouton et s'ecrit sous la carte.
|
|
43
43
|
*/
|
|
44
44
|
disabledReason?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Precision ecrite sous la carte, sans rien empecher : ou la visite va
|
|
47
|
+
* emmener le lecteur, ce qu'elle suppose. A distinguer de `disabledReason`,
|
|
48
|
+
* qui refuse le depart.
|
|
49
|
+
*/
|
|
50
|
+
note?: string;
|
|
45
51
|
}
|