@tekkare/romulus 0.1.61 → 0.1.63

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.60",
7
+ "version": "0.1.62",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -1,41 +1,126 @@
1
+ <!--
2
+ RmButton - bouton du design system.
3
+
4
+ Six niveaux d'emphase, du plus fort au plus discret : `primary`, `secondary`,
5
+ `tertiary`, `text`, `text-danger`, `danger`. Une seule action `primary` par
6
+ ecran ou par section, sinon plus rien ne ressort.
7
+
8
+ Le bouton texte est TOUJOURS souligne : dans un tableau dense, un mot violet
9
+ sans soulignement ne se distingue pas d'une valeur mise en avant, et le
10
+ daltonien ne le voit pas du tout.
11
+
12
+ `danger` est le seul a inverser son emphase au survol (contour rouge, puis
13
+ fond rouge plein) : l'inversion est le signal, elle marque le moment ou
14
+ l'action devient possible.
15
+
16
+ Les anciens noms (`primary-fill`, `primary-stroke`, `outline`, `ghost`,
17
+ `discovery-fill`...) restent acceptes et retombent sur les six variantes. Ils
18
+ ne sont plus a utiliser dans du code neuf.
19
+ -->
1
20
  <script setup lang="ts">
21
+ import { computed } from 'vue'
22
+
23
+ /** Les six niveaux d'emphase de la reference. */
24
+ export type RmButtonVariant =
25
+ | 'primary'
26
+ | 'secondary'
27
+ | 'tertiary'
28
+ | 'text'
29
+ | 'text-danger'
30
+ | 'danger'
31
+
32
+ /** Noms d'avant l'unification, gardes pour ne pas casser les appels en place. */
33
+ export type RmButtonLegacyVariant =
34
+ | 'primary-fill'
35
+ | 'primary-stroke'
36
+ | 'secondary-fill'
37
+ | 'secondary-stroke'
38
+ | 'secondary-transparent'
39
+ | 'outline'
40
+ | 'ghost'
41
+ | 'discovery-fill'
42
+ | 'discovery-stroke'
43
+ | 'databank-fill'
44
+ | 'databank-stroke'
45
+ | 'pharma-fill'
46
+ | 'pharma-stroke'
47
+
2
48
  export interface RmButtonProps {
3
- /** Visual style variant */
4
- variant?: 'primary' | 'primary-fill' | 'primary-stroke' | 'discovery-fill' | 'discovery-stroke' | 'databank-fill' | 'databank-stroke' | 'pharma-fill' | 'pharma-stroke' | 'secondary' | 'outline' | 'ghost' | 'danger'
5
- /** Size of the button */
49
+ variant?: RmButtonVariant | RmButtonLegacyVariant
6
50
  size?: 'sm' | 'md' | 'lg'
7
- /** Whether the button is disabled */
8
51
  disabled?: boolean
9
- /** Render as a full-width button */
52
+ /** Pleine largeur, pour un tiroir ou un formulaire dense. */
10
53
  block?: boolean
11
- /** HTML button type attribute */
54
+ /** Forme pilule, pour les controles d'une barre de filtres. */
55
+ pill?: boolean
56
+ /**
57
+ * Carre a une seule icone, 32x32 (28 en `sm`, 40 en `lg`). Demande un
58
+ * `aria-label` : un bouton sans texte n'est annonce que par son intitule.
59
+ */
60
+ iconOnly?: boolean
61
+ /** Fond et bordure transparents. Pour une action discrete posee sur une carte. */
62
+ ghost?: boolean
12
63
  type?: 'button' | 'submit' | 'reset'
13
- /** Phosphor icon name before the label */
64
+ /** Icone Phosphor avant le libelle. */
14
65
  prefixIcon?: string
15
- /** Phosphor icon name after the label */
66
+ /** Icone Phosphor apres le libelle. */
16
67
  suffixIcon?: string
17
68
  /**
18
69
  * Action en cours : le bouton tourne et n'accepte plus de clic. Sans cet
19
- * etat, un export long laisse croire que le clic n'a pas pris et l'utilisateur
20
- * relance la meme requete.
70
+ * etat, un export long laisse croire que le clic n'a pas pris et
71
+ * l'utilisateur relance la meme requete.
21
72
  */
22
73
  loading?: boolean
74
+ /** Teinte de marque d'une action `primary` : la couleur du produit courant. */
75
+ accent?: 'healthcare' | 'discovery' | 'databank' | 'pharma'
23
76
  }
24
77
 
