@appsbd/vue3-appsbd-ui 1.0.14 → 1.0.16

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.
@@ -0,0 +1,148 @@
1
+ # AbChatBox — AI Reference
2
+
3
+ A full chat conversation container built on top of `AbChatInput` — header, scrollable message list (with
4
+ empty/typing states), and the composer. Every part is slot-overridable, so pick this over building a chat UI by
5
+ hand whenever you need a message list around `AbChatInput`, not just the composer itself.
6
+
7
+ ## Installation
8
+
9
+ Not globally registered — import it explicitly:
10
+
11
+ ```js
12
+ import { AbChatBox } from 'vue3-appsbd-ui';
13
+ ```
14
+
15
+ No extra peer deps beyond `AbChatInput`'s own (`@lucide/vue`).
16
+
17
+ ## Usage
18
+
19
+ `v-model:messages` holds the conversation array, `v-model` holds the composer's text. Submitting auto-appends the
20
+ user's message and fires `send` — handle that to call your API and push the reply back:
21
+
22
+ ```vue
23
+ <template>
24
+ <AbChatBox
25
+ v-model="message"
26
+ v-model:messages="messages"
27
+ title="Support Bot"
28
+ subtitle="Usually replies in a few seconds"
29
+ :loading="loading"
30
+ @send="onSend"
31
+ />
32
+ </template>
33
+
34
+ <script setup>
35
+ import { ref } from 'vue';
36
+ import { AbChatBox } from 'vue3-appsbd-ui';
37
+
38
+ const message = ref('');
39
+ const messages = ref([
40
+ { id: 1, role: 'assistant', name: 'Assistant', text: 'Hi! How can I help you today?' },
41
+ ]);
42
+ const loading = ref(false);
43
+
44
+ function onSend({ message, files }) {
45
+ loading.value = true;
46
+ callMyApi(message).then((reply) => {
47
+ loading.value = false;
48
+ messages.value.push({ id: Date.now(), role: 'assistant', text: reply });
49
+ });
50
+ }
51
+ </script>
52
+ ```
53
+
54
+ Any `AbChatInput` prop/event/slot works directly on `AbChatBox` — it's forwarded straight to the inner composer:
55
+
56
+ ```vue
57
+ <template>
58
+ <AbChatBox
59
+ v-model="message"
60
+ v-model:messages="messages"
61
+ v-model:files="files"
62
+ :max-file-size="2"
63
+ :allowed-extensions="['jpg', 'jpeg', 'png', 'pdf']"
64
+ placeholder="Ask anything"
65
+ >
66
+ <template #single-input="{ addToAttach }">
67
+ <AbChatInputMenuItem :icon="Paperclip" name="Upload file" :action="() => addToAttach()" />
68
+ </template>
69
+ </AbChatBox>
70
+ </template>
71
+ ```
72
+
73
+ ## Props
74
+
75
+ | Name | Type | Default | Description |
76
+ | --- | --- | --- | --- |
77
+ | `messages` | `Array` | `[]` | v-model:messages. See "Message shape" below. |
78
+ | `modelValue` | `String` | `""` | Two-way-bound composer text — forwarded to the inner `AbChatInput`. |
79
+ | `title` | `String` | `""` | Default header title (translated). |
80
+ | `subtitle` | `String` | `""` | Default header subtitle (translated). |
81
+ | `avatar` | `String` | `""` | Default header avatar image URL. |
82
+ | `loading` | `Boolean` | `false` | Shows the typing indicator at the bottom of the message list. |
83
+ | `emptyText` | `String` | `"Start a conversation"` | Text shown by the default empty state (translated). |
84
+ | `emptyIcon` | `Object \| Function` | `MessageCircle` | Icon shown above `emptyText`. Pass `null` to hide it. |
85
+ | `autoScroll` | `Boolean` | `true` | Auto-scrolls the message list to the bottom on new messages / `loading` changes. |
86
+ | `containerClass` | `String` | `undefined` | Extra class on the root element. |
87
+
88
+ Any other prop not listed above (`placeholder`, `disabled`, `readonly`, `minHeight`, `maxHeight`, `submitOnEnter`,
89
+ `allowNewLine`, `autoFocus`, `autoPreview`, `maxFileSize`, `allowedExtensions`, `borderRadius`,
90
+ `filePreviewWidth`, `filePreviewHeight`, `showFileName`, `showVoiceButton`, ...) falls through untouched to the
91
+ inner `AbChatInput` — see `.ai/ai_ref_AbChatInput.md` for the full list.
92
+
93
+ ## Events
94
+
95
+ | Event | Payload | Description |
96
+ | --- | --- | --- |
97
+ | `update:messages` | `Message[]` | v-model:messages sync — fires after the user's message is auto-appended, or after `addMessage()`. |
98
+ | `update:modelValue` | `string` | v-model sync for the composer text. |
99
+ | `send` | `{ message: string, files: FileEntry[] }` | Fired right after the user's message is auto-appended to `messages` — hook here to call an API and push the reply. |
100
+
101
+ Any other `AbChatInput` event (`focus`, `blur`, `keydown`, `file-click`, `file-remove`, `file-reject`,
102
+ `voice-start`, `voice-stop`, `voice-cancel`, `voice-error`, `update:files`) is forwarded straight from the inner
103
+ composer — listen for it on `AbChatBox` exactly as you would on `AbChatInput`. Note: `submit` itself is **not**
104
+ forwarded — `AbChatBox` consumes it internally to auto-append + emit `send`.
105
+
106
+ ## Slots
107
+
108
+ | Name | Description |
109
+ | --- | --- |
110
+ | `header` | Overrides the entire header. Scoped: `{ title, subtitle, avatar }`. |
111
+ | `header-actions` | Adds content to the right side of the default header, without replacing it. |
112
+ | `empty` | Overrides the empty state (shown when `messages` is empty). |
113
+ | `message` | Overrides one message's whole bubble. Scoped: `{ message, index }`. |
114
+ | `message-avatar` | Overrides just the avatar inside the default bubble. Scoped: `{ message }`. |
115
+ | `typing` | Overrides the typing indicator (shown when `loading` is `true`). |
116
+ | `left` / `right` / `send-button` / `menu-trigger` / `single-input` / `multiple-input` | `AbChatInput`'s own slots — forwarded straight through to the inner composer with the same scoped props (`addToAttach`, `addFiles`, `removeFile`, `clearFiles`, `updateText`, `focus`, `submit`). See `.ai/ai_ref_AbChatInput.md`. |
117
+
118
+ ## v-model
119
+
120
+ Yes — two v-models: `modelValue` + `update:modelValue` (string, the composer text) and `messages` +
121
+ `update:messages` (`Message[]`, the conversation). A third, `files` + `update:files`, is available by passthrough
122
+ if you use `v-model:files` (forwarded to the inner `AbChatInput`).
123
+
124
+ ### Message shape
125
+
126
+ Any plain object works — only `role` is read internally (to align the bubble left/right); everything else is
127
+ just read by the default bubble template (or your own, via `#message`).
128
+
129
+ ```js
130
+ {
131
+ id, // stable key; auto-generated if omitted
132
+ role, // "user" | "assistant" | "system" — "user" aligns right, others left
133
+ text, // message body
134
+ name, // optional sender name, shown above the text
135
+ avatar, // optional avatar image URL
136
+ time, // optional string, shown below the text
137
+ files, // optional array — rendered as a small attachment list ({ name } or a plain string)
138
+ }
139
+ ```
140
+
141
+ ## Exposed methods
142
+
143
+ | Method | Description |
144
+ | --- | --- |
145
+ | `addMessage(message)` | Appends a message (fills in a default `id`/`role` if omitted), fires `update:messages`, and auto-scrolls. Returns the stored message. |
146
+ | `scrollToBottom()` | Scrolls the message list to the bottom. |
147
+ | `focus()` | Focuses the composer's textarea. |
148
+ | `chatInput` | Raw template ref to the inner `AbChatInput` — call any of its own exposed methods directly (`submit()`, `addToAttach()`, `startRecording()`, etc). See `.ai/ai_ref_AbChatInput.md`. |
@@ -14,21 +14,17 @@ No extra peer deps beyond the library's own (`@lucide/vue` for the default "+" i
14
14
 
