@tekkare/romulus 0.1.8 → 0.1.10
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/runtime/assets/styles/components.css +1 -1
- package/dist/runtime/assets/styles/tokens.css +1 -1
- package/dist/runtime/components/ActionBar.vue +1 -1
- package/dist/runtime/components/AdvancedSearch.vue +29 -10
- package/dist/runtime/components/BarChart.vue +45 -0
- package/dist/runtime/components/Breadcrumb.vue +1 -1
- package/dist/runtime/components/Button.vue +1 -1
- package/dist/runtime/components/Card.vue +1 -1
- package/dist/runtime/components/Chart.vue +75 -0
- package/dist/runtime/components/ChartCard.vue +5 -1
- package/dist/runtime/components/DonutChart.vue +27 -0
- package/dist/runtime/components/Drawer.vue +1 -1
- package/dist/runtime/components/FilterCheckbox.vue +1 -1
- package/dist/runtime/components/FilterComplex.vue +3 -3
- package/dist/runtime/components/FilterDropdown.vue +26 -1
- package/dist/runtime/components/Input.vue +1 -1
- package/dist/runtime/components/LineChart.vue +42 -0
- package/dist/runtime/components/NavLink.vue +1 -1
- package/dist/runtime/components/Pill.vue +1 -1
- package/dist/runtime/components/PillTabs.vue +1 -1
- package/dist/runtime/components/SearchBar.vue +103 -19
- package/dist/runtime/components/Sidebar.vue +1 -1
- package/dist/runtime/components/SparklineChart.vue +33 -0
- package/dist/runtime/components/Tabs.vue +5 -5
- package/dist/runtime/components/ToolbarFilter.vue +1 -1
- package/dist/runtime/composables/useChartTheme.d.ts +7 -0
- package/dist/runtime/composables/useChartTheme.js +27 -0
- package/dist/runtime/composables/useTheme.d.ts +17 -0
- package/dist/runtime/composables/useTheme.js +98 -0
- package/dist/runtime/utils/chart.d.ts +475 -0
- package/dist/runtime/utils/chart.js +209 -0
- package/package.json +3 -2
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmLineChart - courbe, aire, ou marche d'escalier (`step`) pour les series de
|
|
3
|
+
prix et de decisions discretes.
|
|
4
|
+
-->
|
|
5
|
+
<script setup lang="ts">
|
|
6
|
+
import { computed } from 'vue'
|
|
7
|
+
import { useChartTheme } from '../composables/useChartTheme'
|
|
8
|
+
import { rmLineOption, type RmSeriesInput, RM_CHART_HEIGHTS } from '../utils/chart'
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<{
|
|
11
|
+
categories: (string | number)[]
|
|
12
|
+
series: RmSeriesInput[]
|
|
13
|
+
/** Remplissage degrade sous la courbe. */
|
|
14
|
+
area?: boolean
|
|
15
|
+
/** Marche d'escalier : prix, taux de remboursement, decisions datees. */
|
|
16
|
+
step?: boolean
|
|
17
|
+
smooth?: boolean
|
|
18
|
+
height?: keyof typeof RM_CHART_HEIGHTS | number
|
|
19
|
+
description?: string
|
|
20
|
+
}>(), {
|
|
21
|
+
area: false,
|
|
22
|
+
step: false,
|
|
23
|
+
smooth: true,
|
|
24
|
+
height: 'standard',
|
|
25
|
+
description: undefined,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const { theme, palette, decal } = useChartTheme()
|
|
29
|
+
|
|
30
|
+
const option = computed(() =>
|
|
31
|
+
rmLineOption(props.categories, props.series, theme.value, palette.value.cat, {
|
|
32
|
+
area: props.area,
|
|
33
|
+
step: props.step,
|
|
34
|
+
smooth: props.smooth,
|
|
35
|
+
decal: decal.value,
|
|
36
|
+
}),
|
|
37
|
+
)
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<RmChart :option="option" :height="height" :description="description" />
|
|
42
|
+
</template>
|
|
@@ -56,5 +56,5 @@ defineEmits<{
|
|
|
56
56
|
</template>
|
|
57
57
|
|
|
58
58
|
<style scoped>
|
|
59
|
-
.rm-nav-link{background:none;border:none;border-radius:var(--
|
|
59
|
+
.rm-nav-link{background:none;border:none;border-radius:var(--radius-lg);cursor:pointer;gap:8px;padding:6px 12px 6px 6px;text-align:left;width:100%}.rm-nav-link,.rm-nav-link__icon{align-items:center;display:flex;transition:all var(--duration-fast) var(--ease-in-out)}.rm-nav-link__icon{background-color:var(--bg-surface-3);border-radius:var(--radius-md);color:var(--text-secondary);flex-shrink:0;height:28px;justify-content:center;width:28px}.rm-nav-link__label{color:var(--text-primary);flex:1;font-family:var(--font-sans);font-size:14px;font-weight:400;line-height:160%;overflow:hidden;text-overflow:ellipsis;transition:all var(--duration-fast) var(--ease-in-out);white-space:nowrap}.rm-nav-link__caret{align-items:center;color:var(--text-muted);display:flex;flex-shrink:0;transition:transform .25s ease,color var(--duration-fast) var(--ease-in-out)}.rm-nav-link__caret--expanded{transform:rotate(-180deg)}.rm-nav-link:not(.rm-nav-link--active):hover{background-color:var(--bg-hover)}.rm-nav-link:not(.rm-nav-link--active):hover .rm-nav-link__icon{background-color:var(--bg-hover);color:var(--color-healthcare)}.rm-nav-link:not(.rm-nav-link--active):hover .rm-nav-link__caret{color:var(--color-healthcare)}.rm-nav-link--active{background-color:var(--bg-surface-3)}.rm-nav-link--active .rm-nav-link__icon{background-color:var(--color-healthcare);color:var(--color-text-inverse)}.rm-nav-link--active .rm-nav-link__caret{color:var(--color-healthcare)}
|
|
60
60
|
</style>
|
|
@@ -33,5 +33,5 @@ defineEmits<{
|
|
|
33
33
|
</template>
|
|
34
34
|
|
|
35
35
|
<style scoped>
|
|
36
|
-
.toolbar-pill{align-items:center;background-color
|
|
36
|
+
.toolbar-pill{align-items:center;background-color:var(--color-text-inverse);border:1px solid var(--border-default);border-radius:38px;color:var(--text-secondary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-14);font-weight:var(--weight-medium);gap:4px;padding:4px 12px;transition:all var(--duration-fast) var(--ease-in-out);white-space:nowrap}.toolbar-pill:hover{box-shadow:0 1px 2px 0 rgba(var(--shadow-rgb),.1)}.toolbar-pill--active{background-color:var(--color-healthcare);border-color:var(--color-healthcare);color:var(--color-text-inverse)}.toolbar-pill--active:hover{background-color:var(--color-healthcare-active);border-color:var(--color-healthcare-active);box-shadow:none}.toolbar-pill--primary{background-color:var(--color-healthcare);border-color:var(--color-healthcare);color:var(--color-text-inverse)}.toolbar-pill--primary:hover{background-color:var(--color-healthcare-active);border-color:var(--color-healthcare-active);box-shadow:none}.toolbar-pill--secondary{background-color:var(--bg-surface-3);border-color:transparent;color:var(--text-primary)}.toolbar-pill--secondary:hover{background-color:var(--border-default);border-color:transparent}.toolbar-pill--pharma{background-color:var(--color-pharma);border-color:var(--color-pharma);color:var(--color-text-inverse)}.toolbar-pill--pharma:hover{background-color:var(--color-pharma-active);border-color:var(--color-pharma-active)}.toolbar-pill--discovery{background-color:var(--color-discovery);border-color:var(--color-discovery);color:var(--color-text-inverse)}.toolbar-pill--discovery:hover{background-color:var(--color-discovery-active);border-color:var(--color-discovery-active)}.toolbar-pill--databank{background-color:var(--color-databank);border-color:var(--color-databank);color:var(--color-text-inverse)}.toolbar-pill--databank:hover{background-color:var(--color-databank-active);border-color:var(--color-databank-active)}
|
|
37
37
|
</style>
|
|
@@ -30,5 +30,5 @@ defineEmits<{
|
|
|
30
30
|
</template>
|
|
31
31
|
|
|
32
32
|
<style scoped>
|
|
33
|
-
.rm-pill-tabs{display:flex;gap:4px}.rm-pill-tabs__tab{background:transparent;border:none;border-radius:var(--
|
|
33
|
+
.rm-pill-tabs{display:flex;gap:4px}.rm-pill-tabs__tab{background:transparent;border:none;border-radius:var(--radius-lg);cursor:pointer;font-family:var(--font-sans);font-size:14px;font-weight:400;padding:6px 14px;transition:all var(--duration-fast) var(--ease-in-out)}.rm-pill-tabs__tab,.rm-pill-tabs__tab:hover{color:var(--text-secondary)}.rm-pill-tabs__tab--active{background-color:var(--bg-surface-3);color:var(--text-primary);font-weight:600}
|
|
34
34
|
</style>
|
|
@@ -1,38 +1,122 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { PhMagnifyingGlass } from '@phosphor-icons/vue'
|
|
2
|
+
import { PhMagnifyingGlass, PhX } from '@phosphor-icons/vue'
|
|
3
|
+
|
|
4
|
+
export interface SearchSuggestion {
|
|
5
|
+
label: string
|
|
6
|
+
field?: string
|
|
7
|
+
}
|
|
3
8
|
|
|
4
9
|
const model = defineModel<string>({ default: '' })
|
|
5
10
|
|
|
6
|
-
withDefaults(defineProps<{
|
|
11
|
+
const props = withDefaults(defineProps<{
|
|
7
12
|
advancedActive?: boolean
|
|
13
|
+
advancedCount?: number
|
|
14
|
+
suggestions?: SearchSuggestion[]
|
|
15
|
+
placeholder?: string
|
|
8
16
|
}>(), {
|
|
9
17
|
advancedActive: false,
|
|
18
|
+
advancedCount: 0,
|
|
19
|
+
suggestions: () => [],
|
|
20
|
+
placeholder: 'Search...',
|
|
10
21
|
})
|
|
11
22
|
|
|
12
|
-
defineEmits<{
|
|
23
|
+
const emit = defineEmits<{
|
|
13
24
|
advancedSearch: []
|
|
25
|
+
search: [value: string]
|
|
26
|
+
typing: [value: string]
|
|
27
|
+
selectSuggestion: [suggestion: SearchSuggestion]
|
|
14
28
|
}>()
|
|
29
|
+
|
|
30
|
+
const showSuggestions = ref(false)
|
|
31
|
+
const barRef = ref<HTMLElement>()
|
|
32
|
+
let debounceTimer: ReturnType<typeof setTimeout>
|
|
33
|
+
|
|
34
|
+
function onInput() {
|
|
35
|
+
clearTimeout(debounceTimer)
|
|
36
|
+
debounceTimer = setTimeout(() => {
|
|
37
|
+
emit('typing', model.value)
|
|
38
|
+
if (model.value.length >= 2) {
|
|
39
|
+
showSuggestions.value = true
|
|
40
|
+
} else {
|
|
41
|
+
showSuggestions.value = false
|
|
42
|
+
}
|
|
43
|
+
}, 300)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function submitSearch() {
|
|
47
|
+
showSuggestions.value = false
|
|
48
|
+
emit('search', model.value)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function selectSuggestion(sugg: SearchSuggestion) {
|
|
52
|
+
model.value = sugg.label
|
|
53
|
+
showSuggestions.value = false
|
|
54
|
+
emit('selectSuggestion', sugg)
|
|
55
|
+
emit('search', sugg.label)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function clearSearch() {
|
|
59
|
+
model.value = ''
|
|
60
|
+
showSuggestions.value = false
|
|
61
|
+
emit('search', '')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function onClickOutside(e: Event) {
|
|
65
|
+
if (barRef.value && !barRef.value.contains(e.target as Node)) {
|
|
66
|
+
showSuggestions.value = false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
onMounted(() => document.addEventListener('click', onClickOutside))
|
|
71
|
+
onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
15
72
|
</script>
|
|
16
73
|
|
|
17
74
|
<template>
|
|
18
|
-
<div
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
75
|
+
<div ref="barRef" class="search-bar-wrapper">
|
|
76
|
+
<div class="search-bar" :class="{ 'search-bar--advanced-active': advancedActive, 'search-bar--has-value': model.length > 0 }">
|
|
77
|
+
<PhMagnifyingGlass :size="16" class="search-bar__icon" />
|
|
78
|
+
<input
|
|
79
|
+
v-model="model"
|
|
80
|
+
type="text"
|
|
81
|
+
:placeholder="placeholder"
|
|
82
|
+
class="search-bar__input"
|
|
83
|
+
@input="onInput"
|
|
84
|
+
@keydown.enter="submitSearch"
|
|
85
|
+
>
|
|
86
|
+
<button
|
|
87
|
+
v-if="model.length > 0"
|
|
88
|
+
class="search-bar__clear"
|
|
89
|
+
@click="clearSearch"
|
|
90
|
+
>
|
|
91
|
+
<PhX :size="12" />
|
|
92
|
+
</button>
|
|
93
|
+
<button
|
|
94
|
+
class="search-bar__advanced"
|
|
95
|
+
:class="{ 'search-bar__advanced--active': advancedActive }"
|
|
96
|
+
@click="$emit('advancedSearch')"
|
|
97
|
+
>
|
|
98
|
+
Advanced search
|
|
99
|
+
<span v-if="advancedCount > 0" class="search-bar__advanced-badge">{{ advancedCount }}</span>
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<Transition name="suggestions">
|
|
104
|
+
<div v-if="showSuggestions && suggestions.length > 0" class="search-bar__suggestions">
|
|
105
|
+
<button
|
|
106
|
+
v-for="(sugg, i) in suggestions"
|
|
107
|
+
:key="i"
|
|
108
|
+
class="search-bar__suggestion"
|
|
109
|
+
@click="selectSuggestion(sugg)"
|
|
110
|
+
>
|
|
111
|
+
<PhMagnifyingGlass :size="14" class="search-bar__suggestion-icon" />
|
|
112
|
+
<span class="search-bar__suggestion-label">{{ sugg.label }}</span>
|
|
113
|
+
<span v-if="sugg.field" class="search-bar__suggestion-field">{{ sugg.field.split('.').pop() }}</span>
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</Transition>
|
|
33
117
|
</div>
|
|
34
118
|
</template>
|
|
35
119
|
|
|
36
120
|
<style scoped>
|
|
37
|
-
.search-bar{align-items:center;border:1px solid var(--
|
|
121
|
+
.search-bar-wrapper{position:relative}.search-bar{align-items:center;border:1px solid var(--border-default);border-radius:var(--radius-pill);display:inline-flex;gap:var(--space-8);height:32px;min-width:200px;padding:var(--space-4) var(--space-12);transition:border-color var(--duration-fast) var(--ease-in-out)}.search-bar:focus-within{border-color:var(--color-secondary);box-shadow:var(--shadow-focus)}.search-bar__icon{color:var(--text-muted);flex-shrink:0}.search-bar__input{background:transparent;border:none;color:var(--text-secondary);font-family:var(--font-sans);font-size:var(--font-14);outline:none;width:100%}.search-bar__input::-moz-placeholder{color:var(--text-muted)}.search-bar__input::placeholder{color:var(--text-muted)}.search-bar__clear{align-items:center;border:none;border-radius:50%;cursor:pointer;display:flex;flex-shrink:0;height:18px;justify-content:center;transition:all var(--duration-fast) var(--ease-in-out);width:18px}.search-bar__clear,.search-bar__clear:hover{background:var(--border-default);color:var(--text-secondary)}.search-bar__advanced{background:none;border:none;border-left:1px solid var(--border-default);color:var(--text-secondary);cursor:pointer;flex-shrink:0;font-family:var(--font-sans);font-size:var(--font-14);padding:0 0 0 var(--space-8);text-decoration:underline;transition:color var(--duration-fast) var(--ease-in-out);white-space:nowrap}.search-bar__advanced:hover{color:var(--color-healthcare)}.search-bar--advanced-active{border-color:var(--color-healthcare)}.search-bar__advanced--active{color:var(--color-healthcare);font-weight:500}.search-bar__advanced-badge{align-items:center;background-color:var(--color-healthcare);border-radius:100px;color:var(--color-text-inverse);display:inline-flex;font-size:10px;font-weight:600;height:16px;justify-content:center;line-height:1;min-width:16px;padding:0 4px}.search-bar__suggestions{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:8px;box-shadow:0 4px 12px rgba(var(--shadow-rgb),.08);left:0;max-height:280px;overflow-y:auto;padding:4px;position:absolute;right:0;top:calc(100% + 4px);z-index:50}.search-bar__suggestion{align-items:center;background:none;border:none;border-radius:4px;color:var(--text-secondary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);gap:8px;padding:8px 10px;text-align:left;transition:background-color var(--duration-fast) var(--ease-in-out);width:100%}.search-bar__suggestion:hover{background-color:var(--bg-surface-2)}.search-bar__suggestion-icon{color:var(--text-muted);flex-shrink:0}.search-bar__suggestion-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.search-bar__suggestion-field{background:var(--bg-surface-2);border-radius:3px;color:var(--text-muted);flex-shrink:0;font-size:11px;padding:1px 6px}.suggestions-enter-active,.suggestions-leave-active{transition:opacity var(--duration-fast) var(--ease-in-out),transform var(--duration-fast) var(--ease-in-out)}.suggestions-enter-from,.suggestions-leave-to{opacity:0;transform:translateY(-4px)}
|
|
38
122
|
</style>
|
|
@@ -139,5 +139,5 @@ function handleClick(item: RmSidebarMenuItem) {
|
|
|
139
139
|
</template>
|
|
140
140
|
|
|
141
141
|
<style scoped>
|
|
142
|
-
.rm-sidebar{background-color:var(--
|
|
142
|
+
.rm-sidebar{background-color:var(--color-text-inverse);border-right:1px solid var(--border-default);bottom:0;display:flex;flex-direction:column;left:0;overflow-y:auto;position:fixed;top:0;transition:width var(--duration-base) var(--ease-in-out);width:240px;z-index:50}.rm-sidebar--collapsed{width:64px}.rm-sidebar__header{align-items:center;border-bottom:1px solid var(--border-default);display:flex;justify-content:space-between;min-height:64px;padding:0 16px}.rm-sidebar__brand{align-items:center;display:flex;gap:8px}.rm-sidebar__logo{color:var(--color-healthcare)}.rm-sidebar__name{color:var(--text-primary);font-family:var(--font-sans);font-size:16px;font-weight:700;white-space:nowrap}.rm-sidebar__toggle{align-items:center;background:none;border:none;border-radius:var(--radius-md);color:var(--text-secondary);cursor:pointer;display:flex;justify-content:center;padding:4px}.rm-sidebar__toggle:hover{background-color:var(--bg-surface-3)}.rm-sidebar__nav{display:flex;flex:1;flex-direction:column;gap:6px;padding:8px}.rm-sidebar__submenu{display:flex;flex-direction:column;gap:6px;padding-left:20px}.rm-sidebar__submenu-item{animation:rm-submenu-fade-in .25s ease both}@keyframes rm-submenu-fade-in{0%{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:translateX(0)}}.rm-sidebar__footer{border-top:1px solid var(--border-default);padding:12px}
|
|
143
143
|
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmSparklineChart - micro-courbe pour une cellule de tableau ou une tuile KPI.
|
|
3
|
+
Sans axes ni tooltip : elle donne une forme, pas une valeur. La valeur doit
|
|
4
|
+
rester lisible en texte a cote, le survol n'etant jamais le seul acces a
|
|
5
|
+
l'information.
|
|
6
|
+
-->
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { computed } from 'vue'
|
|
9
|
+
import { useChartTheme } from '../composables/useChartTheme'
|
|
10
|
+
import { rmSparklineOption, RM_CHART_HEIGHTS } from '../utils/chart'
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<{
|
|
13
|
+
values: (number | null)[]
|
|
14
|
+
/** Couleur explicite ; par defaut la primaire de la palette active. */
|
|
15
|
+
color?: string
|
|
16
|
+
height?: keyof typeof RM_CHART_HEIGHTS | number
|
|
17
|
+
description?: string
|
|
18
|
+
}>(), {
|
|
19
|
+
color: undefined,
|
|
20
|
+
height: 'sparkline',
|
|
21
|
+
description: undefined,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// Pas de `theme` ici : une sparkline n'a ni axe ni tooltip a colorer. Le
|
|
25
|
+
// basculement jour/nuit la recalcule quand meme, parce que `palette` en depend.
|
|
26
|
+
const { palette } = useChartTheme()
|
|
27
|
+
|
|
28
|
+
const option = computed(() => rmSparklineOption(props.values, props.color ?? palette.value.primary))
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<RmChart :option="option" :height="height" :description="description" />
|
|
33
|
+
</template>
|
|
@@ -13,13 +13,13 @@ const props = withDefaults(defineProps<RmTabsProps>(), {
|
|
|
13
13
|
variant: 'default',
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
const defaultColors = { color: '
|
|
16
|
+
const defaultColors = { color: 'var(--color-healthcare)', bg: 'rgba(var(--healthcare-rgb), 0.05)' }
|
|
17
17
|
|
|
18
18
|
const variantColors: Record<string, { color: string; bg: string }> = {
|
|
19
19
|
default: defaultColors,
|
|
20
|
-
pharma: { color: '
|
|
21
|
-
discovery: { color: '
|
|
22
|
-
databank: { color: '
|
|
20
|
+
pharma: { color: 'var(--color-pharma)', bg: 'rgba(var(--pharma-rgb), 0.05)' },
|
|
21
|
+
discovery: { color: 'var(--color-discovery)', bg: 'rgba(var(--discovery-rgb), 0.05)' },
|
|
22
|
+
databank: { color: 'var(--color-databank)', bg: 'rgba(var(--databank-rgb), 0.05)' },
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const currentColors = computed((): { color: string; bg: string } => variantColors[props.variant] ?? defaultColors)
|
|
@@ -101,5 +101,5 @@ watch(() => props.modelValue, (val) => updateIndicator(val))
|
|
|
101
101
|
</template>
|
|
102
102
|
|
|
103
103
|
<style scoped>
|
|
104
|
-
.rm-tabs{background-color:var(--
|
|
104
|
+
.rm-tabs{background-color:var(--color-text-inverse);border-bottom:1px solid var(--border-default)}.rm-tabs__list{display:flex;padding:8px 20px 0;position:relative}.rm-tabs__tab{background:none;border:none;border-radius:4px 4px 0 0;color:var(--text-secondary);cursor:pointer;font-family:var(--font-sans);font-size:14px;font-weight:400;padding:12px 16px;position:relative;transition:color var(--duration-fast) var(--ease-in-out),background var(--duration-fast) var(--ease-in-out);white-space:nowrap}.rm-tabs__tab:hover{background:var(--tab-active-bg);color:var(--text-secondary)}.rm-tabs__tab--active{font-weight:500}.rm-tabs__indicator{border-radius:2px 2px 0 0;bottom:0;height:2px;left:0;position:absolute;transition:transform .3s cubic-bezier(.4,0,.2,1),width .3s cubic-bezier(.4,0,.2,1)}
|
|
105
105
|
</style>
|
|
@@ -130,5 +130,5 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
130
130
|
</template>
|
|
131
131
|
|
|
132
132
|
<style scoped>
|
|
133
|
-
.toolbar-filter{position:relative}.toolbar-filter__caret{opacity:.6;transition:transform var(--
|
|
133
|
+
.toolbar-filter{position:relative}.toolbar-filter__caret{opacity:.6;transition:transform var(--duration-fast) var(--ease-in-out)}.toolbar-filter__caret--open{transform:rotate(180deg)}.toolbar-filter__dropdown{background-color:var(--color-text-inverse);border:1px solid var(--border-default);border-radius:var(--radius-lg);box-shadow:0 4px 12px rgba(var(--shadow-rgb),.08),0 2px 4px rgba(var(--shadow-rgb),.04);left:0;max-height:260px;min-width:200px;overflow-y:auto;padding:var(--space-4);position:absolute;top:calc(100% + var(--space-8));z-index:50}.toolbar-filter__option{align-items:center;background:none;border:none;border-radius:var(--radius-md);color:var(--text-secondary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-14);gap:var(--space-8);padding:var(--space-8) var(--space-12);text-align:left;transition:background-color var(--duration-fast) var(--ease-in-out);width:100%}.toolbar-filter__option:hover{background-color:var(--bg-surface-2)}.toolbar-filter__option--selected{color:var(--text-primary);font-weight:var(--weight-medium)}.toolbar-filter__checkbox{align-items:center;border:1.5px solid var(--border-default);border-radius:var(--radius-sm);display:flex;flex-shrink:0;height:16px;justify-content:center;transition:all var(--duration-fast) var(--ease-in-out);width:16px}.toolbar-filter__checkbox--checked{background-color:var(--color-healthcare);border-color:var(--color-healthcare);color:var(--color-text-inverse)}.toolbar-filter__radio{align-items:center;border:1.5px solid var(--border-default);border-radius:var(--radius-pill);display:flex;flex-shrink:0;height:16px;justify-content:center;transition:all var(--duration-fast) var(--ease-in-out);width:16px}.toolbar-filter__radio--checked{border-color:var(--color-healthcare)}.toolbar-filter__radio--checked:after{background-color:var(--color-healthcare);border-radius:var(--radius-pill);content:"";height:8px;width:8px}.dropdown-enter-active,.dropdown-leave-active{transition:opacity var(--duration-fast) var(--ease-in-out),transform var(--duration-fast) var(--ease-in-out)}.dropdown-enter-from,.dropdown-leave-to{opacity:0;transform:translateY(-4px)}
|
|
134
134
|
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function useChartTheme(): {
|
|
2
|
+
night: import("vue").Ref<boolean, boolean>;
|
|
3
|
+
cvd: import("vue").Ref<boolean, boolean>;
|
|
4
|
+
theme: import("vue").ComputedRef<import("../utils/chart").RmChartTheme>;
|
|
5
|
+
palette: import("vue").ComputedRef<import("../utils/chart").RmPalette>;
|
|
6
|
+
decal: import("vue").ComputedRef<boolean>;
|
|
7
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
|
2
|
+
import { rmChartTheme, rmPalette } from "../utils/chart.js";
|
|
3
|
+
const CVD_CLASSES = ["cvd-deutera", "cvd-protan", "cvd-trita", "cvd-mono"];
|
|
4
|
+
export function useChartTheme() {
|
|
5
|
+
const night = ref(false);
|
|
6
|
+
const cvd = ref(false);
|
|
7
|
+
let observer = null;
|
|
8
|
+
function read() {
|
|
9
|
+
if (typeof document === "undefined") return;
|
|
10
|
+
const list = document.body.classList;
|
|
11
|
+
night.value = list.contains("night");
|
|
12
|
+
cvd.value = CVD_CLASSES.some((c) => list.contains(c));
|
|
13
|
+
}
|
|
14
|
+
onMounted(() => {
|
|
15
|
+
read();
|
|
16
|
+
observer = new MutationObserver(read);
|
|
17
|
+
observer.observe(document.body, { attributes: true, attributeFilter: ["class"] });
|
|
18
|
+
});
|
|
19
|
+
onBeforeUnmount(() => {
|
|
20
|
+
observer?.disconnect();
|
|
21
|
+
observer = null;
|
|
22
|
+
});
|
|
23
|
+
const theme = computed(() => rmChartTheme(night.value));
|
|
24
|
+
const palette = computed(() => rmPalette(cvd.value ? "cvdSafe" : "default", night.value));
|
|
25
|
+
const decal = computed(() => cvd.value);
|
|
26
|
+
return { night, cvd, theme, palette, decal };
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type RmCvdMode = 'none' | 'deutera' | 'protan' | 'trita' | 'mono';
|
|
2
|
+
export type RmTextScale = 'normal' | 'large' | 'xl';
|
|
3
|
+
export declare function useRomulusTheme(): {
|
|
4
|
+
night: Readonly<import("vue").Ref<boolean, boolean>>;
|
|
5
|
+
highContrast: Readonly<import("vue").Ref<boolean, boolean>>;
|
|
6
|
+
reduceMotion: Readonly<import("vue").Ref<boolean, boolean>>;
|
|
7
|
+
compact: Readonly<import("vue").Ref<boolean, boolean>>;
|
|
8
|
+
cvd: Readonly<import("vue").Ref<RmCvdMode, RmCvdMode>>;
|
|
9
|
+
textScale: Readonly<import("vue").Ref<RmTextScale, RmTextScale>>;
|
|
10
|
+
setNight: (value: boolean) => void;
|
|
11
|
+
setHighContrast: (value: boolean) => void;
|
|
12
|
+
setReduceMotion: (value: boolean) => void;
|
|
13
|
+
setCompact: (value: boolean) => void;
|
|
14
|
+
setCvd: (mode: RmCvdMode) => void;
|
|
15
|
+
setTextScale: (scale: RmTextScale) => void;
|
|
16
|
+
reset: () => void;
|
|
17
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { onMounted, readonly, ref } from "vue";
|
|
2
|
+
const CVD_CLASSES = {
|
|
3
|
+
deutera: "cvd-deutera",
|
|
4
|
+
protan: "cvd-protan",
|
|
5
|
+
trita: "cvd-trita",
|
|
6
|
+
mono: "cvd-mono"
|
|
7
|
+
};
|
|
8
|
+
const TEXT_CLASSES = {
|
|
9
|
+
large: "text-large",
|
|
10
|
+
xl: "text-xl"
|
|
11
|
+
};
|
|
12
|
+
export function useRomulusTheme() {
|
|
13
|
+
const night = ref(false);
|
|
14
|
+
const highContrast = ref(false);
|
|
15
|
+
const reduceMotion = ref(false);
|
|
16
|
+
const compact = ref(false);
|
|
17
|
+
const cvd = ref("none");
|
|
18
|
+
const textScale = ref("normal");
|
|
19
|
+
function apply(mutate) {
|
|
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
|
+
}
|
|
28
|
+
const setNight = (value) => apply(() => {
|
|
29
|
+
night.value = value;
|
|
30
|
+
document.body.classList.toggle("night", value);
|
|
31
|
+
});
|
|
32
|
+
const setHighContrast = (value) => apply(() => {
|
|
33
|
+
highContrast.value = value;
|
|
34
|
+
document.body.classList.toggle("high-contrast", value);
|
|
35
|
+
});
|
|
36
|
+
const setReduceMotion = (value) => apply(() => {
|
|
37
|
+
reduceMotion.value = value;
|
|
38
|
+
document.body.classList.toggle("reduce-motion", value);
|
|
39
|
+
});
|
|
40
|
+
const setCompact = (value) => apply(() => {
|
|
41
|
+
compact.value = value;
|
|
42
|
+
document.body.classList.toggle("density-compact", value);
|
|
43
|
+
});
|
|
44
|
+
const setCvd = (mode) => apply(() => {
|
|
45
|
+
cvd.value = mode;
|
|
46
|
+
const { body } = document;
|
|
47
|
+
Object.values(CVD_CLASSES).forEach((c) => body.classList.remove(c));
|
|
48
|
+
if (mode !== "none") body.classList.add(CVD_CLASSES[mode]);
|
|
49
|
+
});
|
|
50
|
+
const setTextScale = (scale) => apply(() => {
|
|
51
|
+
textScale.value = scale;
|
|
52
|
+
const { body } = document;
|
|
53
|
+
Object.values(TEXT_CLASSES).forEach((c) => body.classList.remove(c));
|
|
54
|
+
if (scale !== "normal") body.classList.add(TEXT_CLASSES[scale]);
|
|
55
|
+
});
|
|
56
|
+
const reset = () => apply(() => {
|
|
57
|
+
const { body } = document;
|
|
58
|
+
night.value = false;
|
|
59
|
+
highContrast.value = false;
|
|
60
|
+
reduceMotion.value = false;
|
|
61
|
+
compact.value = false;
|
|
62
|
+
cvd.value = "none";
|
|
63
|
+
textScale.value = "normal";
|
|
64
|
+
body.classList.remove(
|
|
65
|
+
"night",
|
|
66
|
+
"high-contrast",
|
|
67
|
+
"reduce-motion",
|
|
68
|
+
"density-compact",
|
|
69
|
+
...Object.values(CVD_CLASSES),
|
|
70
|
+
...Object.values(TEXT_CLASSES)
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
onMounted(() => {
|
|
74
|
+
const list = document.body.classList;
|
|
75
|
+
night.value = list.contains("night");
|
|
76
|
+
highContrast.value = list.contains("high-contrast");
|
|
77
|
+
reduceMotion.value = list.contains("reduce-motion");
|
|
78
|
+
compact.value = list.contains("density-compact");
|
|
79
|
+
const found = Object.entries(CVD_CLASSES).find(([, c]) => list.contains(c));
|
|
80
|
+
cvd.value = found ? found[0] : "none";
|
|
81
|
+
textScale.value = list.contains("text-xl") ? "xl" : list.contains("text-large") ? "large" : "normal";
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
night: readonly(night),
|
|
85
|
+
highContrast: readonly(highContrast),
|
|
86
|
+
reduceMotion: readonly(reduceMotion),
|
|
87
|
+
compact: readonly(compact),
|
|
88
|
+
cvd: readonly(cvd),
|
|
89
|
+
textScale: readonly(textScale),
|
|
90
|
+
setNight,
|
|
91
|
+
setHighContrast,
|
|
92
|
+
setReduceMotion,
|
|
93
|
+
setCompact,
|
|
94
|
+
setCvd,
|
|
95
|
+
setTextScale,
|
|
96
|
+
reset
|
|
97
|
+
};
|
|
98
|
+
}
|