frutjam 2.3.0-beta.0 → 2.3.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 (273) hide show
  1. package/LICENSE +1 -1
  2. package/README.github.md +382 -0
  3. package/README.md +90 -42
  4. package/README.npm.md +124 -0
  5. package/dist/base/reboot/index.js +5 -0
  6. package/dist/base/reboot/object.js +1 -0
  7. package/dist/base/tokens/index.js +5 -0
  8. package/dist/base/tokens/object.js +1 -0
  9. package/dist/base.css +62 -0
  10. package/dist/base.css.d.ts +1 -0
  11. package/dist/components/accordion/index.js +7 -0
  12. package/dist/components/accordion/object.js +1 -0
  13. package/dist/components/accordion.css +2 -0
  14. package/dist/components/accordion.css.d.ts +1 -0
  15. package/dist/components/alert/index.js +7 -0
  16. package/dist/components/alert/object.js +1 -0
  17. package/dist/components/alert.css +2 -0
  18. package/dist/components/alert.css.d.ts +1 -0
  19. package/dist/components/avatar/index.js +7 -0
  20. package/dist/components/avatar/object.js +1 -0
  21. package/dist/components/avatar.css +2 -0
  22. package/dist/components/avatar.css.d.ts +1 -0
  23. package/dist/components/badge/index.js +7 -0
  24. package/dist/components/badge/object.js +1 -0
  25. package/dist/components/badge.css +2 -0
  26. package/dist/components/badge.css.d.ts +1 -0
  27. package/dist/components/breadcrumb/index.js +7 -0
  28. package/dist/components/breadcrumb/object.js +1 -0
  29. package/dist/components/breadcrumb.css +2 -0
  30. package/dist/components/breadcrumb.css.d.ts +1 -0
  31. package/dist/components/button/index.js +7 -0
  32. package/dist/components/button/object.js +1 -0
  33. package/dist/components/button.css +2 -0
  34. package/dist/components/button.css.d.ts +1 -0
  35. package/dist/components/card/index.js +7 -0
  36. package/dist/components/card/object.js +1 -0
  37. package/dist/components/card.css +2 -0
  38. package/dist/components/card.css.d.ts +1 -0
  39. package/dist/components/carousel/index.js +7 -0
  40. package/dist/components/carousel/object.js +1 -0
  41. package/dist/components/carousel.css +2 -0
  42. package/dist/components/carousel.css.d.ts +1 -0
  43. package/dist/components/chat/index.js +7 -0
  44. package/dist/components/chat/object.js +1 -0
  45. package/dist/components/chat.css +2 -0
  46. package/dist/components/chat.css.d.ts +1 -0
  47. package/dist/components/checkbox/index.js +7 -0
  48. package/dist/components/checkbox/object.js +1 -0
  49. package/dist/components/checkbox.css +2 -0
  50. package/dist/components/checkbox.css.d.ts +1 -0
  51. package/dist/components/collapsible/index.js +7 -0
  52. package/dist/components/collapsible/object.js +1 -0
  53. package/dist/components/collapsible.css +2 -0
  54. package/dist/components/collapsible.css.d.ts +1 -0
  55. package/dist/components/combobox/index.js +7 -0
  56. package/dist/components/combobox/object.js +1 -0
  57. package/dist/components/combobox.css +2 -0
  58. package/dist/components/combobox.css.d.ts +1 -0
  59. package/dist/components/countdown/index.js +7 -0
  60. package/dist/components/countdown/object.js +1 -0
  61. package/dist/components/countdown.css +2 -0
  62. package/dist/components/countdown.css.d.ts +1 -0
  63. package/dist/components/diff/index.js +7 -0
  64. package/dist/components/diff/object.js +1 -0
  65. package/dist/components/diff.css +2 -0
  66. package/dist/components/diff.css.d.ts +1 -0
  67. package/dist/components/divider/index.js +7 -0
  68. package/dist/components/divider/object.js +1 -0
  69. package/dist/components/divider.css +2 -0
  70. package/dist/components/divider.css.d.ts +1 -0
  71. package/dist/components/drawer/index.js +7 -0
  72. package/dist/components/drawer/object.js +1 -0
  73. package/dist/components/drawer.css +2 -0
  74. package/dist/components/drawer.css.d.ts +1 -0
  75. package/dist/components/footer/index.js +7 -0
  76. package/dist/components/footer/object.js +1 -0
  77. package/dist/components/footer.css +2 -0
  78. package/dist/components/footer.css.d.ts +1 -0
  79. package/dist/components/header/index.js +7 -0
  80. package/dist/components/header/object.js +1 -0
  81. package/dist/components/header.css +2 -0
  82. package/dist/components/header.css.d.ts +1 -0
  83. package/dist/components/hero/index.js +7 -0
  84. package/dist/components/hero/object.js +1 -0
  85. package/dist/components/hero.css +2 -0
  86. package/dist/components/hero.css.d.ts +1 -0
  87. package/dist/components/indicator/index.js +7 -0
  88. package/dist/components/indicator/object.js +1 -0
  89. package/dist/components/indicator.css +2 -0
  90. package/dist/components/indicator.css.d.ts +1 -0
  91. package/dist/components/input/index.js +7 -0
  92. package/dist/components/input/object.js +1 -0
  93. package/dist/components/input.css +2 -0
  94. package/dist/components/input.css.d.ts +1 -0
  95. package/dist/components/join/index.js +7 -0
  96. package/dist/components/join/object.js +1 -0
  97. package/dist/components/join.css +2 -0
  98. package/dist/components/join.css.d.ts +1 -0
  99. package/dist/components/kbd/index.js +7 -0
  100. package/dist/components/kbd/object.js +1 -0
  101. package/dist/components/kbd.css +2 -0
  102. package/dist/components/kbd.css.d.ts +1 -0
  103. package/dist/components/link/index.js +7 -0
  104. package/dist/components/link/object.js +1 -0
  105. package/dist/components/link.css +2 -0
  106. package/dist/components/link.css.d.ts +1 -0
  107. package/dist/components/loading/index.js +7 -0
  108. package/dist/components/loading/object.js +1 -0
  109. package/dist/components/loading.css +2 -0
  110. package/dist/components/loading.css.d.ts +1 -0
  111. package/dist/components/marquee/index.js +7 -0
  112. package/dist/components/marquee/object.js +1 -0
  113. package/dist/components/marquee.css +2 -0
  114. package/dist/components/marquee.css.d.ts +1 -0
  115. package/dist/components/mask/index.js +7 -0
  116. package/dist/components/mask/object.js +1 -0
  117. package/dist/components/mask.css +2 -0
  118. package/dist/components/mask.css.d.ts +1 -0
  119. package/dist/components/menu/index.js +7 -0
  120. package/dist/components/menu/object.js +1 -0
  121. package/dist/components/menu.css +2 -0
  122. package/dist/components/menu.css.d.ts +1 -0
  123. package/dist/components/modal/index.js +7 -0
  124. package/dist/components/modal/object.js +1 -0
  125. package/dist/components/modal.css +2 -0
  126. package/dist/components/modal.css.d.ts +1 -0
  127. package/dist/components/navbar/index.js +7 -0
  128. package/dist/components/navbar/object.js +1 -0
  129. package/dist/components/navbar.css +2 -0
  130. package/dist/components/navbar.css.d.ts +1 -0
  131. package/dist/components/popover/index.js +7 -0
  132. package/dist/components/popover/object.js +1 -0
  133. package/dist/components/popover.css +2 -0
  134. package/dist/components/popover.css.d.ts +1 -0
  135. package/dist/components/progress/index.js +7 -0
  136. package/dist/components/progress/object.js +1 -0
  137. package/dist/components/progress.css +2 -0
  138. package/dist/components/progress.css.d.ts +1 -0
  139. package/dist/components/radial-progress/index.js +7 -0
  140. package/dist/components/radial-progress/object.js +1 -0
  141. package/dist/components/radial-progress.css +2 -0
  142. package/dist/components/radial-progress.css.d.ts +1 -0
  143. package/dist/components/radio/index.js +7 -0
  144. package/dist/components/radio/object.js +1 -0
  145. package/dist/components/radio.css +2 -0
  146. package/dist/components/radio.css.d.ts +1 -0
  147. package/dist/components/range/index.js +7 -0
  148. package/dist/components/range/object.js +1 -0
  149. package/dist/components/range.css +2 -0
  150. package/dist/components/range.css.d.ts +1 -0
  151. package/dist/components/rating/index.js +7 -0
  152. package/dist/components/rating/object.js +1 -0
  153. package/dist/components/rating.css +2 -0
  154. package/dist/components/rating.css.d.ts +1 -0
  155. package/dist/components/select/index.js +7 -0
  156. package/dist/components/select/object.js +1 -0
  157. package/dist/components/select.css +2 -0
  158. package/dist/components/select.css.d.ts +1 -0
  159. package/dist/components/sidebar/index.js +7 -0
  160. package/dist/components/sidebar/object.js +1 -0
  161. package/dist/components/sidebar.css +2 -0
  162. package/dist/components/sidebar.css.d.ts +1 -0
  163. package/dist/components/skeleton/index.js +7 -0
  164. package/dist/components/skeleton/object.js +1 -0
  165. package/dist/components/skeleton.css +2 -0
  166. package/dist/components/skeleton.css.d.ts +1 -0
  167. package/dist/components/stat/index.js +7 -0
  168. package/dist/components/stat/object.js +1 -0
  169. package/dist/components/stat.css +2 -0
  170. package/dist/components/stat.css.d.ts +1 -0
  171. package/dist/components/status/index.js +7 -0
  172. package/dist/components/status/object.js +1 -0
  173. package/dist/components/status.css +2 -0
  174. package/dist/components/status.css.d.ts +1 -0
  175. package/dist/components/steps/index.js +7 -0
  176. package/dist/components/steps/object.js +1 -0
  177. package/dist/components/steps.css +2 -0
  178. package/dist/components/steps.css.d.ts +1 -0
  179. package/dist/components/surface/index.js +7 -0
  180. package/dist/components/surface/object.js +1 -0
  181. package/dist/components/surface.css +2 -0
  182. package/dist/components/surface.css.d.ts +1 -0
  183. package/dist/components/swap/index.js +7 -0
  184. package/dist/components/swap/object.js +1 -0
  185. package/dist/components/swap.css +2 -0
  186. package/dist/components/swap.css.d.ts +1 -0
  187. package/dist/components/table/index.js +7 -0
  188. package/dist/components/table/object.js +1 -0
  189. package/dist/components/table.css +2 -0
  190. package/dist/components/table.css.d.ts +1 -0
  191. package/dist/components/tabs/index.js +7 -0
  192. package/dist/components/tabs/object.js +1 -0
  193. package/dist/components/tabs.css +2 -0
  194. package/dist/components/tabs.css.d.ts +1 -0
  195. package/dist/components/tag/index.js +7 -0
  196. package/dist/components/tag/object.js +1 -0
  197. package/dist/components/tag.css +2 -0
  198. package/dist/components/tag.css.d.ts +1 -0
  199. package/dist/components/textarea/index.js +7 -0
  200. package/dist/components/textarea/object.js +1 -0
  201. package/dist/components/textarea.css +2 -0
  202. package/dist/components/textarea.css.d.ts +1 -0
  203. package/dist/components/timeline/index.js +7 -0
  204. package/dist/components/timeline/object.js +1 -0
  205. package/dist/components/timeline.css +2 -0
  206. package/dist/components/timeline.css.d.ts +1 -0
  207. package/dist/components/toast/index.js +7 -0
  208. package/dist/components/toast/object.js +1 -0
  209. package/dist/components/toast.css +2 -0
  210. package/dist/components/toast.css.d.ts +1 -0
  211. package/dist/components/toggle/index.js +7 -0
  212. package/dist/components/toggle/object.js +1 -0
  213. package/dist/components/toggle.css +2 -0
  214. package/dist/components/toggle.css.d.ts +1 -0
  215. package/dist/components/tooltip/index.js +7 -0
  216. package/dist/components/tooltip/object.js +1 -0
  217. package/dist/components/tooltip.css +2 -0
  218. package/dist/components/tooltip.css.d.ts +1 -0
  219. package/dist/frutjam.components.css +62 -0
  220. package/dist/frutjam.components.css.d.ts +1 -0
  221. package/dist/frutjam.min.css +65 -3
  222. package/dist/frutjam.min.css.d.ts +1 -0
  223. package/dist/functions/addPrefix.js +14 -0
  224. package/dist/plugin.js +207 -0
  225. package/dist/themes/darkberry/index.js +5 -0
  226. package/dist/themes/darkberry/object.js +1 -0
  227. package/dist/themes/darkberry.css +2 -0
  228. package/dist/themes/darkberry.css.d.ts +1 -0
  229. package/dist/themes/snowberry/index.js +5 -0
  230. package/dist/themes/snowberry/object.js +1 -0
  231. package/dist/themes/snowberry.css +2 -0
  232. package/dist/themes/snowberry.css.d.ts +1 -0
  233. package/dist/unocss/map.js +5 -0
  234. package/dist/utilities/headings/index.js +7 -0
  235. package/dist/utilities/headings/object.js +1 -0
  236. package/dist/utilities/headings.css +2 -0
  237. package/dist/utilities/headings.css.d.ts +1 -0
  238. package/dist/utilities/para/index.js +7 -0
  239. package/dist/utilities/para/object.js +1 -0
  240. package/dist/utilities/para.css +2 -0
  241. package/dist/utilities/para.css.d.ts +1 -0
  242. package/dist/utilities/typography/index.js +7 -0
  243. package/dist/utilities/typography/object.js +1 -0
  244. package/dist/utilities/typography.css +2 -0
  245. package/dist/utilities/typography.css.d.ts +1 -0
  246. package/dist/utilities/wrapper/index.js +7 -0
  247. package/dist/utilities/wrapper/object.js +1 -0
  248. package/dist/utilities/wrapper.css +2 -0
  249. package/dist/utilities/wrapper.css.d.ts +1 -0
  250. package/index.js +332 -0
  251. package/package.json +90 -43
  252. package/packages/js/carousel.js +20 -0
  253. package/packages/js/collapsible.js +14 -0
  254. package/packages/js/combobox.js +101 -0
  255. package/packages/js/drawer.js +1 -0
  256. package/packages/js/index.js +8 -0
  257. package/packages/js/modal.js +8 -0
  258. package/packages/js/tabs.js +89 -0
  259. package/packages/js/toast.js +27 -0
  260. package/packages/js/tooltip.js +8 -0
  261. package/packages/react/index.js +8 -0
  262. package/packages/react/useCarousel.js +23 -0
  263. package/packages/react/useCollapsible.js +20 -0
  264. package/packages/react/useCombobox.js +81 -0
  265. package/packages/react/useDrawer.js +1 -0
  266. package/packages/react/useModal.js +12 -0
  267. package/packages/react/useTabs.js +53 -0
  268. package/packages/react/useToast.js +27 -0
  269. package/packages/react/useTooltip.js +12 -0
  270. package/packages/unocss/index.js +77 -0
  271. package/dist/frutjam.dev.css +0 -2
  272. package/dist/frutjam.min.js +0 -2
  273. package/dist/frutjam.min.js.LICENSE.txt +0 -1
