adata-ui 2.1.40-beta.1 → 2.1.40-beta.11

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 (56) hide show
  1. package/.claude/settings.local.json +19 -0
  2. package/components/elements/a-select-row/ASelectRowV2.vue +214 -0
  3. package/components/elements/button/AButtonV2.vue +112 -0
  4. package/components/elements/leave-note/ALeaveNote.vue +38 -17
  5. package/components/elements/segmented/ASegmentedV2.vue +58 -0
  6. package/components/elements/select/ASelectV2.vue +589 -0
  7. package/components/elements/show-more/AShowMoreV2.vue +26 -0
  8. package/components/elements/star-rating/AStarRating.vue +12 -4
  9. package/components/elements/status-badge-v2/StatusBadgeV2.vue +78 -0
  10. package/components/features/payment/process/PaymentKaspiQrSidePanel.vue +43 -27
  11. package/components/features/payment/process/PaymentKaspiRedirectSidePanel.vue +34 -23
  12. package/components/features/payment/process/PaymentMethodSidePanel.vue +61 -49
  13. package/components/features/payment/process/PaymentProcess.vue +6 -6
  14. package/components/features/payment/process/PaymentTopUpSidePanel.vue +44 -63
  15. package/components/forms/checkbox/ACheckboxV2.vue +229 -0
  16. package/components/forms/feedback-form/FeedbackForm.vue +8 -8
  17. package/components/forms/input/AInputV2.vue +542 -0
  18. package/components/forms/input/textarea/ATextareaV2.vue +248 -0
  19. package/components/forms/toggle/AToggleV2.vue +71 -0
  20. package/components/modals/AnotherDeviceModal.vue +22 -18
  21. package/components/modals/ApplicationAcceptedModal.vue +12 -15
  22. package/components/modals/ConnectingTariffModal.vue +13 -20
  23. package/components/modals/ContentNavigationModal.vue +77 -333
  24. package/components/modals/Insufficient-funds-modal.vue +18 -33
  25. package/components/modals/LimitReachedModal.vue +4 -3
  26. package/components/modals/NoAccessModal.vue +9 -11
  27. package/components/modals/PaymentMethodModal.vue +1 -1
  28. package/components/modals/ReportBugConfirmModal.vue +12 -10
  29. package/components/modals/ReportBugModal.vue +42 -36
  30. package/components/modals/Resend.vue +9 -10
  31. package/components/modals/SubmitApplicationModal.vue +27 -24
  32. package/components/modals/id/IdConfirmAccountOtpModal.vue +4 -9
  33. package/components/modals/id/IdLoginModal.vue +3 -8
  34. package/components/modals/id/IdModals.vue +23 -10
  35. package/components/modals/id/IdTwoFactorModal.vue +3 -8
  36. package/components/navigation/bottom-navigation/ABottomNavigation.vue +1 -1
  37. package/components/navigation/header/AHeader.vue +15 -19
  38. package/components/navigation/header/HeaderLink.vue +15 -21
  39. package/components/navigation/header/NavList.vue +8 -32
  40. package/components/navigation/header/ProductMenu.vue +52 -50
  41. package/components/navigation/pill-tabs/APillTabsV2.vue +134 -0
  42. package/components/overlays/modal/AModalV2.vue +391 -0
  43. package/composables/useAccessTokenCookie.ts +25 -0
  44. package/composables/useChipOverflow.ts +92 -0
  45. package/composables/useHeaderNavigationLinks.ts +53 -23
  46. package/icons/arrow/arrow-right.vue +14 -0
  47. package/icons/briefcase.vue +14 -0
  48. package/icons/car-side.vue +16 -0
  49. package/icons/cart.vue +15 -0
  50. package/icons/download-rounded.vue +15 -0
  51. package/icons/shield-verified.vue +14 -0
  52. package/lang/en.ts +35 -4
  53. package/lang/kk.ts +36 -5
  54. package/lang/ru.ts +37 -6
  55. package/package.json +1 -1
  56. package/shared/constans/pages.ts +8 -2
