@tekkare/romulus 0.1.140 → 0.1.142

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.139",
7
+ "version": "0.1.141",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -68,6 +68,22 @@ export interface RmSavedView {
68
68
  /** La vue s'applique a l'ouverture de la page. */
69
69
  isDefault?: boolean
70
70
  starred?: boolean
71
+ /**
72
+ * La vue vient de quelqu'un d'autre : son nom. Le lecteur ne peut alors
73
+ * qu'activer la vue ou la dupliquer, le contenu reste a son auteur ;
74
+ * l'etoile et la vue par defaut, elles, restent a sa main.
75
+ */
76
+ sharedBy?: string
77
+ /** Ma vue, ouverte a d'autres. Le bandeau le rappelle a son auteur. */
78
+ shared?: boolean
79
+ /**
80
+ * Menu propre a cette vue, a la place de celui de la liste. Une liste ou
81
+ * cohabitent mes vues et celles des autres n'a pas un seul menu : le
82
+ * lecteur d'une vue partagee ne peut ni la renommer ni la supprimer.
83
+ */
84
+ menu?: RmSavedViewMenuItem[]
85
+ /** Renommage sur place, a la place du reglage de la liste. */
86
+ renamable?: boolean
71
87
  config?: RmSavedViewConfigRow[]
72
88
  /** Pied de carte : auteur, date, compteurs. */
73
89
  meta?: RmSavedViewMeta[]
@@ -99,6 +115,11 @@ const props = withDefaults(
99
115
  { menu: () => [], renamable: false, starrable: false, lang: 'fr' },
100
116
  )
101
117
 
118
+ /** Menu de cette carte : celui de la vue s'il y en a un, sinon celui de la liste. */
119
+ const menuItems = computed(() => props.view.menu ?? props.menu)
120
+ /** Idem pour le renommage sur place. */
121
+ const canRename = computed(() => props.view.renamable ?? props.renamable)
122
+
102
123
  const emit = defineEmits<{
103
124
  activate: [id: string]
104
125
  star: [id: string]
@@ -117,6 +138,9 @@ const T = computed(() => ({
117
138
  isDefault: en.value
118
139
  ? 'Default view, applied on opening'
119
140
  : "Vue par défaut, s'active à l'ouverture",
141
+ shared: en.value ? 'Shared' : 'Partagée',
142
+ sharedBy: (name: string) =>
143
+ en.value ? `Shared by ${name}` : `Partagée par ${name}`,
120
144
  cancel: en.value ? 'Cancel' : 'Annuler',
121
145
  confirmPrefix: en.value ? 'Permanently delete' : 'Supprimer définitivement',
122
146
  }))
@@ -140,7 +164,7 @@ function pick(item: RmSavedViewMenuItem) {
140
164
  pending.value = item
141
165
  return
142
166
  }
143
- if (item.key === 'rename' && props.renamable) {
167
+ if (item.key === 'rename' && canRename.value) {
144
168
  startRename()
145
169
  return
146
170
  }
@@ -220,9 +244,15 @@ function onCardClick() {
220
244
  :class="{ 'rm-view--active': view.active, 'rm-view--default': view.isDefault }"
221
245
  @click="onCardClick"
222
246
  >
223
- <div v-if="view.isDefault" class="rm-view__strip">
224
- <RmIcon name="PushPin" :size="14" weight="fill" />
225
- {{ T.isDefault }}
247
+ <div v-if="view.isDefault || view.sharedBy || view.shared" class="rm-view__strip">
248
+ <span v-if="view.isDefault" class="rm-view__strip-mark">
249
+ <RmIcon name="PushPin" :size="14" weight="fill" />
250
+ {{ T.isDefault }}
251
+ </span>
252
+ <span v-if="view.sharedBy || view.shared" class="rm-view__strip-mark">
253
+ <RmIcon name="UsersThree" :size="14" weight="fill" />
254
+ {{ view.sharedBy ? T.sharedBy(view.sharedBy) : T.shared }}
255
+ </span>
226
256
  </div>
227
257
 
228
258
  <div class="rm-view__head">
@@ -258,7 +288,7 @@ function onCardClick() {
258
288
  @keydown.esc.prevent="renaming = false"
259
289
  @blur="commitRename"
260
290
  >
261
- <span v-else class="rm-view__title" @dblclick.stop="renamable && startRename()">
291
+ <span v-else class="rm-view__title" @dblclick.stop="canRename && startRename()">
262
292
  {{ view.title }}
263
293
  </span>
264
294
 
@@ -276,7 +306,7 @@ function onCardClick() {
276
306
  {{ view.actionLabel || T.activate }}
277
307
  </RmButton>
278
308
 
279
- <div v-if="menu.length" class="rm-view__menu-wrap">
309
+ <div v-if="menuItems.length" class="rm-view__menu-wrap">
280
310
  <button
281
311
  type="button"
282
312
  class="rm-view__more"
@@ -294,7 +324,7 @@ function onCardClick() {
294
324
  <div class="rm-view__menu-shade" @click.stop="menuOpen = false" />
295
325
  <div class="rm-view__menu" role="menu">
296
326
  <button
297
- v-for="item in menu"
327
+ v-for="item in menuItems"
298
328
  :key="item.key"
299
329
  type="button"
300
330
  class="rm-view__menu-item"
@@ -366,5 +396,5 @@ function onCardClick() {
366
396
  </template>
367
397
 
368
398
  <style scoped>
369
- .rm-view{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);color:var(--text-primary);cursor:pointer;font-family:var(--font-sans);padding:14px 18px;transition:border-color var(--duration-fast) var(--ease-out),box-shadow var(--duration-fast) var(--ease-out),background var(--duration-fast) var(--ease-out)}.rm-view:not(.rm-view--active):hover{background:var(--bg-hover);border-color:var(--color-accent-soft)}.rm-view--active{border-color:var(--color-secondary);box-shadow:0 0 0 3px rgba(var(--brand-rgb),.08);cursor:default}.rm-view__strip{background:var(--bg-hover);border-bottom:1px solid var(--border-soft);border-radius:7px 7px 0 0;color:var(--color-secondary);font-size:var(--font-11);font-weight:var(--weight-bold);gap:var(--space-8);letter-spacing:.06em;margin:-14px -18px var(--space-12);padding:var(--space-8) var(--space-16);text-transform:uppercase}.rm-view__head,.rm-view__strip{align-items:center;display:flex}.rm-view__head{gap:var(--space-10)}.rm-view__icon{align-items:center;border-radius:var(--radius-md);display:inline-flex;flex:0 0 auto;height:34px;justify-content:center;width:34px}.rm-view__icon--primary{background:var(--bg-hover);color:var(--color-secondary)}.rm-view__icon--success{background:var(--color-success-bg);color:var(--color-success)}.rm-view__icon--warning{background:var(--color-warning-bg);color:var(--color-warning)}.rm-view__icon--danger{background:var(--color-danger-bg);color:var(--color-danger)}.rm-view__icon--info{background:var(--color-info-bg);color:var(--color-info)}.rm-view__star{align-items:center;background:transparent;border:1px solid transparent;border-radius:var(--radius-md);color:var(--text-muted);cursor:pointer;display:inline-flex;flex:0 0 auto;height:28px;justify-content:center;padding:0;width:28px}.rm-view__star:hover{background:var(--bg-hover)}.rm-view__star--on,.rm-view__star:hover{color:var(--color-warning)}.rm-view__title,.rm-view__title-edit{flex:1;font-size:var(--font-16);font-weight:var(--weight-bold);min-width:0}.rm-view__title-edit{background:var(--bg-surface);border:1px solid var(--color-secondary);border-radius:var(--radius-md);color:var(--text-primary);font-family:inherit;outline:none;padding:var(--space-2) var(--space-8)}.rm-view__title-edit:focus{box-shadow:0 0 0 3px rgba(var(--brand-rgb),.12)}.rm-view__actions{align-items:center;display:flex;gap:var(--space-8)}.rm-view__badge{align-items:center;background:var(--color-success-bg);border-radius:var(--radius-pill);color:var(--color-success);display:inline-flex;font-size:var(--font-10);font-weight:var(--weight-bold);gap:var(--space-4);letter-spacing:.06em;padding:3px var(--space-8);text-transform:uppercase}.rm-view__menu-wrap{position:relative}.rm-view__more{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-muted);cursor:pointer;display:inline-flex;height:28px;justify-content:center;padding:0;width:28px}.rm-view__more:hover{background:var(--bg-hover);color:var(--text-primary)}.rm-view__menu-shade{inset:0;position:fixed;z-index:60}.rm-view__menu{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);display:flex;flex-direction:column;min-width:200px;padding:var(--space-4);position:absolute;right:0;top:calc(100% + 6px);z-index:61}.rm-view__menu-item{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-13);gap:var(--space-8);padding:var(--space-8) var(--space-12);text-align:left;width:100%}.rm-view__menu-item:hover{background:var(--bg-hover);color:var(--color-secondary)}.rm-view__menu-item--danger{color:var(--color-danger)}.rm-view__menu-item--danger:hover{background:var(--color-danger-bg);color:var(--color-danger)}.rm-view__desc{color:var(--text-secondary);font-size:var(--font-13);margin:var(--space-8) 0 var(--space-12)}.rm-view__config{background:var(--bg-surface-2);border:1px solid var(--border-soft);border-radius:var(--radius-md);display:flex;flex-direction:column;gap:var(--space-6);margin:10px 0 var(--space-12);padding:var(--space-10) 14px}.rm-view__row{align-items:flex-start;display:flex;flex-wrap:wrap;font-size:var(--font-12);gap:14px}.rm-view__row-key{color:var(--text-muted);display:inline-flex;flex-shrink:0;font-size:var(--font-10);font-weight:var(--weight-semibold);letter-spacing:.04em;min-width:96px;padding-top:var(--space-4);text-transform:uppercase}.rm-view__row-key,.rm-view__row-val{align-items:center;gap:var(--space-6)}.rm-view__row-val{color:var(--text-primary);display:flex;flex:1 1 200px;flex-wrap:wrap;padding:var(--space-2) 0}.rm-view__pill{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);display:inline-flex;font-size:var(--font-12);gap:var(--space-4);padding:5px var(--space-10)}.rm-view__pill-k{color:var(--color-primary);font-weight:var(--weight-semibold)}.rm-view__pill-tip{min-width:0}.rm-view__pill-tip :deep(.rm-tooltip__bubble){max-width:320px}.rm-view__pill-v{display:-webkit-box;-webkit-box-orient:vertical;color:var(--text-primary);font-weight:var(--weight-medium);-webkit-line-clamp:3;overflow:hidden}.rm-view__foot{border-top:1px solid var(--border-soft);color:var(--text-muted);display:flex;flex-wrap:wrap;font-size:var(--font-11);gap:var(--space-16);padding-top:var(--space-10)}.rm-view__foot span{align-items:center;display:inline-flex;gap:var(--space-4)}.rm-view__confirm{align-items:center;background:var(--color-danger-bg);border:1px solid var(--color-danger);border-radius:var(--radius-md);color:var(--color-danger);display:flex;flex-wrap:wrap;font-size:var(--font-13);gap:var(--space-12);margin-top:var(--space-12);padding:var(--space-8) var(--space-12)}.rm-view__confirm>span{align-items:center;display:inline-flex;flex:1 1 220px;gap:var(--space-8)}.rm-view__confirm-actions{display:flex;flex-shrink:0;gap:var(--space-8)}
399
+ .rm-view{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);color:var(--text-primary);cursor:pointer;font-family:var(--font-sans);padding:14px 18px;transition:border-color var(--duration-fast) var(--ease-out),box-shadow var(--duration-fast) var(--ease-out),background var(--duration-fast) var(--ease-out)}.rm-view:not(.rm-view--active):hover{background:var(--bg-hover);border-color:var(--color-accent-soft)}.rm-view--active{border-color:var(--color-secondary);box-shadow:0 0 0 3px rgba(var(--brand-rgb),.08);cursor:default}.rm-view__strip{align-items:center;background:var(--bg-hover);border-bottom:1px solid var(--border-soft);border-radius:7px 7px 0 0;color:var(--color-secondary);display:flex;font-size:var(--font-11);font-weight:var(--weight-bold);gap:var(--space-8);letter-spacing:.06em;margin:-14px -18px var(--space-12);padding:var(--space-8) var(--space-16);text-transform:uppercase}.rm-view__strip-mark{align-items:center;display:inline-flex;gap:var(--space-6)}.rm-view__strip-mark+.rm-view__strip-mark:before{background:var(--border-soft);content:"";height:10px;margin-right:var(--space-8);width:1px}.rm-view__head{align-items:center;display:flex;gap:var(--space-10)}.rm-view__icon{align-items:center;border-radius:var(--radius-md);display:inline-flex;flex:0 0 auto;height:34px;justify-content:center;width:34px}.rm-view__icon--primary{background:var(--bg-hover);color:var(--color-secondary)}.rm-view__icon--success{background:var(--color-success-bg);color:var(--color-success)}.rm-view__icon--warning{background:var(--color-warning-bg);color:var(--color-warning)}.rm-view__icon--danger{background:var(--color-danger-bg);color:var(--color-danger)}.rm-view__icon--info{background:var(--color-info-bg);color:var(--color-info)}.rm-view__star{align-items:center;background:transparent;border:1px solid transparent;border-radius:var(--radius-md);color:var(--text-muted);cursor:pointer;display:inline-flex;flex:0 0 auto;height:28px;justify-content:center;padding:0;width:28px}.rm-view__star:hover{background:var(--bg-hover)}.rm-view__star--on,.rm-view__star:hover{color:var(--color-warning)}.rm-view__title,.rm-view__title-edit{flex:1;font-size:var(--font-16);font-weight:var(--weight-bold);min-width:0}.rm-view__title-edit{background:var(--bg-surface);border:1px solid var(--color-secondary);border-radius:var(--radius-md);color:var(--text-primary);font-family:inherit;outline:none;padding:var(--space-2) var(--space-8)}.rm-view__title-edit:focus{box-shadow:0 0 0 3px rgba(var(--brand-rgb),.12)}.rm-view__actions{align-items:center;display:flex;gap:var(--space-8)}.rm-view__badge{align-items:center;background:var(--color-success-bg);border-radius:var(--radius-pill);color:var(--color-success);display:inline-flex;font-size:var(--font-10);font-weight:var(--weight-bold);gap:var(--space-4);letter-spacing:.06em;padding:3px var(--space-8);text-transform:uppercase}.rm-view__menu-wrap{position:relative}.rm-view__more{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-muted);cursor:pointer;display:inline-flex;height:28px;justify-content:center;padding:0;width:28px}.rm-view__more:hover{background:var(--bg-hover);color:var(--text-primary)}.rm-view__menu-shade{inset:0;position:fixed;z-index:60}.rm-view__menu{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);display:flex;flex-direction:column;min-width:200px;padding:var(--space-4);position:absolute;right:0;top:calc(100% + 6px);z-index:61}.rm-view__menu-item{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-13);gap:var(--space-8);padding:var(--space-8) var(--space-12);text-align:left;width:100%}.rm-view__menu-item:hover{background:var(--bg-hover);color:var(--color-secondary)}.rm-view__menu-item--danger{color:var(--color-danger)}.rm-view__menu-item--danger:hover{background:var(--color-danger-bg);color:var(--color-danger)}.rm-view__desc{color:var(--text-secondary);font-size:var(--font-13);margin:var(--space-8) 0 var(--space-12)}.rm-view__config{background:var(--bg-surface-2);border:1px solid var(--border-soft);border-radius:var(--radius-md);display:flex;flex-direction:column;gap:var(--space-6);margin:10px 0 var(--space-12);padding:var(--space-10) 14px}.rm-view__row{align-items:flex-start;display:flex;flex-wrap:wrap;font-size:var(--font-12);gap:14px}.rm-view__row-key{color:var(--text-muted);display:inline-flex;flex-shrink:0;font-size:var(--font-10);font-weight:var(--weight-semibold);letter-spacing:.04em;min-width:96px;padding-top:var(--space-4);text-transform:uppercase}.rm-view__row-key,.rm-view__row-val{align-items:center;gap:var(--space-6)}.rm-view__row-val{color:var(--text-primary);display:flex;flex:1 1 200px;flex-wrap:wrap;padding:var(--space-2) 0}.rm-view__pill{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);display:inline-flex;font-size:var(--font-12);gap:var(--space-4);padding:5px var(--space-10)}.rm-view__pill-k{color:var(--color-primary);font-weight:var(--weight-semibold)}.rm-view__pill-tip{min-width:0}.rm-view__pill-tip :deep(.rm-tooltip__bubble){max-width:320px}.rm-view__pill-v{display:-webkit-box;-webkit-box-orient:vertical;color:var(--text-primary);font-weight:var(--weight-medium);-webkit-line-clamp:3;overflow:hidden}.rm-view__foot{border-top:1px solid var(--border-soft);color:var(--text-muted);display:flex;flex-wrap:wrap;font-size:var(--font-11);gap:var(--space-16);padding-top:var(--space-10)}.rm-view__foot span{align-items:center;display:inline-flex;gap:var(--space-4)}.rm-view__confirm{align-items:center;background:var(--color-danger-bg);border:1px solid var(--color-danger);border-radius:var(--radius-md);color:var(--color-danger);display:flex;flex-wrap:wrap;font-size:var(--font-13);gap:var(--space-12);margin-top:var(--space-12);padding:var(--space-8) var(--space-12)}.rm-view__confirm>span{align-items:center;display:inline-flex;flex:1 1 220px;gap:var(--space-8)}.rm-view__confirm-actions{display:flex;flex-shrink:0;gap:var(--space-8)}
370
400
  </style>
