@tekkare/romulus 0.1.65 → 0.1.67

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.64",
7
+ "version": "0.1.66",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -0,0 +1,229 @@
1
+ <!--
2
+ RmAccordionMenu - menu de barre laterale en sections depliables.
3
+
4
+ Reprend `arg-accordion-menu` d'Auriga a l'identique : sections repliables,
5
+ sous-sections sur fond voile, pastille de donnee a droite des entrees, et
6
+ `singleOpen` qui referme les autres sections quand on en ouvre une.
7
+
8
+ Trois choses changent. Les en-tetes de section sont des `<button
9
+ aria-expanded>` : les `<div @click>` d'Auriga n'etaient ni atteignables au
10
+ clavier ni annonces comme repliables.
11
+
12
+ La pastille de donnee porte un intitule lu a voix haute. En couleur seule,
13
+ elle ne disait rien a qui ne distingue pas le vert du gris.
14
+
15
+ Les couleurs en dur (`#ececf2`, `#1a1a2e`, `#2017ad`) passent aux tokens.
16
+ -->
17
+ <script setup lang="ts">
18
+ import { reactive, watch } from 'vue'
19
+
20
+ export interface RmAccordionItem {
21
+ label: string
22
+ /** Icone Phosphor sans le prefixe "Ph". */
23
+ icon?: string
24
+ /** Cible de navigation. Sans elle, l'entree emet `navigate` et rien d'autre. */
25
+ to?: string
26
+ /**
27
+ * Pastille a droite : verte si la page porte des donnees, grise sinon.
28
+ * Absente, aucune pastille.
29
+ */
30
+ haveData?: boolean
31
+ }
32
+
33
+ export interface RmAccordionSubSection {
34
+ label: string
35
+ items: RmAccordionItem[]
36
+ }
37
+
38
+ export interface RmAccordionSection {
39
+ title: string
40
+ items?: RmAccordionItem[]
41
+ subSections?: RmAccordionSubSection[]
42
+ /** Section ouverte au montage. Sans precision, seule la premiere l'est. */
43
+ defaultOpen?: boolean
44
+ }
45
+
46
+ const props = withDefaults(defineProps<{
47
+ menu: RmAccordionSection[]
48
+ /** Ouvrir une section referme les autres. */
49
+ singleOpen?: boolean
50
+ /** Intitules des pastilles de donnee, lus a voix haute. */
51
+ dataLabel?: string
52
+ noDataLabel?: string
53
+ }>(), {
54
+ singleOpen: true,
55
+ dataLabel: 'Données disponibles',
56
+ noDataLabel: 'Aucune donnée',
57
+ })
58
+
59
+ const emit = defineEmits<{ navigate: [item: RmAccordionItem] }>()
60
+
61
+ const openSections = reactive<Record<number, boolean>>({})
62
+ const openSubSections = reactive<Record<string, boolean>>({})
63
+
64
+ /**
65
+ * Les sous-sections partent ouvertes : elles ne portent que quelques entrees,
66
+ * les replier ferait un second geste pour rien.
67
+ */
68
+ function initSections(menu: RmAccordionSection[]) {
69
+ for (const key of Object.keys(openSections)) delete openSections[Number(key)]
70
+ for (const key of Object.keys(openSubSections)) delete openSubSections[key]
71
+
72
+ menu.forEach((section, index) => {
73
+ openSections[index] = section.defaultOpen ?? index === 0
74
+ section.subSections?.forEach((_sub, subIndex) => {
75
+ openSubSections[`${index}-${subIndex}`] = true
76
+ })
77
+ })
78
+ }
79
+
80
+ initSections(props.menu)
81
+ watch(() => props.menu, menu => initSections(menu), { deep: true })
82
+
83
+ function toggleSection(index: number) {
84
+ if (!props.singleOpen) {
85
+ openSections[index] = !openSections[index]
86
+ return
87
+ }
88
+ const wasOpen = openSections[index]
89
+ for (const key of Object.keys(openSections)) openSections[Number(key)] = false
90
+ openSections[index] = !wasOpen
91
+ }
92
+
93
+ function toggleSub(sectionIndex: number, subIndex: number) {
94
+ const key = `${sectionIndex}-${subIndex}`
95
+ openSubSections[key] = !openSubSections[key]
96
+ }
97
+ </script>
98
+
99
+ <template>
100
+ <div class="rm-accordion">
101
+ <div v-for="(section, s) in menu" :key="s" class="rm-accordion__section">
102
+ <button
103
+ type="button"
104
+ class="rm-accordion__head"
105
+ :aria-expanded="!!openSections[s]"
106
+ @click="toggleSection(s)"
107
+ >
108
+ <span class="rm-accordion__title">{{ section.title }}</span>
109
+ <span class="rm-accordion__caret" :class="{ 'is-open': openSections[s] }">
110
+ <RmIcon name="CaretDown" :size="16" />
111
+ </span>
112
+ </button>
113
+
114
+ <div class="rm-accordion__body" :class="{ 'is-open': openSections[s] }">
115
+ <!-- Entrees posees directement sous la section -->
116
+ <template v-for="(item, i) in section.items" :key="`i-${i}`">
117
+ <NuxtLink
118
+ v-if="item.to"
119
+ :to="item.to"
120
+ class="rm-accordion__item"
121
+ active-class="is-active"
122
+ @click="emit('navigate', item)"
123
+ >
124
+ <span class="rm-accordion__item-left">
125
+ <RmIcon v-if="item.icon" :name="item.icon" :size="20" />
126
+ <span>{{ item.label }}</span>
127
+ </span>
128
+ <span
129
+ v-if="item.haveData !== undefined"
130
+ class="rm-accordion__dot"
131
+ :class="item.haveData ? 'has-data' : 'no-data'"
132
+ :title="item.haveData ? dataLabel : noDataLabel"
133
+ :aria-label="item.haveData ? dataLabel : noDataLabel"
134
+ role="img"
135
+ />
136
+ </NuxtLink>
137
+ <button
138
+ v-else
139
+ type="button"
140
+ class="rm-accordion__item"
141
+ @click="emit('navigate', item)"
142
+ >
143
+ <span class="rm-accordion__item-left">
144
+ <RmIcon v-if="item.icon" :name="item.icon" :size="20" />
145
+ <span>{{ item.label }}</span>
146
+ </span>
147
+ <span
148
+ v-if="item.haveData !== undefined"
149
+ class="rm-accordion__dot"
150
+ :class="item.haveData ? 'has-data' : 'no-data'"
151
+ :title="item.haveData ? dataLabel : noDataLabel"
152
+ :aria-label="item.haveData ? dataLabel : noDataLabel"
153
+ role="img"
154
+ />
155
+ </button>
156
+ </template>
157
+
158
+ <!-- Sous-sections -->
159
+ <div v-for="(sub, sb) in section.subSections" :key="`s-${sb}`" class="rm-accordion__sub">
160
+ <button
161
+ type="button"
162
+ class="rm-accordion__sub-head"
163
+ :aria-expanded="!!openSubSections[`${s}-${sb}`]"
164
+ @click="toggleSub(s, sb)"
165
+ >
166
+ <span class="rm-accordion__sub-label">{{ sub.label }}</span>
167
+ <span
168
+ class="rm-accordion__caret rm-accordion__caret--sub"
169
+ :class="{ 'is-open': openSubSections[`${s}-${sb}`] }"
170
+ >
171
+ <RmIcon name="CaretDown" :size="12" />
172
+ </span>
173
+ </button>
174
+
175
+ <div
176
+ class="rm-accordion__sub-body"
177
+ :class="{ 'is-open': openSubSections[`${s}-${sb}`] }"
178
+ >
179
+ <template v-for="(item, i) in sub.items" :key="`si-${i}`">
180
+ <NuxtLink
181
+ v-if="item.to"
182
+ :to="item.to"
183
+ class="rm-accordion__item"
184
+ active-class="is-active"
185
+ @click="emit('navigate', item)"
186
+ >
187
+ <span class="rm-accordion__item-left">
188
+ <RmIcon v-if="item.icon" :name="item.icon" :size="20" />
189
+ <span>{{ item.label }}</span>
190
+ </span>
191
+ <span
192
+ v-if="item.haveData !== undefined"
193
+ class="rm-accordion__dot"
194
+ :class="item.haveData ? 'has-data' : 'no-data'"
195
+ :title="item.haveData ? dataLabel : noDataLabel"
196
+ :aria-label="item.haveData ? dataLabel : noDataLabel"
197
+ role="img"
198
+ />
199
+ </NuxtLink>
200
+ <button
201
+ v-else
202
+ type="button"
203
+ class="rm-accordion__item"
204
+ @click="emit('navigate', item)"
205
+ >
206
+ <span class="rm-accordion__item-left">
207
+ <RmIcon v-if="item.icon" :name="item.icon" :size="20" />
208
+ <span>{{ item.label }}</span>
209
+ </span>
210
+ <span
211
+ v-if="item.haveData !== undefined"
212
+ class="rm-accordion__dot"
213
+ :class="item.haveData ? 'has-data' : 'no-data'"
214
+ :title="item.haveData ? dataLabel : noDataLabel"
215
+ :aria-label="item.haveData ? dataLabel : noDataLabel"
216
+ role="img"
217
+ />
218
+ </button>
219
+ </template>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ </template>
226
+
227
+ <style scoped>
228
+ .rm-accordion{display:flex;flex-direction:column;width:100%}.rm-accordion__section{border-bottom:1px solid var(--border-default);margin-bottom:4px;padding-bottom:8px}.rm-accordion__section:last-child{border-bottom:none}.rm-accordion__head,.rm-accordion__sub-head{align-items:center;background:none;border:none;border-radius:var(--radius-md,8px);cursor:pointer;display:flex;font-family:inherit;justify-content:space-between;transition:background-color .2s ease;width:100%}.rm-accordion__head{padding:8px}.rm-accordion__head:hover,.rm-accordion__sub-head:hover{background:var(--bg-hover)}.rm-accordion__title{color:var(--text-primary);font-size:var(--font-13,13px);font-weight:700;line-height:normal}.rm-accordion__caret{align-items:center;color:var(--text-muted);display:flex;transition:transform .2s ease}.rm-accordion__caret.is-open{transform:rotate(180deg)}.rm-accordion__body{max-height:0;overflow:hidden;padding-left:12px;padding-right:8px;transition:max-height .3s ease}.rm-accordion__body.is-open{margin-top:4px;max-height:2000px}.rm-accordion__item{align-items:center;align-self:stretch;background:none;border:none;border-radius:var(--radius-sm,4px);color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-14,14px);font-weight:400;gap:8px;justify-content:space-between;line-height:1.6;padding:8px;text-align:left;text-decoration:none;width:100%}.rm-accordion__item:hover:not(.is-active){background:var(--bg-hover)}.rm-accordion__item.is-active{background:rgba(var(--brand-rgb),.1)}.rm-accordion__item :deep(svg){color:var(--text-secondary)}.rm-accordion__item.is-active :deep(svg),.rm-accordion__item:hover:not(.is-active) :deep(svg){color:var(--color-primary)}.rm-accordion__item-left{align-items:center;display:flex;gap:8px}.rm-accordion__dot{border-radius:50%;flex-shrink:0;height:10px;width:10px}.rm-accordion__dot.has-data{background:var(--color-success)}.rm-accordion__dot.no-data{background:var(--r-color-gray-300)}.rm-accordion__sub{margin-bottom:8px}.rm-accordion__sub-head{padding:4px 8px}.rm-accordion__sub-label{color:var(--text-secondary);font-size:var(--font-12,12px);font-weight:700}.rm-accordion__sub-body{background:var(--bg-surface-2);border-radius:var(--radius-md,8px);margin-top:4px;max-height:0;overflow:hidden;padding:0 8px;transition:max-height .2s ease,padding .2s ease}.rm-accordion__sub-body.is-open{max-height:300px;padding:8px}.rm-accordion__head:focus-visible,.rm-accordion__item:focus-visible,.rm-accordion__sub-head:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px}
229
+ </style>
@@ -0,0 +1,71 @@
1
+ <!--
2
+ RmFilterTabs - rail d'onglets pose sur un fond voile, un seul actif.
3
+
4
+ Reprend `arg-filter-tabs` d'Auriga a l'identique : pastille blanche ombree
5
+ sur l'onglet actif, fond voile pour le rail.
6
+
7
+ Ce n'est pas `RmSegmented`, qui sert une echelle ordonnee (SMR, phases) et
8
+ colle ses segments : ici les onglets sont des vues qui n'ont pas d'ordre, et
9
+ la pastille se pose sous celui qu'on lit.
10
+
11
+ Deux choses changent. Les onglets sont des `<button>` dans un
12
+ `role="tablist"` : les `<div @click>` d'Auriga n'etaient ni atteignables au
13
+ clavier ni annonces comme des onglets.
14
+
15
+ L'onglet actif est un `v-model` plutot qu'un etat interne synchronise depuis
16
+ `index_menu` au montage : la valeur d'Auriga ne suivait plus la prop des que
17
+ la page la changeait elle-meme, par exemple au retour d'une route.
18
+ -->
19
+ <script setup lang="ts">
20
+ import { computed } from 'vue'
21
+
22
+ export interface RmFilterTab {
23
+ /** Intitule affiche. */
24
+ name: string
25
+ /** Valeur portee par le modele. Absente, c'est l'index. */
26
+ value?: string | number
27
+ }
28
+
29
+ const props = withDefaults(defineProps<{
30
+ tabs: RmFilterTab[]
31
+ /** Intitule du rail pour les technologies d'assistance. */
32
+ ariaLabel?: string
33
+ }>(), {
34
+ ariaLabel: undefined,
35
+ })
36
+
37
+ /** Valeur de l'onglet actif : sa `value`, ou son index si elle manque. */
38
+ const selected = defineModel<string | number>({ default: 0 })
39
+
40
+ function valueOf(tab: RmFilterTab, index: number) {
41
+ return tab.value ?? index
42
+ }
43
+
44
+ const activeIndex = computed(() => {
45
+ const found = props.tabs.findIndex((tab, i) => valueOf(tab, i) === selected.value)
46
+ // Une valeur inconnue ne doit pas laisser le rail sans onglet actif : un
47
+ // rail ou rien n'est pose se lit comme un chargement en cours.
48
+ return found === -1 ? 0 : found
49
+ })
50
+ </script>
51
+
52
+ <template>
53
+ <div class="rm-filter-tabs" role="tablist" :aria-label="ariaLabel">
54
+ <button
55
+ v-for="(tab, index) in tabs"
56
+ :key="index"
57
+ type="button"
58
+ role="tab"
59
+ class="rm-filter-tabs__tab"
60
+ :class="{ 'is-active': index === activeIndex }"
61
+ :aria-selected="index === activeIndex"
62
+ @click="selected = valueOf(tab, index)"
63
+ >
64
+ {{ tab.name }}
65
+ </button>
66
+ </div>
67
+ </template>
68
+
69
+ <style scoped>
70
+ .rm-filter-tabs{align-items:center;background:var(--bg-surface-2);border-radius:var(--radius-xl,12px);display:flex;padding:4px;width:-moz-fit-content;width:fit-content}.rm-filter-tabs__tab{align-items:center;background:none;border:none;border-radius:var(--radius-md,8px);color:var(--text-secondary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-14,14px);font-weight:700;gap:6px;justify-content:center;line-height:normal;padding:8px 24px}.rm-filter-tabs__tab.is-active{background:var(--bg-surface);box-shadow:0 0 8px 0 rgba(var(--shadow-rgb,62 63 94),.12);color:var(--text-primary)}.rm-filter-tabs__tab:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}
71
+ </style>
@@ -1,103 +1,83 @@
1
1
  <!--
