@tekkare/romulus 0.1.0 → 0.1.5
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 +2 -1
- package/dist/runtime/assets/styles/tokens.css +1 -1
- package/dist/runtime/assets/styles/utilities.css +1 -0
- package/dist/runtime/components/ActionBar.vue +95 -0
- package/dist/runtime/components/AdvancedSearch.vue +110 -0
- package/dist/runtime/components/Breadcrumb.vue +46 -0
- package/dist/runtime/components/Button.vue +17 -9
- package/dist/runtime/components/Drawer.vue +79 -0
- package/dist/runtime/components/FilterCheckbox.vue +26 -0
- package/dist/runtime/components/FilterComplex.vue +205 -0
- package/dist/runtime/components/FilterDropdown.vue +119 -0
- package/dist/runtime/components/Icon.vue +43 -0
- package/dist/runtime/components/NavLink.vue +60 -0
- package/dist/runtime/components/Pill.vue +37 -0
- package/dist/runtime/components/PillTabs.vue +34 -0
- package/dist/runtime/components/SearchBar.vue +38 -0
- package/dist/runtime/components/Sidebar.vue +143 -0
- package/dist/runtime/components/Tabs.vue +105 -0
- package/dist/runtime/components/ToolbarFilter.vue +134 -0
- package/package.json +4 -3
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
PhCaretDown,
|
|
4
|
+
PhMagnifyingGlass,
|
|
5
|
+
PhPlusCircle,
|
|
6
|
+
PhMinusCircle,
|
|
7
|
+
PhX,
|
|
8
|
+
} from '@phosphor-icons/vue'
|
|
9
|
+
import type { Component } from 'vue'
|
|
10
|
+
|
|
11
|
+
export interface FilterComplexOption {
|
|
12
|
+
label: string
|
|
13
|
+
value: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<{
|
|
17
|
+
icon?: Component
|
|
18
|
+
label: string
|
|
19
|
+
options: FilterComplexOption[]
|
|
20
|
+
}>(), {
|
|
21
|
+
icon: undefined,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const selected = defineModel<string[]>({ default: () => [] })
|
|
25
|
+
|
|
26
|
+
const isOpen = ref(false)
|
|
27
|
+
const wrapperRef = ref<HTMLElement>()
|
|
28
|
+
const addSearch = ref('')
|
|
29
|
+
const delSearch = ref('')
|
|
30
|
+
|
|
31
|
+
const isActive = computed(() => selected.value.length > 0)
|
|
32
|
+
|
|
33
|
+
const availableOptions = computed(() => {
|
|
34
|
+
return props.options.filter(o => !selected.value.includes(o.value))
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const filteredAvailable = computed(() => {
|
|
38
|
+
if (!addSearch.value) return availableOptions.value
|
|
39
|
+
const q = addSearch.value.toLowerCase()
|
|
40
|
+
return availableOptions.value.filter(o => o.label.toLowerCase().includes(q))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const selectedOptions = computed(() => {
|
|
44
|
+
return props.options.filter(o => selected.value.includes(o.value))
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const filteredSelected = computed(() => {
|
|
48
|
+
if (!delSearch.value) return selectedOptions.value
|
|
49
|
+
const q = delSearch.value.toLowerCase()
|
|
50
|
+
return selectedOptions.value.filter(o => o.label.toLowerCase().includes(q))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
function addOption(value: string) {
|
|
54
|
+
selected.value = [...selected.value, value]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function removeOption(value: string) {
|
|
58
|
+
selected.value = selected.value.filter(v => v !== value)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function addAll() {
|
|
62
|
+
selected.value = [...selected.value, ...filteredAvailable.value.map(o => o.value)]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function clearAll() {
|
|
66
|
+
selected.value = []
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toggle() {
|
|
70
|
+
isOpen.value = !isOpen.value
|
|
71
|
+
if (!isOpen.value) {
|
|
72
|
+
addSearch.value = ''
|
|
73
|
+
delSearch.value = ''
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function onClickOutside(e: Event) {
|
|
78
|
+
if (wrapperRef.value && !wrapperRef.value.contains(e.target as Node)) {
|
|
79
|
+
isOpen.value = false
|
|
80
|
+
addSearch.value = ''
|
|
81
|
+
delSearch.value = ''
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
onMounted(() => document.addEventListener('click', onClickOutside))
|
|
86
|
+
onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<template>
|
|
90
|
+
<div ref="wrapperRef" class="filter-complex">
|
|
91
|
+
<button
|
|
92
|
+
class="filter-complex__trigger"
|
|
93
|
+
:class="{ 'filter-complex__trigger--active': isActive }"
|
|
94
|
+
@click="toggle"
|
|
95
|
+
>
|
|
96
|
+
<component :is="icon" v-if="icon" :size="16" class="filter-complex__icon" />
|
|
97
|
+
<span class="filter-complex__label">{{ label }}</span>
|
|
98
|
+
<span v-if="isActive" class="filter-complex__badge">
|
|
99
|
+
{{ selected.length }}
|
|
100
|
+
</span>
|
|
101
|
+
<PhCaretDown
|
|
102
|
+
:size="12"
|
|
103
|
+
class="filter-complex__caret"
|
|
104
|
+
:class="{ 'filter-complex__caret--open': isOpen }"
|
|
105
|
+
/>
|
|
106
|
+
</button>
|
|
107
|
+
|
|
108
|
+
<Transition name="dropdown">
|
|
109
|
+
<div v-if="isOpen" class="filter-complex__panel" @click.stop>
|
|
110
|
+
<!-- Left: Add -->
|
|
111
|
+
<div class="filter-complex__left">
|
|
112
|
+
<h3 class="filter-complex__title">Custom selection</h3>
|
|
113
|
+
|
|
114
|
+
<div class="filter-complex__search">
|
|
115
|
+
<PhMagnifyingGlass :size="14" color="#9ca3af" />
|
|
116
|
+
<input
|
|
117
|
+
v-model="addSearch"
|
|
118
|
+
type="text"
|
|
119
|
+
placeholder="Search to add..."
|
|
120
|
+
class="filter-complex__search-input"
|
|
121
|
+
>
|
|
122
|
+
<PhX
|
|
123
|
+
v-if="addSearch"
|
|
124
|
+
:size="12"
|
|
125
|
+
class="filter-complex__search-clear"
|
|
126
|
+
@click="addSearch = ''"
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<button
|
|
131
|
+
v-if="filteredAvailable.length > 0"
|
|
132
|
+
class="filter-complex__action filter-complex__action--add"
|
|
133
|
+
@click="addAll"
|
|
134
|
+
>
|
|
135
|
+
Add all ({{ filteredAvailable.length }})
|
|
136
|
+
</button>
|
|
137
|
+
|
|
138
|
+
<div class="filter-complex__list">
|
|
139
|
+
<button
|
|
140
|
+
v-for="option in filteredAvailable"
|
|
141
|
+
:key="option.value"
|
|
142
|
+
class="filter-complex__item"
|
|
143
|
+
@click="addOption(option.value)"
|
|
144
|
+
>
|
|
145
|
+
<span class="filter-complex__item-label">{{ option.label }}</span>
|
|
146
|
+
<PhPlusCircle :size="16" class="filter-complex__item-icon filter-complex__item-icon--add" />
|
|
147
|
+
</button>
|
|
148
|
+
<p v-if="filteredAvailable.length === 0" class="filter-complex__empty">
|
|
149
|
+
No results
|
|
150
|
+
</p>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<!-- Right: Selection -->
|
|
155
|
+
<div class="filter-complex__right">
|
|
156
|
+
<div class="filter-complex__right-header">
|
|
157
|
+
<h3 class="filter-complex__title">Current selection</h3>
|
|
158
|
+
<button
|
|
159
|
+
v-if="selected.length > 0"
|
|
160
|
+
class="filter-complex__action filter-complex__action--clear"
|
|
161
|
+
@click="clearAll"
|
|
162
|
+
>
|
|
163
|
+
Clear
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div v-if="selected.length > 0" class="filter-complex__search">
|
|
168
|
+
<PhMagnifyingGlass :size="14" color="#9ca3af" />
|
|
169
|
+
<input
|
|
170
|
+
v-model="delSearch"
|
|
171
|
+
type="text"
|
|
172
|
+
placeholder="Search selection..."
|
|
173
|
+
class="filter-complex__search-input"
|
|
174
|
+
>
|
|
175
|
+
<PhX
|
|
176
|
+
v-if="delSearch"
|
|
177
|
+
:size="12"
|
|
178
|
+
class="filter-complex__search-clear"
|
|
179
|
+
@click="delSearch = ''"
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div class="filter-complex__list">
|
|
184
|
+
<button
|
|
185
|
+
v-for="option in filteredSelected"
|
|
186
|
+
:key="option.value"
|
|
187
|
+
class="filter-complex__item"
|
|
188
|
+
@click="removeOption(option.value)"
|
|
189
|
+
>
|
|
190
|
+
<span class="filter-complex__item-label">{{ option.label }}</span>
|
|
191
|
+
<PhMinusCircle :size="16" class="filter-complex__item-icon filter-complex__item-icon--remove" />
|
|
192
|
+
</button>
|
|
193
|
+
<p v-if="selected.length === 0" class="filter-complex__empty">
|
|
194
|
+
No selection
|
|
195
|
+
</p>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</Transition>
|
|
200
|
+
</div>
|
|
201
|
+
</template>
|
|
202
|
+
|
|
203
|
+
<style scoped>
|
|
204
|
+
.filter-complex{position:relative}.filter-complex__trigger{align-items:center;background:#fff;border:1px solid #dbdef0;border-radius:6px;box-sizing:border-box;color:var(--r-color-gray-600);cursor:pointer;display:flex;font-family:var(--r-font-family);font-size:var(--r-text-sm);font-weight:var(--r-font-weight-medium);gap:8px;height:32px;padding:8px;transition:all var(--r-transition-fast);white-space:nowrap;width:193px}.filter-complex__trigger:hover{border-color:var(--r-color-gray-300)}.filter-complex__icon{color:#4164ee;flex-shrink:0}.filter-complex__trigger--active{border-color:#4164ee;box-shadow:0 0 8px 0 rgba(65,100,238,.1)}.filter-complex__label{flex:1;min-width:0;overflow:hidden;text-align:left;text-overflow:ellipsis;white-space:nowrap}.filter-complex__badge{align-items:center;background-color:#4164ee;border-radius:100px;color:#fff;display:inline-flex;flex-shrink:0;font-size:11px;font-weight:600;height:18px;justify-content:center;line-height:1;min-width:18px;padding:0 5px}.filter-complex__caret{color:var(--r-color-gray-400);flex-shrink:0;transition:transform var(--r-transition-fast)}.filter-complex__caret--open{transform:rotate(180deg)}.filter-complex__panel{background:#fff;border:1px solid #dbdef0;border-radius:8px;box-shadow:0 4px 24px rgba(0,0,0,.1),0 2px 8px rgba(0,0,0,.05);display:flex;left:0;max-height:380px;overflow:hidden;position:absolute;top:calc(100% + 4px);width:520px;z-index:50}.filter-complex__left{flex:1 1 60%;flex-direction:column}.filter-complex__left,.filter-complex__right{display:flex;gap:8px;overflow:hidden;padding:20px}.filter-complex__right{background-color:var(--r-color-gray-50);border-left:1px solid #dbdef0;flex:1 1 40%;flex-direction:column}.filter-complex__right-header{align-items:center;display:flex;justify-content:space-between}.filter-complex__title{color:var(--r-color-gray-700);font-size:var(--r-text-sm);font-weight:var(--r-font-weight-semibold);margin:0}.filter-complex__search{align-items:center;background:#fff;border:1px solid #dbdef0;border-radius:6px;display:flex;gap:8px;padding:6px 8px}.filter-complex__search:focus-within{border-color:#4164ee}.filter-complex__search-input{background:transparent;border:none;color:var(--r-color-gray-700);flex:1;font-family:var(--r-font-family);font-size:var(--r-text-sm);outline:none}.filter-complex__search-input::-moz-placeholder{color:var(--r-color-gray-400)}.filter-complex__search-input::placeholder{color:var(--r-color-gray-400)}.filter-complex__search-clear{color:var(--r-color-gray-400);cursor:pointer;flex-shrink:0}.filter-complex__search-clear:hover{color:var(--r-color-gray-700)}.filter-complex__action{background:none;border:none;cursor:pointer;font-family:var(--r-font-family);font-size:var(--r-text-sm);padding:0;text-align:left;text-decoration:underline}.filter-complex__action--add{color:#4164ee}.filter-complex__action--add:hover{color:#3451d4}.filter-complex__action--clear{color:var(--r-color-danger-500)}.filter-complex__action--clear:hover{color:var(--r-color-danger-600)}.filter-complex__list{display:flex;flex:1;flex-direction:column;overflow-y:auto}.filter-complex__list::-webkit-scrollbar{width:4px}.filter-complex__list::-webkit-scrollbar-thumb{background:var(--r-color-gray-200);border-radius:100px}.filter-complex__list::-webkit-scrollbar-thumb:hover{background:var(--r-color-gray-400)}.filter-complex__item{align-items:center;background:none;border:none;border-radius:4px;color:var(--r-color-gray-700);cursor:pointer;display:flex;font-family:var(--r-font-family);font-size:var(--r-text-sm);gap:8px;padding:6px 8px;text-align:left;transition:background-color var(--r-transition-fast)}.filter-complex__item:hover{background-color:var(--r-color-gray-50)}.filter-complex__right .filter-complex__item:hover{background-color:var(--r-color-gray-100)}.filter-complex__item-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.filter-complex__item-icon{flex-shrink:0;opacity:0;transition:opacity var(--r-transition-fast)}.filter-complex__item:hover .filter-complex__item-icon{opacity:1}.filter-complex__item-icon--add{color:#4164ee}.filter-complex__item-icon--remove{color:var(--r-color-danger-500)}.filter-complex__empty{color:var(--r-color-gray-400);font-size:var(--r-text-sm);font-style:italic;padding:8px}.dropdown-enter-active,.dropdown-leave-active{transition:opacity var(--r-transition-fast),transform var(--r-transition-fast)}.dropdown-enter-from,.dropdown-leave-to{opacity:0;transform:translateY(-4px)}
|
|
205
|
+
</style>
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { PhCaretDown, PhCheck } from '@phosphor-icons/vue'
|
|
3
|
+
import type { Component } from 'vue'
|
|
4
|
+
|
|
5
|
+
export interface FilterDropdownOption {
|
|
6
|
+
label: string
|
|
7
|
+
value: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<{
|
|
11
|
+
icon?: Component
|
|
12
|
+
label: string
|
|
13
|
+
options: FilterDropdownOption[]
|
|
14
|
+
multiple?: boolean
|
|
15
|
+
}>(), {
|
|
16
|
+
icon: undefined,
|
|
17
|
+
multiple: true,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const selected = defineModel<string[]>({ default: () => [] })
|
|
21
|
+
|
|
22
|
+
const isOpen = ref(false)
|
|
23
|
+
const chipRef = ref<HTMLElement>()
|
|
24
|
+
const hasUserChanged = ref(false)
|
|
25
|
+
|
|
26
|
+
// Initialise synchrone pour éviter hydration mismatch
|
|
27
|
+
if (!props.multiple && selected.value.length === 0 && props.options.length > 0) {
|
|
28
|
+
selected.value = [props.options[0]!.value]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const isActive = computed(() => hasUserChanged.value && selected.value.length > 0)
|
|
32
|
+
|
|
33
|
+
const displayLabel = computed(() => {
|
|
34
|
+
if (!props.multiple && isActive.value && selected.value.length === 1) {
|
|
35
|
+
const option = props.options.find(o => o.value === selected.value[0])
|
|
36
|
+
return option ? `${props.label}: ${option.label}` : props.label
|
|
37
|
+
}
|
|
38
|
+
return props.label
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
function toggle() {
|
|
42
|
+
isOpen.value = !isOpen.value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toggleOption(value: string) {
|
|
46
|
+
hasUserChanged.value = true
|
|
47
|
+
if (props.multiple) {
|
|
48
|
+
const index = selected.value.indexOf(value)
|
|
49
|
+
if (index === -1) {
|
|
50
|
+
selected.value = [...selected.value, value]
|
|
51
|
+
} else {
|
|
52
|
+
selected.value = selected.value.filter(v => v !== value)
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
selected.value = [value]
|
|
56
|
+
isOpen.value = false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isSelected(value: string) {
|
|
61
|
+
return selected.value.includes(value)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function onClickOutside(e: Event) {
|
|
65
|
+
if (chipRef.value && !chipRef.value.contains(e.target as Node)) {
|
|
66
|
+
isOpen.value = false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
onMounted(() => document.addEventListener('click', onClickOutside))
|
|
71
|
+
onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<div ref="chipRef" class="filter-chip-wrapper">
|
|
76
|
+
<button
|
|
77
|
+
class="filter-chip"
|
|
78
|
+
:class="{ 'filter-chip--active': isActive }"
|
|
79
|
+
@click="toggle"
|
|
80
|
+
>
|
|
81
|
+
<component :is="icon" v-if="icon" :size="16" class="filter-chip__icon" />
|
|
82
|
+
<span class="filter-chip__label">{{ displayLabel }}</span>
|
|
83
|
+
<span v-if="isActive && multiple && selected.length > 0" class="filter-chip__badge">
|
|
84
|
+
{{ selected.length }}
|
|
85
|
+
</span>
|
|
86
|
+
<PhCaretDown
|
|
87
|
+
:size="12"
|
|
88
|
+
class="filter-chip__caret"
|
|
89
|
+
:class="{ 'filter-chip__caret--open': isOpen }"
|
|
90
|
+
/>
|
|
91
|
+
</button>
|
|
92
|
+
|
|
93
|
+
<Transition name="dropdown">
|
|
94
|
+
<div v-if="isOpen" class="filter-chip__dropdown">
|
|
95
|
+
<button
|
|
96
|
+
v-for="option in options"
|
|
97
|
+
:key="option.value"
|
|
98
|
+
class="filter-chip__option"
|
|
99
|
+
:class="{ 'filter-chip__option--selected': isSelected(option.value) }"
|
|
100
|
+
@click="toggleOption(option.value)"
|
|
101
|
+
>
|
|
102
|
+
<span
|
|
103
|
+
:class="[
|
|
104
|
+
multiple ? 'filter-chip__checkbox' : 'filter-chip__radio',
|
|
105
|
+
isSelected(option.value) && (multiple ? 'filter-chip__checkbox--checked' : 'filter-chip__radio--checked'),
|
|
106
|
+
]"
|
|
107
|
+
>
|
|
108
|
+
<PhCheck v-if="multiple && isSelected(option.value)" :size="10" weight="bold" />
|
|
109
|
+
</span>
|
|
110
|
+
<span>{{ option.label }}</span>
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
</Transition>
|
|
114
|
+
</div>
|
|
115
|
+
</template>
|
|
116
|
+
|
|
117
|
+
<style scoped>
|
|
118
|
+
.filter-chip-wrapper{position:relative}.filter-chip{align-items:center;background:#fff;border:1px solid #dbdef0;border-radius:6px;box-sizing:border-box;color:var(--r-color-gray-600);cursor:pointer;display:flex;font-family:var(--r-font-family);font-size:var(--r-text-sm);font-weight:var(--r-font-weight-medium);gap:8px;height:32px;padding:8px;transition:all var(--r-transition-fast);white-space:nowrap;width:193px}.filter-chip:hover{border-color:var(--r-color-gray-300)}.filter-chip__icon{color:#4164ee;flex-shrink:0}.filter-chip--active{border-color:#4164ee;box-shadow:0 0 8px 0 rgba(65,100,238,.1)}.filter-chip__label{flex:1;min-width:0;overflow:hidden;text-align:left;text-overflow:ellipsis;white-space:nowrap}.filter-chip__badge{align-items:center;background-color:#4164ee;border-radius:100px;color:#fff;display:inline-flex;flex-shrink:0;font-size:11px;font-weight:600;height:18px;justify-content:center;line-height:1;min-width:18px;padding:0 5px}.filter-chip__caret{color:var(--r-color-gray-400);flex-shrink:0;transition:transform var(--r-transition-fast)}.filter-chip__caret--open{transform:rotate(180deg)}.filter-chip__dropdown{background-color:#fff;border:1px solid #dbdef0;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.08),0 2px 4px rgba(0,0,0,.04);left:0;max-height:260px;min-width:200px;overflow-y:auto;padding:4px;position:absolute;top:calc(100% + 4px);z-index:50}.filter-chip__option{align-items:center;background:none;border:none;border-radius:4px;color:var(--r-color-gray-700);cursor:pointer;display:flex;font-family:var(--r-font-family);font-size:var(--r-text-sm);gap:8px;padding:6px 8px;text-align:left;transition:background-color var(--r-transition-fast);width:100%}.filter-chip__option:hover{background-color:var(--r-color-gray-50)}.filter-chip__option--selected{color:var(--r-color-gray-900);font-weight:var(--r-font-weight-medium)}.filter-chip__checkbox{align-items:center;border:1.5px solid var(--r-color-gray-300);border-radius:3px;display:flex;flex-shrink:0;height:16px;justify-content:center;transition:all var(--r-transition-fast);width:16px}.filter-chip__checkbox--checked{background-color:#4164ee;border-color:#4164ee;color:#fff}.filter-chip__radio{align-items:center;border:1.5px solid var(--r-color-gray-300);border-radius:100%;display:flex;flex-shrink:0;height:16px;justify-content:center;transition:all var(--r-transition-fast);width:16px}.filter-chip__radio--checked{border-color:#4164ee}.filter-chip__radio--checked:after{background-color:#4164ee;border-radius:100%;content:"";height:8px;width:8px}.dropdown-enter-active,.dropdown-leave-active{transition:opacity var(--r-transition-fast),transform var(--r-transition-fast)}.dropdown-enter-from,.dropdown-leave-to{opacity:0;transform:translateY(-4px)}
|
|
119
|
+
</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, defineAsyncComponent, type Component } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface RmIconProps {
|
|
5
|
+
/** Phosphor icon name without the "Ph" prefix (e.g. "House", "Gear", "ChartBar") */
|
|
6
|
+
name: string
|
|
7
|
+
/** Icon size in pixels */
|
|
8
|
+
size?: number
|
|
9
|
+
/** Icon weight: thin, light, regular, bold, fill, duotone */
|
|
10
|
+
weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone'
|
|
11
|
+
/** Icon color */
|
|
12
|
+
color?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const props = withDefaults(defineProps<RmIconProps>(), {
|
|
16
|
+
size: 20,
|
|
17
|
+
weight: 'regular',
|
|
18
|
+
color: undefined,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const iconComponent = computed<Component>(() => {
|
|
22
|
+
const iconName = `Ph${props.name}`
|
|
23
|
+
return defineAsyncComponent(() =>
|
|
24
|
+
import('@phosphor-icons/vue').then((mod) => {
|
|
25
|
+
return (mod as Record<string, Component>)[iconName] || mod.PhQuestion
|
|
26
|
+
}),
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<ClientOnly>
|
|
33
|
+
<component
|
|
34
|
+
:is="iconComponent"
|
|
35
|
+
:size="size"
|
|
36
|
+
:weight="weight"
|
|
37
|
+
:color="color"
|
|
38
|
+
/>
|
|
39
|
+
<template #fallback>
|
|
40
|
+
<span :style="{ display: 'inline-block', width: size + 'px', height: size + 'px' }" />
|
|
41
|
+
</template>
|
|
42
|
+
</ClientOnly>
|
|
43
|
+
</template>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
export interface RmNavLinkProps {
|
|
3
|
+
/** Phosphor icon name (e.g. "ChartBar", "Flask") */
|
|
4
|
+
icon: string
|
|
5
|
+
/** Link label */
|
|
6
|
+
label: string
|
|
7
|
+
/** Navigation path */
|
|
8
|
+
to?: string
|
|
9
|
+
/** Whether the link is currently active */
|
|
10
|
+
active?: boolean
|
|
11
|
+
/** Caret type: 'expand' (down/up), 'navigate' (right), 'none' */
|
|
12
|
+
caret?: 'expand' | 'navigate' | 'none'
|
|
13
|
+
/** Whether the submenu is expanded (only for caret='expand') */
|
|
14
|
+
expanded?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
withDefaults(defineProps<RmNavLinkProps>(), {
|
|
18
|
+
to: undefined,
|
|
19
|
+
active: false,
|
|
20
|
+
caret: 'none',
|
|
21
|
+
expanded: false,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
defineEmits<{
|
|
25
|
+
click: []
|
|
26
|
+
}>()
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<button
|
|
31
|
+
class="rm-nav-link"
|
|
32
|
+
:class="{
|
|
33
|
+
'rm-nav-link--active': active,
|
|
34
|
+
}"
|
|
35
|
+
@click="$emit('click')"
|
|
36
|
+
>
|
|
37
|
+
<span class="rm-nav-link__icon">
|
|
38
|
+
<RmIcon :name="icon" :size="18" />
|
|
39
|
+
</span>
|
|
40
|
+
<span class="rm-nav-link__label">{{ label }}</span>
|
|
41
|
+
|
|
42
|
+
<!-- Expand caret (down/up) -->
|
|
43
|
+
<span
|
|
44
|
+
v-if="caret === 'expand'"
|
|
45
|
+
class="rm-nav-link__caret"
|
|
46
|
+
:class="{ 'rm-nav-link__caret--expanded': expanded }"
|
|
47
|
+
>
|
|
48
|
+
<RmIcon name="CaretDown" :size="14" />
|
|
49
|
+
</span>
|
|
50
|
+
|
|
51
|
+
<!-- Navigate caret (right) -->
|
|
52
|
+
<span v-if="caret === 'navigate'" class="rm-nav-link__caret">
|
|
53
|
+
<RmIcon name="CaretRight" :size="14" />
|
|
54
|
+
</span>
|
|
55
|
+
</button>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<style scoped>
|
|
59
|
+
.rm-nav-link{background:none;border:none;border-radius:var(--r-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(--r-transition-fast)}.rm-nav-link__icon{background-color:var(--r-color-gray-100);border-radius:var(--r-radius-md);color:var(--r-color-gray-500);flex-shrink:0;height:28px;justify-content:center;width:28px}.rm-nav-link__label{color:#1e293b;flex:1;font-family:var(--r-font-family);font-size:14px;font-weight:400;line-height:160%;overflow:hidden;text-overflow:ellipsis;transition:all var(--r-transition-fast);white-space:nowrap}.rm-nav-link__caret{align-items:center;color:var(--r-color-gray-400);display:flex;flex-shrink:0;transition:transform .25s ease,color var(--r-transition-fast)}.rm-nav-link__caret--expanded{transform:rotate(-180deg)}.rm-nav-link:not(.rm-nav-link--active):hover{background-color:var(--r-color-primary-50)}.rm-nav-link:not(.rm-nav-link--active):hover .rm-nav-link__icon{background-color:var(--r-color-primary-100);color:var(--r-color-primary-600)}.rm-nav-link:not(.rm-nav-link--active):hover .rm-nav-link__caret{color:var(--r-color-primary-600)}.rm-nav-link--active{background-color:var(--r-color-gray-100)}.rm-nav-link--active .rm-nav-link__icon{background-color:var(--r-color-primary-600);color:var(--r-color-white)}.rm-nav-link--active .rm-nav-link__caret{color:var(--r-color-primary-600)}
|
|
60
|
+
</style>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Component } from 'vue'
|
|
3
|
+
|
|
4
|
+
withDefaults(defineProps<{
|
|
5
|
+
icon?: Component
|
|
6
|
+
label: string
|
|
7
|
+
active?: boolean
|
|
8
|
+
variant?: 'default' | 'primary' | 'secondary' | 'pharma' | 'discovery' | 'databank'
|
|
9
|
+
}>(), {
|
|
10
|
+
icon: undefined,
|
|
11
|
+
active: false,
|
|
12
|
+
variant: 'default',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
defineEmits<{
|
|
16
|
+
click: []
|
|
17
|
+
}>()
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<button
|
|
22
|
+
class="toolbar-pill"
|
|
23
|
+
:class="[
|
|
24
|
+
active && 'toolbar-pill--active',
|
|
25
|
+
`toolbar-pill--${variant}`,
|
|
26
|
+
]"
|
|
27
|
+
@click="$emit('click')"
|
|
28
|
+
>
|
|
29
|
+
<component :is="icon" v-if="icon" :size="16" />
|
|
30
|
+
<span>{{ label }}</span>
|
|
31
|
+
<slot />
|
|
32
|
+
</button>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<style scoped>
|
|
36
|
+
.toolbar-pill{align-items:center;background-color:#fff;border:1px solid #dbdef0;border-radius:38px;color:var(--r-color-gray-600);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--r-text-sm);font-weight:var(--r-font-weight-medium);gap:4px;padding:4px 12px;transition:all var(--r-transition-fast);white-space:nowrap}.toolbar-pill:hover{box-shadow:0 1px 2px 0 rgba(30,41,59,.1)}.toolbar-pill--active{background-color:#2176ff;border-color:#2176ff;color:var(--r-color-white)}.toolbar-pill--active:hover{background-color:#1a65e0;border-color:#1a65e0;box-shadow:none}.toolbar-pill--primary{background-color:#2176ff;border-color:#2176ff;color:var(--r-color-white)}.toolbar-pill--primary:hover{background-color:#1a65e0;border-color:#1a65e0;box-shadow:none}.toolbar-pill--secondary{background-color:var(--r-color-gray-100);border-color:transparent;color:var(--r-color-gray-800)}.toolbar-pill--secondary:hover{background-color:var(--r-color-gray-200);border-color:transparent}.toolbar-pill--pharma{background-color:#f7520e;border-color:#f7520e;color:var(--r-color-white)}.toolbar-pill--pharma:hover{background-color:#d9470c;border-color:#d9470c}.toolbar-pill--discovery{background-color:#07816f;border-color:#07816f;color:var(--r-color-white)}.toolbar-pill--discovery:hover{background-color:#066b5c;border-color:#066b5c}.toolbar-pill--databank{background-color:#e04380;border-color:#e04380;color:var(--r-color-white)}.toolbar-pill--databank:hover{background-color:#c73a6f;border-color:#c73a6f}
|
|
37
|
+
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
export interface RmPillTabsProps {
|
|
3
|
+
/** Tab labels */
|
|
4
|
+
tabs: string[]
|
|
5
|
+
/** Active tab index */
|
|
6
|
+
modelValue?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
withDefaults(defineProps<RmPillTabsProps>(), {
|
|
10
|
+
modelValue: 0,
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
defineEmits<{
|
|
14
|
+
'update:modelValue': [index: number]
|
|
15
|
+
}>()
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div class="rm-pill-tabs">
|
|
20
|
+
<button
|
|
21
|
+
v-for="(tab, idx) in tabs"
|
|
22
|
+
:key="idx"
|
|
23
|
+
class="rm-pill-tabs__tab"
|
|
24
|
+
:class="{ 'rm-pill-tabs__tab--active': modelValue === idx }"
|
|
25
|
+
@click="$emit('update:modelValue', idx)"
|
|
26
|
+
>
|
|
27
|
+
{{ tab }}
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<style scoped>
|
|
33
|
+
.rm-pill-tabs{display:flex;gap:4px}.rm-pill-tabs__tab{background:transparent;border:none;border-radius:var(--r-radius-lg);color:var(--r-color-gray-500);cursor:pointer;font-family:var(--r-font-family);font-size:14px;font-weight:400;padding:6px 14px;transition:all var(--r-transition-fast)}.rm-pill-tabs__tab:hover{color:var(--r-color-gray-700)}.rm-pill-tabs__tab--active{background-color:var(--r-color-gray-100);color:var(--r-color-gray-900);font-weight:600}
|
|
34
|
+
</style>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { PhMagnifyingGlass } from '@phosphor-icons/vue'
|
|
3
|
+
|
|
4
|
+
const model = defineModel<string>({ default: '' })
|
|
5
|
+
|
|
6
|
+
withDefaults(defineProps<{
|
|
7
|
+
advancedActive?: boolean
|
|
8
|
+
}>(), {
|
|
9
|
+
advancedActive: false,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
defineEmits<{
|
|
13
|
+
advancedSearch: []
|
|
14
|
+
}>()
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div class="search-bar" :class="{ 'search-bar--advanced-active': advancedActive }">
|
|
19
|
+
<PhMagnifyingGlass :size="16" class="search-bar__icon" />
|
|
20
|
+
<input
|
|
21
|
+
v-model="model"
|
|
22
|
+
type="text"
|
|
23
|
+
placeholder="Search..."
|
|
24
|
+
class="search-bar__input"
|
|
25
|
+
>
|
|
26
|
+
<button
|
|
27
|
+
class="search-bar__advanced"
|
|
28
|
+
:class="{ 'search-bar__advanced--active': advancedActive }"
|
|
29
|
+
@click="$emit('advancedSearch')"
|
|
30
|
+
>
|
|
31
|
+
Advanced search
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<style scoped>
|
|
37
|
+
.search-bar{align-items:center;border:1px solid var(--r-color-gray-200);border-radius:var(--r-radius-full);display:inline-flex;gap:var(--r-space-2);height:32px;min-width:200px;padding:var(--r-space-1) var(--r-space-3);transition:border-color var(--r-transition-fast)}.search-bar:focus-within{border-color:var(--r-color-primary-400)}.search-bar__icon{color:var(--r-color-gray-400);flex-shrink:0}.search-bar__input{background:transparent;border:none;color:var(--r-color-gray-700);font-family:inherit;font-size:var(--r-text-sm);outline:none;width:100%}.search-bar__input::-moz-placeholder{color:var(--r-color-gray-400)}.search-bar__input::placeholder{color:var(--r-color-gray-400)}.search-bar__advanced{background:none;border:none;border-left:1px solid var(--r-color-gray-200);color:var(--r-color-gray-500);cursor:pointer;flex-shrink:0;font-family:inherit;font-size:var(--r-text-sm);padding:0 0 0 var(--r-space-2);text-decoration:underline;transition:color var(--r-transition-fast);white-space:nowrap}.search-bar__advanced:hover{color:var(--r-color-primary-600)}.search-bar--advanced-active{border-color:#4164ee}.search-bar__advanced--active{color:#4164ee;font-weight:var(--r-font-weight-medium)}
|
|
38
|
+
</style>
|