@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,156 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, watch } from 'vue';
3
+
4
+ export interface TabsListItem {
5
+ label: string;
6
+ value: string;
7
+ disabled?: boolean;
8
+ }
9
+
10
+ export interface TabsListProps {
11
+ items: TabsListItem[];
12
+ size?: 'md' | 'lg';
13
+ }
14
+
15
+ const props = withDefaults(defineProps<TabsListProps>(), {
16
+ size: 'md',
17
+ });
18
+
19
+ const model = defineModel<string>({ default: '' });
20
+
21
+ const listRef = ref<HTMLUListElement | null>(null);
22
+ const focusedIndex = ref<number>(-1);
23
+
24
+ const activeIndex = computed<number>(() => {
25
+ const idx = props.items.findIndex((item) => item.value === model.value && !item.disabled);
26
+ return idx >= 0 ? idx : props.items.findIndex((item) => !item.disabled);
27
+ });
28
+
29
+ const tabFocusIndex = computed<number>(() => {
30
+ const idx = focusedIndex.value >= 0 ? focusedIndex.value : activeIndex.value;
31
+ return Math.min(idx, props.items.length - 1);
32
+ });
33
+
34
+ function getTabIndex(index: number): 0 | -1 {
35
+ return index === tabFocusIndex.value ? 0 : -1;
36
+ }
37
+
38
+ function handleKeydown(event: KeyboardEvent, index: number): void {
39
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
40
+ event.preventDefault();
41
+
42
+ const enabled = props.items.map((_, i) => i).filter((i) => !props.items[i].disabled);
43
+ const pos = enabled.indexOf(index);
44
+ if (pos === -1) return;
45
+
46
+ const next =
47
+ event.key === 'ArrowRight'
48
+ ? enabled[(pos + 1) % enabled.length]
49
+ : enabled[(pos - 1 + enabled.length) % enabled.length];
50
+
51
+ focusedIndex.value = next;
52
+ listRef.value?.querySelectorAll<HTMLButtonElement>('button[role="tab"]')[next]?.focus();
53
+ }
54
+
55
+ function handleFocusOut(event: FocusEvent): void {
56
+ const related = event.relatedTarget as Node | null;
57
+ if (!listRef.value?.contains(related)) {
58
+ focusedIndex.value = -1;
59
+ }
60
+ }
61
+
62
+ watch(model, () => {
63
+ focusedIndex.value = -1;
64
+ });
65
+ </script>
66
+
67
+ <template>
68
+ <ul
69
+ ref="listRef"
70
+ role="tablist"
71
+ :class="[
72
+ 'tabsList',
73
+ `tabsList--${size}`,
74
+ 'flex flex-nowrap items-center overflow-x-auto list-none m-0 p-0',
75
+ ]"
76
+ @focusout="handleFocusOut"
77
+ >
78
+ <li
79
+ v-for="(item, index) in items"
80
+ :key="item.value"
81
+ role="presentation"
82
+ class="shrink-0"
83
+ >
84
+ <button
85
+ role="tab"
86
+ :class="[
87
+ 'tabsList__tab',
88
+ {
89
+ 'tabsList__tab--active': model === item.value,
90
+ 'tabsList__tab--disabled': item.disabled,
91
+ },
92
+ 'inline-flex items-center justify-center whitespace-nowrap cursor-pointer border-0',
93
+ ]"
94
+ :tabindex="getTabIndex(index)"
95
+ :aria-selected="model === item.value"
96
+ :disabled="item.disabled"
97
+ @click="model = item.value"
98
+ @keydown="handleKeydown($event, index)"
99
+ >
100
+ {{ item.label }}
101
+ </button>
102
+ </li>
103
+ </ul>
104
+ </template>
105
+
106
+ <style scoped>
107
+ @reference "tailwindcss";
108
+
109
+ .tabsList {
110
+ gap: var(--tabsList-gap, 0.5rem);
111
+
112
+ @apply leading-[1.2];
113
+ }
114
+
115
+ .tabsList__tab {
116
+ background-color: var(--tabsList-bg, #ffffff);
117
+ color: var(--tabsList-fg, #0e161b);
118
+ border-radius: var(--tabsList-radius, 3.5rem);
119
+ padding: 0 var(--tabsList-px, 1.75rem);
120
+ height: var(--tabsList-h, 2.5rem);
121
+ font-size: var(--tabsList-font-size, 0.875rem);
122
+ font-weight: var(--tabsList-font-weight, 600);
123
+ outline: var(--tabsList-outline-width, 4px) solid transparent;
124
+ outline-offset: var(--tabsList-outline-offset, -4px);
125
+ }
126
+
127
+ .tabsList__tab:hover:not(.tabsList__tab--active):not(.tabsList__tab--disabled) {
128
+ background-color: var(--tabsList-bg-hover, #f3f5f7);
129
+ color: var(--tabsList-fg-hover, #0e161b);
130
+ }
131
+
132
+ .tabsList__tab:focus-visible {
133
+ outline-color: var(--tabsList-outline-color, #0066cc);
134
+ }
135
+
136
+ .tabsList__tab--active {
137
+ background-color: var(--tabsList-active-bg, #5c6970);
138
+ color: var(--tabsList-active-fg, #ffffff);
139
+ }
140
+
141
+ .tabsList__tab--disabled {
142
+ @apply cursor-not-allowed pointer-events-none opacity-50;
143
+ }
144
+
145
+ /* ── lg ── */
146
+
147
+ .tabsList--lg {
148
+ gap: var(--tabsList-lg-gap, 1rem);
149
+ }
150
+
151
+ .tabsList--lg .tabsList__tab {
152
+ padding: 0 var(--tabsList-lg-px, 2.25rem);
153
+ height: var(--tabsList-lg-h, 3.5rem);
154
+ font-size: var(--tabsList-lg-font-size, 1rem);
155
+ }
156
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as TabsList } from './TabsList.vue';
2
+ export type { TabsListProps, TabsListItem } from './TabsList.vue';
@@ -0,0 +1,125 @@
1
+ import { ref } from 'vue';
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
+ import type { ConcreteComponent } from 'vue';
4
+ import type { TextInputProps } from './TextInput.vue';
5
+ import TextInput from './TextInput.vue';
6
+
7
+ const meta: Meta<TextInputProps> = {
8
+ title: 'Form/TextInput',
9
+ component: TextInput as unknown as ConcreteComponent<TextInputProps>,
10
+ tags: ['autodocs'],
11
+ decorators: [
12
+ () => ({ template: '<div style="max-width: 25rem; width: 100%;"><story /></div>' }),
13
+ ],
14
+ argTypes: {
15
+ size: { control: 'select', options: ['sm', 'md', 'lg'] },
16
+ type: { control: 'select', options: ['text', 'password', 'email', 'tel', 'search'] },
17
+ filter: { control: 'select', options: [undefined, 'number', 'number-dash', 'alpha', 'alpha-space'] },
18
+ inputmode: { control: 'select', options: [undefined, 'numeric'] },
19
+ invalid: { control: 'boolean' },
20
+ required: { control: 'boolean' },
21
+ disabled: { control: 'boolean' },
22
+ },
23
+ };
24
+
25
+ export default meta;
26
+ type Story = StoryObj<TextInputProps>;
27
+
28
+ export const Default: Story = {
29
+ args: { name: 'default', size: 'md', placeholder: 'Enter value' },
30
+ render: (args: TextInputProps) => ({
31
+ components: { TextInput },
32
+ setup() {
33
+ const value = ref('');
34
+ return { args, value };
35
+ },
36
+ template: '<TextInput v-bind="args" v-model="value" />',
37
+ }),
38
+ };
39
+
40
+ export const WithIconLeft: Story = {
41
+ args: {
42
+ name: 'icon-left',
43
+ iconLeft: { name: 'search', ariaLabel: 'Search' },
44
+ placeholder: 'Search',
45
+ },
46
+ render: (args: TextInputProps) => ({
47
+ components: { TextInput },
48
+ setup() {
49
+ const value = ref('');
50
+ return { args, value };
51
+ },
52
+ template: '<TextInput v-bind="args" v-model="value" />',
53
+ }),
54
+ };
55
+
56
+ export const WithIconRight: Story = {
57
+ args: {
58
+ name: 'icon-right',
59
+ iconRight: { name: 'search', ariaLabel: 'Toggle visibility' },
60
+ placeholder: 'Search',
61
+ },
62
+ render: (args: TextInputProps) => ({
63
+ components: { TextInput },
64
+ setup() {
65
+ const value = ref('');
66
+ return { args, value };
67
+ },
68
+ template: '<TextInput v-bind="args" v-model="value" />',
69
+ }),
70
+ };
71
+
72
+ export const Invalid: Story = {
73
+ args: { name: 'invalid', invalid: true, placeholder: 'Invalid value' },
74
+ render: (args: TextInputProps) => ({
75
+ components: { TextInput },
76
+ setup() {
77
+ const value = ref('');
78
+ return { args, value };
79
+ },
80
+ template: '<TextInput v-bind="args" v-model="value" />',
81
+ }),
82
+ };
83
+
84
+ export const Disabled: Story = {
85
+ args: { name: 'disabled', disabled: true },
86
+ render: (args: TextInputProps) => ({
87
+ components: { TextInput },
88
+ setup() {
89
+ const value = ref('Disabled value');
90
+ return { args, value };
91
+ },
92
+ template: '<TextInput v-bind="args" v-model="value" />',
93
+ }),
94
+ };
95
+
96
+ export const FilterAlpha: Story = {
97
+ args: { name: 'filter-alpha', filter: 'alpha', placeholder: 'Letters only' },
98
+ render: (args: TextInputProps) => ({
99
+ components: { TextInput },
100
+ setup() {
101
+ const value = ref('');
102
+ return { args, value };
103
+ },
104
+ template: '<TextInput v-bind="args" v-model="value" />',
105
+ }),
106
+ };
107
+
108
+ export const Sizes: Story = {
109
+ render: () => ({
110
+ components: { TextInput },
111
+ setup() {
112
+ const sm = ref('');
113
+ const md = ref('');
114
+ const lg = ref('');
115
+ return { sm, md, lg };
116
+ },
117
+ template: `
118
+ <div class="flex flex-col gap-4 w-80">
119
+ <TextInput name="sm" size="sm" placeholder="Small" v-model="sm" />
120
+ <TextInput name="md" size="md" placeholder="Medium" v-model="md" />
121
+ <TextInput name="lg" size="lg" placeholder="Large" v-model="lg" />
122
+ </div>
123
+ `,
124
+ }),
125
+ };
@@ -0,0 +1,273 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, type Ref } from 'vue';
3
+ import type { IconSize } from '../../types/icon';
4
+ import InputIconButton from './components/InputIconButton.vue';
5
+
6
+ export type InputType = 'text' | 'password' | 'email' | 'tel' | 'search';
7
+ export type FilterType = 'number' | 'number-dash' | 'alpha' | 'alpha-space';
8
+
9
+ export interface TextInputIcon {
10
+ name: string;
11
+ iconPath?: string;
12
+ size?: IconSize;
13
+ ariaLabel?: string;
14
+ class?: string | object;
15
+ }
16
+
17
+ export interface TextInputProps {
18
+ name: string;
19
+ id?: string;
20
+ type?: InputType;
21
+ iconLeft?: TextInputIcon;
22
+ iconRight?: TextInputIcon;
23
+ placeholder?: string;
24
+ autocomplete?: string;
25
+ invalid?: boolean;
26
+ required?: boolean;
27
+ disabled?: boolean;
28
+ size?: 'sm' | 'md' | 'lg';
29
+ filter?: FilterType;
30
+ inputmode?: 'numeric';
31
+ }
32
+
33
+ const props = withDefaults(defineProps<TextInputProps>(), {
34
+ type: 'text',
35
+ placeholder: '',
36
+ size: 'md',
37
+ id: undefined,
38
+ iconLeft: undefined,
39
+ iconRight: undefined,
40
+ autocomplete: undefined,
41
+ filter: undefined,
42
+ inputmode: undefined,
43
+ });
44
+
45
+ const emit = defineEmits<{
46
+ focus: [FocusEvent];
47
+ blur: [FocusEvent];
48
+ clickIcon: ['left' | 'right'];
49
+ enter: [];
50
+ }>();
51
+
52
+ const model = defineModel<string>({ required: true });
53
+
54
+ const el = ref<HTMLInputElement>();
55
+
56
+ const pattern = computed(() => {
57
+ if (props.inputmode === 'numeric') return '[0-9]*';
58
+ return undefined;
59
+ });
60
+
61
+ const placeholderText = computed(() =>
62
+ props.placeholder ? `${props.placeholder}${props.required ? ' *' : ''}` : undefined
63
+ );
64
+
65
+ function onBlur(e: FocusEvent) {
66
+ setTimeout(() => emit('blur', e), 50);
67
+ }
68
+
69
+ function onFocus(e: FocusEvent) {
70
+ emit('focus', e);
71
+ }
72
+
73
+ function onInput(event: Event) {
74
+ const input = event.target as HTMLInputElement;
75
+ switch (props.filter) {
76
+ case 'number':
77
+ model.value = input.value.replace(/\D/g, '');
78
+ break;
79
+ case 'number-dash':
80
+ model.value = input.value.replace(/[^0-9-]/g, '');
81
+ break;
82
+ case 'alpha':
83
+ model.value = input.value.replace(/[^a-zA-Z]/g, '');
84
+ break;
85
+ case 'alpha-space':
86
+ model.value = input.value.replace(/[^a-zA-Z ]/g, '');
87
+ break;
88
+ }
89
+ }
90
+
91
+ defineExpose<{ el: Ref<HTMLInputElement | undefined> }>({ el });
92
+ </script>
93
+
94
+ <template>
95
+ <div :class="['textInput', `textInput--${size}`, 'relative']">
96
+ <input
97
+ :id="id || name"
98
+ ref="el"
99
+ v-model="model"
100
+ :type="type"
101
+ :name="name"
102
+ :placeholder="placeholderText"
103
+ :autocomplete="autocomplete"
104
+ :required="required"
105
+ :disabled="disabled"
106
+ :aria-disabled="disabled"
107
+ :aria-invalid="invalid"
108
+ :inputmode="inputmode"
109
+ :pattern="pattern"
110
+ :class="[
111
+ 'textInput__field',
112
+ {
113
+ 'textInput__field--icon-left': iconLeft,
114
+ 'textInput__field--icon-right': iconRight,
115
+ 'textInput__field--invalid': invalid,
116
+ 'textInput__field--valid': invalid === false,
117
+ 'textInput__field--disabled': disabled,
118
+ },
119
+ 'leading-normal w-full',
120
+ ]"
121
+ @blur="onBlur"
122
+ @focus="onFocus"
123
+ @input="onInput"
124
+ @keyup.enter="emit('enter')"
125
+ >
126
+
127
+ <InputIconButton
128
+ v-if="iconLeft"
129
+ :icon-name="iconLeft.name"
130
+ :icon-path="iconLeft.iconPath"
131
+ :icon-size="iconLeft.size"
132
+ :aria-label="iconLeft.ariaLabel"
133
+ :class="['absolute top-1/2 -translate-y-1/2 left-3', iconLeft.class]"
134
+ @click="emit('clickIcon', 'left')"
135
+ />
136
+
137
+ <InputIconButton
138
+ v-if="iconRight"
139
+ :icon-name="iconRight.name"
140
+ :icon-path="iconRight.iconPath"
141
+ :icon-size="iconRight.size"
142
+ :aria-label="iconRight.ariaLabel"
143
+ :class="['absolute top-1/2 -translate-y-1/2 right-3', iconRight.class]"
144
+ @click="emit('clickIcon', 'right')"
145
+ />
146
+ </div>
147
+ </template>
148
+
149
+ <style scoped>
150
+ @reference "tailwindcss";
151
+
152
+ .textInput {
153
+ /* md / mobile */
154
+ --_h: var(--input-h-mobile, 2.5rem);
155
+ --_px: var(--input-px-mobile, 0.625rem);
156
+ --_py: var(--input-py-mobile, 0.5rem);
157
+ --_radius: var(--input-radius-mobile, 0.25rem);
158
+ --_fs: var(--input-font-size-mobile, 1rem);
159
+ }
160
+
161
+ @media (min-width: 48rem) {
162
+ .textInput {
163
+ /* md / desktop */
164
+ --_h: var(--input-h, 3rem);
165
+ --_px: var(--input-px, 0.75rem);
166
+ --_py: var(--input-py, 0.625rem);
167
+ --_radius: var(--input-radius, 0.375rem);
168
+ --_fs: var(--input-font-size, 1rem);
169
+ }
170
+ }
171
+
172
+ .textInput--sm {
173
+ /* sm / mobile */
174
+ --_h: var(--input-sm-h-mobile, 2.5rem);
175
+ --_px: var(--input-sm-px-mobile, 0.625rem);
176
+ --_py: var(--input-sm-py-mobile, 0.5rem);
177
+ --_radius: var(--input-sm-radius-mobile, 0.25rem);
178
+ --_fs: var(--input-sm-font-size-mobile, 0.875rem);
179
+ }
180
+
181
+ @media (min-width: 48rem) {
182
+ .textInput--sm {
183
+ /* sm / desktop */
184
+ --_h: var(--input-sm-h, 2.5rem);
185
+ --_px: var(--input-sm-px, 0.625rem);
186
+ --_py: var(--input-sm-py, 0.5rem);
187
+ --_radius: var(--input-sm-radius, 0.25rem);
188
+ --_fs: var(--input-sm-font-size, 0.875rem);
189
+ }
190
+ }
191
+
192
+ .textInput--lg {
193
+ /* lg / mobile */
194
+ --_h: var(--input-lg-h-mobile, 3rem);
195
+ --_px: var(--input-lg-px-mobile, 0.75rem);
196
+ --_py: var(--input-lg-py-mobile, 0.625rem);
197
+ --_radius: var(--input-lg-radius-mobile, 0.375rem);
198
+ --_fs: var(--input-lg-font-size-mobile, 1.125rem);
199
+ }
200
+
201
+ @media (min-width: 48rem) {
202
+ .textInput--lg {
203
+ /* lg / desktop */
204
+ --_h: var(--input-lg-h, 3.5rem);
205
+ --_px: var(--input-lg-px, 1rem);
206
+ --_py: var(--input-lg-py, 0.75rem);
207
+ --_radius: var(--input-lg-radius, 0.5rem);
208
+ --_fs: var(--input-lg-font-size, 1.125rem);
209
+ }
210
+ }
211
+
212
+ /* ── field ───────────────────────────────────────────────────────────────── */
213
+
214
+ .textInput__field {
215
+ height: var(--_h);
216
+ padding: var(--_py) var(--_px);
217
+ border-radius: var(--_radius);
218
+ font-size: var(--_fs);
219
+ background-color: var(--input-bg, #ffffff);
220
+ color: var(--input-fg, #0e161b);
221
+ border: var(--input-border-width, 1px) solid var(--input-border-color, #d6dde1);
222
+ outline: var(--input-outline-width, 4px) solid transparent;
223
+ outline-offset: var(--input-outline-offset, -4px);
224
+ }
225
+
226
+ .textInput__field::placeholder {
227
+ color: var(--input-placeholder-fg, #5b6a72);
228
+ }
229
+
230
+ .textInput__field:hover:not(:disabled) {
231
+ background-color: var(--input-bg-hover, #f3f5f7);
232
+ border-color: var(--input-border-color-hover, #b0babf);
233
+ }
234
+
235
+ .textInput__field:focus {
236
+ outline-color: transparent;
237
+ }
238
+
239
+ .textInput__field:focus-visible {
240
+ outline-color: var(--input-outline, #3b82f6);
241
+ }
242
+
243
+ .textInput__field--icon-left {
244
+ padding-left: var(--input-icon-pl, 3rem);
245
+ }
246
+
247
+ .textInput__field--icon-right,
248
+ .textInput__field--invalid,
249
+ .textInput__field--valid {
250
+ padding-right: var(--input-icon-pr, 3rem);
251
+ }
252
+
253
+ /* ── invalid ─────────────────────────────────────────────────────────────── */
254
+
255
+ .textInput__field--invalid {
256
+ border-color: var(--input-border-color-invalid, #ef4444);
257
+ border-width: 2px;
258
+ }
259
+
260
+ /* ── disabled ────────────────────────────────────────────────────────────── */
261
+
262
+ .textInput__field--disabled {
263
+ background-color: var(--input-bg-disabled, #d6dde1);
264
+ color: var(--input-fg-disabled, #89979f);
265
+ border-color: var(--input-border-color-disabled, #d6dde1);
266
+
267
+ @apply cursor-not-allowed;
268
+ }
269
+
270
+ .textInput__field--disabled::placeholder {
271
+ color: var(--input-placeholder-fg-disabled, #89979f);
272
+ }
273
+ </style>
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ import type { IconSize } from '../../../types/icon';
3
+ import BaseIcon from '../../BaseIcon/BaseIcon.vue';
4
+
5
+ export interface InputIconButtonProps {
6
+ iconName: string;
7
+ iconPath?: string;
8
+ iconSize?: IconSize;
9
+ ariaLabel?: string;
10
+ }
11
+
12
+ withDefaults(defineProps<InputIconButtonProps>(), {
13
+ iconPath: '/icons',
14
+ iconSize: 'sm',
15
+ ariaLabel: undefined,
16
+ });
17
+
18
+ const emit = defineEmits<{
19
+ click: [];
20
+ }>();
21
+ </script>
22
+
23
+ <template>
24
+ <button
25
+ type="button"
26
+ class="inputIconButton inline-flex items-center justify-center cursor-pointer"
27
+ :aria-label="ariaLabel"
28
+ @click="emit('click')"
29
+ >
30
+ <BaseIcon
31
+ :name="iconName"
32
+ :icon-path="iconPath"
33
+ :size="iconSize"
34
+ />
35
+ </button>
36
+ </template>
37
+
38
+ <style scoped>
39
+ .inputIconButton {
40
+ color: var(--input-icon-fg, #0e161b);
41
+ background: var(--input-icon-bg, transparent);
42
+ padding: var(--input-icon-p, 0.25rem);
43
+ border-radius: var(--input-icon-radius, 9999px);
44
+ }
45
+
46
+ .inputIconButton:hover {
47
+ background-color: var(--input-icon-bg-hover, #f3f4f6);
48
+ }
49
+
50
+ .inputIconButton:focus-visible {
51
+ outline: 2px solid var(--input-icon-outline, #0066cc);
52
+ outline-offset: 1px;
53
+ }
54
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as TextInput } from './TextInput.vue';
2
+ export type { TextInputProps, TextInputIcon, InputType, FilterType } from './TextInput.vue';