@@ -0,0 +1,19 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "PowerShell(npm view *)",
5
+ "PowerShell(git rm *)",
6
+ "PowerShell(npx --yes @tailwindcss/upgrade@latest --force)",
7
+ "PowerShell(Out-File *)",
8
+ "PowerShell(pnpm install *)",
9
+ "PowerShell(Get-ChildItem node_modules\\\\eslint-plugin-tailwindcss\\\\lib\\\\rules | Select-Object -ExpandProperty Name; node -e \"const p=require\\('eslint-plugin-tailwindcss'\\); console.log\\(Object.keys\\(p.rules\\).join\\('\\\\n'\\)\\); console.log\\('CONFIGS:', Object.keys\\(p.configs\\).join\\(','\\)\\)\")",
10
+ "PowerShell(node -e \"const p=require\\('eslint-plugin-tailwindcss'\\); console.log\\(JSON.stringify\\(p.configs.recommended, \\(k,v\\)=> typeof v==='object'&&v!==null&&k==='plugins'?Object.keys\\(v\\):v, 2\\).substring\\(0,800\\)\\)\"; Select-String -Path node_modules\\\\eslint-plugin-tailwindcss\\\\README.md -Pattern 'entryPoint|settings' | Select-Object -First 10 | ForEach-Object Line)",
11
+ "PowerShell(Select-String -Path node_modules\\\\eslint-plugin-tailwindcss\\\\README.md -Pattern 'cssConfigPath|vue|\\\\.vue' -Context 2,4 | Select-Object -First 12 | ForEach-Object {$_.Context.PreContext + $_.Line + $_.Context.PostContext} | ForEach-Object {$_})",
12
+ "PowerShell(pnpm build *)",
13
+ "PowerShell($c = Get-Content .playground\\\\.output\\\\server\\\\chunks\\\\build\\\\entry-styles.B6sbRG45.mjs -Raw; foreach \\($m in '--color-deepblue-900','bg-gr-blue','heading-01','hover\\\\:bg-purple-900\\\\/20','a-container','--font-sans','outline-hidden','scrollbar-gutter'\\) { \"$m => $\\($c.Contains\\($m\\)\\)\" })",
14
+ "PowerShell(pnpm eslint *)",
15
+ "PowerShell(pnpm *)",
16
+ "PowerShell(Get-Content C:\\\\adata\\\\adata-ui\\\\app.vue; \"=== -error.vue \\(first 100\\) ===\"; Get-Content C:\\\\adata\\\\adata-ui\\\\-error.vue -TotalCount 100; \"=== .playground/app.vue ===\"; Get-Content C:\\\\adata\\\\adata-ui\\\\.playground\\\\app.vue -TotalCount 60)"
17
+ ]
18
+ }
19
+ }
@@ -0,0 +1,214 @@
1
+ <script setup lang="ts">
2
+ import { useChipOverflow } from '#adata-ui/composables/useChipOverflow'
3
+
4
+ interface Item {
5
+ id: number | string
6
+ name: string
7
+ }
8
+
9
+ interface Props {
10
+ label: string
11
+ selectedItems: Item[]
12
+ clearable?: boolean
13
+ disabled?: boolean
14
+ deletable?: boolean
15
+ btnClass?: string
16
+ size?: 'sm' | 'md'
17
+ required?: boolean
18
+ }
19
+
20
+ const props = withDefaults(defineProps<Props>(), {
21
+ clearable: true,
22
+ disabled: false,
23
+ deletable: false,
24
+ btnClass: '',
25
+ size: 'md',
26
+ required: false,
27
+ })
28
+
29
+ const emit = defineEmits<{
30
+ (e: 'delete', item: Item): void
31
+ (e: 'clear'): void
32
+ }>()
33
+
34
+ const isOpen = defineModel<boolean>('isOpen', { default: false })
35
+
36
+ const chipsRow = ref<HTMLElement | null>(null)
37
+ const measureRow = ref<HTMLElement | null>(null)
38
+
39
+ const itemCount = computed(() => props.selectedItems?.length ?? 0)
40
+
41
+ const { visibleCount, hiddenCount } = useChipOverflow({
42
+ container: chipsRow,
43
+ measure: measureRow,
44
+ count: itemCount,
45
+ contentKey: computed(() => props.selectedItems?.map(item => item.name).join(' ') ?? ''),
46
+ gap: 6,
47
+ })
48
+
49
+ const visibleItems = computed(() => props.selectedItems.slice(0, visibleCount.value))
50
+
51
+ const showFloatingLabel = computed(() => !!props.label && !visibleItems.value?.length)
52
+
53
+ const hasValue = computed(() => itemCount.value > 0)
54
+
55
+ const isLabelFloated = computed(() => hasValue.value || isOpen.value)
56
+
57
+ function deleteItem(item: Item) {
58
+ emit('delete', item)
59
+ }
60
+
61
+ function onClear() {
62
+ emit('clear')
63
+ }
64
+
65
+ const triggerLayoutClass = computed(() => {
66
+ if (hasValue.value || !props.label) {
67
+ return props.size === 'md' ? 'h-10 py-1.5' : 'h-9 py-1.5'
68
+ }
69
+ return props.size === 'md' ? 'h-10 pt-3.5 pb-1' : 'h-9 pt-[14px] pb-1'
70
+ })
71
+
72
+ defineExpose({ onClear })
73
+ </script>
74
+
75
+ <template>
76
+ <div class="select-row-v2 relative w-full text-sm">
77
+ <button
78
+ type="button"
79
+ :disabled="disabled"
80
+ :class="[
81
+ triggerLayoutClass,
82
+ clearable ? 'pr-16' : 'pr-9',
83
+ disabled
84
+ ? 'border-gray-200 bg-gray-100/70 dark:border-gray-700 dark:bg-white/[0.03]'
85
+ : (btnClass || 'border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900'),
86
+ { 'select-row-v2__trigger--open': isOpen },
87
+ ]"
88
+ class="select-row-v2__trigger text-deepblue-900 relative flex w-full items-center gap-2 rounded-[10px] border border-solid pl-4 transition-colors duration-200 hover:border-blue-500 focus-visible:border-blue-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600/20 dark:text-gray-200 dark:hover:border-blue-400 dark:focus-visible:border-blue-400 dark:focus-visible:ring-blue-400/20"
89
+ @click="isOpen = !isOpen"
90
+ >
91
+ <span
92
+ v-if="showFloatingLabel"
93
+ :data-size="size"
94
+ class="select-row-v2__label pointer-events-none absolute left-4 top-1/2 max-w-[calc(100%-48px)] truncate text-gray-500 dark:text-gray-400"
95
+ :class="{ 'select-row-v2__label--floated': isLabelFloated }"
96
+ >
97
+ {{ label }}<span v-if="required" class="text-red-500 dark:text-red-400">&nbsp;*</span>
98
+ </span>
99
+
100
+ <span ref="chipsRow" class="relative flex w-full min-w-0 items-center gap-1.5 overflow-hidden text-start leading-none">
101
+ <template v-if="hasValue">
102
+ <span
103
+ v-for="item in visibleItems"
104
+ :key="item.id"
105
+ class="select-row-v2__chip inline-flex min-w-0 items-center gap-1 rounded-md border border-gray-200 bg-gray-50 px-2 py-0.5 text-xs dark:border-gray-700 dark:bg-white/[0.06]"
106
+ @click.stop="deletable ? deleteItem(item) : (isOpen = true)"
107
+ >
108
+ <span class="truncate">{{ item.name }}</span>
109
+ <button
110
+ v-if="deletable"
111
+ type="button"
112
+ class="flex shrink-0 items-center justify-center rounded-full p-0.5 transition-colors hover:bg-gray-200 dark:hover:bg-white/[0.12]"
113
+ >
114
+ <a-icon-x-mark class="size-2.5" />
115
+ </button>
116
+ </span>
117
+ <span
118
+ v-if="hiddenCount > 0"
119
+ class="inline-flex shrink-0 items-center whitespace-nowrap rounded-md border border-blue-200 bg-blue-50 px-2 py-0.5 text-xs font-medium text-blue-700 dark:border-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
120
+ >
121
+ +{{ hiddenCount }}
122
+ </span>
123
+
124
+ <span
125
+ ref="measureRow"
126
+ aria-hidden="true"
127
+ class="pointer-events-none invisible absolute left-0 top-0 flex h-0 w-max items-center gap-1.5"
128
+ >
129
+ <span
130
+ v-for="item in selectedItems"
131
+ :key="`measure-${item.id}`"
132
+ class="inline-flex items-center gap-1 whitespace-nowrap rounded-md border border-gray-200 bg-gray-50 px-2 py-0.5 text-xs"
133
+ >
134
+ <span>{{ item.name }}</span>
135
+ <span
136
+ v-if="deletable"
137
+ class="flex shrink-0 items-center justify-center rounded-full p-0.5"
138
+ >
139
+ <a-icon-x-mark class="size-2.5" />
140
+ </span>
141
+ </span>
142
+ <span class="inline-flex shrink-0 items-center whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium">
143
+ +{{ itemCount }}
144
+ </span>
145
+ </span>
146
+ </template>
147
+ <span v-else class="text-gray-500 dark:text-gray-400">&#8203;</span>
148
+ </span>
149
+
150
+ <button
151
+ v-if="clearable && hasValue && !disabled"
152
+ type="button"
153
+ class="absolute right-9 top-1/2 flex -translate-y-1/2 items-center justify-center rounded-full p-0.5 transition-colors hover:bg-gray-100 dark:hover:bg-white/[0.08] dark:hover:text-gray-300"
154
+ @click.stop="onClear"
155
+ >
156
+ <a-icon-x-mark class="!m-0 size-3.5" />
157
+ </button>
158
+
159
+ <span
160
+ :class="{ 'rotate-180': isOpen }"
161
+ class="absolute right-3 top-1/2 -translate-y-1/2 transition-transform duration-200"
162
+ >
163
+ <a-icon-chevron-down class="!m-0 size-4 text-gray-400 dark:text-gray-500" />
164
+ </span>
165
+ </button>
166
+ </div>
167
+ </template>
168
+
169
+ <style scoped>
170
+ .select-row-v2__trigger:disabled {
171
+ pointer-events: none;
172
+ cursor: not-allowed;
173
+ opacity: 0.7;
174
+ }
175
+
176
+ .select-row-v2__chip {
177
+ cursor: pointer;
178
+ transition: background-color 150ms;
179
+ }
180
+
181
+ .select-row-v2__chip:hover {
182
+ background-color: theme('colors.gray.100');
183
+ }
184
+
185
+ :is(.dark) .select-row-v2__chip:hover {
186
+ background-color: rgb(255 255 255 / 0.08);
187
+ }
188
+
189
+ .select-row-v2__label {
190
+ font-size: 14px;
191
+ line-height: 1.3;
192
+ transform: translateY(-50%);
193
+ transition:
194
+ transform 300ms cubic-bezier(0.22, 1, 0.36, 1),
195
+ font-size 300ms cubic-bezier(0.22, 1, 0.36, 1),
196
+ color 300ms ease-out;
197
+ }
198
+
199
+ .select-row-v2__label--floated {
200
+ font-size: 10px;
201
+ transform: translateY(-17px);
202
+ }
203
+
204
+ .select-row-v2__label--floated[data-size="sm"] {
205
+ transform: translateY(-14px);
206
+ }
207
+
208
+ @media (prefers-reduced-motion: reduce) {
209
+ .select-row-v2__label,
210
+ .select-row-v2__trigger {
211
+ transition: none;
212
+ }
213
+ }
214
+ </style>
@@ -0,0 +1,112 @@
1
+ <script setup lang="ts">
2
+ import type { Component } from 'vue'
3
+
4
+ type View = 'solid' | 'outline' | 'ghost' | 'soft'
5
+ type Size = 'sm' | 'md' | 'lg'
6
+ type Color = 'blue' | 'green' | 'red' | 'gray'
7
+
8
+ const props = withDefaults(defineProps<{
9
+ view?: View
10
+ color?: Color
11
+ size?: Size
12
+ icon?: Component
13
+ iconRight?: boolean
14
+ loading?: boolean
15
+ disabled?: boolean
16
+ block?: boolean
17
+ type?: 'button' | 'submit' | 'reset'
18
+ }>(), {
19
+ view: 'solid',
20
+ color: 'blue',
21
+ size: 'md',
22
+ iconRight: false,
23
+ loading: false,
24
+ disabled: false,
25
+ block: false,
26
+ type: 'button',
27
+ })
28
+
29
+ const emit = defineEmits<{ click: [event: MouseEvent] }>()
30
+
31
+ const VIEW_CLASS: Record<Color, Record<View, string>> = {
32
+ blue: {
33
+ solid: 'bg-blue-600 text-white shadow-sm hover:bg-blue-700 hover:shadow-md active:bg-blue-800 dark:bg-blue-500 dark:hover:bg-blue-400',
34
+ outline: 'border border-blue-500 bg-white text-blue-700 hover:bg-blue-50 active:bg-blue-100 dark:border-blue-500 dark:bg-gray-900 dark:text-blue-300 dark:hover:bg-blue-950/40',
35
+ ghost: 'text-deepblue-900 hover:bg-gray-100 active:bg-gray-200 dark:text-gray-100 dark:hover:bg-white/[0.06] dark:active:bg-white/[0.1]',
36
+ soft: 'bg-blue-50 text-blue-700 hover:bg-blue-100 active:bg-blue-200/70 dark:bg-blue-950/40 dark:text-blue-300 dark:hover:bg-blue-900/40',
37
+ },
38
+ green: {
39
+ solid: 'bg-green-600 text-white shadow-sm hover:bg-green-700 hover:shadow-md active:bg-green-800 dark:bg-green-500 dark:hover:bg-green-400',
40
+ outline: 'border border-green-500 bg-white text-green-700 hover:bg-green-50 active:bg-green-100 dark:border-green-500 dark:bg-gray-900 dark:text-green-300 dark:hover:bg-green-950/40',
41
+ ghost: 'text-green-700 hover:bg-green-50 active:bg-green-100 dark:text-green-300 dark:hover:bg-green-950/40',
42
+ soft: 'bg-green-50 text-green-700 hover:bg-green-100 active:bg-green-200/70 dark:bg-green-950/40 dark:text-green-300 dark:hover:bg-green-900/40',
43
+ },
44
+ red: {
45
+ solid: 'bg-red-600 text-white shadow-sm hover:bg-red-700 hover:shadow-md active:bg-red-800 dark:bg-red-500 dark:hover:bg-red-400',
46
+ outline: 'border border-red-500 bg-white text-red-700 hover:bg-red-50 active:bg-red-100 dark:border-red-500 dark:bg-gray-900 dark:text-red-300 dark:hover:bg-red-950/40',
47
+ ghost: 'text-red-700 hover:bg-red-50 active:bg-red-100 dark:text-red-300 dark:hover:bg-red-950/40',
48
+ soft: 'bg-red-50 text-red-700 hover:bg-red-100 active:bg-red-200/70 dark:bg-red-950/40 dark:text-red-300 dark:hover:bg-red-900/40',
49
+ },
50
+ gray: {
51
+ solid: 'bg-gray-600 text-white shadow-sm hover:bg-gray-700 hover:shadow-md active:bg-gray-800 dark:bg-gray-500 dark:hover:bg-gray-400',
52
+ outline: 'border border-gray-300 bg-white text-deepblue-900 hover:border-gray-400 hover:bg-gray-50 active:bg-gray-100 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:border-gray-600 dark:hover:bg-white/[0.06]',
53
+ ghost: 'text-deepblue-900 hover:bg-gray-100 active:bg-gray-200 dark:text-gray-100 dark:hover:bg-white/[0.06] dark:active:bg-white/[0.1]',
54
+ soft: 'bg-gray-100 text-gray-700 hover:bg-gray-200 active:bg-gray-200/70 dark:bg-gray-800/60 dark:text-gray-200 dark:hover:bg-gray-700/60',
55
+ },
56
+ }
57
+
58
+ const SIZE_CLASS: Record<Size, string> = {
59
+ sm: 'h-8 gap-1.5 px-3 text-xs',
60
+ md: 'h-10 gap-2 px-4 text-sm',
61
+ lg: 'h-12 gap-2.5 px-5 text-base',
62
+ }
63
+
64
+ const ICON_SIZE: Record<Size, string> = {
65
+ sm: 'size-4',
66
+ md: 'size-5',
67
+ lg: 'size-5',
68
+ }
69
+
70
+ function onClick(event: MouseEvent) {
71
+ if (props.disabled || props.loading) return
72
+ emit('click', event)
73
+ }
74
+ </script>
75
+
76
+ <template>
77
+ <button
78
+ :type="type"
79
+ :disabled="disabled || loading"
80
+ class="button-v2 inline-flex select-none items-center justify-center rounded-xl font-semibold transition-all duration-150 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
81
+ :class="[
82
+ VIEW_CLASS[color][view],
83
+ SIZE_CLASS[size],
84
+ block ? 'w-full' : '',
85
+ iconRight ? 'flex-row-reverse' : '',
86
+ ]"
87
+ @click="onClick"
88
+ >
89
+ <a-icon-loader-circle
90
+ v-if="loading"
91
+ class="shrink-0 animate-spin"
92
+ :class="ICON_SIZE[size]"
93
+ />
94
+ <component
95
+ :is="icon"
96
+ v-else-if="icon"
97
+ class="shrink-0"
98
+ :class="ICON_SIZE[size]"
99
+ />
100
+ <span v-if="$slots.default" class="truncate">
101
+ <slot />
102
+ </span>
103
+ </button>
104
+ </template>
105
+
106
+ <style scoped>
107
+ @media (prefers-reduced-motion: reduce) {
108
+ .button-v2 {
109
+ transition: none;
110
+ }
111
+ }
112
+ </style>
@@ -1,13 +1,18 @@
1
1
  <script setup lang="ts">
