@tekkare/romulus 0.1.11 → 0.1.13
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 +1 -1
- package/dist/module.mjs +5 -0
- package/dist/runtime/assets/styles/chrome.css +1 -0
- package/dist/runtime/assets/styles/components.css +1 -1
- package/dist/runtime/assets/styles/tokens.css +1 -1
- package/dist/runtime/components/AiChat.vue +254 -0
- package/dist/runtime/components/AppShell.vue +68 -0
- package/dist/runtime/components/HybridTable.vue +70 -0
- package/dist/runtime/components/LineChart.vue +7 -0
- package/dist/runtime/components/Modal.vue +79 -0
- package/dist/runtime/components/NavBar.vue +147 -0
- package/dist/runtime/components/SimpleBar.vue +85 -0
- package/dist/runtime/components/Table.vue +161 -0
- package/dist/runtime/components/ToolStrip.vue +73 -0
- package/dist/runtime/components/TopBar.vue +46 -0
- package/dist/runtime/composables/useTheme.js +2 -9
- package/dist/runtime/utils/chart.d.ts +77 -8
- package/dist/runtime/utils/chart.js +23 -3
- package/dist/runtime/utils/transitionGuard.d.ts +24 -0
- package/dist/runtime/utils/transitionGuard.js +14 -0
- package/package.json +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmNavBar - la barre laterale du design system : identite, recherche, sections
|
|
3
|
+
de navigation, pied avec utilisateur et sortie.
|
|
4
|
+
|
|
5
|
+
Markup et classes reprises de `app-shell.html` (`.sidebar`, `.sidebar-top`,
|
|
6
|
+
`.sidebar-search`, `.nav-group`, `.nav-section`, `.nav-item`,
|
|
7
|
+
`.sidebar-footer`). Les icones utilisent la police Phosphor via
|
|
8
|
+
`<i class="ph ph-...">`, comme le design system : les regles de taille
|
|
9
|
+
d'oip.css ciblent cet element, un SVG ne les recevrait pas.
|
|
10
|
+
|
|
11
|
+
Les sections sont un accordeon exclusif : huit vues ne tiennent pas
|
|
12
|
+
depliees. L'etat vit ici, pas dans chaque section.
|
|
13
|
+
-->
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
import { computed, ref, watch } from 'vue'
|
|
16
|
+
|
|
17
|
+
export interface RmNavItem {
|
|
18
|
+
label: string
|
|
19
|
+
/** Icone Phosphor, nom en kebab-case sans le prefixe `ph-`. */
|
|
20
|
+
icon: string
|
|
21
|
+
to: string
|
|
22
|
+
}
|
|
23
|
+
export interface RmNavSection {
|
|
24
|
+
key: string
|
|
25
|
+
label: string
|
|
26
|
+
items: RmNavItem[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(defineProps<{
|
|
30
|
+
/** Nom court affiche a cote du logo. */
|
|
31
|
+
name: string
|
|
32
|
+
/** Sous-titre du logo. */
|
|
33
|
+
subtitle?: string
|
|
34
|
+
sections: RmNavSection[]
|
|
35
|
+
/** Chemin actif, pour marquer l'item courant. */
|
|
36
|
+
activePath?: string
|
|
37
|
+
collapsed?: boolean
|
|
38
|
+
searchPlaceholder?: string
|
|
39
|
+
/** Bloc utilisateur du pied. Masque si absent. */
|
|
40
|
+
user?: { initials: string, name: string, org?: string }
|
|
41
|
+
/** Lien de sortie du pied. */
|
|
42
|
+
exitLabel?: string
|
|
43
|
+
exitHref?: string
|
|
44
|
+
}>(), {
|
|
45
|
+
subtitle: undefined,
|
|
46
|
+
activePath: undefined,
|
|
47
|
+
collapsed: false,
|
|
48
|
+
searchPlaceholder: 'Rechercher...',
|
|
49
|
+
user: undefined,
|
|
50
|
+
exitLabel: undefined,
|
|
51
|
+
exitHref: undefined,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const emit = defineEmits<{
|
|
55
|
+
toggle: []
|
|
56
|
+
navigate: [to: string]
|
|
57
|
+
search: [term: string]
|
|
58
|
+
}>()
|
|
59
|
+
|
|
60
|
+
// Section ouverte : celle qui contient le chemin actif, sinon la premiere.
|
|
61
|
+
const sectionOf = (path?: string) =>
|
|
62
|
+
props.sections.find(s => s.items.some(i => i.to === path))?.key ?? props.sections[0]?.key ?? null
|
|
63
|
+
|
|
64
|
+
const openSection = ref<string | null>(sectionOf(props.activePath))
|
|
65
|
+
watch(() => props.activePath, (path) => {
|
|
66
|
+
const key = sectionOf(path)
|
|
67
|
+
if (key && key !== openSection.value) openSection.value = key
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const term = ref('')
|
|
71
|
+
watch(term, v => emit('search', v))
|
|
72
|
+
|
|
73
|
+
const initials = computed(() => props.user?.initials ?? '')
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<template>
|
|
77
|
+
<aside class="sidebar">
|
|
78
|
+
<div class="sidebar-top">
|
|
79
|
+
<div class="logo-block">
|
|
80
|
+
<div class="logo-mark">{{ name.charAt(0) }}</div>
|
|
81
|
+
<div>
|
|
82
|
+
<div class="logo-text">{{ name }}</div>
|
|
83
|
+
<div v-if="subtitle" class="logo-sub">{{ subtitle }}</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<button
|
|
87
|
+
class="topbar-toggle"
|
|
88
|
+
type="button"
|
|
89
|
+
:aria-label="collapsed ? 'Déplier la barre latérale' : 'Replier la barre latérale'"
|
|
90
|
+
:aria-expanded="!collapsed"
|
|
91
|
+
@click="emit('toggle')"
|
|
92
|
+
>
|
|
93
|
+
<i class="ph ph-sidebar-simple" />
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div class="sidebar-search">
|
|
98
|
+
<i class="ph ph-magnifying-glass" />
|
|
99
|
+
<input v-model="term" type="text" :placeholder="searchPlaceholder" aria-label="Rechercher dans la navigation">
|
|
100
|
+
<kbd>⌘K</kbd>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div v-for="section in sections" :key="section.key" class="nav-group">
|
|
104
|
+
<div class="nav-section">
|
|
105
|
+
<button
|
|
106
|
+
class="nav-section-header"
|
|
107
|
+
type="button"
|
|
108
|
+
:aria-expanded="openSection === section.key"
|
|
109
|
+
@click="openSection = openSection === section.key ? null : section.key"
|
|
110
|
+
>
|
|
111
|
+
<span class="label">{{ section.label }}</span>
|
|
112
|
+
<span class="chev"><i class="ph ph-caret-down" /></span>
|
|
113
|
+
</button>
|
|
114
|
+
<div v-show="openSection === section.key" class="nav-items">
|
|
115
|
+
<a
|
|
116
|
+
v-for="item in section.items"
|
|
117
|
+
:key="item.to"
|
|
118
|
+
class="nav-item"
|
|
119
|
+
:class="{ active: item.to === activePath }"
|
|
120
|
+
:href="item.to"
|
|
121
|
+
:data-tip="collapsed ? item.label : undefined"
|
|
122
|
+
@click.prevent="emit('navigate', item.to)"
|
|
123
|
+
>
|
|
124
|
+
<span class="icn"><i :class="`ph ph-${item.icon}`" /></span>
|
|
125
|
+
<span class="lbl">{{ item.label }}</span>
|
|
126
|
+
</a>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class="sidebar-footer">
|
|
132
|
+
<div v-if="user" class="sb-user">
|
|
133
|
+
<div class="sb-user-avatar">{{ initials }}</div>
|
|
134
|
+
<div class="sb-user-info">
|
|
135
|
+
<div class="sb-user-name">{{ user.name }}</div>
|
|
136
|
+
<div v-if="user.org" class="sb-user-org">{{ user.org }}</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="sb-actions">
|
|
140
|
+
<slot name="footer-actions" />
|
|
141
|
+
<a v-if="exitHref" class="primary" :href="exitHref">
|
|
142
|
+
<i class="ph ph-arrow-up-right" /> {{ exitLabel ?? 'Sortir' }}
|
|
143
|
+
</a>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</aside>
|
|
147
|
+
</template>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SimpleBar — lightweight horizontal bar chart without ApexCharts.
|
|
4
|
+
* Pure CSS, no dependencies. Great for small inline visualizations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface SimpleBarItem {
|
|
8
|
+
label: string
|
|
9
|
+
value: number
|
|
10
|
+
color?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(defineProps<{
|
|
14
|
+
items: SimpleBarItem[]
|
|
15
|
+
title?: string
|
|
16
|
+
showValues?: boolean
|
|
17
|
+
showPercent?: boolean
|
|
18
|
+
height?: number
|
|
19
|
+
}>(), {
|
|
20
|
+
title: undefined,
|
|
21
|
+
showValues: true,
|
|
22
|
+
showPercent: false,
|
|
23
|
+
height: 24,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const { PALETTE } = useChartTheme()
|
|
27
|
+
|
|
28
|
+
const total = computed(() => props.items.reduce((sum, i) => sum + i.value, 0))
|
|
29
|
+
const maxValue = computed(() => Math.max(...props.items.map(i => i.value), 1))
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<div class="simple-bar">
|
|
34
|
+
<h3 v-if="title" class="simple-bar__title">{{ title }}</h3>
|
|
35
|
+
|
|
36
|
+
<!-- Stacked bar -->
|
|
37
|
+
<div v-if="showPercent" class="simple-bar__stacked" :style="{ height: `${height}px` }">
|
|
38
|
+
<div
|
|
39
|
+
v-for="(item, idx) in items"
|
|
40
|
+
:key="idx"
|
|
41
|
+
class="simple-bar__segment"
|
|
42
|
+
:style="{
|
|
43
|
+
width: `${(item.value / total) * 100}%`,
|
|
44
|
+
backgroundColor: item.color ?? PALETTE[idx % PALETTE.length],
|
|
45
|
+
}"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Individual bars -->
|
|
50
|
+
<div class="simple-bar__list">
|
|
51
|
+
<div
|
|
52
|
+
v-for="(item, idx) in items"
|
|
53
|
+
:key="idx"
|
|
54
|
+
class="simple-bar__item"
|
|
55
|
+
>
|
|
56
|
+
<div class="simple-bar__item-header">
|
|
57
|
+
<span
|
|
58
|
+
class="simple-bar__dot"
|
|
59
|
+
:style="{ backgroundColor: item.color ?? PALETTE[idx % PALETTE.length] }"
|
|
60
|
+
/>
|
|
61
|
+
<span class="simple-bar__label">{{ item.label }}</span>
|
|
62
|
+
<span v-if="showValues" class="simple-bar__value">
|
|
63
|
+
{{ item.value.toLocaleString() }}
|
|
64
|
+
<span v-if="showPercent" class="simple-bar__pct">
|
|
65
|
+
({{ total > 0 ? ((item.value / total) * 100).toFixed(1) : 0 }}%)
|
|
66
|
+
</span>
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
<div v-if="!showPercent" class="simple-bar__track" :style="{ height: `${height}px` }">
|
|
70
|
+
<div
|
|
71
|
+
class="simple-bar__fill"
|
|
72
|
+
:style="{
|
|
73
|
+
width: `${(item.value / maxValue) * 100}%`,
|
|
74
|
+
backgroundColor: item.color ?? PALETTE[idx % PALETTE.length],
|
|
75
|
+
}"
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<style scoped>
|
|
84
|
+
.simple-bar{font-family:var(--r-font-family)}.simple-bar__title{color:var(--r-color-gray-700);font-size:14px;font-weight:600;margin:0 0 12px}.simple-bar__stacked{border-radius:6px;display:flex;margin-bottom:12px;overflow:hidden}.simple-bar__segment{min-width:2px;transition:width .4s ease}.simple-bar__segment:first-child{border-radius:6px 0 0 6px}.simple-bar__segment:last-child{border-radius:0 6px 6px 0}.simple-bar__list{display:flex;flex-direction:column;gap:8px}.simple-bar__item-header{align-items:center;display:flex;gap:8px;margin-bottom:4px}.simple-bar__dot{border-radius:50%;flex-shrink:0;height:8px;width:8px}.simple-bar__label{color:var(--r-color-gray-700);flex:1;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.simple-bar__value{color:var(--r-color-gray-900);flex-shrink:0;font-size:13px;font-weight:600}.simple-bar__pct{color:var(--r-color-gray-400);font-weight:400}.simple-bar__track{background:var(--r-color-gray-50);border-radius:4px;overflow:hidden}.simple-bar__fill{border-radius:4px;height:100%;min-width:2px;transition:width .4s ease}
|
|
85
|
+
</style>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { PhCaretUp, PhCaretDown } from '@phosphor-icons/vue'
|
|
3
|
+
|
|
4
|
+
export interface RmTableColumn {
|
|
5
|
+
key: string
|
|
6
|
+
label: string
|
|
7
|
+
sortable?: boolean
|
|
8
|
+
align?: 'left' | 'center' | 'right'
|
|
9
|
+
width?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RmTableProps {
|
|
13
|
+
columns: RmTableColumn[]
|
|
14
|
+
rows: Record<string, any>[]
|
|
15
|
+
sortField?: string
|
|
16
|
+
sortOrder?: 'asc' | 'desc'
|
|
17
|
+
striped?: boolean
|
|
18
|
+
hoverable?: boolean
|
|
19
|
+
stickyHeader?: boolean
|
|
20
|
+
maxHeight?: string
|
|
21
|
+
loading?: boolean
|
|
22
|
+
emptyText?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const props = withDefaults(defineProps<RmTableProps>(), {
|
|
26
|
+
sortField: '',
|
|
27
|
+
sortOrder: 'asc',
|
|
28
|
+
striped: true,
|
|
29
|
+
hoverable: true,
|
|
30
|
+
stickyHeader: true,
|
|
31
|
+
maxHeight: '500px',
|
|
32
|
+
loading: false,
|
|
33
|
+
emptyText: 'No data',
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const emit = defineEmits<{
|
|
37
|
+
sort: [field: string, order: 'asc' | 'desc']
|
|
38
|
+
rowClick: [row: Record<string, any>, index: number]
|
|
39
|
+
}>()
|
|
40
|
+
|
|
41
|
+
const currentSort = ref(props.sortField)
|
|
42
|
+
const currentOrder = ref<'asc' | 'desc'>(props.sortOrder)
|
|
43
|
+
|
|
44
|
+
function toggleSort(col: RmTableColumn) {
|
|
45
|
+
if (!col.sortable) return
|
|
46
|
+
|
|
47
|
+
if (currentSort.value === col.key) {
|
|
48
|
+
currentOrder.value = currentOrder.value === 'asc' ? 'desc' : 'asc'
|
|
49
|
+
} else {
|
|
50
|
+
currentSort.value = col.key
|
|
51
|
+
currentOrder.value = 'asc'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
emit('sort', currentSort.value, currentOrder.value)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getCellValue(row: Record<string, any>, key: string): any {
|
|
58
|
+
return key.split('.').reduce((obj, k) => obj?.[k], row)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const sortedRows = computed(() => {
|
|
62
|
+
if (!currentSort.value) return props.rows
|
|
63
|
+
|
|
64
|
+
return [...props.rows].sort((a, b) => {
|
|
65
|
+
const aVal = getCellValue(a, currentSort.value) ?? ''
|
|
66
|
+
const bVal = getCellValue(b, currentSort.value) ?? ''
|
|
67
|
+
|
|
68
|
+
const aStr = Array.isArray(aVal) ? aVal.join(', ') : String(aVal)
|
|
69
|
+
const bStr = Array.isArray(bVal) ? bVal.join(', ') : String(bVal)
|
|
70
|
+
|
|
71
|
+
const aNum = Number(aStr)
|
|
72
|
+
const bNum = Number(bStr)
|
|
73
|
+
const isNumeric = !isNaN(aNum) && !isNaN(bNum) && aStr !== '' && bStr !== ''
|
|
74
|
+
|
|
75
|
+
const cmp = isNumeric ? aNum - bNum : aStr.localeCompare(bStr)
|
|
76
|
+
return currentOrder.value === 'asc' ? cmp : -cmp
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<template>
|
|
82
|
+
<div class="rm-table-wrapper" :style="maxHeight !== 'auto' && stickyHeader ? { maxHeight } : undefined">
|
|
83
|
+
<table class="rm-table">
|
|
84
|
+
<thead class="rm-table__head" :class="{ 'rm-table__head--sticky': stickyHeader }">
|
|
85
|
+
<tr>
|
|
86
|
+
<th
|
|
87
|
+
v-for="col in columns"
|
|
88
|
+
:key="col.key"
|
|
89
|
+
class="rm-table__th"
|
|
90
|
+
:class="[
|
|
91
|
+
col.align && `rm-table__th--${col.align}`,
|
|
92
|
+
col.sortable && 'rm-table__th--sortable',
|
|
93
|
+
]"
|
|
94
|
+
:style="col.width ? { width: col.width } : undefined"
|
|
95
|
+
@click="toggleSort(col)"
|
|
96
|
+
>
|
|
97
|
+
<span class="rm-table__th-content">
|
|
98
|
+
{{ col.label }}
|
|
99
|
+
<span v-if="col.sortable" class="rm-table__sort">
|
|
100
|
+
<PhCaretUp
|
|
101
|
+
:size="10"
|
|
102
|
+
:weight="currentSort === col.key && currentOrder === 'asc' ? 'bold' : 'regular'"
|
|
103
|
+
:class="{ 'rm-table__sort--active': currentSort === col.key && currentOrder === 'asc' }"
|
|
104
|
+
/>
|
|
105
|
+
<PhCaretDown
|
|
106
|
+
:size="10"
|
|
107
|
+
:weight="currentSort === col.key && currentOrder === 'desc' ? 'bold' : 'regular'"
|
|
108
|
+
:class="{ 'rm-table__sort--active': currentSort === col.key && currentOrder === 'desc' }"
|
|
109
|
+
/>
|
|
110
|
+
</span>
|
|
111
|
+
</span>
|
|
112
|
+
</th>
|
|
113
|
+
</tr>
|
|
114
|
+
</thead>
|
|
115
|
+
|
|
116
|
+
<tbody v-if="loading">
|
|
117
|
+
<tr v-for="i in 5" :key="i">
|
|
118
|
+
<td v-for="col in columns" :key="col.key" class="rm-table__td">
|
|
119
|
+
<div class="rm-table__skeleton" />
|
|
120
|
+
</td>
|
|
121
|
+
</tr>
|
|
122
|
+
</tbody>
|
|
123
|
+
|
|
124
|
+
<tbody v-else-if="sortedRows.length === 0">
|
|
125
|
+
<tr>
|
|
126
|
+
<td :colspan="columns.length" class="rm-table__empty">
|
|
127
|
+
{{ emptyText }}
|
|
128
|
+
</td>
|
|
129
|
+
</tr>
|
|
130
|
+
</tbody>
|
|
131
|
+
|
|
132
|
+
<tbody v-else>
|
|
133
|
+
<tr
|
|
134
|
+
v-for="(row, idx) in sortedRows"
|
|
135
|
+
:key="idx"
|
|
136
|
+
class="rm-table__row"
|
|
137
|
+
:class="{
|
|
138
|
+
'rm-table__row--striped': striped && idx % 2 === 1,
|
|
139
|
+
'rm-table__row--hoverable': hoverable,
|
|
140
|
+
}"
|
|
141
|
+
@click="emit('rowClick', row, idx)"
|
|
142
|
+
>
|
|
143
|
+
<td
|
|
144
|
+
v-for="col in columns"
|
|
145
|
+
:key="col.key"
|
|
146
|
+
class="rm-table__td"
|
|
147
|
+
:class="col.align && `rm-table__td--${col.align}`"
|
|
148
|
+
>
|
|
149
|
+
<slot :name="col.key" :value="getCellValue(row, col.key)" :row="row" :index="idx">
|
|
150
|
+
{{ getCellValue(row, col.key) ?? '—' }}
|
|
151
|
+
</slot>
|
|
152
|
+
</td>
|
|
153
|
+
</tr>
|
|
154
|
+
</tbody>
|
|
155
|
+
</table>
|
|
156
|
+
</div>
|
|
157
|
+
</template>
|
|
158
|
+
|
|
159
|
+
<style scoped>
|
|
160
|
+
.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(--r-color-gray-400);font-style:italic;padding:32px 16px;text-align:center}.rm-table__skeleton{animation:rm-table-shimmer 1.5s infinite;background:linear-gradient(90deg,#eee 25%,#e0e0e0 50%,#eee 75%);background-size:200% 100%;border-radius:4px;height:14px;width:80%}@keyframes rm-table-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}
|
|
161
|
+
</style>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmToolStrip - le strip d'actions fixe a droite. C'est une barre a part
|
|
3
|
+
entiere, pas un panneau replie dans le contenu : tuiles verticales toujours
|
|
4
|
+
visibles, panneau qui se superpose au clic.
|
|
5
|
+
|
|
6
|
+
Markup et classes de `app-shell.html` (`.rsidebar`, `.rstrip-top`,
|
|
7
|
+
`.rstrip-collapsed`, `.strip-icon`, `.strip-label`, `.strip-badge`).
|
|
8
|
+
Le skill recommande de conserver les tuiles Sources et Acces par defaut.
|
|
9
|
+
-->
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
export interface RmStripTile {
|
|
12
|
+
key: string
|
|
13
|
+
label: string
|
|
14
|
+
/** Icone Phosphor, nom en kebab-case sans le prefixe `ph-`. */
|
|
15
|
+
icon: string
|
|
16
|
+
/** Compteur affiche en pastille. Un zero n'est pas affiche. */
|
|
17
|
+
badge?: number | string
|
|
18
|
+
/** Point d'attention, sans compteur. */
|
|
19
|
+
dot?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const props = withDefaults(defineProps<{
|
|
23
|
+
tiles: RmStripTile[]
|
|
24
|
+
/** Tuile dont le panneau est ouvert. */
|
|
25
|
+
active?: string | null
|
|
26
|
+
open?: boolean
|
|
27
|
+
}>(), { active: null, open: false })
|
|
28
|
+
|
|
29
|
+
const emit = defineEmits<{
|
|
30
|
+
select: [key: string]
|
|
31
|
+
toggle: []
|
|
32
|
+
}>()
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<aside class="rsidebar">
|
|
37
|
+
<div class="rstrip-top">
|
|
38
|
+
<button
|
|
39
|
+
class="topbar-toggle"
|
|
40
|
+
type="button"
|
|
41
|
+
aria-label="Panneau d'actions"
|
|
42
|
+
:aria-expanded="open"
|
|
43
|
+
@click="emit('toggle')"
|
|
44
|
+
>
|
|
45
|
+
<i class="ph ph-sidebar-simple" />
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="rstrip-collapsed">
|
|
50
|
+
<button
|
|
51
|
+
v-for="tile in tiles"
|
|
52
|
+
:key="tile.key"
|
|
53
|
+
class="strip-icon"
|
|
54
|
+
:class="{ active: active === tile.key }"
|
|
55
|
+
type="button"
|
|
56
|
+
:data-open-section="tile.key"
|
|
57
|
+
:aria-pressed="active === tile.key"
|
|
58
|
+
@click="emit('select', tile.key)"
|
|
59
|
+
>
|
|
60
|
+
<i :class="`ph ph-${tile.icon}`" />
|
|
61
|
+
<span class="strip-label">{{ tile.label }}</span>
|
|
62
|
+
<span v-if="tile.badge" class="strip-badge">{{ tile.badge }}</span>
|
|
63
|
+
<span v-else-if="tile.dot" class="dot" />
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<slot />
|
|
68
|
+
</aside>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<style scoped>
|
|
72
|
+
.strip-icon,.topbar-toggle{font:inherit;text-align:center}
|
|
73
|
+
</style>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmTopBar - l'en-tete d'analyse : titre de page et onglets de sous-vues.
|
|
3
|
+
|
|
4
|
+
Classes de `app-shell.html` (`.topbar`, `.pane-title`, `.ptabs`, `.ptab`).
|
|
5
|
+
Regle du skill : la sidebar porte les grands chapitres, ces onglets portent
|
|
6
|
+
les sous-analyses du chapitre actif. Ne pas melanger les deux.
|
|
7
|
+
-->
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
export interface RmPaneTab {
|
|
10
|
+
key: string
|
|
11
|
+
label: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
withDefaults(defineProps<{
|
|
15
|
+
title: string
|
|
16
|
+
tabs?: RmPaneTab[]
|
|
17
|
+
active?: string | null
|
|
18
|
+
}>(), { tabs: () => [], active: null })
|
|
19
|
+
|
|
20
|
+
const emit = defineEmits<{ select: [key: string] }>()
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<div class="topbar">
|
|
25
|
+
<h1 class="pane-title">{{ title }}</h1>
|
|
26
|
+
<div v-if="tabs.length" class="ptabs" role="tablist">
|
|
27
|
+
<button
|
|
28
|
+
v-for="tab in tabs"
|
|
29
|
+
:key="tab.key"
|
|
30
|
+
class="ptab"
|
|
31
|
+
:class="{ active: active === tab.key }"
|
|
32
|
+
type="button"
|
|
33
|
+
role="tab"
|
|
34
|
+
:aria-selected="active === tab.key"
|
|
35
|
+
@click="emit('select', tab.key)"
|
|
36
|
+
>
|
|
37
|
+
{{ tab.label }}
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
<slot />
|
|
41
|
+
</div>
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<style scoped>
|
|
45
|
+
.ptab{font:inherit}
|
|
46
|
+
</style>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { onMounted, readonly, ref } from "vue";
|
|
2
|
+
import { rmWithoutTransitions } from "../utils/transitionGuard.js";
|
|
2
3
|
const CVD_CLASSES = {
|
|
3
4
|
deutera: "cvd-deutera",
|
|
4
5
|
protan: "cvd-protan",
|
|
@@ -16,15 +17,7 @@ export function useRomulusTheme() {
|
|
|
16
17
|
const compact = ref(false);
|
|
17
18
|
const cvd = ref("none");
|
|
18
19
|
const textScale = ref("normal");
|
|
19
|
-
|
|
20
|
-
if (typeof document === "undefined") return;
|
|
21
|
-
const { body } = document;
|
|
22
|
-
body.classList.add("theme-switching");
|
|
23
|
-
mutate();
|
|
24
|
-
requestAnimationFrame(() => {
|
|
25
|
-
requestAnimationFrame(() => body.classList.remove("theme-switching"));
|
|
26
|
-
});
|
|
27
|
-
}
|
|
20
|
+
const apply = rmWithoutTransitions;
|
|
28
21
|
const setNight = (value) => apply(() => {
|
|
29
22
|
night.value = value;
|
|
30
23
|
document.body.classList.toggle("night", value);
|
|
@@ -100,6 +100,13 @@ export interface RmSeriesInput {
|
|
|
100
100
|
color?: string;
|
|
101
101
|
/** Cle d'empilement : deux series avec la meme cle s'empilent. */
|
|
102
102
|
stack?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Axe des ordonnees, 0 a gauche et 1 a droite. A n'utiliser que pour deux
|
|
105
|
+
* series d'UNITES differentes : deux axes sur la meme unite laissent croire
|
|
106
|
+
* a un ecart qui n'existe pas. Le libelle de chaque axe est alors
|
|
107
|
+
* obligatoire, sinon le lecteur ne sait pas quelle courbe lire ou.
|
|
108
|
+
*/
|
|
109
|
+
axis?: 0 | 1;
|
|
103
110
|
}
|
|
104
111
|
/** Bar chart vertical, une ou plusieurs series, empilable. */
|
|
105
112
|
export declare function rmBarVerticalOption(categories: (string | number)[], series: RmSeriesInput[], theme: RmChartTheme, palette: string[], decal?: boolean): {
|
|
@@ -279,6 +286,8 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
|
|
|
279
286
|
step?: boolean;
|
|
280
287
|
smooth?: boolean;
|
|
281
288
|
decal?: boolean;
|
|
289
|
+
/** Libelles des deux axes, requis des qu'une serie porte `axis: 1`. */
|
|
290
|
+
axisLabels?: [string, string];
|
|
282
291
|
}): {
|
|
283
292
|
legend: {
|
|
284
293
|
top: number;
|
|
@@ -288,6 +297,13 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
|
|
|
288
297
|
fontFamily: string;
|
|
289
298
|
};
|
|
290
299
|
} | undefined;
|
|
300
|
+
grid: {
|
|
301
|
+
left: number;
|
|
302
|
+
right: number;
|
|
303
|
+
top: number;
|
|
304
|
+
bottom: number;
|
|
305
|
+
containLabel: boolean;
|
|
306
|
+
};
|
|
291
307
|
xAxis: {
|
|
292
308
|
splitLine: {
|
|
293
309
|
show: boolean;
|
|
@@ -307,6 +323,16 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
|
|
|
307
323
|
data: (string | number)[];
|
|
308
324
|
};
|
|
309
325
|
yAxis: {
|
|
326
|
+
type: string;
|
|
327
|
+
name: string | undefined;
|
|
328
|
+
nameLocation: string;
|
|
329
|
+
nameGap: number;
|
|
330
|
+
nameTextStyle: {
|
|
331
|
+
fontSize: number;
|
|
332
|
+
color: string;
|
|
333
|
+
fontFamily: string;
|
|
334
|
+
align: string;
|
|
335
|
+
};
|
|
310
336
|
axisLabel: {
|
|
311
337
|
fontSize: number;
|
|
312
338
|
color: string;
|
|
@@ -322,11 +348,61 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
|
|
|
322
348
|
color: string;
|
|
323
349
|
};
|
|
324
350
|
};
|
|
351
|
+
} | ({
|
|
325
352
|
type: string;
|
|
326
|
-
|
|
353
|
+
name: string | undefined;
|
|
354
|
+
nameLocation: string;
|
|
355
|
+
nameGap: number;
|
|
356
|
+
nameTextStyle: {
|
|
357
|
+
fontSize: number;
|
|
358
|
+
color: string;
|
|
359
|
+
fontFamily: string;
|
|
360
|
+
align: string;
|
|
361
|
+
};
|
|
362
|
+
axisLabel: {
|
|
363
|
+
fontSize: number;
|
|
364
|
+
color: string;
|
|
365
|
+
fontFamily: string;
|
|
366
|
+
};
|
|
367
|
+
axisLine: {
|
|
368
|
+
lineStyle: {
|
|
369
|
+
color: string;
|
|
370
|
+
};
|
|
371
|
+
};
|
|
372
|
+
splitLine: {
|
|
373
|
+
lineStyle: {
|
|
374
|
+
color: string;
|
|
375
|
+
};
|
|
376
|
+
};
|
|
377
|
+
} | {
|
|
378
|
+
splitLine: {
|
|
379
|
+
show: boolean;
|
|
380
|
+
};
|
|
381
|
+
type: string;
|
|
382
|
+
name: string | undefined;
|
|
383
|
+
nameLocation: string;
|
|
384
|
+
nameGap: number;
|
|
385
|
+
nameTextStyle: {
|
|
386
|
+
fontSize: number;
|
|
387
|
+
color: string;
|
|
388
|
+
fontFamily: string;
|
|
389
|
+
align: string;
|
|
390
|
+
};
|
|
391
|
+
axisLabel: {
|
|
392
|
+
fontSize: number;
|
|
393
|
+
color: string;
|
|
394
|
+
fontFamily: string;
|
|
395
|
+
};
|
|
396
|
+
axisLine: {
|
|
397
|
+
lineStyle: {
|
|
398
|
+
color: string;
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
})[];
|
|
327
402
|
series: {
|
|
328
403
|
type: string;
|
|
329
404
|
name: string | undefined;
|
|
405
|
+
yAxisIndex: 0 | 1 | undefined;
|
|
330
406
|
data: (number | null)[];
|
|
331
407
|
smooth: boolean;
|
|
332
408
|
step: string | undefined;
|
|
@@ -364,13 +440,6 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
|
|
|
364
440
|
textStyle: {
|
|
365
441
|
fontFamily: string;
|
|
366
442
|
};
|
|
367
|
-
grid: {
|
|
368
|
-
left: number;
|
|
369
|
-
right: number;
|
|
370
|
-
top: number;
|
|
371
|
-
bottom: number;
|
|
372
|
-
containLabel: boolean;
|
|
373
|
-
};
|
|
374
443
|
tooltip: {
|
|
375
444
|
trigger: string;
|
|
376
445
|
backgroundColor: string;
|