Skip to content

Commit c12ed0a

Browse files
committed
feat: better select component
1 parent 35bdbf4 commit c12ed0a

File tree

6 files changed

+287
-23
lines changed

6 files changed

+287
-23
lines changed

components/Select.vue

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
<script setup lang="ts" generic="T extends string">
2+
import Chevron from '@/components/icons/Chevron.vue'
3+
import OptionCheck from '@/components/icons/OptionCheck.vue'
4+
import { computed, watch } from 'vue'
5+
6+
export interface SelectOption<T extends string> {
7+
label: string
8+
value: T
9+
}
10+
11+
const { placeholder = 'Select…', options = [] } = defineProps<{
12+
placeholder?: string
13+
options?: SelectOption<T>[]
14+
}>()
15+
16+
const model = defineModel<T | null | undefined>()
17+
18+
watch(
19+
() => options,
20+
(list) => {
21+
if (model.value == null) {
22+
return
23+
}
24+
25+
if (!list.some((option) => option.value === model.value)) {
26+
model.value = null
27+
}
28+
},
29+
{ deep: true }
30+
)
31+
32+
const isEmpty = computed(() => model.value == null)
33+
34+
const selectedIndex = computed(() => {
35+
if (model.value == null) {
36+
return 0
37+
}
38+
39+
const index = options.findIndex((option) => option.value === model.value)
40+
return index >= 0 ? index : 0
41+
})
42+
</script>
43+
44+
<template>
45+
<select ref="root" class="tp-select" :data-empty="isEmpty" v-model="model">
46+
<button type="button" class="tp-select-trigger">
47+
<span class="tp-select-value">
48+
<span class="tp-select-placeholder">{{ placeholder }}</span>
49+
<selectedcontent class="tp-select-selected" />
50+
</span>
51+
<Chevron class="tp-select-chevron" aria-hidden="true" />
52+
</button>
53+
<option
54+
v-for="{ label, value } in options"
55+
:key="value"
56+
class="tp-select-option"
57+
:value="value"
58+
>
59+
<span class="tp-select-option-check" aria-hidden="true">
60+
<OptionCheck class="tp-select-option-check-icon" />
61+
</span>
62+
<span class="tp-select-option-text">{{ label }}</span>
63+
</option>
64+
</select>
65+
</template>
66+
67+
<style scoped>
68+
.tp-select {
69+
appearance: base-select;
70+
anchor-name: --tp-select-anchor;
71+
--tp-select-panel-padding-y: 8px;
72+
--tp-select-trigger-padding-left: 7px;
73+
--tp-select-option-height: var(--spacer-4);
74+
--tp-select-selected-index: v-bind(selectedIndex);
75+
display: inline-flex;
76+
align-items: stretch;
77+
width: 100%;
78+
min-width: 0;
79+
height: var(--spacer-4);
80+
border-radius: var(--radius-medium);
81+
border: 1px solid var(--color-border);
82+
background: var(--color-bg);
83+
color: var(--color-text);
84+
fill: var(--color-icon);
85+
box-sizing: border-box;
86+
padding: 0;
87+
overflow: hidden;
88+
}
89+
90+
.tp-select:focus-visible {
91+
border-color: var(--color-border-selected);
92+
outline: 1px solid var(--color-border-selected);
93+
outline-offset: 0;
94+
}
95+
96+
.tp-select-trigger {
97+
appearance: none;
98+
display: inline-flex;
99+
align-items: center;
100+
gap: var(--spacer-1);
101+
padding-left: var(--tp-select-trigger-padding-left);
102+
width: 100%;
103+
height: 100%;
104+
flex: 1 0 auto;
105+
border: none;
106+
background: none;
107+
color: inherit;
108+
fill: inherit;
109+
font: inherit;
110+
cursor: inherit;
111+
}
112+
113+
.tp-select-value {
114+
flex: 1 1 auto;
115+
min-width: 0;
116+
display: inline-flex;
117+
align-items: center;
118+
}
119+
120+
.tp-select-placeholder,
121+
.tp-select-selected {
122+
flex: 1 0 0;
123+
min-width: 0;
124+
white-space: nowrap;
125+
overflow: hidden;
126+
text-overflow: ellipsis;
127+
}
128+
129+
.tp-select-placeholder {
130+
color: var(--color-text-secondary);
131+
}
132+
133+
.tp-select[data-empty='true'] .tp-select-selected,
134+
.tp-select:not([data-empty='true']) .tp-select-placeholder {
135+
display: none;
136+
}
137+
138+
.tp-select-selected {
139+
display: inline-flex;
140+
align-items: center;
141+
}
142+
143+
.tp-select-selected .tp-select-option-text {
144+
display: block;
145+
width: 100%;
146+
}
147+
148+
.tp-select-selected .tp-select-option-check {
149+
display: none;
150+
}
151+
152+
.tp-select-chevron {
153+
width: 24px;
154+
height: 24px;
155+
margin: -1px;
156+
}
157+
158+
.tp-select::picker-icon {
159+
display: none;
160+
}
161+
162+
.tp-select::picker(select) {
163+
appearance: base-select;
164+
}
165+
166+
.tp-select:open::picker(select) {
167+
position-anchor: --tp-select-anchor;
168+
top: calc(
169+
anchor(top) - var(--tp-select-panel-padding-y) - var(--tp-select-option-height) *
170+
var(--tp-select-selected-index)
171+
);
172+
left: calc(anchor(left) - var(--tp-select-trigger-padding-left));
173+
margin: 0;
174+
border-radius: var(--radius-large);
175+
padding: var(--tp-select-panel-padding-y) var(--spacer-2);
176+
background: var(--color-bg-menu);
177+
box-shadow: var(--elevation-400-menu-panel);
178+
min-width: calc(anchor-size(width) + var(--spacer-2) * 2);
179+
width: max-content;
180+
max-height: min(320px, 40vh);
181+
overflow-y: auto;
182+
display: flex;
183+
flex-direction: column;
184+
position-try-fallbacks: flip-block;
185+
z-index: 10;
186+
}
187+
188+
.tp-select-option {
189+
display: flex;
190+
align-items: center;
191+
min-height: var(--tp-select-option-height);
192+
width: var(--tp-select-width, 100%);
193+
padding: 0;
194+
border-radius: var(--radius-medium);
195+
font: inherit;
196+
fill: var(--color-icon-menu);
197+
color: var(--color-text-menu);
198+
}
199+
200+
.tp-select-option:active {
201+
background-color: transparent;
202+
}
203+
204+
.tp-select-option::checkmark {
205+
display: none;
206+
}
207+
208+
.tp-select-option-check {
209+
display: inline-flex;
210+
align-items: center;
211+
justify-content: center;
212+
padding: 0 var(--spacer-1);
213+
width: 18px;
214+
color: inherit;
215+
opacity: 0;
216+
transition: opacity 0.12s ease;
217+
}
218+
219+
.tp-select-option-check-icon {
220+
flex-shrink: 0;
221+
}
222+
223+
.tp-select-option:checked .tp-select-option-check {
224+
opacity: 1;
225+
}
226+
227+
.tp-select-option-text {
228+
flex: 1 0 0;
229+
white-space: nowrap;
230+
overflow: hidden;
231+
text-overflow: ellipsis;
232+
padding-right: 8px;
233+
}
234+
235+
.tp-select-option:is(:hover, :focus-visible) {
236+
background: var(--color-bg-menu-selected);
237+
color: var(--color-text-menu-onselected);
238+
fill: var(--color-icon-menu-onselected);
239+
outline: none;
240+
}
241+
</style>

