@tekkare/romulus 0.1.56 → 0.1.58

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.55",
7
+ "version": "0.1.57",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -0,0 +1,69 @@
1
+ <!--
2
+ RmCheckbox - case a cocher encadree, avec son intitule.
3
+
4
+ Reprend `arg-checkbox` d'Auriga : meme boite bordee, meme icone carree qui
5
+ se remplit, meme bulle d'aide optionnelle a droite. Trois choses changent.
6
+
7
+ C'est un vrai `<input type="checkbox">` visuellement masque, dans un
8
+ `<label>`. Auriga posait un `<div tabindex="0">` avec un `@click` : la barre
9
+ d'espace ne cochait rien et le lecteur d'ecran n'annoncait ni role ni etat.
10
+
11
+ L'etat est un `v-model` plutot qu'une copie interne de `defaultSelect` :
12
+ une case dont le parent refuse le changement revenait, chez Auriga, dans
13
+ l'etat coche malgre le refus.
14
+
15
+ Le masque `paint(squircle)` saute. Il demandait un worklet CSS charge a la
16
+ main, ne marchait que sur Chrome, et un `border-radius` donne le meme angle.
17
+ -->
18
+ <script setup lang="ts">
19
+ withDefaults(defineProps<{
20
+ /** Intitule pose a cote de la case. */
21
+ label: string
22
+ /** Intitule du champ, au-dessus de la boite. Absent, rien ne sort. */
23
+ fieldLabel?: string
24
+ /** Bulle d'aide, sortie par une icone a droite. */
25
+ tooltip?: string
26
+ tooltipDir?: 'top' | 'right' | 'bottom' | 'left'
27
+ disabled?: boolean
28
+ }>(), {
29
+ fieldLabel: '',
30
+ tooltip: '',
31
+ tooltipDir: 'top',
32
+ disabled: false,
33
+ })
34
+
35
+ const checked = defineModel<boolean>({ default: false })
36
+ </script>
37
+
38
+ <template>
39
+ <div class="rm-checkbox">
40
+ <p v-if="fieldLabel" class="rm-checkbox__field-label">{{ fieldLabel }}</p>
41
+
42
+ <div class="rm-checkbox__wrap">
43
+ <label class="rm-checkbox__box" :class="{ 'is-disabled': disabled }">
44
+ <input
45
+ v-model="checked"
46
+ type="checkbox"
47
+ class="rm-checkbox__input"
48
+ :disabled="disabled"
49
+ >
50
+ <span class="rm-checkbox__icon" :class="{ 'is-checked': checked }">
51
+ <RmIcon
52
+ :name="checked ? 'CheckSquare' : 'Square'"
53
+ :weight="checked ? 'fill' : 'regular'"
54
+ :size="20"
55
+ />
56
+ </span>
57
+ <span class="rm-checkbox__label">{{ label }}</span>
58
+ </label>
59
+
60
+ <RmTooltip v-if="tooltip" :text="tooltip" :dir="tooltipDir" class="rm-checkbox__help">
61
+ <RmIcon name="Question" :size="16" />
62
+ </RmTooltip>
63
+ </div>
64
+ </div>
65
+ </template>
66
+
67
+ <style scoped>
68
+ .rm-checkbox__field-label{color:var(--text-secondary);font-size:var(--font-12,12px);font-weight:600;margin-bottom:6px}.rm-checkbox__wrap{position:relative}.rm-checkbox__box,.rm-checkbox__wrap{align-items:center;display:flex;gap:8px}.rm-checkbox__box{background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:var(--radius-md,8px);color:var(--text-primary);cursor:pointer;flex:1;font-size:var(--font-14,14px);font-weight:600;min-height:40px;padding:0 12px;transition:border-color var(--duration-fast,.15s) var(--ease-out,ease)}.rm-checkbox__box:hover:not(.is-disabled){border-color:var(--color-primary)}.rm-checkbox__box:focus-within{border-color:var(--color-primary);box-shadow:0 0 0 4px rgba(var(--brand-rgb),.1)}.rm-checkbox__box.is-disabled{color:var(--text-muted);cursor:not-allowed}.rm-checkbox__input{height:1px;opacity:0;pointer-events:none;position:absolute;width:1px}.rm-checkbox__icon{color:var(--text-muted);display:flex;flex-shrink:0}.rm-checkbox__icon.is-checked{color:var(--color-primary)}.rm-checkbox__box.is-disabled .rm-checkbox__icon{color:var(--border-default)}.rm-checkbox__label{min-width:0}.rm-checkbox__help{color:var(--text-muted)}
69
+ </style>
@@ -0,0 +1,27 @@
1
+ <!--
2
+ RmCircleAmount - compte pose a cote d'un intitule (« Filtres 3 »).
3
+
4
+ Reprend `arg-circle-amount` d'Auriga, meme pastille bordee. Ce n'est pas un
5
+ RmBadge : le badge nomme un statut et porte toujours du texte, celui-ci ne
6
+ porte qu'un nombre et disparait quand il vaut zero, parce qu'un « 0 » affiche
7
+ se lit comme un filtre pose sur rien.
8
+ -->
9
+ <script setup lang="ts">
10
+ import { computed } from 'vue'
11
+
12
+ const props = withDefaults(defineProps<{
13
+ amount: number | string
14
+ /** Garde la pastille visible a zero, pour un compteur qui doit tenir sa place. */
15
+ showZero?: boolean
16
+ }>(), { showZero: false })
17
+
18
+ const visible = computed(() => props.showZero || Number(props.amount) !== 0)
19
+ </script>
20
+
21
+ <template>
22
+ <span v-if="visible" class="rm-circle-amount">{{ amount }}</span>
23
+ </template>
24
+
25
+ <style scoped>
26
+ .rm-circle-amount{align-items:center;border:1px solid var(--border-default);border-radius:var(--radius-pill,999px);color:var(--text-primary);display:inline-flex;font-size:var(--font-12,12px);font-weight:700;justify-content:center;line-height:normal;padding:2px 8px}
27
+ </style>
@@ -0,0 +1,52 @@
1
+ <!--
2
+ RmEmoji - un emoji a la bonne taille, nomme plutot que colle dans le code.
3
+
4
+ Reprend `arg-emoji` d'Auriga et ses trois entrees. Deux choses changent.
5
+
6
+ Le rendu passe par du texte, pas par `v-html` : le composant d'Auriga
7
+ injectait une entite HTML, ce qui ouvre la porte a n'importe quel balisage
8
+ des que le nom vient d'une donnee.
9
+
10
+ Un nom inconnu ne rend plus le vide : la valeur passe telle quelle, si bien
11
+ qu'un emoji absent de la table s'ecrit directement sans toucher au paquet.
12
+
13
+ L'emoji porte `role="img"` et un intitule, sinon un lecteur d'ecran lit le
14
+ nom Unicode complet au milieu d'une phrase.
15
+ -->
16
+ <script setup lang="ts">
17
+ import { computed } from 'vue'
18
+
19
+ /** Les emojis nommes du design system. Un nom absent passe tel quel. */
20
+ const EMOJIS: Record<string, string> = {
21
+ Lightbulb: '💡',
22
+ Pin: '📍',
23
+ Television: '📺',
24
+ }
25
+
26
+ const props = withDefaults(defineProps<{
27
+ /** Nom de la table, ou l'emoji lui-meme. */
28
+ name: string
29
+ size?: number
30
+ /** Intitule lu a voix haute. Absent, l'emoji est purement decoratif. */
31
+ alt?: string
32
+ }>(), {
33
+ size: 20,
34
+ alt: '',
35
+ })
36
+
37
+ const glyph = computed(() => EMOJIS[props.name] ?? props.name)
38
+ </script>
39
+
40
+ <template>
41
+ <span
42
+ class="rm-emoji"
43
+ :style="{ fontSize: `${size}px` }"
44
+ :role="alt ? 'img' : undefined"
45
+ :aria-label="alt || undefined"
46
+ :aria-hidden="alt ? undefined : 'true'"
47
+ >{{ glyph }}</span>
48
+ </template>
49
+
50
+ <style scoped>
51
+ .rm-emoji{display:inline-block;line-height:1}
52
+ </style>
@@ -178,13 +178,6 @@ onBeforeUnmount(() => {
178
178
  <span>{{ t.customize }}</span>
179
179
  </button>
180
180
 
181
- <!-- Un reglage pose SUR la barre : le niveau d'agregation, une periode.
182
- A reserver a ce qu'on change a chaque lecture ; tout le reste va
183
- dans le panneau, sinon la barre redevient un panneau ouvert. -->
184
- <div v-if="hasRail" class="rm-fbar__rail">
185
- <slot name="Rail" />
186
- </div>
187
-
188
181
  <div class="rm-fbar__search">
189
182
  <slot name="Search" />
190
183
 
@@ -205,6 +198,22 @@ onBeforeUnmount(() => {
205
198
 
206
199
  </div>
207
200
 
201
+ <!-- Un reglage pose SUR la barre : le niveau d'agregation, une periode.
202
+ A reserver a ce qu'on change a chaque lecture ; tout le reste va
203
+ dans le panneau, sinon la barre redevient un panneau ouvert.
204
+
205
+ Sa propre rangee, sous les puces. Il partageait la premiere avec le
206
+ champ de recherche, et les deux se disputaient la place : un rail de
207
+ huit segments (les niveaux d'agregation, du produit au fabricant)
208
+ sortait du cadre par la droite, ses derniers segments coupes, et le
209
+ champ tombait a sa largeur minimale. Chacun sur sa ligne prend toute
210
+ la largeur de la colonne. -->
211
+ <div v-if="hasRail" class="rm-fbar__rail-row">
212
+ <div class="rm-fbar__rail">
213
+ <slot name="Rail" />
214
+ </div>
215
+ </div>
216
+
208
217
  <div v-show="open" class="rm-fbar__panel">
209
218
  <!-- Le panneau se presente comme une section : son titre, et les
210
219
  commandes juste a cote, pres des filtres qu'elles vident. -->
@@ -251,5 +260,5 @@ onBeforeUnmount(() => {
251
260
  </template>
252
261
 
253
262
  <style scoped>
254
- .rm-fbar{margin-inline:calc((min(var(--oip-content-max, 100%), 100%) - var(--oip-main-w, 100%))/2);position:sticky;width:var(--oip-main-w,100%);z-index:91;--rm-fbar-field-h:32px;backdrop-filter:blur(12px);background:rgba(245,245,249,.8);border-bottom:1px solid var(--border-default);display:flex;flex-direction:column;font-family:var(--font-sans)}.rm-fbar__views{align-items:center;display:flex;flex:0 0 auto}.rm-fbar__views:empty{display:none}.rm-fbar__row{align-items:flex-start;display:flex;gap:12px;padding-block:8px;padding-inline:max(32px,(100% - var(--oip-content-max,100%))/2 + 32px)}.rm-fbar--open .rm-fbar__row{border-bottom:1px solid var(--border-default)}.rm-fbar__toggle :deep(p),.rm-fbar__toggle :deep(span){font-size:inherit;font-weight:inherit;line-height:inherit;margin:0}.rm-fbar__toggle{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-pill);color:var(--text-primary);cursor:pointer;display:flex;flex:0 0 auto;font-family:inherit;font-size:var(--font-14);font-weight:var(--weight-medium);gap:6px;height:var(--rm-fbar-field-h);justify-content:center;padding:0 14px}.rm-fbar__toggle:hover{background:var(--bg-surface-3)}.rm-fbar__toggle.is-active,.rm-fbar__toggle.is-active:hover{background:var(--color-primary);border-color:var(--color-primary);color:var(--color-text-inverse)}.rm-fbar__toggle.is-active .rm-fbar__badge{background:var(--color-text-inverse);color:var(--color-primary)}.rm-fbar__badge{align-items:center;background:var(--color-primary);border-radius:16px;color:var(--color-text-inverse);display:inline-flex;font-size:var(--font-11);font-weight:var(--weight-bold);justify-content:center;min-width:18px;padding:0 5px}.rm-fbar__view{align-items:center;display:flex;flex:0 0 auto}.rm-fbar__advanced{align-items:center;background:none;border:none;border-left:1px solid var(--border-default);color:var(--color-primary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-13);font-weight:var(--weight-medium);height:var(--rm-fbar-field-h);padding:0 0 0 12px;position:absolute;right:14px;text-decoration-line:underline;top:0;white-space:nowrap;z-index:2}.rm-fbar__advanced.is-active,.rm-fbar__advanced:hover{color:var(--color-primary-hover,var(--color-primary));text-decoration-thickness:2px}.rm-fbar__rail{align-items:center;display:flex;flex:0 1 auto;min-width:0}.rm-fbar__rail:empty{display:none}.rm-fbar__search{align-items:center;display:flex;flex:1 1 auto;gap:8px;min-width:0;position:relative}.rm-fbar__search:empty{display:none}.rm-fbar__search :deep(.filters_input_search){border-radius:var(--radius-pill);box-sizing:border-box;height:auto;min-height:var(--rm-fbar-field-h);padding-right:calc(var(--rm-fbar-advanced-w, 0px) + 12px);width:100%}.rm-fbar__search :deep(.research_tags:not(:has(*))){display:none}.rm-fbar__search :deep(.select_shortcut){box-sizing:border-box;font-size:var(--font-11);height:24px;padding-block:0}.rm-fbar__search :deep(.select_shortcut:hover){background:var(--bg-surface-3)!important;border-color:var(--border-default)!important;color:inherit!important}.rm-fbar__search :deep(.select_shortcut:hover svg){color:inherit!important}.rm-fbar__search :deep(.remove_search_section){border-left:none;border-radius:0 var(--radius-pill) var(--radius-pill) 0}.rm-fbar__search :deep(.remove_search_section:hover){background:transparent!important}.rm-fbar__search :deep(.remove_search_section:hover svg){color:var(--text-primary)!important}.rm-fbar__search :deep(.filters_input_search.no_bottom){border-bottom:1.5px solid var(--color-primary)!important;border-radius:var(--radius-pill)}.rm-fbar__search :deep(.search_suggestions){border:1px solid var(--border-default);border-radius:var(--radius-2xl);box-shadow:var(--shadow-lg);left:0;padding:6px;right:0;top:calc(100% + 6px);width:auto}.rm-fbar__search :deep(.sugg_container>*){border-radius:var(--radius-lg)}.rm-fbar__search :deep(.sugg_container>.selected){background:color-mix(in srgb,var(--color-primary) 6%,transparent)!important}.rm-fbar__panel{--rm-filter-col:224px;--rm-fbar-panel-field-h:40px;align-items:end;display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(0,var(--rm-filter-col)));justify-content:center;padding-block:16px 20px;padding-inline:56px}.rm-fbar__panel--wide{align-items:stretch;display:flex;flex-direction:column;gap:12px}.rm-fbar__filters{display:contents}.rm-fbar__filters :deep(.filter-chip),.rm-fbar__filters :deep(.filter-complex__trigger),.rm-fbar__filters :deep(.rm-number__box){border-radius:var(--radius-lg);height:var(--rm-fbar-panel-field-h);width:100%}.rm-fbar__panel-head{align-items:baseline;display:flex;gap:16px;grid-column:1/-1;width:100%}.rm-fbar__customize{grid-column:1/-1}.rm-fbar__panel :deep(.filters_row_break){grid-column:1/-1}.rm-fbar__panel-title{font-size:var(--font-18);font-weight:800;margin:0}.rm-fbar__action{background:none;border:none;cursor:pointer;font-family:inherit;font-size:var(--font-14);font-weight:var(--weight-medium);padding:0;text-decoration-line:underline}.rm-fbar__action--save{color:var(--color-primary)}.rm-fbar__action--clear{color:var(--text-secondary)}
263
+ .rm-fbar{margin-inline:calc((min(var(--oip-content-max, 100%), 100%) - var(--oip-main-w, 100%))/2);position:sticky;width:var(--oip-main-w,100%);z-index:91;--rm-fbar-field-h:32px;backdrop-filter:blur(12px);background:rgba(245,245,249,.8);border-bottom:1px solid var(--border-default);display:flex;flex-direction:column;font-family:var(--font-sans)}.rm-fbar__views{align-items:center;display:flex;flex:0 0 auto}.rm-fbar__views:empty{display:none}.rm-fbar__row{align-items:flex-start;display:flex;gap:12px;padding-block:8px;padding-inline:max(32px,(100% - var(--oip-content-max,100%))/2 + 32px)}.rm-fbar--open .rm-fbar__row:not(:has(~.rm-fbar__rail-row)){border-bottom:1px solid var(--border-default)}.rm-fbar__toggle :deep(p),.rm-fbar__toggle :deep(span){font-size:inherit;font-weight:inherit;line-height:inherit;margin:0}.rm-fbar__toggle{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-pill);color:var(--text-primary);cursor:pointer;display:flex;flex:0 0 auto;font-family:inherit;font-size:var(--font-14);font-weight:var(--weight-medium);gap:6px;height:var(--rm-fbar-field-h);justify-content:center;padding:0 14px}.rm-fbar__toggle:hover{background:var(--bg-surface-3)}.rm-fbar__toggle.is-active,.rm-fbar__toggle.is-active:hover{background:var(--color-primary);border-color:var(--color-primary);color:var(--color-text-inverse)}.rm-fbar__toggle.is-active .rm-fbar__badge{background:var(--color-text-inverse);color:var(--color-primary)}.rm-fbar__badge{align-items:center;background:var(--color-primary);border-radius:16px;color:var(--color-text-inverse);display:inline-flex;font-size:var(--font-11);font-weight:var(--weight-bold);justify-content:center;min-width:18px;padding:0 5px}.rm-fbar__view{align-items:center;display:flex;flex:0 0 auto}.rm-fbar__advanced{align-items:center;background:none;border:none;border-left:1px solid var(--border-default);color:var(--color-primary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-13);font-weight:var(--weight-medium);height:var(--rm-fbar-field-h);padding:0 0 0 12px;position:absolute;right:14px;text-decoration-line:underline;top:0;white-space:nowrap;z-index:2}.rm-fbar__advanced.is-active,.rm-fbar__advanced:hover{color:var(--color-primary-hover,var(--color-primary));text-decoration-thickness:2px}.rm-fbar__rail-row{display:flex;padding-bottom:8px;padding-inline:max(32px,(100% - var(--oip-content-max,100%))/2 + 32px)}.rm-fbar--open .rm-fbar__rail-row{border-bottom:1px solid var(--border-default)}.rm-fbar__rail{align-items:center;display:flex;flex:0 1 auto;min-width:0;overflow-x:auto;scrollbar-width:thin}.rm-fbar__rail:empty{display:none}.rm-fbar__search{align-items:center;display:flex;flex:1 1 auto;gap:8px;min-width:0;position:relative}.rm-fbar__search:empty{display:none}.rm-fbar__search :deep(.search-bar-wrapper){flex:1 1 auto;min-width:0}.rm-fbar__search :deep(.search-bar){background:var(--bg-surface);box-sizing:border-box;display:flex;height:var(--rm-fbar-field-h);width:100%}.rm-fbar__search :deep(.filters_input_search){border-radius:var(--radius-pill);box-sizing:border-box;height:auto;min-height:var(--rm-fbar-field-h);padding-right:calc(var(--rm-fbar-advanced-w, 0px) + 12px);width:100%}.rm-fbar__search :deep(.research_tags:not(:has(*))){display:none}.rm-fbar__search :deep(.select_shortcut){box-sizing:border-box;font-size:var(--font-11);height:24px;padding-block:0}.rm-fbar__search :deep(.select_shortcut:hover){background:var(--bg-surface-3)!important;border-color:var(--border-default)!important;color:inherit!important}.rm-fbar__search :deep(.select_shortcut:hover svg){color:inherit!important}.rm-fbar__search :deep(.remove_search_section){border-left:none;border-radius:0 var(--radius-pill) var(--radius-pill) 0}.rm-fbar__search :deep(.remove_search_section:hover){background:transparent!important}.rm-fbar__search :deep(.remove_search_section:hover svg){color:var(--text-primary)!important}.rm-fbar__search :deep(.filters_input_search.no_bottom){border-bottom:1.5px solid var(--color-primary)!important;border-radius:var(--radius-pill)}.rm-fbar__search :deep(.search_suggestions){border:1px solid var(--border-default);border-radius:var(--radius-2xl);box-shadow:var(--shadow-lg);left:0;padding:6px;right:0;top:calc(100% + 6px);width:auto}.rm-fbar__search :deep(.sugg_container>*){border-radius:var(--radius-lg)}.rm-fbar__search :deep(.sugg_container>.selected){background:color-mix(in srgb,var(--color-primary) 6%,transparent)!important}.rm-fbar__panel{--rm-filter-col:224px;--rm-fbar-panel-field-h:40px;align-items:end;display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(0,var(--rm-filter-col)));justify-content:center;padding-block:16px 20px;padding-inline:56px}.rm-fbar__panel--wide{align-items:stretch;display:flex;flex-direction:column;gap:12px}.rm-fbar__filters{display:contents}.rm-fbar__filters :deep(.filter-chip),.rm-fbar__filters :deep(.filter-complex__trigger),.rm-fbar__filters :deep(.rm-number__box){border-radius:var(--radius-lg);height:var(--rm-fbar-panel-field-h);width:100%}.rm-fbar__panel-head{align-items:baseline;display:flex;gap:16px;grid-column:1/-1;width:100%}.rm-fbar__customize{grid-column:1/-1}.rm-fbar__panel :deep(.filters_row_break){grid-column:1/-1}.rm-fbar__panel-title{font-size:var(--font-18);font-weight:800;margin:0}.rm-fbar__action{background:none;border:none;cursor:pointer;font-family:inherit;font-size:var(--font-14);font-weight:var(--weight-medium);padding:0;text-decoration-line:underline}.rm-fbar__action--save{color:var(--color-primary)}.rm-fbar__action--clear{color:var(--text-secondary)}
255
264
  </style>
@@ -14,11 +14,21 @@ const props = withDefaults(defineProps<{
14
14
  advancedCount?: number
15
15
  suggestions?: SearchSuggestion[]
16
16
  placeholder?: string
17
+ /**
18
+ * Lien « Advanced search » au bout du champ. A passer a `false` quand la
19
+ * page n'a pas de recherche avancee : le lien prenait la place du champ et
20
+ * n'ecoutait personne, ce qui se lit comme une commande cassee.
21
+ */
22
+ haveAdvanced?: boolean
23
+ /** Libelle du lien, pour les pages qui ne sont pas en anglais. */
24
+ advancedLabel?: string
17
25
  }>(), {
18
26
  advancedActive: false,
19
27
  advancedCount: 0,
20
28
  suggestions: () => [],
21
29
  placeholder: 'Search...',
30
+ haveAdvanced: true,
31
+ advancedLabel: 'Advanced search',
22
32
  })
23
33
 
24
34
  const emit = defineEmits<{
@@ -92,11 +102,12 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
92
102
  <PhX :size="12" />
93
103
  </button>
94
104
  <button
105
+ v-if="haveAdvanced"
95
106
  class="search-bar__advanced"
96
107
  :class="{ 'search-bar__advanced--active': advancedActive }"
97
108
  @click="$emit('advancedSearch')"
98
109
  >
99
- Advanced search
110
+ {{ advancedLabel }}
100
111
  <span v-if="advancedCount > 0" class="search-bar__advanced-badge">{{ advancedCount }}</span>
101
112
  </button>
102
113
  </div>
@@ -0,0 +1,63 @@
1
+ <!--
2
+ RmSwitch - bascule entre deux etats nommes.
3
+
4
+ Reprend `arg-switch` d'Auriga : les deux intitules encadrent la bascule, et
5
+ celui qui vaut est le seul en couleur pleine. Ce n'est pas une case a cocher
6
+ deguisee : les deux etats ont un nom, aucun n'est « rien ».
7
+
8
+ Trois choses changent. C'est un `<button role="switch">` porteur de
9
+ `aria-checked` : le `<div tabindex="0">` d'Auriga n'annoncait ni role ni
10
+ etat, et ne repondait pas a la barre d'espace.
11
+
12
+ L'etat est un `v-model` plutot qu'une copie interne : la bascule d'Auriga
13
+ avancait toute seule, meme quand le parent refusait le changement.
14
+
15
+ Le masque `paint(squircle)` saute, avec le worklet CSS qu'il fallait charger
16
+ a la main et qui ne peignait que sur Chrome. Un `border-radius` suffit.
17
+ -->
18
+ <script setup lang="ts">
19
+ withDefaults(defineProps<{
20
+ /** Intitule de l'etat actif, a gauche. */
21
+ activeText: string
22
+ /** Intitule de l'etat inactif, a droite. */
23
+ inactiveText: string
24
+ /** Intitule du champ, au-dessus. Absent, rien ne sort. */
25
+ label?: string
26
+ disabled?: boolean
27
+ }>(), {
28
+ label: '',
29
+ disabled: false,
30
+ })
31
+
32
+ const active = defineModel<boolean>({ default: true })
33
+
34
+ function toggle() {
35
+ active.value = !active.value
36
+ }
37
+ </script>
38
+
39
+ <template>
40
+ <div class="rm-switch">
41
+ <p v-if="label" class="rm-switch__label">{{ label }}</p>
42
+
43
+ <button
44
+ type="button"
45
+ role="switch"
46
+ class="rm-switch__control"
47
+ :aria-checked="active"
48
+ :aria-label="label || undefined"
49
+ :disabled="disabled"
50
+ @click="toggle"
51
+ >
52
+ <span class="rm-switch__text" :class="{ 'is-on': active }">{{ activeText }}</span>
53
+ <span class="rm-switch__track">
54
+ <span class="rm-switch__knob" :class="{ 'is-right': !active }" />
55
+ </span>
56
+ <span class="rm-switch__text" :class="{ 'is-on': !active }">{{ inactiveText }}</span>
57
+ </button>
58
+ </div>
59
+ </template>
60
+
61
+ <style scoped>
62
+ .rm-switch__label{color:var(--text-secondary);font-size:var(--font-12,12px);font-weight:600;margin-bottom:6px}.rm-switch__control{align-items:center;background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:var(--radius-md,8px);cursor:pointer;display:flex;gap:10px;padding:10px 16px;width:-moz-fit-content;width:fit-content}.rm-switch__control:focus-visible{border-color:var(--color-primary);box-shadow:0 0 0 4px rgba(var(--brand-rgb),.1);outline:none}.rm-switch__control:disabled{cursor:not-allowed;opacity:.6}.rm-switch__text{color:var(--text-muted);font-size:var(--font-14,14px);font-weight:600;line-height:16px;transition:color var(--duration-fast,.15s) var(--ease-out,ease)}.rm-switch__text.is-on{color:var(--text-primary)}.rm-switch__track{background:var(--color-primary);border-radius:var(--radius-pill,999px);flex-shrink:0;height:18px;position:relative;width:36px}.rm-switch__knob{background:var(--r-color-white);border-radius:50%;height:14px;left:2px;position:absolute;top:50%;transform:translateY(-50%);transition:left var(--duration-fast,.15s) var(--ease-out,ease);width:14px}.rm-switch__knob.is-right{left:20px}
63
+ </style>
@@ -0,0 +1,67 @@
1
+ <!--
2
+ RmTooltip - bulle d'aide au survol d'un element.
3
+
4
+ Reprend `arg-tooltip` d'Auriga : meme bulle sombre, memes quatre directions.
5
+ Trois choses changent.
6
+
7
+ La bulle est un vrai `role="tooltip"` relie au contenu par
8
+ `aria-describedby` : au clavier et au lecteur d'ecran, une bulle qui n'existe
9
+ qu'au `:hover` CSS n'est jamais lue. Elle sort donc aussi au focus.
10
+
11
+ Elle se centre par `translateX(-50%)` au lieu du `margin-left: -72px` en dur
12
+ d'Auriga, qui decalait la bulle des que le texte n'atteignait pas la largeur
13
+ maximale.
14
+
15
+ Le texte passe par un slot nomme plutot que par une prop : un lien ou une
16
+ mise en forme dans une bulle n'obligent plus a sortir du composant.
17
+ -->
18
+ <script setup lang="ts">
19
+ import { useId } from 'vue'
20
+
21
+ withDefaults(defineProps<{
22
+ /** Texte de la bulle. Ignore si le slot `content` est fourni. */
23
+ text?: string
24
+ /** Cote ou la bulle sort, par rapport a l'element survole. */
25
+ dir?: 'top' | 'right' | 'bottom' | 'left'
26
+ /**
27
+ * Neutralise la bulle sans retirer le composant : un intitule tronque n'a
28
+ * besoin de son texte complet que lorsqu'il est effectivement coupe.
29
+ */
30
+ disabled?: boolean
31
+ /**
32
+ * Laisse la bulle s'etendre sur toute sa hauteur. Par defaut elle s'arrete a
33
+ * quatre lignes : une bulle plus haute recouvre ce qu'on est en train de
34
+ * lire.
35
+ */
36
+ full?: boolean
37
+ }>(), {
38
+ text: '',
39
+ dir: 'top',
40
+ disabled: false,
41
+ full: false,
42
+ })
43
+
44
+ const id = `rm-tooltip-${useId()}`
45
+ </script>
46
+
47
+ <template>
48
+ <!-- `aria-describedby` est pose en permanence : un lecteur d'ecran lit la
49
+ bulle a l'arrivee sur l'element, sans attendre un survol qu'il ne fait
50
+ pas. C'est le CSS seul qui la montre a l'oeil. -->
51
+ <span class="rm-tooltip" :aria-describedby="disabled ? undefined : id">
52
+ <slot />
53
+ <span
54
+ v-if="!disabled"
55
+ :id="id"
56
+ role="tooltip"
57
+ class="rm-tooltip__bubble"
58
+ :class="[`rm-tooltip__bubble--${dir}`, { 'rm-tooltip__bubble--full': full }]"
59
+ >
60
+ <slot name="content">{{ text }}</slot>
61
+ </span>
62
+ </span>
63
+ </template>
64
+
65
+ <style scoped>
66
+ .rm-tooltip{display:inline-flex;position:relative;width:-moz-fit-content;width:fit-content}.rm-tooltip__bubble{background:var(--r-color-gray-900);border-radius:var(--radius-sm,4px);color:var(--r-color-white);display:-webkit-box;font-size:var(--font-13,13px);font-weight:400;-webkit-line-clamp:4;line-height:1.6;max-width:220px;padding:4px 12px;position:absolute;text-align:left;white-space:pre-line;width:-moz-max-content;width:max-content;z-index:60;-webkit-box-orient:vertical;opacity:0;overflow:hidden;transition:opacity var(--duration-fast,.15s) var(--ease-out,ease);visibility:hidden}.rm-tooltip__bubble--full{display:block;overflow:visible}.rm-tooltip:focus-within .rm-tooltip__bubble,.rm-tooltip:hover .rm-tooltip__bubble{opacity:1;visibility:visible}.rm-tooltip__bubble--top{bottom:calc(100% + 6px);left:50%;transform:translateX(-50%)}.rm-tooltip__bubble--bottom{left:50%;top:calc(100% + 6px);transform:translateX(-50%)}.rm-tooltip__bubble--right{left:calc(100% + 8px);top:50%;transform:translateY(-50%)}.rm-tooltip__bubble--left{right:calc(100% + 8px);top:50%;transform:translateY(-50%)}
67
+ </style>
@@ -0,0 +1,49 @@
1
+ <!--
2
+ RmTrendIndicator - pastille de tendance, une fleche montante ou descendante.
3
+
4
+ Reprend `arg-trend-indicator` d'Auriga : meme pastille ronde a fond teinte,
5
+ memes fleches diagonales. Deux choses changent.
6
+
7
+ Le sens n'est jamais porte par la seule couleur : la fleche pointe en haut ou
8
+ en bas, et la pastille porte un `title` lisible par un lecteur d'ecran. Chez
9
+ Auriga, un daltonien lisait deux ronds identiques.
10
+
11
+ `semantic` remplace `isGreenAndRed`, nomme a l'envers de ce qu'il fait : une
12
+ hausse n'est bonne que dans certains tableaux (des ventes, pas des ruptures),
13
+ d'ou la teinte neutre par defaut et le vert/rouge sur demande.
14
+ -->
15
+ <script setup lang="ts">
16
+ import { computed } from 'vue'
17
+
18
+ const props = withDefaults(defineProps<{
19
+ /** Sens de la tendance. */
20
+ up?: boolean
21
+ /** Vert pour une hausse, rouge pour une baisse. Sinon, teinte de marque. */
22
+ semantic?: boolean
23
+ /** Lu par les technologies d'assistance. */
24
+ title?: string
25
+ }>(), {
26
+ up: true,
27
+ semantic: false,
28
+ title: '',
29
+ })
30
+
31
+ const color = computed(() => {
32
+ if (!props.semantic) return props.up ? 'var(--color-primary)' : 'var(--color-secondary)'
33
+ return props.up ? 'var(--color-success)' : 'var(--color-danger)'
34
+ })
35
+ </script>
36
+
37
+ <template>
38
+ <span
39
+ class="rm-trend"
40
+ :class="[up ? 'rm-trend--up' : 'rm-trend--down', { 'rm-trend--semantic': semantic }]"
41
+ :title="title || undefined"
42
+ >
43
+ <RmIcon :name="up ? 'ArrowUpRight' : 'ArrowDownRight'" :size="16" :color="color" />
44
+ </span>
45
+ </template>
46
+
47
+ <style scoped>
48
+ .rm-trend{align-items:center;border-radius:50%;display:inline-flex;height:24px;justify-content:center;width:24px}.rm-trend--up{background:rgba(var(--brand-rgb),.1)}.rm-trend--down{background:color-mix(in srgb,var(--color-secondary) 12%,transparent)}.rm-trend--semantic.rm-trend--up{background:var(--color-success-bg)}.rm-trend--semantic.rm-trend--down{background:var(--color-danger-bg)}
49
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.56",
3
+ "version": "0.1.58",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",