@designcrowd/fe-shared-lib 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/.claude/settings.local.json +54 -0
  2. package/.claude/skills/playwright-cli/SKILL.md +278 -0
  3. package/.claude/skills/playwright-cli/references/request-mocking.md +87 -0
  4. package/.claude/skills/playwright-cli/references/running-code.md +232 -0
  5. package/.claude/skills/playwright-cli/references/session-management.md +169 -0
  6. package/.claude/skills/playwright-cli/references/storage-state.md +275 -0
  7. package/.claude/skills/playwright-cli/references/test-generation.md +88 -0
  8. package/.claude/skills/playwright-cli/references/tracing.md +139 -0
  9. package/.claude/skills/playwright-cli/references/video-recording.md +43 -0
  10. package/.playwright-cli/page-2026-04-15T02-26-54-483Z.yml +68 -0
  11. package/.playwright-cli/page-2026-04-15T02-27-08-312Z.yml +0 -0
  12. package/.playwright-cli/page-2026-04-15T02-27-25-596Z.yml +0 -0
  13. package/.playwright-cli/page-2026-04-15T02-27-40-203Z.yml +0 -0
  14. package/.playwright-cli/page-2026-04-15T02-38-58-180Z.yml +0 -0
  15. package/.playwright-cli/page-2026-04-15T02-39-01-736Z.yml +26 -0
  16. package/.playwright-cli/page-2026-04-15T02-39-10-233Z.yml +26 -0
  17. package/.playwright-cli/page-2026-04-15T02-39-43-909Z.yml +26 -0
  18. package/.playwright-cli/page-2026-04-15T02-40-44-800Z.yml +0 -0
  19. package/.playwright-cli/page-2026-04-15T02-40-54-188Z.yml +26 -0
  20. package/.playwright-cli/page-2026-04-15T02-40-59-031Z.yml +26 -0
  21. package/.playwright-cli/page-2026-04-15T02-51-07-111Z.yml +0 -0
  22. package/.playwright-cli/page-2026-04-15T02-51-10-941Z.yml +26 -0
  23. package/.playwright-cli/page-2026-04-15T02-51-17-020Z.yml +26 -0
  24. package/.playwright-cli/page-2026-04-15T02-51-42-403Z.yml +2 -0
  25. package/.playwright-cli/page-2026-04-15T02-51-53-552Z.yml +26 -0
  26. package/.playwright-cli/page-2026-04-15T02-51-54-631Z.yml +26 -0
  27. package/.playwright-cli/page-2026-04-15T02-52-16-170Z.yml +26 -0
  28. package/.playwright-cli/page-2026-04-15T02-52-17-246Z.yml +26 -0
  29. package/.playwright-cli/page-2026-04-15T02-52-28-472Z.yml +26 -0
  30. package/.playwright-cli/page-2026-04-15T02-53-15-507Z.yml +0 -0
  31. package/.playwright-cli/page-2026-04-15T02-53-16-554Z.yml +26 -0
  32. package/.playwright-cli/page-2026-04-15T02-53-22-178Z.yml +26 -0
  33. package/.playwright-cli/page-2026-04-15T02-53-34-973Z.yml +26 -0
  34. package/.playwright-cli/page-2026-04-15T06-15-29-589Z.yml +68 -0
  35. package/.playwright-cli/page-2026-04-15T06-15-41-114Z.yml +68 -0
  36. package/.storybook-static/assets/Auth-DT64t5h-.css +1 -0
  37. package/.storybook-static/assets/Auth.stories-C6eXcTSu.js +490 -0
  38. package/.storybook-static/assets/AuthCrazyDomains.stories-DGvEoWCa.js +73 -0
  39. package/.storybook-static/assets/Button-5UzSGUF6.css +1 -0
  40. package/.storybook-static/assets/Button-DKdQT6Fq.js +1 -0
  41. package/.storybook-static/assets/ButtonGroup-DDPXuhxR.css +1 -0
  42. package/.storybook-static/assets/ButtonGroup.stories-DlrYMRSk.js +504 -0
  43. package/.storybook-static/assets/ButtonPrimary-Bu6bXb_c.css +1 -0
  44. package/.storybook-static/assets/ButtonPrimary-BvWW6Duz.js +1 -0
  45. package/.storybook-static/assets/Buttons.stories-CKmd6hkZ.js +761 -0
  46. package/.storybook-static/assets/ButtonsCrazyDomains.stories-DdEuOUrn.js +199 -0
  47. package/.storybook-static/assets/Checkbox.mixin-DkHpdvGa.js +1 -0
  48. package/.storybook-static/assets/Checkbox.stories-DPBUC2Mx.js +246 -0
  49. package/.storybook-static/assets/Checktile.stories-ByaFwplD.js +88 -0
  50. package/.storybook-static/assets/CollapsiblePanel.stories-Y6q3gP9j.js +56 -0
  51. package/.storybook-static/assets/ColorPicker.stories-DdxPUB_R.js +73 -0
  52. package/.storybook-static/assets/CopyToClipboardText.stories-J9qndWxd.js +32 -0
  53. package/.storybook-static/assets/Dropdown.stories-1zKPATii.js +159 -0
  54. package/.storybook-static/assets/DropdownItem-BV-BdThU.css +1 -0
  55. package/.storybook-static/assets/DropdownItem-DA6TdpDb.js +1 -0
  56. package/.storybook-static/assets/FormControl.mixin-DcEBwrV3.js +1 -0
  57. package/.storybook-static/assets/HashRouteModal.stories-BGxvqE22.js +60 -0
  58. package/.storybook-static/assets/HelloBar-CYEZR2kQ.js +1 -0
  59. package/.storybook-static/assets/HelloBar.stories-597Kxj0W.js +342 -0
  60. package/.storybook-static/assets/Icon-C17LFvsP.js +145 -0
  61. package/.storybook-static/assets/Icon.stories-B9iAmcTU.js +151 -0
  62. package/.storybook-static/assets/Icon.stories-CR5vT9H7.js +791 -0
  63. package/.storybook-static/assets/Loader-BWGoT_xC.js +1 -0
  64. package/.storybook-static/assets/LogoBusinessBrandColours-CExzox1Z.js +1 -0
  65. package/.storybook-static/assets/LogoBusinessBrandColours-CeAaMKke.css +1 -0
  66. package/.storybook-static/assets/LogoBusinessBrandColours.stories-kuxAH8B8.js +36 -0
  67. package/.storybook-static/assets/Masonry-C2MNiGg0.css +1 -0
  68. package/.storybook-static/assets/Masonry.stories-CTXJLQ_i.js +71 -0
  69. package/.storybook-static/assets/Modal-CGwEIF5R.css +1 -0
  70. package/.storybook-static/assets/Modal-CydTNprT.js +1 -0
  71. package/.storybook-static/assets/Modal.stories-DZiG5NGM.js +345 -0
  72. package/.storybook-static/assets/Notice.stories-ChOj8CWm.js +222 -0
  73. package/.storybook-static/assets/NumberStepper-Blffv09R.css +1 -0
  74. package/.storybook-static/assets/NumberStepper.stories-CVbKJ_oJ.js +64 -0
  75. package/.storybook-static/assets/PaymentConfigList-BpUMV6cp.css +1 -0
  76. package/.storybook-static/assets/PaymentConfigList.stories-DUD7OZBS.js +130 -0
  77. package/.storybook-static/assets/Picture-B8m1I9xN.js +1 -0
  78. package/.storybook-static/assets/Picture.stories-MMzybhJ6.js +119 -0
  79. package/.storybook-static/assets/Pill-DLXZ_TL8.js +1 -0
  80. package/.storybook-static/assets/Pill.stories-DCP7szJm.js +18 -0
  81. package/.storybook-static/assets/PillBar-os4mJV3M.css +1 -0
  82. package/.storybook-static/assets/PillBar.stories-Bry-zQ6f.js +41 -0
  83. package/.storybook-static/assets/Price-C4GZbDSa.js +1 -0
  84. package/.storybook-static/assets/Price.stories-CMHly9V0.js +337 -0
  85. package/.storybook-static/assets/PromoCard.stories-xsbFtADE.js +299 -0
  86. package/.storybook-static/assets/PublishBrandPageModal-Q9-mNG1q.css +1 -0
  87. package/.storybook-static/assets/PublishBrandPageModal.stories-C9XzW_1m.js +324 -0
  88. package/.storybook-static/assets/SearchBar.stories-DaIneOSz.js +12 -0
  89. package/.storybook-static/assets/Select-DnioWQmi.css +1 -0
  90. package/.storybook-static/assets/Select.stories-BmGYB4pw.js +108 -0
  91. package/.storybook-static/assets/SellDomainNameList.fixtures-LC6fjr_b.js +1 -0
  92. package/.storybook-static/assets/SellDomainNameListModal-DH6khE10.css +1 -0
  93. package/.storybook-static/assets/SellDomainNameListModal-ymtVclFP.js +1 -0
  94. package/.storybook-static/assets/SellDomainNameListModal.stories-DvGvylgx.js +71 -0
  95. package/.storybook-static/assets/SellDomainNameListSearchResult-Cpxq0jDA.css +1 -0
  96. package/.storybook-static/assets/SellDomainNameListSearchResult-D-1CrQyf.js +1 -0
  97. package/.storybook-static/assets/SellDomainNameSearchWithResults-bX--zu97.js +1 -0
  98. package/.storybook-static/assets/SellDomainNameSearchWithResults.stories-DRUJjSdH.js +37 -0
  99. package/.storybook-static/assets/SellDomainNameWidget.stories-CC3LX10s.js +36 -0
  100. package/.storybook-static/assets/SignIn-CPjf8_2O.css +1 -0
  101. package/.storybook-static/assets/SignIn-DI0DSDFe.js +1 -0
  102. package/.storybook-static/assets/Slider-Cog2FFdj.css +1 -0
  103. package/.storybook-static/assets/Slider.stories-B2KGwnJy.js +141 -0
  104. package/.storybook-static/assets/SparkleIcon.stories-Dk904hVE.js +547 -0
  105. package/.storybook-static/assets/StarRating-BtKh7pzm.css +1 -0
  106. package/.storybook-static/assets/StarRating.stories-d2mgOuo2.js +45 -0
  107. package/.storybook-static/assets/TabMenu.stories-Cg2yenqj.js +47 -0
  108. package/.storybook-static/assets/TextCopyField-B66NKTk_.js +1 -0
  109. package/.storybook-static/assets/TextCopyField.stories-B4_ZlfLU.js +47 -0
  110. package/.storybook-static/assets/TextInput-CMoUjT_5.js +1 -0
  111. package/.storybook-static/assets/TextInput.stories-oyyxxf3j.js +233 -0
  112. package/.storybook-static/assets/Textarea.stories-BvhZR6K2.js +207 -0
  113. package/.storybook-static/assets/Toggle.stories-yT5-rL2k.js +161 -0
  114. package/.storybook-static/assets/Tooltip-DyXIgFQH.css +1 -0
  115. package/.storybook-static/assets/Tooltip-ZukyujG5.js +1 -0
  116. package/.storybook-static/assets/Tooltip.stories-sJFylRS_.js +953 -0
  117. package/.storybook-static/assets/UploadYourLogoApplication-Dmw8QcH3.css +1 -0
  118. package/.storybook-static/assets/UploadYourLogoApplication.stories-C9AvzHO_.js +186 -0
  119. package/.storybook-static/assets/UploadYourLogoDropzone-B1ffcicv.js +24 -0
  120. package/.storybook-static/assets/UploadYourLogoDropzone-DQqACf-e.css +1 -0
  121. package/.storybook-static/assets/UploadYourLogoDropzone.stories-D1Dt2ord.js +55 -0
  122. package/.storybook-static/assets/UploadedLogoSearchResultCard.stories-D8oF1Yrx.js +79 -0
  123. package/.storybook-static/assets/WebsiteContextualUpgradeModal-8u1zOZrW.css +1 -0
  124. package/.storybook-static/assets/WebsiteContextualUpgradeModal.stories-mtcvWOAg.js +211 -0
  125. package/.storybook-static/assets/_commonjsHelpers-CE1G-McA.js +1 -0
  126. package/.storybook-static/assets/_plugin-vue_export-helper-DlAUqK2U.js +1 -0
  127. package/.storybook-static/assets/api-lSJGRrF2.js +1 -0
  128. package/.storybook-static/assets/axe-DrS73Vi2.js +20 -0
  129. package/.storybook-static/assets/brand-crowd-api.client-D45NKshX.js +1 -0
  130. package/.storybook-static/assets/bundled-translations-BoWhEDU_.js +1 -0
  131. package/.storybook-static/assets/bundled-translations.de-DE-C4lqla4O.js +1 -0
  132. package/.storybook-static/assets/bundled-translations.es-ES-BxMIllUH.js +1 -0
  133. package/.storybook-static/assets/bundled-translations.fr-CA-MxZpyz0w.js +1 -0
  134. package/.storybook-static/assets/bundled-translations.fr-FR-N7UPCZVr.js +1 -0
  135. package/.storybook-static/assets/bundled-translations.pt-BR-C8tscYuG.js +1 -0
  136. package/.storybook-static/assets/bundled-translations.pt-PT-Dszj5Xfa.js +1 -0
  137. package/.storybook-static/assets/carousel-BelyIYOK.css +1 -0
  138. package/.storybook-static/assets/carousel.stories-CJw3-Iy6.js +668 -0
  139. package/.storybook-static/assets/event-constants-CMO9VQVu.js +1 -0
  140. package/.storybook-static/assets/iframe-B3A6OXQU.js +1104 -0
  141. package/.storybook-static/assets/index-B-eiLVzF.js +7 -0
  142. package/.storybook-static/assets/index-QquxUozE.js +6 -0
  143. package/.storybook-static/assets/matchers-5TDFFDYO-HJu_DfWo.js +14 -0
  144. package/.storybook-static/assets/mediaQueryMixin-CISNqd93.js +1 -0
  145. package/.storybook-static/assets/preload-helper-PPVm8Dsz.js +1 -0
  146. package/.storybook-static/assets/tracking-ATsLLehC.js +1 -0
  147. package/.storybook-static/css/tailwind-brandCrowd.css +2508 -0
  148. package/.storybook-static/css/tailwind-brandPage.css +2188 -0
  149. package/.storybook-static/css/tailwind-crazyDomains.css +2508 -0
  150. package/.storybook-static/css/tailwind-designCom.css +2508 -0
  151. package/.storybook-static/css/tailwind-designCrowd.css +2508 -0
  152. package/.storybook-static/favicon-wrapper.svg +46 -0
  153. package/.storybook-static/favicon.svg +1 -0
  154. package/.storybook-static/iframe.html +713 -0
  155. package/.storybook-static/index.html +148 -0
  156. package/.storybook-static/index.json +1 -0
  157. package/.storybook-static/nunito-sans-bold-italic.woff2 +0 -0
  158. package/.storybook-static/nunito-sans-bold.woff2 +0 -0
  159. package/.storybook-static/nunito-sans-italic.woff2 +0 -0
  160. package/.storybook-static/nunito-sans-regular.woff2 +0 -0
  161. package/.storybook-static/project.json +1 -0
  162. package/.storybook-static/sb-addons/a11y-1/manager-bundle.js +57 -0
  163. package/.storybook-static/sb-addons/links-2/manager-bundle.js +3 -0
  164. package/.storybook-static/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js +628 -0
  165. package/.storybook-static/sb-addons/themes-3/manager-bundle.js +3 -0
  166. package/.storybook-static/sb-common-assets/favicon-wrapper.svg +46 -0
  167. package/.storybook-static/sb-common-assets/favicon.svg +1 -0
  168. package/.storybook-static/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
  169. package/.storybook-static/sb-common-assets/nunito-sans-bold.woff2 +0 -0
  170. package/.storybook-static/sb-common-assets/nunito-sans-italic.woff2 +0 -0
  171. package/.storybook-static/sb-common-assets/nunito-sans-regular.woff2 +0 -0
  172. package/.storybook-static/sb-manager/globals-runtime.js +77935 -0
  173. package/.storybook-static/sb-manager/globals.js +24 -0
  174. package/.storybook-static/sb-manager/manager-stores.js +23 -0
  175. package/.storybook-static/sb-manager/runtime.js +20404 -0
  176. package/.storybook-static/vite-inject-mocker-entry.js +2 -0
  177. package/CLAUDE.md +35 -0
  178. package/dist/css/tailwind-brandCrowd.css +4 -34
  179. package/dist/css/tailwind-brandPage.css +3 -25
  180. package/dist/css/tailwind-crazyDomains.css +4 -34
  181. package/dist/css/tailwind-designCom.css +4 -34
  182. package/dist/css/tailwind-designCrowd.css +4 -34
  183. package/docs/voice-to-text-discussion-attachments/Screenshot 2026-04-15 at 11.44.18/342/200/257am.png +0 -0
  184. package/docs/voice-to-text-discussion-attachments/image.png +0 -0
  185. package/docs/voice-to-text-discussion-attachments/image_1.png +0 -0
  186. package/docs/voice-to-text-discussion-attachments/image_2.png +0 -0
  187. package/docs/voice-to-text-discussion-attachments/image_3.png +0 -0
  188. package/docs/voice-to-text-discussion-attachments/image_4.png +0 -0
  189. package/docs/voice-to-text-discussion.md +330 -0
  190. package/index.js +2 -0
  191. package/package.json +3 -1
  192. package/public/css/tailwind-brandCrowd.css +2563 -0
  193. package/public/css/tailwind-brandPage.css +2231 -0
  194. package/public/css/tailwind-crazyDomains.css +2563 -0
  195. package/public/css/tailwind-designCom.css +2563 -0
  196. package/public/css/tailwind-designCrowd.css +2563 -0
  197. package/src/atoms/components/Icon/Icon.vue +2 -0
  198. package/src/atoms/components/Icon/icons/microphone.vue +5 -0
  199. package/src/atoms/components/VoiceToTextButton/VoiceToTextButton.stories.ts +242 -0
  200. package/src/atoms/components/VoiceToTextButton/VoiceToTextButton.vue +147 -0
  201. package/src/types/speech-recognition.d.ts +8 -0
  202. package/src/useVoiceToText.ts +196 -0