15
15
  ## Usage
16
16
 
17
+ A send button (arrow-up icon, `AbButton` with `color="primary"`) renders on the right by default and calls the
18
+ same submit logic as pressing Enter — no manual wiring needed:
19
+
17
20
  ```vue
18
21
  <template>
19
- <AbChatInput v-model="message" placeholder="Ask anything" @submit="onSubmit">
20
- <template #right>
21
- <button type="button" class="btn btn-primary btn-sm rounded-circle">
22
- <ArrowUp :size="16" />
23
- </button>
24
- </template>
25
- </AbChatInput>
22
+ <AbChatInput v-model="message" placeholder="Ask anything" @submit="onSubmit" />
26
23
  </template>
27
24
 
28
25
  <script setup>
29
26
  import { ref } from 'vue';
30
27
  import { AbChatInput } from 'vue3-appsbd-ui';
31
- import { ArrowUp } from '@lucide/vue';
32
28
 
33
29
  const message = ref('');
34
30
  function onSubmit({ message, files }) {
@@ -37,6 +33,21 @@ function onSubmit({ message, files }) {
37
33
  </script>
38
34
  ```
39
35
 
36
+ Override the default send button via the scoped `#send-button` slot (e.g. to add a label, swap the icon, or
37
+ change its color) — call the scoped `submit()` to fire the same `submit` event the default button would:
38
+
39
+ ```vue
40
+ <template>
41
+ <AbChatInput v-model="message" @submit="onSubmit">
42
+ <template #send-button="{ submit }">
43
+ <AbButton color="success" is-icon-btn is-rounded size="sm" @click="submit">
44
+ <Send :size="18" />
45
+ </AbButton>
46
+ </template>
47
+ </AbChatInput>
48
+ </template>
49
+ ```
50
+
40
51
  Add/more dropdown (0 items = no trigger, 1 item = direct action with no popover, 2+ items = a real dropdown):