components/icons/Chevron.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<template>
2+
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
3+
<path
4+
fill="var(--fpl-icon-color, var(--color-icon))"
5+
fill-rule="evenodd"
6+
d="M9.646 11.146a.5.5 0 0 1 .708 0L12 12.793l1.646-1.647a.5.5 0 0 1 .708.708l-2 2a.5.5 0 0 1-.708 0l-2-2a.5.5 0 0 1 0-.708"
7+
clip-rule="evenodd"
8+
></path>
9+
</svg>
10+
</template>

components/icons/OptionCheck.vue

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<template>
2+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
3+
<path
4+
fill="currentColor"
5+
fill-opacity="1"
6+
fill-rule="nonzero"
7+
stroke="none"
8+
d="M13.207 5.207 7 11.414 3.292 7.707l1.415-1.414L7 8.586l4.793-4.793z"
9+
></path>
10+
</svg>
11+
</template>

components/sections/PrefSection.vue

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script setup lang="ts">
2+
import Select, { type SelectOption } from '@/components/Select.vue'
23
import IconButton from '@/components/IconButton.vue'
34
import Inspect from '@/components/icons/Inspect.vue'
45
import Measure from '@/components/icons/Measure.vue'
@@ -26,6 +27,11 @@ useSelectAll(fontSizeInput)
2627
2728
const scaleInput = useTemplateRef('scaleInput')
2829
useSelectAll(scaleInput)
30+
31+
const cssUnitOptions: SelectOption[] = [
32+
{ label: 'px', value: 'px' },
33+
{ label: 'rem', value: 'rem' }
34+
]
2935
</script>
3036

