@stacksjs/components 0.2.3

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 (342) hide show
  1. package/README.md +477 -0
  2. package/dist/Accordion-x2gjedap.stx +108 -0
  3. package/dist/Audio-0fk6p9tn.stx +50 -0
  4. package/dist/Avatar-5m0v0s4x.stx +88 -0
  5. package/dist/Badge-ynes82vv.stx +70 -0
  6. package/dist/Breadcrumb-h9n427sk.stx +79 -0
  7. package/dist/Button-7xtrzn14.stx +89 -0
  8. package/dist/Calendar-egvx1pma.stx +232 -0
  9. package/dist/Card-r9w7vy1a.stx +74 -0
  10. package/dist/Checkbox-m8v46pmx.stx +92 -0
  11. package/dist/Checkout-vcnnr176.stx +39 -0
  12. package/dist/CodeBlock-mwex82cm.stx +57 -0
  13. package/dist/Combobox-npb08vkp.stx +13 -0
  14. package/dist/ComboboxButton-vcdyq4zw.stx +13 -0
  15. package/dist/ComboboxInput-c9d2p02m.stx +25 -0
  16. package/dist/ComboboxOption-7d0h9ash.stx +52 -0
  17. package/dist/ComboboxOptions-4dtqc0qa.stx +12 -0
  18. package/dist/CommandPalette-ah00bf8v.stx +39 -0
  19. package/dist/CommandPaletteItem-2yx0gvtj.stx +32 -0
  20. package/dist/DefaultPaymentMethod-emw5fg8s.stx +62 -0
  21. package/dist/Dialog-1w4rz7x0.stx +19 -0
  22. package/dist/DialogBackdrop-qbm3y5ab.stx +9 -0
  23. package/dist/DialogDescription-01w5j74b.stx +11 -0
  24. package/dist/DialogPanel-brsszrrs.stx +16 -0
  25. package/dist/DialogTitle-3rxhwg21.stx +11 -0
  26. package/dist/Drawer-yfaz8q7f.stx +137 -0
  27. package/dist/Dropdown-ng7nx556.stx +24 -0
  28. package/dist/DropdownButton-vc4zpyrb.stx +23 -0
  29. package/dist/DropdownItem-z491y23c.stx +43 -0
  30. package/dist/DropdownItems-7ykf29sj.stx +18 -0
  31. package/dist/EmailInput-bfshpj92.stx +41 -0
  32. package/dist/Footer-xdaf02nr.stx +66 -0
  33. package/dist/Form-z4f7w6r1.stx +209 -0
  34. package/dist/Hero-9vvp7pxf.stx +25 -0
  35. package/dist/Image-ktk0451c.stx +197 -0
  36. package/dist/Installation-33bj4rm7.stx +39 -0
  37. package/dist/Listbox-xcejcmyc.stx +14 -0
  38. package/dist/ListboxButton-nx5kmw19.stx +17 -0
  39. package/dist/ListboxLabel-aky8a73n.stx +11 -0
  40. package/dist/ListboxOption-6h4hsdz5.stx +57 -0
  41. package/dist/ListboxOptions-2sacdtm5.stx +16 -0
  42. package/dist/Login-s2jkvd9y.stx +163 -0
  43. package/dist/Navigator-tqqdgdkw.stx +138 -0
  44. package/dist/Notification-sry8egea.stx +71 -0
  45. package/dist/NumberInput-zm93ddfw.stx +138 -0
  46. package/dist/Pagination-yza9v4qw.stx +183 -0
  47. package/dist/PasswordInput-4xnndp5y.stx +155 -0
  48. package/dist/PaymentMethods-jxba6kek.stx +113 -0
  49. package/dist/Popover-scbqatjq.stx +24 -0
  50. package/dist/PopoverButton-rbxhzj4k.stx +23 -0
  51. package/dist/PopoverPanel-9ng1rm6e.stx +23 -0
  52. package/dist/Portal-j111yvqp.stx +74 -0
  53. package/dist/Progress-gbmw3akr.stx +142 -0
  54. package/dist/Radio-1n9qamv1.stx +81 -0
  55. package/dist/RadioGroup-12dw2vhk.stx +13 -0
  56. package/dist/RadioGroupDescription-bjm3mdht.stx +11 -0
  57. package/dist/RadioGroupLabel-aky8a73n.stx +11 -0
  58. package/dist/RadioGroupOption-9pcxgwmd.stx +51 -0
  59. package/dist/SearchInput-g240e2nq.stx +63 -0
  60. package/dist/Select-p9f94b20.stx +89 -0
  61. package/dist/Sidebar-gwxw8q38.stx +288 -0
  62. package/dist/SidebarFooter-887kxret.stx +95 -0
  63. package/dist/SidebarHeader-qjeyx0hv.stx +100 -0
  64. package/dist/SidebarItem-4dpwtmb4.stx +118 -0
  65. package/dist/SidebarSection-kh92fk4m.stx +108 -0
  66. package/dist/Signup-3nxq5zag.stx +161 -0
  67. package/dist/Skeleton-g0nx1njm.stx +48 -0
  68. package/dist/Spinner-w4zj58xp.stx +109 -0
  69. package/dist/Stepper-rwc5actp.stx +17 -0
  70. package/dist/StepperStep-h0k657nm.stx +53 -0
  71. package/dist/SubscriptionCheckout-4cbxdg7w.stx +353 -0
  72. package/dist/Switch-kjq8qd2p.stx +74 -0
  73. package/dist/Table-59j9qwp0.stx +14 -0
  74. package/dist/TableBody-reka2gwv.stx +10 -0
  75. package/dist/TableCell-j7nh4sgr.stx +10 -0
  76. package/dist/TableHead-ecbrwpqm.stx +10 -0
  77. package/dist/TableHeader-jn2shwsv.stx +13 -0
  78. package/dist/TableRow-xz8rrxhc.stx +11 -0
  79. package/dist/Tabs-kb6ex6m7.stx +145 -0
  80. package/dist/Teleport-819cg9zm.stx +101 -0
  81. package/dist/TextInput-zx9kgr79.stx +146 -0
  82. package/dist/Textarea-5xvmrb0z.stx +119 -0
  83. package/dist/Tooltip-5ehqsy49.stx +76 -0
  84. package/dist/Transition-2j39s044.stx +174 -0
  85. package/dist/TwoFactorChallenge-2q4gg89m.stx +125 -0
  86. package/dist/Video-6aq4shh4.stx +45 -0
  87. package/dist/VirtualList-weh5qtm1.stx +83 -0
  88. package/dist/VirtualTable-59a1eskc.stx +113 -0
  89. package/dist/components/index.d.ts +4 -0
  90. package/dist/composables/index.d.ts +4 -0
  91. package/dist/composables/index.js +3 -0
  92. package/dist/composables/useCopyCode.d.ts +19 -0
  93. package/dist/composables/useDarkMode.d.ts +25 -0
  94. package/dist/composables/useSEO.d.ts +36 -0
  95. package/dist/composables/utilities/index.d.ts +5 -0
  96. package/dist/composables/utilities/useClickOutside.d.ts +28 -0
  97. package/dist/composables/utilities/useFocusTrap.d.ts +34 -0
  98. package/dist/composables/utilities/useKeyboard.d.ts +55 -0
  99. package/dist/composables/utilities/useLocalStorage.d.ts +33 -0
  100. package/dist/composables/utilities/useMediaQuery.d.ts +64 -0
  101. package/dist/index.d.ts +86 -0
  102. package/dist/index.js +19 -0
  103. package/dist/ui/accordion/index.d.ts +12 -0
  104. package/dist/ui/audio/index.d.ts +12 -0
  105. package/dist/ui/auth/index.d.ts +29 -0
  106. package/dist/ui/avatar/index.d.ts +11 -0
  107. package/dist/ui/badge/index.d.ts +9 -0
  108. package/dist/ui/breadcrumb/index.d.ts +13 -0
  109. package/dist/ui/button/index.d.ts +45 -0
  110. package/dist/ui/calendar/index.d.ts +13 -0
  111. package/dist/ui/card/index.d.ts +12 -0
  112. package/dist/ui/checkbox/index.d.ts +15 -0
  113. package/dist/ui/combobox/index.d.ts +35 -0
  114. package/dist/ui/command-palette/index.d.ts +13 -0
  115. package/dist/ui/dialog/index.d.ts +27 -0
  116. package/dist/ui/drawer/index.d.ts +8 -0
  117. package/dist/ui/dropdown/index.d.ts +25 -0
  118. package/dist/ui/form/index.d.ts +48 -0
  119. package/dist/ui/image/index.d.ts +21 -0
  120. package/dist/ui/input/index.d.ts +73 -0
  121. package/dist/ui/listbox/index.d.ts +34 -0
  122. package/dist/ui/navigator/index.d.ts +18 -0
  123. package/dist/ui/notification/index.d.ts +11 -0
  124. package/dist/ui/pagination/index.d.ts +10 -0
  125. package/dist/ui/payment/index.d.ts +53 -0
  126. package/dist/ui/popover/index.d.ts +19 -0
  127. package/dist/ui/portal/index.d.ts +5 -0
  128. package/dist/ui/progress/index.d.ts +11 -0
  129. package/dist/ui/radio/index.d.ts +14 -0
  130. package/dist/ui/radio-group/index.d.ts +26 -0
  131. package/dist/ui/select/index.d.ts +19 -0
  132. package/dist/ui/sidebar/index.d.ts +129 -0
  133. package/dist/ui/skeleton/index.d.ts +10 -0
  134. package/dist/ui/spinner/index.d.ts +8 -0
  135. package/dist/ui/stepper/index.d.ts +17 -0
  136. package/dist/ui/storage/index.d.ts +40 -0
  137. package/dist/ui/switch/index.d.ts +9 -0
  138. package/dist/ui/table/index.d.ts +29 -0
  139. package/dist/ui/tabs/index.d.ts +14 -0
  140. package/dist/ui/teleport/index.d.ts +6 -0
  141. package/dist/ui/textarea/index.d.ts +19 -0
  142. package/dist/ui/tooltip/index.d.ts +9 -0
  143. package/dist/ui/transition/index.d.ts +24 -0
  144. package/dist/ui/video/index.d.ts +15 -0
  145. package/dist/ui/virtual-list/index.d.ts +9 -0
  146. package/dist/ui/virtual-table/index.d.ts +19 -0
  147. package/dist/utils/accessibility.d.ts +285 -0
  148. package/dist/utils/animation/easing.d.ts +112 -0
  149. package/dist/utils/animation/helpers.d.ts +119 -0
  150. package/dist/utils/animation/index.d.ts +3 -0
  151. package/dist/utils/animation/keyframes.d.ts +334 -0
  152. package/dist/utils/error-handling.d.ts +182 -0
  153. package/dist/utils/highlighter.d.ts +27 -0
  154. package/dist/utils/highlighter.js +2 -0
  155. package/dist/utils/i18n.d.ts +245 -0
  156. package/dist/utils/keep-alive.d.ts +171 -0
  157. package/dist/utils/lazy-loading.d.ts +174 -0
  158. package/dist/utils/prop-validation.d.ts +106 -0
  159. package/dist/utils/theme.d.ts +234 -0
  160. package/package.json +69 -0
  161. package/src/components/CodeBlock.stx +57 -0
  162. package/src/components/Footer.stx +66 -0
  163. package/src/components/Hero.stx +25 -0
  164. package/src/components/Installation.stx +39 -0
  165. package/src/components/index.ts +4 -0
  166. package/src/composables/index.ts +4 -0
  167. package/src/composables/useCopyCode.ts +73 -0
  168. package/src/composables/useDarkMode.ts +100 -0
  169. package/src/composables/useSEO.ts +229 -0
  170. package/src/composables/utilities/index.ts +7 -0
  171. package/src/composables/utilities/useClickOutside.ts +72 -0
  172. package/src/composables/utilities/useFocusTrap.ts +159 -0
  173. package/src/composables/utilities/useKeyboard.ts +124 -0
  174. package/src/composables/utilities/useLocalStorage.ts +132 -0
  175. package/src/composables/utilities/useMediaQuery.ts +118 -0
  176. package/src/index.ts +99 -0
  177. package/src/stx.d.ts +4 -0
  178. package/src/ui/accordion/Accordion.stx +108 -0
  179. package/src/ui/accordion/index.ts +14 -0
  180. package/src/ui/audio/Audio.stx +50 -0
  181. package/src/ui/audio/README.md +42 -0
  182. package/src/ui/audio/index.ts +13 -0
  183. package/src/ui/auth/Login.stx +163 -0
  184. package/src/ui/auth/README.md +145 -0
  185. package/src/ui/auth/Signup.stx +161 -0
  186. package/src/ui/auth/TwoFactorChallenge.stx +125 -0
  187. package/src/ui/auth/index.ts +32 -0
  188. package/src/ui/avatar/Avatar.stx +88 -0
  189. package/src/ui/avatar/index.ts +12 -0
  190. package/src/ui/badge/Badge.stx +70 -0
  191. package/src/ui/badge/index.ts +10 -0
  192. package/src/ui/breadcrumb/Breadcrumb.stx +79 -0
  193. package/src/ui/breadcrumb/index.ts +15 -0
  194. package/src/ui/button/Button.stx +89 -0
  195. package/src/ui/button/README.md +100 -0
  196. package/src/ui/button/index.ts +85 -0
  197. package/src/ui/calendar/Calendar.stx +232 -0
  198. package/src/ui/calendar/README.md +254 -0
  199. package/src/ui/calendar/index.ts +14 -0
  200. package/src/ui/card/Card.stx +74 -0
  201. package/src/ui/card/index.ts +13 -0
  202. package/src/ui/checkbox/Checkbox.stx +92 -0
  203. package/src/ui/checkbox/index.ts +16 -0
  204. package/src/ui/combobox/Combobox.stx +13 -0
  205. package/src/ui/combobox/ComboboxButton.stx +13 -0
  206. package/src/ui/combobox/ComboboxInput.stx +25 -0
  207. package/src/ui/combobox/ComboboxOption.stx +52 -0
  208. package/src/ui/combobox/ComboboxOptions.stx +12 -0
  209. package/src/ui/combobox/README.md +154 -0
  210. package/src/ui/combobox/index.ts +40 -0
  211. package/src/ui/command-palette/CommandPalette.stx +39 -0
  212. package/src/ui/command-palette/CommandPaletteItem.stx +32 -0
  213. package/src/ui/command-palette/README.md +42 -0
  214. package/src/ui/command-palette/index.ts +15 -0
  215. package/src/ui/dialog/Dialog.stx +19 -0
  216. package/src/ui/dialog/DialogBackdrop.stx +9 -0
  217. package/src/ui/dialog/DialogDescription.stx +11 -0
  218. package/src/ui/dialog/DialogPanel.stx +16 -0
  219. package/src/ui/dialog/DialogTitle.stx +11 -0
  220. package/src/ui/dialog/README.md +133 -0
  221. package/src/ui/dialog/index.ts +32 -0
  222. package/src/ui/drawer/Drawer.stx +137 -0
  223. package/src/ui/drawer/README.md +110 -0
  224. package/src/ui/drawer/index.ts +9 -0
  225. package/src/ui/dropdown/Dropdown.stx +24 -0
  226. package/src/ui/dropdown/DropdownButton.stx +23 -0
  227. package/src/ui/dropdown/DropdownItem.stx +43 -0
  228. package/src/ui/dropdown/DropdownItems.stx +18 -0
  229. package/src/ui/dropdown/README.md +142 -0
  230. package/src/ui/dropdown/index.ts +29 -0
  231. package/src/ui/form/Form.stx +209 -0
  232. package/src/ui/form/index.ts +67 -0
  233. package/src/ui/heatmap/Heatmap.stx +315 -0
  234. package/src/ui/image/Image.stx +197 -0
  235. package/src/ui/image/README.md +171 -0
  236. package/src/ui/image/index.ts +22 -0
  237. package/src/ui/input/EmailInput.stx +41 -0
  238. package/src/ui/input/NumberInput.stx +138 -0
  239. package/src/ui/input/PasswordInput.stx +155 -0
  240. package/src/ui/input/SearchInput.stx +63 -0
  241. package/src/ui/input/TextInput.stx +146 -0
  242. package/src/ui/input/index.ts +180 -0
  243. package/src/ui/listbox/Listbox.stx +14 -0
  244. package/src/ui/listbox/ListboxButton.stx +17 -0
  245. package/src/ui/listbox/ListboxLabel.stx +11 -0
  246. package/src/ui/listbox/ListboxOption.stx +57 -0
  247. package/src/ui/listbox/ListboxOptions.stx +16 -0
  248. package/src/ui/listbox/README.md +143 -0
  249. package/src/ui/listbox/index.ts +39 -0
  250. package/src/ui/navigator/Navigator.stx +138 -0
  251. package/src/ui/navigator/README.md +352 -0
  252. package/src/ui/navigator/index.ts +20 -0
  253. package/src/ui/notification/Notification.stx +71 -0
  254. package/src/ui/notification/README.md +118 -0
  255. package/src/ui/notification/index.ts +12 -0
  256. package/src/ui/pagination/Pagination.stx +183 -0
  257. package/src/ui/pagination/index.ts +11 -0
  258. package/src/ui/payment/Checkout.stx +39 -0
  259. package/src/ui/payment/DefaultPaymentMethod.stx +62 -0
  260. package/src/ui/payment/PaymentMethods.stx +113 -0
  261. package/src/ui/payment/README.md +427 -0
  262. package/src/ui/payment/SubscriptionCheckout.stx +353 -0
  263. package/src/ui/payment/index.ts +59 -0
  264. package/src/ui/popover/Popover.stx +24 -0
  265. package/src/ui/popover/PopoverButton.stx +23 -0
  266. package/src/ui/popover/PopoverPanel.stx +23 -0
  267. package/src/ui/popover/README.md +95 -0
  268. package/src/ui/popover/index.ts +22 -0
  269. package/src/ui/portal/Portal.stx +74 -0
  270. package/src/ui/portal/index.ts +6 -0
  271. package/src/ui/progress/Progress.stx +142 -0
  272. package/src/ui/progress/index.ts +12 -0
  273. package/src/ui/radio/Radio.stx +81 -0
  274. package/src/ui/radio/index.ts +15 -0
  275. package/src/ui/radio-group/README.md +145 -0
  276. package/src/ui/radio-group/RadioGroup.stx +13 -0
  277. package/src/ui/radio-group/RadioGroupDescription.stx +11 -0
  278. package/src/ui/radio-group/RadioGroupLabel.stx +11 -0
  279. package/src/ui/radio-group/RadioGroupOption.stx +51 -0
  280. package/src/ui/radio-group/index.ts +30 -0
  281. package/src/ui/select/README.md +204 -0
  282. package/src/ui/select/Select.stx +89 -0
  283. package/src/ui/select/index.ts +21 -0
  284. package/src/ui/sidebar/Sidebar.stx +288 -0
  285. package/src/ui/sidebar/SidebarFooter.stx +95 -0
  286. package/src/ui/sidebar/SidebarHeader.stx +100 -0
  287. package/src/ui/sidebar/SidebarItem.stx +118 -0
  288. package/src/ui/sidebar/SidebarSection.stx +108 -0
  289. package/src/ui/sidebar/index.ts +175 -0
  290. package/src/ui/skeleton/Skeleton.stx +48 -0
  291. package/src/ui/skeleton/index.ts +11 -0
  292. package/src/ui/spinner/Spinner.stx +109 -0
  293. package/src/ui/spinner/index.ts +9 -0
  294. package/src/ui/stepper/README.md +36 -0
  295. package/src/ui/stepper/Stepper.stx +17 -0
  296. package/src/ui/stepper/StepperStep.stx +53 -0
  297. package/src/ui/stepper/index.ts +19 -0
  298. package/src/ui/storage/README.md +193 -0
  299. package/src/ui/storage/index.ts +347 -0
  300. package/src/ui/switch/README.md +133 -0
  301. package/src/ui/switch/Switch.stx +74 -0
  302. package/src/ui/switch/index.ts +10 -0
  303. package/src/ui/table/README.md +39 -0
  304. package/src/ui/table/Table.stx +14 -0
  305. package/src/ui/table/TableBody.stx +10 -0
  306. package/src/ui/table/TableCell.stx +10 -0
  307. package/src/ui/table/TableHead.stx +10 -0
  308. package/src/ui/table/TableHeader.stx +13 -0
  309. package/src/ui/table/TableRow.stx +11 -0
  310. package/src/ui/table/index.ts +35 -0
  311. package/src/ui/tabs/Tabs.stx +145 -0
  312. package/src/ui/tabs/index.ts +16 -0
  313. package/src/ui/teleport/Teleport.stx +101 -0
  314. package/src/ui/teleport/index.ts +7 -0
  315. package/src/ui/textarea/Textarea.stx +119 -0
  316. package/src/ui/textarea/index.ts +20 -0
  317. package/src/ui/tooltip/Tooltip.stx +76 -0
  318. package/src/ui/tooltip/index.ts +10 -0
  319. package/src/ui/transition/README.md +30 -0
  320. package/src/ui/transition/Transition.stx +174 -0
  321. package/src/ui/transition/index.ts +25 -0
  322. package/src/ui/video/README.md +48 -0
  323. package/src/ui/video/Video.stx +45 -0
  324. package/src/ui/video/index.ts +16 -0
  325. package/src/ui/virtual-list/VirtualList.stx +83 -0
  326. package/src/ui/virtual-list/index.ts +10 -0
  327. package/src/ui/virtual-table/VirtualTable.stx +113 -0
  328. package/src/ui/virtual-table/index.ts +21 -0
  329. package/src/utils/PROP_VALIDATION.md +458 -0
  330. package/src/utils/THEME_SYSTEM.md +702 -0
  331. package/src/utils/accessibility.ts +841 -0
  332. package/src/utils/animation/easing.ts +368 -0
  333. package/src/utils/animation/helpers.ts +223 -0
  334. package/src/utils/animation/index.ts +5 -0
  335. package/src/utils/animation/keyframes.ts +230 -0
  336. package/src/utils/error-handling.ts +473 -0
  337. package/src/utils/highlighter.ts +93 -0
  338. package/src/utils/i18n.ts +634 -0
  339. package/src/utils/keep-alive.ts +818 -0
  340. package/src/utils/lazy-loading.ts +530 -0
  341. package/src/utils/prop-validation.ts +478 -0
  342. package/src/utils/theme.ts +626 -0