41
52
 
42
53
  ```vue
@@ -76,24 +87,88 @@ a native file picker and routes the selection through `addFiles`) among other me
76
87
  ## Voice input
77
88
 
78
89
  Uses the browser's native Web Speech API (`SpeechRecognition`) — no extra dependency, no API key. A built-in mic
79
- button renders in the `right` area (next to the `right` slot) whenever `showVoiceButton` is `true` (default).
80
- Clicking it (or calling `startRecording()`) swaps the whole composer row for a full-width recording bar — a live
81
- audio-reactive waveform (via `getUserMedia` + `AnalyserNode`) plus three controls:
90
+ button renders in the `right` area (before the `right` slot and the send button) whenever `showVoiceButton` is
91
+ `true` (default).
92
+
93
+ The composer layout never changes while recording, on every platform — no full-width recording bar swap: the
94
+ textarea stays visible **and stays fully editable** (typing/deleting mid-recording works normally) and fills with
95
+ the transcribed text live as it's recognized. The mic button shows a Google Assistant-style listening cue: its
96
+ circle fills with the theme's primary color (icon turns white to read against it), with a few rings pulsing
97
+ outward from behind it on a loop (`.apbd-chat-input__voice-ring`, staggered so they overlap continuously). Clicking
98
+ it (or calling `stopRecording()`) stops and keeps the text — there's no dedicated cancel button in this UI
99
+ (`cancelRecording()` is still available programmatically via a template ref), and the default send button (or
100
+ `submit()`/Enter generally) stops recording first when called mid-listening, then fires `submit` immediately.
82
101
 
83
- - **✕ cancel** — discards everything spoken this session, reverting `modelValue` to what it was before recording started.
84
- - **■ stop** — stops recording and keeps the transcribed text in the textarea (no submit).
85
- - **↑ submit** — stops recording, keeps the text, then fires `submit` immediately.
102
+ On every platform, the textarea is refocused automatically the moment listening ends (stop, cancel, or a
103
+ voice-triggered submit) — the cursor lands right back in it so the user can keep typing without an extra tap.
86
104
 
87
- Recognized speech is appended to the existing `modelValue` (not replaced), one final phrase at a time — interim
88
- (still-speaking) text is not shown live.
105
+ Recognized speech is appended to the existing `modelValue` (not replaced), converting instantly as the user speaks
106
+ on every platform — interim results drive `modelValue` live so the text is already there before the user taps the
107
+ mic to stop, not just once they do (see below).
89
108
 
90
109
  A small language-code badge (e.g. `EN`, `BN`) sits on the mic button's corner. Clicking it opens an `AbPopover`
91
110
  with a searchable list of ~130 BCP-47 locales; picking one sets that recognition session's `SpeechRecognition.lang`
92
111
  and persists the choice to `localStorage` (key `apbd-chat-input-voice-lang`) so it's remembered next visit. This
93
112
  never touches the browser's own language/locale settings — only this component's own recognizer instance.
94
113
 
95
- Not supported in Firefox/Safari — the mic button still renders there but is `disabled`, with a tooltip explaining
96
- why. Requires the browser's mic permission prompt to be accepted.
114
+ ### Voice punctuation
115
+
116
+ Two independent behaviors, each its own boolean prop, **both default `true`**:
117
+
118
+ - **`voicePunctuationCommands`** — saying a punctuation word inserts the actual mark, same idea as Google/Apple
119
+ dictation: English `"period"`/`"full stop"` → `.`, `"comma"` → `,`, `"question mark"` → `?`; Bengali
120
+ `"পূর্ণচ্ছেদ"` → `।`, `"কমা"` → `,`, `"প্রশ্নবোধক"` → `?`. Matched against the recognized transcript text itself
121
+ (not `voiceLang`), so it works regardless of which recognition language is selected. Matching uses
122
+ script-agnostic lookaround (not `\b`, which only understands ASCII letters — a no-op for Bengali) so a command
123
+ word only triggers standalone, never as a substring of a longer word (e.g. saying "দাঁড়িপাল্লা" doesn't trigger
124
+ on the "দাড়ি" inside it). `"দাড়ি"` itself (an earlier alternate trigger, same meaning as `"পূর্ণচ্ছেদ"`) was
125
+ dropped entirely rather than just boundary-guarded — it's also ordinary vocabulary (beard), so a sentence
126
+ actually about a beard would still lose that word every time. `"কমা"` keeps a similar, smaller risk (it's also
127
+ a common verb, "to decrease") — left as-is, since removing every word with *any* plausible double meaning
128
+ would leave nothing left to trigger on.
129
+ - **`voiceAutoPunctuate`** — when a new utterance is about to be appended onto existing text, a sentence-ending
130
+ mark is inserted first if the existing text doesn't already end with one — `.` normally, `।` when `voiceLang`
131
+ starts with `bn`. Applies between pauses (each per-utterance restart, see below), not mid-utterance.
132
+
133
+ Set either to `false` to disable it independently; both keep working with the default Web Speech transcript as-is
134
+ when off.
135
+
136
+ Both also auto-capitalize the sentence start they create: `voiceAutoPunctuate` capitalizes the first letter of a
137
+ new utterance right after it inserts a boundary mark (and always capitalizes the very first letter of the whole
138
+ message, regardless of this prop); `voicePunctuationCommands` capitalizes the letter right after any mark it
139
+ inserts mid-utterance (`"hello period how are you"` → `"Hello. How are you"`). Same script-agnostic approach as the
140
+ punctuation matching above — plain `toUpperCase()` on the matched letter, which is a no-op for scripts without a
141
+ case concept (Bengali, Arabic, CJK, …), so it needs no per-language list.
142
+
143
+ Not supported in Firefox. On iOS, only Safari itself gets it — Chrome/Firefox/Edge/Opera on iOS run on the same
144
+ underlying WebKit engine and expose the same `webkitSpeechRecognition` constructor, but iOS never grants the mic
145
+ permission for it outside of Safari's own app (no permission prompt ever appears; `.start()` would otherwise fail
146
+ with a confusing, prompt-less "permission denied"), so these are detected via user-agent and treated as unsupported
147
+ too. The mic button still renders in all of these cases but is `disabled`, with a tooltip explaining why. Where it
148
+ is supported, requires the browser's mic permission prompt to be accepted.
149
+
150
+ Some browsers/builds hold back `isFinal` results until `.stop()` is called instead of firing them per pause segment
151
+ — waiting for that would mean the transcript only shows up once the user taps stop, not while they're actually
152
+ speaking (and on some builds it never reliably fires at all). `interimResults` is on by default to sidestep that,
153
+ converting speech to text instantly instead of only once recording stops — **except on Safari** (desktop and iOS
154
+ both), where it's kept `false`: Safari's engine has a bug where enabling it can fail to (re)start the recognizer
155
+ entirely, surfacing as a generic "Microphone permission denied" error indistinguishable from an actually-denied
156
+ permission. Safari still converts per finished phrase (not mid-utterance), just not keystroke-by-keystroke live.
157
+
158
+ `continuous`+`interimResults` together is also unreliable across browsers/builds in a way delta-tracking
159
+ `e.results` (via `resultIndex` or position) can't fully work around — it's been observed restating/duplicating
160
+ earlier words in a later result with no restart involved at all. So `recognition.continuous` is always `false` —
161
+ each internal session covers exactly one utterance, and a transparent onend-restart (also covers Safari's own
162
+ quirk of ending a `continuous: true` session early) picks the next utterance back up automatically, so it still
163
+ reads as continuous listening. Within one utterance's `onresult`, only the latest `e.results` entry is read — no
164
+ cross-event bookkeeping needed, since there's nothing earlier in that same array to conflict with.
165
+
166
+ Because the textarea stays editable during recording, `onresult` can't just privately accumulate a running
167
+ transcript either — that would silently clobber whatever the user just typed or deleted. Instead it reads the
168
+ current `modelValue`, strips back off the exact suffix it itself inserted last time (if that suffix is still there
169
+ untouched), and rebuilds from that stable base with the freshly recognized text appended; if the suffix isn't
170
+ there anymore, whatever's currently in the field — including the user's edit — becomes the new base instead of
171
+ being overwritten.
97
172
 
98
173
  ## Props
99
174
 
@@ -119,6 +194,8 @@ why. Requires the browser's mic permission prompt to be accepted.
119
194
  | `filePreviewHeight` | `Number` | `24` | Height (px) of each attached file's thumbnail/icon in the chip. |
120
195
  | `showFileName` | `Boolean` | `true` | Show/hide the filename text next to the chip thumbnail. |
121
196
  | `showVoiceButton` | `Boolean` | `true` | Show/hide the built-in mic button (see "Voice input" below). Set `false` to build custom voice UI via slots instead. |
197
+ | `voicePunctuationCommands` | `Boolean` | `true` | Say a punctuation word (e.g. "period", "পূর্ণচ্ছেদ") to insert the actual mark — see "Voice punctuation" above. |
198
+ | `voiceAutoPunctuate` | `Boolean` | `true` | Auto-insert a sentence-ending mark between voice utterances when one isn't already there — see "Voice punctuation" above. |
122
199
 
123
200
  ## Events
124
201
 
@@ -133,8 +210,8 @@ why. Requires the browser's mic permission prompt to be accepted.
133
210
  | `file-remove` | `FileEntry` | A file chip's remove button was clicked. |
134
211
  | `file-reject` | `{ file: File, reason: string }` | A file failed `maxFileSize`/`allowedExtensions` validation. A `useToast` error is also shown. |
135
212
  | `voice-start` | — | Voice recording started (mic button clicked or `startRecording()` called). |
136
- | `voice-stop` | — | Recording stopped normally (■ button, ↑ submit button, `stopRecording()`, or the browser ending it on its own) — transcribed text is kept in `modelValue`. |
137
- | `voice-cancel` | — | Recording was cancelled (✕ button or `cancelRecording()`) — `modelValue` is reverted to what it was before recording started. |
213
+ | `voice-stop` | — | Recording stopped normally (mic button tapped again, `stopRecording()`, or the browser ending it on its own) — transcribed text is kept in `modelValue`. |
214
+ | `voice-cancel` | — | Recording was cancelled via `cancelRecording()` (no dedicated UI button — call it programmatically via a template ref) — `modelValue` is reverted to what it was before recording started. |
138
215
  | `voice-error` | `Error \| SpeechRecognitionErrorEvent` | Mic permission was denied, or the recognition engine errored. A `useToast` error is also shown (except benign `no-speech`/`aborted` errors). |
139
216
 
140
217
  ## Slots
@@ -142,12 +219,13 @@ why. Requires the browser's mic permission prompt to be accepted.
142
219
  | Name | Description |
143
220
  | --- | --- |
144
221
  | `left` | Left action area (e.g. an attachment button). Scoped — see below. |
145
- | `right` | Right action area (e.g. send/mic button). Scoped — see below. |
222
+ | `right` | Right action area, rendered before the send button (e.g. extra icon buttons). Scoped — see below. |
223
+ | `send-button` | Overrides the default send button (an `AbButton` with an arrow-up icon, `color="primary"`, calling `submit()`). Always rendered — there's no prop to hide it; override this slot with empty content if you don't want one. Scoped — see below. |
146
224
  | `menu-trigger` | Overrides the default "+" icon on the add/more trigger. Only rendered when the trigger itself renders (see below). Scoped — see below. |
147
225
  | `single-input` | One static `AbChatInputMenuItem` for the add/more dropdown. Scoped — see below. |
148
226
  | `multiple-input` | A `v-for`'d set of `AbChatInputMenuItem`s for the add/more dropdown. Scoped — see below. |
149
227
 
150
- **All five slots are scoped**, receiving the same props so slot content can drive the composer directly:
228
+ **All six slots are scoped**, receiving the same props so slot content can drive the composer directly:
151
229
 
152
230
  | Scoped prop | Description |
153
231
  | --- | --- |
@@ -157,6 +235,7 @@ why. Requires the browser's mic permission prompt to be accepted.
157
235
  | `clearFiles()` | Same as the exposed method. |
158
236
  | `updateText(text)` | Replaces the message text (fires `update:modelValue`). |
159
237
  | `focus()` | Focuses the textarea. |
238
+ | `submit()` | Same as the exposed method — fires `submit` with `{ message, files }`. No-op when `disabled`/`readonly`. |
160
239
 
161
240
  A single-item `#single-input`/`#multiple-input` action can use these directly — `onMenuTriggerClick`'s no-dropdown