3137
<template>
@@ -47,10 +53,12 @@ useSelectAll(scaleInput)
4753
</div>
4854
<div class="tp-row tp-row-justify tp-pref-field">
4955
<label for="css-unit">CSS unit</label>
50-
<select id="css-unit" class="tp-pref-input" v-model="options.cssUnit">
51-
<option value="px">px</option>
52-
<option value="rem">rem</option>
53-
</select>
56+
<Select
57+
id="css-unit"
58+
class="tp-pref-input"
59+
:options="cssUnitOptions"
60+
v-model="options.cssUnit"
61+
/>
5462
</div>
5563
<div class="tp-row tp-row-justify tp-pref-field">
5664
<label for="root-font-size">Root font size</label>

entrypoints/ui/style.css

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,26 +51,9 @@ tempad .tp-ellipsis {
5151
min-width: 0;
5252
}
5353

54+
tempad input,
5455
tempad select {
55-
appearance: none;
56-
height: 28px;
57-
border: 1px solid var(--color-border);
58-
border-radius: 2px;
59-
padding: 0 20px 0 7px;
60-
background-color: transparent;
61-
background-image: url("data:image/svg+xml,%3Csvg class='svg' xmlns='http://www.w3.org/2000/svg' width='8' height='7' viewBox='0 0 8 7'%3E%3Cpath fill='%230000004d' fill-opacity='1' fill-rule='evenodd' stroke='none' d='m3.646 5.354-3-3 .708-.708L4 4.293l2.646-2.647.708.708-3 3L4 5.707l-.354-.353z'%3E%3C/path%3E%3C/svg%3E");
62-
background-repeat: no-repeat;
63-
background-position: center right 8px;
64-
}
65-
66-
[data-preferred-theme="dark"] tempad select {
67-
background-image: url("data:image/svg+xml,%3Csvg class='svg' xmlns='http://www.w3.org/2000/svg' width='8' height='7' viewBox='0 0 8 7'%3E%3Cpath fill='%23ffffff66' fill-opacity='1' fill-rule='evenodd' stroke='none' d='m3.646 5.354-3-3 .708-.708L4 4.293l2.646-2.647.708.708-3 3L4 5.707l-.354-.353z'%3E%3C/path%3E%3C/svg%3E");
68-
}
69-
70-
tempad select:focus-visible {
71-
border: 1px solid var(--color-border-selected);
72-
outline: 1px solid var(--color-border-selected);
73-
outline-offset: -2px;
56+
cursor: default;
7457
}
7558

7659
tempad input:not([type]),

wxt.config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
22
import { defineConfig } from 'wxt'
33

4+
const newElements = ['selectedcontent']
5+
46
export default defineConfig({
57
modules: ['@wxt-dev/module-vue'],
8+
vue: {
9+
vite: {
10+
template: {
11+
compilerOptions: {
12+
isCustomElement: (tag) => newElements.includes(tag)
13+
}
14+
}
15+
}
16+
},
617
vite: () => ({
718
plugins: [cssInjectedByJsPlugin()],
819
optimizeDeps: {

0 commit comments

Comments
 (0)