@300codes/design-system 1.0.0

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.
Files changed (47) hide show
  1. package/README.md +155 -0
  2. package/package.json +63 -0
  3. package/src/components/BaseIcon/BaseIcon.stories.ts +66 -0
  4. package/src/components/BaseIcon/BaseIcon.vue +96 -0
  5. package/src/components/BaseIcon/index.ts +2 -0
  6. package/src/components/BaseLabel/BaseLabel.stories.ts +114 -0
  7. package/src/components/BaseLabel/BaseLabel.vue +149 -0
  8. package/src/components/BaseLabel/index.ts +2 -0
  9. package/src/components/BaseTooltip/BaseTooltip.stories.ts +113 -0
  10. package/src/components/BaseTooltip/BaseTooltip.vue +123 -0
  11. package/src/components/BaseTooltip/index.ts +2 -0
  12. package/src/components/ButtonWithIcon/ButtonWithIcon.stories.ts +149 -0
  13. package/src/components/ButtonWithIcon/ButtonWithIcon.vue +77 -0
  14. package/src/components/ButtonWithIcon/index.ts +2 -0
  15. package/src/components/CheckboxInput/CheckboxInput.stories.ts +99 -0
  16. package/src/components/CheckboxInput/CheckboxInput.vue +176 -0
  17. package/src/components/CheckboxInput/index.ts +2 -0
  18. package/src/components/LabelInput/LabelInput.vue +111 -0
  19. package/src/components/LabelInput/index.ts +2 -0
  20. package/src/components/RadioInput/RadioInput.stories.ts +114 -0
  21. package/src/components/RadioInput/RadioInput.vue +174 -0
  22. package/src/components/RadioInput/index.ts +2 -0
  23. package/src/components/SearchInput/SearchInput.stories.ts +103 -0
  24. package/src/components/SearchInput/SearchInput.vue +83 -0
  25. package/src/components/SearchInput/index.ts +2 -0
  26. package/src/components/SelectInput/SelectInput.stories.ts +111 -0
  27. package/src/components/SelectInput/SelectInput.vue +497 -0
  28. package/src/components/SelectInput/index.ts +2 -0
  29. package/src/components/SelectInputField/SelectInputField.stories.ts +141 -0
  30. package/src/components/SelectInputField/SelectInputField.vue +64 -0
  31. package/src/components/SelectInputField/index.ts +2 -0
  32. package/src/components/SimpleButton/SimpleButton.stories.ts +143 -0
  33. package/src/components/SimpleButton/SimpleButton.vue +193 -0
  34. package/src/components/SimpleButton/index.ts +2 -0
  35. package/src/components/TabsList/TabsList.stories.ts +83 -0
  36. package/src/components/TabsList/TabsList.vue +156 -0
  37. package/src/components/TabsList/index.ts +2 -0
  38. package/src/components/TextInput/TextInput.stories.ts +125 -0
  39. package/src/components/TextInput/TextInput.vue +273 -0
  40. package/src/components/TextInput/components/InputIconButton.vue +54 -0
  41. package/src/components/TextInput/index.ts +2 -0
  42. package/src/components/TextInputField/TextInputField.stories.ts +133 -0
  43. package/src/components/TextInputField/TextInputField.vue +93 -0
  44. package/src/components/TextInputField/index.ts +2 -0
  45. package/src/components/index.ts +15 -0
  46. package/src/css/tokens.css +417 -0
  47. package/src/types/icon.ts +1 -0