@@ -0,0 +1,250 @@
1
+ <!--
2
+ RmShareDialog - a qui une vue sauvegardee est ouverte.
3
+
4
+ Trois portees, exclusives : a personne, a des gens nommes, a tout l'espace
5
+ de travail. « Tout l'espace » n'est pas un raccourci pour « cocher tout le
6
+ monde » : c'est une regle, et elle vaut aussi pour les membres qui arrivent
7
+ apres. Deplier la case en une liste de noms au moment du partage laisserait
8
+ les arrivants dehors, sans que personne s'en apercoive.
9
+
10
+ La liste des noms reste montee quand la portee change : on revient de
11
+ « tout l'espace » a « des personnes » sans avoir reperdu sa selection.
12
+
13
+ Le dialogue ne parle pas a l'API. Il rend une portee et une liste d'ids ;
14
+ l'appelant enregistre. Le package ne sait pas ou vivent les vues.
15
+ -->
16
+ <script setup lang="ts">
17
+ import { computed, ref, watch } from 'vue'
18
+
19
+ export interface RmShareMember {
20
+ userId: string
21
+ /** Nom affiche. A defaut, l'appelant y met l'adresse. */
22
+ name: string
23
+ email?: string
24
+ }
25
+
26
+ export type RmShareScope = 'private' | 'people' | 'workspace'
27
+
28
+ const props = withDefaults(
29
+ defineProps<{
30
+ modelValue: boolean
31
+ /** Nom de la vue partagee, rappele dans l'en-tete. */
32
+ title?: string
33
+ /** Membres de l'espace, sans son proprietaire : l'appelant le retire. */
34
+ members?: RmShareMember[]
35
+ /** Portee enregistree. `people` se deduit d'une liste non vide. */
36
+ visibility?: 'private' | 'workspace'
37
+ /** Ids deja destinataires. */
38
+ sharedWith?: string[]
39
+ loading?: boolean
40
+ lang?: 'fr' | 'en'
41
+ }>(),
42
+ {
43
+ title: '',
44
+ members: () => [],
45
+ visibility: 'private',
46
+ sharedWith: () => [],
47
+ loading: false,
48
+ lang: 'fr',
49
+ },
50
+ )
51
+
52
+ const emit = defineEmits<{
53
+ 'update:modelValue': [value: boolean]
54
+ save: [value: { visibility: 'private' | 'workspace'; sharedWith: string[] }]
55
+ }>()
56
+
57
+ const en = computed(() => props.lang === 'en')
58
+
59
+ const T = computed(() => ({
60
+ heading: en.value ? 'Share the view' : 'Partager la vue',
61
+ private: en.value ? 'Only me' : 'Moi seul',
62
+ privateHint: en.value
63
+ ? 'Nobody else sees this view.'
64
+ : 'Personne d\'autre ne voit cette vue.',
65
+ people: en.value ? 'Chosen people' : 'Des personnes choisies',
66
+ peopleHint: en.value
67
+ ? 'They can apply the view and mark it, not change it.'
68
+ : 'Elles peuvent activer la vue et la marquer, pas la modifier.',
69
+ workspace: en.value ? 'The whole workspace' : 'Tout l\'espace de travail',
70
+ workspaceHint: en.value
71
+ ? 'Members who join later see it too.'
72
+ : 'Les membres qui arrivent ensuite la voient aussi.',
73
+ search: en.value ? 'Search for a person' : 'Chercher une personne',
74
+ empty: en.value ? 'No member to share with.' : 'Aucun membre a qui partager.',
75
+ noMatch: en.value ? 'No match.' : 'Aucun resultat.',
76
+ count: (n: number) =>
77
+ en.value
78
+ ? `${n} ${n > 1 ? 'people' : 'person'} selected`
79
+ : `${n} ${n > 1 ? 'personnes' : 'personne'} selectionnee${n > 1 ? 's' : ''}`,
80
+ cancel: en.value ? 'Cancel' : 'Annuler',
81
+ save: en.value ? 'Share' : 'Partager',
82
+ }))
83
+
84
+ const scope = ref<RmShareScope>('private')
85
+ const picked = ref<string[]>([])
86
+ const search = ref('')
87
+
88
+ /**
89
+ * L'etat ouvert fait foi : le dialogue se recharge a chaque ouverture, jamais
90
+ * pendant, sinon une reponse de l'API en cours de saisie reposerait les cases.
91
+ */
92
+ watch(
93
+ () => props.modelValue,
94
+ (open) => {
95
+ if (!open) return
96
+ picked.value = [...props.sharedWith]
97
+ scope.value =
98
+ props.visibility === 'workspace'
99
+ ? 'workspace'
100
+ : props.sharedWith.length
101
+ ? 'people'
102
+ : 'private'
103
+ search.value = ''
104
+ },
105
+ { immediate: true },
106
+ )
107
+
108
+ const matches = computed(() => {
109
+ const q = search.value.trim().toLowerCase()
110
+ if (!q) return props.members
111
+ return props.members.filter(
112
+ m =>
113
+ m.name.toLowerCase().includes(q) ||
114
+ (m.email ?? '').toLowerCase().includes(q),
115
+ )
116
+ })
117
+
118
+ function toggle(userId: string) {
119
+ const next = new Set(picked.value)
120
+ if (next.has(userId)) next.delete(userId)
121
+ else next.add(userId)
122
+ picked.value = [...next]
123
+ }
124
+
125
+ function close() {
126
+ emit('update:modelValue', false)
127
+ }
128
+
129
+ /**
130
+ * Enregistrer efface ce que la portee retenue ne porte pas : passer a « moi
131
+ * seul » retire les destinataires, sinon ils resteraient en base a attendre
132
+ * le prochain partage sans que rien ne le dise a l'ecran.
133
+ */
134
+ function save() {
135
+ emit('save', {
136
+ visibility: scope.value === 'workspace' ? 'workspace' : 'private',
137
+ sharedWith: scope.value === 'people' ? picked.value : [],
138
+ })
139
+ }
140
+
141
+ const scopes = computed<Array<{ key: RmShareScope; icon: string; label: string; hint: string }>>(
142
+ () => [
143
+ { key: 'private', icon: 'Lock', label: T.value.private, hint: T.value.privateHint },
144
+ { key: 'people', icon: 'UserPlus', label: T.value.people, hint: T.value.peopleHint },
145
+ {
146
+ key: 'workspace',
147
+ icon: 'UsersThree',
148
+ label: T.value.workspace,
149
+ hint: T.value.workspaceHint,
150
+ },
151
+ ],
152
+ )
153
+ </script>
154
+
155
+ <template>
156
+ <RmModal
157
+ :model-value="modelValue"
158
+ size="sm"
159
+ mode="free"
160
+ :label="T.heading"
161
+ @update:model-value="close"
162
+ >
163
+ <div class="rm-share">
164
+ <header class="rm-share__head">
165
+ <h2 class="rm-share__title">{{ T.heading }}</h2>
166
+ <p v-if="title" class="rm-share__subject">{{ title }}</p>
167
+ </header>
168
+
169
+ <!--
170
+ Trois choix exclusifs : des boutons radio natifs, pas trois boutons.
171
+ Le groupe s'annonce alors comme un choix, se parcourt aux fleches et
172
+ dit lequel est retenu, sans qu'on ait a le recoder.
173
+ -->
174
+ <ul class="rm-share__scopes">
175
+ <li v-for="item in scopes" :key="item.key">
176
+ <label class="rm-share__scope" :class="{ 'is-on': scope === item.key }">
177
+ <input
178
+ v-model="scope"
179
+ type="radio"
180
+ name="rm-share-scope"
181
+ class="rm-share__radio"
182
+ :value="item.key"
183
+ :aria-label="item.label"
184
+ >
185
+ <RmIcon :name="item.icon" :size="18" />
186
+ <span class="rm-share__scope-text">
187
+ <span class="rm-share__scope-label">{{ item.label }}</span>
188
+ <span class="rm-share__scope-hint">{{ item.hint }}</span>
189
+ </span>
190
+ <RmIcon
191
+ v-if="scope === item.key"
192
+ name="CheckCircle"
193
+ :size="16"
194
+ weight="fill"
195
+ class="rm-share__scope-mark"
196
+ />
197
+ </label>
198
+ </li>
199
+ </ul>
200
+
201
+ <div v-show="scope === 'people'" class="rm-share__people">
202
+ <label class="rm-share__search">
203
+ <RmIcon name="MagnifyingGlass" :size="16" />
204
+ <input v-model="search" type="search" :placeholder="T.search" :aria-label="T.search">
205
+ </label>
206
+
207
+ <p v-if="!members.length" class="rm-share__empty">{{ T.empty }}</p>
208
+ <p v-else-if="!matches.length" class="rm-share__empty">{{ T.noMatch }}</p>
209
+
210
+ <ul v-else class="rm-share__list">
211
+ <li v-for="member in matches" :key="member.userId">
212
+ <label class="rm-share__member">
213
+ <input
214
+ type="checkbox"
215
+ class="rm-share__check"
216
+ :checked="picked.includes(member.userId)"
217
+ :aria-label="member.name"
218
+ @change="toggle(member.userId)"
219
+ >
220
+ <RmIcon
221
+ :name="picked.includes(member.userId) ? 'CheckSquare' : 'Square'"
222
+ :weight="picked.includes(member.userId) ? 'fill' : 'regular'"
223
+ :size="18"
224
+ />
225
+ <span class="rm-share__member-text">
226
+ <span class="rm-share__member-name">{{ member.name }}</span>
227
+ <span v-if="member.email" class="rm-share__member-mail">{{ member.email }}</span>
228
+ </span>
229
+ </label>
230
+ </li>
231
+ </ul>
232
+
233
+ <p v-if="picked.length" class="rm-share__count">{{ T.count(picked.length) }}</p>
234
+ </div>
235
+
236
+ <footer class="rm-share__foot">
237
+ <RmButton variant="ghost" size="sm" @click="close">
238
+ {{ T.cancel }}
239
+ </RmButton>
240
+ <RmButton variant="primary" size="sm" :disabled="loading" @click="save">
241
+ {{ T.save }}
242
+ </RmButton>
243
+ </footer>
244
+ </div>
245
+ </RmModal>
246
+ </template>
247
+
248
+ <style scoped>
249
+ .rm-share{gap:var(--space-16);padding:var(--space-4)}.rm-share,.rm-share__head{display:flex;flex-direction:column}.rm-share__head{gap:var(--space-4)}.rm-share__title{color:var(--color-primary);font-size:var(--font-16);font-weight:var(--weight-bold)}.rm-share__subject{color:var(--color-secondary);font-size:var(--font-13)}.rm-share__scopes{display:flex;flex-direction:column;gap:var(--space-8);list-style:none}.rm-share__scope{align-items:flex-start;background:var(--bg-surface);border:1px solid var(--border-soft);border-radius:var(--radius-lg);color:var(--color-primary);cursor:pointer;display:flex;gap:var(--space-10);padding:var(--space-10) var(--space-12);text-align:left;width:100%}.rm-share__scope:hover{background:var(--bg-hover)}.rm-share__check,.rm-share__radio{border:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);white-space:nowrap}.rm-share__scope:has(.rm-share__radio:focus-visible){outline:2px solid var(--color-secondary);outline-offset:2px}.rm-share__scope.is-on{background:var(--bg-hover);border-color:var(--color-secondary)}.rm-share__scope-text{display:flex;flex:1;flex-direction:column;gap:2px}.rm-share__scope-label{font-size:var(--font-13);font-weight:var(--weight-medium)}.rm-share__scope-hint{font-size:var(--font-12)}.rm-share__scope-hint,.rm-share__scope-mark{color:var(--color-secondary)}.rm-share__people{display:flex;flex-direction:column;gap:var(--space-8)}.rm-share__search{align-items:center;background:var(--bg-surface);border:1px solid var(--border-soft);border-radius:var(--radius-pill);color:var(--color-secondary);display:flex;gap:var(--space-8);padding:var(--space-6) var(--space-10)}.rm-share__search input{background:none;border:none;color:var(--color-primary);flex:1;font-size:var(--font-13);outline:none}.rm-share__list{display:flex;flex-direction:column;gap:2px;list-style:none;max-height:220px;overflow-y:auto}.rm-share__member{align-items:center;border-radius:var(--radius-md);cursor:pointer;display:flex;gap:var(--space-8);padding:var(--space-6) var(--space-8)}.rm-share__member:hover{background:var(--bg-hover)}.rm-share__member:has(.rm-share__check:focus-visible){outline:2px solid var(--color-secondary);outline-offset:2px}.rm-share__member-text{display:flex;flex-direction:column}.rm-share__member-name{color:var(--color-primary);font-size:var(--font-13)}.rm-share__member-mail{color:var(--color-secondary);font-size:var(--font-11)}.rm-share__count,.rm-share__empty{color:var(--color-secondary);font-size:var(--font-12)}.rm-share__foot{display:flex;gap:var(--space-8);justify-content:flex-end}
250
+ </style>
@@ -94,6 +94,7 @@ export declare function rmBaseOption(theme: RmChartTheme, decal?: boolean): {
94
94
  borderWidth: number;
95
95
  };