2
- RmLangSelect - choix de la langue, drapeau puis nom.
2
+ RmLangSelect - bascule de langue, un drapeau par langue dans une capsule.
3
3
 
4
- Reprend `arg-lang-select` d'Auriga : la langue courante s'affiche, le clic
5
- deroule les autres. Trois choses changent.
4
+ Meme dessin que la bascule du rail (`.rm-lang` de shell.css) : les langues
5
+ sont toutes posees, celle qui vaut est en couleur, les autres en gris, et un
6
+ pouce blanc glisse sous la langue active. Rien ne se deroule : a deux ou trois
7
+ langues, un menu demande deux gestes la ou la bascule en demande un, et cache
8
+ la moitie du choix entre les deux.
6
9
 
7
- C'est un `<button aria-expanded>` suivi d'une liste `role="listbox"`, et
8
- Echap referme. Auriga posait un `<div @click>` sans role ni etat, injoignable
9
- au clavier.
10
+ Les drapeaux passent par `RmFlag`, pas par l'emoji : sous Windows, Segoe UI
11
+ Emoji ne ligature pas les indicateurs regionaux et Chrome affiche « FR » dans
12
+ deux carres.
10
13
 
11
- La langue courante est un `v-model` sur l'iso, plus une copie interne : une
12
- langue changee par la route ou par un autre menu se refletait mal.
13
-
14
- `collapsed` remplace le `sidebarOpen` a trois etats (`true`, `false`, `null`)
15
- d'Auriga, qui melangeait « barre repliee » et « pas dans une barre » dans la
16
- meme valeur.
14
+ Le gris n'est pas le seul signal : la langue active porte `aria-pressed` et
15
+ son pouce, si bien qu'elle se repere sans distinguer les couleurs.
17
16
  -->