25
- withDefaults(defineProps<RmButtonProps>(), {
26
- variant: 'primary-fill',
78
+ const props = withDefaults(defineProps<RmButtonProps>(), {
79
+ variant: 'primary',
27
80
  size: 'md',
28
81
  disabled: false,
29
82
  block: false,
83
+ pill: false,
84
+ iconOnly: false,
85
+ ghost: false,
30
86
  type: 'button',
31
87
  prefixIcon: undefined,
32
88
  suffixIcon: undefined,
33
89
  loading: false,
90
+ accent: undefined,
91
+ })
92
+
93
+ defineEmits<{ click: [event: MouseEvent] }>()
94
+
95
+ /**
96
+ * Ou retombe chaque ancien nom. Le fond plein devient l'emphase forte, le
97
+ * contour l'emphase moyenne, le transparent l'emphase basse.
98
+ */
99
+ const LEGACY: Record<string, { variant: RmButtonVariant, ghost?: boolean, accent?: RmButtonProps['accent'] }> = {
100
+ 'primary-fill': { variant: 'primary' },
101
+ 'primary-stroke': { variant: 'secondary' },
102
+ 'secondary-fill': { variant: 'tertiary' },
103
+ 'secondary-stroke': { variant: 'secondary' },
104
+ 'secondary-transparent': { variant: 'tertiary', ghost: true },
105
+ 'outline': { variant: 'secondary' },
106
+ 'ghost': { variant: 'tertiary', ghost: true },
107
+ 'discovery-fill': { variant: 'primary', accent: 'discovery' },
108
+ 'discovery-stroke': { variant: 'secondary' },
109
+ 'databank-fill': { variant: 'primary', accent: 'databank' },
110
+ 'databank-stroke': { variant: 'secondary' },
111
+ 'pharma-fill': { variant: 'primary', accent: 'pharma' },
112
+ 'pharma-stroke': { variant: 'secondary' },
113
+ }
114
+
115
+ const resolved = computed(() => LEGACY[props.variant] ?? { variant: props.variant as RmButtonVariant })
116
+ const variantClass = computed(() => `rm-button--${resolved.value.variant}`)
117
+ const isGhost = computed(() => props.ghost || resolved.value.ghost === true)
118
+ const accentClass = computed(() => {
119
+ const accent = props.accent ?? resolved.value.accent
120
+ return accent ? `rm-button--accent-${accent}` : undefined
34
121
  })
35
122
 
36
- defineEmits<{
37
- click: [event: MouseEvent]
38
- }>()
123
+ const iconSize = computed(() => (props.size === 'sm' ? 14 : props.size === 'lg' ? 20 : 16))
39
124
  </script>
40
125
 
41
126
  <template>
@@ -45,19 +130,26 @@ defineEmits<{
45
130
  :aria-busy="loading ? 'true' : undefined"
46
131
  :class="[
47
132
  'rm-button',
48
- `rm-button--${variant}`,
133
+ variantClass,
134
+ accentClass,
49
135
  `rm-button--${size}`,
50
- { 'rm-button--block': block, 'rm-button--loading': loading },
136
+ {
137
+ 'rm-button--block': block,
138
+ 'rm-button--pill': pill,
139
+ 'rm-button--icon': iconOnly,
140
+ 'rm-button--ghost': isGhost,
141
+ 'rm-button--loading': loading,
142
+ },
51
143
  ]"
52
144
  @click="$emit('click', $event)"
53
145
  >
54
- <RmIcon v-if="loading" name="CircleNotch" :size="size === 'sm' ? 14 : 16" class="rm-button__spinner" />
55
- <RmIcon v-else-if="prefixIcon" :name="prefixIcon" :size="size === 'sm' ? 14 : 16" />
146
+ <RmIcon v-if="loading" name="CircleNotch" :size="iconSize" class="rm-button__spinner" />
147
+ <RmIcon v-else-if="prefixIcon" :name="prefixIcon" :size="iconSize" />
56
148
  <slot />
57
- <RmIcon v-if="suffixIcon" :name="suffixIcon" :size="size === 'sm' ? 14 : 16" />
149
+ <RmIcon v-if="suffixIcon && !loading" :name="suffixIcon" :size="iconSize" />
58
150
  </button>
59
151
  </template>
60
152
 
61
153
  <style scoped>
62
- .rm-button--loading{cursor:progress}.rm-button__spinner{animation:rm-button-spin .9s linear infinite}@keyframes rm-button-spin{to{transform:rotate(1turn)}}.rm-button{align-items:center;border:1px solid transparent;border-radius:6px;cursor:pointer;display:inline-flex;font-family:var(--font-sans);font-weight:500;gap:8px;justify-content:center;transition:all var(--duration-fast) var(--ease-in-out);-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.rm-button:focus-visible{outline:2px solid var(--color-healthcare);outline-offset:2px}.rm-button:disabled{cursor:not-allowed;opacity:.5}.rm-button--sm{font-size:13px;height:32px;padding:6px 16px}.rm-button--md{font-size:14px;height:40px;padding:10px 24px}.rm-button--lg{font-size:15px;height:48px;padding:12px 28px}.rm-button--primary{background:var(--color-primary);border-color:var(--color-primary);color:var(--color-text-inverse)}.rm-button--primary:active:not(:disabled),.rm-button--primary:hover:not(:disabled){background:var(--color-primary-hover);border-color:var(--color-primary-hover)}.rm-button--primary-fill{background:linear-gradient(180deg,rgba(var(--healthcare-rgb),.8) 0,rgba(var(--healthcare-rgb),.9) 100%);border-color:var(--color-healthcare);box-shadow:0 2px 3px 0 rgba(var(--healthcare-rgb),.5);color:var(--color-text-inverse)}.rm-button--primary-fill:hover:not(:disabled){background:linear-gradient(180deg,var(--color-healthcare) 0,var(--color-healthcare) 100%);box-shadow:0 2px 4px 0 rgba(var(--healthcare-rgb),.6)}.rm-button--primary-fill:active:not(:disabled){background:var(--color-healthcare-active);box-shadow:0 1px 2px 0 rgba(var(--healthcare-rgb),.4)}.rm-button--primary-stroke{box-shadow:0 2px 3px 0 rgba(var(--healthcare-rgb),.5);color:var(--color-healthcare)}.rm-button--primary-stroke,.rm-button--primary-stroke:hover:not(:disabled){background:linear-gradient(180deg,rgba(var(--surface-rgb),.5) 0,rgba(var(--surface-rgb),.9) 100%);border-color:var(--color-healthcare)}.rm-button--primary-stroke:hover:not(:disabled){box-shadow:none}.rm-button--primary-stroke:active:not(:disabled){background:var(--bg-hover);box-shadow:none}.rm-button--discovery-fill{background:linear-gradient(180deg,rgba(var(--discovery-rgb),.8) 0,rgba(var(--discovery-rgb),.9) 100%);border-color:var(--color-discovery);box-shadow:0 2px 3px 0 rgba(var(--discovery-rgb),.5);color:var(--color-text-inverse)}.rm-button--discovery-fill:hover:not(:disabled){background:linear-gradient(180deg,var(--color-discovery) 0,var(--color-discovery) 100%);border-color:var(--color-discovery);box-shadow:none}.rm-button--discovery-fill:active:not(:disabled){background:linear-gradient(180deg,var(--color-discovery-active) 0,var(--color-discovery-active) 100%);box-shadow:none}.rm-button--discovery-stroke{box-shadow:0 2px 3px 0 rgba(var(--discovery-rgb),.5);color:var(--color-discovery)}.rm-button--discovery-stroke,.rm-button--discovery-stroke:hover:not(:disabled){background:linear-gradient(180deg,rgba(var(--surface-rgb),.5) 0,rgba(var(--surface-rgb),.9) 100%);border-color:var(--color-discovery)}.rm-button--discovery-stroke:hover:not(:disabled){box-shadow:none}.rm-button--discovery-stroke:active:not(:disabled){background:rgba(var(--discovery-rgb),.05);box-shadow:none}.rm-button--databank-fill{background:linear-gradient(180deg,rgba(var(--databank-rgb),.8) 0,rgba(var(--databank-rgb),.9) 100%);border-color:var(--color-databank);box-shadow:0 2px 3px 0 rgba(var(--databank-rgb),.5);color:var(--color-text-inverse)}.rm-button--databank-fill:hover:not(:disabled){background:linear-gradient(180deg,var(--color-databank) 0,var(--color-databank) 100%);border-color:var(--color-databank);box-shadow:none}.rm-button--databank-fill:active:not(:disabled){background:linear-gradient(180deg,var(--color-databank-active) 0,var(--color-databank-active) 100%);box-shadow:none}.rm-button--databank-stroke{box-shadow:0 2px 3px 0 rgba(var(--databank-rgb),.5);color:var(--color-databank)}.rm-button--databank-stroke,.rm-button--databank-stroke:hover:not(:disabled){background:linear-gradient(180deg,rgba(var(--surface-rgb),.5) 0,rgba(var(--surface-rgb),.9) 100%);border-color:var(--color-databank)}.rm-button--databank-stroke:hover:not(:disabled){box-shadow:none}.rm-button--databank-stroke:active:not(:disabled){background:rgba(var(--databank-rgb),.05);box-shadow:none}.rm-button--pharma-fill{background:linear-gradient(180deg,var(--color-pharma-soft) 0,var(--color-pharma) 100%);border-color:var(--color-pharma);box-shadow:0 2px 3px 0 rgba(var(--pharma-rgb),.5);color:var(--color-text-inverse)}.rm-button--pharma-fill:hover:not(:disabled){background:linear-gradient(180deg,var(--color-pharma) 0,var(--color-pharma) 100%);border-color:var(--color-pharma);box-shadow:none}.rm-button--pharma-fill:active:not(:disabled){background:linear-gradient(180deg,var(--color-pharma-active) 0,var(--color-pharma-active) 100%);box-shadow:none}.rm-button--pharma-stroke{box-shadow:0 2px 3px 0 rgba(var(--pharma-rgb),.5);color:var(--color-pharma)}.rm-button--pharma-stroke,.rm-button--pharma-stroke:hover:not(:disabled){background:linear-gradient(180deg,rgba(var(--surface-rgb),.5) 0,rgba(var(--surface-rgb),.9) 100%);border-color:var(--color-pharma)}.rm-button--pharma-stroke:hover:not(:disabled){box-shadow:none}.rm-button--pharma-stroke:active:not(:disabled){background:rgba(var(--pharma-rgb),.05);box-shadow:none}.rm-button--secondary{background-color:var(--bg-surface);border-color:var(--border-default);color:var(--text-secondary)}.rm-button--secondary:active:not(:disabled),.rm-button--secondary:hover:not(:disabled){background-color:var(--bg-surface-3);border-color:var(--color-violet-pale);color:var(--text-primary)}.rm-button--outline{background-color:transparent;border-color:var(--border-default);color:var(--text-secondary)}.rm-button--outline:hover:not(:disabled){background-color:var(--bg-surface-2);border-color:var(--text-muted)}.rm-button--outline:active:not(:disabled){background-color:var(--bg-surface-3)}.rm-button--danger{background-color:var(--bg-surface);border-color:var(--color-danger);color:var(--color-danger)}.rm-button--danger:active:not(:disabled),.rm-button--danger:hover:not(:disabled){background-color:var(--color-danger);color:var(--color-text-inverse)}.rm-button--ghost{background-color:transparent;color:var(--text-secondary)}.rm-button--ghost:hover:not(:disabled){background-color:var(--bg-surface-3)}.rm-button--ghost:active:not(:disabled){background-color:var(--border-default)}.rm-button--block{width:100%}
154
+ .rm-button{align-items:center;border:1px solid transparent;border-radius:8px;cursor:pointer;display:inline-flex;font-family:inherit;font-size:13px;font-weight:600;gap:6px;justify-content:center;padding:9px 16px;transition:background var(--duration-fast,.15s),color var(--duration-fast,.15s),border-color var(--duration-fast,.15s);-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.rm-button:focus-visible{outline:3px solid var(--color-secondary);outline-offset:2px}.rm-button:disabled{cursor:not-allowed;opacity:.5;pointer-events:none}.rm-button--loading{cursor:progress}.rm-button__spinner{animation:rm-button-spin .9s linear infinite}@keyframes rm-button-spin{to{transform:rotate(1turn)}}.rm-button--primary{background:var(--color-primary);border-color:var(--color-primary);color:var(--color-text-inverse)}.rm-button--primary:hover:not(:disabled){background:var(--color-primary-hover);border-color:var(--color-primary-hover)}.rm-button--secondary{background:var(--bg-surface);border-color:var(--border-default);color:var(--text-secondary)}.rm-button--secondary:hover:not(:disabled){background:var(--bg-surface-3);border-color:var(--color-violet-pale);color:var(--text-primary)}.rm-button--tertiary{background:var(--bg-surface-3);color:var(--text-secondary)}.rm-button--tertiary:hover:not(:disabled){background:var(--bg-hover);color:var(--color-secondary)}.rm-button--text,.rm-button--text-danger{background:transparent;border:none;padding:4px 0;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px}.rm-button--text{color:var(--color-primary)}.rm-button--text:hover:not(:disabled){color:var(--color-primary-hover);text-decoration-thickness:2px}.rm-button--text-danger{color:var(--color-danger)}.rm-button--text-danger:hover:not(:disabled){color:var(--color-danger);opacity:.8;text-decoration-thickness:2px}.rm-button--danger{background:var(--bg-surface);border-color:var(--color-danger);color:var(--color-danger)}.rm-button--danger:hover:not(:disabled){background:var(--color-danger);color:var(--color-text-inverse)}.rm-button--accent-healthcare{background:var(--color-healthcare);border-color:var(--color-healthcare)}.rm-button--accent-discovery{background:var(--color-discovery);border-color:var(--color-discovery)}.rm-button--accent-databank{background:var(--color-databank);border-color:var(--color-databank)}.rm-button--accent-pharma{background:var(--color-pharma);border-color:var(--color-pharma)}.rm-button--sm{font-size:12px;padding:6px 12px}.rm-button--lg{font-size:14px;padding:12px 22px}.rm-button--text-danger.rm-button--lg,.rm-button--text-danger.rm-button--sm,.rm-button--text.rm-button--lg,.rm-button--text.rm-button--sm{padding:4px 0}.rm-button--pill{border-radius:var(--radius-pill,999px)}.rm-button--block{display:flex;width:100%}.rm-button--icon{background:var(--bg-surface);border-color:var(--border-default);color:var(--text-secondary);height:32px;padding:0;width:32px}.rm-button--icon:hover:not(:disabled){background:var(--bg-hover);border-color:var(--color-violet-pale);color:var(--color-secondary)}.rm-button--icon.rm-button--sm{height:28px;width:28px}.rm-button--icon.rm-button--lg{height:40px;width:40px}.rm-button--ghost{background:transparent;border-color:transparent}.rm-button--ghost:hover:not(:disabled){background:var(--bg-hover);border-color:transparent}
63
155
  </style>
@@ -29,7 +29,21 @@ const props = withDefaults(defineProps<{
29
29
  description: undefined,
30
30
  })
31
31
 
32
+ /**
33
+ * ECharts arrive en import dynamique : le premier graphe d'une session attend
34
+ * le telechargement du morceau, plusieurs centaines de kilooctets, pendant
35
+ * lesquelles la carte n'est qu'un vide de la hauteur du graphe. Un squelette
36
+ * tient la place le temps du premier trace.
37
+ *
38
+ * Le module est garde ici, hors du composant : une fois charge, les graphes
39
+ * suivants s'initialisent sans attente et ne doivent PAS faire clignoter un
40
+ * squelette d'une frame. `pending` part donc de l'etat du cache, pas de
41
+ * `true`.
42
+ */
43
+ let echartsModule: typeof import('echarts') | null = null
44
+
32
45
  const el = ref<HTMLElement | null>(null)
46
+ const pending = ref(echartsModule === null)
33
47
  const chart = shallowRef<{ setOption: (o: unknown, n?: boolean) => void, resize: () => void, dispose: () => void } | null>(null)
34
48
  let observer: ResizeObserver | null = null
35
49
 
@@ -38,9 +52,12 @@ const heightPx = () =>
38
52
 
39
53
  onMounted(async () => {
40
54
  if (!el.value) return
41
- const echarts = await import('echarts')
42
- chart.value = echarts.init(el.value, undefined, { renderer: 'svg' })
55
+ echartsModule = echartsModule ?? await import('echarts')
56
+ // Le composant peut avoir ete demonte pendant l'attente du morceau.
57
+ if (!el.value) return
58
+ chart.value = echartsModule.init(el.value, undefined, { renderer: 'svg' })
43
59
  chart.value.setOption(props.option)
60
+ pending.value = false
44
61
 
45
62
  // ResizeObserver plutot qu'un listener global sur window : un graphe dans un
46
63
  // panneau repliable ou une grille change de taille sans que la fenetre bouge.
@@ -65,11 +82,14 @@ defineExpose({ instance: chart })
65
82
 
66
83
  <template>
67
84
  <figure class="rm-chart">
68
- <div ref="el" class="echart" :style="{ height: `${heightPx()}px` }" role="img" :aria-label="description" />
85
+ <div class="rm-chart__frame" :style="{ height: `${heightPx()}px` }">
86
+ <div ref="el" class="echart" :style="{ height: `${heightPx()}px` }" role="img" :aria-label="description" />
87
+ <RmSkeleton v-if="pending" class="rm-chart__skeleton" width="100%" :height="heightPx()" radius="8px" />
88
+ </div>
69
89
  <figcaption v-if="description" class="sr-only">{{ description }}</figcaption>
70
90
  </figure>
71
91
  </template>
72
92
 
73
93
  <style scoped>
74
- .rm-chart{margin:0}.echart{width:100%}
94
+ .rm-chart{margin:0}.rm-chart__frame{position:relative}.echart{width:100%}.rm-chart__skeleton{inset:0;position:absolute}
75
95
  </style>
@@ -10,7 +10,8 @@
10
10
 
11
11
  Slots : #CardTitle (libelle de la puce si `customTitle`), #Rail (sur la
12
12
  barre, a cote de la puce : le reglage qu'on change tout le temps et qui ne
13
- merite pas d'ouvrir le panneau), #Search, #Filters.
13
+ merite pas d'ouvrir le panneau ; il passe sur sa propre rangee quand la page
14
+ fournit aussi #Search), #Search, #Filters.
14
15
  Le contenu projete vient encore, dans les apps, des champs Auriga
15
16
  (arg-advanced-search-bar) : les regles `:deep` en bas les remettent a la
16
17
  hauteur et au rayon de la rangee. Elles tomberont avec ces champs.
@@ -73,6 +74,14 @@ const slots = useSlots()
73
74
  const hasAdvanced = computed(() => !!slots.Advanced)
74
75
  const hasCustomize = computed(() => !!slots.Customize)
75
76
  const hasRail = computed(() => !!slots.Rail)
77
+ const hasSearch = computed(() => !!slots.Search)
78
+ /**
79
+ * Le rail ne descend sur sa propre rangee que si la barre porte AUSSI un champ
80
+ * de recherche : c'est le champ qui lui dispute la place, pas les puces. Sans
81
+ * champ, il tient a cote de « Filtres » et une rangee de plus ne ferait
82
+ * qu'eloigner le contenu du bandeau.
83
+ */
84
+ const railBelow = computed(() => hasRail.value && hasSearch.value)
76
85
 
77
86
  function toggle(which: RmFilterBarPanel) {
78
87
  panel.value = panel.value === which ? null : which
@@ -178,6 +187,12 @@ onBeforeUnmount(() => {
178
187
  <span>{{ t.customize }}</span>
179
188
  </button>
180
189
 
190
+ <!-- Rail pose sur la rangee des puces : cas ordinaire, une page sans
191
+ champ de recherche. -->
192
+ <div v-if="hasRail && !railBelow" class="rm-fbar__rail">
193
+ <slot name="Rail" />
194
+ </div>
195
+
181
196
  <div class="rm-fbar__search">
182
197
  <slot name="Search" />
183
198
 
@@ -202,13 +217,13 @@ onBeforeUnmount(() => {
202
217
  A reserver a ce qu'on change a chaque lecture ; tout le reste va
203
218
  dans le panneau, sinon la barre redevient un panneau ouvert.
204
219
 
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">
220
+ Sa propre rangee UNIQUEMENT face a un champ de recherche : les deux
221
+ se disputaient la premiere, un rail de huit segments (les niveaux
222
+ d'agregation, du produit au fabricant) sortait du cadre par la droite,
223
+ ses derniers segments coupes, et le champ tombait a sa largeur
224
+ minimale. Chacun sur sa ligne prend alors toute la largeur de la
225
+ colonne. -->
226
+ <div v-if="railBelow" class="rm-fbar__rail-row">
212
227
  <div class="rm-fbar__rail">
213
228
  <slot name="Rail" />
214
229
  </div>
@@ -1,6 +1,31 @@
1
+ <!--
2
+ RmTable - la table du design system.
3
+
4
+ Des qu'il faut comparer plusieurs entites sur plusieurs dimensions. Pour une
5
+ seule metrique, c'est un KPI ; pour une liste sans structure de colonnes,
6
+ c'est une `<ul>`.
7
+
8
+ Ce que la reference impose et que le composant tient a votre place :
9
+
10
+ - Un nom accessible. `caption` est pose en `<caption>`, masque a l'oeil par
11
+ defaut parce que le titre de la carte le porte deja a l'ecran. Sans lui,
12
+ un lecteur d'ecran annonce « tableau » et rien d'autre.
13
+ - `scope="col"` sur chaque en-tete, et la colonne d'identite rendue en
14
+ `<th scope="row">` : la cellule lue en premier nomme la ligne au lieu de
15
+ reciter la premiere valeur.
16
+ - `aria-sort` sur les colonnes triables, et le tri declenche par un vrai
17
+ bouton : un `<th>` avec un `@click` ne s'atteint pas au clavier.
18
+ - Pas de bordure verticale. La zebre suffit a suivre une ligne, les traits
19
+ verticaux ajoutent du bruit a chaque colonne.
20
+ - Les nombres a droite en chiffres a chasse fixe : sans cela les unites ne
21
+ tombent pas les unes sous les autres et la colonne ne se compare plus.
22
+
23
+ Les variantes de la reference sont des drapeaux : `compact`, `sticky-header`,
24
+ `selectable`, `expandable`, plus `groupBy` pour les sections et `heat` par
25
+ colonne pour la carte de chaleur.
26
+ -->
1
27
  <script setup lang="ts">
2
28
  import { computed, ref } from 'vue'
3
- import { PhCaretUp, PhCaretDown } from '@phosphor-icons/vue'
4
29
 
5
30
  export interface RmTableColumn {
6
31
  key: string
@@ -8,30 +33,70 @@ export interface RmTableColumn {
8
33
  sortable?: boolean
9
34
  align?: 'left' | 'center' | 'right'
10
35
  width?: string
36
+ /**
37
+ * Colonne de nombres : alignee a droite, chiffres a chasse fixe. C'est ce
38
+ * qui aligne les unites entre elles et rend la colonne comparable d'un coup
39
+ * d'oeil.
40
+ */
41
+ numeric?: boolean
42
+ /**
43
+ * Colonne d'identite : la cellule sort en `<th scope="row">`. Une seule
44
+ * colonne par table, la premiere en general (le pays, le produit).
45
+ */
46
+ rowHeader?: boolean
47
+ /**
48
+ * Carte de chaleur : renvoie le niveau de 1 (bas) a 5 (haut), ou `null`
49
+ * pour laisser la cellule nue. La valeur reste ecrite dans la cellule, la
50
+ * couleur ne la remplace jamais.
51
+ */
52
+ heat?: (value: any, row: Record<string, any>) => 1 | 2 | 3 | 4 | 5 | null
11
53
  }
12
54
 
13
55
  export interface RmTableProps {
14
56
  columns: RmTableColumn[]
15
57
  rows: Record<string, any>[]
58
+ /** Nom accessible de la table. Masque a l'oeil sauf `caption-visible`. */
59
+ caption?: string
60
+ captionVisible?: boolean
16
61
  sortField?: string
17
62
  sortOrder?: 'asc' | 'desc'
18
63
  striped?: boolean
19
64
  hoverable?: boolean
65
+ /** Rythme dense : 12px de texte, 6/12 de gouttiere. Pour les longues tables. */
66
+ compact?: boolean
20
67
  stickyHeader?: boolean
21
68
  maxHeight?: string
22
69
  loading?: boolean
23
70
  emptyText?: string
71
+ /** Case a cocher par ligne, plus une barre d'actions groupees. */
72
+ selectable?: boolean
73
+ /** Champ qui identifie une ligne. Sans lui, c'est l'index. */
74
+ rowKey?: string
75
+ /** Regroupe les lignes par la valeur de ce champ, sous un intitule de section. */
76
+ groupBy?: string
77
+ /** Intitule d'une section, a partir de la valeur groupee. */
78
+ groupLabel?: (value: any) => string
79
+ /** Chaque ligne se deplie sur un detail, rendu par le slot `expanded`. */
80
+ expandable?: boolean
24
81
  }
25
82
 
26
83
  const props = withDefaults(defineProps<RmTableProps>(), {
84
+ caption: '',
85
+ captionVisible: false,
27
86
  sortField: '',
28
87
  sortOrder: 'asc',
29
88
  striped: true,
30
89
  hoverable: true,
90
+ compact: false,
31
91
  stickyHeader: true,
32
92
  maxHeight: '500px',
33
93
  loading: false,
34
- emptyText: 'No data',
94
+ emptyText: 'Aucune donnée',
95
+ selectable: false,
96
+ rowKey: '',
97
+ groupBy: '',
98
+ groupLabel: undefined,
99
+ expandable: false,
35
100
  })
36
101
 
37
102
  const emit = defineEmits<{
@@ -39,124 +104,259 @@ const emit = defineEmits<{
39
104
  rowClick: [row: Record<string, any>, index: number]
40
105
  }>()
41
106
 
107
+ /** Les lignes retenues, par leur cle. */
108
+ const selected = defineModel<(string | number)[]>('selected', { default: () => [] })
109
+
42
110
  const currentSort = ref(props.sortField)
43
111
  const currentOrder = ref<'asc' | 'desc'>(props.sortOrder)
112
+ const expanded = ref<(string | number)[]>([])
44
113
 
45
114
  function toggleSort(col: RmTableColumn) {
46
115
  if (!col.sortable) return
47
-
48
116
  if (currentSort.value === col.key) {
49
117
  currentOrder.value = currentOrder.value === 'asc' ? 'desc' : 'asc'
50
- } else {
118
+ }
119
+ else {
51
120
  currentSort.value = col.key
52
121
  currentOrder.value = 'asc'
53
122
  }
54
-
55
123
  emit('sort', currentSort.value, currentOrder.value)
56
124
  }
57
125
 
58
- function getCellValue(row: Record<string, any>, key: string): any {
126
+ /** `aria-sort` ne se pose que sur la colonne triee : ailleurs c'est `none`. */
127
+ function ariaSort(col: RmTableColumn) {
128
+ if (!col.sortable) return undefined
129
+ if (currentSort.value !== col.key) return 'none'
130
+ return currentOrder.value === 'asc' ? 'ascending' : 'descending'
131
+ }
132
+
133
+ function cellValue(row: Record<string, any>, key: string): any {
59
134
  return key.split('.').reduce((obj, k) => obj?.[k], row)
60
135
  }
61
136
 
137
+ function keyOf(row: Record<string, any>, index: number) {
138
+ return props.rowKey ? cellValue(row, props.rowKey) : index
139
+ }
140
+
62
141
  const sortedRows = computed(() => {
63
142
  if (!currentSort.value) return props.rows
64
-
65
143
  return [...props.rows].sort((a, b) => {
66
- const aVal = getCellValue(a, currentSort.value) ?? ''
67
- const bVal = getCellValue(b, currentSort.value) ?? ''
68
-
144
+ const aVal = cellValue(a, currentSort.value) ?? ''
145
+ const bVal = cellValue(b, currentSort.value) ?? ''
69
146
  const aStr = Array.isArray(aVal) ? aVal.join(', ') : String(aVal)
70
147
  const bStr = Array.isArray(bVal) ? bVal.join(', ') : String(bVal)
71
-
72
148
  const aNum = Number(aStr)
73
149
  const bNum = Number(bStr)
74
- const isNumeric = !isNaN(aNum) && !isNaN(bNum) && aStr !== '' && bStr !== ''
75
-
76
- const cmp = isNumeric ? aNum - bNum : aStr.localeCompare(bStr)
150
+ const numeric = !Number.isNaN(aNum) && !Number.isNaN(bNum) && aStr !== '' && bStr !== ''
151
+ const cmp = numeric ? aNum - bNum : aStr.localeCompare(bStr)
77
152
  return currentOrder.value === 'asc' ? cmp : -cmp
78
153
  })
79
154
  })
155
+
156
+ /**
157
+ * Les lignes decoupees en sections. Sans `groupBy`, une seule section sans
158
+ * intitule : le rendu reste une suite de `<tr>` dans un seul `<tbody>`.
159
+ */
160
+ const sections = computed(() => {
161
+ const rows = sortedRows.value.map((row, index) => ({ row, index }))
162
+ if (!props.groupBy) return [{ label: '', rows }]
163
+
164
+ const buckets = new Map<any, typeof rows>()
165
+ for (const entry of rows) {
166
+ const value = cellValue(entry.row, props.groupBy)
167
+ if (!buckets.has(value)) buckets.set(value, [])
168
+ buckets.get(value)!.push(entry)
169
+ }
170
+ return [...buckets.entries()].map(([value, group]) => ({
171
+ label: props.groupLabel ? props.groupLabel(value) : String(value ?? ''),
172
+ rows: group,
173
+ }))
174
+ })
175
+
176
+ const colspan = computed(
177
+ () => props.columns.length + (props.selectable ? 1 : 0) + (props.expandable ? 1 : 0),
178
+ )
179
+
180
+ const allKeys = computed(() => sortedRows.value.map((row, index) => keyOf(row, index)))
181
+ const allSelected = computed(
182
+ () => allKeys.value.length > 0 && allKeys.value.every(key => selected.value.includes(key)),
183
+ )
184
+
185
+ function toggleAll() {
186
+ selected.value = allSelected.value ? [] : [...allKeys.value]
187
+ }
188
+
189
+ function toggleRow(key: string | number) {
190
+ selected.value = selected.value.includes(key)
191
+ ? selected.value.filter(k => k !== key)
192
+ : [...selected.value, key]
193
+ }
194
+
195
+ function toggleExpand(key: string | number) {
196
+ expanded.value = expanded.value.includes(key)
197
+ ? expanded.value.filter(k => k !== key)
198
+ : [...expanded.value, key]
199
+ }
200
+
201
+ function heatClass(col: RmTableColumn, row: Record<string, any>) {
202
+ if (!col.heat) return undefined
203
+ const level = col.heat(cellValue(row, col.key), row)
204
+ return level ? `rm-table__td--heat-${level}` : undefined
205
+ }
80
206
  </script>
81
207
 
82
208
  <template>
83
- <div class="rm-table-wrapper" :style="maxHeight !== 'auto' && stickyHeader ? { maxHeight } : undefined">
84
- <table class="rm-table">
85
- <thead class="rm-table__head" :class="{ 'rm-table__head--sticky': stickyHeader }">
86
- <tr>
87
- <th
88
- v-for="col in columns"
89
- :key="col.key"
90
- class="rm-table__th"
91
- :class="[
92
- col.align && `rm-table__th--${col.align}`,
93
- col.sortable && 'rm-table__th--sortable',
94
- ]"
95
- :style="col.width ? { width: col.width } : undefined"
96
- @click="toggleSort(col)"
97
- >
98
- <span class="rm-table__th-content">
99
- {{ col.label }}
100
- <span v-if="col.sortable" class="rm-table__sort">
101
- <PhCaretUp
102
- :size="10"
103
- :weight="currentSort === col.key && currentOrder === 'asc' ? 'bold' : 'regular'"
104
- :class="{ 'rm-table__sort--active': currentSort === col.key && currentOrder === 'asc' }"
105
- />
106
- <PhCaretDown
107
- :size="10"
108
- :weight="currentSort === col.key && currentOrder === 'desc' ? 'bold' : 'regular'"
109
- :class="{ 'rm-table__sort--active': currentSort === col.key && currentOrder === 'desc' }"
110
- />
111
- </span>
112
- </span>
113
- </th>
114
- </tr>
115
- </thead>
116
-
117
- <tbody v-if="loading">
118
- <tr v-for="i in 5" :key="i">
119
- <td v-for="col in columns" :key="col.key" class="rm-table__td">
120
- <div class="rm-skeleton rm-table__skeleton" />
121
- </td>
122
- </tr>
123
- </tbody>
124
-
125
- <tbody v-else-if="sortedRows.length === 0">
126
- <tr>
127
- <td :colspan="columns.length" class="rm-table__empty">
128
- {{ emptyText }}
129
- </td>
130
- </tr>
131
- </tbody>
132
-
133
- <tbody v-else>
134
- <tr
135
- v-for="(row, idx) in sortedRows"
136
- :key="idx"
137
- class="rm-table__row"
138
- :class="{
139
- 'rm-table__row--striped': striped && idx % 2 === 1,
140
- 'rm-table__row--hoverable': hoverable,
141
- }"
142
- @click="emit('rowClick', row, idx)"
143
- >
144
- <td
145
- v-for="col in columns"
146
- :key="col.key"
147
- class="rm-table__td"
148
- :class="col.align && `rm-table__td--${col.align}`"
149
- >
150
- <slot :name="col.key" :value="getCellValue(row, col.key)" :row="row" :index="idx">
151
- {{ getCellValue(row, col.key) ?? '-' }}
152
- </slot>
153
- </td>
154
- </tr>
155
- </tbody>
156
- </table>
209
+ <div class="rm-table-wrapper">
210
+ <!-- La barre d'actions groupees ne sort qu'a partir d'une ligne retenue :
211
+ posee en permanence, elle occuperait une bande vide en haut de table. -->
212
+ <div v-if="selectable && selected.length > 0" class="rm-table__bulk">
213
+ <span class="rm-table__bulk-count">
214
+ <b>{{ selected.length }}</b> {{ selected.length > 1 ? 'lignes retenues' : 'ligne retenue' }}
215
+ </span>
216
+ <span class="rm-table__bulk-actions">
217
+ <slot name="bulk" :selected="selected" />
218
+ </span>
219
+ </div>
220
+
221
+ <div
222
+ class="rm-table__scroll"
223
+ :style="stickyHeader && maxHeight !== 'auto' ? { maxHeight } : undefined"
224
+ >
225
+ <table
226
+ class="rm-table"
227
+ :class="{
228
+ 'rm-table--compact': compact,
229
+ 'rm-table--sticky': stickyHeader,
230
+ 'rm-table--hoverable': hoverable,
231
+ }"
232
+ >
233
+ <caption v-if="caption" :class="{ 'rm-sr-only': !captionVisible }">{{ caption }}</caption>
234
+
235
+ <thead class="rm-table__head">
236
+ <tr>
237
+ <th v-if="selectable" scope="col" class="rm-table__th rm-table__th--check">
238
+ <input
239
+ type="checkbox"
240
+ :checked="allSelected"
241
+ aria-label="Tout sélectionner"
242
+ @change="toggleAll"
243
+ >
244
+ </th>
245
+ <th v-if="expandable" scope="col" class="rm-table__th rm-table__th--expand">
246
+ <span class="rm-sr-only">Détail</span>
247
+ </th>
248
+ <th
249
+ v-for="col in columns"
250
+ :key="col.key"
251
+ scope="col"
252
+ class="rm-table__th"
253
+ :class="[
254
+ (col.numeric ? 'rm-table__th--num' : col.align && `rm-table__th--${col.align}`),
255
+ ]"
256
+ :style="col.width ? { width: col.width } : undefined"
257
+ :aria-sort="ariaSort(col)"
258
+ >
259
+ <!-- Le tri passe par un bouton : un `<th>` cliquable n'est ni
260
+ atteignable au clavier ni annonce comme actionnable. -->
261
+ <button
262
+ v-if="col.sortable"
263
+ type="button"
264
+ class="rm-table__sort"
265
+ @click="toggleSort(col)"
266
+ >
267
+ {{ col.label }}
268
+ </button>
269
+ <template v-else>{{ col.label }}</template>
270
+ </th>
271
+ </tr>
272
+ </thead>
273
+
274
+ <tbody v-if="loading">
275
+ <tr v-for="i in 5" :key="i">
276
+ <td v-for="n in colspan" :key="n" class="rm-table__td">
277
+ <div class="rm-skeleton rm-table__skeleton" />
278
+ </td>
279
+ </tr>
280
+ </tbody>
281
+
282
+ <tbody v-else-if="sortedRows.length === 0">
283
+ <tr>
284
+ <td :colspan="colspan" class="rm-table__empty">{{ emptyText }}</td>
285
+ </tr>
286
+ </tbody>
287
+
288
+ <tbody v-for="(section, s) in sections" v-else :key="s">
289
+ <tr v-if="section.label" class="rm-table__group">
290
+ <th :colspan="colspan" scope="colgroup">{{ section.label }}</th>
291
+ </tr>
292
+
293
+ <template v-for="(entry, i) in section.rows" :key="keyOf(entry.row, entry.index)">
294
+ <!-- La zebre suit le rang dans la section, pas la position dans le
295
+ DOM : les lignes de detail depliees decaleraient la parite. -->
296
+ <tr
297
+ class="rm-table__row"
298
+ :class="{
299
+ 'rm-table__row--striped': striped && i % 2 === 1,
300
+ 'rm-table__row--selected': selectable && selected.includes(keyOf(entry.row, entry.index)),
301
+ 'rm-table__row--expandable': expandable,
302
+ }"
303
+ @click="expandable ? toggleExpand(keyOf(entry.row, entry.index)) : emit('rowClick', entry.row, entry.index)"
304
+ >
305
+ <td v-if="selectable" class="rm-table__td rm-table__td--check" @click.stop>
306
+ <input
307
+ type="checkbox"
308
+ :checked="selected.includes(keyOf(entry.row, entry.index))"
309
+ :aria-label="`Retenir la ligne ${i + 1}`"
310
+ @change="toggleRow(keyOf(entry.row, entry.index))"
311
+ >
312
+ </td>
313
+
314
+ <td v-if="expandable" class="rm-table__td rm-table__td--expand">
315
+ <span
316
+ class="rm-table__caret"
317
+ :class="{ 'is-open': expanded.includes(keyOf(entry.row, entry.index)) }"
318
+ >
319
+ <RmIcon name="CaretRight" :size="14" />
320
+ </span>
321
+ </td>
322
+
323
+ <component
324
+ :is="col.rowHeader ? 'th' : 'td'"
325
+ v-for="col in columns"
326
+ :key="col.key"
327
+ :scope="col.rowHeader ? 'row' : undefined"
328
+ class="rm-table__td"
329
+ :class="[
330
+ (col.numeric ? 'rm-table__td--num' : col.align && `rm-table__td--${col.align}`),
331
+ heatClass(col, entry.row),
332
+ ]"
333
+ >
334
+ <slot
335
+ :name="col.key"
336
+ :value="cellValue(entry.row, col.key)"
337
+ :row="entry.row"
338
+ :index="entry.index"
339
+ >{{ cellValue(entry.row, col.key) ?? '-' }}</slot>
340
+ </component>
341
+ </tr>
342
+
343
+ <tr
344
+ v-if="expandable && expanded.includes(keyOf(entry.row, entry.index))"
345
+ class="rm-table__detail"
346
+ >
347
+ <td :colspan="colspan">
348
+ <div class="rm-table__detail-body">
349
+ <slot name="expanded" :row="entry.row" :index="entry.index" />
350
+ </div>
351
+ </td>
352
+ </tr>
353
+ </template>
354
+ </tbody>
355
+ </table>
356
+ </div>
157
357
  </div>
158
358
  </template>
159
359
 
160
360
  <style scoped>
161
- .rm-table-wrapper{background:#fff;border:1px solid #dbdef0;border-radius:8px;flex:1;overflow:auto}.rm-table{border-collapse:collapse;font-family:var(--r-font-family);font-size:var(--r-text-sm);width:100%}.rm-table__head{background:var(--r-color-gray-50)}.rm-table__head--sticky{position:sticky;top:0;z-index:2}.rm-table__th{border-bottom:1px solid #dbdef0;color:var(--r-color-gray-500);font-size:12px;font-weight:600;letter-spacing:.3px;padding:10px 16px;text-align:left;text-transform:uppercase;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.rm-table__th--center{text-align:center}.rm-table__th--right{text-align:right}.rm-table__th--sortable{cursor:pointer}.rm-table__th--sortable:hover{color:var(--r-color-gray-700)}.rm-table__th-content{align-items:center;display:inline-flex;gap:4px}.rm-table__sort{color:var(--r-color-gray-300);display:inline-flex;flex-direction:column;gap:-2px}.rm-table__sort--active{color:#4164ee}.rm-table__td{border-bottom:1px solid var(--r-color-gray-100);color:var(--r-color-gray-700);padding:10px 16px;vertical-align:top}.rm-table__td--center{text-align:center}.rm-table__td--right{text-align:right}.rm-table__row:last-child .rm-table__td{border-bottom:none}.rm-table__row--striped{background-color:var(--r-color-gray-50)}.rm-table__row--hoverable:hover{background-color:rgba(65,100,238,.06);cursor:pointer}.rm-table__empty{color:var(--text-muted);padding:32px 16px;text-align:center}.rm-table__skeleton{height:14px;width:80%}
361
+ .rm-table-wrapper{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md,8px);flex:1;overflow:hidden}.rm-table__scroll{overflow:auto}.rm-table{border-collapse:collapse;font-size:var(--font-13,13px);width:100%}.rm-sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;white-space:nowrap}.rm-table caption{caption-side:top;color:var(--text-primary);font-size:var(--font-13,13px);font-weight:700;padding:0 0 var(--space-8,8px);text-align:left}.rm-table__th{background:var(--bg-surface-2);border-bottom:1px solid var(--border-default);color:var(--text-secondary);font-size:var(--font-11,11px);font-weight:700;letter-spacing:.08em;padding:12px 14px;text-align:left;text-transform:uppercase;white-space:nowrap}.rm-table--sticky .rm-table__th{border-bottom:none;box-shadow:inset 0 -1px 0 var(--border-default);position:sticky;top:0;z-index:2}.rm-table__th--center{text-align:center}.rm-table__th--num,.rm-table__th--right{text-align:right}.rm-table__th--check,.rm-table__th--expand{padding-right:0;width:36px}.rm-table__sort{align-items:center;background:none;border:none;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:var(--space-6,6px);letter-spacing:inherit;padding:0;text-transform:inherit}.rm-table__sort:hover{color:var(--text-primary)}.rm-table__sort:after{border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid var(--text-muted);content:"";display:inline-block;height:0;opacity:.5;vertical-align:middle;width:0}.rm-table__th[aria-sort=ascending] .rm-table__sort:after{border-bottom:5px solid var(--color-primary);border-top:none;opacity:1}.rm-table__th[aria-sort=descending] .rm-table__sort:after{border-top:5px solid var(--color-primary);opacity:1}.rm-table a:focus-visible,.rm-table button:focus-visible{border-radius:var(--radius-sm,4px);outline:2px solid var(--color-primary);outline-offset:2px}.rm-table__td{border-bottom:1px solid var(--border-soft);color:var(--text-primary);padding:12px 14px;text-align:left;vertical-align:top}th.rm-table__td{background:transparent;font-weight:700;letter-spacing:0;text-transform:none}.rm-table__td--center{text-align:center}.rm-table__td--right{text-align:right}.rm-table__td--num{font-variant-numeric:tabular-nums;text-align:right}.rm-table__td--check,.rm-table__td--expand{color:var(--text-muted);padding-right:0;width:36px}.rm-table__row--striped>.rm-table__td{background:var(--bg-surface-2)}.rm-table--hoverable .rm-table__row:hover>.rm-table__td,.rm-table__row--selected>.rm-table__td{background:var(--bg-hover)}.rm-table__row--expandable{cursor:pointer}.rm-table__caret{display:inline-flex;transition:transform .16s var(--ease-out,ease)}.rm-table__caret.is-open{color:var(--color-primary);transform:rotate(90deg)}.rm-table__group>th{background:var(--bg-hover);color:var(--color-primary);font-size:var(--font-11,11px);font-weight:700;letter-spacing:.08em;padding:var(--space-8,8px) var(--space-12,12px);text-align:left;text-transform:uppercase}.rm-table__detail>td{background:var(--bg-surface-2);border-bottom:1px solid var(--border-soft);padding:0}.rm-table__detail-body{color:var(--text-secondary);font-size:var(--font-12,12px);line-height:1.6;padding:var(--space-12,12px) var(--space-20,20px)}.rm-table__td--heat-1{background:#fee5e5!important;color:#911616}.rm-table__td--heat-2{background:#ffe7d2!important;color:#8a4309}.rm-table__td--heat-3{background:#fff6cc!important;color:#6b5400}.rm-table__td--heat-4{background:#d8f3dc!important;color:#1b5e25}.rm-table__td--heat-5{background:#b7e4c7!important;color:#0e4b19}:root[data-theme=dark] .rm-table__td--heat-1{background:#3f1414!important;color:#ffb4b4}:root[data-theme=dark] .rm-table__td--heat-2{background:#3d2410!important;color:#ffc58a}:root[data-theme=dark] .rm-table__td--heat-3{background:#3a3210!important;color:#ffe07a}:root[data-theme=dark] .rm-table__td--heat-4{background:#18351f!important;color:#8ae0a4}:root[data-theme=dark] .rm-table__td--heat-5{background:#0f4019!important;color:#b4f0c2}.rm-table--compact .rm-table__td,.rm-table--compact .rm-table__th{font-size:var(--font-12,12px);padding:var(--space-6,6px) var(--space-12,12px)}.rm-table--compact .rm-table__th{font-size:var(--font-10,10px)}.rm-table__bulk{align-items:center;background:var(--bg-hover);border-bottom:1px solid var(--color-violet-pale);display:flex;font-size:var(--font-12,12px);justify-content:space-between;padding:var(--space-8,8px) var(--space-16,16px)}.rm-table__bulk-count b{color:var(--color-primary)}.rm-table__bulk-actions{display:flex;gap:var(--space-4,4px)}.rm-table__empty{color:var(--text-muted);padding:32px 16px;text-align:center}.rm-table__skeleton{height:14px;width:80%}
162
362
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.61",
3
+ "version": "0.1.63",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",