@tekkare/romulus 0.1.232 → 0.1.234

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "0.1.231",
7
+ "version": "0.1.233",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -0,0 +1,79 @@
1
+ <!--
2
+ RmCopyCode - un identifiant (RPPS, FINESS, NCT, EUCT, PMID...) qu'on copie
3
+ d'un clic.
4
+
5
+ Ces codes sont ce que l'utilisateur recopie dans un autre outil : les
6
+ laisser en pastille grise au bout de la ligne de tags les noyait parmi les
7
+ statuts. Le composant les sort avec leur libelle (« RPPS · 1000... ») pour
8
+ qu'on sache quel code on lit sans connaitre son format.
9
+
10
+ Le clic ne remonte pas : posee dans une carte cliquable (RmResultCard), la
11
+ copie ouvrirait la fiche. Retour visuel obligatoire : coche verte deux
12
+ secondes, annoncee aux lecteurs d'ecran, sinon l'utilisateur reclique. Pas
13
+ de coche si la copie a echoue, elle mentirait.
14
+
15
+ Pas d'equivalent Auriga : nouveau composant.
16
+ -->
17
+ <script setup lang="ts">
18
+ import { computed, ref, onUnmounted } from 'vue'
19
+ import { rmCopyText } from '../utils/clipboard'
20
+
21
+ const props = withDefaults(defineProps<{
22
+ /** Le code copie, tel quel. */
23
+ value: string
24
+ /** Nom du code (« RPPS », « FINESS »). Affiche, jamais copie. */
25
+ label?: string
26
+ lang?: 'fr' | 'en'
27
+ }>(), {
28
+ label: '',
29
+ lang: 'fr',
30
+ })
31
+
32
+ const emit = defineEmits<{ copy: [value: string] }>()
33
+
34
+ const WORDS = {
35
+ fr: { copy: 'Copier', copied: 'Copié' },
36
+ en: { copy: 'Copy', copied: 'Copied' },
37
+ }
38
+ const t = computed(() => WORDS[props.lang] ?? WORDS.fr)
39
+
40
+ const copied = ref(false)
41
+ let timer: ReturnType<typeof setTimeout> | undefined
42
+
43
+ async function copy() {
44
+ if (!(await rmCopyText(props.value))) return
45
+ copied.value = true
46
+ clearTimeout(timer)
47
+ timer = setTimeout(() => (copied.value = false), 2000)
48
+ emit('copy', props.value)
49
+ }
50
+
51
+ onUnmounted(() => clearTimeout(timer))
52
+
53
+ const name = computed(() => [props.label, props.value].filter(Boolean).join(' '))
54
+ </script>
55
+
56
+ <template>
57
+ <button
58
+ type="button"
59
+ class="rm-copy-code"
60
+ :class="{ 'rm-copy-code--copied': copied }"
61
+ :aria-label="`${copied ? t.copied : t.copy} ${name}`"
62
+ :title="copied ? t.copied : t.copy"
63
+ @click.stop="copy"
64
+ >
65
+ <span v-if="label" class="rm-copy-code__label">{{ label }}</span>
66
+ <span class="rm-copy-code__value">{{ value }}</span>
67
+ <RmIcon
68
+ class="rm-copy-code__icon"
69
+ :name="copied ? 'Check' : 'Copy'"
70
+ :size="14"
71
+ :weight="copied ? 'bold' : 'regular'"
72
+ />
73
+ <span class="rm-copy-code__live" aria-live="polite">{{ copied ? t.copied : '' }}</span>
74
+ </button>
75
+ </template>
76
+
77
+ <style scoped>
78
+ .rm-copy-code{align-items:center;background:var(--bg-surface-2);border:1px solid var(--border-default);border-radius:var(--radius-sm);color:var(--text-primary);cursor:pointer;display:inline-flex;font-family:var(--font-sans);font-size:var(--font-13);gap:var(--space-8);line-height:1.4;max-width:100%;padding:var(--space-2) var(--space-8);transition:background var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out)}.rm-copy-code:hover{background:var(--bg-hover);border-color:var(--color-violet-pale)}.rm-copy-code:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.rm-copy-code__label{color:var(--text-muted);font-size:var(--font-11);font-weight:var(--weight-bold);letter-spacing:.08em;text-transform:uppercase}.rm-copy-code__value{font-family:Courier New,monospace;font-weight:var(--weight-bold);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-copy-code__icon{color:var(--text-muted);flex-shrink:0}.rm-copy-code--copied,.rm-copy-code--copied:hover{background:var(--color-success-bg);border-color:var(--color-success)}.rm-copy-code--copied .rm-copy-code__icon{color:var(--color-success)}.rm-copy-code__live{clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}
79
+ </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>
@@ -59,6 +59,13 @@ const props = withDefaults(defineProps<{
59
59
  open?: boolean
60
60
  collapseLabel?: string
61
61
  expandLabel?: string
62
+ /**
63
+ * true : toutes les sections repliables arrivent deployees, et restent
64
+ * repliables au clic. Pour une app dont la navigation est courte, ou le
65
+ * lecteur veut voir d'emblee tout ce qui est offert sans deplier groupe par
66
+ * groupe.
67
+ */
68
+ defaultOpen?: boolean
62
69
  }>(), {
63
70
  flag: undefined,
64
71
  titleTo: undefined,
@@ -67,6 +74,7 @@ const props = withDefaults(defineProps<{
67
74
  open: false,
68
75
  collapseLabel: 'Replier la barre latérale',
69
76
  expandLabel: 'Déplier la barre latérale',
77
+ defaultOpen: false,
70
78
  })
71
79
 
72
80
  const emit = defineEmits<{
@@ -128,6 +136,19 @@ watch(() => sectionOf(props.activePath), (key) => {
128
136
  if (key && !openSections.value.has(key)) openSections.value.add(key)
129
137
  })
130
138
 
139
+ // `defaultOpen` : chaque section deployee a sa premiere apparition, y compris
140
+ // celles que les droits d'acces ajoutent en cours de route. Une section deja
141
+ // vue garde l'etat que le lecteur lui a donne.
142
+ const seenSections = new Set<string>()
143
+ watch(() => collapsibles.value.map(s => s.key), (keys) => {
144
+ if (!props.defaultOpen) return
145
+ for (const key of keys) {
146
+ if (seenSections.has(key)) continue
147
+ seenSections.add(key)
148
+ openSections.value.add(key)
149
+ }
150
+ }, { immediate: true })
151
+
131
152
  function toggleSection(key: string) {
132
153
  if (openSections.value.has(key)) openSections.value.delete(key)
133
154
  else openSections.value.add(key)
@@ -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 writeToClipboard(props.title))) return
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.232",
3
+ "version": "0.1.234",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",