@@ -237,6 +237,7 @@ import IconLinkInBioFilled from './icons/link-in-bio-filled.vue';
237
237
  import IconMedia from './icons/media.vue';
238
238
  import IconMinusCircleLight from './icons/minus-circle-light.vue';
239
239
  import IconMinus from './icons/minus.vue';
240
+ import IconMicrophone from './icons/microphone.vue';
240
241
  import IconMobile from './icons/mobile.vue';
241
242
  import IconOther from './icons/other.vue';
242
243
  import IconPageButtons from './icons/page-buttons.vue';
@@ -638,6 +639,7 @@ export default {
638
639
  IconContactMessage,
639
640
  IconMinus,
640
641
  IconMinusCircleLight,
642
+ IconMicrophone,
641
643
  IconMobile,
642
644
  IconMug,
643
645
  IconOther,
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <path
3
+ d="M8 1a2 2 0 0 0-2 2v4a2 2 0 1 0 4 0V3a2 2 0 0 0-2-2zM4.5 7a.5.5 0 0 1 .5.5V8a3 3 0 1 0 6 0v-.5a.5.5 0 0 1 1 0V8a4 4 0 0 1-3.5 3.97V14h2a.5.5 0 0 1 0 1h-5a.5.5 0 0 1 0-1h2v-2.03A4 4 0 0 1 4 8v-.5a.5.5 0 0 1 .5-.5z"
4
+ />
5
+ </template>
@@ -0,0 +1,242 @@
1
+ import VoiceToTextButton from './VoiceToTextButton.vue';
2
+
3
+ export default {
4
+ title: 'Components/VoiceToTextButton',
5
+ component: VoiceToTextButton,
6
+ };
7
+
8
+ export const PromptInputDemo = () => ({
9
+ components: { VoiceToTextButton },
10
+ data() {
11
+ return {
12
+ transcript: '',
13
+ isListening: false,
14
+ error: null,
15
+ };
16
+ },
17
+ template: `
18
+ <div class="tw-min-h-[400px] tw-p-8 tw-flex tw-flex-col tw-items-center tw-justify-center" style="background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);">
19
+ <h1 class="tw-text-white tw-text-3xl tw-font-bold tw-mb-2 tw-text-center">Design anything with AI</h1>
20
+ <p class="tw-text-grayscale-400 tw-mb-8 tw-text-center">Design a logo, a website, or anything else in seconds with the power of AI</p>
21
+
22
+ <div class="tw-w-full tw-max-w-xl tw-bg-grayscale-800 tw-rounded-full tw-px-6 tw-py-3 tw-flex tw-items-center tw-gap-3">
23
+ <input
24
+ v-model="transcript"
25
+ type="text"
26
+ placeholder="What would you like to design?"
27
+ class="tw-flex-1 tw-bg-transparent tw-border-none tw-text-white tw-placeholder-grayscale-500 focus:tw-outline-none tw-text-base"
28
+ />
29
+ <VoiceToTextButton
30
+ variant="dark"
31
+ size="md"
32
+ @on-transcript="transcript = $event"
33
+ @on-interim-transcript="transcript = $event"
34
+ @on-start="isListening = true; error = null"
35
+ @on-stop="isListening = false"
36
+ @on-error="error = $event"
37
+ />
38
+ <button class="tw-w-10 tw-h-10 tw-rounded-full tw-bg-secondary-500 tw-flex tw-items-center tw-justify-center tw-text-white hover:tw-bg-secondary-600 tw-transition-colors">
39
+ <svg xmlns="http://www.w3.org/2000/svg" class="tw-w-5 tw-h-5" viewBox="0 0 20 20" fill="currentColor">
40
+ <path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd" />
41
+ </svg>
42
+ </button>
43
+ </div>
44
+
45
+ <div class="tw-mt-4 tw-text-sm tw-h-6">
46
+ <p v-if="isListening" class="tw-text-error-500 tw-font-medium">
47
+ Recording... speak now
48
+ </p>
49
+ <p v-else-if="error" class="tw-text-error-400">
50
+ {{ error }}
51
+ </p>
52
+ </div>
53
+ </div>
54
+ `,
55
+ });
56
+
57
+ PromptInputDemo.story = {
58
+ name: 'Dark - Prompt Input',
59
+ };
60
+
61
+ export const DarkVariantStates = () => ({
62
+ components: { VoiceToTextButton },
63
+ template: `
64
+ <div class="tw-p-8" style="background: #1a1a2e;">
65
+ <h3 class="tw-text-white tw-text-lg tw-font-semibold tw-mb-6">Dark Variant - All States</h3>
66
+
67
+ <div class="tw-flex tw-gap-8 tw-items-start">
68
+ <div class="tw-text-center">
69
+ <div class="tw-mb-2 tw-p-4 tw-bg-grayscale-800 tw-rounded-lg tw-inline-block">
70
+ <VoiceToTextButton variant="dark" size="sm" />
71
+ </div>
72
+ <p class="tw-text-grayscale-400 tw-text-xs">Small</p>
73
+ </div>
74
+
75
+ <div class="tw-text-center">
76
+ <div class="tw-mb-2 tw-p-4 tw-bg-grayscale-800 tw-rounded-lg tw-inline-block">
77
+ <VoiceToTextButton variant="dark" size="md" />
78
+ </div>
79
+ <p class="tw-text-grayscale-400 tw-text-xs">Medium (default)</p>
80
+ </div>
81
+
82
+ <div class="tw-text-center">
83
+ <div class="tw-mb-2 tw-p-4 tw-bg-grayscale-800 tw-rounded-lg tw-inline-block">
84
+ <VoiceToTextButton variant="dark" size="lg" />
85
+ </div>
86
+ <p class="tw-text-grayscale-400 tw-text-xs">Large</p>
87
+ </div>
88
+
89
+ <div class="tw-text-center">
90
+ <div class="tw-mb-2 tw-p-4 tw-bg-grayscale-800 tw-rounded-lg tw-inline-block">
91
+ <VoiceToTextButton variant="dark" :disabled="true" />
92
+ </div>
93
+ <p class="tw-text-grayscale-400 tw-text-xs">Disabled</p>
94
+ </div>
95
+ </div>
96
+
97
+ <p class="tw-text-grayscale-500 tw-text-sm tw-mt-6">
98
+ Click any button to see the recording state with grey circle indicator
99
+ </p>
100
+ </div>
101
+ `,
102
+ });
103
+
104
+ DarkVariantStates.story = {
105
+ name: 'Dark - Variant States',
106
+ };
107
+
108
+ export const LightInputDemo = () => ({
109
+ components: { VoiceToTextButton },
110
+ data() {
111
+ return {
112
+ transcript: '',
113
+ isListening: false,
114
+ error: null,
115
+ };
116
+ },
117
+ template: `
118
+ <div class="tw-min-h-[400px] tw-p-8 tw-flex tw-flex-col tw-items-center tw-justify-center tw-bg-white">
119
+ <h1 class="tw-text-grayscale-800 tw-text-3xl tw-font-bold tw-mb-2 tw-text-center">Search</h1>
120
+ <p class="tw-text-grayscale-600 tw-mb-8 tw-text-center">Find what you're looking for</p>
121
+
122
+ <div class="tw-w-full tw-max-w-xl tw-bg-grayscale-100 tw-rounded-full tw-px-6 tw-py-3 tw-flex tw-items-center tw-gap-3 tw-border tw-border-grayscale-300">
123
+ <input
124
+ v-model="transcript"
125
+ type="text"
126
+ placeholder="Type to search..."
127
+ class="tw-flex-1 tw-bg-transparent tw-border-none tw-text-grayscale-800 tw-placeholder-grayscale-500 focus:tw-outline-none tw-text-base"
128
+ />
129
+ <VoiceToTextButton
130
+ variant="light"
131
+ size="md"
132
+ @on-transcript="transcript = $event"
133
+ @on-interim-transcript="transcript = $event"
134
+ @on-start="isListening = true; error = null"
135
+ @on-stop="isListening = false"
136
+ @on-error="error = $event"
137
+ />
138
+ </div>
139
+
140
+ <div class="tw-mt-4 tw-text-sm tw-h-6">
141
+ <p v-if="isListening" class="tw-text-error-500 tw-font-medium">
142
+ Recording... speak now
143
+ </p>
144
+ <p v-else-if="error" class="tw-text-error-400">
145
+ {{ error }}
146
+ </p>
147
+ </div>
148
+ </div>
149
+ `,
150
+ });
151
+
152
+ LightInputDemo.story = {
153
+ name: 'Light - Search Input',
154
+ };
155
+
156
+ export const LightVariantStates = () => ({
157
+ components: { VoiceToTextButton },
158
+ template: `
159
+ <div class="tw-p-8 tw-bg-white">
160
+ <h3 class="tw-text-grayscale-800 tw-text-lg tw-font-semibold tw-mb-6">Light Variant - All States</h3>
161
+
162
+ <div class="tw-flex tw-gap-8 tw-items-start">
163
+ <div class="tw-text-center">
164
+ <div class="tw-mb-2 tw-p-4 tw-bg-grayscale-100 tw-rounded-lg tw-inline-block">
165
+ <VoiceToTextButton variant="light" size="sm" />
166
+ </div>
167
+ <p class="tw-text-grayscale-600 tw-text-xs">Small</p>
168
+ </div>
169
+
170
+ <div class="tw-text-center">
171
+ <div class="tw-mb-2 tw-p-4 tw-bg-grayscale-100 tw-rounded-lg tw-inline-block">
172
+ <VoiceToTextButton variant="light" size="md" />
173
+ </div>
174
+ <p class="tw-text-grayscale-600 tw-text-xs">Medium (default)</p>
175
+ </div>
176
+
177
+ <div class="tw-text-center">
178
+ <div class="tw-mb-2 tw-p-4 tw-bg-grayscale-100 tw-rounded-lg tw-inline-block">
179
+ <VoiceToTextButton variant="light" size="lg" />
180
+ </div>
181
+ <p class="tw-text-grayscale-600 tw-text-xs">Large</p>
182
+ </div>
183
+
184
+ <div class="tw-text-center">
185
+ <div class="tw-mb-2 tw-p-4 tw-bg-grayscale-100 tw-rounded-lg tw-inline-block">
186
+ <VoiceToTextButton variant="light" :disabled="true" />
187
+ </div>
188
+ <p class="tw-text-grayscale-600 tw-text-xs">Disabled</p>
189
+ </div>
190
+ </div>
191
+
192
+ <p class="tw-text-grayscale-500 tw-text-sm tw-mt-6">
193
+ Click any button to see the recording state with grey circle indicator
194
+ </p>
195
+ </div>
196
+ `,
197
+ });
198
+
199
+ LightVariantStates.story = {
200
+ name: 'Light - Variant States',
201
+ };
202
+
203
+ export const SideBySide = () => ({
204
+ components: { VoiceToTextButton },
205
+ template: `
206
+ <div class="tw-flex tw-min-h-[300px]">
207
+ <div class="tw-flex-1 tw-p-8 tw-flex tw-flex-col tw-items-center tw-justify-center" style="background: #1a1a2e;">
208
+ <h3 class="tw-text-white tw-text-lg tw-font-semibold tw-mb-6">Dark Variant</h3>
209
+ <div class="tw-flex tw-gap-4 tw-items-center">
210
+ <div class="tw-p-3 tw-bg-grayscale-800 tw-rounded-lg">
211
+ <VoiceToTextButton variant="dark" size="sm" />
212
+ </div>
213
+ <div class="tw-p-3 tw-bg-grayscale-800 tw-rounded-lg">
214
+ <VoiceToTextButton variant="dark" size="md" />
215
+ </div>
216
+ <div class="tw-p-3 tw-bg-grayscale-800 tw-rounded-lg">
217
+ <VoiceToTextButton variant="dark" size="lg" />
218
+ </div>
219
+ </div>
220
+ </div>
221
+
222
+ <div class="tw-flex-1 tw-p-8 tw-flex tw-flex-col tw-items-center tw-justify-center tw-bg-white">
223
+ <h3 class="tw-text-grayscale-800 tw-text-lg tw-font-semibold tw-mb-6">Light Variant</h3>
224
+ <div class="tw-flex tw-gap-4 tw-items-center">
225
+ <div class="tw-p-3 tw-bg-grayscale-100 tw-rounded-lg">
226
+ <VoiceToTextButton variant="light" size="sm" />
227
+ </div>
228
+ <div class="tw-p-3 tw-bg-grayscale-100 tw-rounded-lg">
229
+ <VoiceToTextButton variant="light" size="md" />
230
+ </div>
231
+ <div class="tw-p-3 tw-bg-grayscale-100 tw-rounded-lg">
232
+ <VoiceToTextButton variant="light" size="lg" />
233
+ </div>
234
+ </div>
235
+ </div>
236
+ </div>
237
+ `,
238
+ });
239
+
240
+ SideBySide.story = {
241
+ name: 'Side by Side Comparison',
242
+ };
@@ -0,0 +1,147 @@
1
+ <template>
2
+ <button
3
+ v-if="isSupported"
4
+ type="button"
5
+ :disabled="disabled"
6
+ :aria-label="isListening ? 'Stop voice input' : 'Start voice input'"
7
+ data-test="voice-to-text-button"
8
+ class="voice-prompt-button tw-rounded-full tw-border-0 tw-flex tw-items-center tw-justify-center tw-transition-all tw-duration-200 tw-cursor-pointer focus:tw-outline-none focus-visible:tw-ring-2 focus-visible:tw-ring-offset-2 focus-visible:tw-ring-primary-500"
9
+ :class="[
10
+ sizeClasses,
11
+ variantClasses.button,
12
+ isListening ? 'voice-prompt-listening' : '',
13
+ disabled ? 'tw-opacity-50 tw-cursor-not-allowed' : '',
14
+ ]"
15
+ :style="isListening ? { '--voice-bg': variantClasses.recordingBg } : {}"
16
+ @click="toggle"
17
+ >
18
+ <Icon
19
+ name="microphone"
20
+ :size="iconSize"
21
+ :class="variantClasses.icon"
22
+ aria-hidden="true"
23
+ />
24
+ </button>
25
+ </template>
26
+
27
+ <script setup lang="ts">
28
+ import { watch, toRef, computed } from 'vue';
29
+ import Icon from '../Icon/Icon.vue';
30
+ import { useVoiceToText } from '../../../useVoiceToText';
31
+
32
+ type ButtonSize = 'sm' | 'md' | 'lg';
33
+ type ButtonVariant = 'light' | 'dark';
34
+
35
+ interface VoiceToTextButtonProps {
36
+ lang?: string;
37
+ disabled?: boolean;
38
+ size?: ButtonSize;
39
+ variant?: ButtonVariant;
40
+ }
41
+
42
+ const props = withDefaults(defineProps<VoiceToTextButtonProps>(), {
43
+ lang: 'en-US',
44
+ disabled: false,
45
+ size: 'md',
46
+ variant: 'dark',
47
+ });
48
+
49
+ const sizeClasses = computed(() => {
50
+ const sizes: Record<ButtonSize, string> = {
51
+ sm: 'tw-w-8 tw-h-8',
52
+ md: 'tw-w-10 tw-h-10',
53
+ lg: 'tw-w-12 tw-h-12',
54
+ };
55
+ return sizes[props.size];
56
+ });
57
+
58
+ const iconSize = computed(() => {
59
+ const sizes: Record<ButtonSize, string> = {
60
+ sm: 'sm',
61
+ md: 'md',
62
+ lg: 'md',
63
+ };
64
+ return sizes[props.size];
65
+ });
66
+
67
+ const emit = defineEmits<{
68
+ 'on-transcript': [transcript: string];
69
+ 'on-interim-transcript': [transcript: string];
70
+ 'on-start': [];
71
+ 'on-stop': [];
72
+ 'on-error': [error: string];
73
+ }>();
74
+
75
+ const { isSupported, isListening, transcript, isFinal, error, toggle, setLang } = useVoiceToText({
76
+ lang: props.lang,
77
+ });
78
+
79
+ const variantClasses = computed(() => {
80
+ if (props.variant === 'light') {
81
+ return {
82
+ button: isListening.value ? '' : 'tw-bg-transparent hover:tw-bg-grayscale-200',
83
+ icon: isListening.value ? 'tw-text-grayscale-700' : 'tw-text-grayscale-600',
84
+ recordingBg: '#EDEDED',
85
+ };
86
+ }
87
+ return {
88
+ button: isListening.value ? '' : 'tw-bg-transparent hover:tw-bg-grayscale-700',
89
+ icon: isListening.value ? 'tw-text-white' : 'tw-text-grayscale-400',
90
+ recordingBg: '#606060',
91
+ };
92
+ });
93
+
94
+ // Keep recognition language in sync with prop
95
+ watch(toRef(props, 'lang'), setLang);
96
+
97
+ // Watch for transcript changes and emit appropriate events
98
+ watch(
99
+ [transcript, isFinal],
100
+ ([newTranscript, newIsFinal]) => {
101
+ if (newIsFinal && newTranscript) {
102
+ emit('on-transcript', newTranscript);
103
+ } else if (newTranscript) {
104
+ emit('on-interim-transcript', newTranscript);
105
+ }
106
+ },
107
+ { flush: 'sync' },
108
+ );
109
+
110
+ // Watch for listening state changes
111
+ watch(isListening, (newVal, oldVal) => {
112
+ if (newVal && !oldVal) {
113
+ emit('on-start');
114
+ }
115
+ if (!newVal && oldVal) {
116
+ emit('on-stop');
117
+ }
118
+ });
119
+
120
+ // Watch for errors
121
+ watch(error, (newError) => {
122
+ if (newError) {
123
+ emit('on-error', newError);
124
+ }
125
+ });
126
+ </script>
127
+
128
+ <style scoped>
129
+ .voice-prompt-button {
130
+ -webkit-tap-highlight-color: transparent;
131
+ }
132
+
133
+ .voice-prompt-listening {
134
+ background-color: var(--voice-bg);
135
+ animation: voice-bg-pulse 2s ease-in-out infinite;
136
+ }
137
+
138
+ @keyframes voice-bg-pulse {
139
+ 0%,
140
+ 100% {
141
+ opacity: 1;
142
+ }
143
+ 50% {
144
+ opacity: 0.5;
145
+ }
146
+ }
147
+ </style>
@@ -0,0 +1,8 @@
1
+ /* eslint-disable no-undef, no-unused-vars */
2
+ export {};
3
+
4
+ declare global {
5
+ interface Window {
6
+ webkitSpeechRecognition: typeof SpeechRecognition;
7
+ }
8
+ }
@@ -0,0 +1,196 @@
1
+ /* eslint-disable no-undef */
2
+ import { ref, computed, readonly, type Ref, type ComputedRef, type DeepReadonly } from 'vue';
3
+
4
+ export interface UseVoiceToTextOptions {
5
+ lang?: string;
6
+ }
7
+
8
+ export interface UseVoiceToTextReturn {
9
+ isSupported: ComputedRef<boolean>;
10
+ isListening: DeepReadonly<Ref<boolean>>;
11
+ transcript: DeepReadonly<Ref<string>>;
12
+ isFinal: DeepReadonly<Ref<boolean>>;
13
+ error: DeepReadonly<Ref<string | null>>;
14
+ start: () => void;
15
+ stop: () => void;
16
+ toggle: () => void;
17
+ // eslint-disable-next-line no-unused-vars
18
+ setLang: (lang: string) => void;
19
+ }
20
+
21
+ interface VoiceToTextState {
22
+ isListening: Ref<boolean>;
23
+ transcript: Ref<string>;
24
+ isFinal: Ref<boolean>;
25
+ error: Ref<string | null>;
26
+ }
27
+
28
+ // Singleton instance and state (lazily initialized)
29
+ let recognition: SpeechRecognition | null = null;
30
+ let isInitialized = false;
31
+ let errorClearTimeout: ReturnType<typeof setTimeout> | null = null;
32
+ let state: VoiceToTextState | null = null;
33
+
34
+ // Error message mapping per spec
35
+ const ERROR_MESSAGES: Record<string, string> = {
36
+ 'not-allowed': 'Microphone permission was denied. Please allow access.',
37
+ 'language-not-supported': 'This language is not supported.',
38
+ network: 'A network error occurred. Please check your connection.',
39
+ 'audio-capture': 'No microphone was found or microphone is not working.',
40
+ };
41
+
42
+ const ERROR_CLEAR_DELAY = 5000;
43
+
44
+ function getState(): VoiceToTextState {
45
+ if (!state) {
46
+ state = {
47
+ isListening: ref(false),
48
+ transcript: ref(''),
49
+ isFinal: ref(false),
50
+ error: ref<string | null>(null),
51
+ };
52
+ }
53
+ return state;
54
+ }
55
+
56
+ /**
57
+ * Singleton composable that wraps the Web Speech API (SpeechRecognition).
58
+ * All calls to useVoiceToText() return the same shared instance.
59
+ */
60
+ export function useVoiceToText(options: UseVoiceToTextOptions = {}): UseVoiceToTextReturn {
61
+ const { lang = 'en-US' } = options;
62
+
63
+ // Get or create shared state
64
+ const { isListening, transcript, isFinal, error } = getState();
65
+
66
+ // Check for browser support
67
+ const SpeechRecognitionCtor: typeof SpeechRecognition | null =
68
+ typeof window !== 'undefined' ? window.SpeechRecognition || window.webkitSpeechRecognition : null;
69
+
70
+ const isSupported = computed(() => !!SpeechRecognitionCtor);
71
+
72
+ // Initialize singleton once
73
+ if (!isInitialized && SpeechRecognitionCtor) {
74
+ recognition = new SpeechRecognitionCtor();
75
+ recognition.continuous = true;
76
+ recognition.interimResults = true;
77
+
78
+ recognition.onresult = (event: SpeechRecognitionEvent) => {
79
+ let interimTranscript = '';
80
+ let finalTranscript = '';
81
+
82
+ for (let i = event.resultIndex; i < event.results.length; i += 1) {
83
+ const result = event.results[i];
84
+ if (result.isFinal) {
85
+ finalTranscript += result[0].transcript;
86
+ } else {
87
+ interimTranscript += result[0].transcript;
88
+ }
89
+ }
90
+
91
+ if (finalTranscript) {
92
+ transcript.value = finalTranscript;
93
+ isFinal.value = true;
94
+ } else {
95
+ transcript.value = interimTranscript;
96
+ isFinal.value = false;
97
+ }
98
+ };
99
+
100
+ recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
101
+ // Suppress no-speech and aborted errors per spec
102
+ if (event.error === 'no-speech' || event.error === 'aborted') {
103
+ return;
104
+ }
105
+
106
+ const message = ERROR_MESSAGES[event.error] || 'An error occurred with speech recognition.';
107
+ error.value = message;
108
+
109
+ // eslint-disable-next-line no-console
110
+ console.warn('[useVoiceToText]', event.error, message);
111
+
112
+ // Auto-clear error after timeout
113
+ if (errorClearTimeout) {
114
+ clearTimeout(errorClearTimeout);
115
+ }
116
+ errorClearTimeout = setTimeout(() => {
117
+ error.value = null;
118
+ }, ERROR_CLEAR_DELAY);
119
+ };
120
+
121
+ recognition.onend = () => {
122
+ isListening.value = false;
123
+ };
124
+
125
+ isInitialized = true;
126
+ }
127
+
128
+ // Update language on existing instance
129
+ if (recognition) {
130
+ recognition.lang = lang;
131
+ }
132
+
133
+ const start = () => {
134
+ if (!recognition || isListening.value) return;
135
+
136
+ // Clear previous state
137
+ transcript.value = '';
138
+ isFinal.value = false;
139
+ error.value = null;
140
+ if (errorClearTimeout) {
141
+ clearTimeout(errorClearTimeout);
142
+ errorClearTimeout = null;
143
+ }
144
+
145
+ try {
146
+ recognition.start();
147
+ isListening.value = true;
148
+ } catch (e: unknown) {
149
+ // Handle case where recognition is already started
150
+ // eslint-disable-next-line no-console
151
+ console.warn('[useVoiceToText] Failed to start:', (e as Error).message);
152
+ }
153
+ };
154
+
155
+ const stop = () => {
156
+ if (!recognition || !isListening.value) return;
157
+
158
+ isListening.value = false;
159
+ try {
160
+ recognition.stop();
161
+ } catch (e: unknown) {
162
+ // Handle case where recognition is already stopped
163
+ // eslint-disable-next-line no-console
164
+ console.warn('[useVoiceToText] Failed to stop:', (e as Error).message);
165
+ }
166
+ };
167
+
168
+ const toggle = () => {
169
+ if (isListening.value) {
170
+ stop();
171
+ } else {
172
+ start();
173
+ }
174
+ };
175
+
176
+ const setLang = (newLang: string) => {
177
+ if (recognition) {
178
+ recognition.lang = newLang;
179
+ }
180
+ };
181
+
182
+ return {
183
+ // State (reactive, read-only)
184
+ isSupported,
185
+ isListening: readonly(isListening),
186
+ transcript: readonly(transcript),
187
+ isFinal: readonly(isFinal),
188
+ error: readonly(error),
189
+
190
+ // Actions
191
+ start,
192
+ stop,
193
+ toggle,
194
+ setLang,
195
+ };
196
+ }