@appsbd/vue3-appsbd-ui 1.0.15 → 1.0.17

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.
@@ -89,21 +89,57 @@ a native file picker and routes the selection through `addFiles`) among other me
89
89
  Uses the browser's native Web Speech API (`SpeechRecognition`) — no extra dependency, no API key. A built-in mic
90
90
  button renders in the `right` area (before the `right` slot and the send button) whenever `showVoiceButton` is
91
91
  `true` (default).
92
- Clicking it (or calling `startRecording()`) swaps the whole composer row for a full-width recording bar — a live
93
- audio-reactive waveform (via `getUserMedia` + `AnalyserNode`) plus three controls:
94
92
 
95
- - **✕ cancel** — discards everything spoken this session, reverting `modelValue` to what it was before recording started.
96
- - **■ stop** — stops recording and keeps the transcribed text in the textarea (no submit).
97
- - **↑ submit** — stops recording, keeps the text, then fires `submit` immediately.
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.
98
101
 
99
- Recognized speech is appended to the existing `modelValue` (not replaced), one final phrase at a time — interim
100
- (still-speaking) text is not shown live.
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.
104
+
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).
101
108
 
102
109
  A small language-code badge (e.g. `EN`, `BN`) sits on the mic button's corner. Clicking it opens an `AbPopover`
103
110
  with a searchable list of ~130 BCP-47 locales; picking one sets that recognition session's `SpeechRecognition.lang`
104
111
  and persists the choice to `localStorage` (key `apbd-chat-input-voice-lang`) so it's remembered next visit. This
105
112
  never touches the browser's own language/locale settings — only this component's own recognizer instance.
106
113
 
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
+
107
143
  Not supported in Firefox. On iOS, only Safari itself gets it — Chrome/Firefox/Edge/Opera on iOS run on the same
108
144
  underlying WebKit engine and expose the same `webkitSpeechRecognition` constructor, but iOS never grants the mic
109
145
  permission for it outside of Safari's own app (no permission prompt ever appears; `.start()` would otherwise fail
@@ -111,10 +147,28 @@ with a confusing, prompt-less "permission denied"), so these are detected via us
111
147
  too. The mic button still renders in all of these cases but is `disabled`, with a tooltip explaining why. Where it
112
148
  is supported, requires the browser's mic permission prompt to be accepted.
113
149
 
114
- On Android, the live waveform is skipped (bars stay flat) — recognition and transcription work normally. Android's
115
- mic hardware only allows one exclusive audio-capture session at a time; a second concurrent `getUserMedia` stream
116
- for the waveform would starve `SpeechRecognition` of audio (no error, no transcript, ever). This is detected via
117
- user-agent, not feature-tested.
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.
118
172
 
119
173
  ## Props
120
174
 
@@ -140,6 +194,8 @@ user-agent, not feature-tested.
140
194
  | `filePreviewHeight` | `Number` | `24` | Height (px) of each attached file's thumbnail/icon in the chip. |
141
195
  | `showFileName` | `Boolean` | `true` | Show/hide the filename text next to the chip thumbnail. |
142
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. |
143
199
 
144
200
  ## Events
145
201
 
@@ -154,8 +210,8 @@ user-agent, not feature-tested.
154
210
  | `file-remove` | `FileEntry` | A file chip's remove button was clicked. |
155
211
  | `file-reject` | `{ file: File, reason: string }` | A file failed `maxFileSize`/`allowedExtensions` validation. A `useToast` error is also shown. |
156
212
  | `voice-start` | — | Voice recording started (mic button clicked or `startRecording()` called). |
