@santi020k/lumen-astro 0.1.0 → 0.2.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/README.md +5 -0
- package/components/Accordion.astro +8 -2
- package/components/Affix.astro +1 -1
- package/components/Agenda.astro +1 -1
- package/components/Alert.astro +1 -1
- package/components/AlertDialog.astro +1 -1
- package/components/Anchor.astro +1 -1
- package/components/AnimatedLogo.astro +1 -1
- package/components/AnimatedNumber.astro +1 -1
- package/components/AnimatedPortrait.astro +1 -1
- package/components/AspectRatio.astro +1 -1
- package/components/Attachment.astro +1 -1
- package/components/Autocomplete.astro +1 -1
- package/components/Avatar.astro +1 -1
- package/components/BackToTop.astro +1 -1
- package/components/Badge.astro +1 -1
- package/components/Breadcrumb.astro +1 -1
- package/components/Bubble.astro +1 -1
- package/components/Button.astro +1 -1
- package/components/ButtonGroup.astro +1 -1
- package/components/ButtonLink.astro +5 -4
- package/components/Calendar.astro +1 -1
- package/components/Callout.astro +1 -1
- package/components/Card.astro +3 -2
- package/components/Carousel.astro +1 -1
- package/components/Cascader.astro +1 -1
- package/components/Chart.astro +1 -1
- package/components/Checkbox.astro +1 -1
- package/components/Code.astro +5 -2
- package/components/Collapsible.astro +1 -1
- package/components/ColorPicker.astro +1 -1
- package/components/Combobox.astro +1 -1
- package/components/Command.astro +1 -1
- package/components/ContextMenu.astro +1 -1
- package/components/CoverImage.astro +1 -1
- package/components/DataTable.astro +1 -1
- package/components/DatePicker.astro +1 -1
- package/components/DateRangePicker.astro +1 -1
- package/components/Descriptions.astro +1 -1
- package/components/Dialog.astro +1 -1
- package/components/Direction.astro +1 -1
- package/components/Drawer.astro +1 -1
- package/components/DropdownMenu.astro +1 -1
- package/components/Empty.astro +1 -1
- package/components/Eyebrow.astro +1 -1
- package/components/Field.astro +1 -1
- package/components/FileUpload.astro +1 -1
- package/components/FloatingBadge.astro +1 -1
- package/components/FormattedDate.astro +1 -1
- package/components/GradientDivider.astro +1 -1
- package/components/HoverCard.astro +1 -1
- package/components/Icon.astro +5 -1
- package/components/Image.astro +1 -1
- package/components/Input.astro +1 -1
- package/components/InputGroup.astro +1 -1
- package/components/InputOTP.astro +1 -1
- package/components/Item.astro +1 -1
- package/components/Kbd.astro +1 -1
- package/components/Label.astro +1 -1
- package/components/LanguageToggle.astro +1 -1
- package/components/Link.astro +16 -2
- package/components/Marker.astro +1 -1
- package/components/Mentions.astro +1 -1
- package/components/Menubar.astro +1 -1
- package/components/Message.astro +1 -1
- package/components/MessageScroller.astro +1 -1
- package/components/NativeSelect.astro +1 -1
- package/components/NavigationMenu.astro +5 -1
- package/components/NumberField.astro +1 -1
- package/components/Pagination.astro +1 -1
- package/components/Particles.astro +1 -1
- package/components/PhoneInput.astro +1 -1
- package/components/Pill.astro +13 -8
- package/components/Popconfirm.astro +1 -1
- package/components/Popover.astro +1 -1
- package/components/Progress.astro +1 -1
- package/components/Prose.astro +1 -1
- package/components/QRCode.astro +1 -1
- package/components/RadioGroup.astro +1 -1
- package/components/Resizable.astro +1 -1
- package/components/RevealGroup.astro +1 -1
- package/components/RichTextEditor.astro +1 -1
- package/components/Schedule.astro +1 -1
- package/components/ScrollArea.astro +1 -1
- package/components/ScrollReveal.astro +1 -1
- package/components/SearchField.astro +1 -1
- package/components/Segmented.astro +1 -1
- package/components/Select.astro +1 -1
- package/components/Separator.astro +1 -1
- package/components/Sheet.astro +1 -1
- package/components/Sidebar.astro +5 -1
- package/components/Skeleton.astro +1 -1
- package/components/SkipLink.astro +1 -1
- package/components/Slider.astro +1 -1
- package/components/Sonner.astro +1 -1
- package/components/SpeedDial.astro +1 -1
- package/components/Spinner.astro +1 -1
- package/components/Stat.astro +1 -1
- package/components/Stepper.astro +1 -1
- package/components/Switch.astro +1 -1
- package/components/Table.astro +1 -1
- package/components/Tabs.astro +7 -1
- package/components/TagGroup.astro +1 -1
- package/components/Textarea.astro +1 -1
- package/components/ThemeBuilder.astro +1 -1
- package/components/ThemeToggle.astro +13 -1
- package/components/TimeField.astro +1 -1
- package/components/Toast.astro +1 -1
- package/components/Toggle.astro +12 -4
- package/components/ToggleGroup.astro +1 -1
- package/components/Toolbar.astro +1 -1
- package/components/Tooltip.astro +1 -1
- package/components/Tour.astro +1 -1
- package/components/Transfer.astro +1 -1
- package/components/Tree.astro +1 -1
- package/components/TreeGrid.astro +1 -1
- package/components/TreeSelect.astro +1 -1
- package/components/Typography.astro +1 -1
- package/components/VirtualList.astro +1 -1
- package/components/Watermark.astro +1 -1
- package/package.json +5 -4
- package/runtime/UIPrimitives.astro +255 -146
- package/runtime/controllers/motion.ts +145 -0
- package/styles/layers.css +1 -0
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
CSS.supports('position-area: block-end')
|
|
93
93
|
|
|
94
94
|
let uiAnchorCount = 0
|
|
95
|
+
let uiTabsCount = 0
|
|
95
96
|
const nextAnchorName = (): string => `--ui-anchor-${++uiAnchorCount}`
|
|
96
97
|
|
|
97
98
|
const isElementVisible = (element: HTMLElement): boolean =>
|
|
@@ -612,15 +613,47 @@
|
|
|
612
613
|
root.dataset.uiBound = 'true'
|
|
613
614
|
|
|
614
615
|
const tabs = [...root.querySelectorAll<HTMLElement>('[role="tab"]')]
|
|
616
|
+
.filter(tab => tab.closest('[data-ui-tabs]') === root)
|
|
617
|
+
|
|
615
618
|
const panels = [...root.querySelectorAll<HTMLElement>('[role="tabpanel"]')]
|
|
619
|
+
.filter(panel => panel.closest('[data-ui-tabs]') === root)
|
|
616
620
|
|
|
617
621
|
if (!tabs.length || !panels.length) continue
|
|
618
622
|
|
|
619
|
-
const
|
|
620
|
-
|
|
623
|
+
const rootId = root.id || `ui-tabs-${++uiTabsCount}`
|
|
624
|
+
|
|
625
|
+
root.id = rootId
|
|
626
|
+
|
|
627
|
+
for (const [index, tab] of tabs.entries()) {
|
|
628
|
+
const panel = panels.find(candidate => (
|
|
629
|
+
candidate.dataset.value && candidate.dataset.value === tab.dataset.value
|
|
630
|
+
)) ?? panels[index]
|
|
631
|
+
|
|
632
|
+
const value = tab.dataset.value ?? panel?.dataset.value ?? String(index)
|
|
633
|
+
|
|
634
|
+
tab.dataset.value = value
|
|
635
|
+
|
|
636
|
+
if (panel) {
|
|
637
|
+
panel.dataset.value = value
|
|
638
|
+
|
|
639
|
+
tab.id ||= `${rootId}-tab-${index + 1}`
|
|
640
|
+
|
|
641
|
+
panel.id ||= `${rootId}-panel-${index + 1}`
|
|
642
|
+
|
|
643
|
+
tab.setAttribute('aria-controls', panel.id)
|
|
644
|
+
|
|
645
|
+
panel.setAttribute('aria-labelledby', tab.id)
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const activate = (tab: HTMLElement, emit = true): void => {
|
|
650
|
+
const value = tab.dataset.value ?? ''
|
|
651
|
+
const wasSelected = tab.getAttribute('aria-selected') === 'true'
|
|
652
|
+
|
|
653
|
+
for (const [index, item] of tabs.entries()) {
|
|
621
654
|
const selected = item === tab
|
|
622
655
|
const panelId = item.getAttribute('aria-controls')
|
|
623
|
-
const panel = panelId ? document.getElementById(panelId) :
|
|
656
|
+
const panel = panelId ? document.getElementById(panelId) : panels[index]
|
|
624
657
|
|
|
625
658
|
item.setAttribute('aria-selected', String(selected))
|
|
626
659
|
|
|
@@ -630,11 +663,57 @@
|
|
|
630
663
|
panel.hidden = !selected
|
|
631
664
|
}
|
|
632
665
|
}
|
|
666
|
+
|
|
667
|
+
root.dataset.value = value
|
|
668
|
+
|
|
669
|
+
if (!emit || wasSelected) return
|
|
670
|
+
|
|
671
|
+
const storageKey = root.dataset.storageKey
|
|
672
|
+
|
|
673
|
+
if (storageKey) {
|
|
674
|
+
try {
|
|
675
|
+
localStorage.setItem(storageKey, value)
|
|
676
|
+
} catch {
|
|
677
|
+
// Storage can be unavailable in privacy modes; tab selection should still work.
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
for (const otherRoot of document.querySelectorAll<HTMLElement>('[data-ui-tabs][data-storage-key]')) {
|
|
681
|
+
if (otherRoot === root || otherRoot.dataset.storageKey !== storageKey || otherRoot.dataset.value === value) {
|
|
682
|
+
continue
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const matchingTab = [...otherRoot.querySelectorAll<HTMLElement>('[role="tab"]')]
|
|
686
|
+
.find(candidate => candidate.closest('[data-ui-tabs]') === otherRoot && candidate.dataset.value === value)
|
|
687
|
+
|
|
688
|
+
matchingTab?.click()
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
root.dispatchEvent(new CustomEvent('ui:tabs-change', {
|
|
693
|
+
bubbles: true,
|
|
694
|
+
detail: { storageKey, value }
|
|
695
|
+
}))
|
|
633
696
|
}
|
|
634
697
|
|
|
635
|
-
|
|
698
|
+
let storedValue: string | null = null
|
|
636
699
|
|
|
637
|
-
if (
|
|
700
|
+
if (root.dataset.storageKey) {
|
|
701
|
+
try {
|
|
702
|
+
storedValue = localStorage.getItem(root.dataset.storageKey)
|
|
703
|
+
} catch {
|
|
704
|
+
// Storage can be unavailable in privacy modes.
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const initialValue = storedValue ?? root.dataset.initialValue
|
|
709
|
+
|
|
710
|
+
const initiallySelected = (
|
|
711
|
+
initialValue
|
|
712
|
+
? tabs.find(tab => tab.dataset.value === initialValue)
|
|
713
|
+
: undefined
|
|
714
|
+
) ?? tabs.find(tab => tab.getAttribute('aria-selected') === 'true') ?? tabs[0]
|
|
715
|
+
|
|
716
|
+
if (initiallySelected) activate(initiallySelected, false)
|
|
638
717
|
|
|
639
718
|
for (const [index, tab] of tabs.entries()) {
|
|
640
719
|
tab.addEventListener('click', () => activate(tab))
|
|
@@ -719,9 +798,161 @@
|
|
|
719
798
|
toggle.addEventListener('click', () => {
|
|
720
799
|
const pressed = toggle.getAttribute('aria-pressed') !== 'true'
|
|
721
800
|
|
|
801
|
+
const intent = new CustomEvent('ui:toggle-intent', {
|
|
802
|
+
bubbles: true,
|
|
803
|
+
cancelable: true,
|
|
804
|
+
detail: { pressed }
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
toggle.dispatchEvent(intent)
|
|
808
|
+
|
|
809
|
+
if (intent.defaultPrevented || toggle.hasAttribute('data-ui-controlled')) return
|
|
810
|
+
|
|
722
811
|
toggle.setAttribute('aria-pressed', String(pressed))
|
|
812
|
+
})
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const initThemeToggles = (scope: ParentNode): void => {
|
|
817
|
+
const root = document.documentElement
|
|
818
|
+
const selector = '[data-ui-theme-toggle]'
|
|
819
|
+
const transitionDurationMs = 580
|
|
820
|
+
const transitionFallbackDelayMs = 750
|
|
821
|
+
const transitionZIndex = 99999
|
|
822
|
+
const animatingControls = new WeakSet<HTMLButtonElement>()
|
|
823
|
+
|
|
824
|
+
const sync = (theme: string): void => {
|
|
825
|
+
for (const control of document.querySelectorAll<HTMLButtonElement>(selector)) {
|
|
826
|
+
const darkTheme = control.dataset.darkTheme ?? 'dark'
|
|
827
|
+
const isDark = theme === darkTheme
|
|
828
|
+
|
|
829
|
+
control.setAttribute('aria-pressed', String(isDark))
|
|
830
|
+
|
|
831
|
+
control.dataset.uiTheme = theme
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const apply = (theme: string, control?: HTMLButtonElement): void => {
|
|
836
|
+
const previousTheme = root.dataset.theme
|
|
837
|
+
const darkTheme = control?.dataset.darkTheme ?? 'dark'
|
|
838
|
+
|
|
839
|
+
root.dataset.theme = theme
|
|
840
|
+
|
|
841
|
+
root.classList.toggle('dark', theme === darkTheme || theme.endsWith('-dark'))
|
|
842
|
+
|
|
843
|
+
sync(theme)
|
|
844
|
+
|
|
845
|
+
if (!control || previousTheme === theme) return
|
|
846
|
+
|
|
847
|
+
const storageKey = control.dataset.storageKey ?? 'lumen-theme'
|
|
848
|
+
|
|
849
|
+
try {
|
|
850
|
+
localStorage.setItem(storageKey, theme)
|
|
851
|
+
} catch {
|
|
852
|
+
// Storage can be unavailable in privacy modes; theme switching should still work.
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
control.dispatchEvent(new CustomEvent('ui:theme-change', {
|
|
856
|
+
bubbles: true,
|
|
857
|
+
detail: { previousTheme, storageKey, theme }
|
|
858
|
+
}))
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const transition = (control: HTMLButtonElement, theme: string): void => {
|
|
862
|
+
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
863
|
+
const isTouchDevice = window.matchMedia('(hover: none) and (pointer: coarse)').matches
|
|
864
|
+
|
|
865
|
+
if (prefersReducedMotion || isTouchDevice) {
|
|
866
|
+
apply(theme, control)
|
|
867
|
+
|
|
868
|
+
return
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
animatingControls.add(control)
|
|
872
|
+
|
|
873
|
+
control.dataset.uiThemeAnimating = 'true'
|
|
874
|
+
|
|
875
|
+
const rect = control.getBoundingClientRect()
|
|
876
|
+
const originX = Math.round(rect.left + (rect.width / 2))
|
|
877
|
+
const originY = Math.round(rect.top + (rect.height / 2))
|
|
878
|
+
|
|
879
|
+
const maximumRadius = Math.hypot(
|
|
880
|
+
Math.max(originX, window.innerWidth - originX),
|
|
881
|
+
Math.max(originY, window.innerHeight - originY)
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
const overlay = document.createElement('div')
|
|
885
|
+
const backgroundColor = getComputedStyle(document.body).backgroundColor
|
|
886
|
+
|
|
887
|
+
overlay.dataset.uiThemeTransition = ''
|
|
888
|
+
|
|
889
|
+
overlay.setAttribute('aria-hidden', 'true')
|
|
890
|
+
|
|
891
|
+
Object.assign(overlay.style, {
|
|
892
|
+
position: 'fixed',
|
|
893
|
+
inset: '0',
|
|
894
|
+
zIndex: String(transitionZIndex),
|
|
895
|
+
pointerEvents: 'none',
|
|
896
|
+
backgroundColor,
|
|
897
|
+
clipPath: `circle(${maximumRadius}px at ${originX}px ${originY}px)`,
|
|
898
|
+
willChange: 'clip-path'
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
document.body.appendChild(overlay)
|
|
902
|
+
|
|
903
|
+
apply(theme, control)
|
|
904
|
+
|
|
905
|
+
requestAnimationFrame(() => {
|
|
906
|
+
requestAnimationFrame(() => {
|
|
907
|
+
overlay.style.transition = `clip-path ${transitionDurationMs}ms cubic-bezier(0.4, 0, 0.2, 1)`
|
|
908
|
+
|
|
909
|
+
overlay.style.clipPath = `circle(0 at ${originX}px ${originY}px)`
|
|
910
|
+
|
|
911
|
+
const complete = (): void => {
|
|
912
|
+
overlay.remove()
|
|
723
913
|
|
|
724
|
-
|
|
914
|
+
animatingControls.delete(control)
|
|
915
|
+
|
|
916
|
+
delete control.dataset.uiThemeAnimating
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
overlay.addEventListener('transitionend', complete, { once: true })
|
|
920
|
+
|
|
921
|
+
window.setTimeout(complete, transitionFallbackDelayMs)
|
|
922
|
+
})
|
|
923
|
+
})
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
for (const control of scope.querySelectorAll<HTMLButtonElement>(selector)) {
|
|
927
|
+
if (control.dataset.uiBound === 'true') continue
|
|
928
|
+
|
|
929
|
+
control.dataset.uiBound = 'true'
|
|
930
|
+
|
|
931
|
+
const storageKey = control.dataset.storageKey ?? 'lumen-theme'
|
|
932
|
+
let storedTheme: string | null = null
|
|
933
|
+
|
|
934
|
+
try {
|
|
935
|
+
storedTheme = localStorage.getItem(storageKey)
|
|
936
|
+
} catch {
|
|
937
|
+
// Storage can be unavailable in privacy modes.
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const currentTheme = storedTheme ?? root.dataset.theme
|
|
941
|
+
|
|
942
|
+
if (currentTheme) {
|
|
943
|
+
apply(currentTheme)
|
|
944
|
+
} else {
|
|
945
|
+
sync(control.dataset.lightTheme ?? 'light')
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
control.addEventListener('click', () => {
|
|
949
|
+
if (control.hasAttribute('data-ui-controlled') || animatingControls.has(control)) return
|
|
950
|
+
|
|
951
|
+
const lightTheme = control.dataset.lightTheme ?? 'light'
|
|
952
|
+
const darkTheme = control.dataset.darkTheme ?? 'dark'
|
|
953
|
+
const current = root.dataset.theme
|
|
954
|
+
|
|
955
|
+
transition(control, current === darkTheme ? lightTheme : darkTheme)
|
|
725
956
|
})
|
|
726
957
|
}
|
|
727
958
|
}
|
|
@@ -1785,10 +2016,12 @@
|
|
|
1785
2016
|
}
|
|
1786
2017
|
|
|
1787
2018
|
if (dispatch) {
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
2019
|
+
const detail = { values: selectedValues }
|
|
2020
|
+
|
|
2021
|
+
root.dispatchEvent(new CustomEvent('ui:data-table-selection-change', { bubbles: true, detail }))
|
|
2022
|
+
|
|
2023
|
+
// Deprecated compatibility event. Remove in Lumen 1.0.
|
|
2024
|
+
root.dispatchEvent(new CustomEvent('ui:datatable-selection-change', { bubbles: true, detail }))
|
|
1792
2025
|
}
|
|
1793
2026
|
}
|
|
1794
2027
|
|
|
@@ -4345,140 +4578,18 @@
|
|
|
4345
4578
|
}
|
|
4346
4579
|
}
|
|
4347
4580
|
|
|
4348
|
-
const
|
|
4349
|
-
|
|
4350
|
-
|
|
4581
|
+
const initOptionalControllers = (scope: ParentNode): void => {
|
|
4582
|
+
if (!scope.querySelector(
|
|
4583
|
+
'[data-ui-back-to-top], [data-ui-scroll-reveal], [data-ui-reveal-group], [data-ui-animated-number]'
|
|
4584
|
+
)) return
|
|
4351
4585
|
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
window.
|
|
4356
|
-
|
|
4357
|
-
top: 0
|
|
4586
|
+
import('./controllers/motion.js')
|
|
4587
|
+
.then(({ initMotionControllers }) => initMotionControllers(scope))
|
|
4588
|
+
.catch(error => {
|
|
4589
|
+
window.setTimeout(() => {
|
|
4590
|
+
throw error
|
|
4358
4591
|
})
|
|
4359
4592
|
})
|
|
4360
|
-
}
|
|
4361
|
-
}
|
|
4362
|
-
|
|
4363
|
-
const initScrollReveals = (scope: ParentNode): void => {
|
|
4364
|
-
for (const root of scope.querySelectorAll<HTMLElement>(
|
|
4365
|
-
'[data-ui-scroll-reveal], [data-ui-reveal-group]'
|
|
4366
|
-
)) {
|
|
4367
|
-
if (root.dataset.uiScrollRevealBound === 'true') continue
|
|
4368
|
-
|
|
4369
|
-
root.dataset.uiScrollRevealBound = 'true'
|
|
4370
|
-
|
|
4371
|
-
if (root.matches('[data-ui-reveal-group]')) {
|
|
4372
|
-
for (const [index, child] of [...root.children].entries()) {
|
|
4373
|
-
if (child instanceof HTMLElement) {
|
|
4374
|
-
child.style.setProperty('--ui-reveal-index', String(index))
|
|
4375
|
-
}
|
|
4376
|
-
}
|
|
4377
|
-
}
|
|
4378
|
-
|
|
4379
|
-
if (
|
|
4380
|
-
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
4381
|
-
|| typeof IntersectionObserver === 'undefined'
|
|
4382
|
-
) {
|
|
4383
|
-
root.classList.add('is-revealed')
|
|
4384
|
-
|
|
4385
|
-
continue
|
|
4386
|
-
}
|
|
4387
|
-
|
|
4388
|
-
const once = root.dataset.uiRevealOnce !== 'false'
|
|
4389
|
-
|
|
4390
|
-
const threshold = Math.min(
|
|
4391
|
-
1,
|
|
4392
|
-
Math.max(0, Number(root.dataset.uiRevealThreshold) || 0)
|
|
4393
|
-
)
|
|
4394
|
-
|
|
4395
|
-
const observer = new IntersectionObserver((entries) => {
|
|
4396
|
-
const entry = entries[0]
|
|
4397
|
-
|
|
4398
|
-
if (!entry) return
|
|
4399
|
-
|
|
4400
|
-
root.classList.toggle('is-revealed', entry.isIntersecting)
|
|
4401
|
-
|
|
4402
|
-
if (entry.isIntersecting && once) observer.disconnect()
|
|
4403
|
-
}, { threshold })
|
|
4404
|
-
|
|
4405
|
-
observer.observe(root)
|
|
4406
|
-
}
|
|
4407
|
-
}
|
|
4408
|
-
|
|
4409
|
-
const initAnimatedNumbers = (scope: ParentNode): void => {
|
|
4410
|
-
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-animated-number]')) {
|
|
4411
|
-
if (root.dataset.uiBound === 'true') continue
|
|
4412
|
-
|
|
4413
|
-
const output = root.querySelector<HTMLElement>('[data-ui-animated-number-output]')
|
|
4414
|
-
const from = Number(root.dataset.uiAnimatedNumberFrom)
|
|
4415
|
-
const value = Number(root.dataset.uiAnimatedNumberValue)
|
|
4416
|
-
|
|
4417
|
-
if (!output || !Number.isFinite(from) || !Number.isFinite(value)) continue
|
|
4418
|
-
|
|
4419
|
-
root.dataset.uiBound = 'true'
|
|
4420
|
-
|
|
4421
|
-
const decimals = Math.max(
|
|
4422
|
-
0,
|
|
4423
|
-
Math.min(20, Number(root.dataset.uiAnimatedNumberDecimals) || 0)
|
|
4424
|
-
)
|
|
4425
|
-
|
|
4426
|
-
const prefix = root.dataset.uiAnimatedNumberPrefix ?? ''
|
|
4427
|
-
const suffix = root.dataset.uiAnimatedNumberSuffix ?? ''
|
|
4428
|
-
|
|
4429
|
-
const formatter = new Intl.NumberFormat(root.dataset.uiAnimatedNumberLocale || undefined, {
|
|
4430
|
-
maximumFractionDigits: decimals,
|
|
4431
|
-
minimumFractionDigits: decimals
|
|
4432
|
-
})
|
|
4433
|
-
|
|
4434
|
-
const format = (current: number): string =>
|
|
4435
|
-
`${prefix}${formatter.format(current)}${suffix}`
|
|
4436
|
-
|
|
4437
|
-
if (
|
|
4438
|
-
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
4439
|
-
|| typeof IntersectionObserver === 'undefined'
|
|
4440
|
-
) {
|
|
4441
|
-
output.textContent = format(value)
|
|
4442
|
-
|
|
4443
|
-
continue
|
|
4444
|
-
}
|
|
4445
|
-
|
|
4446
|
-
output.textContent = format(from)
|
|
4447
|
-
|
|
4448
|
-
const observer = new IntersectionObserver((entries) => {
|
|
4449
|
-
if (!entries.some(entry => entry.isIntersecting)) return
|
|
4450
|
-
|
|
4451
|
-
observer.disconnect()
|
|
4452
|
-
|
|
4453
|
-
const durationValue = getComputedStyle(root)
|
|
4454
|
-
.getPropertyValue('--ui-motion-duration')
|
|
4455
|
-
.trim()
|
|
4456
|
-
|
|
4457
|
-
const parsedDuration = Number.parseFloat(durationValue)
|
|
4458
|
-
let durationMs = 300
|
|
4459
|
-
|
|
4460
|
-
if (Number.isFinite(parsedDuration)) {
|
|
4461
|
-
durationMs = durationValue.endsWith('ms')
|
|
4462
|
-
? parsedDuration
|
|
4463
|
-
: parsedDuration * 1000
|
|
4464
|
-
}
|
|
4465
|
-
|
|
4466
|
-
const startedAt = performance.now()
|
|
4467
|
-
|
|
4468
|
-
const update = (now: number): void => {
|
|
4469
|
-
const progress = Math.min(1, (now - startedAt) / Math.max(1, durationMs))
|
|
4470
|
-
const eased = 1 - Math.pow(1 - progress, 3)
|
|
4471
|
-
|
|
4472
|
-
output.textContent = format(from + ((value - from) * eased))
|
|
4473
|
-
|
|
4474
|
-
if (progress < 1) requestAnimationFrame(update)
|
|
4475
|
-
}
|
|
4476
|
-
|
|
4477
|
-
requestAnimationFrame(update)
|
|
4478
|
-
}, { threshold: 0.15 })
|
|
4479
|
-
|
|
4480
|
-
observer.observe(root)
|
|
4481
|
-
}
|
|
4482
4593
|
}
|
|
4483
4594
|
|
|
4484
4595
|
const initUiPrimitives = (scope: ParentNode = document): void => {
|
|
@@ -4496,6 +4607,8 @@
|
|
|
4496
4607
|
|
|
4497
4608
|
initToggles(scope)
|
|
4498
4609
|
|
|
4610
|
+
initThemeToggles(scope)
|
|
4611
|
+
|
|
4499
4612
|
initCommand(scope)
|
|
4500
4613
|
|
|
4501
4614
|
initComboboxes(scope)
|
|
@@ -4554,11 +4667,7 @@
|
|
|
4554
4667
|
|
|
4555
4668
|
initTreeSelects(scope)
|
|
4556
4669
|
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
initScrollReveals(scope)
|
|
4560
|
-
|
|
4561
|
-
initAnimatedNumbers(scope)
|
|
4670
|
+
initOptionalControllers(scope)
|
|
4562
4671
|
|
|
4563
4672
|
initToasts()
|
|
4564
4673
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/* eslint-disable complexity, unicorn/consistent-function-scoping -- Optional motion controllers keep their state and formatting behavior local to the matching primitives. */
|
|
2
|
+
|
|
3
|
+
const initBackToTopButtons = (scope: ParentNode): void => {
|
|
4
|
+
for (const button of scope.querySelectorAll<HTMLButtonElement>('[data-ui-back-to-top]')) {
|
|
5
|
+
if (button.dataset.uiBound === 'true') continue
|
|
6
|
+
|
|
7
|
+
button.dataset.uiBound = 'true'
|
|
8
|
+
|
|
9
|
+
button.addEventListener('click', () => {
|
|
10
|
+
window.scrollTo({
|
|
11
|
+
behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth',
|
|
12
|
+
top: 0
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const initScrollReveals = (scope: ParentNode): void => {
|
|
19
|
+
for (const root of scope.querySelectorAll<HTMLElement>(
|
|
20
|
+
'[data-ui-scroll-reveal], [data-ui-reveal-group]'
|
|
21
|
+
)) {
|
|
22
|
+
if (root.dataset.uiScrollRevealBound === 'true') continue
|
|
23
|
+
|
|
24
|
+
root.dataset.uiScrollRevealBound = 'true'
|
|
25
|
+
|
|
26
|
+
if (root.matches('[data-ui-reveal-group]')) {
|
|
27
|
+
for (const [index, child] of [...root.children].entries()) {
|
|
28
|
+
if (child instanceof HTMLElement) {
|
|
29
|
+
child.style.setProperty('--ui-reveal-index', String(index))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
36
|
+
|| typeof IntersectionObserver === 'undefined'
|
|
37
|
+
) {
|
|
38
|
+
root.classList.add('is-revealed')
|
|
39
|
+
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const once = root.dataset.uiRevealOnce !== 'false'
|
|
44
|
+
|
|
45
|
+
const threshold = Math.min(
|
|
46
|
+
1,
|
|
47
|
+
Math.max(0, Number(root.dataset.uiRevealThreshold) || 0)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const observer = new IntersectionObserver((entries) => {
|
|
51
|
+
const entry = entries[0]
|
|
52
|
+
|
|
53
|
+
if (!entry) return
|
|
54
|
+
|
|
55
|
+
root.classList.toggle('is-revealed', entry.isIntersecting)
|
|
56
|
+
|
|
57
|
+
if (entry.isIntersecting && once) observer.disconnect()
|
|
58
|
+
}, { threshold })
|
|
59
|
+
|
|
60
|
+
observer.observe(root)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const initAnimatedNumbers = (scope: ParentNode): void => {
|
|
65
|
+
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-animated-number]')) {
|
|
66
|
+
if (root.dataset.uiBound === 'true') continue
|
|
67
|
+
|
|
68
|
+
const output = root.querySelector<HTMLElement>('[data-ui-animated-number-output]')
|
|
69
|
+
const from = Number(root.dataset.uiAnimatedNumberFrom)
|
|
70
|
+
const value = Number(root.dataset.uiAnimatedNumberValue)
|
|
71
|
+
|
|
72
|
+
if (!output || !Number.isFinite(from) || !Number.isFinite(value)) continue
|
|
73
|
+
|
|
74
|
+
root.dataset.uiBound = 'true'
|
|
75
|
+
|
|
76
|
+
const decimals = Math.max(
|
|
77
|
+
0,
|
|
78
|
+
Math.min(20, Number(root.dataset.uiAnimatedNumberDecimals) || 0)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const prefix = root.dataset.uiAnimatedNumberPrefix ?? ''
|
|
82
|
+
const suffix = root.dataset.uiAnimatedNumberSuffix ?? ''
|
|
83
|
+
|
|
84
|
+
const formatter = new Intl.NumberFormat(root.dataset.uiAnimatedNumberLocale || undefined, {
|
|
85
|
+
maximumFractionDigits: decimals,
|
|
86
|
+
minimumFractionDigits: decimals
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const format = (current: number): string =>
|
|
90
|
+
`${prefix}${formatter.format(current)}${suffix}`
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
94
|
+
|| typeof IntersectionObserver === 'undefined'
|
|
95
|
+
) {
|
|
96
|
+
output.textContent = format(value)
|
|
97
|
+
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
output.textContent = format(from)
|
|
102
|
+
|
|
103
|
+
const observer = new IntersectionObserver((entries) => {
|
|
104
|
+
if (!entries.some(entry => entry.isIntersecting)) return
|
|
105
|
+
|
|
106
|
+
observer.disconnect()
|
|
107
|
+
|
|
108
|
+
const durationValue = getComputedStyle(root)
|
|
109
|
+
.getPropertyValue('--ui-motion-duration')
|
|
110
|
+
.trim()
|
|
111
|
+
|
|
112
|
+
const parsedDuration = Number.parseFloat(durationValue)
|
|
113
|
+
let durationMs = 300
|
|
114
|
+
|
|
115
|
+
if (Number.isFinite(parsedDuration)) {
|
|
116
|
+
durationMs = durationValue.endsWith('ms')
|
|
117
|
+
? parsedDuration
|
|
118
|
+
: parsedDuration * 1000
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const startedAt = performance.now()
|
|
122
|
+
|
|
123
|
+
const update = (now: number): void => {
|
|
124
|
+
const progress = Math.min(1, (now - startedAt) / Math.max(1, durationMs))
|
|
125
|
+
const eased = 1 - Math.pow(1 - progress, 3)
|
|
126
|
+
|
|
127
|
+
output.textContent = format(from + ((value - from) * eased))
|
|
128
|
+
|
|
129
|
+
if (progress < 1) requestAnimationFrame(update)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
requestAnimationFrame(update)
|
|
133
|
+
}, { threshold: 0.15 })
|
|
134
|
+
|
|
135
|
+
observer.observe(root)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const initMotionControllers = (scope: ParentNode): void => {
|
|
140
|
+
initBackToTopButtons(scope)
|
|
141
|
+
|
|
142
|
+
initScrollReveals(scope)
|
|
143
|
+
|
|
144
|
+
initAnimatedNumbers(scope)
|
|
145
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "@santi020k/lumen/layers.css";
|