96
96
  };
97
+ export declare const rmTooltipTrigger: (count: number) => "axis" | "item";
97
98
  export declare function rmLegend(theme: RmChartTheme, count: number, position?: 'top' | 'bottom'): {
98
99
  pageIconSize?: number | undefined;
99
100
  pageIconColor?: string | undefined;
@@ -177,6 +178,17 @@ export interface RmSeriesInput {
177
178
  }
178
179
  /** Bar chart vertical, une ou plusieurs series, empilable. */
179
180
  export declare function rmBarVerticalOption(categories: (string | number)[], series: RmSeriesInput[], theme: RmChartTheme, palette: string[], decal?: boolean): {
181
+ tooltip: {
182
+ trigger: string;
183
+ backgroundColor: string;
184
+ textStyle: {
185
+ color: string;
186
+ fontFamily: string;
187
+ fontSize: number;
188
+ };
189
+ borderColor: string;
190
+ borderWidth: number;
191
+ };
180
192
  legend: {
181
193
  pageIconSize?: number | undefined;
182
194
  pageIconColor?: string | undefined;
@@ -282,17 +294,6 @@ export declare function rmBarVerticalOption(categories: (string | number)[], ser
282
294
  textStyle: {
283
295
  fontFamily: string;
284
296
  };
285
- tooltip: {
286
- trigger: string;
287
- backgroundColor: string;
288
- textStyle: {
289
- color: string;
290
- fontFamily: string;
291
- fontSize: number;
292
- };
293
- borderColor: string;
294
- borderWidth: number;
295
- };
296
297
  };
297
298
  /** Bar chart horizontal, avec valeur affichee en bout de barre. */
298
299
  export declare function rmBarHorizontalOption(labels: string[], values: (number | null)[], theme: RmChartTheme, palette: string[], decal?: boolean, formatter?: (value: number) => string): {
@@ -422,17 +423,7 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
422
423
  valueFormatter?: (value: number) => string;
423
424
  }): {
424
425
  tooltip: {
425
- trigger: string;
426
- backgroundColor: string;
427
- textStyle: {
428
- color: string;
429
- fontFamily: string;
430
- fontSize: number;
431
- };
432
- borderColor: string;
433
- borderWidth: number;
434
- } | {
435
- valueFormatter: (value: unknown) => string;
426
+ valueFormatter?: ((value: unknown) => string) | undefined;
436
427
  trigger: string;
437
428
  backgroundColor: string;
438
429
  textStyle: {
@@ -90,6 +90,8 @@ export function rmBaseOption(theme, decal = false) {
90
90
  }
91
91
  };
92
92
  }
93
+ const TOOLTIP_AXIS_MIN_SERIES = 10;
94
+ export const rmTooltipTrigger = (count) => count < TOOLTIP_AXIS_MIN_SERIES ? "item" : "axis";
93
95
  const LEGEND_MAX_INLINE = 6;
94
96
  const LEGEND_NAME_MAX = 38;
95
97
  const LEGEND_NAME_TAIL = 12;
@@ -141,6 +143,7 @@ const axisCommon = (theme) => ({
141
143
  export function rmBarVerticalOption(categories, series, theme, palette, decal = false) {
142
144
  return {
143
145
  ...rmBaseOption(theme, decal),
146
+ tooltip: { ...rmBaseOption(theme, decal).tooltip, trigger: rmTooltipTrigger(series.length) },
144
147
  legend: series.length > 1 ? rmLegend(theme, series.length) : void 0,
145
148
  // La legende passee sous le graphe prend sa place dans la gouttiere basse,
146
149
  // sinon elle recouvre les libelles de l'axe des abscisses.
@@ -205,10 +208,11 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
205
208
  const base = rmBaseOption(theme, decal);
206
209
  return {
207
210
  ...base,
208
- tooltip: valueFormatter ? {
211
+ tooltip: {
209
212
  ...base.tooltip,
210
- valueFormatter: (value) => typeof value === "number" ? format(value) : ""
211
- } : base.tooltip,
213
+ trigger: rmTooltipTrigger(series.length),
214
+ ...valueFormatter ? { valueFormatter: (value) => typeof value === "number" ? format(value) : "" } : {}
215
+ },
212
216
  // Legende obligatoire en double axe : sans elle, rien ne dit quelle courbe
213
217
  // se lit sur quel axe.
214
218
  legend: series.length > 1 || dual ? rmLegend(theme, series.length, legendPosition) : void 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.140",
3
+ "version": "0.1.142",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",