@@ -0,0 +1,83 @@
1
+ <script setup lang="ts">
2
+ import { ref, nextTick } from 'vue';
3
+ import TextInput from '../TextInput/TextInput.vue';
4
+
5
+ export interface SearchInputProps {
6
+ name: string;
7
+ id?: string;
8
+ placeholder?: string;
9
+ autocomplete?: string;
10
+ required?: boolean;
11
+ disabled?: boolean;
12
+ size?: 'sm' | 'md' | 'lg';
13
+ iconPath?: string;
14
+ clearAriaLabel?: string;
15
+ }
16
+
17
+ withDefaults(defineProps<SearchInputProps>(), {
18
+ size: 'md',
19
+ iconPath: '/icons',
20
+ clearAriaLabel: 'Clear search',
21
+ id: undefined,
22
+ placeholder: undefined,
23
+ autocomplete: undefined,
24
+ });
25
+
26
+ const emit = defineEmits<{
27
+ focus: [FocusEvent];
28
+ blur: [FocusEvent];
29
+ enter: [];
30
+ clear: [];
31
+ }>();
32
+
33
+ const model = defineModel<string>({ required: true });
34
+
35
+ const textInputRef = ref<{ el: HTMLInputElement | undefined }>();
36
+
37
+ async function handleClickIcon(side: 'left' | 'right') {
38
+ if (side === 'right') {
39
+ model.value = '';
40
+ emit('clear');
41
+ await nextTick();
42
+ textInputRef.value?.el?.focus();
43
+ } else {
44
+ emit('enter');
45
+ }
46
+ }
47
+ </script>
48
+
49
+ <template>
50
+ <div
51
+ class="searchInput"
52
+ role="search"
53
+ >
54
+ <TextInput
55
+ :id="id"
56
+ ref="textInputRef"
57
+ v-model="model"
58
+ :name="name"
59
+ :placeholder="placeholder"
60
+ :autocomplete="autocomplete"
61
+ :required="required"
62
+ :disabled="disabled"
63
+ :size="size"
64
+ :icon-left="{ name: 'search', iconPath: iconPath, size: 'md' }"
65
+ :icon-right="model ? { name: 'close-circle', iconPath: iconPath, size: 'md', ariaLabel: clearAriaLabel } : undefined"
66
+ @focus="emit('focus', $event)"
67
+ @blur="emit('blur', $event)"
68
+ @enter="emit('enter')"
69
+ @click-icon="handleClickIcon"
70
+ />
71
+ </div>
72
+ </template>
73
+
74
+ <style scoped>
75
+ .searchInput {
76
+ --input-bg: var(--searchInput-bg, #f3f5f7);
77
+ --input-border-color: var(--searchInput-border, #b0babf);
78
+ --input-bg-hover: var(--searchInput-bg-hover, #ffffff);
79
+ --input-border-color-hover: var(--searchInput-border-hover, #0e161b);
80
+ --input-radius: var(--searchInput-radius, 3rem);
81
+ --input-radius-mobile: var(--searchInput-radius, 3rem);
82
+ }
83
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as SearchInput } from './SearchInput.vue';
2
+ export type { SearchInputProps } from './SearchInput.vue';
@@ -0,0 +1,111 @@
1
+ import { ref } from 'vue';
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
+ import type { ConcreteComponent } from 'vue';
4
+ import type { SelectInputProps } from './SelectInput.vue';
5
+ import SelectInput from './SelectInput.vue';
6
+
7
+ const OPTIONS = [
8
+ { value: 'pl', label: 'Poland' },
9
+ { value: 'de', label: 'Germany' },
10
+ { value: 'fr', label: 'France' },
11
+ { value: 'es', label: 'Spain' },
12
+ { value: 'it', label: 'Italy' },
13
+ ];
14
+
15
+ const meta: Meta<SelectInputProps> = {
16
+ title: 'Form/SelectInput',
17
+ component: SelectInput as unknown as ConcreteComponent<SelectInputProps>,
18
+ tags: ['autodocs'],
19
+ decorators: [
20
+ () => ({ template: '<div style="max-width: 25rem; width: 100%;"><story /></div>' }),
21
+ ],
22
+ argTypes: {
23
+ size: { control: 'select', options: ['sm', 'md', 'lg'] },
24
+ mobileTitle: { control: 'text' },
25
+ invalid: { control: 'boolean' },
26
+ required: { control: 'boolean' },
27
+ disabled: { control: 'boolean' },
28
+ },
29
+ };
30
+
31
+ export default meta;
32
+ type Story = StoryObj<SelectInputProps>;
33
+
34
+ export const Default: Story = {
35
+ args: { name: 'country', options: OPTIONS, placeholder: 'Select country', size: 'md' },
36
+ render: (args: SelectInputProps) => ({
37
+ components: { SelectInput },
38
+ setup() {
39
+ const value = ref('');
40
+ return { args, value };
41
+ },
42
+ template: '<SelectInput v-bind="args" v-model="value" />',
43
+ }),
44
+ };
45
+
46
+ export const WithValue: Story = {
47
+ args: { name: 'country', options: OPTIONS, placeholder: 'Select country', size: 'md' },
48
+ render: (args: SelectInputProps) => ({
49
+ components: { SelectInput },
50
+ setup() {
51
+ const value = ref('de');
52
+ return { args, value };
53
+ },
54
+ template: '<SelectInput v-bind="args" v-model="value" />',
55
+ }),
56
+ };
57
+
58
+ export const Invalid: Story = {
59
+ args: { name: 'country', options: OPTIONS, placeholder: 'Select country', invalid: true },
60
+ render: (args: SelectInputProps) => ({
61
+ components: { SelectInput },
62
+ setup() {
63
+ const value = ref('');
64
+ return { args, value };
65
+ },
66
+ template: '<SelectInput v-bind="args" v-model="value" />',
67
+ }),
68
+ };
69
+
70
+ export const Disabled: Story = {
71
+ args: { name: 'country', options: OPTIONS, placeholder: 'Select country', disabled: true },
72
+ render: (args: SelectInputProps) => ({
73
+ components: { SelectInput },
74
+ setup() {
75
+ const value = ref('pl');
76
+ return { args, value };
77
+ },
78
+ template: '<SelectInput v-bind="args" v-model="value" />',
79
+ }),
80
+ };
81
+
82
+ export const Required: Story = {
83
+ args: { name: 'country', options: OPTIONS, placeholder: 'Select country', required: true },
84
+ render: (args: SelectInputProps) => ({
85
+ components: { SelectInput },
86
+ setup() {
87
+ const value = ref('');
88
+ return { args, value };
89
+ },
90
+ template: '<SelectInput v-bind="args" v-model="value" />',
91
+ }),
92
+ };
93
+
94
+ export const Sizes: Story = {
95
+ render: () => ({
96
+ components: { SelectInput },
97
+ setup() {
98
+ const sm = ref('');
99
+ const md = ref('');
100
+ const lg = ref('');
101
+ return { sm, md, lg, OPTIONS };
102
+ },
103
+ template: `
104
+ <div class="flex flex-col gap-4 w-80">
105
+ <SelectInput name="sm" size="sm" placeholder="Small" :options="OPTIONS" v-model="sm" />
106
+ <SelectInput name="md" size="md" placeholder="Medium" :options="OPTIONS" v-model="md" />
107
+ <SelectInput name="lg" size="lg" placeholder="Large" :options="OPTIONS" v-model="lg" />
108
+ </div>
109
+ `,
110
+ }),
111
+ };
@@ -0,0 +1,497 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, nextTick, watch } from 'vue';
3
+ import { useMediaQuery, onClickOutside } from '@vueuse/core';
4
+ import BaseIcon from '../BaseIcon/BaseIcon.vue';
5
+ import InputIconButton from '../TextInput/components/InputIconButton.vue';
6
+
7
+ export type SelectOption = string | { value: string; label: string };
8
+
9
+ export interface SelectInputProps {
10
+ name: string;
11
+ id?: string;
12
+ options: readonly SelectOption[];
13
+ placeholder?: string;
14
+ mobileTitle?: string;
15
+ invalid?: boolean;
16
+ required?: boolean;
17
+ disabled?: boolean;
18
+ size?: 'sm' | 'md' | 'lg';
19
+ }
20
+
21
+ const props = withDefaults(defineProps<SelectInputProps>(), {
22
+ id: undefined,
23
+ placeholder: '',
24
+ mobileTitle: undefined,
25
+ size: 'md',
26
+ disabled: false,
27
+ });
28
+
29
+ const model = defineModel<string>({ required: true });
30
+
31
+ const isOpen = ref(false);
32
+
33
+ const wrapperRef = ref<HTMLDivElement | null>(null);
34
+ const nativeSelectRef = ref<HTMLSelectElement | null>(null);
35
+ const listRef = ref<HTMLUListElement | null>(null);
36
+ const dialogRef = ref<HTMLDialogElement | null>(null);
37
+
38
+ const isMobile = useMediaQuery('(max-width: 767px)');
39
+
40
+ onClickOutside(wrapperRef, () => {
41
+ if (!isMobile.value) isOpen.value = false;
42
+ });
43
+
44
+ const selectedLabel = computed(() => {
45
+ if (!model.value) return '';
46
+ const found = props.options.find((o) => getOptionValue(o) === model.value);
47
+ return found ? getOptionLabel(found) : '';
48
+ });
49
+
50
+ const listboxId = computed(() => `${props.id || props.name}-listbox`);
51
+
52
+ function getOptionValue(opt: SelectOption): string {
53
+ return typeof opt === 'string' ? opt : opt.value;
54
+ }
55
+
56
+ function getOptionLabel(opt: SelectOption): string {
57
+ return typeof opt === 'string' ? opt : opt.label;
58
+ }
59
+
60
+ function openDropdown() {
61
+ if (props.disabled) return;
62
+ isOpen.value = true;
63
+ nextTick(() => {
64
+ if (isMobile.value) dialogRef.value?.showModal();
65
+ const selected = listRef.value?.querySelector<HTMLElement>('[aria-selected="true"]');
66
+ const first = listRef.value?.querySelector<HTMLElement>('[role="option"]');
67
+ (selected || first)?.focus();
68
+ });
69
+ }
70
+
71
+ function closeDropdown(returnFocus = false) {
72
+ isOpen.value = false;
73
+ dialogRef.value?.close();
74
+ if (returnFocus) nextTick(() => nativeSelectRef.value?.focus());
75
+ }
76
+
77
+ function handleDialogBackdropClick(e: MouseEvent) {
78
+ const rect = dialogRef.value?.getBoundingClientRect();
79
+ if (rect && e.clientY < rect.top) closeDropdown(true);
80
+ }
81
+
82
+ function selectOption(value: string) {
83
+ model.value = value;
84
+ closeDropdown(true);
85
+ }
86
+
87
+ function handleWrapperTouchStart(e: TouchEvent) {
88
+ if (!isMobile.value || props.disabled) return;
89
+
90
+ e.preventDefault();
91
+ openDropdown();
92
+ }
93
+
94
+ function handleMouseDown(e: MouseEvent) {
95
+ e.preventDefault();
96
+ if (props.disabled) return;
97
+ if (isOpen.value) {
98
+ closeDropdown();
99
+ nativeSelectRef.value?.focus();
100
+ } else {
101
+ openDropdown();
102
+ }
103
+ }
104
+
105
+ function handleNativeKeydown(e: KeyboardEvent) {
106
+ if (['ArrowDown', 'ArrowUp', ' ', 'Enter'].includes(e.key)) {
107
+ e.preventDefault();
108
+ if (isOpen.value) {
109
+ handleListKeydown(e);
110
+ } else {
111
+ openDropdown();
112
+ }
113
+ } else if (e.key === 'Escape') {
114
+ closeDropdown(true);
115
+ }
116
+ }
117
+
118
+ function handleListKeydown(e: KeyboardEvent) {
119
+ const items = Array.from(listRef.value?.querySelectorAll<HTMLElement>('[role="option"]') ?? []);
120
+ const idx = items.indexOf(document.activeElement as HTMLElement);
121
+
122
+ if (e.key === 'ArrowDown') {
123
+ e.preventDefault();
124
+ items[idx === -1 ? 0 : (idx + 1) % items.length]?.focus();
125
+ } else if (e.key === 'ArrowUp') {
126
+ e.preventDefault();
127
+ items[idx === -1 ? items.length - 1 : (idx - 1 + items.length) % items.length]?.focus();
128
+ } else if (e.key === 'Escape' || e.key === 'Tab') {
129
+ closeDropdown(true);
130
+ }
131
+ }
132
+
133
+ watch(isMobile, () => {
134
+ if (isOpen.value) closeDropdown();
135
+ });
136
+ </script>
137
+
138
+ <template>
139
+ <div
140
+ ref="wrapperRef"
141
+ :class="[
142
+ 'selectInput',
143
+ `selectInput--${size}`,
144
+ {
145
+ 'selectInput--invalid': invalid,
146
+ 'selectInput--disabled': disabled,
147
+ 'selectInput--open': isOpen && !isMobile,
148
+ },
149
+ 'relative',
150
+ ]"
151
+ @touchstart="handleWrapperTouchStart"
152
+ >
153
+ <select
154
+ :id="id || name"
155
+ ref="nativeSelectRef"
156
+ v-model="model"
157
+ :name="name"
158
+ :disabled="disabled"
159
+ :required="required"
160
+ :class="[
161
+ 'selectInput__native',
162
+ 'w-full h-full absolute inset-0 opacity-0 cursor-pointer',
163
+ { 'pointer-events-none': isMobile },
164
+ ]"
165
+ @mousedown="handleMouseDown"
166
+ @click.prevent
167
+ @keydown="handleNativeKeydown"
168
+ >
169
+ <option
170
+ value=""
171
+ disabled
172
+ >
173
+ {{ placeholder }}
174
+ </option>
175
+ <option
176
+ v-for="opt in options"
177
+ :key="getOptionValue(opt)"
178
+ :value="getOptionValue(opt)"
179
+ >
180
+ {{ getOptionLabel(opt) }}
181
+ </option>
182
+ </select>
183
+
184
+ <div
185
+ aria-hidden="true"
186
+ :class="[
187
+ 'selectInput__trigger',
188
+ 'flex items-center leading-normal justify-between w-full pointer-events-none select-none',
189
+ {
190
+ 'selectInput__trigger--open': isOpen && !isMobile,
191
+ 'selectInput__trigger--invalid': invalid,
192
+ 'selectInput__trigger--disabled': disabled,
193
+ },
194
+ ]"
195
+ >
196
+ <span
197
+ :class="[
198
+ 'truncate',
199
+ model ? 'selectInput__trigger-value' : 'selectInput__trigger-placeholder',
200
+ ]"
201
+ >
202
+ {{ selectedLabel || placeholder }}
203
+ </span>
204
+
205
+ <BaseIcon
206
+ name="chevron-down"
207
+ size="auto"
208
+ :class="[
209
+ 'selectInput__chevron',
210
+ 'ml-2 transition-transform shrink-0',
211
+ { 'rotate-180': isOpen },
212
+ ]"
213
+ aria-hidden="true"
214
+ />
215
+ </div>
216
+
217
+ <ul
218
+ v-if="!isMobile && isOpen"
219
+ :id="listboxId"
220
+ ref="listRef"
221
+ role="listbox"
222
+ :aria-label="placeholder"
223
+ class="selectInput__dropdown absolute top-full left-0 right-0 z-10 overflow-y-auto outline-none"
224
+ @keydown="handleListKeydown"
225
+ >
226
+ <li
227
+ v-for="opt in options"
228
+ :key="getOptionValue(opt)"
229
+ role="option"
230
+ :aria-selected="getOptionValue(opt) === model"
231
+ :class="[
232
+ 'selectInput__option',
233
+ { 'selectInput__option--selected': getOptionValue(opt) === model },
234
+ 'cursor-pointer',
235
+ ]"
236
+ tabindex="0"
237
+ @touchend.prevent="selectOption(getOptionValue(opt))"
238
+ @click="selectOption(getOptionValue(opt))"
239
+ @keydown.enter.prevent="selectOption(getOptionValue(opt))"
240
+ @keydown.space.prevent="selectOption(getOptionValue(opt))"
241
+ >
242
+ {{ getOptionLabel(opt) }}
243
+ </li>
244
+ </ul>
245
+
246
+ <Teleport to="body">
247
+ <dialog
248
+ v-if="isMobile && isOpen"
249
+ ref="dialogRef"
250
+ class="selectInput__sheet border-0 m-0 w-full max-w-full p-0 fixed top-auto inset-x-0 bottom-0 overflow-y-auto"
251
+ :aria-labelledby="mobileTitle || placeholder ? `${listboxId}-title` : undefined"
252
+ @cancel.prevent="closeDropdown(true)"
253
+ @click="handleDialogBackdropClick"
254
+ >
255
+ <div
256
+ v-if="mobileTitle || placeholder"
257
+ :id="`${listboxId}-title`"
258
+ class="selectInput__sheet-header sticky top-0 flex items-center"
259
+ >
260
+ {{ mobileTitle || placeholder }}
261
+
262
+ <InputIconButton
263
+ icon-name="close"
264
+ aria-label="Close"
265
+ class="ml-auto"
266
+ @click="closeDropdown(true)"
267
+ />
268
+ </div>
269
+
270
+ <ul
271
+ :id="listboxId"
272
+ ref="listRef"
273
+ role="listbox"
274
+ :aria-label="placeholder"
275
+ class="selectInput__list outline-none"
276
+ @keydown="handleListKeydown"
277
+ >
278
+ <li
279
+ v-for="opt in options"
280
+ :key="getOptionValue(opt)"
281
+ role="option"
282
+ :aria-selected="getOptionValue(opt) === model"
283
+ :class="[
284
+ 'selectInput__option',
285
+ { 'selectInput__option--selected': getOptionValue(opt) === model },
286
+ 'cursor-pointer',
287
+ ]"
288
+ tabindex="0"
289
+ @touchend.prevent="selectOption(getOptionValue(opt))"
290
+ @click="selectOption(getOptionValue(opt))"
291
+ @keydown.enter.prevent="selectOption(getOptionValue(opt))"
292
+ @keydown.space.prevent="selectOption(getOptionValue(opt))"
293
+ >
294
+ {{ getOptionLabel(opt) }}
295
+ </li>
296
+ </ul>
297
+ </dialog>
298
+ </Teleport>
299
+ </div>
300
+ </template>
301
+
302
+ <style scoped>
303
+ @reference "tailwindcss";
304
+
305
+ .selectInput {
306
+ --_h: var(--input-h-mobile, 2.5rem);
307
+ --_px: var(--input-px-mobile, 0.625rem);
308
+ --_py: var(--input-py-mobile, 0.5rem);
309
+ --_radius: var(--input-radius-mobile, 0.25rem);
310
+ --_fs: var(--input-font-size-mobile, 1rem);
311
+ --_icon-size: var(--selectInput-chevron-size-mobile, 1.25rem);
312
+ --_dropdown-py: var(--selectInput-dropdown-py, 0.25rem);
313
+ }
314
+
315
+ @media (min-width: 48rem) {
316
+ .selectInput {
317
+ --_h: var(--input-h, 3rem);
318
+ --_px: var(--input-px, 0.75rem);
319
+ --_py: var(--input-py, 0.625rem);
320
+ --_radius: var(--input-radius, 0.375rem);
321
+ --_fs: var(--input-font-size, 1rem);
322
+ --_icon-size: var(--selectInput-chevron-size, 1.5rem);
323
+ --_list-spacing: var(--selectInput-list-spacing, 0.25rem);
324
+ }
325
+ }
326
+
327
+ .selectInput--sm {
328
+ --_h: var(--input-sm-h-mobile, 2.5rem);
329
+ --_px: var(--input-sm-px-mobile, 0.625rem);
330
+ --_py: var(--input-sm-py-mobile, 0.5rem);
331
+ --_radius: var(--input-sm-radius-mobile, 0.25rem);
332
+ --_fs: var(--input-sm-font-size-mobile, 0.875rem);
333
+ --_icon-size: var(--selectInput-sm-chevron-size-mobile, 1.25rem);
334
+ --_dropdown-py: var(--selectInput-sm-dropdown-py, 0.25rem);
335
+ }
336
+
337
+ @media (min-width: 48rem) {
338
+ .selectInput--sm {
339
+ --_h: var(--input-sm-h, 2.5rem);
340
+ --_px: var(--input-sm-px, 0.625rem);
341
+ --_py: var(--input-sm-py, 0.5rem);
342
+ --_radius: var(--input-sm-radius, 0.25rem);
343
+ --_fs: var(--input-sm-font-size, 0.875rem);
344
+ --_icon-size: var(--selectInput-sm-chevron-size, 1.5rem);
345
+ --_list-spacing: var(--selectInput-sm-list-spacing, 0.25rem);
346
+ }
347
+ }
348
+
349
+ .selectInput--lg {
350
+ --_h: var(--input-lg-h-mobile, 3rem);
351
+ --_px: var(--input-lg-px-mobile, 0.75rem);
352
+ --_py: var(--input-lg-py-mobile, 0.625rem);
353
+ --_radius: var(--input-lg-radius-mobile, 0.375rem);
354
+ --_fs: var(--input-lg-font-size-mobile, 1.125rem);
355
+ --_icon-size: var(--selectInput-lg-chevron-size-mobile, 1.75rem);
356
+ --_dropdown-py: var(--selectInput-lg-dropdown-py, 0.5rem);
357
+ }
358
+
359
+ @media (min-width: 48rem) {
360
+ .selectInput--lg {
361
+ --_h: var(--input-lg-h, 3.5rem);
362
+ --_px: var(--input-lg-px, 1rem);
363
+ --_py: var(--input-lg-py, 0.75rem);
364
+ --_radius: var(--input-lg-radius, 0.5rem);
365
+ --_fs: var(--input-lg-font-size, 1.125rem);
366
+ --_icon-size: var(--selectInput-lg-chevron-size, 2rem);
367
+ --_list-spacing: var(--selectInput-lg-list-spacing, 0.5rem);
368
+ }
369
+ }
370
+
371
+ /* ── native select ───────────────────────────────────────────────────────── */
372
+
373
+ .selectInput__native {
374
+ @apply z-1;
375
+ }
376
+
377
+ .selectInput--disabled .selectInput__native {
378
+ @apply cursor-not-allowed;
379
+ }
380
+
381
+ /* ── visual trigger ──────────────────────────────────────────────────────── */
382
+
383
+ .selectInput__trigger {
384
+ height: var(--_h);
385
+ padding: var(--_py) var(--_px);
386
+ border-radius: var(--_radius);
387
+ font-size: var(--_fs);
388
+ background-color: var(--input-bg, #ffffff);
389
+ color: var(--input-fg, #0e161b);
390
+ border: var(--input-border-width, 1px) solid var(--input-border-color, #d6dde1);
391
+ outline: var(--input-outline-width, 4px) solid transparent;
392
+ outline-offset: var(--input-outline-offset, -4px);
393
+ }
394
+
395
+ /* hover — applied on wrapper because native select captures pointer events */
396
+ .selectInput:hover:not(.selectInput--disabled):not(:has(.selectInput__dropdown:hover))
397
+ .selectInput__trigger {
398
+ background-color: var(--input-bg-hover, #f3f5f7);
399
+ border-color: var(--input-border-color-hover, #b0babf);
400
+ }
401
+
402
+ .selectInput__trigger-placeholder {
403
+ color: var(--input-placeholder-fg, #5b6a72);
404
+ }
405
+
406
+ /* focused */
407
+ .selectInput__native:focus-visible ~ .selectInput__trigger {
408
+ outline-color: var(--input-outline, #3b82f6);
409
+ }
410
+
411
+ /* ── invalid ─────────────────────────────────────────────────────────────── */
412
+
413
+ .selectInput__trigger--invalid {
414
+ border-color: var(--input-border-color-invalid, #ef4444);
415
+
416
+ @apply border-2;
417
+ }
418
+
419
+ /* ── chevron ─────────────────────────────────────────────────────────────── */
420
+
421
+ .selectInput__chevron {
422
+ color: var(--selectInput-chevron-fg, #0e161b);
423
+ width: var(--_icon-size);
424
+ height: var(--_icon-size);
425
+ }
426
+
427
+ /* ── disabled ────────────────────────────────────────────────────────────── */
428
+
429
+ .selectInput__trigger--disabled {
430
+ background-color: var(--input-bg-disabled, #d6dde1);
431
+ color: var(--input-fg-disabled, #89979f);
432
+ border-color: var(--input-border-color-disabled, #d6dde1);
433
+ }
434
+
435
+ .selectInput__trigger--disabled .selectInput__chevron {
436
+ color: var(--input-fg-disabled, #89979f);
437
+ }
438
+
439
+ /* ── desktop dropdown ────────────────────────────────────────────────────── */
440
+
441
+ .selectInput__dropdown {
442
+ background-color: var(--selectInput-dropdown-bg, #ffffff);
443
+ border: var(--selectInput-dropdown-border-width, 1px) solid
444
+ var(--selectInput-dropdown-border, #d6dde1);
445
+ border-radius: var(--selectInput-dropdown-radius, 0.375rem);
446
+ max-height: var(--selectInput-dropdown-max-h, 16rem);
447
+ padding-block: var(--_dropdown-py);
448
+ margin-top: var(--_list-spacing);
449
+ }
450
+
451
+ /* ── option ──────────────────────────────────────────────────────────────── */
452
+
453
+ .selectInput__option {
454
+ padding: var(--selectInput-option-py, 0.75rem) var(--selectInput-option-px, 1rem);
455
+ font-size: var(--_fs);
456
+ color: var(--selectInput-option-fg, #0e161b);
457
+ }
458
+
459
+ .selectInput__option:hover,
460
+ .selectInput__option:focus-visible {
461
+ background-color: var(--selectInput-option-hover-bg, #f3f5f7);
462
+
463
+ @apply outline-none;
464
+ }
465
+
466
+ .selectInput__option--selected {
467
+ background-color: var(--selectInput-option-selected-bg, #f3f5f7);
468
+ color: var(--selectInput-option-selected-fg, #1d4ed8);
469
+ }
470
+
471
+ /* ── mobile sheet (native dialog) ────────────────────────────────────────── */
472
+
473
+ .selectInput__sheet {
474
+ background-color: var(--selectInput-sheet-bg, #ffffff);
475
+ border-top-left-radius: var(--selectInput-dropdown-radius, 0.375rem);
476
+ border-top-right-radius: var(--selectInput-dropdown-radius, 0.375rem);
477
+ max-height: var(--selectInput-sheet-max-h, 80vh);
478
+ }
479
+
480
+ .selectInput__sheet::backdrop {
481
+ background-color: var(--selectInput-overlay-bg, rgba(0, 0, 0, 0.5));
482
+ }
483
+
484
+ .selectInput__sheet-header {
485
+ padding: var(--selectInput-sheet-header-py, 0.75rem) var(--selectInput-sheet-header-px, 1rem);
486
+ font-size: var(--selectInput-sheet-header-fs, 1rem);
487
+ font-weight: var(--selectInput-sheet-header-fw, 600);
488
+ color: var(--selectInput-sheet-header-fg, #0e161b);
489
+ background-color: var(--selectInput-sheet-bg, #ffffff);
490
+ border-bottom: var(--selectInput-dropdown-border-width, 1px) solid
491
+ var(--selectInput-sheet-header-border, #d6dde1);
492
+ }
493
+
494
+ .selectInput__list {
495
+ padding-block: var(--_dropdown-py, 0.25rem);
496
+ }
497
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as SelectInput } from './SelectInput.vue';
2
+ export type { SelectInputProps, SelectOption } from './SelectInput.vue';