@tekkare/romulus 0.1.231 → 0.1.233
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/CopyCode.vue +79 -0
- package/dist/runtime/components/ProjectAddButton.vue +226 -0
- package/dist/runtime/components/ProjectBanner.vue +134 -0
- package/dist/runtime/components/ProjectComments.vue +110 -0
- package/dist/runtime/components/ProjectCreateDialog.vue +117 -0
- package/dist/runtime/components/ProjectDeleteDialog.vue +60 -0
- package/dist/runtime/components/ProjectList.vue +115 -0
- package/dist/runtime/components/ProjectOverview.vue +294 -0
- package/dist/runtime/components/ProjectPanel.vue +193 -0
- package/dist/runtime/components/ProjectRenameDialog.vue +67 -0
- package/dist/runtime/components/ProjectResources.vue +258 -0
- package/dist/runtime/components/ResultCard.vue +4 -1
- package/dist/runtime/components/SourceCard.vue +2 -30
- package/dist/runtime/utils/clipboard.d.ts +8 -0
- package/dist/runtime/utils/clipboard.js +21 -0
- package/dist/runtime/utils/projects.d.ts +200 -0
- package/dist/runtime/utils/projects.js +123 -0
- package/package.json +1 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmProjectPanel - le panneau « Projets » du rail.
|
|
3
|
+
|
|
4
|
+
Il repond a une question : ou partent mes ajouts ? Le projet actif s'y dit
|
|
5
|
+
en tete, avec son compte. Chaque carte porte les deux gestes separement,
|
|
6
|
+
ecrits en toutes lettres : y envoyer ses ajouts, ou ouvrir le projet. Les
|
|
7
|
+
confondre obligeait a rendre un projet actif pour aller le lire.
|
|
8
|
+
|
|
9
|
+
On peut y creer un projet d'un nom : il devient l'actif sans quitter la
|
|
10
|
+
page (l'app le fait a la reception de `create`). Le bouton de creation peut
|
|
11
|
+
etre remplace par celui de l'app (slot `new`), qui proposerait une creation
|
|
12
|
+
assistee. Renommer et supprimer restent sur la page du projet.
|
|
13
|
+
|
|
14
|
+
La recherche n'apparait qu'a partir de cinq projets : en dessous, la liste
|
|
15
|
+
se lit d'un coup d'oeil et le champ coute une rangee pour rien.
|
|
16
|
+
-->
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import { computed, ref } from 'vue'
|
|
19
|
+
import { rmProjectCount, rmProjectGroups, rmProjectSummary, type RmProject } from '../utils/projects'
|
|
20
|
+
|
|
21
|
+
const props = withDefaults(
|
|
22
|
+
defineProps<{
|
|
23
|
+
projects: RmProject[]
|
|
24
|
+
activeId?: string | null
|
|
25
|
+
/** Elements du projet actif. */
|
|
26
|
+
activeCount?: number
|
|
27
|
+
loading?: boolean
|
|
28
|
+
failed?: boolean
|
|
29
|
+
/** Creation en cours. */
|
|
30
|
+
creating?: boolean
|
|
31
|
+
/** Lien « Tous mes projets ». Vide : pas de lien. */
|
|
32
|
+
allTo?: string
|
|
33
|
+
lang?: 'fr' | 'en'
|
|
34
|
+
}>(),
|
|
35
|
+
{ activeId: null, activeCount: 0, loading: false, failed: false, creating: false, allTo: '', lang: 'fr' },
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const emit = defineEmits<{
|
|
39
|
+
activate: [projectId: string]
|
|
40
|
+
open: [projectId: string]
|
|
41
|
+
pin: [projectId: string]
|
|
42
|
+
create: [title: string]
|
|
43
|
+
}>()
|
|
44
|
+
|
|
45
|
+
const WORDS = {
|
|
46
|
+
fr: {
|
|
47
|
+
goTo: 'Mes ajouts vont dans', name: 'Nom du projet', placeholder: 'Veille oncologie', required: 'Un nom est requis',
|
|
48
|
+
create: 'Créer', cancel: 'Annuler', loading: 'Chargement...', failed: 'Les projets ne sont pas accessibles pour le moment.',
|
|
49
|
+
search: 'Rechercher un projet', new: 'Nouveau projet', created: 'Créé le', active: 'Projet actif',
|
|
50
|
+
activate: 'Activer ce projet', open: 'Ouvrir', noMatch: 'Aucun projet ne correspond.', none: 'Aucun projet pour le moment.',
|
|
51
|
+
all: 'Tous mes projets', pin: 'Épingler en haut', unpin: 'Désépingler',
|
|
52
|
+
},
|
|
53
|
+
en: {
|
|
54
|
+
goTo: 'Additions go to', name: 'Project name', placeholder: 'Oncology watch', required: 'A name is required',
|
|
55
|
+
create: 'Create', cancel: 'Cancel', loading: 'Loading...', failed: 'Projects are unavailable right now.',
|
|
56
|
+
search: 'Search a project', new: 'New project', created: 'Created the', active: 'Active project',
|
|
57
|
+
activate: 'Activate this project', open: 'Open', noMatch: 'No project matches.', none: 'No project yet.',
|
|
58
|
+
all: 'All my projects', pin: 'Pin to the top', unpin: 'Unpin',
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
const T = computed(() => WORDS[props.lang] ?? WORDS.fr)
|
|
62
|
+
|
|
63
|
+
const active = computed(() => props.projects.find(p => p.id === props.activeId) ?? null)
|
|
64
|
+
|
|
65
|
+
const search = ref('')
|
|
66
|
+
const shown = computed(() => {
|
|
67
|
+
const needle = search.value.trim().toLowerCase()
|
|
68
|
+
return needle ? props.projects.filter(p => p.title.toLowerCase().includes(needle)) : props.projects
|
|
69
|
+
})
|
|
70
|
+
const groups = computed(() => rmProjectGroups(shown.value, props.lang))
|
|
71
|
+
|
|
72
|
+
const formOpen = ref(false)
|
|
73
|
+
const draft = ref('')
|
|
74
|
+
const missing = ref(false)
|
|
75
|
+
|
|
76
|
+
function openForm() {
|
|
77
|
+
formOpen.value = true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function cancel() {
|
|
81
|
+
formOpen.value = false
|
|
82
|
+
draft.value = ''
|
|
83
|
+
missing.value = false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function submit() {
|
|
87
|
+
const title = draft.value.trim()
|
|
88
|
+
if (!title) {
|
|
89
|
+
missing.value = true
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
emit('create', title)
|
|
93
|
+
cancel()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
defineExpose({ openForm })
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<template>
|
|
100
|
+
<div class="rm-project-panel">
|
|
101
|
+
<div v-if="active" class="rm-project-panel__head">
|
|
102
|
+
<p class="rm-project-panel__label">{{ T.goTo }}</p>
|
|
103
|
+
<p class="rm-project-panel__active">{{ active.title }}</p>
|
|
104
|
+
<p class="rm-project-panel__muted">{{ rmProjectCount(activeCount, lang) }}</p>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<form v-if="formOpen" class="rm-project-panel__create" @submit.prevent="submit">
|
|
108
|
+
<RmInput v-model="draft" :label="T.name" :placeholder="T.placeholder" :error="missing ? T.required : ''" />
|
|
109
|
+
<div class="rm-project-panel__actions">
|
|
110
|
+
<RmButton type="submit" variant="primary" size="sm" :loading="creating">{{ T.create }}</RmButton>
|
|
111
|
+
<RmButton variant="tertiary" size="sm" @click="cancel">{{ T.cancel }}</RmButton>
|
|
112
|
+
</div>
|
|
113
|
+
</form>
|
|
114
|
+
|
|
115
|
+
<div class="rm-project-panel__list">
|
|
116
|
+
<p v-if="loading && !projects.length" class="rm-project-panel__muted">{{ T.loading }}</p>
|
|
117
|
+
<p v-else-if="failed" class="rm-project-panel__muted">{{ T.failed }}</p>
|
|
118
|
+
<template v-else>
|
|
119
|
+
<div class="rm-project-panel__tools">
|
|
120
|
+
<RmSearchBar
|
|
121
|
+
v-if="projects.length > 4"
|
|
122
|
+
v-model="search"
|
|
123
|
+
class="rm-project-panel__search"
|
|
124
|
+
:have-advanced="false"
|
|
125
|
+
:placeholder="T.search"
|
|
126
|
+
/>
|
|
127
|
+
<span v-if="!formOpen" class="rm-project-panel__new">
|
|
128
|
+
<slot name="new" :open-form="openForm">
|
|
129
|
+
<RmButton variant="secondary" size="sm" prefix-icon="Plus" @click="openForm">{{ T.new }}</RmButton>
|
|
130
|
+
</slot>
|
|
131
|
+
</span>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<template v-for="group in groups" :key="group.key">
|
|
135
|
+
<p class="rm-project-panel__group">
|
|
136
|
+
<RmIcon v-if="group.key === 'pinned'" name="PushPin" weight="fill" :size="12" />
|
|
137
|
+
{{ group.label }}
|
|
138
|
+
</p>
|
|
139
|
+
<article
|
|
140
|
+
v-for="project in group.projects"
|
|
141
|
+
:key="project.id"
|
|
142
|
+
class="rm-project-panel__card"
|
|
143
|
+
:class="{ 'is-active': project.id === activeId }"
|
|
144
|
+
>
|
|
145
|
+
<div class="rm-project-panel__card-head">
|
|
146
|
+
<p class="rm-project-panel__card-title">{{ project.title }}</p>
|
|
147
|
+
<RmButton
|
|
148
|
+
variant="tertiary"
|
|
149
|
+
size="sm"
|
|
150
|
+
icon-only
|
|
151
|
+
ghost
|
|
152
|
+
class="rm-project-panel__pin"
|
|
153
|
+
:class="{ 'is-on': project.pinned }"
|
|
154
|
+
:aria-pressed="!!project.pinned"
|
|
155
|
+
:aria-label="project.pinned ? T.unpin : T.pin"
|
|
156
|
+
:title="project.pinned ? T.unpin : T.pin"
|
|
157
|
+
@click="emit('pin', project.id)"
|
|
158
|
+
>
|
|
159
|
+
<RmIcon name="PushPin" :weight="project.pinned ? 'fill' : 'regular'" :size="14" />
|
|
160
|
+
</RmButton>
|
|
161
|
+
</div>
|
|
162
|
+
<p class="rm-project-panel__meta">{{ rmProjectSummary(project, lang) }}</p>
|
|
163
|
+
<p v-if="project.createdAt" class="rm-project-panel__meta">
|
|
164
|
+
{{ T.created }} <RmDate :date="project.createdAt" :lang="lang" />
|
|
165
|
+
</p>
|
|
166
|
+
<div class="rm-project-panel__actions">
|
|
167
|
+
<RmButton
|
|
168
|
+
:variant="project.id === activeId ? 'primary' : 'tertiary'"
|
|
169
|
+
size="sm"
|
|
170
|
+
:disabled="project.id === activeId"
|
|
171
|
+
@click="emit('activate', project.id)"
|
|
172
|
+
>
|
|
173
|
+
{{ project.id === activeId ? T.active : T.activate }}
|
|
174
|
+
</RmButton>
|
|
175
|
+
<RmButton variant="tertiary" size="sm" @click="emit('open', project.id)">{{ T.open }}</RmButton>
|
|
176
|
+
</div>
|
|
177
|
+
</article>
|
|
178
|
+
</template>
|
|
179
|
+
|
|
180
|
+
<p v-if="search && !shown.length" class="rm-project-panel__muted">{{ T.noMatch }}</p>
|
|
181
|
+
<p v-if="!projects.length" class="rm-project-panel__muted">{{ T.none }}</p>
|
|
182
|
+
</template>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div v-if="allTo" class="rm-project-panel__footer">
|
|
186
|
+
<NuxtLink :to="allTo" class="rm-project-panel__link">{{ T.all }}</NuxtLink>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</template>
|
|
190
|
+
|
|
191
|
+
<style scoped>
|
|
192
|
+
.rm-project-panel{display:flex;flex-direction:column;gap:16px;width:100%}.rm-project-panel__head{background:color-mix(in srgb,var(--rm-shell-accent) 12%,var(--rm-shell-surface));border:1px solid color-mix(in srgb,var(--rm-shell-accent) 35%,transparent);border-left:4px solid var(--rm-shell-accent);border-radius:var(--radius-lg);display:flex;flex-direction:column;gap:2px;padding:var(--space-12)}.rm-project-panel__label{color:var(--rm-shell-text-dark);font-size:var(--font-12);font-weight:var(--weight-semibold);letter-spacing:.02em;margin:0;text-transform:uppercase}.rm-project-panel__active{color:var(--rm-shell-accent);font-size:var(--font-16);font-weight:var(--weight-bold);margin:0}.rm-project-panel__muted{color:var(--rm-shell-text-dark);font-size:var(--font-12);margin:0}.rm-project-panel__create{background:var(--rm-shell-surface);border:1px solid var(--rm-shell-border);border-radius:10px;padding:12px}.rm-project-panel__create,.rm-project-panel__list{display:flex;flex-direction:column;gap:8px}.rm-project-panel__tools{align-items:center;display:flex;gap:8px}.rm-project-panel__search{flex:1;min-width:0}.rm-project-panel__search :deep(.search-bar){box-sizing:border-box;display:flex;min-width:0;width:100%}.rm-project-panel__new{display:inline-flex;flex:0 0 auto;margin-left:auto}.rm-project-panel__group{align-items:center;color:var(--rm-shell-text-muted);display:flex;font-size:11px;font-weight:700;gap:6px;letter-spacing:.06em;margin:4px 0 0;text-transform:uppercase}.rm-project-panel__card{background:var(--rm-shell-surface);border:1px solid var(--rm-shell-border);border-radius:10px;display:flex;flex-direction:column;gap:6px;padding:12px}.rm-project-panel__card.is-active{background:color-mix(in srgb,var(--rm-shell-accent) 8%,var(--rm-shell-surface));border-color:color-mix(in srgb,var(--rm-shell-accent) 45%,transparent)}.rm-project-panel__card-head{align-items:center;display:flex;gap:8px}.rm-project-panel__card-title{color:var(--rm-shell-text-dark);flex:1;font-size:14px;font-weight:600;margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-project-panel__pin{margin-right:-6px}.rm-project-panel__pin.is-on{color:var(--color-primary)}.rm-project-panel__meta{color:var(--rm-shell-text-muted);display:flex;font-size:12px;gap:4px;margin:0}.rm-project-panel__actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:2px}.rm-project-panel__footer{border-top:1px solid var(--rm-shell-border);padding-top:12px}.rm-project-panel__link{color:var(--rm-shell-accent);font-size:12px;font-weight:600;text-decoration:none}.rm-project-panel__link:hover{text-decoration:underline}
|
|
193
|
+
</style>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmProjectRenameDialog - renommer un projet.
|
|
3
|
+
|
|
4
|
+
Un champ dans une modale plutot qu'un titre editable sur place : pour que
|
|
5
|
+
l'enregistrement et l'echec aient ou se dire. Le champ repart du titre
|
|
6
|
+
courant a chaque ouverture ; Entree enregistre.
|
|
7
|
+
-->
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import { computed, ref, watch } from 'vue'
|
|
10
|
+
|
|
11
|
+
const props = withDefaults(
|
|
12
|
+
defineProps<{
|
|
13
|
+
modelValue: boolean
|
|
14
|
+
/** Titre actuel du projet. */
|
|
15
|
+
title: string
|
|
16
|
+
loading?: boolean
|
|
17
|
+
error?: string
|
|
18
|
+
lang?: 'fr' | 'en'
|
|
19
|
+
}>(),
|
|
20
|
+
{ loading: false, error: '', lang: 'fr' },
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const emit = defineEmits<{
|
|
24
|
+
'update:modelValue': [value: boolean]
|
|
25
|
+
'save': [title: string]
|
|
26
|
+
}>()
|
|
27
|
+
|
|
28
|
+
const WORDS = {
|
|
29
|
+
fr: { heading: 'Renommer le projet', name: 'Nom du projet', cancel: 'Annuler', save: 'Enregistrer' },
|
|
30
|
+
en: { heading: 'Rename the project', name: 'Project name', cancel: 'Cancel', save: 'Save' },
|
|
31
|
+
}
|
|
32
|
+
const T = computed(() => WORDS[props.lang] ?? WORDS.fr)
|
|
33
|
+
|
|
34
|
+
const draft = ref('')
|
|
35
|
+
watch(() => props.modelValue, (open) => {
|
|
36
|
+
if (open) draft.value = props.title
|
|
37
|
+
}, { immediate: true })
|
|
38
|
+
|
|
39
|
+
const open = computed({
|
|
40
|
+
get: () => props.modelValue,
|
|
41
|
+
set: (value: boolean) => emit('update:modelValue', value),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
function save() {
|
|
45
|
+
const title = draft.value.trim()
|
|
46
|
+
if (!title || props.loading) return
|
|
47
|
+
emit('save', title)
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<RmModal v-model="open" size="sm" :label="T.heading">
|
|
53
|
+
<template #header>
|
|
54
|
+
<h2 class="rm-project-dialog__title">{{ T.heading }}</h2>
|
|
55
|
+
</template>
|
|
56
|
+
<RmInput v-model="draft" :label="T.name" @keyup.enter="save" />
|
|
57
|
+
<p v-if="error" class="rm-project-dialog__error">{{ error }}</p>
|
|
58
|
+
<template #footer>
|
|
59
|
+
<RmButton variant="tertiary" @click="open = false">{{ T.cancel }}</RmButton>
|
|
60
|
+
<RmButton variant="primary" :loading="loading" :disabled="!draft.trim()" @click="save">{{ T.save }}</RmButton>
|
|
61
|
+
</template>
|
|
62
|
+
</RmModal>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<style scoped>
|
|
66
|
+
.rm-project-dialog__title{font-size:var(--font-18);font-weight:var(--weight-bold);margin:0}.rm-project-dialog__error{color:var(--color-danger);font-size:var(--font-13);margin:8px 0 0}
|
|
67
|
+
</style>
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmProjectResources - l'onglet « Ressources » d'un projet.
|
|
3
|
+
|
|
4
|
+
Une ligne par element range, toutes familles confondues : icone de la
|
|
5
|
+
famille, titre (un vrai lien : Ctrl/Cmd-clic ouvre la fiche dans un onglet,
|
|
6
|
+
le clic simple rend `open` pour un apercu sur place), sous-titre, d'ou vient
|
|
7
|
+
l'element et quand il est entre, puis Commenter et Retirer.
|
|
8
|
+
|
|
9
|
+
Recherche, puces de famille, filtre « commentees » et tri se font en
|
|
10
|
+
memoire (cf. rmProjectFilterRows) : un projet tient quelques centaines
|
|
11
|
+
d'elements, deja relus. L'app donne les lignes (cf. rmProjectRows), avec
|
|
12
|
+
`path` deja prefixe de la langue.
|
|
13
|
+
|
|
14
|
+
Un element dont le document a disparu de l'index reste liste sous son
|
|
15
|
+
identifiant : mieux vaut le montrer, et laisser le retirer, que le faire
|
|
16
|
+
disparaitre en silence.
|
|
17
|
+
-->
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
import { computed, ref } from 'vue'
|
|
20
|
+
import {
|
|
21
|
+
rmProjectCount,
|
|
22
|
+
rmProjectFamilies,
|
|
23
|
+
rmProjectFilterRows,
|
|
24
|
+
type RmProjectResource,
|
|
25
|
+
type RmProjectSort,
|
|
26
|
+
} from '../utils/projects'
|
|
27
|
+
|
|
28
|
+
const props = withDefaults(
|
|
29
|
+
defineProps<{
|
|
30
|
+
rows: RmProjectResource[]
|
|
31
|
+
loading?: boolean
|
|
32
|
+
failed?: boolean
|
|
33
|
+
/** `type-id` de la ligne en cours de retrait. */
|
|
34
|
+
removing?: string
|
|
35
|
+
/** Texte de l'etat vide : ou trouver le bouton Projet dans cette app. */
|
|
36
|
+
emptyText?: string
|
|
37
|
+
/** Id de l'element a la barre de filtres collante. Vide : pas de barre collante. */
|
|
38
|
+
stickyTo?: string | null
|
|
39
|
+
lang?: 'fr' | 'en'
|
|
40
|
+
}>(),
|
|
41
|
+
{ loading: false, failed: false, removing: '', emptyText: '', stickyTo: 'rm_page_header', lang: 'fr' },
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const emit = defineEmits<{
|
|
45
|
+
open: [row: RmProjectResource]
|
|
46
|
+
comment: [row: RmProjectResource]
|
|
47
|
+
remove: [row: RmProjectResource]
|
|
48
|
+
}>()
|
|
49
|
+
|
|
50
|
+
const WORDS = {
|
|
51
|
+
fr: {
|
|
52
|
+
search: 'Rechercher une ressource', sortBy: 'Trier par', comments: 'Annotations', withComments: 'Contient des commentaires',
|
|
53
|
+
loading: 'Chargement', failedTitle: 'Projet indisponible', failedText: 'Le service des projets n\'a pas répondu.',
|
|
54
|
+
emptyTitle: 'Ce projet est vide', emptyText: 'Parcourez les pages et utilisez le bouton Projet d\'une carte pour la ranger ici.',
|
|
55
|
+
families: 'Filtrer par famille', all: 'Toutes', resources: 'Ressources', noMatch: 'Aucune ressource ne correspond à ces filtres.',
|
|
56
|
+
comment: 'Commenter', commentsN: (n: number) => `Commentaires (${n})`, remove: 'Retirer du projet',
|
|
57
|
+
unknown: (id: string) => `Élément inconnu (id ${id})`, source: 'Source', added: 'Ajouté le',
|
|
58
|
+
filed: (n: number) => `${rmProjectCount(n, 'fr')} ${n > 1 ? 'rangées' : 'rangée'} dans le projet.`,
|
|
59
|
+
shown: (s: number, n: number) => `${s} ${s > 1 ? 'affichées' : 'affichée'} sur ${rmProjectCount(n, 'fr')}.`,
|
|
60
|
+
sorts: { 'date-desc': 'Ajout, plus récent d\'abord', 'date-asc': 'Ajout, plus ancien d\'abord', 'title': 'Titre', 'family': 'Famille' },
|
|
61
|
+
},
|
|
62
|
+
en: {
|
|
63
|
+
search: 'Search a resource', sortBy: 'Sort by', comments: 'Comments', withComments: 'With comments',
|
|
64
|
+
loading: 'Loading', failedTitle: 'Project unavailable', failedText: 'The project service did not answer.',
|
|
65
|
+
emptyTitle: 'Nothing in this project yet', emptyText: 'Browse the pages and use the Project button on a card to file it here.',
|
|
66
|
+
families: 'Filter by family', all: 'All', resources: 'Resources', noMatch: 'No resource matches these filters.',
|
|
67
|
+
comment: 'Comment', commentsN: (n: number) => `Comments (${n})`, remove: 'Remove from the project',
|
|
68
|
+
unknown: (id: string) => `Unknown item (id ${id})`, source: 'Source', added: 'Added on',
|
|
69
|
+
filed: (n: number) => `${rmProjectCount(n, 'en')} filed in the project.`,
|
|
70
|
+
shown: (s: number, n: number) => `${s} shown out of ${rmProjectCount(n, 'en')}.`,
|
|
71
|
+
sorts: { 'date-desc': 'Newest first', 'date-asc': 'Oldest first', 'title': 'Title', 'family': 'Family' },
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
const T = computed(() => WORDS[props.lang] ?? WORDS.fr)
|
|
75
|
+
|
|
76
|
+
const search = ref('')
|
|
77
|
+
const family = ref('all')
|
|
78
|
+
const onlyCommented = ref(false)
|
|
79
|
+
const sort = ref<RmProjectSort>('date-desc')
|
|
80
|
+
|
|
81
|
+
/** RmDropdownSelect ne porte que des libelles : on traduit dans les deux sens. */
|
|
82
|
+
const sortLabels = computed(() => Object.values(T.value.sorts))
|
|
83
|
+
const sortLabel = computed({
|
|
84
|
+
get: () => T.value.sorts[sort.value],
|
|
85
|
+
set: (label: string) => {
|
|
86
|
+
const found = (Object.keys(T.value.sorts) as RmProjectSort[]).find(key => T.value.sorts[key] === label)
|
|
87
|
+
if (found) sort.value = found
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
function clearFilters() {
|
|
92
|
+
search.value = ''
|
|
93
|
+
family.value = 'all'
|
|
94
|
+
onlyCommented.value = false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const families = computed(() => rmProjectFamilies(props.rows))
|
|
98
|
+
const shown = computed(() => rmProjectFilterRows(props.rows, {
|
|
99
|
+
search: search.value,
|
|
100
|
+
family: family.value,
|
|
101
|
+
onlyCommented: onlyCommented.value,
|
|
102
|
+
sort: sort.value,
|
|
103
|
+
}, props.lang))
|
|
104
|
+
|
|
105
|
+
const context = computed(() => {
|
|
106
|
+
const total = props.rows.length
|
|
107
|
+
return shown.value.length === total ? T.value.filed(total) : T.value.shown(shown.value.length, total)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
function formatDate(value?: string) {
|
|
111
|
+
const date = new Date(value ?? '')
|
|
112
|
+
if (Number.isNaN(date.getTime())) return ''
|
|
113
|
+
return date.toLocaleDateString(props.lang === 'en' ? 'en-GB' : 'fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function meta(row: RmProjectResource) {
|
|
117
|
+
return [
|
|
118
|
+
row.section ? `${T.value.source} : ${row.section}` : '',
|
|
119
|
+
row.createdAt ? `${T.value.added} ${formatDate(row.createdAt)}` : '',
|
|
120
|
+
].filter(Boolean).join(' · ')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function commentLabel(row: RmProjectResource) {
|
|
124
|
+
return row.comments ? T.value.commentsN(row.comments) : T.value.comment
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Le clic simple ouvre l'apercu ; les autres gardent le comportement du lien. */
|
|
128
|
+
function onTitle(event: MouseEvent, row: RmProjectResource) {
|
|
129
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) return
|
|
130
|
+
event.preventDefault()
|
|
131
|
+
emit('open', row)
|
|
132
|
+
}
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
<template>
|
|
136
|
+
<div class="rm-project-res">
|
|
137
|
+
<RmFilterBar
|
|
138
|
+
:filters-amount="onlyCommented ? 1 : 0"
|
|
139
|
+
:lang="lang"
|
|
140
|
+
:sticky-to="stickyTo"
|
|
141
|
+
@clear-filters="clearFilters"
|
|
142
|
+
>
|
|
143
|
+
<template #Search>
|
|
144
|
+
<div class="rm-project-res__search">
|
|
145
|
+
<RmSearchBar v-model="search" :have-advanced="false" :placeholder="T.search" />
|
|
146
|
+
</div>
|
|
147
|
+
</template>
|
|
148
|
+
<template #Filters>
|
|
149
|
+
<RmDropdownSelect
|
|
150
|
+
v-model="sortLabel"
|
|
151
|
+
:label="T.sortBy"
|
|
152
|
+
:options="sortLabels"
|
|
153
|
+
:search="false"
|
|
154
|
+
sort-icon
|
|
155
|
+
close-on-click
|
|
156
|
+
:lang="lang"
|
|
157
|
+
/>
|
|
158
|
+
<RmCheckbox v-model="onlyCommented" :field-label="T.comments" :label="T.withComments" />
|
|
159
|
+
</template>
|
|
160
|
+
</RmFilterBar>
|
|
161
|
+
|
|
162
|
+
<RmLoadingState v-if="loading && !rows.length" spinner :label="T.loading" />
|
|
163
|
+
|
|
164
|
+
<RmErrorState v-else-if="failed" icon="CloudWarning" :title="T.failedTitle" :text="T.failedText" action-label="" />
|
|
165
|
+
|
|
166
|
+
<RmEmptyState v-else-if="!rows.length" icon="FolderSimplePlus" :title="T.emptyTitle" :text="emptyText || T.emptyText" />
|
|
167
|
+
|
|
168
|
+
<template v-else>
|
|
169
|
+
<div class="rm-project-res__chips" role="group" :aria-label="T.families">
|
|
170
|
+
<RmPill :label="T.all" :active="family === 'all'" @click="family = 'all'">
|
|
171
|
+
<span class="rm-project-res__chip-count">{{ rows.length }}</span>
|
|
172
|
+
</RmPill>
|
|
173
|
+
<RmPill
|
|
174
|
+
v-for="chip in families"
|
|
175
|
+
:key="chip.type"
|
|
176
|
+
:label="chip.label"
|
|
177
|
+
:active="family === chip.type"
|
|
178
|
+
@click="family = chip.type"
|
|
179
|
+
>
|
|
180
|
+
<span class="rm-project-res__chip-count">{{ chip.count }}</span>
|
|
181
|
+
</RmPill>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<RmCard padding="lg">
|
|
185
|
+
<div class="rm-project-res__head">
|
|
186
|
+
<h2 class="rm-project-res__title">{{ T.resources }}</h2>
|
|
187
|
+
<p class="rm-project-res__context">{{ context }}</p>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<ol v-if="shown.length" class="rm-project-res__list">
|
|
191
|
+
<li v-for="row in shown" :key="`${row.type}-${row.id}`" class="rm-project-res__item">
|
|
192
|
+
<span class="rm-project-res__icon" aria-hidden="true">
|
|
193
|
+
<RmIcon :name="row.icon" :size="17" />
|
|
194
|
+
</span>
|
|
195
|
+
|
|
196
|
+
<div class="rm-project-res__text">
|
|
197
|
+
<p class="rm-project-res__item-title">
|
|
198
|
+
<a v-if="row.path" :href="row.path" @click="onTitle($event, row)">{{ row.title || T.unknown(row.id) }}</a>
|
|
199
|
+
<span v-else>{{ row.title || T.unknown(row.id) }}</span>
|
|
200
|
+
<button
|
|
201
|
+
v-if="row.comments"
|
|
202
|
+
type="button"
|
|
203
|
+
class="rm-project-res__comments"
|
|
204
|
+
:aria-label="commentLabel(row)"
|
|
205
|
+
@click="emit('comment', row)"
|
|
206
|
+
>
|
|
207
|
+
<RmIcon name="ChatCircle" :size="12" />
|
|
208
|
+
{{ row.comments }}
|
|
209
|
+
</button>
|
|
210
|
+
</p>
|
|
211
|
+
<p v-if="row.subtitle" class="rm-project-res__sub">{{ row.subtitle }}</p>
|
|
212
|
+
<p v-if="meta(row)" class="rm-project-res__meta">{{ meta(row) }}</p>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<RmBadge variant="info" class="rm-project-res__family">{{ row.family }}</RmBadge>
|
|
216
|
+
|
|
217
|
+
<!-- Des icones seules : la bulle dit le geste avant le clic. Elle
|
|
218
|
+
sort a gauche, la carte couperait une bulle centree. -->
|
|
219
|
+
<div class="rm-project-res__actions">
|
|
220
|
+
<RmTooltip :text="commentLabel(row)" dir="left">
|
|
221
|
+
<RmButton
|
|
222
|
+
variant="tertiary"
|
|
223
|
+
size="sm"
|
|
224
|
+
icon-only
|
|
225
|
+
ghost
|
|
226
|
+
prefix-icon="ChatCircle"
|
|
227
|
+
:aria-label="commentLabel(row)"
|
|
228
|
+
@click="emit('comment', row)"
|
|
229
|
+
/>
|
|
230
|
+
</RmTooltip>
|
|
231
|
+
<RmTooltip :text="T.remove" dir="left">
|
|
232
|
+
<RmButton
|
|
233
|
+
variant="tertiary"
|
|
234
|
+
size="sm"
|
|
235
|
+
icon-only
|
|
236
|
+
ghost
|
|
237
|
+
prefix-icon="Trash"
|
|
238
|
+
:loading="removing === `${row.type}-${row.id}`"
|
|
239
|
+
:aria-label="T.remove"
|
|
240
|
+
@click="emit('remove', row)"
|
|
241
|
+
/>
|
|
242
|
+
</RmTooltip>
|
|
243
|
+
</div>
|
|
244
|
+
</li>
|
|
245
|
+
</ol>
|
|
246
|
+
|
|
247
|
+
<p v-else class="rm-project-res__none">
|
|
248
|
+
<RmIcon name="FunnelX" :size="16" />
|
|
249
|
+
{{ T.noMatch }}
|
|
250
|
+
</p>
|
|
251
|
+
</RmCard>
|
|
252
|
+
</template>
|
|
253
|
+
</div>
|
|
254
|
+
</template>
|
|
255
|
+
|
|
256
|
+
<style scoped>
|
|
257
|
+
.rm-project-res{display:flex;flex-direction:column;gap:16px}.rm-project-res__search{flex:0 1 360px;min-width:160px}.rm-project-res__chips{display:flex;flex-wrap:wrap;gap:6px}.rm-project-res__chip-count{background:var(--bg-surface-3);border-radius:999px;color:var(--text-secondary);font-size:10px;font-weight:700;padding:1px 6px}.toolbar-pill--active .rm-project-res__chip-count{background:hsla(0,0%,100%,.25);color:var(--color-text-inverse)}.rm-project-res__head{display:flex;flex-direction:column;gap:4px;margin-bottom:8px}.rm-project-res__title{color:var(--text-primary);font-size:15px;font-weight:var(--weight-bold);margin:0}.rm-project-res__context{color:var(--text-muted);font-size:12px;margin:0}.rm-project-res__list{display:flex;flex-direction:column;list-style:none;margin:0;padding:0}.rm-project-res__item{align-items:center;border-bottom:1px solid var(--border-soft,var(--border-default));display:flex;gap:12px;padding:10px 0}.rm-project-res__item:last-child{border-bottom:none}.rm-project-res__icon{align-items:center;background:var(--bg-surface-3);border-radius:var(--radius-lg,8px);color:var(--text-secondary);display:flex;flex:none;height:34px;justify-content:center;width:34px}.rm-project-res__text{flex:1;min-width:0}.rm-project-res__item-title{align-items:center;color:var(--text-primary);display:flex;font-size:13px;font-weight:var(--weight-semibold);gap:6px;margin:0}.rm-project-res__item-title a{color:var(--color-primary);text-decoration:underline;text-underline-offset:2px}.rm-project-res__comments{align-items:center;background:var(--bg-surface-3);border:none;border-radius:999px;color:var(--text-secondary);cursor:pointer;display:inline-flex;font:inherit;font-size:11px;gap:3px;padding:1px 6px}.rm-project-res__sub{color:var(--text-secondary);font-size:12px;margin:2px 0 0}.rm-project-res__meta{color:var(--text-muted);font-size:11px;margin:2px 0 0}.rm-project-res__family{flex:none}.rm-project-res__actions{align-items:center;display:flex;flex:none;gap:2px}.rm-project-res__none{align-items:center;color:var(--text-secondary);display:flex;font-size:13px;gap:6px;margin:8px 0}
|
|
258
|
+
</style>
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
internes (export, liens).
|
|
15
15
|
|
|
16
16
|
Slots :
|
|
17
|
+
code — identifiant au-dessus du titre (RmCopyCode : RPPS, NCT...).
|
|
18
|
+
Hors de la ligne meta, ou il se perdait parmi les tags.
|
|
17
19
|
title — identite, une ligne en gras
|
|
18
20
|
subtitle — complement gris sous le titre
|
|
19
21
|
actions — a droite du titre (export...). Le clic doit y etre stoppe.
|
|
@@ -97,6 +99,7 @@ withDefaults(
|
|
|
97
99
|
<div class="rm-rcard" :class="{ 'rm-rcard--clickable': clickable, 'rm-rcard--dense': dense }">
|
|
98
100
|
<div class="rm-rcard__head">
|
|
99
101
|
<div class="rm-rcard__identity">
|
|
102
|
+
<div v-if="$slots.code" class="rm-rcard__code"><slot name="code" /></div>
|
|
100
103
|
<h3 class="rm-rcard__title"><slot name="title" /></h3>
|
|
101
104
|
<div v-if="$slots.subtitle" class="rm-rcard__subtitle"><slot name="subtitle" /></div>
|
|
102
105
|
</div>
|
|
@@ -135,5 +138,5 @@ withDefaults(
|
|
|
135
138
|
</template>
|
|
136
139
|
|
|
137
140
|
<style scoped>
|
|
138
|
-
.rm-rcard{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-2xl);color:var(--text-primary);display:block;font-family:var(--font-sans);overflow:hidden;transition:border-color var(--duration-fast) linear,box-shadow var(--duration-fast) linear;width:100%}.rm-rcard--clickable{cursor:pointer}.rm-rcard--clickable:hover{border-color:color-mix(in srgb,var(--color-primary) 35%,var(--border-default));box-shadow:var(--shadow-md)}.rm-rcard__head{align-items:flex-start;display:flex;gap:var(--space-16);padding:var(--space-20) var(--space-24) 0}.rm-rcard__identity{flex:1 1 auto;min-width:0}.rm-rcard__title{font-size:var(--font-18);font-weight:800;letter-spacing:.01em;margin:0}.rm-rcard__subtitle{color:var(--text-muted);font-size:var(--font-14);margin-top:4px;overflow-wrap:anywhere}.rm-rcard__actions{align-items:center;display:flex;flex:0 0 auto;gap:var(--space-8)}.rm-rcard__body{align-items:stretch;display:flex;gap:var(--space-24);padding:var(--space-16) var(--space-24) var(--space-20)}.rm-rcard__count{align-items:center;border-right:1px solid var(--border-default);display:flex;flex:0 0 128px;flex-direction:column;gap:var(--space-2);justify-content:center;padding-right:var(--space-24);text-align:center}.rm-rcard__count-value{color:var(--color-primary);font-size:var(--font-32);font-weight:var(--weight-bold);line-height:1.1;margin:0}.rm-rcard__count-label{color:var(--text-muted);font-size:var(--font-12);font-weight:var(--weight-semibold);letter-spacing:.08em;margin:0;text-transform:uppercase}.rm-rcard__count-extra{color:var(--color-primary);font-size:var(--font-13);line-height:1.3;margin:6px 0 0}.rm-rcard__count-extra+.rm-rcard__count-extra{margin-top:2px}.rm-rcard__main{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--space-6);min-width:0}.rm-rcard__meta{align-items:center;display:flex;flex-wrap:wrap;gap:var(--space-8)}.rm-rcard__more{color:var(--color-primary);font-size:var(--font-14);font-weight:var(--weight-semibold);margin:4px 0 0}.rm-rcard__footer{background:var(--bg-surface-2);border-top:1px solid var(--border-default);font-size:var(--font-13);padding:var(--space-12) var(--space-24)}.rm-rcard :deep(.rm-rcard__label){align-items:center;color:var(--text-secondary);display:inline-flex;font-size:var(--font-12);font-weight:var(--weight-bold);gap:var(--space-4);letter-spacing:.06em;text-transform:uppercase}.rm-rcard :deep(.rm-rcard__ref){background:var(--bg-surface-3);border-radius:var(--radius-sm);color:var(--text-secondary);font-family:Courier New,monospace;font-size:var(--font-12);padding:var(--space-2) var(--space-8)}.rm-rcard :deep(.rm-rcard__lead){font-size:15px;font-weight:var(--weight-bold);margin:0}.rm-rcard :deep(.rm-rcard__text){font-size:15px;margin:0}.rm-rcard :deep(.rm-rcard__desc){color:var(--text-secondary);font-size:var(--font-14);line-height:1.5;margin:0}.rm-rcard :deep(.rm-rcard__clamp){display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.rm-rcard--dense{border-radius:var(--radius-xl)}.rm-rcard--dense .rm-rcard__head{gap:var(--space-12);padding:var(--space-12) var(--space-16) 0}.rm-rcard--dense .rm-rcard__title{font-size:15px}.rm-rcard--dense .rm-rcard__subtitle{font-size:var(--font-13);margin-top:2px}.rm-rcard--dense .rm-rcard__body{gap:var(--space-16);padding:var(--space-8) var(--space-16) var(--space-12)}.rm-rcard--dense .rm-rcard__count{flex-basis:104px;padding-right:var(--space-16)}.rm-rcard--dense .rm-rcard__count-value{font-size:var(--font-24)}.rm-rcard--dense .rm-rcard__count-label{font-size:var(--font-11)}.rm-rcard--dense .rm-rcard__main{gap:var(--space-4)}.rm-rcard--dense .rm-rcard__count-extra,.rm-rcard--dense .rm-rcard__footer,.rm-rcard--dense .rm-rcard__main>:not(.rm-rcard__meta,.rm-rcard__lead){display:none}.rm-rcard--dense :deep(.rm-rcard__lead){display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}.rm-rcard--dense .rm-rcard__body{align-items:center}.rm-rcard--dense :deep(.rm-rcard__lead),.rm-rcard--dense :deep(.rm-rcard__text){font-size:var(--font-14)}.rm-rcard--dense :deep(.rm-rcard__desc){font-size:var(--font-13)}
|
|
141
|
+
.rm-rcard{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-2xl);color:var(--text-primary);display:block;font-family:var(--font-sans);overflow:hidden;transition:border-color var(--duration-fast) linear,box-shadow var(--duration-fast) linear;width:100%}.rm-rcard--clickable{cursor:pointer}.rm-rcard--clickable:hover{border-color:color-mix(in srgb,var(--color-primary) 35%,var(--border-default));box-shadow:var(--shadow-md)}.rm-rcard__head{align-items:flex-start;display:flex;gap:var(--space-16);padding:var(--space-20) var(--space-24) 0}.rm-rcard__identity{flex:1 1 auto;min-width:0}.rm-rcard__code{display:flex;flex-wrap:wrap;gap:var(--space-8);margin-bottom:var(--space-8)}.rm-rcard__title{font-size:var(--font-18);font-weight:800;letter-spacing:.01em;margin:0}.rm-rcard__subtitle{color:var(--text-muted);font-size:var(--font-14);margin-top:4px;overflow-wrap:anywhere}.rm-rcard__actions{align-items:center;display:flex;flex:0 0 auto;gap:var(--space-8)}.rm-rcard__body{align-items:stretch;display:flex;gap:var(--space-24);padding:var(--space-16) var(--space-24) var(--space-20)}.rm-rcard__count{align-items:center;border-right:1px solid var(--border-default);display:flex;flex:0 0 128px;flex-direction:column;gap:var(--space-2);justify-content:center;padding-right:var(--space-24);text-align:center}.rm-rcard__count-value{color:var(--color-primary);font-size:var(--font-32);font-weight:var(--weight-bold);line-height:1.1;margin:0}.rm-rcard__count-label{color:var(--text-muted);font-size:var(--font-12);font-weight:var(--weight-semibold);letter-spacing:.08em;margin:0;text-transform:uppercase}.rm-rcard__count-extra{color:var(--color-primary);font-size:var(--font-13);line-height:1.3;margin:6px 0 0}.rm-rcard__count-extra+.rm-rcard__count-extra{margin-top:2px}.rm-rcard__main{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--space-6);min-width:0}.rm-rcard__meta{align-items:center;display:flex;flex-wrap:wrap;gap:var(--space-8)}.rm-rcard__more{color:var(--color-primary);font-size:var(--font-14);font-weight:var(--weight-semibold);margin:4px 0 0}.rm-rcard__footer{background:var(--bg-surface-2);border-top:1px solid var(--border-default);font-size:var(--font-13);padding:var(--space-12) var(--space-24)}.rm-rcard :deep(.rm-rcard__label){align-items:center;color:var(--text-secondary);display:inline-flex;font-size:var(--font-12);font-weight:var(--weight-bold);gap:var(--space-4);letter-spacing:.06em;text-transform:uppercase}.rm-rcard :deep(.rm-rcard__ref){background:var(--bg-surface-3);border-radius:var(--radius-sm);color:var(--text-secondary);font-family:Courier New,monospace;font-size:var(--font-12);padding:var(--space-2) var(--space-8)}.rm-rcard :deep(.rm-rcard__lead){font-size:15px;font-weight:var(--weight-bold);margin:0}.rm-rcard :deep(.rm-rcard__text){font-size:15px;margin:0}.rm-rcard :deep(.rm-rcard__desc){color:var(--text-secondary);font-size:var(--font-14);line-height:1.5;margin:0}.rm-rcard :deep(.rm-rcard__clamp){display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.rm-rcard--dense{border-radius:var(--radius-xl)}.rm-rcard--dense .rm-rcard__head{gap:var(--space-12);padding:var(--space-12) var(--space-16) 0}.rm-rcard--dense .rm-rcard__title{font-size:15px}.rm-rcard--dense .rm-rcard__subtitle{font-size:var(--font-13);margin-top:2px}.rm-rcard--dense .rm-rcard__body{gap:var(--space-16);padding:var(--space-8) var(--space-16) var(--space-12)}.rm-rcard--dense .rm-rcard__count{flex-basis:104px;padding-right:var(--space-16)}.rm-rcard--dense .rm-rcard__count-value{font-size:var(--font-24)}.rm-rcard--dense .rm-rcard__count-label{font-size:var(--font-11)}.rm-rcard--dense .rm-rcard__main{gap:var(--space-4)}.rm-rcard--dense .rm-rcard__count-extra,.rm-rcard--dense .rm-rcard__footer,.rm-rcard--dense .rm-rcard__main>:not(.rm-rcard__meta,.rm-rcard__lead){display:none}.rm-rcard--dense :deep(.rm-rcard__lead){display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}.rm-rcard--dense .rm-rcard__body{align-items:center}.rm-rcard--dense :deep(.rm-rcard__lead),.rm-rcard--dense :deep(.rm-rcard__text){font-size:var(--font-14)}.rm-rcard--dense :deep(.rm-rcard__desc){font-size:var(--font-13)}
|
|
139
142
|
</style>
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
partent jamais dans le navigateur.
|
|
20
20
|
-->
|
|
21
21
|
<script setup lang="ts">
|
|
22
|
+
import { rmCopyText } from '../utils/clipboard'
|
|
22
23
|
import { computed, ref, useId, onUnmounted } from 'vue'
|
|
23
24
|
|
|
24
25
|
const props = withDefaults(defineProps<{
|
|
@@ -193,37 +194,8 @@ const teaserHtml = computed(() => (props.summary ? '' : props.aboutSource || '')
|
|
|
193
194
|
const copied = ref(false)
|
|
194
195
|
let copyTimer: ReturnType<typeof setTimeout> | undefined
|
|
195
196
|
|
|
196
|
-
/**
|
|
197
|
-
* Le presse-papiers moderne demande un contexte sur, et refuse dans un iframe
|
|
198
|
-
* sans permission : la selection cachee reste le seul chemin qui marche partout.
|
|
199
|
-
* En cas d'echec des deux, pas de coche : elle mentirait.
|
|
200
|
-
*/
|
|
201
|
-
async function writeToClipboard(text: string) {
|
|
202
|
-
try {
|
|
203
|
-
await navigator.clipboard.writeText(text)
|
|
204
|
-
return true
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
207
|
-
try {
|
|
208
|
-
const field = document.createElement('textarea')
|
|
209
|
-
field.value = text
|
|
210
|
-
field.setAttribute('readonly', '')
|
|
211
|
-
field.style.position = 'fixed'
|
|
212
|
-
field.style.opacity = '0'
|
|
213
|
-
document.body.appendChild(field)
|
|
214
|
-
field.select()
|
|
215
|
-
const done = document.execCommand('copy')
|
|
216
|
-
document.body.removeChild(field)
|
|
217
|
-
return done
|
|
218
|
-
}
|
|
219
|
-
catch {
|
|
220
|
-
return false
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
197
|
async function copyTitle() {
|
|
226
|
-
if (!(await
|
|
198
|
+
if (!(await rmCopyText(props.title))) return
|
|
227
199
|
copied.value = true
|
|
228
200
|
clearTimeout(copyTimer)
|
|
229
201
|
copyTimer = setTimeout(() => (copied.value = false), 2000)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copie un texte dans le presse-papiers. Rend `false` si rien n'a ete copie,
|
|
3
|
+
* pour qu'un composant n'affiche pas une coche qui mentirait.
|
|
4
|
+
*
|
|
5
|
+
* Le presse-papiers moderne demande un contexte sur, et refuse dans un iframe
|
|
6
|
+
* sans permission : la selection cachee reste le seul chemin qui marche partout.
|
|
7
|
+
*/
|
|
8
|
+
export declare function rmCopyText(text: string): Promise<boolean>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export async function rmCopyText(text) {
|
|
2
|
+
try {
|
|
3
|
+
await navigator.clipboard.writeText(text);
|
|
4
|
+
return true;
|
|
5
|
+
} catch {
|
|
6
|
+
try {
|
|
7
|
+
const field = document.createElement("textarea");
|
|
8
|
+
field.value = text;
|
|
9
|
+
field.setAttribute("readonly", "");
|
|
10
|
+
field.style.position = "fixed";
|
|
11
|
+
field.style.opacity = "0";
|
|
12
|
+
document.body.appendChild(field);
|
|
13
|
+
field.select();
|
|
14
|
+
const done = document.execCommand("copy");
|
|
15
|
+
document.body.removeChild(field);
|
|
16
|
+
return done;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|