18
17
  <script setup lang="ts">
19
- import { computed, ref, onMounted, onUnmounted } from 'vue'
18
+ import { computed } from 'vue'
20
19
 
21
20
  export interface RmLang {
22
- /** Code ISO du pays, pour le drapeau (`fr`, `gb`...). */
23
- iso: string
24
- /** Nom affiche. */
25
- title: string
21
+ /** Code de langue, ce que porte le modele (`fr`, `en`). */
22
+ code: string
23
+ /** Code ISO 3166-1 alpha-2 du drapeau (`fr`, `gb`). Sans lui, `code` sert. */
24
+ iso?: string
25
+ /** Nom de la langue, lu a voix haute et pose en infobulle. */
26
+ label: string
26
27
  }
27
28
 
28
29
  const props = withDefaults(defineProps<{
29
30
  langs: RmLang[]
30
- /** Barre laterale repliee : le drapeau seul, sans le nom. */
31
- collapsed?: boolean
32
- /** Intitule du bouton pour les technologies d'assistance. */
31
+ /** Cote a cote plutot qu'empile, pour un pied de page ou une barre. */
32
+ horizontal?: boolean
33
+ /** Intitule du groupe pour les technologies d'assistance. */
33
34
  label?: string
34
35
  }>(), {
35
- collapsed: false,
36
+ horizontal: false,
36
37
  label: 'Langue',
37
38
  })
