Skip to content

Commit 70173ba

Browse files
staredclaude
andcommitted
Fix dropdown UI consistency and URL input layout issues
- Redesigned Example selector to match Libraries dropdown style - Changed from native select to custom dropdown button - Added descriptions for each example - Consistent hover and selection states - Fixed "Load from URL" layout breaking issue - Replaced inline input with dropdown modal approach - Clean modal design with proper spacing - Prevents toolbar layout from breaking - Unified visual style across all dropdowns - Same button appearance and interaction patterns - Consistent shadows, borders, and spacing - Better mobile responsiveness 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 9898125 commit 70173ba

File tree

3 files changed

+370
-94
lines changed

3 files changed

+370
-94
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "webr-ggplot2-demo",
33
"private": true,
4-
"version": "0.2.11",
4+
"version": "0.2.12",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src/components/ExampleSelector.vue

Lines changed: 159 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { ref, computed } from 'vue'
2+
import { ref, computed, onMounted, onUnmounted } from 'vue'
33
import { examples } from '@/data/examples'
44
import type { RExample } from '@/types'
55
@@ -15,74 +15,189 @@ const emit = defineEmits<{
1515
1616
// Initialize with the first example's id
1717
const selectedExample = ref<string>(examples[0].id)
18+
const isOpen = ref(false)
19+
const dropdownRef = ref<HTMLElement>()
1820
1921
const currentExample = computed(() => {
2022
return examples.find((ex) => ex.id === selectedExample.value) || null
2123
})
2224
23-
const handleExampleChange = (): void => {
24-
if (currentExample.value) {
25-
emit('exampleSelected', currentExample.value)
25+
const handleExampleSelect = (exampleId: string): void => {
26+
selectedExample.value = exampleId
27+
isOpen.value = false
28+
const example = examples.find((ex) => ex.id === exampleId)
29+
if (example) {
30+
emit('exampleSelected', example)
2631
}
2732
}
33+
34+
const toggleDropdown = (): void => {
35+
isOpen.value = !isOpen.value
36+
}
37+
38+
const handleClickOutside = (event: MouseEvent): void => {
39+
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
40+
isOpen.value = false
41+
}
42+
}
43+
44+
onMounted(() => {
45+
document.addEventListener('click', handleClickOutside)
46+
})
47+
48+
onUnmounted(() => {
49+
document.removeEventListener('click', handleClickOutside)
50+
})
2851
</script>
2952

3053
<template>
31-
<div class="example-selector">
32-
<select
33-
id="example-select"
34-
v-model="selectedExample"
35-
class="select"
36-
@change="handleExampleChange"
54+
<div
55+
ref="dropdownRef"
56+
class="example-selector"
57+
>
58+
<button
59+
class="example-button"
60+
@click="toggleDropdown"
3761
>
38-
<option
39-
v-for="example in examples"
40-
:key="example.id"
41-
:value="example.id"
42-
>
43-
{{ example.title }}
44-
</option>
45-
</select>
62+
<span class="example-text">
63+
{{ currentExample?.title || 'Select example' }}
64+
</span>
65+
<span
66+
class="dropdown-arrow"
67+
:class="{ 'open': isOpen }"
68+
>▼</span>
69+
</button>
70+
71+
<div
72+
v-if="isOpen"
73+
class="example-dropdown"
74+
>
75+
<div class="example-list">
76+
<button
77+
v-for="example in examples"
78+
:key="example.id"
79+
class="example-item"
80+
:class="{ 'selected': example.id === selectedExample }"
81+
@click="handleExampleSelect(example.id)"
82+
>
83+
<div class="example-info">
84+
<span class="example-name">{{ example.title }}</span>
85+
<span class="example-desc">{{ example.description }}</span>
86+
</div>
87+
</button>
88+
</div>
89+
</div>
4690
</div>
4791
</template>
4892

4993
<style scoped>
5094
.example-selector {
95+
position: relative;
5196
display: flex;
5297
align-items: center;
5398
min-width: 0;
5499
flex: 1;
55100
max-width: 250px;
56101
}
57102
58-
.select {
103+
.example-button {
104+
display: flex;
105+
align-items: center;
106+
gap: 0.5rem;
59107
background: #f3f4f6;
60108
border: 1px solid #d1d5db;
61109
border-radius: 6px;
62110
padding: 0.375rem 0.625rem;
63-
padding-right: 1.5rem;
64111
font-size: 0.875rem;
65112
cursor: pointer;
66113
transition: all 0.2s ease;
114+
white-space: nowrap;
115+
min-height: 32px;
67116
width: 100%;
68117
max-width: 200px;
69-
min-height: 32px;
70-
appearance: none;
71-
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2714%27%20height%3D%278%27%20viewBox%3D%270%200%2014%208%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3Cpath%20d%3D%27M1%201l6%206%206-6%27%20stroke%3D%27%236B7280%27%20stroke-width%3D%272%27%20fill%3D%27none%27%20fill-rule%3D%27evenodd%27%2F%3E%3C%2Fsvg%3E');
72-
background-repeat: no-repeat;
73-
background-position: right 0.5rem center;
74-
background-size: 12px;
118+
justify-content: space-between;
75119
}
76120
77-
.select:hover {
121+
.example-button:hover {
78122
background: #e5e7eb;
79123
border-color: #3b82f6;
80124
}
81125
82-
.select:focus {
83-
outline: none;
84-
border-color: #3b82f6;
85-
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
126+
.example-text {
127+
font-weight: 500;
128+
color: #374151;
129+
overflow: hidden;
130+
text-overflow: ellipsis;
131+
}
132+
133+
.dropdown-arrow {
134+
font-size: 0.75rem;
135+
transition: transform 0.3s ease;
136+
margin-left: 0.25rem;
137+
flex-shrink: 0;
138+
}
139+
140+
.dropdown-arrow.open {
141+
transform: rotate(180deg);
142+
}
143+
144+
.example-dropdown {
145+
position: absolute;
146+
top: calc(100% + 0.25rem);
147+
left: 0;
148+
background: white;
149+
border: 1px solid #e5e7eb;
150+
border-radius: 6px;
151+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
152+
z-index: 1000;
153+
min-width: 280px;
154+
max-width: 350px;
155+
overflow-y: auto;
156+
max-height: 400px;
157+
}
158+
159+
.example-list {
160+
padding: 0.375rem;
161+
}
162+
163+
.example-item {
164+
display: flex;
165+
align-items: flex-start;
166+
padding: 0.5rem;
167+
border-radius: 4px;
168+
cursor: pointer;
169+
transition: background-color 0.2s ease;
170+
width: 100%;
171+
text-align: left;
172+
background: none;
173+
border: none;
174+
}
175+
176+
.example-item:hover {
177+
background: #f3f4f6;
178+
}
179+
180+
.example-item.selected {
181+
background: #e0f2fe;
182+
}
183+
184+
.example-info {
185+
display: flex;
186+
flex-direction: column;
187+
gap: 0.125rem;
188+
width: 100%;
189+
}
190+
191+
.example-name {
192+
font-size: 0.875rem;
193+
font-weight: 500;
194+
color: #374151;
195+
}
196+
197+
.example-desc {
198+
font-size: 0.6875rem;
199+
color: #6b7280;
200+
line-height: 1.3;
86201
}
87202
88203
@media (max-width: 768px) {
@@ -93,15 +208,24 @@ const handleExampleChange = (): void => {
93208
align-items: center;
94209
}
95210
96-
.select {
211+
.example-button {
97212
max-width: none;
98213
min-height: 28px;
99214
padding: 0.25rem 0.5rem;
100-
padding-right: 1.25rem;
101215
font-size: 0.8125rem;
102216
border-radius: 4px;
103-
background-size: 10px;
104-
background-position: right 0.375rem center;
217+
gap: 0.25rem;
218+
}
219+
220+
.dropdown-arrow {
221+
font-size: 0.625rem;
222+
}
223+
224+
.example-dropdown {
225+
left: 0;
226+
right: 0;
227+
max-width: calc(100vw - 2rem);
228+
z-index: 2000;
105229
}
106230
}
107231
</style>

0 commit comments

Comments
 (0)