162
241
  fast path reads the item's `action` prop with these same bindings applied, so `:action="addToAttach"` works there too.
@@ -202,6 +281,7 @@ any slot — e.g. a toolbar button elsewhere on the page.
202
281
  | `addToAttach(options?)` | Opens a native file picker and routes the selection through `addFiles`. `options`: `{ accept, multiple }`. |
203
282
  | `updateText(text)` | Replaces the message text (fires `update:modelValue`). |
204
283
  | `focus()` | Focuses the textarea. |
284
+ | `submit()` | Fires `submit` with `{ message, files }` (same as clicking the default send button). No-op when `disabled`/`readonly`. |
205
285
  | `startRecording()` | Starts voice input (same as clicking the mic button). No-op if unsupported/disabled/readonly/already recording. |
206
286
  | `stopRecording()` | Stops recording, keeping the transcribed text (same as the ■ button). |
207
287
  | `cancelRecording()` | Stops recording and discards the transcribed text (same as the ✕ button). |
@@ -46,7 +46,7 @@ const datetime = ref(null);
46
46
  | `monthNames` | `Array` | `undefined` | Custom full month names. |
47
47
  | `monthNamesShort` | `Array` | `undefined` | Custom short month names. |
48
48
  | `dayNames` | `Array` | `undefined` | Custom day names. |
