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.
- package/LICENSE +1 -1
- package/README.github.md +382 -0
- package/README.md +90 -42
- package/README.npm.md +124 -0
- package/dist/base/reboot/index.js +5 -0
- package/dist/base/reboot/object.js +1 -0
- package/dist/base/tokens/index.js +5 -0
- package/dist/base/tokens/object.js +1 -0
- package/dist/base.css +62 -0
- package/dist/base.css.d.ts +1 -0
- package/dist/components/accordion/index.js +7 -0
- package/dist/components/accordion/object.js +1 -0
- package/dist/components/accordion.css +2 -0
- package/dist/components/accordion.css.d.ts +1 -0
- package/dist/components/alert/index.js +7 -0
- package/dist/components/alert/object.js +1 -0
- package/dist/components/alert.css +2 -0
- package/dist/components/alert.css.d.ts +1 -0
- package/dist/components/avatar/index.js +7 -0
- package/dist/components/avatar/object.js +1 -0
- package/dist/components/avatar.css +2 -0
- package/dist/components/avatar.css.d.ts +1 -0
- package/dist/components/badge/index.js +7 -0
- package/dist/components/badge/object.js +1 -0
- package/dist/components/badge.css +2 -0
- package/dist/components/badge.css.d.ts +1 -0
- package/dist/components/breadcrumb/index.js +7 -0
- package/dist/components/breadcrumb/object.js +1 -0
- package/dist/components/breadcrumb.css +2 -0
- package/dist/components/breadcrumb.css.d.ts +1 -0
- package/dist/components/button/index.js +7 -0
- package/dist/components/button/object.js +1 -0
- package/dist/components/button.css +2 -0
- package/dist/components/button.css.d.ts +1 -0
- package/dist/components/card/index.js +7 -0
- package/dist/components/card/object.js +1 -0
- package/dist/components/card.css +2 -0
- package/dist/components/card.css.d.ts +1 -0
- package/dist/components/carousel/index.js +7 -0
- package/dist/components/carousel/object.js +1 -0
- package/dist/components/carousel.css +2 -0
- package/dist/components/carousel.css.d.ts +1 -0
- package/dist/components/chat/index.js +7 -0
- package/dist/components/chat/object.js +1 -0
- package/dist/components/chat.css +2 -0
- package/dist/components/chat.css.d.ts +1 -0
- package/dist/components/checkbox/index.js +7 -0
- package/dist/components/checkbox/object.js +1 -0
- package/dist/components/checkbox.css +2 -0
- package/dist/components/checkbox.css.d.ts +1 -0
- package/dist/components/collapsible/index.js +7 -0
- package/dist/components/collapsible/object.js +1 -0
- package/dist/components/collapsible.css +2 -0
- package/dist/components/collapsible.css.d.ts +1 -0
- package/dist/components/combobox/index.js +7 -0
- package/dist/components/combobox/object.js +1 -0
- package/dist/components/combobox.css +2 -0
- package/dist/components/combobox.css.d.ts +1 -0
- package/dist/components/countdown/index.js +7 -0
- package/dist/components/countdown/object.js +1 -0
- package/dist/components/countdown.css +2 -0
- package/dist/components/countdown.css.d.ts +1 -0
- package/dist/components/diff/index.js +7 -0
- package/dist/components/diff/object.js +1 -0
- package/dist/components/diff.css +2 -0
- package/dist/components/diff.css.d.ts +1 -0
- package/dist/components/divider/index.js +7 -0
- package/dist/components/divider/object.js +1 -0
- package/dist/components/divider.css +2 -0
- package/dist/components/divider.css.d.ts +1 -0
- package/dist/components/drawer/index.js +7 -0
- package/dist/components/drawer/object.js +1 -0
- package/dist/components/drawer.css +2 -0
- package/dist/components/drawer.css.d.ts +1 -0
- package/dist/components/footer/index.js +7 -0
- package/dist/components/footer/object.js +1 -0
- package/dist/components/footer.css +2 -0
- package/dist/components/footer.css.d.ts +1 -0
- package/dist/components/header/index.js +7 -0
- package/dist/components/header/object.js +1 -0
- package/dist/components/header.css +2 -0
- package/dist/components/header.css.d.ts +1 -0
- package/dist/components/hero/index.js +7 -0
- package/dist/components/hero/object.js +1 -0
- package/dist/components/hero.css +2 -0
- package/dist/components/hero.css.d.ts +1 -0
- package/dist/components/indicator/index.js +7 -0
- package/dist/components/indicator/object.js +1 -0
- package/dist/components/indicator.css +2 -0
- package/dist/components/indicator.css.d.ts +1 -0
- package/dist/components/input/index.js +7 -0
- package/dist/components/input/object.js +1 -0
- package/dist/components/input.css +2 -0
- package/dist/components/input.css.d.ts +1 -0
- package/dist/components/join/index.js +7 -0
- package/dist/components/join/object.js +1 -0
- package/dist/components/join.css +2 -0
- package/dist/components/join.css.d.ts +1 -0
- package/dist/components/kbd/index.js +7 -0
- package/dist/components/kbd/object.js +1 -0
- package/dist/components/kbd.css +2 -0
- package/dist/components/kbd.css.d.ts +1 -0
- package/dist/components/link/index.js +7 -0
- package/dist/components/link/object.js +1 -0
- package/dist/components/link.css +2 -0
- package/dist/components/link.css.d.ts +1 -0
- package/dist/components/loading/index.js +7 -0
- package/dist/components/loading/object.js +1 -0
- package/dist/components/loading.css +2 -0
- package/dist/components/loading.css.d.ts +1 -0
- package/dist/components/marquee/index.js +7 -0
- package/dist/components/marquee/object.js +1 -0
- package/dist/components/marquee.css +2 -0
- package/dist/components/marquee.css.d.ts +1 -0
- package/dist/components/mask/index.js +7 -0
- package/dist/components/mask/object.js +1 -0
- package/dist/components/mask.css +2 -0
- package/dist/components/mask.css.d.ts +1 -0
- package/dist/components/menu/index.js +7 -0
- package/dist/components/menu/object.js +1 -0
- package/dist/components/menu.css +2 -0
- package/dist/components/menu.css.d.ts +1 -0
- package/dist/components/modal/index.js +7 -0
- package/dist/components/modal/object.js +1 -0
- package/dist/components/modal.css +2 -0
- package/dist/components/modal.css.d.ts +1 -0
- package/dist/components/navbar/index.js +7 -0
- package/dist/components/navbar/object.js +1 -0
- package/dist/components/navbar.css +2 -0
- package/dist/components/navbar.css.d.ts +1 -0
- package/dist/components/popover/index.js +7 -0
- package/dist/components/popover/object.js +1 -0
- package/dist/components/popover.css +2 -0
- package/dist/components/popover.css.d.ts +1 -0
- package/dist/components/progress/index.js +7 -0
- package/dist/components/progress/object.js +1 -0
- package/dist/components/progress.css +2 -0
- package/dist/components/progress.css.d.ts +1 -0
- package/dist/components/radial-progress/index.js +7 -0
- package/dist/components/radial-progress/object.js +1 -0
- package/dist/components/radial-progress.css +2 -0
- package/dist/components/radial-progress.css.d.ts +1 -0
- package/dist/components/radio/index.js +7 -0
- package/dist/components/radio/object.js +1 -0
- package/dist/components/radio.css +2 -0
- package/dist/components/radio.css.d.ts +1 -0
- package/dist/components/range/index.js +7 -0
- package/dist/components/range/object.js +1 -0
- package/dist/components/range.css +2 -0
- package/dist/components/range.css.d.ts +1 -0
- package/dist/components/rating/index.js +7 -0
- package/dist/components/rating/object.js +1 -0
- package/dist/components/rating.css +2 -0
- package/dist/components/rating.css.d.ts +1 -0
- package/dist/components/select/index.js +7 -0
- package/dist/components/select/object.js +1 -0
- package/dist/components/select.css +2 -0
- package/dist/components/select.css.d.ts +1 -0
- package/dist/components/sidebar/index.js +7 -0
- package/dist/components/sidebar/object.js +1 -0
- package/dist/components/sidebar.css +2 -0
- package/dist/components/sidebar.css.d.ts +1 -0
- package/dist/components/skeleton/index.js +7 -0
- package/dist/components/skeleton/object.js +1 -0
- package/dist/components/skeleton.css +2 -0
- package/dist/components/skeleton.css.d.ts +1 -0
- package/dist/components/stat/index.js +7 -0
- package/dist/components/stat/object.js +1 -0
- package/dist/components/stat.css +2 -0
- package/dist/components/stat.css.d.ts +1 -0
- package/dist/components/status/index.js +7 -0
- package/dist/components/status/object.js +1 -0
- package/dist/components/status.css +2 -0
- package/dist/components/status.css.d.ts +1 -0
- package/dist/components/steps/index.js +7 -0
- package/dist/components/steps/object.js +1 -0
- package/dist/components/steps.css +2 -0
- package/dist/components/steps.css.d.ts +1 -0
- package/dist/components/surface/index.js +7 -0
- package/dist/components/surface/object.js +1 -0
- package/dist/components/surface.css +2 -0
- package/dist/components/surface.css.d.ts +1 -0
- package/dist/components/swap/index.js +7 -0
- package/dist/components/swap/object.js +1 -0
- package/dist/components/swap.css +2 -0
- package/dist/components/swap.css.d.ts +1 -0
- package/dist/components/table/index.js +7 -0
- package/dist/components/table/object.js +1 -0
- package/dist/components/table.css +2 -0
- package/dist/components/table.css.d.ts +1 -0
- package/dist/components/tabs/index.js +7 -0
- package/dist/components/tabs/object.js +1 -0
- package/dist/components/tabs.css +2 -0
- package/dist/components/tabs.css.d.ts +1 -0
- package/dist/components/tag/index.js +7 -0
- package/dist/components/tag/object.js +1 -0
- package/dist/components/tag.css +2 -0
- package/dist/components/tag.css.d.ts +1 -0
- package/dist/components/textarea/index.js +7 -0
- package/dist/components/textarea/object.js +1 -0
- package/dist/components/textarea.css +2 -0
- package/dist/components/textarea.css.d.ts +1 -0
- package/dist/components/timeline/index.js +7 -0
- package/dist/components/timeline/object.js +1 -0
- package/dist/components/timeline.css +2 -0
- package/dist/components/timeline.css.d.ts +1 -0
- package/dist/components/toast/index.js +7 -0
- package/dist/components/toast/object.js +1 -0
- package/dist/components/toast.css +2 -0
- package/dist/components/toast.css.d.ts +1 -0
- package/dist/components/toggle/index.js +7 -0
- package/dist/components/toggle/object.js +1 -0
- package/dist/components/toggle.css +2 -0
- package/dist/components/toggle.css.d.ts +1 -0
- package/dist/components/tooltip/index.js +7 -0
- package/dist/components/tooltip/object.js +1 -0
- package/dist/components/tooltip.css +2 -0
- package/dist/components/tooltip.css.d.ts +1 -0
- package/dist/frutjam.components.css +62 -0
- package/dist/frutjam.components.css.d.ts +1 -0
- package/dist/frutjam.min.css +65 -3
- package/dist/frutjam.min.css.d.ts +1 -0
- package/dist/functions/addPrefix.js +14 -0
- package/dist/plugin.js +207 -0
- package/dist/themes/darkberry/index.js +5 -0
- package/dist/themes/darkberry/object.js +1 -0
- package/dist/themes/darkberry.css +2 -0
- package/dist/themes/darkberry.css.d.ts +1 -0
- package/dist/themes/snowberry/index.js +5 -0
- package/dist/themes/snowberry/object.js +1 -0
- package/dist/themes/snowberry.css +2 -0
- package/dist/themes/snowberry.css.d.ts +1 -0
- package/dist/unocss/map.js +5 -0
- package/dist/utilities/headings/index.js +7 -0
- package/dist/utilities/headings/object.js +1 -0
- package/dist/utilities/headings.css +2 -0
- package/dist/utilities/headings.css.d.ts +1 -0
- package/dist/utilities/para/index.js +7 -0
- package/dist/utilities/para/object.js +1 -0
- package/dist/utilities/para.css +2 -0
- package/dist/utilities/para.css.d.ts +1 -0
- package/dist/utilities/typography/index.js +7 -0
- package/dist/utilities/typography/object.js +1 -0
- package/dist/utilities/typography.css +2 -0
- package/dist/utilities/typography.css.d.ts +1 -0
- package/dist/utilities/wrapper/index.js +7 -0
- package/dist/utilities/wrapper/object.js +1 -0
- package/dist/utilities/wrapper.css +2 -0
- package/dist/utilities/wrapper.css.d.ts +1 -0
- package/index.js +332 -0
- package/package.json +90 -43
- package/packages/js/carousel.js +20 -0
- package/packages/js/collapsible.js +14 -0
- package/packages/js/combobox.js +101 -0
- package/packages/js/drawer.js +1 -0
- package/packages/js/index.js +8 -0
- package/packages/js/modal.js +8 -0
- package/packages/js/tabs.js +89 -0
- package/packages/js/toast.js +27 -0
- package/packages/js/tooltip.js +8 -0
- package/packages/react/index.js +8 -0
- package/packages/react/useCarousel.js +23 -0
- package/packages/react/useCollapsible.js +20 -0
- package/packages/react/useCombobox.js +81 -0
- package/packages/react/useDrawer.js +1 -0
- package/packages/react/useModal.js +12 -0
- package/packages/react/useTabs.js +53 -0
- package/packages/react/useToast.js +27 -0
- package/packages/react/useTooltip.js +12 -0
- package/packages/unocss/index.js +77 -0
- package/dist/frutjam.dev.css +0 -2
- package/dist/frutjam.min.js +0 -2
- 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,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 { 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
|