157
- | `voice-stop` | — | Recording stopped normally (■ button, ↑ submit button, `stopRecording()`, or the browser ending it on its own) — transcribed text is kept in `modelValue`. |
158
- | `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. |
159
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). |
160
216
 
161
217
  ## Slots
@@ -33,6 +33,9 @@ const datetime = ref(null);
33
33
  | `isRange` | `Boolean` | `false` | Enable date-range mode. |
34
34
  | `size` | `String` | `"md"` | Adjusts component size (`xs`, `sm`, `md`, `lg`, `xl`, `xxl`). |
35
35
  | `containerClass` | `String` | `"mb-3"` | Wrapper class. |
36
+ | `width` | `String \| Number` | `undefined` | Explicit width on the `.apbd-date-time-container` wrapper. A number is treated as px (e.g. `220` -> `220px`); a string is used as-is (e.g. `"50%"`). |
37
+ | `minWidth` | `String \| Number` | `undefined` | `min-width` on the wrapper (same number/string rules as `width`). |
38
+ | `maxWidth` | `String \| Number` | `undefined` | `max-width` on the wrapper (same number/string rules as `width`) — caps growth inside a flex/grid parent without a fixed `width`. |
36
39
  | `hints` | `String` | `""` | Helper text. |
37
40
  | `isDisable` | `Boolean` | `false` | Disable the picker. |
38
41
  | `visibility` | `String` | `"click"` | v-calendar popover trigger. |
@@ -4,6 +4,12 @@ Advanced filter panel for list and table views. Supports quick/advance modes, si
4
4
 
5
5
  `isMultiple` is functional in `isQuickAdvance` mode for `dd` fields: selections toggle on/off and render as removable tag chips on the popover trigger (no `AbMultiSelect` involved). The popover's option list is searchable — locally against `options`, or via `isAjaxSearch` + `onSearch` for server-side search.
6
6
 
7
+ `isBuilderFilter` is a Shopify-style filter builder: no field is visible until the user clicks the `+` button. That opens a popover which first lists every not-yet-applied `filterOptions` entry by `name`; picking one swaps the same popover to a value editor matching that field's `type` (`dd`→`AbMultiSelect`, `t`→`AbInputField`, `tr`→min/max inputs, `d`/`dr`→`AbDateTimePicker`) plus Cancel/Apply Filter buttons. Apply pushes it in as a removable chip (e.g. "Email contains gmail") and immediately fires `searchFilter` — there's no global Search button in this mode, only Reset (clears every applied chip and value at once). A field's `operators` is fixed by its schema entry, same as other modes — this mode does not add an is/is-not/contains operator picker, only the value input changes per `type`.
8
+
9
+ Standard-search, advance-search, and quick-advance field layouts are all plain flex (`d-flex flex-wrap gap-2`) — no Bootstrap grid. Each field grows/shrinks on its own, which means a field left alone on the last row will stretch to fill the remaining row width; give a `filterOptions` entry a `width`, `minWidth`, and/or `maxWidth` key (`String | Number` — a number is px, e.g. `maxWidth: 280`, or a CSS string like `"50%"`) to control that specific field's size. All three are forwarded straight to the underlying `AbInputField`/`AbMultiSelect`/`AbDateTimePicker`'s own same-named props, and also constrain that field's flex-item wrapper directly (`width` sets `flex: 0 0 <width>` so it stops growing/shrinking; `minWidth`/`maxWidth` clamp it while it keeps growing/shrinking with its siblings).
10
+
11
+ The standard-mode property-selector (and the scan field it swaps for) isn't part of `filterOptions` — it's not tied to one field, it lists all of them — so it has no `width`/`minWidth`/`maxWidth` key to set. It's also the only item in its row until a property is picked (or whenever `showScanFld` is on), so without sizing it it'll stretch to fill the row the same way an unsized `filterOptions` field would. Use the panel-level `propertySelectWidth`/`propertySelectMinWidth`/`propertySelectMaxWidth` props for that slot instead.
12
+
7
13
  ## Usage
8
14
 
9
15
  ```vue