38
39
 
39
40
  const selected = defineModel<string>({ default: '' })
40
41
 
41
- const isOpen = ref(false)
42
- const wrapperRef = ref<HTMLElement>()
43
-
44
- const current = computed(() => props.langs.find(lang => lang.iso === selected.value))
45
- const others = computed(() => props.langs.filter(lang => lang.iso !== selected.value))
46
-
47
- function pick(lang: RmLang) {
48
- selected.value = lang.iso
49
- isOpen.value = false
50
- }
51
-
52
- function onClickOutside(event: Event) {
53
- if (wrapperRef.value && !wrapperRef.value.contains(event.target as Node)) isOpen.value = false
54
- }
42
+ /** Rang de la langue active : c'est lui qui place le pouce. */
43
+ const activeIndex = computed(() =>
44
+ Math.max(0, props.langs.findIndex(lang => lang.code === selected.value)),
45
+ )
55
46
 
56
- function onKeydown(event: KeyboardEvent) {
57
- if (event.key === 'Escape' && isOpen.value) isOpen.value = false
58
- }
47
+ /** 32px de pastille plus 2px de gouttiere. */
48
+ const STEP = 34
59
49
 
60
- onMounted(() => {
61
- document.addEventListener('click', onClickOutside)
62
- document.addEventListener('keydown', onKeydown)
63
- })
64
- onUnmounted(() => {
65
- document.removeEventListener('click', onClickOutside)
66
- document.removeEventListener('keydown', onKeydown)
67
- })
50
+ const thumbStyle = computed(() => ({
51
+ transform: props.horizontal
52
+ ? `translateX(${activeIndex.value * STEP}px)`
53
+ : `translateY(${activeIndex.value * STEP}px)`,
54
+ }))
68
55
  </script>