@@ -0,0 +1,101 @@
1
+ let _comboboxIdCounter = 0
2
+
3
+ export function createCombobox(el) {
4
+ const input = el.querySelector('input')
5
+ let activeIndex = -1
6
+
7
+ const uid = ++_comboboxIdCounter
8
+ const listbox = el.querySelector('[role="listbox"]') ?? el.querySelector('.combobox-dropdown') ?? (() => {
9
+ const found = el.querySelector('.combobox-item, [role="option"]')
10
+ return found ? found.parentElement : null
11
+ })()
12
+
13
+ if (listbox && !listbox.id) listbox.id = `fj-combobox-listbox-${uid}`
14
+ if (listbox && !listbox.getAttribute('role')) listbox.setAttribute('role', 'listbox')
15
+ if (input && listbox) {
16
+ input.setAttribute('aria-controls', listbox.id)
17
+ if (!input.getAttribute('role')) input.setAttribute('role', 'combobox')
18
+ if (!input.getAttribute('aria-autocomplete')) input.setAttribute('aria-autocomplete', 'list')
19
+ }
20
+
21
+ const allOptions = () => Array.from(el.querySelectorAll('[role="option"], .combobox-item'))
22
+ const visibleOptions = () => allOptions().filter((o) => !o.hidden)
23
+
24
+ function open() {
25
+ el.classList.add('combobox-open')
26
+ input?.setAttribute('aria-expanded', 'true')
27
+ }
28
+
29
+ function close() {
30
+ el.classList.remove('combobox-open')
31
+ input?.setAttribute('aria-expanded', 'false')
32
+ highlight(-1)
33
+ }
34
+
35
+ function toggle() {
36
+ el.classList.contains('combobox-open') ? close() : open()
37
+ }
38
+
39
+ function filter(query) {
40
+ allOptions().forEach((opt) => {
41
+ opt.hidden = !opt.textContent.toLowerCase().includes(query.toLowerCase())
42
+ })
43
+ highlight(-1)
44
+ }
45
+
46
+ function highlight(index) {
47
+ const opts = visibleOptions()
48
+ opts.forEach((o, i) => {
49
+ o.classList.remove('combobox-item-active')
50
+ if (!o.id) o.id = `fj-combobox-option-${uid}-${i}`
51
+ })
52
+ if (index >= 0 && opts[index]) {
53
+ opts[index].classList.add('combobox-item-active')
54
+ opts[index].scrollIntoView({ block: 'nearest' })
55
+ input?.setAttribute('aria-activedescendant', opts[index].id)
56
+ } else {
57
+ input?.removeAttribute('aria-activedescendant')
58
+ }
59
+ activeIndex = index
60
+ }
61
+
62
+ function select(opt) {
63
+ if (input) input.value = opt.textContent.trim()
64
+ el.dispatchEvent(new CustomEvent('fj:select', { detail: { value: opt.dataset.value ?? opt.textContent.trim() } }))
65
+ close()
66
+ }
67
+
68
+ input?.addEventListener('input', (e) => {
69
+ open()
70
+ filter(e.target.value)
71
+ })
72
+
73
+ input?.addEventListener('keydown', (e) => {
74
+ const opts = visibleOptions()
75
+ if (e.key === 'ArrowDown') {
76
+ e.preventDefault()
77
+ if (!el.classList.contains('combobox-open')) { open(); return }
78
+ highlight(Math.min(activeIndex + 1, opts.length - 1))
79
+ } else if (e.key === 'ArrowUp') {
80
+ e.preventDefault()
81
+ highlight(Math.max(activeIndex - 1, 0))
82
+ } else if (e.key === 'Enter' && activeIndex >= 0) {
83
+ e.preventDefault()
84
+ select(opts[activeIndex])
85
+ } else if (e.key === 'Escape') {
86
+ close()
87
+ }
88
+ })
89
+
90
+ el.addEventListener('click', (e) => {
91
+ const opt = e.target.closest('[role="option"], .combobox-item')
92
+ if (opt) { select(opt); return }
93
+ if (e.target === input) open()
94
+ })
95
+
96
+ document.addEventListener('click', (e) => {
97
+ if (!el.contains(e.target)) close()
98
+ }, { capture: true })
99
+
100
+ return { open, close, toggle, filter }
101
+ }
@@ -0,0 +1 @@
1
+ export { createModal as createDrawer } from './modal.js'
@@ -0,0 +1,8 @@
1
+ export { createModal } from './modal.js'
2
+ export { createDrawer } from './drawer.js'
3
+ export { createCollapsible } from './collapsible.js'
4
+ export { createTooltip } from './tooltip.js'
5
+ export { createTabs } from './tabs.js'
6
+ export { createToastManager } from './toast.js'
7
+ export { createCarousel } from './carousel.js'
8
+ export { createCombobox } from './combobox.js'
@@ -0,0 +1,8 @@
1
+ export function createModal(el) {
2
+ return {
3
+ open: () => el.showModal(),
4
+ close: () => el.close(),
5
+ toggle: () => (el.open ? el.close() : el.showModal()),
6
+ isOpen: () => el.open,
7
+ }
8
+ }
@@ -0,0 +1,89 @@
1
+ let _tabsIdCounter = 0
2
+
3
+ export function createTabs(tabsEl, panelsEl) {
4
+ const uid = ++_tabsIdCounter
5
+ const getTabs = () => Array.from(tabsEl.querySelectorAll('.tab'))
6
+ const getPanels = () => (panelsEl ? Array.from(panelsEl.children) : [])
7
+
8
+ if (!tabsEl.getAttribute('role')) tabsEl.setAttribute('role', 'tablist')
9
+
10
+ function initAria() {
11
+ getTabs().forEach((tab, i) => {
12
+ if (!tab.id) tab.id = `fj-tab-${uid}-${i}`
13
+ if (!tab.getAttribute('role')) tab.setAttribute('role', 'tab')
14
+ })
15
+ getPanels().forEach((panel, i) => {
16
+ if (!panel.id) panel.id = `fj-panel-${uid}-${i}`
17
+ if (!panel.getAttribute('role')) panel.setAttribute('role', 'tabpanel')
18
+ const tab = getTabs()[i]
19
+ if (tab && !panel.getAttribute('aria-labelledby')) {
20
+ panel.setAttribute('aria-labelledby', tab.id)
21
+ }
22
+ if (!panel.hasAttribute('tabindex')) panel.setAttribute('tabindex', '0')
23
+ })
24
+ getTabs().forEach((tab, i) => {
25
+ const panel = getPanels()[i]
26
+ if (panel && !tab.getAttribute('aria-controls')) {
27
+ tab.setAttribute('aria-controls', panel.id)
28
+ }
29
+ })
30
+ }
31
+
32
+ function activate(index) {
33
+ getTabs().forEach((tab, i) => {
34
+ const active = i === index
35
+ tab.classList.toggle('tab-active', active)
36
+ tab.setAttribute('aria-selected', String(active))
37
+ tab.setAttribute('tabindex', active ? '0' : '-1')
38
+ })
39
+ getPanels().forEach((panel, i) => {
40
+ panel.hidden = i !== index
41
+ })
42
+ }
43
+
44
+ initAria()
45
+
46
+ tabsEl.addEventListener('click', (e) => {
47
+ const tab = e.target.closest('.tab')
48
+ if (tab) activate(getTabs().indexOf(tab))
49
+ })
50
+
51
+ tabsEl.addEventListener('keydown', (e) => {
52
+ const tabs = getTabs()
53
+ const current = tabs.findIndex((t) => t === document.activeElement)
54
+ if (current === -1) return
55
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
56
+ e.preventDefault()
57
+ const next = (current + 1) % tabs.length
58
+ activate(next)
59
+ tabs[next].focus()
60
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
61
+ e.preventDefault()
62
+ const prev = (current - 1 + tabs.length) % tabs.length
63
+ activate(prev)
64
+ tabs[prev].focus()
65
+ } else if (e.key === 'Home') {
66
+ e.preventDefault()
67
+ activate(0)
68
+ tabs[0].focus()
69
+ } else if (e.key === 'End') {
70
+ e.preventDefault()
71
+ activate(tabs.length - 1)
72
+ tabs[tabs.length - 1].focus()
73
+ }
74
+ })
75
+
76
+ return {
77
+ activate,
78
+ next() {
79
+ const all = getTabs()
80
+ const current = all.findIndex((t) => t.classList.contains('tab-active'))
81
+ activate((current + 1) % all.length)
82
+ },
83
+ prev() {
84
+ const all = getTabs()
85
+ const current = all.findIndex((t) => t.classList.contains('tab-active'))
86
+ activate((current - 1 + all.length) % all.length)
87
+ },
88
+ }
89
+ }
@@ -0,0 +1,27 @@
1
+ export function createToastManager(containerEl) {
2
+ containerEl.setAttribute('aria-live', 'polite')
3
+ containerEl.setAttribute('aria-atomic', 'false')
4
+
5
+ function show(message, { duration = 3000, type = '' } = {}) {
6
+ const el = document.createElement('div')
7
+ el.className = ['alert', type && `alert-${type}`].filter(Boolean).join(' ')
8
+ el.textContent = message
9
+ if (type === 'error' || type === 'warning') {
10
+ el.setAttribute('role', 'alert')
11
+ } else {
12
+ el.setAttribute('role', 'status')
13
+ }
14
+ containerEl.appendChild(el)
15
+ if (duration > 0) setTimeout(() => el.remove(), duration)
16
+ return el
17
+ }
18
+
19
+ return {
20
+ show,
21
+ success: (msg, opts) => show(msg, { ...opts, type: 'success' }),
22
+ error: (msg, opts) => show(msg, { ...opts, type: 'error' }),
23
+ warning: (msg, opts) => show(msg, { ...opts, type: 'warning' }),
24
+ info: (msg, opts) => show(msg, { ...opts, type: 'info' }),
25
+ dismiss: (el) => el?.remove(),
26
+ }
27
+ }
@@ -0,0 +1,8 @@
1
+ export function createTooltip(el) {
2
+ return {
3
+ show: () => el.classList.add('tooltip-open'),
4
+ hide: () => el.classList.remove('tooltip-open'),
5
+ toggle: () => el.classList.toggle('tooltip-open'),
6
+ isOpen: () => el.classList.contains('tooltip-open'),
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ export { useModal } from './useModal.js'
2
+ export { useDrawer } from './useDrawer.js'
3
+ export { useCollapsible } from './useCollapsible.js'
4
+ export { useTooltip } from './useTooltip.js'
5
+ export { useTabs } from './useTabs.js'
6
+ export { useToast } from './useToast.js'
7
+ export { useCarousel } from './useCarousel.js'
8
+ export { useCombobox } from './useCombobox.js'
@@ -0,0 +1,23 @@
1
+ import { useRef, useCallback, useState } from 'react'
2
+
3
+ export function useCarousel() {
4
+ const ref = useRef(null)
5
+ const [current, setCurrent] = useState(0)
6
+
7
+ const goTo = useCallback((index) => {
8
+ if (!ref.current) return
9
+ const items = Array.from(ref.current.querySelectorAll('.carousel-item'))
10
+ if (!items.length) return
11
+ const next = Math.max(0, Math.min(index, items.length - 1))
12
+ setCurrent(next)
13
+ items[next].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' })
14
+ }, [])
15
+
16
+ return {
17
+ ref,
18
+ current,
19
+ goTo,
20
+ next: useCallback(() => goTo(current + 1), [current, goTo]),
21
+ prev: useCallback(() => goTo(current - 1), [current, goTo]),
22
+ }
23
+ }
@@ -0,0 +1,20 @@
1
+ import { useRef, useCallback } from 'react'
2
+
3
+ export function useCollapsible() {
4
+ const ref = useRef(null)
5
+
6
+ function syncAria() {
7
+ if (!ref.current) return
8
+ const isOpen = ref.current.classList.contains('collapsible-open')
9
+ const trigger = ref.current.querySelector('[aria-expanded]')
10
+ if (trigger) trigger.setAttribute('aria-expanded', String(isOpen))
11
+ }
12
+
13
+ return {
14
+ ref,
15
+ open: useCallback(() => { ref.current?.classList.add('collapsible-open'); syncAria() }, []),
16
+ close: useCallback(() => { ref.current?.classList.remove('collapsible-open'); syncAria() }, []),
17
+ toggle: useCallback(() => { ref.current?.classList.toggle('collapsible-open'); syncAria() }, []),
18
+ isOpen: () => ref.current?.classList.contains('collapsible-open') ?? false,
19
+ }
20
+ }
@@ -0,0 +1,81 @@
1
+ import { useState, useCallback, useId } from 'react'
2
+
3
+ export function useCombobox({ items = [], filterFn } = {}) {
4
+ const uid = useId()
5
+ const listboxId = `${uid}-listbox`
6
+
7
+ const [open, setOpen] = useState(false)
8
+ const [query, setQuery] = useState('')
9
+ const [activeIndex, setActiveIndex] = useState(-1)
10
+ const [value, setValue] = useState('')
11
+
12
+ const filtered = filterFn
13
+ ? filterFn(items, query)
14
+ : items.filter((item) =>
15
+ String(item.label ?? item).toLowerCase().includes(query.toLowerCase())
16
+ )
17
+
18
+ const activeDescendant = activeIndex >= 0 ? `${uid}-option-${activeIndex}` : undefined
19
+
20
+ const select = useCallback((item) => {
21
+ const label = item.label ?? String(item)
22
+ setValue(label)
23
+ setQuery(label)
24
+ setOpen(false)
25
+ setActiveIndex(-1)
26
+ }, [])
27
+
28
+ const handleKeyDown = useCallback((e) => {
29
+ if (e.key === 'ArrowDown') {
30
+ e.preventDefault()
31
+ if (!open) { setOpen(true); return }
32
+ setActiveIndex((i) => Math.min(i + 1, filtered.length - 1))
33
+ } else if (e.key === 'ArrowUp') {
34
+ e.preventDefault()
35
+ setActiveIndex((i) => Math.max(i - 1, 0))
36
+ } else if (e.key === 'Enter' && activeIndex >= 0) {
37
+ e.preventDefault()
38
+ select(filtered[activeIndex])
39
+ } else if (e.key === 'Escape') {
40
+ setOpen(false)
41
+ setActiveIndex(-1)
42
+ }
43
+ }, [open, filtered, activeIndex, select])
44
+
45
+ return {
46
+ open,
47
+ value,
48
+ query,
49
+ filtered,
50
+ activeIndex,
51
+ select,
52
+ containerProps: {
53
+ className: open ? 'combobox combobox-open' : 'combobox',
54
+ },
55
+ inputProps: {
56
+ value: query,
57
+ onChange: (e) => { setQuery(e.target.value); setOpen(true); setActiveIndex(-1) },
58
+ onKeyDown: handleKeyDown,
59
+ onFocus: () => setOpen(true),
60
+ onBlur: () => setTimeout(() => setOpen(false), 150),
61
+ role: 'combobox',
62
+ 'aria-expanded': open,
63
+ 'aria-autocomplete': 'list',
64
+ 'aria-controls': listboxId,
65
+ 'aria-activedescendant': activeDescendant,
66
+ },
67
+ listboxProps: {
68
+ id: listboxId,
69
+ role: 'listbox',
70
+ },
71
+ optionProps: (item, index) => ({
72
+ id: `${uid}-option-${index}`,
73
+ className: activeIndex === index ? 'combobox-option combobox-option-active' : 'combobox-option',
74
+ role: 'option',
75
+ 'aria-selected': activeIndex === index,
76
+ onMouseDown: (e) => e.preventDefault(),
77
+ onClick: () => select(item),
78
+ onMouseEnter: () => setActiveIndex(index),
79
+ }),
80
+ }
81
+ }
@@ -0,0 +1 @@
1
+ export { useModal as useDrawer } from './useModal.js'
@@ -0,0 +1,12 @@
1
+ import { useRef, useCallback } from 'react'
2
+
3
+ export function useModal() {
4
+ const ref = useRef(null)
5
+ return {
6
+ ref,
7
+ open: useCallback(() => ref.current?.showModal(), []),
8
+ close: useCallback(() => ref.current?.close(), []),
9
+ toggle: useCallback(() => (ref.current?.open ? ref.current.close() : ref.current?.showModal()), []),
10
+ isOpen: () => ref.current?.open ?? false,
11
+ }
12
+ }
@@ -0,0 +1,53 @@
1
+ import { useState, useCallback, useId } from 'react'
2
+
3
+ export function useTabs(initialIndex = 0) {
4
+ const uid = useId()
5
+ const [active, setActive] = useState(initialIndex)
6
+
7
+ const activate = useCallback((index) => setActive(index), [])
8
+
9
+ const tabId = (index) => `${uid}-tab-${index}`
10
+ const panelId = (index) => `${uid}-panel-${index}`
11
+
12
+ const handleKeyDown = useCallback((index, count) => (e) => {
13
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
14
+ e.preventDefault()
15
+ activate((index + 1) % count)
16
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
17
+ e.preventDefault()
18
+ activate((index - 1 + count) % count)
19
+ } else if (e.key === 'Home') {
20
+ e.preventDefault()
21
+ activate(0)
22
+ } else if (e.key === 'End') {
23
+ e.preventDefault()
24
+ activate(count - 1)
25
+ }
26
+ }, [activate])
27
+
28
+ return {
29
+ active,
30
+ activate,
31
+ isActive: (index) => active === index,
32
+ tablistProps: {
33
+ role: 'tablist',
34
+ },
35
+ tabProps: (index, count = 0) => ({
36
+ id: tabId(index),
37
+ className: active === index ? 'tab tab-active' : 'tab',
38
+ onClick: () => activate(index),
39
+ onKeyDown: handleKeyDown(index, count),
40
+ role: 'tab',
41
+ 'aria-selected': active === index,
42
+ 'aria-controls': panelId(index),
43
+ tabIndex: active === index ? 0 : -1,
44
+ }),
45
+ panelProps: (index) => ({
46
+ id: panelId(index),
47
+ hidden: active !== index,
48
+ role: 'tabpanel',
49
+ 'aria-labelledby': tabId(index),
50
+ tabIndex: 0,
51
+ }),
52
+ }
53
+ }
@@ -0,0 +1,27 @@
1
+ import { useState, useCallback, useRef } from 'react'
2
+
3
+ export function useToast() {
4
+ const [toasts, setToasts] = useState([])
5
+ const idRef = useRef(0)
6
+
7
+ const dismiss = useCallback((id) => {
8
+ setToasts((prev) => prev.filter((t) => t.id !== id))
9
+ }, [])
10
+
11
+ const show = useCallback((message, { duration = 3000, type = '' } = {}) => {
12
+ const id = ++idRef.current
13
+ setToasts((prev) => [...prev, { id, message, type }])
14
+ if (duration > 0) setTimeout(() => dismiss(id), duration)
15
+ return id
16
+ }, [dismiss])
17
+
18
+ return {
19
+ toasts,
20
+ show,
21
+ dismiss,
22
+ success: (msg, opts) => show(msg, { ...opts, type: 'success' }),
23
+ error: (msg, opts) => show(msg, { ...opts, type: 'error' }),
24
+ warning: (msg, opts) => show(msg, { ...opts, type: 'warning' }),
25
+ info: (msg, opts) => show(msg, { ...opts, type: 'info' }),
26
+ }
27
+ }
@@ -0,0 +1,12 @@
1
+ import { useRef, useCallback } from 'react'
2
+
3
+ export function useTooltip() {
4
+ const ref = useRef(null)
5
+ return {
6
+ ref,
7
+ show: useCallback(() => ref.current?.classList.add('tooltip-open'), []),
8
+ hide: useCallback(() => ref.current?.classList.remove('tooltip-open'), []),
9
+ toggle: useCallback(() => ref.current?.classList.toggle('tooltip-open'), []),
10
+ isOpen: () => ref.current?.classList.contains('tooltip-open') ?? false,
11
+ }
12
+ }
@@ -0,0 +1,77 @@
1
+ import { symbols } from "@unocss/core"
2
+ // Generated by scripts/build-unocss.mjs from the same objects the Tailwind
3
+ // plugin ships, so the two cannot describe different components.
4
+ import { rules as tokenRules, preflight, base, components } from "../../dist/unocss/map.js"
5
+
6
+ /**
7
+ * Frutjam as an UnoCSS preset.
8
+ *
9
+ * Every class the Tailwind plugin generates is available here too, so
10
+ * `class="btn btn-error"` works, and with presetAttributify so does
11
+ * `btn="error xl"`. Only what you use is emitted.
12
+ *
13
+ * Bodies go through symbols.body rather than a plain object, because several
14
+ * components nest, and symbols.body takes nested CSS while leaving variants
15
+ * intact. That is what keeps `md:btn-error` and `hover:card-primary` working.
16
+ *
17
+ * @param {object} [options]
18
+ * @param {boolean} [options.preflight=true] Emit Frutjam's tokens, reset and
19
+ * themes. Turn off if you already
20
+ * import frutjam/css/base yourself.
21
+ * @param {string} [options.prefix=''] Matches the Tailwind plugin's
22
+ * prefix option, so `fj` gives
23
+ * fj-btn and fj-btn="error".
24
+ * @param {string} [options.layer='frutjam'] Cascade layer for the components.
25
+ */
26
+ export function presetFrutjam(options = {}) {
27
+ const { preflight: withPreflight = true, prefix = "", layer = "frutjam" } = options
28
+
29
+ const p = prefix ? `${prefix}-` : ""
30
+ const named = (token) => `${p}${token}`
31
+ const componentAttrs = new Set(components.map(named))
32
+
33
+ /*
34
+ * `btn="error xl"` expands to btn-error and btn-xl and never to a bare btn,
35
+ * so the component itself would be missing. Asking for it here, at extraction
36
+ * time, keeps it free of whatever variant the attribute carried: with
37
+ * `btn="md:error"` the button must exist at every width and only the colour
38
+ * belongs inside the media query.
39
+ */
40
+ const attributeBaseExtractor = {
41
+ name: "frutjam-attribute-base",
42
+ order: -1,
43
+ extract({ code }) {
44
+ const found = new Set()
45
+ for (const match of code.matchAll(/(?:^|[\s"'`])([a-z][a-z0-9-]*)\s*=\s*["']/g)) {
46
+ if (componentAttrs.has(match[1])) found.add(match[1])
47
+ }
48
+ return found
49
+ },
50
+ }
51
+
52
+ // Static rules rather than one catch-all regex: UnoCSS can look these up
53
+ // directly instead of running a pattern against every token it sees.
54
+ const rules = Object.entries(tokenRules).map(([token, body]) => {
55
+ const css = { [symbols.body]: body }
56
+ // A component answers to its class and to its attribute. UnoCSS merges the
57
+ // two into one selector list when both appear, so this costs nothing.
58
+ if (componentAttrs.has(named(token))) {
59
+ css[symbols.selector] = (selector) => `${selector},[${named(token)}]`
60
+ }
61
+ return [named(token), css, { layer }]
62
+ })
63
+
64
+ const preflights = [{ layer, getCSS: () => preflight }]
65
+ if (withPreflight) preflights.unshift({ layer, getCSS: () => base })
66
+
67
+ return {
68
+ name: "frutjam",
69
+ layers: { [layer]: -1 },
70
+ extractors: [attributeBaseExtractor],
71
+ rules,
72
+ preflights,
73
+ autocomplete: { templates: Object.keys(tokenRules).map(named) },
74
+ }
75
+ }
76
+
77
+ export default presetFrutjam