torch-glare 1.0.2

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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +207 -0
  3. package/cli/bin/addComponent.js +278 -0
  4. package/cli/bin/addHooks.js +75 -0
  5. package/cli/bin/addLayout.js +71 -0
  6. package/cli/bin/addProvider.js +71 -0
  7. package/cli/bin/addUtils.js +74 -0
  8. package/cli/bin/cli.js +73 -0
  9. package/cli/bin/init/init.js +15 -0
  10. package/cli/bin/init/tailwindInit.js +174 -0
  11. package/cli/bin/update.js +147 -0
  12. package/lib/components/ActionButton.tsx +63 -0
  13. package/lib/components/ActionsGroup.tsx +34 -0
  14. package/lib/components/AlertDialog.tsx +211 -0
  15. package/lib/components/Badge.tsx +116 -0
  16. package/lib/components/BadgeField.tsx +192 -0
  17. package/lib/components/Button.tsx +277 -0
  18. package/lib/components/Card.tsx +63 -0
  19. package/lib/components/Checkbox.tsx +122 -0
  20. package/lib/components/CountBadge.tsx +54 -0
  21. package/lib/components/DatePicker.tsx +464 -0
  22. package/lib/components/Drawer.tsx +118 -0
  23. package/lib/components/DropdownMenu.tsx +399 -0
  24. package/lib/components/FieldHint.tsx +76 -0
  25. package/lib/components/ImageAttachment.tsx +180 -0
  26. package/lib/components/InnerLabelField.tsx +155 -0
  27. package/lib/components/Input.tsx +179 -0
  28. package/lib/components/InputField.tsx +147 -0
  29. package/lib/components/Label.tsx +107 -0
  30. package/lib/components/LabelField.tsx +75 -0
  31. package/lib/components/LabeledCheckBox.tsx +65 -0
  32. package/lib/components/LabeledRadio.tsx +45 -0
  33. package/lib/components/LinkButton.tsx +94 -0
  34. package/lib/components/LoginButton.tsx +56 -0
  35. package/lib/components/PasswordLevel.tsx +58 -0
  36. package/lib/components/Popover.tsx +274 -0
  37. package/lib/components/ProfileMenu.tsx +90 -0
  38. package/lib/components/Radio.tsx +77 -0
  39. package/lib/components/RadioCard.tsx +72 -0
  40. package/lib/components/RingLoading.tsx +190 -0
  41. package/lib/components/SearchField.tsx +49 -0
  42. package/lib/components/Select.tsx +417 -0
  43. package/lib/components/SlideDatePicker.tsx +120 -0
  44. package/lib/components/SpinLoading.tsx +190 -0
  45. package/lib/components/Switcher.tsx +56 -0
  46. package/lib/components/TabFormItem.tsx +158 -0
  47. package/lib/components/Table.tsx +395 -0
  48. package/lib/components/Textarea.tsx +108 -0
  49. package/lib/components/Tooltip.tsx +111 -0
  50. package/lib/components/TransparentLabel.tsx +72 -0
  51. package/lib/components/TreeDropDown.tsx +69 -0
  52. package/lib/hooks/MobileSlidePicker/components/Picker.tsx +218 -0
  53. package/lib/hooks/MobileSlidePicker/components/PickerColumn.tsx +238 -0
  54. package/lib/hooks/MobileSlidePicker/components/PickerItem.tsx +64 -0
  55. package/lib/hooks/MobileSlidePicker/index.ts +10 -0
  56. package/lib/hooks/useActiveTreeItem.tsx +61 -0
  57. package/lib/hooks/useClickOutside.tsx +20 -0
  58. package/lib/hooks/useResize.tsx +78 -0
  59. package/lib/layouts/CLayout.tsx +326 -0
  60. package/lib/layouts/FieldSection.tsx +64 -0
  61. package/lib/layouts/TreeSubLayout.tsx +187 -0
  62. package/lib/providers/ThemeProvider.tsx +99 -0
  63. package/lib/utils/cn.ts +6 -0
  64. package/lib/utils/convertImageFileToDataUrl.ts +17 -0
  65. package/lib/utils/resize.ts +35 -0
  66. package/lib/utils/types.ts +12 -0
  67. package/package.json +28 -0
  68. package/torch-glare.js +24 -0