69
56
 
70
57
  <template>
71
- <div v-if="current" ref="wrapperRef" class="rm-lang">
58
+ <div
59
+ class="rm-lang"
60
+ :class="{ 'rm-lang--horizontal': horizontal }"
61
+ role="group"
62
+ :aria-label="label"
63
+ >
64
+ <span class="rm-lang__thumb" :style="thumbStyle" aria-hidden="true" />
72
65
  <button
66
+ v-for="lang in langs"
67
+ :key="lang.code"
73
68
  type="button"
74
- class="rm-lang__current"
75
- :aria-label="label"
76
- :aria-expanded="isOpen"
77
- @click="isOpen = !isOpen"
69
+ class="rm-lang__option"
70
+ :class="{ 'rm-lang__option--active': lang.code === selected }"
71
+ :title="lang.label"
72
+ :aria-label="lang.label"
73
+ :aria-pressed="lang.code === selected"
74
+ @click="selected = lang.code"
78
75
  >
79
- <RmFlag :code="current.iso" :height="18" :title="current.title" />
80
- <span v-if="!collapsed" class="rm-lang__name">{{ current.title }}</span>
81
- <RmIcon
82
- v-if="!collapsed"
83
- name="CaretDown"
84
- :size="14"
85
- class="rm-lang__caret"
86
- :class="{ 'is-open': isOpen }"
87
- />
76
+ <RmFlag :code="lang.iso || lang.code" :height="18" />
88
77
  </button>