2
2
  import * as z from 'zod'
3
3
  import { useUIValidation } from '#adata-ui/composables/useUIValidation'
4
+ import AButtonV2 from '#adata-ui/components/elements/button/AButtonV2.vue'
4
5
 
5
- const props = defineProps<{
6
+ const props = withDefaults(defineProps<{
6
7
  isError?: boolean,
7
8
  errorText?: string
8
- }>()
9
+ }>(), {
10
+ isError: false,
11
+ errorText: '',
12
+ })
9
13
 
10
14
  const emit = defineEmits<{
15
+ (e: 'onCancel'): void
11
16
  (e: 'onSubmit'): void
12
17
  }>()
13
18
 
@@ -30,6 +35,12 @@ const { validation, getError } = useUIValidation(
30
35
  submitted,
31
36
  )
32
37
 
38
+ function onCancel() {
39
+ submitted.value = false
40
+ note.value = ''
41
+ emit('onCancel')
42
+ }
43
+
33
44
  async function onSubmit() {
34
45
  submitted.value = true
35
46
  if (validation.value) return
@@ -48,31 +59,41 @@ defineExpose({
48
59
  </p>
49
60
  <a-textarea
50
61
  v-model="note"
62
+ class="min-h-[120px]"
63
+ color-classes="bg-deepblue-900/5 outline outline-1 outline-gray-200 dark:outline-gray-600 dark:bg-gray-200/5 !rounded-[10px]"
64
+ size="sm"
65
+ resizeable="y"
51
66
  :label="t('forms.leaveNote.placeholder')"
52
67
  :error="getError('note')"
53
- class="mb-2 min-h-[120px]"
54
- resizeable="none"
55
- clearable
56
- required
57
68
  >
58
69
  <template #alert>
59
70
  <a-alert
60
- v-if="isError || getError('note')"
71
+ v-if="isError"
61
72
  color="red"
62
73
  class="mt-1"
63
74
  >
64
- {{ getError('note') || (isError ? (!!errorText ? errorText : t('forms.leaveNote.limit')) : '') }}
75
+ {{ isError ? (!!errorText ? errorText : t('forms.leaveNote.limit')) : '' }}
65
76
  </a-alert>
66
77
  </template>
67
78
  </a-textarea>
68
- <a-button
69
- :disabled="!note.trim() && isLoading"
70
- class="w-full sm:w-auto"
71
- size="lg"
72
- :loading="isLoading"
73
- @click="onSubmit"
74
- >
75
- {{ t('actions.send') }}
76
- </a-button>
79
+ <div class="mt-2 flex w-full justify-end items-center gap-2 md:gap-[10px]">
80
+ <a-button-v2
81
+ class="w-full sm:w-auto"
82
+ view="outline"
83
+ size="md"
84
+ @click="onCancel"
85
+ >
86
+ {{ t('actions.cancel') }}
87
+ </a-button-v2>
88
+ <a-button-v2
89
+ :disabled="!note.trim() && isLoading"
90
+ class="w-full sm:w-auto"
91
+ size="md"
92
+ :loading="isLoading"
93
+ @click="onSubmit"
94
+ >
95
+ {{ t('actions.send') }}
96
+ </a-button-v2>
97
+ </div>
77
98
  </div>
78
99
  </template>
@@ -0,0 +1,58 @@
1
+ <script generic="T extends string | number" lang="ts" setup>
2
+ interface Option {
3
+ value: T
4
+ label: string
5
+ disabled?: boolean
6
+ count?: number
7
+ dotColor?: string
8
+ }
9
+
10
+ const props = withDefaults(defineProps<{
11
+ options: Option[]
12
+ size?: 'sm' | 'md'
13
+ deselectable?: boolean
14
+ }>(), {
15
+ size: 'md',
16
+ deselectable: true,
17
+ })
18
+
19
+ const modelValue = defineModel<T | undefined | null>()
20
+
21
+ function select(option: Option) {
22
+ if (option.disabled) return
23
+ if (props.deselectable && modelValue.value === option.value) {
24
+ modelValue.value = undefined
25
+ return
26
+ }
27
+ modelValue.value = option.value
28
+ }
29
+ </script>
30
+
31
+ <template>
32
+ <div class="flex flex-wrap gap-2">
33
+ <button
34
+ v-for="option in options"
35
+ :key="String(option.value)"
36
+ type="button"
37
+ :disabled="option.disabled"
38
+ class="inline-flex items-center gap-1.5 rounded-full border transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-50"
39
+ :class="[
40
+ size === 'sm' ? 'px-3 py-1 text-xs' : 'px-3.5 py-1.5 text-sm',
41
+ modelValue === option.value
42
+ ? 'border-transparent bg-blue-700 font-medium text-white dark:bg-blue-500 dark:text-gray-900'
43
+ : 'border-gray-300 bg-transparent text-gray-700 hover:border-gray-400 dark:border-gray-600 dark:text-gray-300 dark:hover:border-gray-500',
44
+ ]"
45
+ @click="select(option)"
46
+ >
47
+ <span
48
+ v-if="option.dotColor"
49
+ class="size-2 shrink-0 rounded-full"
50
+ :style="{ backgroundColor: option.dotColor }"
51
+ />
52
+ <span>{{ option.label }}</span>
53
+ <span v-if="option.count != null" class="tabular-nums opacity-70">
54
+ {{ option.count }}
55
+ </span>
56
+ </button>
57
+ </div>
58
+ </template>