@@ -0,0 +1,69 @@
1
+ 'use client'
2
+ import { cn } from "../utils/cn";
3
+ import { cva, VariantProps } from "class-variance-authority";
4
+ import { HTMLAttributes, ReactNode, useEffect, useState } from "react";
5
+
6
+ const TreeDropDownVariants = cva(["flex px-[6px] h-[40px] gap-2 justify-start items-center w-full",
7
+ "text-content-system-global-primary border-l-[2px] rtl:border-r-[2px] border-transparent outline-none",
8
+ "hover:bg-white-alpha-075 hover:border-black-300 hover:text-content-system-action-primary-hover hover:gap-[14px]",
9
+ "rounded-r-[4px] text-start whitespace-nowrap transition-all duration-150 ease-in-out",
10
+ ], {
11
+ variants: {
12
+ variant: {
13
+ secondary: "",
14
+ default: ""
15
+ },
16
+ active: {
17
+ true: "hover:gap-[8px]"
18
+ }
19
+ },
20
+ compoundVariants: [
21
+ {
22
+ active: true,
23
+ variant: "default",
24
+ className: [
25
+ "bg-background-system-action-primary-hover border-border-system-action-primary-hover [&_button]:bg-purple-alpha-15 hover:bg-background-system-action-primary-hover hover:border-border-system-action-primary-hover",
26
+ ]
27
+ },
28
+ {
29
+ active: true,
30
+ variant: "secondary",
31
+ className: [
32
+ "bg-wavy-navy-1000 border-border-system-action-field-hover-selected [&_button]:bg-blue-sparkle-alpha-15 hover:bg-wavy-navy-1000 hover:border-border-system-action-field-hover-selected",
33
+ ]
34
+ }
35
+ ],
36
+ defaultVariants: {},//
37
+
38
+ });
39
+
40
+ interface Props extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof TreeDropDownVariants> {
41
+ theme?: "dark" | "light" | "default";
42
+ treeLabel: ReactNode;
43
+ open?: boolean;
44
+ variant?: "secondary" | "default";
45
+ childrenContainerClassName?: string
46
+ }
47
+
48
+ export const TreeDropDown = ({ childrenContainerClassName, className, variant = "secondary", treeLabel, open, theme, ...props }: Props) => {
49
+ const [isActive, setIsActive] = useState(open);
50
+ useEffect(() => {
51
+ setIsActive(open)
52
+ }, [open])
53
+ return (
54
+ <div {...props} className={cn("flex h-fit flex-col transition-all ease-in-out duration-500",)}>
55
+ <div onClick={() => setIsActive(!isActive)} data-theme={theme} className={cn(TreeDropDownVariants({ variant, active: isActive }), className)}>
56
+ <button className={cn("outline-none border-none flex-0 leading-0 transition-transform ease-in-out flex justify-center items-center bg-background-system-body-tertiary h-[28px] w-[28px] rounded-full text-[20px] text-content-system-global-primary", { "rotate-180": isActive })}>
57
+ <i className={cn("leading-none ri-arrow-down-s-line ")}></i>
58
+ </button>
59
+ <div className={cn("text-content-system-global-primary typography-body-medium-medium transition-all ease-in-out duration-100 flex-1")}>{treeLabel}</div>
60
+ </div>
61
+ <div className={cn("mt-0 pl-[22px] relative overflow-auto scrollbar-hide transition-all duration-500 ease-in-out", {
62
+ "max-h-[20000px] mt-1": isActive, "max-h-0": !isActive,
63
+ })}>
64
+ <span className="h-full w-[1px] bg-border-system-global-primary absolute left-[21px] top-0 rounded-sm z-10" />
65
+ <div className={cn("h-full flex flex-col gap-1 w-full", childrenContainerClassName)}>{props.children}</div>
66
+ </div>
67
+ </div>
68
+ );
69
+ };
@@ -0,0 +1,218 @@
1
+ import { CSSProperties, HTMLProps, MutableRefObject, createContext, useCallback, useContext, useMemo, useReducer } from 'react'
2
+
3
+ const DEFAULT_HEIGHT = 216
4
+ const DEFAULT_ITEM_HEIGHT = 36
5
+ const DEFAULT_WHEEL_MODE = 'off'
6
+
7
+ interface Option {
8
+ value: string | number
9
+ element: MutableRefObject<HTMLElement | null>
10
+ }
11
+
12
+ export interface PickerValue {
13
+ [key: string]: string | number
14
+ }
15
+
16
+ export interface PickerRootProps<TType extends PickerValue> extends Omit<HTMLProps<HTMLDivElement>, 'value' | 'onChange'> {
17
+ value: TType
18
+ onChange: (value: TType, key: string) => void
19
+ height?: number
20
+ itemHeight?: number
21
+ wheelMode?: 'off' | 'natural' | 'normal'
22
+ }
23
+
24
+ const PickerDataContext = createContext<{
25
+ height: number
26
+ itemHeight: number
27
+ wheelMode: 'off' | 'natural' | 'normal'
28
+ value: PickerValue
29
+ optionGroups: { [key: string]: Option[] }
30
+ } | null>(null)
31
+ PickerDataContext.displayName = 'PickerDataContext'
32
+
33
+ export function usePickerData(componentName: string) {
34
+ const context = useContext(PickerDataContext)
35
+ if (context === null) {
36
+ const error = new Error(`<${componentName} /> is missing a parent <Picker /> component.`)
37
+ if (Error.captureStackTrace) {
38
+ Error.captureStackTrace(error, usePickerData)
39
+ }
40
+ throw error
41
+ }
42
+ return context
43
+ }
44
+
45
+ const PickerActionsContext = createContext<{
46
+ registerOption(key: string, option: Option): () => void
47
+ change(key: string, value: string | number): boolean
48
+ } | null>(null)
49
+ PickerActionsContext.displayName = 'PickerActionsContext'
50
+
51
+ export function usePickerActions(componentName: string) {
52
+ const context = useContext(PickerActionsContext)
53
+ if (context === null) {
54
+ const error = new Error(`<${componentName} /> is missing a parent <Picker /> component.`)
55
+ if (Error.captureStackTrace) {
56
+ Error.captureStackTrace(error, usePickerActions)
57
+ }
58
+ throw error
59
+ }
60
+ return context
61
+ }
62
+
63
+ function sortByDomNode<T>(
64
+ nodes: T[],
65
+ resolveKey: (item: T) => HTMLElement | null = (i) => i as unknown as HTMLElement | null
66
+ ): T[] {
67
+ return nodes.slice().sort((aItem, zItem) => {
68
+ const a = resolveKey(aItem)
69
+ const z = resolveKey(zItem)
70
+
71
+ if (a === null || z === null) return 0
72
+
73
+ const position = a.compareDocumentPosition(z)
74
+
75
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1
76
+ if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1
77
+ return 0
78
+ })
79
+ }
80
+
81
+ function pickerReducer(
82
+ optionGroups: { [key: string]: Option[] },
83
+ action: {
84
+ type: 'REGISTER_OPTION' | 'UNREGISTER_OPTION'
85
+ key: string
86
+ option: Option
87
+ }
88
+ ) {
89
+ switch (action.type) {
90
+ case 'REGISTER_OPTION': {
91
+ const { key, option } = action
92
+ let nextOptionsForKey = [...(optionGroups[key] || []), option]
93
+ nextOptionsForKey = sortByDomNode(nextOptionsForKey, (o) => o.element.current)
94
+ return {
95
+ ...optionGroups,
96
+ [key]: nextOptionsForKey,
97
+ }
98
+ }
99
+ case 'UNREGISTER_OPTION': {
100
+ const { key, option } = action
101
+ return {
102
+ ...optionGroups,
103
+ [key]: (optionGroups[key] || []).filter((o) => o !== option),
104
+ }
105
+ }
106
+ default: {
107
+ throw Error(`Unknown action: ${action.type as string}`);
108
+ }
109
+ }
110
+ }
111
+
112
+ function PickerRoot<TType extends PickerValue>(props: PickerRootProps<TType> | any) {
113
+ const {
114
+ style,
115
+ children,
116
+ value,
117
+ onChange,
118
+ height = DEFAULT_HEIGHT,
119
+ itemHeight = DEFAULT_ITEM_HEIGHT,
120
+ wheelMode = DEFAULT_WHEEL_MODE,
121
+ selectContainerClassName,
122
+ ...restProps
123
+ } = props
124
+
125
+ const highlightStyle = useMemo<CSSProperties>(
126
+ () => ({
127
+ height: itemHeight,
128
+ marginTop: -(itemHeight / 2),
129
+ position: 'absolute',
130
+ top: '50%',
131
+ left: 0,
132
+ width: '100%',
133
+ pointerEvents: 'none',
134
+ }),
135
+ [itemHeight]
136
+ )
137
+ const containerStyle = useMemo<CSSProperties>(
138
+ () => ({
139
+ height: `${height}px`,
140
+ position: 'relative',
141
+ display: 'flex',
142
+ justifyContent: 'center',
143
+ overflow: 'hidden',
144
+ maskImage: 'linear-gradient(to top, transparent, transparent 10%, white 50%, white 60%, transparent 90%, transparent)',
145
+ WebkitMaskImage: 'linear-gradient(to top, transparent, transparent 10%, white 50%, white 60%, transparent 90%, transparent)',
146
+ }),
147
+ [height]
148
+ )
149
+
150
+ const [optionGroups, dispatch] = useReducer(pickerReducer, {})
151
+
152
+ const pickerData = useMemo(
153
+ () => ({ height, itemHeight, wheelMode, value, optionGroups }),
154
+ [height, itemHeight, value, optionGroups, wheelMode]
155
+ )
156
+
157
+ const triggerChange = useCallback((key: string, nextValue: string) => {
158
+ if (value[key] === nextValue) return false
159
+ const nextPickerValue = { ...value, [key]: nextValue }
160
+ onChange(nextPickerValue, key)
161
+ return true
162
+ }, [onChange, value])
163
+ const registerOption = useCallback((key: string, option: Option) => {
164
+ dispatch({ type: 'REGISTER_OPTION', key, option })
165
+ return () => dispatch({ type: 'UNREGISTER_OPTION', key, option })
166
+ }, [])
167
+ const pickerActions = useMemo(
168
+ () => ({ registerOption, change: triggerChange }),
169
+ [registerOption, triggerChange]
170
+ )
171
+
172
+ return (
173
+ <div
174
+ style={{
175
+ ...containerStyle,
176
+ ...style,
177
+ }}
178
+ {...restProps}
179
+ >
180
+ <PickerActionsContext.Provider value={pickerActions}>
181
+ <PickerDataContext.Provider value={pickerData}>
182
+ {children}
183
+ </PickerDataContext.Provider>
184
+ </PickerActionsContext.Provider>
185
+ <div
186
+ className={selectContainerClassName}
187
+ style={highlightStyle}
188
+ >
189
+ <div
190
+ style={{
191
+ position: 'absolute',
192
+ top: 0,
193
+ bottom: 'auto',
194
+ left: 0,
195
+ right: 'auto',
196
+ width: '100%',
197
+ height: '1px',
198
+ transform: 'scaleY(0.5)',
199
+ }}
200
+ />
201
+ <div
202
+ style={{
203
+ position: 'absolute',
204
+ top: 'auto',
205
+ bottom: 0,
206
+ left: 0,
207
+ right: 'auto',
208
+ width: '100%',
209
+ height: '1px',
210
+ transform: 'scaleY(0.5)',
211
+ }}
212
+ />
213
+ </div>
214
+ </div>
215
+ )
216
+ }
217
+
218
+ export default PickerRoot
@@ -0,0 +1,238 @@
1
+ import { CSSProperties, HTMLProps, createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"
2
+ import { usePickerActions, usePickerData } from "./Picker"
3
+
4
+ interface PickerColumnProps extends HTMLProps<HTMLDivElement> {
5
+ name: string
6
+ }
7
+
8
+ const PickerColumnDataContext = createContext<{
9
+ key: string
10
+ } | null>(null)
11
+ PickerColumnDataContext.displayName = 'PickerColumnDataContext'
12
+
13
+ export function useColumnData(componentName: string) {
14
+ const context = useContext(PickerColumnDataContext)
15
+ if (context === null) {
16
+ const error = new Error(`<${componentName} /> is missing a parent <Picker.Column /> component.`)
17
+ if (Error.captureStackTrace) {
18
+ Error.captureStackTrace(error, useColumnData)
19
+ }
20
+ throw error
21
+ }
22
+ return context
23
+ }
24
+
25
+ function PickerColumn({
26
+ style,
27
+ children,
28
+ name: key,
29
+ ...restProps
30
+ }: PickerColumnProps) {
31
+ const { height, itemHeight, wheelMode, value: groupValue, optionGroups } = usePickerData('Picker.Column')
32
+
33
+ // Caculate the selected index
34
+ const value = useMemo(
35
+ () => groupValue[key],
36
+ [groupValue, key],
37
+ )
38
+ const options = useMemo(
39
+ () => optionGroups[key] || [],
40
+ [key, optionGroups],
41
+ )
42
+ const selectedIndex = useMemo(
43
+ () => {
44
+ let index = options.findIndex((o) => o.value === value)
45
+ if (index < 0) {
46
+ index = 0
47
+ }
48
+ return index
49
+ },
50
+ [options, value],
51
+ )
52
+
53
+ // Caculate the translate of scroller
54
+ const minTranslate = useMemo(
55
+ () => height / 2 - itemHeight * options.length + itemHeight / 2,
56
+ [height, itemHeight, options],
57
+ )
58
+ const maxTranslate = useMemo(
59
+ () => height / 2 - itemHeight / 2,
60
+ [height, itemHeight],
61
+ )
62
+ const [scrollerTranslate, setScrollerTranslate] = useState<number>(0)
63
+ useEffect(() => {
64
+ setScrollerTranslate(height / 2 - itemHeight / 2 - selectedIndex * itemHeight)
65
+ }, [height, itemHeight, selectedIndex])
66
+
67
+ // A handler to trigger the value change
68
+ const pickerActions = usePickerActions('Picker.Column')
69
+ const translateRef = useRef<number>(scrollerTranslate)
70
+ translateRef.current = scrollerTranslate
71
+ const handleScrollerTranslateSettled = useCallback(() => {
72
+ let nextActiveIndex = 0
73
+ const currentTrans = translateRef.current
74
+ if (currentTrans >= maxTranslate) {
75
+ nextActiveIndex = 0
76
+ } else if (currentTrans <= minTranslate) {
77
+ nextActiveIndex = options.length - 1
78
+ } else {
79
+ nextActiveIndex = -Math.round((currentTrans - maxTranslate) / itemHeight)
80
+ }
81
+
82
+ const changed = pickerActions.change(key, options[nextActiveIndex].value)
83
+ if (!changed) {
84
+ setScrollerTranslate(height / 2 - itemHeight / 2 - nextActiveIndex * itemHeight)
85
+ }
86
+ }, [pickerActions, height, itemHeight, key, maxTranslate, minTranslate, options])
87
+
88
+ // Handle touch events
89
+ const [startScrollerTranslate, setStartScrollerTranslate] = useState<number>(0)
90
+ const [isMoving, setIsMoving] = useState<boolean>(false)
91
+ const [startTouchY, setStartTouchY] = useState<number>(0)
92
+
93
+ const updateScrollerWhileMoving = useCallback((nextScrollerTranslate: number) => {
94
+ if (nextScrollerTranslate < minTranslate) {
95
+ nextScrollerTranslate = minTranslate - Math.pow(minTranslate - nextScrollerTranslate, 0.8)
96
+ } else if (nextScrollerTranslate > maxTranslate) {
97
+ nextScrollerTranslate = maxTranslate + Math.pow(nextScrollerTranslate - maxTranslate, 0.8)
98
+ }
99
+ setScrollerTranslate(nextScrollerTranslate)
100
+ }, [maxTranslate, minTranslate])
101
+
102
+ const handleTouchStart = useCallback((event: React.TouchEvent) => {
103
+ setStartTouchY(event.targetTouches[0].pageY)
104
+ setStartScrollerTranslate(scrollerTranslate)
105
+ }, [scrollerTranslate])
106
+
107
+ const handleTouchMove = useCallback((event: TouchEvent) => {
108
+ if (event.cancelable) {
109
+ event.preventDefault()
110
+ }
111
+
112
+ if (!isMoving) {
113
+ setIsMoving(true)
114
+ }
115
+
116
+ const nextScrollerTranslate = startScrollerTranslate + event.targetTouches[0].pageY - startTouchY
117
+ updateScrollerWhileMoving(nextScrollerTranslate)
118
+ }, [isMoving, startScrollerTranslate, startTouchY, updateScrollerWhileMoving])
119
+
120
+ const handleTouchEnd = useCallback(() => {
121
+ if (!isMoving) {
122
+ return
123
+ }
124
+ setIsMoving(false)
125
+ setStartTouchY(0)
126
+ setStartScrollerTranslate(0)
127
+
128
+ handleScrollerTranslateSettled()
129
+ }, [handleScrollerTranslateSettled, isMoving])
130
+
131
+ const handleTouchCancel = useCallback(() => {
132
+ if (!isMoving) {
133
+ return
134
+ }
135
+ setIsMoving(false)
136
+ setStartTouchY(0)
137
+ setScrollerTranslate(startScrollerTranslate)
138
+ setStartScrollerTranslate(0)
139
+ }, [isMoving, startScrollerTranslate])
140
+
141
+ // Handle wheel events
142
+ const wheelingTimer = useRef<number | null>(null)
143
+
144
+ const handleWheeling = useCallback((event: WheelEvent) => {
145
+ if (event.deltaY === 0) {
146
+ return
147
+ }
148
+ let delta = event.deltaY * 0.1
149
+ if (Math.abs(delta) < itemHeight) {
150
+ delta = itemHeight * Math.sign(delta)
151
+ }
152
+ if (wheelMode === 'normal') {
153
+ delta = -delta
154
+ }
155
+
156
+
157
+ const nextScrollerTranslate = scrollerTranslate + delta
158
+ updateScrollerWhileMoving(nextScrollerTranslate)
159
+ }, [itemHeight, scrollerTranslate, updateScrollerWhileMoving, wheelMode])
160
+
161
+ const handleWheelEnd = useCallback(() => {
162
+ handleScrollerTranslateSettled()
163
+ }, [handleScrollerTranslateSettled])
164
+
165
+ const handleWheel = useCallback((event: WheelEvent) => {
166
+ if (wheelMode === 'off') {
167
+ return
168
+ }
169
+
170
+ if (event.cancelable) {
171
+ event.preventDefault()
172
+ }
173
+
174
+ handleWheeling(event)
175
+
176
+ if (wheelingTimer.current) {
177
+ clearTimeout(wheelingTimer.current)
178
+ }
179
+
180
+ wheelingTimer.current = setTimeout(() => {
181
+ handleWheelEnd()
182
+ }, 200) as unknown as number
183
+ }, [handleWheelEnd, handleWheeling, wheelingTimer, wheelMode])
184
+
185
+ // 'touchmove' and 'wheel' should not be passive
186
+ const containerRef = useRef<HTMLDivElement | null>(null)
187
+ useEffect(() => {
188
+ const container = containerRef.current
189
+ if (container) {
190
+ container.addEventListener('touchmove', handleTouchMove, { passive: false })
191
+ container.addEventListener('wheel', handleWheel, { passive: false })
192
+ }
193
+ return () => {
194
+ if (container) {
195
+ container.removeEventListener('touchmove', handleTouchMove)
196
+ container.removeEventListener('wheel', handleWheel)
197
+ }
198
+ }
199
+ }, [handleTouchMove, handleWheel])
200
+
201
+ const columnStyle = useMemo<CSSProperties>(
202
+ () => ({
203
+ flex: '1 1 0%',
204
+ maxHeight: '100%',
205
+ transitionProperty: 'transform',
206
+ transitionTimingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
207
+ transitionDuration: isMoving ? '0ms' : '300ms',
208
+ transform: `translate3d(0, ${scrollerTranslate}px, 0)`,
209
+ }),
210
+ [scrollerTranslate, isMoving],
211
+ )
212
+
213
+ const columnData = useMemo(
214
+ () => ({ key }),
215
+ [key],
216
+ )
217
+
218
+ return (
219
+ <div
220
+ style={{
221
+ ...columnStyle,
222
+ ...style,
223
+ }}
224
+ ref={containerRef}
225
+ onTouchStart={handleTouchStart}
226
+ onTouchEnd={handleTouchEnd}
227
+ onTouchCancel={handleTouchCancel}
228
+ {...restProps}
229
+ >
230
+ <PickerColumnDataContext.Provider value={columnData}>
231
+ {children}
232
+ </PickerColumnDataContext.Provider>
233
+ </div>
234
+ )
235
+ }
236
+
237
+
238
+ export default PickerColumn
@@ -0,0 +1,64 @@
1
+ import { HTMLProps, ReactNode, useCallback, useEffect, useMemo, useRef } from 'react'
2
+ import { usePickerActions, usePickerData } from './Picker.tsx'
3
+ import { useColumnData } from './PickerColumn.tsx'
4
+
5
+ interface PickerItemRenderProps {
6
+ selected: boolean
7
+ }
8
+
9
+ export interface PickerItemProps extends Omit<HTMLProps<HTMLDivElement>, 'value' | 'children'> {
10
+ children: ReactNode | ((renderProps: PickerItemRenderProps) => ReactNode)
11
+ value: string | number
12
+ }
13
+
14
+ // eslint-disable-next-line
15
+ function isFunction(functionToCheck: any): functionToCheck is Function {
16
+ return typeof functionToCheck === 'function'
17
+ }
18
+
19
+ function PickerItem({
20
+ style,
21
+ children,
22
+ value,
23
+ ...restProps
24
+ }: PickerItemProps) {
25
+ const optionRef = useRef<HTMLDivElement | null>(null)
26
+ const { itemHeight, value: pickerValue } = usePickerData('Picker.Item')
27
+ const pickerActions = usePickerActions('Picker.Item')
28
+ const { key } = useColumnData('Picker.Item')
29
+
30
+ useEffect(
31
+ () => pickerActions.registerOption(key, { value, element: optionRef }),
32
+ [key, pickerActions, value],
33
+ )
34
+
35
+ const itemStyle = useMemo(
36
+ () => ({
37
+ height: `${itemHeight}px`,
38
+ display: 'flex',
39
+ justifyContent: 'center',
40
+ alignItems: 'center',
41
+ }),
42
+ [itemHeight],
43
+ )
44
+
45
+ const handleClick = useCallback(() => {
46
+ pickerActions.change(key, value)
47
+ }, [pickerActions, key, value])
48
+
49
+ return (
50
+ <div
51
+ style={{
52
+ ...itemStyle,
53
+ ...style,
54
+ }}
55
+ ref={optionRef}
56
+ onClick={handleClick}
57
+ {...restProps}
58
+ >
59
+ {isFunction(children) ? children({ selected: pickerValue[key] === value }) : children}
60
+ </div>
61
+ )
62
+ }
63
+
64
+ export default PickerItem
@@ -0,0 +1,10 @@
1
+ import Picker, { PickerValue, PickerRootProps as PickerProps } from './components/Picker.tsx'
2
+ import Column from './components/PickerColumn.tsx'
3
+ import Item from './components/PickerItem.tsx'
4
+
5
+ export type { PickerProps, PickerValue }
6
+
7
+ export default Object.assign(Picker, {
8
+ Column,
9
+ Item,
10
+ })
@@ -0,0 +1,61 @@
1
+ 'use client';
2
+ import { useState, useEffect } from "react";
3
+
4
+ export function useActiveTreeItem(itemIds: string[]) {
5
+ const [activeId, setActiveId] = useState<string | null>(null);
6
+
7
+ useEffect(() => {
8
+ if (!itemIds || itemIds.length === 0) {
9
+ console.warn("No itemIds provided to useActiveTreeItem.");
10
+ return;
11
+ }
12
+
13
+ const observer = new IntersectionObserver(
14
+ (entries) => {
15
+ let mostVisibleEntry: any = null;
16
+
17
+ entries.forEach((entry) => {
18
+ if (entry.isIntersecting) {
19
+ // Track the most visible entry (highest intersection ratio)
20
+ if (
21
+ !mostVisibleEntry ||
22
+ entry.intersectionRatio > mostVisibleEntry.intersectionRatio
23
+ ) {
24
+ mostVisibleEntry = entry;
25
+ }
26
+ }
27
+ });
28
+
29
+ if (mostVisibleEntry) {
30
+ setActiveId(mostVisibleEntry.target.id);
31
+ }
32
+ },
33
+ {
34
+ rootMargin: '-10% 0% -5% 0%', // Adjust based on your layout
35
+ threshold: [0, 0.25, 0.5, 0.75, 1], // Multiple thresholds for better accuracy
36
+ }
37
+ );
38
+
39
+ // Observe all elements
40
+ itemIds.forEach((id) => {
41
+ const element = document.getElementById(id);
42
+ if (element) {
43
+ observer.observe(element);
44
+ } else {
45
+ console.warn(`Element with id "${id}" not found.`);
46
+ }
47
+ });
48
+
49
+ // Cleanup observer
50
+ return () => {
51
+ itemIds.forEach((id) => {
52
+ const element = document.getElementById(id);
53
+ if (element) {
54
+ observer.unobserve(element);
55
+ }
56
+ });
57
+ };
58
+ }, [itemIds]);
59
+
60
+ return { activeId };
61
+ }
@@ -0,0 +1,20 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ export function useClickOutside<T extends HTMLElement>(callback: () => void) {
4
+ const ref = useRef<T>(null);
5
+
6
+ useEffect(() => {
7
+ function handleClickOutside(event: MouseEvent) {
8
+ if (ref.current && !ref.current.contains(event.target as Node)) {
9
+ callback();
10
+ }
11
+ }
12
+
13
+ document.addEventListener("click", handleClickOutside);
14
+ return () => document.removeEventListener("click", handleClickOutside);
15
+ }, [callback]);
16
+
17
+ return ref;
18
+ }
19
+
20
+