@@ -37,6 +43,7 @@ function handleSearch(data) {
37
43
  | -------------------------- | --------- | --------------- | ---------------------------------------------------------- |
38
44
  | `isAdvance` | `Boolean` | `false` | Show all filters in an expanded layout. |
39
45
  | `isQuickAdvance` | `Boolean` | `false` | Quick advance mode. |
46
+ | `isBuilderFilter` | `Boolean` | `false` | Shopify-style "+" add-filter builder — see mode description above. |
40
47
  | `isSingle` | `Boolean` | `false` | Single-field search mode. |
41
48
  | `isAutoSearch` | `Boolean` | `false` | Auto search on input change (single mode). |
42
49
  | `isAllowed` | `Boolean` | `false` | Permission flag consumed by the template. |
@@ -49,7 +56,10 @@ function handleSearch(data) {
49
56
  | `isSearchable` | `Boolean` | `false` | Allow searching within option lists. |
50
57
  | `isMaxDate` | `Boolean` | `true` | Limit date pickers to today as max. |
51
58
  | `size` | `String` | `"xs"` | Control size. |
52
- | `advanceContainerClass` | `String` | `"col-6"` | Grid column class for inputs in advance mode. |
59
+ | `advanceContainerClass` | `String` | `""` | Extra class on each field's flex-item wrapper in advance mode (base sizing is `flex: 1 1 240px`, wraps as needed — not a Bootstrap grid). To fix a single field's width instead, use that field's own `width` prop (`AbInputField`/`AbMultiSelect`/`AbDateTimePicker`). |
60
+ | `propertySelectWidth` | `String` \| `Number` | `undefined` | Width of the standard-mode property-selector / scan-field slot (number = px). |
61
+ | `propertySelectMinWidth` | `String` \| `Number` | `undefined` | `min-width` of that same slot. |
62
+ | `propertySelectMaxWidth` | `String` \| `Number` | `undefined` | `max-width` of that same slot — it's the only item in its row until a property is picked (or in scan mode), so it stretches to fill it without one. |
53
63
  | `isQuickAdvanceAutoSearch` | `Boolean` | `false` | Fire search automatically on field change (quick advance). |
54
64
  | `defaultFilterId` | `String` \| `Number` | `""` | Pre-selects a default filter property in standard search mode (when `!isSingle && !isAdvance && !isQuickAdvance`). |
55
65
 
@@ -37,6 +37,9 @@ const form = reactive({ email: '' });
37
37
  | `size` | `String` | `undefined` | Adjusts component size (`xs`, `sm`, `md`, `lg`, `xl`, `xxl`). |
38
38
  | `rules` | `String \| Object` | `""` | VeeValidate rules. |
39
39
  | `containerClass` | `String` | `undefined` | Class on the wrapper `<div>`. |
40
+ | `width` | `String \| Number` | `undefined` | Explicit width on the wrapper `<div>`. A number is treated as px (e.g. `220` -> `220px`); a string is used as-is (e.g. `"50%"`). |
41
+ | `minWidth` | `String \| Number` | `undefined` | `min-width` on the wrapper `<div>` (same number/string rules as `width`). |
42
+ | `maxWidth` | `String \| Number` | `undefined` | `max-width` on the wrapper `<div>` (same number/string rules as `width`) — caps growth inside a flex/grid parent without a fixed `width`. |
40
43
  | `hints` | `String` | `""` | Helper text beneath the input. |
41
44
  | `prefix` | `String` | `""` | Prefix text/icon (shortcut for `#prefix` slot). |
42
45
  | `postfix` | `String` | `""` | Postfix text/icon. |
@@ -27,6 +27,9 @@ Searchable single/multi/tag select. Custom built-in replacement for `@vueform/mu
27
27
  | `name` | `String` | `""` | VeeValidate field name. |
28
28
  | `rules` | `String \| Object` | `""` | VeeValidate rules. |
29
29
  | `containerClass` | `String` | `undefined` | Wrapper class. |
30
+ | `width` | `String \| Number` | `undefined` | Explicit width on the wrapper `<div>`. A number is treated as px (e.g. `220` -> `220px`); a string is used as-is (e.g. `"50%"`). |
31
+ | `minWidth` | `String \| Number` | `undefined` | `min-width` on the wrapper `<div>` (same number/string rules as `width`). |
32
+ | `maxWidth` | `String \| Number` | `undefined` | `max-width` on the wrapper `<div>` (same number/string rules as `width`) — caps growth inside a flex/grid parent without a fixed `width`. |
30
33
  | `hints` | `String` | `""` | Helper text. |
31
34
  | `multiple` | `Boolean` | `false` | Deprecated — use `mode` instead. |
32
35
  | `searchable` | `Boolean` | `false` | Enable option search/filter. |
@@ -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. |
@@ -46,3 +46,13 @@ translateGetMsg("Error code: %{code}", { code: "404" });
46
46
  | `initTranslate(gettextInstance)` | `Function` | Registers an existing gettext instance for use outside Vue components. |
47
47
  | `translateGettext(msg, params)` | `Function` | Translates `msg` and interpolates `params`; also translates param values. |
48
48
  | `translateGetMsg(msg, params)` | `Function` | Interpolates `params` into `msg` without translating. |
49
+
50
+ ## Message-key convention
51
+
52
+ `msg` doesn't have to be a literal English sentence — it can be any string used as the `translations` lookup key (`msgid`), including a short dotted key like `"field.required"`. This is the pattern the library's own [`abVeeRules`](./ai_ref_abVeeRules.md) uses internally, and is the recommended approach for any reusable code (composables, utils) that needs translated messages: pick stable keys once, then supply the actual text per language in `translations`. A key with no matching entry for the active language is returned as-is (untranslated), so an unmapped key fails visibly instead of silently.
53
+
54
+ ```javascript
55
+ translateGettext("field.required", { fld_name: "Email" });
56
+ // → looks up translations[currentLang]["field.required"], interpolates %{fld_name}
57
+ // → falls back to the literal string "field.required" if that key isn't defined
58
+ ```
@@ -19,15 +19,57 @@ Object.keys(abAllRules).forEach(rule => {
19
19
 
20
20
  Validation functions conforming to VeeValidate's custom rule signature `(value, params, field)`.
21
21
 
22
- Included rules:
23
- - `required`: Returns translated `%{fld_name} is required`.
24
- - `numeric`: Returns translated `%{fld_name} should be numeric`.
25
- - `email`: Returns translated `%{fld_name} not a valid email address`.
26
- - `min`: Native min validation.
27
- - `min_value`: Returns translated `%{fld_name} should be more than X`.
28
- - `max`: Native max validation.
29
- - `max_value`: Returns translated `%{fld_name} should be less than X`.
30
- - `confirmed`: Checks password confirmation match.
31
- - `url`: Validates URL format.
32
- - `isUnique`: Custom stub for async uniqueness check.
33
- - `isValid`: Custom stub for async validity check.
22
+ On failure, each rule calls `translateGettext(key, params)` with a short, stable **message key** (not a literal English sentence) — the key is the `msgid` that must exist in the consuming app's `translations` object (see `abTranslate`) for that key to render translated; otherwise the raw key string is returned as-is (untranslated).
23
+
24
+ Included rules and their message key:
25
+
26
+ | Rule | Message key | Interpolation params |
27
+ | --- | --- | --- |
28
+ | `required` | `field.required` | `fld_name` |
29
+ | `numeric` | `field.numeric` | `fld_name` |
30
+ | `email` | `field.valid.email` | `fld_name` |
31
+ | `min` | — (native `min` rule, no translated message) | — |
32
+ | `min_value` | `field.min.value` | `fld_name`, `value` (the configured min) |
33
+ | `max` | — (native `max` rule, no translated message) | — |
34
+ | `max_value` | `field.max.value` | `fld_name`, `value` (the configured max) |
35
+ | `confirmed` | `field.confirm.password` | `fld_name` |
36
+ | `url` | `field.valid.url` | `fld_name` |
37
+ | `isUnique` | Custom stub for async uniqueness check (currently always passes). |
38
+ | `isValid` | Custom stub for async validity check; on the `'custom'` param path with too-short input, uses `field.valid.length` (`fld_name`). |
39
+
40
+ ## Adding/overriding these messages in a consuming app
41
+
42
+ These keys are **not translated out of the box** — they resolve to the bare key unless the app's `abCreateGettext({ translations })` config defines them for the active language. Add one entry per key, per language, with the interpolation params above as `%{param}` placeholders:
43
+
44
+ ```javascript
45
+ import { abCreateGettext } from "@vue3-appsbd-ui";
46
+
47
+ const gettext = abCreateGettext({
48
+ availableLanguages: { bn_BD: "Bengali Bangladesh", en_EN: "English" },
49
+ defaultLanguage: "bn_BD",
50
+ translations: {
51
+ bn_BD: {
52
+ "field.required": "%{fld_name} প্রয়োজনীয়",
53
+ "field.valid.email": "%{fld_name} সঠিক ইমেইল নয়",
54
+ "field.numeric": "%{fld_name} সংখ্যা হতে হবে",
55
+ "field.min.value": "%{fld_name} অবশ্যই %{value} এর বেশি হতে হবে",
56
+ "field.max.value": "%{fld_name} অবশ্যই %{value} এর কম হতে হবে",
57
+ "field.confirm.password": "%{fld_name} পাসওয়ার্ড এর সাথে মিলছে না",
58
+ "field.valid.url": "%{fld_name} সঠিক ইউআরএল নয়",
59
+ "field.valid.length": "%{fld_name} এর দৈর্ঘ্য সঠিক নয়, দয়া করে এটি পরীক্ষা করুন",
60
+ },
61
+ en_EN: {
62
+ "field.required": "%{fld_name} required",
63
+ "field.valid.email": "%{fld_name} is not a valid email",
64
+ "field.numeric": "%{fld_name} must be a number",
65
+ "field.min.value": "%{fld_name} must be greater than %{value}",
66
+ "field.max.value": "%{fld_name} must be less than %{value}",
67
+ "field.confirm.password": "%{fld_name} password does not match",
68
+ "field.valid.url": "%{fld_name} is not a valid url",
69
+ "field.valid.length": "%{fld_name} length is not valid, please check it",
70
+ },
71
+ },
72
+ });
73
+ ```
74
+
75
+ Only override the keys actually needed — an omitted key just falls back to being displayed as the bare key string.
@@ -12,6 +12,29 @@
12
12
 
13
13
  ---
14
14
 
15
+ ## [Unreleased]
16
+
17
+ ### ⚠️ Action Required — Breaking
18
+
19
+ - **`AbFilterPanel`'s standard-search and advance-search field layouts switched from a Bootstrap `.row`/`.col-*` grid to plain flex; `advanceContainerClass` default changed from `"col-6"` to `""`.**
20
+ - Find: `<AbFilterPanel ...>` usages in either default (standard) mode or `is-advance`/`isAdvance` mode that relied on the Bootstrap grid for field widths — either the default 2-columns-per-row layout, or an explicit `advance-container-class`/`advanceContainerClass` Bootstrap grid class (e.g. `"col-12 col-sm-6"`).
21
+ - Replace: nothing required to keep working — fields now lay out via flex (`flex: 1 1 240px`, wraps automatically), which also fixes long field labels getting clipped. A Bootstrap grid class passed to `advance-container-class` no longer has a `.row` ancestor to size against and should be replaced with either a plain width class/CSS, or the new `width` prop on the individual field component (`AbInputField`/`AbMultiSelect`/`AbDateTimePicker`) inside `filterOptions`.
22
+
23
+ ### ℹ️ Also shipped (no action needed)
24
+
25
+ - `AbFilterPanel`'s `.apbd-filter-chip-group` (quick-advance multi-select popover trigger) height fixed to match input-field heights at every size (`xs`…`xxl`), and no longer changes height (grows or shrinks) once a tag is selected — a `min-height` per size acts as a floor while still allowing the box to grow when tags wrap onto a second line.
26
+ - New `width`, `minWidth`, `maxWidth` props (`String | Number`) on `AbInputField`, `AbMultiSelect`, `AbDateTimePicker` for sizing a single field directly; `AbFilterPanel` forwards a `width`/`minWidth`/`maxWidth` key from any `filterOptions` entry to that field's props and its flex-item wrapper.
27
+ - New `AbFilterPanel` `propertySelectWidth`/`propertySelectMinWidth`/`propertySelectMaxWidth` props for the standard-mode property-selector/scan-field slot, which isn't a `filterOptions` entry so the field-level keys above can't reach it.
28
+
29
+ ## [1.0.17] - 2026-08-30
30
+
31
+ ### ⚠️ Action Required — Breaking
32
+
33
+ - **`abVeeRules` translation message keys renamed from literal English sentences to short dotted keys.**
34
+ - Find: any entry in a `translations[<lang>]` object (passed to `abCreateGettext`) keyed by one of the old literal sentences — `"%{fld_name} is required"`, `"%{fld_name} should be numeric"`, `"%{fld_name} not a valid email address"`, `"%{fld_name} should be more than " + <value>` (min), `"%{fld_name} should be less than " + <value>` (max), `"%{fld_name} does not match with its password"`, `"%{fld_name} is invalid"` (url), `"%{fld_name} length is not valid, please check it"`.
35
+ - Replace: rename the key to, respectively, `"field.required"`, `"field.numeric"`, `"field.valid.email"`, `"field.min.value"`, `"field.max.value"`, `"field.confirm.password"`, `"field.valid.url"`, `"field.valid.length"`. For `field.min.value`/`field.max.value`, the value is now a separate `%{value}` interpolation param instead of being concatenated into the key/message — the value text should include `%{value}` where the limit belongs (e.g. `"%{fld_name} should be more than %{value}"`), not a hardcoded number.
36
+ - See [`.ai/ai_ref_abVeeRules.md`](./ai_ref_abVeeRules.md) for the full key list and a working example.
37
+
15
38
  ## [1.0.15] - 2026-08-24
16
39
 
17
40
  ### ⚠️ Action Required — Breaking
@@ -25,7 +48,8 @@
25
48
 
26
49
  - New `formatCurrency`/`formatAmount` utils + matching global config keys (`currency`, `currencyPosition`, `thousandSeparator`, `decimalSeparator`, `numberOfDecimals`, `round`) — optional to adopt for consumer-side number/currency formatting.
27
50
  - New `AbChatBox` component (wraps `AbChatInput` as a full chat UI) and new `AbScrollbar` exposed scroll methods.
28
- - `AbFormCheck` checked-state label color and `description-text` truncation CSS fixes; a WordPress `.is-flat-check` margin conflict fix.
51
+ - `AbFormCheck` checked-state label color and `description-text` truncation CSS fixes; a WordPress `.apbd-radio-input-container` margin conflict fix (covers all enhanced variants, not just `card-flat-check`).
52
+ - `AbSidebar` header no longer clamps to a fixed height and top-aligns its content instead of centering — lets multi-line header content grow the header instead of being clipped/centered.
29
53
  - Several `AbChatInput` voice-input (Safari/Android/iOS) and narrow-viewport layout fixes.
30
54
 
31
55
  ## [1.0.14] - 2026-08-19
package/AI_REFERENCE.md CHANGED
@@ -61,6 +61,7 @@
61
61
  | `AbChatInput` | — | ChatGPT-style auto-growing composer with an item-count-driven add/more dropdown, internal file attachment preview, and built-in Web Speech API voice input. | [.ai/ai_ref_AbChatInput.md](./.ai/ai_ref_AbChatInput.md) |
62
62
  | `AbChatInputMenuItem` | — | Presentational row (icon/name/description/action) for `AbChatInput`'s add/more dropdown. | [.ai/ai_ref_AbChatInputMenuItem.md](./.ai/ai_ref_AbChatInputMenuItem.md) |
63
63
  | `AbChatBox` | — | Full chat conversation container (header, message list, empty/typing states) built on `AbChatInput`; every part is slot-overridable. | [.ai/ai_ref_AbChatBox.md](./.ai/ai_ref_AbChatBox.md) |
64
+ | `AbVoiceButton` | — | Standalone Web Speech API voice-to-text mic button (v-model), no composer attached — same engine as `AbChatInput`'s built-in mic. | [.ai/ai_ref_AbVoiceButton.md](./.ai/ai_ref_AbVoiceButton.md) |
64
65
  | `AbDarkModeToggler` | — | Light / dark theme toggle (pairs with `useTheme`). | [.ai/ai_ref_AbDarkModeToggler.md](./.ai/ai_ref_AbDarkModeToggler.md) |
65
66
  | `useAlert` | — | Procedural API to trigger SweetAlert-based modal alerts, notifications, and confirmations. | [.ai/ai_ref_useAlert.md](./.ai/ai_ref_useAlert.md) |
66
67
  | `abEventBus` / `emitterObj` | — | Lightweight global event bus using `tiny-emitter`. | [.ai/ai_ref_abEventBus.md](./.ai/ai_ref_abEventBus.md) |
package/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  All notable changes to `@appsbd/vue3-appsbd-ui` are documented here. Format loosely follows [Keep a Changelog](https://keepachangelog.com/).
4
4
 
5
+ ## [1.0.17]
6
+
7
+ - **Fixed**: `AbFilterPanel`'s `isBuilderFilter` popup editor was too narrow (`max-width: 320px`, no `width`) for longer values like a `"dr"` (date range) field's `"01/09/2026 - 15/09/2026"`-style text, which got clipped inside its input. Raising just `max-width` didn't help on its own — `AbPopover`'s content box is `position: absolute; width: auto`, which sizes to shrink-to-fit, and a shrink-to-fit box ignores a percentage-width child (`AbInputField`'s `.form-control` is `width: 100%`) when computing that fit, so the box never actually grew to use the extra ceiling. Giving it an explicit `width` fixed that, but the first pass (`340px`) was tuned tight enough against one specific date pair that it broke again on a different one — Bengali numeral glyphs (০-৯) aren't uniform-width (e.g. "৪"/"৬" render wider than "১"), so a range using wider digits needed more room than the one used to size it. Now `width: 400px` (`min-width: 280px` / `max-width: 440px` as clamps) with real margin instead of a pixel-exact fit; the popover's own default 400px cap is also raised to 480px so it isn't clipped from outside.
8
+ - **Fixed**: `AbFilterPanel`'s `"tr"` (text range) field's Min/arrow/Max row (`.range-input-panel`, shared by standard, advance, and the new `isBuilderFilter` mode) was built from raw `<div class="input-group">` + `<input class="form-control">` markup instead of `AbInputField`, so it never scaled with the panel's `size` prop — the label/input join used a hardcoded `input-group-sm`, and a copy-paste bug hardcoded the **Max** input to `form-control-sm` specifically (while Min correctly used the dynamic size) — so Min and Max could literally render at two different heights. The row also had no flex layout CSS at all: each `.form-control`'s default `width: 100%` forced Min/Max onto separate stacked lines instead of sitting side by side, and the separator `<svg>` had no explicit size (rendering at the browser's 300×150px SVG default) since the `w-4`/`h-4` classes on it aren't real classes in this project (Bootstrap has no such utilities). Now built from two `AbInputField`s (the first carrying the field's name as its `prefix`, matching every other field type) in a proper flex row with `1em` icon sizing — sizes correctly with the panel's `size` prop like every other field. This had never been visually exercised before — no existing playground demo used a `"tr"` field until `isBuilderFilter`'s "Revenue" example.
9
+ - **Added**: `AbFilterPanel` `isBuilderFilter` prop — a Shopify-style filter builder mode. No field is shown until the user clicks a `+` button; that opens a popover which first lists every not-yet-applied `filterOptions` entry, then (once one is picked) swaps to a value editor matching that field's `type` plus Cancel/Apply Filter buttons. Apply adds it as a removable chip (e.g. "Email contains gmail") and immediately fires `searchFilter`; there's no global Search button in this mode, only Reset (clears every applied chip/value at once). A field's `operators` stays fixed by its schema entry, same as other modes — this mode doesn't add an is/is-not/contains operator picker. The applied-filter chip's height/padding scales with the panel's `size` prop, same as its `+`/Reset buttons.
10
+ - **Fixed**: `AbPopover`'s outside-click-to-close listened on `click`, which races with anything inside its content that reacts on `mousedown` and removes/moves itself before that later `click` fires — e.g. `AbMultiSelect` selecting a single option closes its own dropdown on `mousedown`, so by the time `click` arrived its real target was already whatever sat behind the now-collapsed dropdown, closing the outer popover too (hit when nesting an `AbMultiSelect` with `append-to-body="false"` inside an `AbPopover`, as `AbFilterPanel`'s new `isBuilderFilter` mode does for its dropdown-type fields). Outside detection now runs on `mousedown`, before such handlers can react, and reads `event.composedPath()` instead of `event.target` + `.contains()` so it's also correct when a same-tick handler swaps out the clicked element.
11
+ - **Fixed**: `AbPopover` closed immediately (with no selection) when clicking inside a *nested* `AbPopover` in its content — e.g. an `AbDateTimePicker` (which opens its calendar through its own `AbPopover`) placed inside `AbFilterPanel`'s `isBuilderFilter` editor: any date click, or even changing the calendar's month, closed the outer popover before the inner one could react, because the inner popover's floating content always renders through its own `<Teleport to="body">` and so never sits inside the outer popover's own floating element in the DOM. `AbPopover` now tracks every mounted popover (via the existing `popoverManager`) and treats a click as "inside" when it lands on another popover's floating content whose trigger is nested inside its own — so a calendar (or any `AbPopover`-based control) opened from within another popover's content no longer closes its parent.
12
+ - **Fixed**: `AbFilterPanel`'s `.apbd-filter-chip-group` (the quick-advance multi-select popover trigger) had an inconsistent height across sizes — only `xs`/`sm` had explicit padding, `md`/`lg`/`xl`/`xxl` fell through to browser-default padding/line-height. Every size (`xs` `sm` `md` `lg` `xl` `xxl`) now computes to the same height as `AbInputField`/`AbMultiSelect` at that size (28/36/40/44/48/60px) through the shared padding + line-height tokens — never a hardcoded `height` — and the font-size is now constant across all sizes instead of shrinking at `xs`/`sm`. Border-radius and background also switched from an undefined CSS variable (silently falling back to a hardcoded value) to the real `--#{$prefix}border-radius*`/`--#{$prefix}apbd-input-bg-color` tokens, matching input fields in dark mode too.
13
+ - **Fixed**: `AbFilterPanel`'s `.apbd-filter-chip-group` didn't keep a consistent height once a tag was selected — the selected-tag pill (`.apbd-filter-tag`) inheriting the container's line-height plus its own border + padding first made the box grow taller than its siblings; capping the tag to `line-height: 1` then let the box shrink *shorter* than its siblings instead, since the container has no explicit `height` and was sizing itself off whatever child happened to be present. Each size class now also carries a `min-height` matching that size's target (28/36/40/44/48/60px for `xs`…`xxl`) as a floor — the box can still grow taller than that when tags wrap onto a second line, it just never renders shorter than a same-size input field regardless of what's inside it.
14
+ - **Changed** (breaking): `AbFilterPanel`'s standard-search (default mode) and advance-search (`isAdvance` mode) field layouts — both used Bootstrap's `.row`/`.col-*` grid (nested two levels deep in standard mode); a `.col-*` class doesn't do anything meaningful without a `.row` ancestor, and `.row`'s negative margins could overflow the outer flex wrapper and clip long field labels. Both are now a plain `d-flex flex-wrap gap-2` layout with no grid columns at all — each field is a flex item (`flex: 1 1 240px`, class `apbd-advance-filter-item`) that grows/shrinks and wraps on its own; `advanceContainerClass` default changed from `"col-6"` to `""` since a grid column class no longer does anything useful here. To fix a specific field's width instead, use that field's own new `width` prop.
15
+ - **Added**: `width`, `minWidth`, and `maxWidth` props (`String | Number`) on `AbInputField`, `AbMultiSelect`, and `AbDateTimePicker` — set an explicit width / `min-width` / `max-width` on the field's outer container (a number is treated as px, e.g. `width="220"` or `max-width="50%"`). `AbFilterPanel` forwards all three too: give any entry in `filterOptions` a `width`/`minWidth`/`maxWidth` key and it's passed straight to that field's own same-named prop *and* used to size that field's flex-item wrapper directly — the way to fix (or just bound) a specific field's size now that its layouts have no Bootstrap grid column to size against. Without one, a field left alone on the last row (nothing else to share the row with) stretches to fill it, since each field is a `flex: 1 1 240px` item.
16
+ - **Added**: `AbFilterPanel` `propertySelectWidth`/`propertySelectMinWidth`/`propertySelectMaxWidth` props (`String | Number`) — the standard-mode property selector (and the scan field it swaps for) isn't one of the `filterOptions` entries, so the `width`/`minWidth`/`maxWidth` field keys above don't reach it; it's also the sole item in its row until a property is picked (or whenever `showScanFld` is on), so it stretches to fill the row the same way any unsized flex field does. These three props size that slot directly.
17
+ - **Changed**: `abVeeRules`' built-in validation messages now translate through short, stable message keys (e.g. `field.required`, `field.numeric`, `field.valid.email`, `field.min.value`, `field.max.value`, `field.confirm.password`, `field.valid.url`, `field.valid.length`) instead of literal English sentences — `min_value`/`max_value` also now pass their configured limit as an interpolated `value` param instead of concatenating it directly into the message-key string (which previously produced a different, untranslatable key per limit value). Breaking change for any consumer whose `translations` object overrode these messages by their old literal-sentence key (e.g. `"%{fld_name} is required"`) — those entries must be renamed to the new keys, see `.ai/ai_ref_abVeeRules.md`.
18
+
19
+ ## [1.0.16] - 2026-08-27
20
+
21
+ - **Fixed**: WordPress theme conflict — `AbFormCheck`'s enhanced (`.apbd-radio-input-container`) checkbox/radio inputs now reset `margin: 0` to counter WordPress's default form-control margin. (Previously targeted `.is-flat-check`, which only covers the `card-flat-check` variant and missed the other enhanced variants.)
22
+ - **Fixed**: `AbSidebar` header (`.apbd-sidebar-header`) no longer clamps to a fixed `min-height`/`max-height` (now `unset`) and aligns its content to the top (`align-items: start` instead of `center`) — the header can grow to fit multi-line content instead of clipping/centering it within a fixed band.
23
+ - **Changed**: `AbChatInput` voice recording on Android only — no longer swaps the composer for the full-width recording bar (every other platform keeps that unchanged: waveform + cancel/stop/submit buttons). On Android the textarea now stays visible and fully editable the whole time instead (typing/deleting mid-recording works normally) and fills with the transcribed text live, in place; the mic button gets a Google Assistant-style listening cue instead — its circle fills with the theme's primary color (icon turns white), with a few rings pulsing outward from behind it on a loop. Tap it to stop (keeps the text); the send button (and Enter) still submits immediately while listening by stopping first. There's no cancel button in this Android UI — `cancelRecording()` remains available programmatically via a template ref.
24
+ - **Fixed**: `AbChatInput` voice input on Android — text was only appearing once the user tapped stop, and could double up when it did (words repeating mid-recording, not just at the end). Android's `continuous`+`interimResults` combination turned out unreliable across builds in a way delta-tracking `e.results` (by `resultIndex` or by position) can't fully work around — it can restate/duplicate earlier words in a later result with no restart involved at all. Android now runs one utterance per internal recognition session (`continuous: false`) instead, relying on the existing transparent onend-restart (added earlier for a Safari quirk) to pick the next utterance back up automatically — so `onresult` only ever needs that session's latest result, with no cross-event bookkeeping to get wrong. Combined with the textarea staying editable during recording (see above), `onresult` now also reads the current text and swaps out only the exact suffix it last inserted, so a manual edit mid-recording is preserved instead of being silently overwritten.
25
+ - **Fixed**: `AbChatInput`'s Android listening cue (see above) — the solid circle and ring animation never actually rendered, in either light or dark mode. They referenced `--#{$prefix}apbd-primary`, a CSS variable that isn't defined anywhere in the project; with no fallback, `var()` resolves to nothing and the whole declaration is silently dropped. Now uses `--#{$prefix}theme` (the actual theme accent variable, defined per-skin for both modes), with a `#0d6efd` fallback for safety.
26
+ - **Changed**: `AbChatInput`'s Android listening rings scaled down slightly (max `2.4` → `1.7`) — the previous size read as too large next to the language badge and send button. The textarea also refocuses automatically now the moment listening ends, on every platform (stop, cancel, or a voice-triggered submit) — the cursor lands back in it immediately instead of requiring an extra tap.
27
+ - **Fixed**: `AbChatInput`'s mic button on touchscreens — its `:hover` background had no touch guard, so tapping it (e.g. to stop recording on Android) could leave a lingering gray circle behind, a well-known mobile quirk: touch has no real pointer to move away and clear `:hover` the way a mouse does. Wrapped in `@media (hover: hover)` so it only applies on devices with genuine hover capability.
28
+ - **Added**: `AbChatInput` `voicePunctuationCommands` and `voiceAutoPunctuate` props (both `Boolean`, default `true`, independently toggleable) for voice input punctuation. `voicePunctuationCommands`: saying a punctuation word inserts the actual mark — English `"period"`/`"full stop"` → `.`, `"comma"` → `,`, `"question mark"` → `?`; Bengali `"পূর্ণচ্ছেদ"` → `।`, `"কমা"` → `,`, `"প্রশ্নবোধক"` → `?` — matched against the transcript text itself, independent of the selected recognition language. `voiceAutoPunctuate`: a sentence-ending mark (`.`, or `।` when `voiceLang` starts with `bn`) is inserted between voice utterances (each final phrase on non-Android, each per-utterance restart on Android) whenever the existing text doesn't already end with one.
29
+ - **Fixed**: `AbChatInput`'s `voicePunctuationCommands` (see above) — command-word matching used `\b`, which only recognizes ASCII letters/digits as "word" characters and is effectively a no-op for Bengali script, so a Bengali trigger word could match as a substring inside an unrelated longer word (e.g. saying "দাঁড়িপাল্লা" — "scale/balance" — got mangled into "। পাল্লা"). Now uses script-agnostic lookaround instead, matching only when the word is actually standalone. `"দাড়ি"` was also dropped entirely as a trigger (not just boundary-guarded) — fixing the substring match doesn't fix it also being ordinary vocabulary (beard), so a sentence genuinely about a beard would still lose that word every time; `"পূর্ণচ্ছেদ"` (a purely technical term nobody says in normal conversation) remains as the sole Bengali trigger for `.`/`।`.
30
+ - **Added**: `AbVoiceButton` — a standalone voice-to-text mic button, the same recognition engine as `AbChatInput`'s built-in mic with no composer attached. `v-model` for the transcribed text; `voice-start`/`voice-stop`/`voice-cancel`/`voice-error` events; `startRecording()`/`stopRecording()`/`cancelRecording()` exposed methods. `size` (px, default `40`) and `lang` (BCP-47, auto-detected when unset — no built-in language picker here) props, plus the same `voicePunctuationCommands`/`voiceAutoPunctuate` props as `AbChatInput`. Unlike `AbChatInput`, the Google Assistant-style listening cue (solid theme-color circle + pulsing rings) and the live, per-utterance recognition architecture run on every platform here, not just Android — this component has no legacy full recording-bar behavior to preserve, so there was no reason to keep the two paths.
31
+ - **Changed**: `AbChatInput` voice recording — the Google Assistant-style listening cue (mic icon stays put with pulsing rings, textarea stays visible and live-editable the whole time) now runs on every platform, not just Android, matching `AbVoiceButton`'s architecture. The separate full-width recording bar (audio-reactive waveform + cancel/stop/submit buttons), previously kept for every platform except Android, is removed entirely — there's no dedicated cancel button in the UI anymore; `cancelRecording()` remains available programmatically via a template ref, and the default send button (or `submit()`/Enter) still stops-then-submits when used while listening.
32
+ - **Fixed**: `AbChatInput` voice-to-text not reliably converting speech on non-Android platforms — the old pipeline only ever committed text off a `SpeechRecognition` result's `isFinal` flag, which some browsers/builds hold back indefinitely instead of firing per pause segment, so speech could go untranscribed until — or even past — the point the user stopped recording. Every platform now runs the same per-utterance (`continuous: false`) + `interimResults: true` architecture already proven on Android, merging every recognized chunk into the text live instead of waiting on `isFinal`; the now-unused `getUserMedia`/`AnalyserNode` waveform pipeline (mic-hardware-exclusive, and irrelevant once the waveform UI it drove is gone) was removed along with it.
33
+ - **Fixed**: `AbChatInput` and `AbVoiceButton` mic button showing "Microphone permission denied" on Safari (desktop and iOS) after the `interimResults: true` change above — Safari's `SpeechRecognition` engine has a long-standing bug where turning on `interimResults` can fail to (re)start at all, surfacing as a generic `onerror('not-allowed')` indistinguishable from an actually-denied permission. Safari now keeps `interimResults: false` (final-only, its reliably-supported mode) while every other browser still gets live interim updates; `onresult`'s merge logic already handled final-only results correctly, so no other change was needed.
34
+ - **Fixed**: `AbChatInput` voice input transcribing sentences entirely lowercase (e.g. "hello how are you. hello" instead of "Hello how are you. Hello") — recognized speech was inserted as-is with no capitalization pass. Both existing punctuation props now also auto-capitalize the sentence they start: `voiceAutoPunctuate` capitalizes the first letter of a new utterance right after inserting a boundary mark (and the very first letter of the whole message unconditionally); `voicePunctuationCommands` capitalizes the letter right after any mark it inserts mid-utterance (`"hello period how are you"` → `"Hello. How are you"`). Applied only to the freshly recognized chunk, never to already-committed text, so it can't fight a manual edit made to an earlier sentence while still recording. Uses plain `toUpperCase()` on the matched letter — a safe no-op for scripts without letter case (Bengali, Arabic, CJK, …), so it needs no per-language list to work for any cased script.
35
+
5
36
  ## [1.0.15] - 2026-08-24
6
37
 
7
38
  - **Added**: `AbChatBox` — a full chat conversation container built on top of `AbChatInput`: header (title/subtitle/avatar, or fully custom via the `header`/`header-actions` slots), a scrollable message list with a default bubble renderer (overridable per-message via `message`/`message-avatar`), empty and typing states (`empty`/`typing` slots), and the composer docked at the bottom. `v-model:messages` holds the conversation, `v-model` holds the composer text; submitting auto-appends the user's message and fires `send`. Every `AbChatInput` prop, event, and slot (`left`/`right`/`send-button`/`menu-trigger`/`single-input`/`multiple-input`) is forwarded straight through to the inner composer.