@tekkare/romulus 0.1.166 → 0.1.168
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/ChoroplethMap.vue +248 -0
- package/dist/runtime/components/Textarea.vue +72 -0
- package/dist/runtime/composables/useRmExport.js +253 -92
- package/dist/runtime/internal/geo/de.json +50 -0
- package/dist/runtime/internal/geo/fr-departements.json +406 -0
- package/dist/runtime/internal/geo/fr-regions.json +164 -0
- package/dist/runtime/internal/geo/index.d.ts +28 -0
- package/dist/runtime/internal/geo/index.js +24 -0
- package/dist/runtime/internal/geo/kr.json +104 -0
- package/dist/runtime/internal/geo/us.json +152 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmChoroplethMap - une valeur par region, peinte sur la carte d'un pays.
|
|
3
|
+
|
|
4
|
+
Le pays est une donnee (`geography`), pas un composant : d'un pays a l'autre
|
|
5
|
+
seul le trace changeait. Le trace est charge a la demande, jeu par jeu (cf.
|
|
6
|
+
internal/geo), parce que tous ensemble ils pesent plus lourd que le reste du
|
|
7
|
+
paquet.
|
|
8
|
+
|
|
9
|
+
L'echelle de couleur est la palette sequentielle du design system
|
|
10
|
+
(`palette.seq`), donc elle suit le mode nuit et la palette daltonienne. Une
|
|
11
|
+
region sans valeur n'est pas peinte en clair : elle est peinte dans une
|
|
12
|
+
surface neutre, hors echelle, parce que « pas de donnee » et « valeur
|
|
13
|
+
basse » ne sont pas la meme chose.
|
|
14
|
+
|
|
15
|
+
ORIGINE AURIGA
|
|
16
|
+
--------------
|
|
17
|
+
Remplace les dix-sept composants `argMapsXxx` (un par pays) et leurs
|
|
18
|
+
`oneRegionXx` (un par region), qui partageaient le meme script a la ligne
|
|
19
|
+
pres.
|
|
20
|
+
|
|
21
|
+
| Auriga | Romulus | Note |
|
|
22
|
+
|-------------------------|-----------------|-------------------------------|
|
|
23
|
+
| `<argMapsUs>`, `<argMapsGermany>`, ... | `geography="us"`, `"de"`, ... | un composant, le pays en donnee |
|
|
24
|
+
| `have-custom-title` | slot `#title` | la presence du slot suffit, la prop disait la meme chose une seconde fois |
|
|
25
|
+
| slot `#Tooltip` | slot `#tooltip` | meme donnee (`{ name, value }`) |
|
|
26
|
+
| slot `#Subcontent` | slot `#footer` | |
|
|
27
|
+
| `hide-drom-tom` | `compact` | le nom ne parlait que de la France |
|
|
28
|
+
| `@openSource` | `@source` | |
|
|
29
|
+
| echelle de 10 couleurs ecrites en dur | `palette.seq` | suit le mode nuit et la palette daltonienne |
|
|
30
|
+
| `results` | - | prop morte : aucune carte ne la lisait |
|
|
31
|
+
-->
|
|
32
|
+
<script setup lang="ts">
|
|
33
|
+
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
|
34
|
+
import { RM_GEOGRAPHIES, type RmGeoShape } from '../internal/geo'
|
|
35
|
+
import { useChartTheme } from '../composables/useChartTheme'
|
|
36
|
+
|
|
37
|
+
export interface RmChoroplethValue {
|
|
38
|
+
/** Nom de la region, tel que la geographie la nomme. */
|
|
39
|
+
name: string
|
|
40
|
+
value: number | null | undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const props = withDefaults(
|
|
44
|
+
defineProps<{
|
|
45
|
+
/** Le pays dessine : `fr-regions`, `fr-departements`, `us`, `de`, `kr`. */
|
|
46
|
+
geography: string
|
|
47
|
+
data?: RmChoroplethValue[]
|
|
48
|
+
title?: string | null
|
|
49
|
+
/** Source citee sous le titre. */
|
|
50
|
+
source?: string | null
|
|
51
|
+
/**
|
|
52
|
+
* Inverse la lecture de l'echelle : la valeur basse est la plus foncee.
|
|
53
|
+
* Pour un indicateur ou peu vaut mieux (une couverture manquante).
|
|
54
|
+
*/
|
|
55
|
+
reverseColors?: boolean
|
|
56
|
+
/** Resserre le cadrage sur la partie continentale. */
|
|
57
|
+
compact?: boolean
|
|
58
|
+
/** Hauteur du dessin, en pixels. */
|
|
59
|
+
height?: number
|
|
60
|
+
lang?: 'fr' | 'en'
|
|
61
|
+
}>(),
|
|
62
|
+
{
|
|
63
|
+
data: () => [],
|
|
64
|
+
title: null,
|
|
65
|
+
source: null,
|
|
66
|
+
reverseColors: false,
|
|
67
|
+
compact: false,
|
|
68
|
+
height: 500,
|
|
69
|
+
lang: 'fr',
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const emit = defineEmits<{
|
|
74
|
+
/** La source citee est cliquee. */
|
|
75
|
+
source: []
|
|
76
|
+
/** Une region est cliquee. */
|
|
77
|
+
select: [name: string]
|
|
78
|
+
}>()
|
|
79
|
+
|
|
80
|
+
const { palette } = useChartTheme()
|
|
81
|
+
|
|
82
|
+
const shapes = ref<Record<string, RmGeoShape>>({})
|
|
83
|
+
const loading = ref(true)
|
|
84
|
+
|
|
85
|
+
const geography = computed(() => RM_GEOGRAPHIES[props.geography])
|
|
86
|
+
|
|
87
|
+
watch(
|
|
88
|
+
() => props.geography,
|
|
89
|
+
async (key) => {
|
|
90
|
+
const geo = RM_GEOGRAPHIES[key]
|
|
91
|
+
shapes.value = {}
|
|
92
|
+
if (!geo) {
|
|
93
|
+
loading.value = false
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
loading.value = true
|
|
97
|
+
shapes.value = await geo.load()
|
|
98
|
+
loading.value = false
|
|
99
|
+
},
|
|
100
|
+
{ immediate: true },
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const viewBox = computed(() =>
|
|
104
|
+
(props.compact ? geography.value?.compactViewBox : null) ?? geography.value?.viewBox ?? '0 0 100 100',
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const values = computed(() =>
|
|
108
|
+
props.data.filter(d => d.value !== null && d.value !== undefined).map(d => Number(d.value)),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const bounds = computed(() => {
|
|
112
|
+
if (!values.value.length) return null
|
|
113
|
+
return { min: Math.min(...values.value), max: Math.max(...values.value) }
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Cinq tranches d'egale largeur. Auriga en peignait dix, ecrites en dur :
|
|
118
|
+
* au-dela, deux tranches voisines ne se distinguent plus et la legende promet
|
|
119
|
+
* une precision que l'oeil ne lit pas.
|
|
120
|
+
*
|
|
121
|
+
* La premiere teinte de la palette sequentielle est ecartee : elle est presque
|
|
122
|
+
* blanche, et la region la plus basse se lisait alors comme une region sans
|
|
123
|
+
* donnee, qui est la seule chose que cette carte ne doit pas confondre.
|
|
124
|
+
*/
|
|
125
|
+
const ramp = computed(() => {
|
|
126
|
+
const seq = palette.value.seq.slice(1)
|
|
127
|
+
return props.reverseColors ? [...seq].reverse() : seq
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const byName = computed(() => {
|
|
131
|
+
const out = new Map<string, number | null>()
|
|
132
|
+
for (const entry of props.data) {
|
|
133
|
+
out.set(entry.name, entry.value === null || entry.value === undefined ? null : Number(entry.value))
|
|
134
|
+
}
|
|
135
|
+
return out
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
/** Hors echelle : une region sans valeur ne se lit pas comme une valeur basse. */
|
|
139
|
+
const NO_DATA = 'var(--bg-surface-3)'
|
|
140
|
+
|
|
141
|
+
function colorOf(name: string) {
|
|
142
|
+
const value = byName.value.get(name)
|
|
143
|
+
if (value === null || value === undefined || !bounds.value) return NO_DATA
|
|
144
|
+
const { min, max } = bounds.value
|
|
145
|
+
if (max === min) return ramp.value[ramp.value.length - 1]!
|
|
146
|
+
const step = (value - min) / (max - min)
|
|
147
|
+
const index = Math.min(ramp.value.length - 1, Math.floor(step * ramp.value.length))
|
|
148
|
+
return ramp.value[index]!
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const hovered = ref<string | null>(null)
|
|
152
|
+
const pointer = ref({ x: 0, y: 0 })
|
|
153
|
+
|
|
154
|
+
function onEnter(name: string, event: MouseEvent) {
|
|
155
|
+
hovered.value = name
|
|
156
|
+
onMove(event)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function onMove(event: MouseEvent) {
|
|
160
|
+
const host = (event.currentTarget as SVGElement)?.ownerSVGElement?.parentElement
|
|
161
|
+
const box = host?.getBoundingClientRect()
|
|
162
|
+
pointer.value = {
|
|
163
|
+
x: event.clientX - (box?.left ?? 0),
|
|
164
|
+
y: event.clientY - (box?.top ?? 0),
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function onLeave() {
|
|
169
|
+
hovered.value = null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
onBeforeUnmount(() => {
|
|
173
|
+
hovered.value = null
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const hoveredValue = computed(() =>
|
|
177
|
+
hovered.value === null ? null : (byName.value.get(hovered.value) ?? null),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
const T = computed(() =>
|
|
181
|
+
props.lang === 'en'
|
|
182
|
+
? { noData: 'No data', source: 'Source' }
|
|
183
|
+
: { noData: 'Pas de donnée', source: 'Source' },
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
/** Dégradé de la légende, dans l'ordre de lecture de l'échelle. */
|
|
187
|
+
const legendGradient = computed(() => `linear-gradient(90deg, ${ramp.value.join(', ')})`)
|
|
188
|
+
|
|
189
|
+
const fmt = computed(() => new Intl.NumberFormat(props.lang === 'en' ? 'en-US' : 'fr-FR'))
|
|
190
|
+
</script>
|
|
191
|
+
|
|
192
|
+
<template>
|
|
193
|
+
<div class="rm-choro">
|
|
194
|
+
<div class="rm-choro__head">
|
|
195
|
+
<slot name="title">
|
|
196
|
+
<p v-if="title" class="rm-choro__title">{{ title }}</p>
|
|
197
|
+
</slot>
|
|
198
|
+
<button v-if="source" type="button" class="rm-choro__source" @click="emit('source')">
|
|
199
|
+
{{ T.source }} : {{ source }}
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div class="rm-choro__plot" :style="{ height: `${height}px` }">
|
|
204
|
+
<RmSkeleton v-if="loading" height="100%" />
|
|
205
|
+
<svg v-else class="rm-choro__svg" :viewBox="viewBox" preserveAspectRatio="xMidYMid meet">
|
|
206
|
+
<path
|
|
207
|
+
v-for="(shape, name) in shapes"
|
|
208
|
+
:key="name"
|
|
209
|
+
:d="shape.path"
|
|
210
|
+
:fill="colorOf(String(name))"
|
|
211
|
+
:stroke="colorOf(String(name)) === NO_DATA ? 'var(--border-default)' : colorOf(String(name))"
|
|
212
|
+
stroke-width="1"
|
|
213
|
+
class="rm-choro__shape"
|
|
214
|
+
:class="{ 'rm-choro__shape--dimmed': hovered !== null && hovered !== name }"
|
|
215
|
+
@mouseenter="onEnter(String(name), $event)"
|
|
216
|
+
@mousemove="onMove"
|
|
217
|
+
@mouseleave="onLeave"
|
|
218
|
+
@click="emit('select', String(name))"
|
|
219
|
+
/>
|
|
220
|
+
</svg>
|
|
221
|
+
|
|
222
|
+
<div
|
|
223
|
+
v-if="hovered !== null"
|
|
224
|
+
class="rm-choro__tip"
|
|
225
|
+
:style="{ top: `${pointer.y}px`, left: `${pointer.x}px` }"
|
|
226
|
+
>
|
|
227
|
+
<p class="rm-choro__tip_name">{{ hovered }}</p>
|
|
228
|
+
<slot name="tooltip" :value="{ name: hovered, value: hoveredValue }">
|
|
229
|
+
<p class="rm-choro__tip_value">
|
|
230
|
+
{{ hoveredValue === null ? T.noData : fmt.format(hoveredValue) }}
|
|
231
|
+
</p>
|
|
232
|
+
</slot>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<div v-if="bounds" class="rm-choro__legend">
|
|
237
|
+
<span>{{ fmt.format(Math.floor(bounds.min)) }}</span>
|
|
238
|
+
<span class="rm-choro__legend_bar" :style="{ background: legendGradient }" />
|
|
239
|
+
<span>{{ fmt.format(Math.ceil(bounds.max)) }}</span>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<slot name="footer" />
|
|
243
|
+
</div>
|
|
244
|
+
</template>
|
|
245
|
+
|
|
246
|
+
<style scoped>
|
|
247
|
+
.rm-choro{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl);gap:var(--space-16);padding:var(--space-24)}.rm-choro,.rm-choro__head{display:flex;flex-direction:column}.rm-choro__head{gap:var(--space-4)}.rm-choro__title{color:var(--text-primary);font-family:var(--font-serif);font-size:var(--font-18,18px);font-weight:700;margin:0}.rm-choro__source{align-self:flex-start;background:none;border:0;color:var(--text-muted);cursor:pointer;font-size:var(--font-12,12px);padding:0;text-decoration:underline}.rm-choro__plot{position:relative}.rm-choro__svg{height:100%;width:100%}.rm-choro__shape{cursor:pointer;transition:opacity var(--duration-fast) var(--ease-in-out)}.rm-choro__shape--dimmed{opacity:.45}.rm-choro__tip{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);box-shadow:var(--shadow-md);max-width:260px;padding:var(--space-8) var(--space-12);pointer-events:none;position:absolute;transform:translate(-50%,calc(-100% - 12px));z-index:2}.rm-choro__tip_name{color:var(--text-primary);font-size:var(--font-14,14px);font-weight:700;margin:0}.rm-choro__tip_value{color:var(--text-secondary);font-size:var(--font-14,14px);margin:0}.rm-choro__legend{align-items:center;color:var(--text-muted);display:flex;font-size:var(--font-12,12px);gap:var(--space-8)}.rm-choro__legend_bar{border-radius:4px;flex:1;height:8px}
|
|
248
|
+
</style>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Champ de texte multiligne.
|
|
3
|
+
|
|
4
|
+
Jumeau de RmInput : meme etiquette, meme message d'erreur, meme dessin de
|
|
5
|
+
bordure et de focus. Seule la hauteur change, et elle est reglable en lignes
|
|
6
|
+
plutot qu'en pixels — une description de trois lignes ne se decrit pas en
|
|
7
|
+
`72px`, et la valeur suivrait mal un changement de graisse de texte.
|
|
8
|
+
|
|
9
|
+
ORIGINE AURIGA
|
|
10
|
+
--------------
|
|
11
|
+
Remplace le `<textarea>` nu que les apps posaient a cote d'un `arg-input`,
|
|
12
|
+
avec leur propre feuille de style : il ne suivait ni le mode nuit ni les
|
|
13
|
+
jetons de bordure, et divergeait d'une app a l'autre.
|
|
14
|
+
|
|
15
|
+
`resize` vaut `vertical` par defaut : l'horizontal deborde des colonnes de
|
|
16
|
+
formulaire et casse la grille.
|
|
17
|
+
-->
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
export interface RTextareaProps {
|
|
20
|
+
/** The v-model value */
|
|
21
|
+
modelValue?: string
|
|
22
|
+
/** Label text displayed above the field */
|
|
23
|
+
label?: string
|
|
24
|
+
/** Placeholder text */
|
|
25
|
+
placeholder?: string
|
|
26
|
+
/** Error message to display */
|
|
27
|
+
error?: string
|
|
28
|
+
/** Whether the field is disabled */
|
|
29
|
+
disabled?: boolean
|
|
30
|
+
/** Whether the field is required */
|
|
31
|
+
required?: boolean
|
|
32
|
+
/** Visible height, in lines of text */
|
|
33
|
+
rows?: number
|
|
34
|
+
/** Whether the user may resize the field */
|
|
35
|
+
resize?: 'none' | 'vertical'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
withDefaults(defineProps<RTextareaProps>(), {
|
|
39
|
+
modelValue: '',
|
|
40
|
+
disabled: false,
|
|
41
|
+
required: false,
|
|
42
|
+
rows: 4,
|
|
43
|
+
resize: 'vertical',
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
defineEmits<{
|
|
47
|
+
'update:modelValue': [value: string]
|
|
48
|
+
}>()
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<div class="r-textarea-wrapper">
|
|
53
|
+
<label v-if="label" class="r-textarea-label">
|
|
54
|
+
{{ label }}
|
|
55
|
+
<span v-if="required" class="r-textarea-required">*</span>
|
|
56
|
+
</label>
|
|
57
|
+
<textarea
|
|
58
|
+
:value="modelValue"
|
|
59
|
+
:placeholder="placeholder"
|
|
60
|
+
:disabled="disabled"
|
|
61
|
+
:required="required"
|
|
62
|
+
:rows="rows"
|
|
63
|
+
:class="['r-textarea', `r-textarea--resize-${resize}`, { 'r-textarea--error': error }]"
|
|
64
|
+
@input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
|
|
65
|
+
/>
|
|
66
|
+
<p v-if="error" class="r-textarea-error">{{ error }}</p>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<style scoped>
|
|
71
|
+
.r-textarea-wrapper{display:flex;flex-direction:column;gap:var(--space-4)}.r-textarea-label{color:var(--text-secondary);font-size:var(--font-14);font-weight:var(--weight-medium)}.r-textarea-required{color:var(--color-danger);margin-left:2px}.r-textarea{background-color:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);box-sizing:border-box;color:var(--text-primary);font-family:inherit;font-size:var(--font-14);line-height:1.5;padding:var(--space-8) var(--space-12);transition:border-color var(--duration-fast) var(--ease-in-out),box-shadow var(--duration-fast) var(--ease-in-out);width:100%}.r-textarea--resize-none{resize:none}.r-textarea--resize-vertical{resize:vertical}.r-textarea::-moz-placeholder{color:var(--text-muted)}.r-textarea::placeholder{color:var(--text-muted)}.r-textarea:hover:not(:disabled){border-color:var(--text-muted)}.r-textarea:focus{border-color:var(--color-healthcare);box-shadow:0 0 0 3px var(--bg-hover);outline:none}.r-textarea:disabled{background-color:var(--bg-surface-2);cursor:not-allowed;opacity:.5}.r-textarea--error,.r-textarea--error:focus{border-color:var(--color-danger)}.r-textarea--error:focus{box-shadow:0 0 0 3px rgba(239,68,68,.15)}.r-textarea-error{color:var(--color-danger);font-size:var(--font-12);margin-top:var(--space-4)}
|
|
72
|
+
</style>
|