@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.
@@ -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>