89
-
90
- <ul v-if="isOpen && others.length > 0" class="rm-lang__list" role="listbox" :aria-label="label">
91
- <li v-for="lang in others" :key="lang.iso" role="option" :aria-selected="false">
92
- <button type="button" class="rm-lang__option" @click="pick(lang)">
93
- <RmFlag :code="lang.iso" :height="18" :title="lang.title" />
94
- <span v-if="!collapsed">{{ lang.title }}</span>
95
- </button>
96
- </li>
97
- </ul>
98
78
  </div>
99
79
  </template>
100
80
 
101
81
  <style scoped>
102
- .rm-lang{position:relative}.rm-lang__current,.rm-lang__option{align-items:center;background:none;border:none;border-radius:var(--radius-md,8px);color:var(--text-primary);cursor:pointer;display:flex;font-size:var(--font-14,14px);gap:8px;padding:6px 8px;width:100%}.rm-lang__current:hover,.rm-lang__option:hover{background:var(--bg-hover)}.rm-lang__name{flex:1;text-align:left}.rm-lang__caret{color:var(--text-muted);transition:transform var(--duration-fast,.15s) var(--ease-in-out,ease)}.rm-lang__caret.is-open{transform:rotate(180deg)}.rm-lang__list{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md,8px);bottom:calc(100% + 4px);box-shadow:0 4px 16px rgba(var(--shadow-rgb),.1);left:0;list-style:none;margin:0;min-width:100%;padding:4px;position:absolute;z-index:50}
82
+ .rm-lang{background:var(--rm-shell-subtle,var(--bg-surface-2));border:1px solid var(--rm-shell-accent,var(--color-primary));border-radius:9999px;display:grid;gap:2px;grid-template-rows:repeat(v-bind("langs.length"),1fr);padding:3px;position:relative;width:-moz-fit-content;width:fit-content}.rm-lang--horizontal{grid-auto-flow:column;grid-template-rows:1fr}.rm-lang__thumb{background:var(--rm-shell-surface,var(--bg-surface));border-radius:9999px;box-shadow:0 1px 4px rgba(var(--shadow-rgb,30 41 59),.16);height:32px;left:3px;position:absolute;top:3px;transition:transform .26s cubic-bezier(.34,1.56,.64,1);width:32px}.rm-lang__option{align-items:center;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:none;border-radius:9999px;box-shadow:none;cursor:pointer;display:flex;filter:grayscale(1);height:32px;justify-content:center;margin:0;opacity:.5;padding:0;position:relative;transition:filter .18s ease,opacity .18s ease;width:32px}.rm-lang__option:hover{filter:grayscale(0);opacity:.85}.rm-lang__option--active{filter:grayscale(0);opacity:1}.rm-lang__option:focus-visible{outline:2px solid var(--rm-shell-accent,var(--color-primary));outline-offset:2px}@media (prefers-reduced-motion:reduce){.rm-lang__option,.rm-lang__thumb{transition:none}}
103
83
  </style>