49
- | `formatDigit` | `Function` | `undefined` | Locale digit formatter (e.g. Bengali digits). |
49
+ | `digits` | `Array` | `undefined` | 10-character digits array (0-9) for locale digit transliteration (e.g. Bangla digits), overriding `globalConfig.digits`. |
50
50
  | `rangeSeparator` | `String` | `undefined` | Display separator between range start/end. |
51
51
  | `mode` | `String` | `"date"` | `date`, `time`, or `datetime`. |
52
52
  | `is24Hour` | `Boolean` | `undefined` | 24-hour clock. |
@@ -36,3 +36,30 @@ Each event payload is an object with `scrollTop`, `scrollHeight`, `scrollRatio`,
36
36
  | Name | Description |
37
37
  | --- | --- |
38
38
  | `default` | Scrollable content. |
39
+
40
+ ## Exposed methods
41
+
42
+ Available via a template ref, for programmatic scroll control (e.g. scrolling a chat/message list to the bottom on new content):
43
+
44
+ | Method | Description |
45
+ | --- | --- |
46
+ | `scrollToBottom(behavior?)` | Scrolls to the bottom. `behavior`: `"auto"` (default) or `"smooth"`. |
47
+ | `scrollToTop(behavior?)` | Scrolls to the top. `behavior`: `"auto"` (default) or `"smooth"`. |
48
+ | `scrollTo(options)` | Same as the native `Element.scrollTo(options)` (e.g. `{ top, left, behavior }`). |
49
+ | `scrollArea` | Raw ref to the internal scrollable element, for anything not covered above. |
50
+
51
+ ```vue
52
+ <template>
53
+ <AbScrollbar ref="listRef" style="height: 300px">
54
+ <div v-for="m in messages" :key="m.id">{{ m.text }}</div>
55
+ </AbScrollbar>
56
+ </template>
57
+
58
+ <script setup>
59
+ import { ref, nextTick } from 'vue';
60
+ const listRef = ref(null);
61
+ function onNewMessage() {
62
+ nextTick(() => listRef.value.scrollToBottom());
63
+ }
64
+ </script>
65
+ ```
@@ -0,0 +1,112 @@
1
+ # AbVoiceButton — AI Reference
2
+
3
+ A standalone voice-to-text mic button — the same recognition engine and behavior as `AbChatInput`'s built-in mic,
4
+ with no composer attached. Use it to bolt voice input onto any existing input, form field, search box, or custom
5
+ UI via `v-model`; pick `AbChatInput` instead when you need the whole chat composer (textarea, file attachments,
6
+ add/more dropdown) with voice input built in.
7
+
8
+ ## Installation
9
+
10
+ Not globally registered — import it explicitly:
11
+
12
+ ```js
13
+ import { AbVoiceButton } from 'vue3-appsbd-ui';
14
+ ```
15
+
16
+ No extra peer deps beyond the library's own (`@lucide/vue` for the mic icon). Uses the browser's native Web
17
+ Speech API (`SpeechRecognition`) — no dependency, no API key.
18
+
19
+ ## Usage
20
+
21
+ ```vue
22
+ <template>
23
+ <AbVoiceButton v-model="message" />
24
+ <p>{{ message }}</p>
25
+ </template>
26
+
27
+ <script setup>
28
+ import { ref } from 'vue';
29
+ import { AbVoiceButton } from 'vue3-appsbd-ui';
30
+
31
+ const message = ref('');
32
+ </script>
33
+ ```
34
+
35
+ Pin the recognition language explicitly (there's no built-in language picker here, unlike `AbChatInput`'s mic
36
+ button — pass whichever locale your app already knows the user wants):
37
+
38
+ ```vue
39
+ <AbVoiceButton v-model="message" lang="bn-BD" />
40
+ ```
41
+
42
+ ## Behavior
43
+
44
+ Tapping the button (or calling `startRecording()`) starts listening; the mic icon stays the mic icon, but its
45
+ circle fills with the theme's primary color (`--#{prefix}theme`) and the icon turns white, with a few rings
46
+ pulsing outward from behind it on a loop — a Google Assistant-style listening cue, on every platform (same cue,
47
+ same architecture as `AbChatInput`'s built-in mic — this component just has no composer/textarea attached to it).
48
+ Tap it again (or call `stopRecording()`) to stop and keep the text.
49
+
50
+ Recognized speech merges into `modelValue` live as the user speaks, via one internal recognition session per
51
+ utterance (`continuous: false` + `interimResults: true`, restarted transparently between utterances) — on every
52
+ platform except Safari (desktop and iOS both), where `interimResults` is kept `false`: Safari's engine has a bug
53
+ where enabling it can fail to (re)start the recognizer entirely, surfacing as a generic "Microphone permission
54
+ denied" error indistinguishable from an actually-denied permission. Safari still converts per finished phrase, just
55
+ not keystroke-by-keystroke live. Reads the current `modelValue` on each update and replaces only the exact suffix
56
+ it last inserted itself, so external edits to `modelValue` between voice updates (e.g. the consumer's own code, or
57
+ a bound `<textarea>` the user is also typing into) are preserved rather than clobbered.
58
+
59
+ Not supported in Firefox. On iOS, only Safari itself gets it — Chrome/Firefox/Edge/Opera on iOS run on the same
60
+ underlying WebKit engine and expose the same `webkitSpeechRecognition` constructor, but iOS never grants the mic
61
+ permission for it outside of Safari's own app, so these are detected via user-agent and treated as unsupported too
62
+ (button `disabled`, tooltip explains why). Requires the browser's mic permission prompt to be accepted where
63
+ supported.
64
+
65
+ ### Voice punctuation
66
+
67
+ Two independent behaviors, each its own boolean prop, **both default `true`** (identical to `AbChatInput`'s):
68
+
69
+ - **`voicePunctuationCommands`** — saying a punctuation word inserts the actual mark: English `"period"`/
70
+ `"full stop"` → `.`, `"comma"` → `,`, `"question mark"` → `?`; Bengali `"পূর্ণচ্ছেদ"` → `।`, `"কমা"` → `,`,
71
+ `"প্রশ্নবোধক"` → `?`. Matched against the transcript text itself, independent of `lang`. Matching uses
72
+ script-agnostic Unicode lookaround (not `\b`, a no-op for Bengali script), so a command word only triggers
73
+ standalone, never as a substring of a longer word.
74
+ - **`voiceAutoPunctuate`** — a sentence-ending mark is inserted between voice utterances when the existing text
75
+ doesn't already end with one — `.` normally, `।` when `lang` (or the auto-detected language) starts with `bn`.
76
+
77
+ ## Props
78
+
79
+ | Name | Type | Default | Description |
80
+ | --- | --- | --- | --- |
81
+ | `modelValue` | `String` | `""` | Two-way-bound transcribed text. |
82
+ | `disabled` | `Boolean` | `false` | Disables the button. |
83
+ | `size` | `Number` | `40` | Button diameter in px. Icon and listening rings scale with it. |
84
+ | `lang` | `String` | `""` | BCP-47 recognition locale (e.g. `"bn-BD"`). Empty = auto-detect from `document.documentElement.lang` / `navigator.language`. |
85
+ | `voicePunctuationCommands` | `Boolean` | `true` | Say a punctuation word (e.g. "period", "পূর্ণচ্ছেদ") to insert the actual mark — see "Voice punctuation" above. |
86
+ | `voiceAutoPunctuate` | `Boolean` | `true` | Auto-insert a sentence-ending mark between voice utterances when one isn't already there — see "Voice punctuation" above. |
87
+
88
+ ## Events
89
+
90
+ | Event | Payload | Description |
91
+ | --- | --- | --- |
92
+ | `update:modelValue` | `string` | v-model sync. |
93
+ | `voice-start` | — | Voice recording started (mic tapped, or `startRecording()` called). |
94
+ | `voice-stop` | — | Recording stopped normally (mic tapped again, `stopRecording()`, or the browser ending it on its own) — transcribed text is kept in `modelValue`. |
95
+ | `voice-cancel` | — | Recording was cancelled via `cancelRecording()` — `modelValue` is reverted to what it was before recording started. |
96
+ | `voice-error` | `Error \| SpeechRecognitionErrorEvent` | Mic permission was denied, or the recognition engine errored. A `useToast` error is also shown (except benign `no-speech`/`aborted` errors). |
97
+
98
+ ## Slots
99
+
100
+ None.
101
+
102
+ ## v-model
103
+
104
+ Yes — `modelValue` + `update:modelValue` (string).
105
+
106
+ ## Exposed methods
107
+
108
+ | Method | Description |
109
+ | --- | --- |
110
+ | `startRecording()` | Starts voice input (same as tapping the button). No-op if unsupported/disabled/already recording. |
111
+ | `stopRecording()` | Stops recording, keeping the transcribed text (same as tapping the button again). |
112
+ | `cancelRecording()` | Stops recording and reverts `modelValue` to what it was before recording started. No dedicated UI for this — call it via a template ref. |
@@ -7,13 +7,11 @@ Call `AppsbdUIConfigure` once during app initialization to set library-wide loca
7
7
  ```javascript
8
8
  import { AppsbdUIConfigure } from "@vue3-appsbd-ui";
9
9
 
10
- const banglaDigits = ["০", "১", "২", "৩", "৪", "৫", "৬", "৭", "৮", "৯"];
11
-
12
10
  AppsbdUIConfigure({
13
11
  monthNames: ["জানুয়ারি", "ফেব্রুয়ারি", "মার্চ", "এপ্রিল", "মে", "জুন", "জুলাই", "আগস্ট", "সেপ্টেম্বর", "অক্টোবর", "নভেম্বর", "ডিসেম্বর"],
14
12
  monthNamesShort: ["জানু", "ফেব্রু", "মার্চ", "এপ্রি", "মে", "জুন", "জুলাই", "আগস্ট", "সেপ্টে", "অক্টো", "নভে", "ডিসে"],
15
13
  dayNames: ["শনি", "রবি", "সোম", "মঙ্গল", "বুধ", "বৃহঃ", "শুক্র"],
16
- formatDigit: (val) => String(val).replace(/\d/g, (d) => banglaDigits[d]),
14
+ digits: ["০", "১", "২", "৩", "৪", "৫", "৬", "৭", "৮", "৯"],
17
15
  is24Hour: false,
18
16
  rangeSeparator: " থেকে ",
19
17
  dateDataFormat: "YYYY-MM-DD",
@@ -32,7 +30,7 @@ AppsbdUIConfigure({
32
30
  | `monthNames` | `String[]` | English full names | Array of 12 full month names. |
33
31
  | `monthNamesShort` | `String[]` | English short names | Array of 12 short month names. |
34
32
  | `dayNames` | `String[]` | English 3-letter names | Array of 7 short day names (Sat-first). |
35
- | `formatDigit` | `Function` | `(val) => String(val)` | Digit formatter applied to `AbNumberField` (display-only) and `AbDateTimePicker`. |
33
+ | `digits` | `String[]` | `undefined` | Array of 10 digit characters (0-9) for locale digit transliteration (e.g. Bangla digits), applied by `AbDateTimePicker` and `formatCurrency`/`formatAmount`. |
36
34
  | `rangeSeparator` | `String` | `' ↔ '` | Separator for date ranges in `AbDateTimePicker`. |
37
35
  | `is24Hour` | `Boolean` | `false` | Whether to use 24-hour format in `AbDateTimePicker`. |
38
36
  | `dateDataFormat` | `String` | `'YYYY-MM-DD'` | Global format for date data. |
@@ -56,3 +54,40 @@ AppsbdUIConfigure({
56
54
  | `button.iconPosition` | `String` | `'left'` | Global default icon position for buttons (`left`, `right`). |
57
55
  | `isModalBorder` | `Boolean` | `true` | Global default modal border. |
58
56
  | `modalLoader` | `String` | `'AbLoader'` | Global default modal loader component. |
57
+ | `currency` | `String` | `'$'` | Global default currency symbol, used by `formatCurrency`/`formatAmount`. |
58
+ | `currencyPosition` | `String` | `'left'` | Global default currency symbol position: `'left'`, `'right'`, `'left_space'`, `'right_space'`. |
59
+ | `thousandSeparator` | `String` | `','` | Global default thousand separator, used by `formatCurrency`/`formatAmount`. |
60
+ | `decimalSeparator` | `String` | `'.'` | Global default decimal separator, used by `formatCurrency`/`formatAmount`. |
61
+ | `numberOfDecimals` | `Number` | `2` | Global default number of decimal digits, used by `formatCurrency`/`formatAmount`. |
62
+ | `round` | `Boolean` | `false` | Global default: round to `numberOfDecimals` instead of truncating, used by `formatCurrency`/`formatAmount`. |
63
+
64
+ ## `formatCurrency` / `formatAmount` utils
65
+
66
+ ```javascript
67
+ import { formatCurrency, formatAmount } from "@vue3-appsbd-ui";
68
+
69
+ formatCurrency(1234.5);
70
+ // -> "$1,234.50" (uses globalConfig.currency/currencyPosition/thousandSeparator/decimalSeparator/numberOfDecimals)
71
+
72
+ formatCurrency(1234.5, { currency: "৳", currencyPosition: "right_space", numberOfDecimals: 0 });
73
+ // -> "1,234 ৳"
74
+
75
+ formatAmount(1234.5); // plain number, no currency symbol -> "1,234.50"
76
+
77
+ // round (default false) — extra decimals are truncated, not rounded up:
78
+ formatCurrency(1234.499); // -> "$1,234.49" (truncated)
79
+ formatCurrency(1234.499, { round: true }); // -> "$1,234.50" (rounded)
80
+ ```
81
+
82
+ Any option not passed falls back to `globalConfig`, then to the hardcoded default (prop > global config > built-in default, same resolution order as other components). `digits` (see above, or `options.digits` per-call) is also applied to the formatted output via the internal `formatDigits` util. By default `round` is `false`: `numberOfDecimals` truncates extra digits rather than rounding (`1234.499` at 2 decimals -> `1234.49`, never `1234.50`); pass `round: true` (or `globalConfig.round = true`) for standard rounding.
83
+
84
+ ## `formatDigits` util
85
+
86
+ ```javascript
87
+ import { formatDigits } from "@vue3-appsbd-ui";
88
+
89
+ formatDigits(123); // no globalConfig.digits set -> "123"
90
+ formatDigits(123, ["০", "১", "২", "৩", "৪", "৫", "৬", "৭", "৮", "৯"]); // -> "১২৩"
91
+ ```
92
+
93
+ Used internally by `AbDateTimePicker` (`digits` prop) and `formatCurrency`/`formatAmount` (`options.digits`) to transliterate digits; falls back to `globalConfig.digits` when no array is passed, and returns the value unchanged (stringified) when neither is set.