@@ -0,0 +1,24 @@
1
+ <script>
2
+ export const className = $props.className || ''
3
+ export const as = $props.as || 'div'
4
+ export let isOpen = false
5
+
6
+ export function toggle() {
7
+ isOpen = !isOpen
8
+ }
9
+
10
+ export function close() {
11
+ isOpen = false
12
+ }
13
+
14
+ export function open() {
15
+ isOpen = true
16
+ }
17
+
18
+ const baseClasses = 'relative inline-block text-left'
19
+ const dropdownClasses = `${baseClasses} ${className}`.trim()
20
+ </script>
21
+
22
+ <component :is="{{ as }}" class="{{ dropdownClasses }}">
23
+ <slot :isOpen="isOpen" :toggle="toggle" :close="close" :open="open" />
24
+ </component>
@@ -0,0 +1,23 @@
1
+ <script>
2
+ export const className = $props.className || ''
3
+ export const as = $props.as || 'button'
4
+ export const disabled = $props.disabled || false
5
+ export const onClick = $props.onClick
6
+
7
+ const baseClasses = 'inline-flex justify-center items-center rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75'
8
+ const buttonClasses = `${baseClasses} ${className}`.trim()
9
+
10
+ export function handleClick(e) {
11
+ if (disabled) return
12
+ onClick?.(e)
13
+ }
14
+ </script>
15
+
16
+ <component
17
+ :is="{{ as }}"
18
+ class="{{ buttonClasses }}"
19
+ @if(disabled)disabled aria-disabled="true"@endif
20
+ @click="handleClick"
21
+ >
22
+ <slot />
23
+ </component>
@@ -0,0 +1,43 @@
1
+ <script>
2
+ export const className = $props.className || ''
3
+ export const as = $props.as || 'div'
4
+ export const disabled = $props.disabled || false
5
+ export const onClick = $props.onClick
6
+
7
+ export let active = false
8
+
9
+ const baseClasses = 'group flex w-full items-center px-2 py-2 text-sm transition-colors'
10
+ const stateClasses = active
11
+ ? 'bg-blue-500 text-white dark:bg-blue-600'
12
+ : 'text-neutral-900 dark:text-neutral-100'
13
+ const disabledClasses = disabled
14
+ ? 'opacity-50 cursor-not-allowed'
15
+ : 'cursor-pointer'
16
+
17
+ const itemClasses = `${baseClasses} ${stateClasses} ${disabledClasses} ${className}`.trim()
18
+
19
+ export function handleClick(e) {
20
+ if (disabled) return
21
+ onClick?.(e)
22
+ }
23
+
24
+ export function handleMouseEnter() {
25
+ if (!disabled) active = true
26
+ }
27
+
28
+ export function handleMouseLeave() {
29
+ active = false
30
+ }
31
+ </script>
32
+
33
+ <component
34
+ :is="{{ as }}"
35
+ class="{{ itemClasses }}"
36
+ role="menuitem"
37
+ @if(disabled)aria-disabled="true"@endif
38
+ @click="handleClick"
39
+ @mouseenter="handleMouseEnter"
40
+ @mouseleave="handleMouseLeave"
41
+ >
42
+ <slot :active="active" :disabled="disabled" />
43
+ </component>
@@ -0,0 +1,18 @@
1
+ <script>
2
+ export const className = $props.className || ''
3
+ export const as = $props.as || 'div'
4
+ export const static_ = $props.static || false
5
+
6
+ const baseClasses = 'absolute z-10 mt-2 origin-top-right divide-y divide-neutral-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-neutral-800 dark:divide-neutral-700 dark:ring-neutral-600'
7
+ const itemsClasses = `${baseClasses} ${className}`.trim()
8
+ </script>
9
+
10
+ <component
11
+ :is="{{ as }}"
12
+ class="{{ itemsClasses }}"
13
+ role="menu"
14
+ aria-orientation="vertical"
15
+ tabindex="-1"
16
+ >
17
+ <slot />
18
+ </component>
@@ -0,0 +1,41 @@
1
+ <script>
2
+ export const value = $props.value || ''
3
+ export const onChange = $props.onChange || (() => {})
4
+ export const placeholder = $props.placeholder || 'Enter your email'
5
+ export const disabled = $props.disabled || false
6
+ export const readonly = $props.readonly || false
7
+ export const size = $props.size || 'md'
8
+ export const error = $props.error || false
9
+ export const label = $props.label || ''
10
+ export const helperText = $props.helperText || ''
11
+ export const required = $props.required || false
12
+ export const className = $props.className || ''
13
+
14
+ // Email icon SVG
15
+ const emailIcon = '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>'
16
+ </script>
17
+
18
+ @component('TextInput', {
19
+ value,
20
+ onChange,
21
+ placeholder,
22
+ disabled,
23
+ readonly,
24
+ size,
25
+ error,
26
+ label,
27
+ helperText,
28
+ required,
29
+ leftIcon: emailIcon,
30
+ clearable: true,
31
+ className,
32
+ inputClassName: 'font-mono'
33
+ })
34
+ @endcomponent
35
+
36
+ <script>
37
+ // Add additional email validation logic if needed
38
+ function validateEmail(email) {
39
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
40
+ }
41
+ </script>
@@ -0,0 +1,66 @@
1
+ <script>
2
+ export const author = $props.author || '@chrisbbreuer'
3
+ export const authorUrl = $props.authorUrl || 'https://github.com/chrisbbreuer'
4
+ export const project = $props.project || '@stacksjs'
5
+ export const projectUrl = $props.projectUrl || 'https://github.com/stacksjs/stacks'
6
+ export const twitterUrl = $props.twitterUrl || 'https://twitter.com/chrisbbreuer'
7
+ export const sponsorUrl = $props.sponsorUrl || 'https://github.com/sponsors/chrisbbreuer'
8
+ export const coffeeUrl = $props.coffeeUrl || 'https://www.buymeacoffee.com/xlbd'
9
+
10
+ export const currentYear = new Date().getFullYear()
11
+ </script>
12
+
13
+ <footer class="flex items-center justify-center mt-16 w-full pb-4 text-neutral-600 dark:text-neutral-400 opacity-60">
14
+ <div class="flex flex-col items-center justify-center">
15
+ <p class="text-sm">
16
+ With 💙 by
17
+ <a href="{{ authorUrl }}" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors">
18
+ {{ author }}
19
+ </a>
20
+ &
21
+ <a href="{{ projectUrl }}" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors">
22
+ {{ project }}
23
+ </a>
24
+ <span> © {{ currentYear }}</span>
25
+ </p>
26
+
27
+ <div class="mt-2 flex flex-col items-center sm:flex-row space-x-1 space-y-2 sm:space-y-0">
28
+ <div class="flex items-center space-x-1">
29
+ <svg class="w-4 h-4 text-blue-500" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
30
+ <path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84" />
31
+ </svg>
32
+ <span class="text-sm">
33
+ <a href="{{ twitterUrl }}" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors">
34
+ Follow me on Twitter
35
+ </a>
36
+ </span>
37
+ </div>
38
+
39
+ <span class="hidden px-2 text-neutral-400 sm:block">|</span>
40
+
41
+ <div class="flex items-center space-x-1">
42
+ <svg class="w-4 h-4 text-blue-500" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
43
+ <path d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.067 2.017.144l.228.031c.267.04.533.088.798.145.392.085.895.113 1.07.542.055.137.08.288.111.431l.319 1.484a.237.237 0 01-.199.284h-.003c-.037.006-.075.01-.112.015a36.704 36.704 0 01-4.743.295 37.059 37.059 0 01-4.699-.304c-.14-.017-.293-.042-.417-.06-.326-.048-.649-.108-.973-.161-.393-.065-.768-.032-1.123.161-.29.16-.527.404-.675.701-.154.316-.199.66-.267 1-.069.34-.176.707-.135 1.056.087.753.613 1.365 1.37 1.502a39.69 39.69 0 0011.343.376.483.483 0 01.535.53l-.071.697-1.018 9.907c-.041.41-.047.832-.125 1.237-.122.637-.553 1.028-1.182 1.171-.577.131-1.165.2-1.756.205-.656.004-1.31-.025-1.966-.022-.699.004-1.556-.06-2.095-.58-.475-.458-.54-1.174-.605-1.793l-.731-7.013-.322-3.094c-.037-.351-.286-.695-.678-.678-.336.015-.718.3-.678.679l.228 2.185.949 9.112c.147 1.344 1.174 2.068 2.446 2.272.742.12 1.503.144 2.257.156.966.016 1.942.053 2.892-.122 1.408-.258 2.465-1.198 2.616-2.657.34-3.332.683-6.663 1.024-9.995l.215-2.087a.484.484 0 01.39-.426c.402-.078.787-.212 1.074-.518.455-.488.546-1.124.385-1.766zm-1.478.772c-.145.137-.363.201-.578.233-2.416.359-4.866.54-7.308.46-1.748-.06-3.477-.254-5.207-.498-.17-.024-.353-.055-.47-.18-.22-.236-.111-.71-.054-.995.052-.26.152-.609.463-.646.484-.057 1.046.148 1.526.22.577.088 1.156.16 1.737.212 2.48.226 5.002.19 7.472-.14.45-.06.899-.13 1.345-.21.399-.072.84-.206 1.08.206.166.281.188.657.162.974a.544.544 0 01-.169.364zm-6.159 3.9c-.862.37-1.84.788-3.109.788a5.884 5.884 0 01-1.569-.217l.877 9.004c.065.78.717 1.38 1.5 1.38 0 0 1.243.065 1.658.065.447 0 1.786-.065 1.786-.065.783 0 1.434-.6 1.499-1.38l.94-9.95a3.996 3.996 0 00-1.322-.238c-.826 0-1.491.284-2.26.613z" />
44
+ </svg>
45
+ <span class="text-sm">
46
+ <a href="{{ coffeeUrl }}" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors">
47
+ Buy me a coffee
48
+ </a>
49
+ </span>
50
+ </div>
51
+
52
+ <span class="hidden px-2 text-neutral-400 sm:block">|</span>
53
+
54
+ <div class="flex items-center space-x-1">
55
+ <svg class="w-4 h-4 text-blue-500" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
56
+ <path d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z" />
57
+ </svg>
58
+ <span class="text-sm">
59
+ <a href="{{ sponsorUrl }}" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors">
60
+ Sponsor me on GitHub
61
+ </a>
62
+ </span>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </footer>
@@ -0,0 +1,209 @@
1
+ <script>
2
+ export const action = $props.action || ''
3
+ export const method = $props.method || 'POST'
4
+ export const validationSchema = $props.validationSchema || {}
5
+ export const initialValues = $props.initialValues || {}
6
+ export const validateOnChange = $props.validateOnChange ?? true
7
+ export const validateOnBlur = $props.validateOnBlur ?? true
8
+ export const onSubmit = $props.onSubmit
9
+ export const onError = $props.onError
10
+ export const className = $props.className || ''
11
+
12
+ let values = { ...initialValues }
13
+ let errors = {}
14
+ let touched = {}
15
+ let isSubmitting = false
16
+ let isValidating = false
17
+ let submitCount = 0
18
+
19
+ // Validate a single field
20
+ function validateField(name, value) {
21
+ const fieldSchema = validationSchema[name]
22
+ if (!fieldSchema)
23
+ return null
24
+
25
+ // Required validation
26
+ if (fieldSchema.required && !value) {
27
+ return fieldSchema.requiredMessage || `${name} is required`
28
+ }
29
+
30
+ // Min length validation
31
+ if (fieldSchema.minLength && value.length < fieldSchema.minLength) {
32
+ return fieldSchema.minLengthMessage || `${name} must be at least ${fieldSchema.minLength} characters`
33
+ }
34
+
35
+ // Max length validation
36
+ if (fieldSchema.maxLength && value.length > fieldSchema.maxLength) {
37
+ return fieldSchema.maxLengthMessage || `${name} must be at most ${fieldSchema.maxLength} characters`
38
+ }
39
+
40
+ // Pattern validation
41
+ if (fieldSchema.pattern && !fieldSchema.pattern.test(value)) {
42
+ return fieldSchema.patternMessage || `${name} is invalid`
43
+ }
44
+
45
+ // Custom validation
46
+ if (fieldSchema.validate && typeof fieldSchema.validate === 'function') {
47
+ return fieldSchema.validate(value, values)
48
+ }
49
+
50
+ return null
51
+ }
52
+
53
+ // Validate all fields
54
+ function validateForm() {
55
+ const newErrors = {}
56
+ let isValid = true
57
+
58
+ Object.keys(validationSchema).forEach((name) => {
59
+ const error = validateField(name, values[name])
60
+ if (error) {
61
+ newErrors[name] = error
62
+ isValid = false
63
+ }
64
+ })
65
+
66
+ errors = newErrors
67
+ return isValid
68
+ }
69
+
70
+ // Handle field change
71
+ function handleChange(event) {
72
+ const { name, value } = event.target
73
+ values[name] = value
74
+
75
+ if (validateOnChange && touched[name]) {
76
+ const error = validateField(name, value)
77
+ if (error) {
78
+ errors[name] = error
79
+ }
80
+ else {
81
+ delete errors[name]
82
+ }
83
+ }
84
+ }
85
+
86
+ // Handle field blur
87
+ function handleBlur(event) {
88
+ const { name } = event.target
89
+ touched[name] = true
90
+
91
+ if (validateOnBlur) {
92
+ const error = validateField(name, values[name])
93
+ if (error) {
94
+ errors[name] = error
95
+ }
96
+ else {
97
+ delete errors[name]
98
+ }
99
+ }
100
+ }
101
+
102
+ // Handle form submission
103
+ async function handleSubmit(event) {
104
+ event.preventDefault()
105
+ submitCount++
106
+
107
+ // Mark all fields as touched
108
+ Object.keys(validationSchema).forEach((name) => {
109
+ touched[name] = true
110
+ })
111
+
112
+ // Validate
113
+ isValidating = true
114
+ const isValid = validateForm()
115
+ isValidating = false
116
+
117
+ if (!isValid) {
118
+ if (onError) {
119
+ onError(errors)
120
+ }
121
+ return
122
+ }
123
+
124
+ // Submit
125
+ isSubmitting = true
126
+
127
+ try {
128
+ if (onSubmit) {
129
+ await onSubmit(values, { setErrors, setValues, resetForm })
130
+ }
131
+ }
132
+ catch (error) {
133
+ if (onError) {
134
+ onError(error)
135
+ }
136
+ }
137
+ finally {
138
+ isSubmitting = false
139
+ }
140
+ }
141
+
142
+ // Helper functions
143
+ function setErrors(newErrors) {
144
+ errors = { ...errors, ...newErrors }
145
+ }
146
+
147
+ function setValues(newValues) {
148
+ values = { ...values, ...newValues }
149
+ }
150
+
151
+ function resetForm() {
152
+ values = { ...initialValues }
153
+ errors = {}
154
+ touched = {}
155
+ isSubmitting = false
156
+ submitCount = 0
157
+ }
158
+
159
+ function getFieldProps(name) {
160
+ return {
161
+ name,
162
+ value: values[name] || '',
163
+ error: errors[name],
164
+ touched: touched[name],
165
+ onChange: handleChange,
166
+ onBlur: handleBlur,
167
+ }
168
+ }
169
+
170
+ export const formClasses = `space-y-6 ${className}`.trim()
171
+ export const errorClasses = `mt-1 text-sm text-red-600 dark:text-red-400`.trim()
172
+ </script>
173
+
174
+ <form
175
+ class="{{ formClasses }}"
176
+ @if(action)action="{{ action }}"@endif
177
+ method="{{ method }}"
178
+ onsubmit="handleSubmit(event)"
179
+ novalidate
180
+ >
181
+ <slot
182
+ values="{{ values }}"
183
+ errors="{{ errors }}"
184
+ touched="{{ touched }}"
185
+ isSubmitting="{{ isSubmitting }}"
186
+ isValidating="{{ isValidating }}"
187
+ submitCount="{{ submitCount }}"
188
+ getFieldProps="{{ getFieldProps }}"
189
+ setErrors="{{ setErrors }}"
190
+ setValues="{{ setValues }}"
191
+ resetForm="{{ resetForm }}"
192
+ />
193
+ </form>
194
+
195
+ <script>
196
+ // Export context for child components
197
+ if (typeof window !== 'undefined') {
198
+ window.__formContext = {
199
+ values,
200
+ errors,
201
+ touched,
202
+ isSubmitting,
203
+ isValidating,
204
+ getFieldProps,
205
+ handleChange,
206
+ handleBlur,
207
+ }
208
+ }
209
+ </script>
@@ -0,0 +1,25 @@
1
+ <script>
2
+ export const title = $props.title || 'Component'
3
+ export const description = $props.description || 'A modern component built with STX'
4
+ export const githubUrl = $props.githubUrl || '#'
5
+ </script>
6
+
7
+ <div class="flex flex-col items-center gap-3">
8
+ <h1 class="text-5xl font-bold mb-3 -mt-5 bg-gradient-to-r from-blue-500 to-purple-600 bg-clip-text text-transparent">
9
+ {{ title }}
10
+ </h1>
11
+ <p class="mb-3 mt-0 text-lg text-neutral-600 dark:text-neutral-400">
12
+ {{ description }}
13
+ </p>
14
+ <div class="flex gap-2">
15
+ <slot name="demo" />
16
+ <a
17
+ href="{{ githubUrl }}"
18
+ target="_blank"
19
+ rel="noopener noreferrer"
20
+ class="inline-flex items-center justify-center h-10 px-8 rounded-md text-sm font-semibold bg-gradient-to-b from-white to-neutral-100 dark:from-neutral-800 dark:to-neutral-900 text-neutral-900 dark:text-white shadow-md hover:shadow-lg transition-all duration-200"
21
+ >
22
+ GitHub
23
+ </a>
24
+ </div>
25
+ </div>
@@ -0,0 +1,197 @@
1
+ <script>
2
+ export const src = $props.src || ''
3
+ export const srcSet = $props.srcSet || ''
4
+ export const sizes = $props.sizes || ''
5
+ export const webpSrc = $props.webpSrc || ''
6
+ export const webpSrcSet = $props.webpSrcSet || ''
7
+ export const alt = $props.alt || ''
8
+ export const width = $props.width
9
+ export const height = $props.height
10
+ export const lazy = $props.lazy ?? true
11
+ export const placeholder = $props.placeholder || 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300"%3E%3Crect fill="%23e5e7eb" width="400" height="300"/%3E%3C/svg%3E'
12
+ export const blurDataURL = $props.blurDataURL || ''
13
+ export const onLoad = $props.onLoad
14
+ export const onError = $props.onError
15
+ export const aspectRatio = $props.aspectRatio
16
+ export const objectFit = $props.objectFit || 'cover'
17
+ export const rounded = $props.rounded ?? false
18
+ export const zoomOnHover = $props.zoomOnHover || false
19
+ export const className = $props.className || ''
20
+
21
+ // Has WebP support
22
+ export const hasWebP = webpSrc || webpSrcSet
23
+
24
+ // Object fit classes
25
+ const objectFitClasses = {
26
+ contain: 'object-contain',
27
+ cover: 'object-cover',
28
+ fill: 'object-fill',
29
+ none: 'object-none',
30
+ 'scale-down': 'object-scale-down',
31
+ }
32
+
33
+ // Rounded classes
34
+ const roundedClass = rounded === true ? 'rounded-lg' : rounded === 'full' ? 'rounded-full' : rounded ? `rounded-${rounded}` : ''
35
+
36
+ const zoomClasses = zoomOnHover ? 'transform hover:scale-110 transition-transform duration-300 ease-in-out cursor-zoom-in' : ''
37
+
38
+ export const containerClasses = `relative overflow-hidden ${roundedClass} ${className}`.trim()
39
+ export const imageClasses = `w-full h-full transition-opacity duration-300 ${objectFitClasses[objectFit]} ${zoomClasses}`.trim()
40
+ export const placeholderClasses = `absolute inset-0 bg-gray-200 dark:bg-gray-700 ${roundedClass}`.trim()
41
+ export const blurPlaceholderClasses = `absolute inset-0 ${roundedClass} filter blur-lg scale-110`.trim()
42
+
43
+ // Generate aspect ratio padding if provided
44
+ export const paddingBottom = aspectRatio ? `${(1 / aspectRatio) * 100}%` : undefined
45
+ </script>
46
+
47
+ <div class="{{ containerClasses }}" style="{{ paddingBottom ? `padding-bottom: ${paddingBottom}` : '' }}">
48
+ @if(blurDataURL)
49
+ <!-- Blur placeholder -->
50
+ <img
51
+ src="{{ blurDataURL }}"
52
+ alt=""
53
+ class="{{ blurPlaceholderClasses }}"
54
+ aria-hidden="true"
55
+ />
56
+ @endif
57
+
58
+ @if(hasWebP)
59
+ <!-- Picture element with WebP support -->
60
+ <picture>
61
+ @if(webpSrcSet || webpSrc)
62
+ <source
63
+ type="image/webp"
64
+ @if(webpSrcSet)srcset="{{ webpSrcSet }}"@endif
65
+ @if(webpSrc && !webpSrcSet)srcset="{{ webpSrc }}"@endif
66
+ @if(sizes)sizes="{{ sizes }}"@endif
67
+ />
68
+ @endif
69
+
70
+ @if(lazy)
71
+ <img
72
+ src="{{ placeholder }}"
73
+ data-src="{{ src }}"
74
+ @if(srcSet)data-srcset="{{ srcSet }}"@endif
75
+ @if(sizes)sizes="{{ sizes }}"@endif
76
+ alt="{{ alt }}"
77
+ class="{{ imageClasses }} lazy-image opacity-0"
78
+ width="{{ width }}"
79
+ height="{{ height }}"
80
+ loading="lazy"
81
+ onload="handleImageLoad(event)"
82
+ onerror="handleImageError(event)"
83
+ />
84
+ @else
85
+ <img
86
+ src="{{ src }}"
87
+ @if(srcSet)srcset="{{ srcSet }}"@endif
88
+ @if(sizes)sizes="{{ sizes }}"@endif
89
+ alt="{{ alt }}"
90
+ class="{{ imageClasses }}"
91
+ width="{{ width }}"
92
+ height="{{ height }}"
93
+ onload="handleImageLoad(event)"
94
+ onerror="handleImageError(event)"
95
+ />
96
+ @endif
97
+ </picture>
98
+ @else
99
+ <!-- Regular img element -->
100
+ @if(lazy)
101
+ <img
102
+ src="{{ placeholder }}"
103
+ data-src="{{ src }}"
104
+ @if(srcSet)data-srcset="{{ srcSet }}"@endif
105
+ @if(sizes)sizes="{{ sizes }}"@endif
106
+ alt="{{ alt }}"
107
+ class="{{ imageClasses }} lazy-image opacity-0"
108
+ width="{{ width }}"
109
+ height="{{ height }}"
110
+ loading="lazy"
111
+ onload="handleImageLoad(event)"
112
+ onerror="handleImageError(event)"
113
+ />
114
+ @else
115
+ <img
116
+ src="{{ src }}"
117
+ @if(srcSet)srcset="{{ srcSet }}"@endif
118
+ @if(sizes)sizes="{{ sizes }}"@endif
119
+ alt="{{ alt }}"
120
+ class="{{ imageClasses }}"
121
+ width="{{ width }}"
122
+ height="{{ height }}"
123
+ onload="handleImageLoad(event)"
124
+ onerror="handleImageError(event)"
125
+ />
126
+ @endif
127
+ @endif
128
+
129
+ <!-- Loading skeleton -->
130
+ <div class="{{ placeholderClasses }} skeleton" style="display: none;">
131
+ <div class="animate-pulse w-full h-full bg-gray-300 dark:bg-gray-600"></div>
132
+ </div>
133
+ </div>
134
+
135
+ <script>
136
+ // Intersection Observer for lazy loading
137
+ if (lazy && typeof window !== 'undefined' && 'IntersectionObserver' in window) {
138
+ const lazyImages = document.querySelectorAll('.lazy-image')
139
+
140
+ const imageObserver = new IntersectionObserver((entries, observer) => {
141
+ entries.forEach(entry => {
142
+ if (entry.isIntersecting) {
143
+ const img = entry.target
144
+ const realSrc = img.getAttribute('data-src')
145
+ const realSrcSet = img.getAttribute('data-srcset')
146
+
147
+ if (realSrc) {
148
+ // Load the real image
149
+ const tempImg = new Image()
150
+ tempImg.onload = () => {
151
+ img.src = realSrc
152
+ if (realSrcSet) {
153
+ img.srcset = realSrcSet
154
+ }
155
+ img.classList.remove('opacity-0')
156
+ img.classList.add('opacity-100')
157
+ observer.unobserve(img)
158
+ }
159
+ tempImg.onerror = () => {
160
+ handleImageError({ target: img })
161
+ observer.unobserve(img)
162
+ }
163
+ tempImg.src = realSrc
164
+ if (realSrcSet) {
165
+ tempImg.srcset = realSrcSet
166
+ }
167
+ }
168
+ }
169
+ })
170
+ }, {
171
+ rootMargin: '50px', // Start loading 50px before entering viewport
172
+ })
173
+
174
+ lazyImages.forEach(img => imageObserver.observe(img))
175
+ }
176
+
177
+ function handleImageLoad(event) {
178
+ const img = event.target
179
+ img.classList.remove('opacity-0')
180
+ img.classList.add('opacity-100')
181
+
182
+ if (onLoad) {
183
+ onLoad(event)
184
+ }
185
+ }
186
+
187
+ function handleImageError(event) {
188
+ const img = event.target
189
+
190
+ // Show a broken image placeholder
191
+ img.src = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300"%3E%3Crect fill="%23e5e7eb" width="400" height="300"/%3E%3Ctext x="50%25" y="50%25" fill="%239ca3af" font-size="16" text-anchor="middle" dominant-baseline="middle"%3EImage not found%3C/text%3E%3C/svg%3E'
192
+
193
+ if (onError) {
194
+ onError(event)
195
+ }
196
+ }
197
+ </script>