@@ -0,0 +1,71 @@
1
+ <!--
2
+ RmProductCard - banniere de produit, titre et texte sur une image de fond.
3
+
4
+ Reprend `arg-product-card` d'Auriga : le fond est voile d'un degrade blanc a
5
+ gauche pour que le texte reste lisible, la teinte du titre suit le produit, et
6
+ le bord porte la meme teinte a 10 %.
7
+
8
+ Deux choses changent.
9
+
10
+ L'image de fond est une **prop**, plus trois PNG embarques dans le paquet.
11
+ Ceux d'Auriga pesaient nean de 2 a 4,6 Mo piece : un paquet de composants qui
12
+ transporte neuf megaoctets d'images les fait telecharger a toutes les apps,
13
+ y compris celles qui n'affichent jamais cette banniere.
14
+
15
+ Les titres et textes par defaut sautent. Auriga en portait trois jeux ecrits
16
+ en dur, dont un faux texte (« Spider totalus peruvian-night-powder »), qui
17
+ n'ont rien a faire dans un systeme de design : le contenu vient de l'appelant,
18
+ par prop ou par slot.
19
+ -->
20
+ <script setup lang="ts">
21
+ withDefaults(defineProps<{
22
+ /** Teinte du titre et du bord. */
23
+ accent?: 'healthcare' | 'discovery' | 'databank' | 'pharma'
24
+ /** Image de fond, voilee d'un degrade blanc. Absente, fond uni. */
25
+ background?: string
26
+ /** Titre, si le slot `title` n'est pas fourni. */
27
+ title?: string
28
+ /** Texte, si le slot `text` n'est pas fourni. */
29
+ text?: string
30
+ /**
31
+ * Pose le contenu sur un panneau translucide flou plutot qu'a meme le fond.
32
+ * A utiliser quand l'image est chargee : le texte y tient sans que le
33
+ * degrade ait a manger la moitie de l'image.
34
+ */
35
+ overlay?: boolean
36
+ }>(), {
37
+ accent: 'healthcare',
38
+ background: '',
39
+ title: '',
40
+ text: '',
41
+ overlay: false,
42
+ })
43
+ </script>
44
+
45
+ <template>
46
+ <div
47
+ class="rm-product-card"
48
+ :class="[`rm-product-card--${accent}`, { 'rm-product-card--padded': !overlay }]"
49
+ :style="background
50
+ ? { backgroundImage: `linear-gradient(90deg, rgba(255,255,255,.9) 41%, rgba(255,255,255,0) 100%), url('${background}')` }
51
+ : undefined"
52
+ >
53
+ <div class="rm-product-card__inner" :class="{ 'rm-product-card__inner--overlay': overlay }">
54
+ <div class="rm-product-card__title">
55
+ <slot name="title">{{ title }}</slot>
56
+ </div>
57
+
58
+ <div class="rm-product-card__text">
59
+ <slot name="text">{{ text }}</slot>
60
+ </div>
61
+
62
+ <div v-if="$slots.actions" class="rm-product-card__actions">
63
+ <slot name="actions" />
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </template>
68
+
69
+ <style scoped>
70
+ .rm-product-card{align-self:stretch;background-color:var(--bg-surface);background-position:50%;background-repeat:no-repeat;background-size:cover;border-radius:var(--radius-xl,12px);box-shadow:0 6px 8px 0 rgba(var(--shadow-rgb,30 41 59),.1);display:flex;flex-direction:column;position:relative}.rm-product-card--padded{padding:40px}.rm-product-card--healthcare{border:1px solid rgba(var(--healthcare-rgb),.1)}.rm-product-card--discovery{border:1px solid rgba(var(--discovery-rgb),.1)}.rm-product-card--databank{border:1px solid rgba(var(--databank-rgb),.1)}.rm-product-card--pharma{border:1px solid rgba(var(--pharma-rgb),.1)}.rm-product-card__inner{align-items:flex-start;align-self:stretch;display:flex;flex-direction:column;gap:24px}.rm-product-card__inner--overlay{backdrop-filter:blur(5px);background:hsla(0,0%,100%,.7);border-radius:var(--radius-md,8px);gap:16px;margin:24px;padding:24px;width:calc(100% - 48px)}.rm-product-card__title{font-size:26px;font-weight:900;line-height:normal}.rm-product-card--healthcare .rm-product-card__title{color:var(--color-primary)}.rm-product-card--discovery .rm-product-card__title{color:var(--color-discovery)}.rm-product-card--databank .rm-product-card__title{color:var(--color-databank)}.rm-product-card--pharma .rm-product-card__title{color:var(--color-pharma)}.rm-product-card__text{color:var(--text-primary);max-width:550px}.rm-product-card__actions{align-items:center;display:flex;gap:16px}
71
+ </style>
@@ -0,0 +1,57 @@
1
+ <!--
2
+ RmSectionCard - carte a bandeau, en tete ou en pied.
3
+
4
+ Reprend `arg-section-card` d'Auriga a l'identique : un bandeau voile colle au
5
+ corps blanc, en haut (`position="top"`) ou en bas (`position="bottom"`), et
6
+ les arrondis qui suivent le bandeau pour que la carte reste d'un bloc.
7
+
8
+ Ce n'est pas `RmCard`, qui est une surface nue : ici le bandeau est un
9
+ registre a part, plus petit et plus sourd que le corps, ou l'on pose un pays,
10
+ un statut, une date. Le corps garde le fond plein.
11
+
12
+ `action` marque la carte comme cliquable : le survol sort le bord de marque.
13
+ Sans lui, la carte ne bouge pas, et rien ne laisse croire qu'on peut cliquer.
14
+
15
+ Les couleurs en dur d'Auriga (`#dbdef0`, `#fdfdff`) passent aux tokens.
16
+ -->
17
+ <script setup lang="ts">
18
+ withDefaults(defineProps<{
19
+ /**
20
+ * Cote du bandeau. `top` rend le slot `header`, `bottom` le slot `footer`,
21
+ * `none` ne rend que le corps.
22
+ */
23
+ position?: 'top' | 'bottom' | 'none'
24
+ /** Carte cliquable : le bord se colore au survol. */
25
+ action?: boolean
26
+ }>(), {
27
+ position: 'top',
28
+ action: false,
29
+ })
30
+ </script>
31
+
32
+ <template>
33
+ <div class="rm-section-card" :class="{ 'rm-section-card--action': action }">
34
+ <div v-if="position === 'top'" class="rm-section-card__band rm-section-card__band--top">
35
+ <slot name="header" />
36
+ </div>
37
+
38
+ <div
39
+ class="rm-section-card__body"
40
+ :class="{
41
+ 'rm-section-card__body--under-top': position === 'top',
42
+ 'rm-section-card__body--over-bottom': position === 'bottom',
43
+ 'rm-section-card__body--alone': position === 'none',
44
+ }"
45
+ >
46
+ <slot />
47
+ </div>
48
+
49
+ <div v-if="position === 'bottom'" class="rm-section-card__band rm-section-card__band--bottom">
50
+ <slot name="footer" />
51
+ </div>
52
+ </div>
53
+ </template>
54
+
55
+ <style scoped>
56
+ .rm-section-card{align-items:flex-start;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl,12px);box-shadow:var(--r-shadow-sm);display:flex;flex-direction:column}.rm-section-card--action:hover{border-color:var(--color-primary);box-shadow:var(--r-shadow-md)}.rm-section-card__band{align-self:stretch;background:var(--bg-surface-2);color:var(--text-secondary);font-size:var(--font-12,12px);line-height:normal;padding:8px 16px}.rm-section-card__band--top{border-bottom:1px solid var(--border-default);border-top-left-radius:var(--radius-xl,12px);border-top-right-radius:var(--radius-xl,12px)}.rm-section-card__band--bottom{border-bottom-left-radius:var(--radius-xl,12px);border-bottom-right-radius:var(--radius-xl,12px);border-top:1px solid var(--border-default)}.rm-section-card__body{align-self:stretch;background:var(--bg-surface);padding:24px}.rm-section-card__body--under-top{border-bottom-left-radius:var(--radius-xl,12px);border-bottom-right-radius:var(--radius-xl,12px)}.rm-section-card__body--over-bottom{border-top-left-radius:var(--radius-xl,12px);border-top-right-radius:var(--radius-xl,12px)}.rm-section-card__body--alone{border-radius:var(--radius-xl,12px)}
57
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.65",
3
+ "version": "0.1.67",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",