@tekkare/romulus 0.1.35 → 0.1.37
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/Button.vue +2 -2
- package/dist/runtime/components/Callout.vue +55 -0
- package/dist/runtime/components/ExportPanel.vue +744 -0
- package/dist/runtime/components/SavedViewCard.vue +295 -0
- package/dist/runtime/components/SavedViews.vue +282 -0
- package/dist/runtime/composables/useRmExport.d.ts +29 -0
- package/dist/runtime/composables/useRmExport.js +404 -0
- package/dist/runtime/utils/export.d.ts +199 -0
- package/dist/runtime/utils/export.js +98 -0
- package/package.json +9 -2
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmExportPanel - le panneau d'export d'une page, en quatre etapes.
|
|
3
|
+
|
|
4
|
+
L'export est la sortie de chaque ecran OIP, et jusqu'ici chaque page avait
|
|
5
|
+
son bouton : un XLSX, une colonne fixe, un nom de fichier different. Ce
|
|
6
|
+
panneau remplace ce bouton par une question posee dans l'ordre : quel
|
|
7
|
+
format, quelles donnees, quelle presentation, on lance.
|
|
8
|
+
|
|
9
|
+
Le panneau ne connait pas les formats par leur nom. Chaque format porte
|
|
10
|
+
trois drapeaux (`tabular`, `visual`, `paged`) et chaque section une nature
|
|
11
|
+
(`table`, `visual`, `meta`) : c'est leur croisement qui decide de ce qui est
|
|
12
|
+
propose. Un PNG n'a donc ni colonnes ni mise en page sans qu'aucune ligne du
|
|
13
|
+
composant ne parle de PNG, et un format ajoute plus tard herite du bon
|
|
14
|
+
comportement en posant ses drapeaux.
|
|
15
|
+
|
|
16
|
+
Le composant ne genere rien : il emet une configuration. Les fichiers sont
|
|
17
|
+
rendus par `useRmExport`, et c'est l'app qui fournit le contenu — elle seule
|
|
18
|
+
sait ce que sa page affiche.
|
|
19
|
+
-->
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
import { computed, reactive, ref, watch } from 'vue'
|
|
22
|
+
import type {
|
|
23
|
+
RmExportConfig,
|
|
24
|
+
RmExportFormat,
|
|
25
|
+
RmExportLayout,
|
|
26
|
+
RmExportOption,
|
|
27
|
+
RmExportRange,
|
|
28
|
+
RmExportRecent,
|
|
29
|
+
RmExportSection,
|
|
30
|
+
RmExportTemplate,
|
|
31
|
+
} from '../utils/export'
|
|
32
|
+
import {
|
|
33
|
+
RM_EXPORT_EXTENSIONS,
|
|
34
|
+
RM_EXPORT_FORMATS,
|
|
35
|
+
RM_EXPORT_LAYOUTS,
|
|
36
|
+
RM_EXPORT_OPTIONS,
|
|
37
|
+
RM_EXPORT_SECTIONS,
|
|
38
|
+
rmExportDuration,
|
|
39
|
+
rmExportSize,
|
|
40
|
+
rmExportSizeKb,
|
|
41
|
+
} from '../utils/export'
|
|
42
|
+
|
|
43
|
+
const props = withDefaults(
|
|
44
|
+
defineProps<{
|
|
45
|
+
/** Nom de fichier propose, sans extension. */
|
|
46
|
+
fileName: string
|
|
47
|
+
formats?: RmExportFormat[]
|
|
48
|
+
sections?: RmExportSection[]
|
|
49
|
+
ranges?: RmExportRange[]
|
|
50
|
+
/** Colonnes disponibles, dans l'ordre du tableau de la page. */
|
|
51
|
+
columns?: string[]
|
|
52
|
+
options?: RmExportOption[]
|
|
53
|
+
layouts?: RmExportLayout[]
|
|
54
|
+
/** Modeles pre-remplis. Vide : le panneau ouvre directement le wizard. */
|
|
55
|
+
templates?: RmExportTemplate[]
|
|
56
|
+
recent?: RmExportRecent[]
|
|
57
|
+
/**
|
|
58
|
+
* La programmation recurrente suppose un service qui execute l'export
|
|
59
|
+
* sans le navigateur. Tant qu'il n'existe pas, l'etape reste visible mais
|
|
60
|
+
* inerte : cacher la promesse serait pire que l'annoncer.
|
|
61
|
+
*/
|
|
62
|
+
scheduling?: boolean
|
|
63
|
+
busy?: boolean
|
|
64
|
+
/** Message d'erreur du dernier export. */
|
|
65
|
+
error?: string
|
|
66
|
+
/** Message de confirmation (lien copie, fichier pret). */
|
|
67
|
+
notice?: string
|
|
68
|
+
lang?: 'fr' | 'en'
|
|
69
|
+
}>(),
|
|
70
|
+
{
|
|
71
|
+
formats: () => RM_EXPORT_FORMATS,
|
|
72
|
+
sections: () => RM_EXPORT_SECTIONS,
|
|
73
|
+
ranges: () => [],
|
|
74
|
+
columns: () => [],
|
|
75
|
+
options: () => RM_EXPORT_OPTIONS,
|
|
76
|
+
layouts: () => RM_EXPORT_LAYOUTS,
|
|
77
|
+
templates: () => [],
|
|
78
|
+
recent: () => [],
|
|
79
|
+
scheduling: false,
|
|
80
|
+
busy: false,
|
|
81
|
+
error: '',
|
|
82
|
+
notice: '',
|
|
83
|
+
lang: 'fr',
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const emit = defineEmits<{ export: [config: RmExportConfig] }>()
|
|
88
|
+
|
|
89
|
+
const LABELS = {
|
|
90
|
+
fr: {
|
|
91
|
+
tabExport: 'Exporter',
|
|
92
|
+
tabMine: 'Mes exports',
|
|
93
|
+
lead: 'Démarrez avec un modèle pré-configuré, ou personnalisez chaque paramètre via le wizard 4 étapes.',
|
|
94
|
+
entryTitle: 'Comment voulez-vous commencer ?',
|
|
95
|
+
entryHint: 'Les modèles pré-remplissent toutes les étapes, vous arrivez directement à la revue finale et pouvez ajuster avant de lancer.',
|
|
96
|
+
custom: 'Export personnalisé',
|
|
97
|
+
customMeta: 'Configurer chaque paramètre via le wizard 4 étapes',
|
|
98
|
+
backToEntry: 'Choisir un autre modèle',
|
|
99
|
+
template: 'Modèle',
|
|
100
|
+
steps: ['Format', 'Données', 'Personnaliser', 'Exporter'],
|
|
101
|
+
step1: 'Choisissez le format de votre export. Le contenu disponible (graphiques, tableau, sections) varie selon le format.',
|
|
102
|
+
step2: 'Quelles données inclure dans l\'export ?',
|
|
103
|
+
step3: 'Nom du fichier, mise en page, options avancées.',
|
|
104
|
+
step4: 'Vérifiez votre configuration et lancez l\'export.',
|
|
105
|
+
range: 'Plage de données',
|
|
106
|
+
sectionsLabel: 'Sections à inclure',
|
|
107
|
+
columnsLabel: 'Colonnes du tableau',
|
|
108
|
+
fileNameLabel: 'Nom du fichier',
|
|
109
|
+
fileNameHint: 'Extension ajoutée automatiquement selon le format.',
|
|
110
|
+
layoutLabel: 'Mise en page',
|
|
111
|
+
optionsLabel: 'Options',
|
|
112
|
+
scheduleLabel: 'Type d\'export',
|
|
113
|
+
once: 'Une seule fois',
|
|
114
|
+
recurring: 'Récurrent',
|
|
115
|
+
schedulingSoon: 'La programmation récurrente et l\'envoi par email arrivent avec le service d\'exports. Pour l\'instant, un export part depuis ce navigateur.',
|
|
116
|
+
frequency: 'Fréquence',
|
|
117
|
+
daily: 'Quotidien',
|
|
118
|
+
weekly: 'Hebdomadaire',
|
|
119
|
+
monthly: 'Mensuel',
|
|
120
|
+
email: 'Envoyer par email à chaque export',
|
|
121
|
+
recap: 'Récapitulatif',
|
|
122
|
+
recapFormat: 'Format',
|
|
123
|
+
recapFile: 'Nom du fichier',
|
|
124
|
+
recapData: 'Données',
|
|
125
|
+
recapSections: 'Sections',
|
|
126
|
+
recapColumns: 'Colonnes',
|
|
127
|
+
recapLayout: 'Mise en page',
|
|
128
|
+
recapSchedule: 'Programmation',
|
|
129
|
+
recapSize: 'Taille estimée',
|
|
130
|
+
recapDuration: 'Durée estimée',
|
|
131
|
+
rows: 'lignes',
|
|
132
|
+
of: 'sur',
|
|
133
|
+
prev: 'Retour',
|
|
134
|
+
next: 'Suivant',
|
|
135
|
+
launch: 'Lancer l\'export',
|
|
136
|
+
running: 'Export en cours...',
|
|
137
|
+
progress: 'Étape',
|
|
138
|
+
recentTitle: 'Exports récents',
|
|
139
|
+
recentEmpty: 'Aucun export lancé depuis ce navigateur.',
|
|
140
|
+
recentHint: 'Historique local à ce navigateur : les exports ne sont pas conservés sur nos serveurs.',
|
|
141
|
+
permalinkHint: 'Le lien reprend la vue courante, filtres compris. Rien à configurer.',
|
|
142
|
+
nothing: 'Aucun contenu disponible pour ce format sur cette page.',
|
|
143
|
+
},
|
|
144
|
+
en: {
|
|
145
|
+
tabExport: 'Export',
|
|
146
|
+
tabMine: 'My exports',
|
|
147
|
+
lead: 'Start from a preset, or set every parameter through the 4-step wizard.',
|
|
148
|
+
entryTitle: 'How do you want to start?',
|
|
149
|
+
entryHint: 'Presets fill in every step, you land on the final review and can adjust before running.',
|
|
150
|
+
custom: 'Custom export',
|
|
151
|
+
customMeta: 'Set every parameter through the 4-step wizard',
|
|
152
|
+
backToEntry: 'Pick another preset',
|
|
153
|
+
template: 'Preset',
|
|
154
|
+
steps: ['Format', 'Data', 'Customize', 'Export'],
|
|
155
|
+
step1: 'Pick your export format. Available content (charts, table, sections) depends on the format.',
|
|
156
|
+
step2: 'Which data should the export contain?',
|
|
157
|
+
step3: 'File name, layout, advanced options.',
|
|
158
|
+
step4: 'Check your setup and run the export.',
|
|
159
|
+
range: 'Data range',
|
|
160
|
+
sectionsLabel: 'Sections to include',
|
|
161
|
+
columnsLabel: 'Table columns',
|
|
162
|
+
fileNameLabel: 'File name',
|
|
163
|
+
fileNameHint: 'Extension added automatically, based on the format.',
|
|
164
|
+
layoutLabel: 'Layout',
|
|
165
|
+
optionsLabel: 'Options',
|
|
166
|
+
scheduleLabel: 'Export type',
|
|
167
|
+
once: 'Once',
|
|
168
|
+
recurring: 'Recurring',
|
|
169
|
+
schedulingSoon: 'Recurring exports and email delivery ship with the export service. For now, an export runs from this browser.',
|
|
170
|
+
frequency: 'Frequency',
|
|
171
|
+
daily: 'Daily',
|
|
172
|
+
weekly: 'Weekly',
|
|
173
|
+
monthly: 'Monthly',
|
|
174
|
+
email: 'Email me every export',
|
|
175
|
+
recap: 'Summary',
|
|
176
|
+
recapFormat: 'Format',
|
|
177
|
+
recapFile: 'File name',
|
|
178
|
+
recapData: 'Data',
|
|
179
|
+
recapSections: 'Sections',
|
|
180
|
+
recapColumns: 'Columns',
|
|
181
|
+
recapLayout: 'Layout',
|
|
182
|
+
recapSchedule: 'Schedule',
|
|
183
|
+
recapSize: 'Estimated size',
|
|
184
|
+
recapDuration: 'Estimated time',
|
|
185
|
+
rows: 'rows',
|
|
186
|
+
of: 'of',
|
|
187
|
+
prev: 'Back',
|
|
188
|
+
next: 'Next',
|
|
189
|
+
launch: 'Run export',
|
|
190
|
+
running: 'Exporting...',
|
|
191
|
+
progress: 'Step',
|
|
192
|
+
recentTitle: 'Recent exports',
|
|
193
|
+
recentEmpty: 'No export run from this browser yet.',
|
|
194
|
+
recentHint: 'History is local to this browser: exports are not kept on our servers.',
|
|
195
|
+
permalinkHint: 'The link points at the current view, filters included. Nothing to configure.',
|
|
196
|
+
nothing: 'No content available for this format on this page.',
|
|
197
|
+
},
|
|
198
|
+
} as const
|
|
199
|
+
|
|
200
|
+
const t = computed(() => LABELS[props.lang] ?? LABELS.fr)
|
|
201
|
+
|
|
202
|
+
/** Premier format utilisable : un catalogue peut ouvrir sur un format grise. */
|
|
203
|
+
const firstFormat = computed(() => props.formats.find(format => !format.disabled) ?? props.formats[0])
|
|
204
|
+
|
|
205
|
+
const config = reactive<RmExportConfig>({
|
|
206
|
+
format: firstFormat.value?.key ?? 'xlsx',
|
|
207
|
+
range: props.ranges[0]?.key ?? 'filtered',
|
|
208
|
+
sections: props.sections.filter(section => section.on).map(section => section.key),
|
|
209
|
+
columns: [...props.columns],
|
|
210
|
+
options: props.options.filter(option => option.on).map(option => option.key),
|
|
211
|
+
fileName: props.fileName,
|
|
212
|
+
layout: props.layouts[0]?.key ?? 'a4-portrait',
|
|
213
|
+
schedule: 'once',
|
|
214
|
+
frequency: 'monthly',
|
|
215
|
+
email: false,
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
// Le nom de fichier propose depend de la page et du scope : il change sous le
|
|
219
|
+
// panneau quand l'utilisateur filtre. On suit la proposition tant qu'il n'a
|
|
220
|
+
// pas ecrit son propre nom.
|
|
221
|
+
const renamed = ref(false)
|
|
222
|
+
watch(
|
|
223
|
+
() => props.fileName,
|
|
224
|
+
(next) => {
|
|
225
|
+
if (!renamed.value) config.fileName = next
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
// Les colonnes ne sont connues qu'une fois la page chargee : la selection
|
|
230
|
+
// suit, sinon un export lance tot sort avec toutes les colonnes alors que le
|
|
231
|
+
// panneau en montre cinq.
|
|
232
|
+
watch(
|
|
233
|
+
() => props.columns,
|
|
234
|
+
(next) => {
|
|
235
|
+
config.columns = config.columns.filter(column => next.includes(column))
|
|
236
|
+
if (!config.columns.length) config.columns = [...next]
|
|
237
|
+
},
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
const step = ref(1)
|
|
241
|
+
const mode = ref<'entry' | 'wizard'>(props.templates.length ? 'entry' : 'wizard')
|
|
242
|
+
const templateKey = ref<string | null>(null)
|
|
243
|
+
|
|
244
|
+
const format = computed(
|
|
245
|
+
() => props.formats.find(item => item.key === config.format) ?? firstFormat.value,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
/** Une section n'a de sens que si le format sait la rendre. */
|
|
249
|
+
function enabled(section: RmExportSection) {
|
|
250
|
+
if (!format.value) return false
|
|
251
|
+
if (section.kind === 'table') return !!format.value.tabular
|
|
252
|
+
if (section.kind === 'visual') return !!format.value.visual
|
|
253
|
+
return !!format.value.paged
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const showColumns = computed(() => !!format.value?.tabular && props.columns.length > 0)
|
|
257
|
+
const showLayout = computed(() => !!format.value?.paged && props.layouts.length > 0)
|
|
258
|
+
const showRange = computed(() => !!format.value?.tabular && props.ranges.length > 0)
|
|
259
|
+
const isPermalink = computed(() => !format.value?.tabular && !format.value?.visual)
|
|
260
|
+
|
|
261
|
+
/** Sections cochees, une fois retirees celles que le format ne rend pas. */
|
|
262
|
+
const activeSections = computed(() =>
|
|
263
|
+
props.sections.filter(section => enabled(section) && config.sections.includes(section.key)),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
const availableSections = computed(() => props.sections.filter(section => enabled(section)))
|
|
267
|
+
|
|
268
|
+
const range = computed(
|
|
269
|
+
() => props.ranges.find(item => item.key === config.range) ?? props.ranges[0],
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
const rows = computed(() => (showRange.value ? range.value?.rows ?? 0 : 0))
|
|
273
|
+
|
|
274
|
+
const sizeKb = computed(() =>
|
|
275
|
+
rmExportSizeKb({
|
|
276
|
+
format: config.format,
|
|
277
|
+
rows: rows.value,
|
|
278
|
+
columns: config.columns.length,
|
|
279
|
+
totalColumns: props.columns.length,
|
|
280
|
+
sections: activeSections.value.length,
|
|
281
|
+
includesTable: activeSections.value.some(section => section.kind === 'table'),
|
|
282
|
+
zip: config.options.includes('zip'),
|
|
283
|
+
}),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
const fullFileName = computed(() => {
|
|
287
|
+
if (isPermalink.value) return '—'
|
|
288
|
+
const extension = RM_EXPORT_EXTENSIONS[config.format] ?? config.format
|
|
289
|
+
return `${config.fileName || 'export'}.${extension}`
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
const scheduleLabel = computed(() => {
|
|
293
|
+
if (config.schedule === 'once') return t.value.once
|
|
294
|
+
const names: Record<string, string> = {
|
|
295
|
+
daily: t.value.daily,
|
|
296
|
+
weekly: t.value.weekly,
|
|
297
|
+
monthly: t.value.monthly,
|
|
298
|
+
}
|
|
299
|
+
return `${t.value.recurring}, ${(names[config.frequency] ?? '').toLowerCase()}`
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
/** Le format ne rend rien de ce que la page declare : le lancer serait vide. */
|
|
303
|
+
const emptyForFormat = computed(
|
|
304
|
+
() => !isPermalink.value && availableSections.value.length === 0,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
const formatMeta = computed(() => (format.value?.disabled ? format.value.disabledHint ?? '' : ''))
|
|
308
|
+
|
|
309
|
+
function pickFormat(item: RmExportFormat) {
|
|
310
|
+
if (item.disabled) return
|
|
311
|
+
config.format = item.key
|
|
312
|
+
// Une section que le nouveau format ne rend pas reste cochee dans l'etat
|
|
313
|
+
// mais ne compte plus : c'est ce qui permet de revenir a PDF sans avoir
|
|
314
|
+
// perdu ses choix.
|
|
315
|
+
props.sections.forEach((section) => {
|
|
316
|
+
if (enabled(section) && section.on && !config.sections.includes(section.key)) {
|
|
317
|
+
config.sections.push(section.key)
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function toggle(list: string[], key: string) {
|
|
323
|
+
const index = list.indexOf(key)
|
|
324
|
+
if (index === -1) list.push(key)
|
|
325
|
+
else list.splice(index, 1)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function applyTemplate(template: RmExportTemplate | null) {
|
|
329
|
+
if (!template) {
|
|
330
|
+
templateKey.value = null
|
|
331
|
+
mode.value = 'wizard'
|
|
332
|
+
step.value = 1
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
Object.assign(config, template.config)
|
|
336
|
+
if (template.config.fileName) renamed.value = true
|
|
337
|
+
templateKey.value = template.key
|
|
338
|
+
mode.value = 'wizard'
|
|
339
|
+
step.value = 4
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function goto(next: number) {
|
|
343
|
+
step.value = Math.min(4, Math.max(1, next))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function launch() {
|
|
347
|
+
emit('export', { ...config, sections: activeSections.value.map(section => section.key) })
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const recentDate = (at: string | Date) =>
|
|
351
|
+
new Date(at).toLocaleDateString(props.lang === 'en' ? 'en-US' : 'fr-FR', {
|
|
352
|
+
day: 'numeric',
|
|
353
|
+
month: 'long',
|
|
354
|
+
})
|
|
355
|
+
</script>
|
|
356
|
+
|
|
357
|
+
<template>
|
|
358
|
+
<div class="rm-export">
|
|
359
|
+
<div class="rm-export__tabs" role="tablist">
|
|
360
|
+
<button class="rm-export__tab rm-export__tab--active" type="button" role="tab">
|
|
361
|
+
<RmIcon name="PlusCircle" :size="16" /> {{ t.tabExport }}
|
|
362
|
+
</button>
|
|
363
|
+
<button
|
|
364
|
+
class="rm-export__tab"
|
|
365
|
+
type="button"
|
|
366
|
+
role="tab"
|
|
367
|
+
disabled
|
|
368
|
+
:title="t.schedulingSoon"
|
|
369
|
+
>
|
|
370
|
+
<RmIcon name="ClockClockwise" :size="16" /> {{ t.tabMine }}
|
|
371
|
+
</button>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<p class="rm-export__lead">{{ t.lead }}</p>
|
|
375
|
+
|
|
376
|
+
<!-- Entree : modeles pre-remplis, ou le wizard a la main -->
|
|
377
|
+
<div v-if="mode === 'entry'" class="rm-export__entry">
|
|
378
|
+
<p class="rm-export__eyebrow">{{ t.entryTitle }}</p>
|
|
379
|
+
<div class="rm-export__templates">
|
|
380
|
+
<button
|
|
381
|
+
v-for="template in templates"
|
|
382
|
+
:key="template.key"
|
|
383
|
+
class="rm-export__template"
|
|
384
|
+
type="button"
|
|
385
|
+
@click="applyTemplate(template)"
|
|
386
|
+
>
|
|
387
|
+
<RmIcon :name="template.icon" :size="26" weight="fill" :color="template.color" />
|
|
388
|
+
<span class="rm-export__template-body">
|
|
389
|
+
<span class="rm-export__template-name">{{ template.name }}</span>
|
|
390
|
+
<span class="rm-export__template-meta">{{ template.meta }}</span>
|
|
391
|
+
</span>
|
|
392
|
+
</button>
|
|
393
|
+
<button
|
|
394
|
+
class="rm-export__template rm-export__template--custom"
|
|
395
|
+
type="button"
|
|
396
|
+
@click="applyTemplate(null)"
|
|
397
|
+
>
|
|
398
|
+
<RmIcon name="PlusCircle" :size="26" />
|
|
399
|
+
<span class="rm-export__template-body">
|
|
400
|
+
<span class="rm-export__template-name">{{ t.custom }}</span>
|
|
401
|
+
<span class="rm-export__template-meta">{{ t.customMeta }}</span>
|
|
402
|
+
</span>
|
|
403
|
+
</button>
|
|
404
|
+
</div>
|
|
405
|
+
<p class="rm-export__hint">{{ t.entryHint }}</p>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<!-- Wizard -->
|
|
409
|
+
<div v-else class="rm-export__wizard">
|
|
410
|
+
<div v-if="templates.length" class="rm-export__wizard-head">
|
|
411
|
+
<button class="rm-export__back" type="button" @click="mode = 'entry'">
|
|
412
|
+
<RmIcon name="ArrowLeft" :size="14" /> {{ t.backToEntry }}
|
|
413
|
+
</button>
|
|
414
|
+
<RmBadge v-if="templateKey" variant="info">
|
|
415
|
+
{{ t.template }} : {{ templates.find(item => item.key === templateKey)?.name }}
|
|
416
|
+
</RmBadge>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
<div class="rm-export__stepper" role="tablist">
|
|
420
|
+
<template v-for="(label, index) in t.steps" :key="label">
|
|
421
|
+
<div v-if="index" class="rm-export__step-divider" />
|
|
422
|
+
<button
|
|
423
|
+
class="rm-export__step"
|
|
424
|
+
:class="{
|
|
425
|
+
'rm-export__step--active': step === index + 1,
|
|
426
|
+
'rm-export__step--done': step > index + 1,
|
|
427
|
+
}"
|
|
428
|
+
type="button"
|
|
429
|
+
role="tab"
|
|
430
|
+
:aria-selected="step === index + 1"
|
|
431
|
+
@click="goto(index + 1)"
|
|
432
|
+
>
|
|
433
|
+
<span class="rm-export__step-num">{{ index + 1 }}</span>
|
|
434
|
+
<span class="rm-export__step-label">{{ label }}</span>
|
|
435
|
+
</button>
|
|
436
|
+
</template>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
<!-- Etape 1 : format -->
|
|
440
|
+
<div v-if="step === 1" class="rm-export__panel">
|
|
441
|
+
<p class="rm-export__desc">{{ t.step1 }}</p>
|
|
442
|
+
<div class="rm-export__formats" role="radiogroup">
|
|
443
|
+
<label
|
|
444
|
+
v-for="item in formats"
|
|
445
|
+
:key="item.key"
|
|
446
|
+
class="rm-export__format"
|
|
447
|
+
:class="{
|
|
448
|
+
'rm-export__format--active': config.format === item.key,
|
|
449
|
+
'rm-export__format--off': item.disabled,
|
|
450
|
+
}"
|
|
451
|
+
:title="item.disabled ? item.disabledHint : undefined"
|
|
452
|
+
>
|
|
453
|
+
<input
|
|
454
|
+
type="radio"
|
|
455
|
+
name="rm-export-format"
|
|
456
|
+
:value="item.key"
|
|
457
|
+
:checked="config.format === item.key"
|
|
458
|
+
:disabled="item.disabled"
|
|
459
|
+
@change="pickFormat(item)"
|
|
460
|
+
>
|
|
461
|
+
<RmIcon :name="item.icon" :size="24" weight="fill" :color="item.color" />
|
|
462
|
+
<span class="rm-export__format-text">
|
|
463
|
+
<span class="rm-export__format-name">{{ item.label }}</span>
|
|
464
|
+
<span class="rm-export__format-desc">{{ item.desc }}</span>
|
|
465
|
+
<span v-if="item.disabled && item.disabledHint" class="rm-export__format-meta">
|
|
466
|
+
{{ item.disabledHint }}
|
|
467
|
+
</span>
|
|
468
|
+
</span>
|
|
469
|
+
</label>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
|
|
473
|
+
<!-- Etape 2 : donnees -->
|
|
474
|
+
<div v-else-if="step === 2" class="rm-export__panel">
|
|
475
|
+
<p class="rm-export__desc">{{ isPermalink ? t.permalinkHint : t.step2 }}</p>
|
|
476
|
+
|
|
477
|
+
<div v-if="showRange" class="rm-export__row">
|
|
478
|
+
<span class="rm-export__label">{{ t.range }}</span>
|
|
479
|
+
<RmSegmented
|
|
480
|
+
v-model="config.range"
|
|
481
|
+
block
|
|
482
|
+
size="sm"
|
|
483
|
+
:options="ranges.map(item => ({ value: item.key, label: item.label }))"
|
|
484
|
+
:aria-label="t.range"
|
|
485
|
+
/>
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
<div v-if="availableSections.length" class="rm-export__row">
|
|
489
|
+
<span class="rm-export__label">{{ t.sectionsLabel }}</span>
|
|
490
|
+
<div class="rm-export__checks">
|
|
491
|
+
<label
|
|
492
|
+
v-for="section in sections"
|
|
493
|
+
:key="section.key"
|
|
494
|
+
class="rm-export__check"
|
|
495
|
+
:class="{ 'rm-export__check--off': !enabled(section) }"
|
|
496
|
+
>
|
|
497
|
+
<input
|
|
498
|
+
type="checkbox"
|
|
499
|
+
:checked="config.sections.includes(section.key) && enabled(section)"
|
|
500
|
+
:disabled="!enabled(section)"
|
|
501
|
+
@change="toggle(config.sections, section.key)"
|
|
502
|
+
>
|
|
503
|
+
{{ section.label }}
|
|
504
|
+
</label>
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
|
|
508
|
+
<div v-if="showColumns" class="rm-export__row">
|
|
509
|
+
<span class="rm-export__label">
|
|
510
|
+
{{ t.columnsLabel }} ({{ config.columns.length }} {{ t.of }} {{ columns.length }})
|
|
511
|
+
</span>
|
|
512
|
+
<div class="rm-export__chips">
|
|
513
|
+
<button
|
|
514
|
+
v-for="column in columns"
|
|
515
|
+
:key="column"
|
|
516
|
+
class="rm-export__chip"
|
|
517
|
+
:class="config.columns.includes(column) ? 'rm-export__chip--on' : 'rm-export__chip--off'"
|
|
518
|
+
type="button"
|
|
519
|
+
@click="toggle(config.columns, column)"
|
|
520
|
+
>
|
|
521
|
+
{{ config.columns.includes(column) ? column : `+ ${column}` }}
|
|
522
|
+
<RmIcon v-if="config.columns.includes(column)" name="X" :size="11" />
|
|
523
|
+
</button>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
|
|
527
|
+
<p v-if="emptyForFormat" class="rm-export__hint">{{ t.nothing }}</p>
|
|
528
|
+
</div>
|
|
529
|
+
|
|
530
|
+
<!-- Etape 3 : personnaliser -->
|
|
531
|
+
<div v-else-if="step === 3" class="rm-export__panel">
|
|
532
|
+
<p class="rm-export__desc">{{ isPermalink ? t.permalinkHint : t.step3 }}</p>
|
|
533
|
+
|
|
534
|
+
<div v-if="!isPermalink" class="rm-export__row">
|
|
535
|
+
<label class="rm-export__label" for="rm-export-file">{{ t.fileNameLabel }}</label>
|
|
536
|
+
<input
|
|
537
|
+
id="rm-export-file"
|
|
538
|
+
v-model="config.fileName"
|
|
539
|
+
class="rm-export__input"
|
|
540
|
+
type="text"
|
|
541
|
+
@input="renamed = true"
|
|
542
|
+
>
|
|
543
|
+
<span class="rm-export__hint">{{ t.fileNameHint }}</span>
|
|
544
|
+
</div>
|
|
545
|
+
|
|
546
|
+
<div v-if="showLayout" class="rm-export__row">
|
|
547
|
+
<span class="rm-export__label">{{ t.layoutLabel }}</span>
|
|
548
|
+
<RmSegmented
|
|
549
|
+
v-model="config.layout"
|
|
550
|
+
block
|
|
551
|
+
size="sm"
|
|
552
|
+
:options="layouts.map(item => ({ value: item.key, label: item.label }))"
|
|
553
|
+
:aria-label="t.layoutLabel"
|
|
554
|
+
/>
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
<div v-if="options.length" class="rm-export__row">
|
|
558
|
+
<span class="rm-export__label">{{ t.optionsLabel }}</span>
|
|
559
|
+
<div class="rm-export__checks">
|
|
560
|
+
<label
|
|
561
|
+
v-for="option in options"
|
|
562
|
+
:key="option.key"
|
|
563
|
+
class="rm-export__check"
|
|
564
|
+
:class="{ 'rm-export__check--off': option.disabled }"
|
|
565
|
+
>
|
|
566
|
+
<input
|
|
567
|
+
type="checkbox"
|
|
568
|
+
:checked="config.options.includes(option.key)"
|
|
569
|
+
:disabled="option.disabled"
|
|
570
|
+
@change="toggle(config.options, option.key)"
|
|
571
|
+
>
|
|
572
|
+
{{ option.label }}
|
|
573
|
+
</label>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
<!-- Etape 4 : exporter -->
|
|
579
|
+
<div v-else class="rm-export__panel">
|
|
580
|
+
<p class="rm-export__desc">{{ t.step4 }}</p>
|
|
581
|
+
|
|
582
|
+
<div class="rm-export__row">
|
|
583
|
+
<span class="rm-export__label">{{ t.scheduleLabel }}</span>
|
|
584
|
+
<RmSegmented
|
|
585
|
+
v-model="config.schedule"
|
|
586
|
+
block
|
|
587
|
+
size="sm"
|
|
588
|
+
:options="[
|
|
589
|
+
{ value: 'once', label: t.once, icon: 'download-simple' },
|
|
590
|
+
{ value: 'recurring', label: t.recurring, icon: 'clock-clockwise', disabled: !scheduling },
|
|
591
|
+
]"
|
|
592
|
+
:aria-label="t.scheduleLabel"
|
|
593
|
+
/>
|
|
594
|
+
<span v-if="!scheduling" class="rm-export__hint">{{ t.schedulingSoon }}</span>
|
|
595
|
+
|
|
596
|
+
<div v-if="scheduling && config.schedule === 'recurring'" class="rm-export__sub">
|
|
597
|
+
<span class="rm-export__label">{{ t.frequency }}</span>
|
|
598
|
+
<RmSegmented
|
|
599
|
+
v-model="config.frequency"
|
|
600
|
+
block
|
|
601
|
+
size="sm"
|
|
602
|
+
:options="[
|
|
603
|
+
{ value: 'daily', label: t.daily },
|
|
604
|
+
{ value: 'weekly', label: t.weekly },
|
|
605
|
+
{ value: 'monthly', label: t.monthly },
|
|
606
|
+
]"
|
|
607
|
+
:aria-label="t.frequency"
|
|
608
|
+
/>
|
|
609
|
+
<label class="rm-export__check">
|
|
610
|
+
<input v-model="config.email" type="checkbox"> {{ t.email }}
|
|
611
|
+
</label>
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
|
|
615
|
+
<div class="rm-export__recap">
|
|
616
|
+
<p class="rm-export__eyebrow">{{ t.recap }}</p>
|
|
617
|
+
<div class="rm-export__recap-row">
|
|
618
|
+
<RmIcon name="FileText" :size="16" />
|
|
619
|
+
<div>
|
|
620
|
+
<div class="rm-export__recap-k">{{ t.recapFormat }}</div>
|
|
621
|
+
<div class="rm-export__recap-v">{{ format?.label }}</div>
|
|
622
|
+
</div>
|
|
623
|
+
</div>
|
|
624
|
+
<div class="rm-export__recap-row">
|
|
625
|
+
<RmIcon name="Textbox" :size="16" />
|
|
626
|
+
<div>
|
|
627
|
+
<div class="rm-export__recap-k">{{ t.recapFile }}</div>
|
|
628
|
+
<div class="rm-export__recap-v">{{ fullFileName }}</div>
|
|
629
|
+
</div>
|
|
630
|
+
</div>
|
|
631
|
+
<div v-if="showRange" class="rm-export__recap-row">
|
|
632
|
+
<RmIcon name="Database" :size="16" />
|
|
633
|
+
<div>
|
|
634
|
+
<div class="rm-export__recap-k">{{ t.recapData }}</div>
|
|
635
|
+
<div class="rm-export__recap-v">
|
|
636
|
+
{{ rows.toLocaleString(lang === 'en' ? 'en-US' : 'fr-FR') }} {{ t.rows }}, {{ range?.label }}
|
|
637
|
+
</div>
|
|
638
|
+
</div>
|
|
639
|
+
</div>
|
|
640
|
+
<div v-if="availableSections.length" class="rm-export__recap-row">
|
|
641
|
+
<RmIcon name="CardsThree" :size="16" />
|
|
642
|
+
<div>
|
|
643
|
+
<div class="rm-export__recap-k">{{ t.recapSections }}</div>
|
|
644
|
+
<div class="rm-export__recap-v">
|
|
645
|
+
{{ activeSections.length }} {{ t.of }} {{ availableSections.length }}
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
648
|
+
</div>
|
|
649
|
+
<div v-if="showColumns" class="rm-export__recap-row">
|
|
650
|
+
<RmIcon name="Columns" :size="16" />
|
|
651
|
+
<div>
|
|
652
|
+
<div class="rm-export__recap-k">{{ t.recapColumns }}</div>
|
|
653
|
+
<div class="rm-export__recap-v">
|
|
654
|
+
{{ config.columns.length }} {{ t.of }} {{ columns.length }}
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
<div v-if="showLayout" class="rm-export__recap-row">
|
|
659
|
+
<RmIcon name="Layout" :size="16" />
|
|
660
|
+
<div>
|
|
661
|
+
<div class="rm-export__recap-k">{{ t.recapLayout }}</div>
|
|
662
|
+
<div class="rm-export__recap-v">
|
|
663
|
+
{{ layouts.find(item => item.key === config.layout)?.label }}
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
666
|
+
</div>
|
|
667
|
+
<div class="rm-export__recap-row">
|
|
668
|
+
<RmIcon name="Clock" :size="16" />
|
|
669
|
+
<div>
|
|
670
|
+
<div class="rm-export__recap-k">{{ t.recapSchedule }}</div>
|
|
671
|
+
<div class="rm-export__recap-v">{{ scheduleLabel }}</div>
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
<div v-if="!isPermalink" class="rm-export__recap-row">
|
|
675
|
+
<RmIcon name="HardDrives" :size="16" />
|
|
676
|
+
<div>
|
|
677
|
+
<div class="rm-export__recap-k">{{ t.recapSize }}</div>
|
|
678
|
+
<div class="rm-export__recap-v">{{ rmExportSize(sizeKb) }}</div>
|
|
679
|
+
</div>
|
|
680
|
+
</div>
|
|
681
|
+
<div v-if="!isPermalink" class="rm-export__recap-row">
|
|
682
|
+
<RmIcon name="Timer" :size="16" />
|
|
683
|
+
<div>
|
|
684
|
+
<div class="rm-export__recap-k">{{ t.recapDuration }}</div>
|
|
685
|
+
<div class="rm-export__recap-v">{{ rmExportDuration(sizeKb, rows) }}</div>
|
|
686
|
+
</div>
|
|
687
|
+
</div>
|
|
688
|
+
</div>
|
|
689
|
+
|
|
690
|
+
<p v-if="formatMeta" class="rm-export__hint">{{ formatMeta }}</p>
|
|
691
|
+
<p v-if="notice" class="rm-export__notice">{{ notice }}</p>
|
|
692
|
+
<p v-if="error" class="rm-export__error">{{ error }}</p>
|
|
693
|
+
</div>
|
|
694
|
+
|
|
695
|
+
<div class="rm-export__footer">
|
|
696
|
+
<RmButton variant="secondary" size="sm" :disabled="step === 1" prefix-icon="ArrowLeft" @click="goto(step - 1)">
|
|
697
|
+
{{ t.prev }}
|
|
698
|
+
</RmButton>
|
|
699
|
+
<span class="rm-export__progress">{{ t.progress }} <b>{{ step }}</b> {{ t.of }} 4</span>
|
|
700
|
+
<RmButton
|
|
701
|
+
v-if="step < 4"
|
|
702
|
+
variant="primary-fill"
|
|
703
|
+
size="sm"
|
|
704
|
+
suffix-icon="ArrowRight"
|
|
705
|
+
@click="goto(step + 1)"
|
|
706
|
+
>
|
|
707
|
+
{{ t.next }}
|
|
708
|
+
</RmButton>
|
|
709
|
+
<RmButton
|
|
710
|
+
v-else
|
|
711
|
+
variant="primary-fill"
|
|
712
|
+
size="sm"
|
|
713
|
+
prefix-icon="DownloadSimple"
|
|
714
|
+
:disabled="busy || emptyForFormat"
|
|
715
|
+
@click="launch"
|
|
716
|
+
>
|
|
717
|
+
{{ busy ? t.running : t.launch }}
|
|
718
|
+
</RmButton>
|
|
719
|
+
</div>
|
|
720
|
+
</div>
|
|
721
|
+
|
|
722
|
+
<!-- Exports recents -->
|
|
723
|
+
<div class="rm-export__history">
|
|
724
|
+
<p class="rm-export__eyebrow">
|
|
725
|
+
<RmIcon name="DownloadSimple" :size="14" /> {{ t.recentTitle }}
|
|
726
|
+
</p>
|
|
727
|
+
<p v-if="!recent.length" class="rm-export__hint">{{ t.recentEmpty }}</p>
|
|
728
|
+
<div v-for="(item, index) in recent" :key="`${item.name}-${index}`" class="rm-export__history-row">
|
|
729
|
+
<RmIcon name="File" :size="16" />
|
|
730
|
+
<span class="rm-export__history-name">{{ item.name }}</span>
|
|
731
|
+
<span class="rm-export__history-meta">
|
|
732
|
+
<template v-if="item.rows">{{ item.rows.toLocaleString(lang === 'en' ? 'en-US' : 'fr-FR') }} {{ t.rows }} · </template>
|
|
733
|
+
<template v-if="item.size">{{ item.size }} · </template>
|
|
734
|
+
{{ recentDate(item.at) }}
|
|
735
|
+
</span>
|
|
736
|
+
</div>
|
|
737
|
+
<p v-if="recent.length" class="rm-export__hint">{{ t.recentHint }}</p>
|
|
738
|
+
</div>
|
|
739
|
+
</div>
|
|
740
|
+
</template>
|
|
741
|
+
|
|
742
|
+
<style scoped>
|
|
743
|
+
.rm-export{display:flex;flex-direction:column}.rm-export__tabs{border-bottom:1px solid var(--border-default);display:flex;margin-bottom:var(--space-16)}.rm-export__tab{align-items:center;background:none;border:none;border-bottom:3px solid transparent;color:var(--text-muted);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-14);font-weight:600;gap:var(--space-6);padding:var(--space-12) var(--space-16)}.rm-export__tab--active{border-bottom-color:var(--color-primary);color:var(--color-secondary)}.rm-export__tab:disabled{cursor:not-allowed;opacity:.5}.rm-export__desc,.rm-export__lead{color:var(--text-secondary);font-size:12.5px;line-height:1.55;margin:0 0 var(--space-16)}.rm-export__eyebrow{align-items:center;color:var(--text-muted);display:flex;font-size:10.5px;font-weight:700;gap:var(--space-6);letter-spacing:.1em;margin:0 0 var(--space-8);text-transform:uppercase}.rm-export__hint{color:var(--text-muted);display:block;font-size:11.5px;line-height:1.45;margin:var(--space-8) 0 0}.rm-export__notice{color:var(--color-success)}.rm-export__error,.rm-export__notice{font-size:12px;margin:var(--space-8) 0 0}.rm-export__error{color:var(--color-danger)}.rm-export__templates{display:grid;gap:var(--space-8)}.rm-export__template{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl);cursor:pointer;display:flex;font-family:inherit;gap:var(--space-12);padding:var(--space-12) var(--space-16);text-align:left;transition:border-color var(--duration-fast),background var(--duration-fast),transform var(--duration-fast)}.rm-export__template:hover{background:var(--bg-hover);border-color:var(--color-secondary);transform:translateY(-1px)}.rm-export__template--custom{background:transparent;border-style:dashed}.rm-export__template-body{display:flex;flex-direction:column;min-width:0}.rm-export__template-name{color:var(--text-primary);font-size:13.5px;font-weight:700;line-height:1.2}.rm-export__template-meta{color:var(--text-muted);font-size:11px;line-height:1.35;margin-top:3px}.rm-export__wizard{background:var(--bg-surface-2);border:1px solid var(--border-default);border-radius:var(--radius-2xl);padding:var(--space-16) var(--space-20)}.rm-export__wizard-head{align-items:center;border-bottom:1px solid var(--border-soft);display:flex;gap:var(--space-12);margin-bottom:var(--space-16);padding-bottom:var(--space-12)}.rm-export__back{align-items:center;background:none;border:none;color:var(--color-secondary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:12px;font-weight:600;gap:var(--space-4);padding:0}.rm-export__back:hover{text-decoration:underline;text-underline-offset:3px}.rm-export__stepper{align-items:center;display:flex;gap:var(--space-4);margin-bottom:var(--space-20)}.rm-export__step{background:none;border:none;border-radius:var(--radius-pill);cursor:pointer;font-family:inherit;gap:var(--space-6);padding:var(--space-4) var(--space-8)}.rm-export__step,.rm-export__step-num{align-items:center;display:inline-flex}.rm-export__step-num{background:var(--bg-surface-3);border:1.5px solid var(--border-default);border-radius:50%;color:var(--text-muted);font-size:12px;font-weight:700;height:22px;justify-content:center;transition:background var(--duration-fast),color var(--duration-fast),border-color var(--duration-fast);width:22px}.rm-export__step-label{color:var(--text-muted);font-size:12px;font-weight:600}.rm-export__step:hover .rm-export__step-label{color:var(--text-primary)}.rm-export__step--active .rm-export__step-num{background:var(--color-primary);border-color:var(--color-primary);box-shadow:var(--shadow-focus);color:#fff}.rm-export__step--active .rm-export__step-label{color:var(--color-secondary)}.rm-export__step--done .rm-export__step-num{background:var(--color-secondary);border-color:var(--color-secondary);color:#fff}.rm-export__step--done .rm-export__step-label{color:var(--text-secondary)}.rm-export__step-divider{background:var(--border-default);border-radius:var(--radius-pill);flex:1;height:1.5px}.rm-export__panel{animation:rm-export-in var(--duration-base) var(--ease-out)}@keyframes rm-export-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.rm-export__formats{display:grid;gap:var(--space-8)}.rm-export__format{align-items:flex-start;background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:var(--radius-xl);cursor:pointer;display:flex;gap:var(--space-12);padding:var(--space-12) var(--space-16);transition:border-color var(--duration-fast),background var(--duration-fast),transform var(--duration-fast)}.rm-export__format:hover{border-color:var(--color-secondary);transform:translateY(-1px)}.rm-export__format--active{background:var(--bg-hover);border-color:var(--color-primary);box-shadow:var(--shadow-focus)}.rm-export__format--active .rm-export__format-name{color:var(--color-primary)}.rm-export__format--off{cursor:not-allowed;opacity:.55}.rm-export__format--off:hover{border-color:var(--border-default);transform:none}.rm-export__format input[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:50%;cursor:inherit;flex-shrink:0;height:18px;margin:1px 0 0;position:relative;width:18px}.rm-export__format input[type=radio]:checked{border-color:var(--color-primary)}.rm-export__format input[type=radio]:checked:after{background:var(--color-primary);border-radius:50%;content:"";inset:3px;position:absolute}.rm-export__format-text{display:flex;flex-direction:column;min-width:0}.rm-export__format-name{color:var(--text-primary);font-size:14px;font-weight:700;line-height:1.2}.rm-export__format-desc{color:var(--text-muted);font-size:11.5px;line-height:1.45;margin-top:3px}.rm-export__format-meta{border-top:1px solid var(--border-soft);color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;margin-top:var(--space-8);padding-top:var(--space-8);text-transform:uppercase}.rm-export__row{margin-bottom:var(--space-20)}.rm-export__row:last-child{margin-bottom:0}.rm-export__label{color:var(--text-secondary);display:block;font-size:11px;font-weight:700;letter-spacing:.06em;margin-bottom:var(--space-8);text-transform:uppercase}.rm-export__sub{margin-top:var(--space-12)}.rm-export__input{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);color:var(--text-primary);font-family:inherit;font-size:13px;padding:var(--space-8) var(--space-12);width:100%}.rm-export__input:focus{border-color:var(--color-primary);box-shadow:var(--shadow-focus);outline:none}.rm-export__checks{display:grid;gap:var(--space-6)}.rm-export__check{align-items:center;color:var(--text-primary);cursor:pointer;display:flex;font-size:12.5px;gap:var(--space-8)}.rm-export__check--off{cursor:not-allowed;opacity:.5}.rm-export__check input[type=checkbox]{accent-color:var(--color-primary);cursor:inherit;height:15px;width:15px}.rm-export__chips{display:flex;flex-wrap:wrap;gap:var(--space-6)}.rm-export__chip{align-items:center;border:1px solid var(--border-default);border-radius:var(--radius-pill);cursor:pointer;display:inline-flex;font-family:inherit;font-size:11.5px;font-weight:600;gap:var(--space-4);padding:3px var(--space-8)}.rm-export__chip--on{background:var(--bg-hover);border-color:var(--color-primary);color:var(--color-primary)}.rm-export__chip--off{background:var(--bg-surface);color:var(--text-muted)}.rm-export__chip--off:hover{border-color:var(--color-secondary);color:var(--color-secondary)}.rm-export__recap{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl);margin-top:var(--space-16);padding:var(--space-16)}.rm-export__recap-row{align-items:flex-start;border-bottom:1px solid var(--border-soft);color:var(--color-secondary);display:flex;gap:var(--space-12);padding:var(--space-8) 0}.rm-export__recap-row:last-child{border-bottom:none}.rm-export__recap-row>div{flex:1;min-width:0}.rm-export__recap-k{color:var(--text-muted);font-size:10.5px;font-weight:700;letter-spacing:.08em;margin-bottom:2px;text-transform:uppercase}.rm-export__recap-v{color:var(--text-primary);font-size:13px;font-weight:600;word-break:break-word}.rm-export__footer{align-items:center;border-top:1px solid var(--border-default);display:flex;gap:var(--space-8);margin-top:var(--space-20);padding-top:var(--space-16)}.rm-export__progress{color:var(--text-muted);flex:1;font-size:12px;text-align:center}.rm-export__progress b{color:var(--text-primary)}.rm-export__history{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl);margin-top:var(--space-24);padding:var(--space-16)}.rm-export__history-row{align-items:center;color:var(--color-primary);display:flex;gap:var(--space-8);padding:var(--space-6) 0}.rm-export__history-name{color:var(--text-primary);flex:1;font-size:12.5px;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-export__history-meta{color:var(--text-muted);flex-shrink:0;font-size:11px}
|
|
744
|
+
</style>
|