@startupjs-ui/div 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ ## [0.1.3](https://github.com/startupjs/startupjs-ui/compare/v0.1.2...v0.1.3) (2025-12-29)
7
+
8
+ **Note:** Version bump only for package @startupjs-ui/div
9
+
10
+
11
+
12
+
13
+
14
+ ## [0.1.2](https://github.com/startupjs/startupjs-ui/compare/v0.1.1...v0.1.2) (2025-12-29)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * disable wrong TS error ([199c088](https://github.com/startupjs/startupjs-ui/commit/199c088718256d88bef037536c9f1e71fd39c7be))
20
+ * **div:** fix tooltip typings ([97f7bf7](https://github.com/startupjs/startupjs-ui/commit/97f7bf7bcf289f0430e8ff5c72c1bfb21207dca2))
21
+ * **div:** Refactor to use Pressable and get rid of an extra wrapper when the Div is pressable ([9e7c852](https://github.com/startupjs/startupjs-ui/commit/9e7c852f383f540cab58b7a31aba0bd64a531adc))
22
+ * **div:** refactor tooltip-related logic to move it fully out into a separate hook to decorate props ([2fa9cf0](https://github.com/startupjs/startupjs-ui/commit/2fa9cf0821a6c726d5e2ad621a6c15d4091e59c7))
23
+ * remove async ([9782d01](https://github.com/startupjs/startupjs-ui/commit/9782d01aa728b4970cf3919055a87eaee27a5d16))
24
+ * remove async await in components ([67c97ec](https://github.com/startupjs/startupjs-ui/commit/67c97eccce202095f0f4c85b1d7c204f6cdc4a6e))
25
+ * specify explicit return for react components ([20648e6](https://github.com/startupjs/startupjs-ui/commit/20648e67c9e004c1e7e8eb01a92bbaf36a0362d0))
26
+
27
+
28
+ ### Features
29
+
30
+ * add mdx and docs packages. Refactor docs to get rid of any @startupjs/ui usage and use startupjs-ui instead ([703c926](https://github.com/startupjs/startupjs-ui/commit/703c92636efb0421ffd11783f692fc892b74018f))
31
+ * auto-generate .d.ts files for components on commit ([7d51efe](https://github.com/startupjs/startupjs-ui/commit/7d51efed601aabd217670490ae1cd438b7852970))
32
+ * **div:** bring back tooltip support ([7ea1f62](https://github.com/startupjs/startupjs-ui/commit/7ea1f62b2537f536c9c01803c3a4ae61fbcc17b0))
33
+ * **div:** Migrate Div component ([d43c2f7](https://github.com/startupjs/startupjs-ui/commit/d43c2f784fab74cb4537932d30ecb974dbf4541a))
34
+ * **icon:** refactor Icon component ([70237be](https://github.com/startupjs/startupjs-ui/commit/70237be3cd372154f1010def67735ef08db0c02a))
package/README.mdx ADDED
@@ -0,0 +1,212 @@
1
+ import { useState } from 'react'
2
+ import { pug, u } from 'startupjs'
3
+ import { Sandbox } from '@startupjs-ui/docs'
4
+ import { styl } from 'startupjs'
5
+ import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'
6
+ import Div, { _PropsJsonSchema as DivPropsJsonSchema } from './index.tsx'
7
+ import Span from '@startupjs-ui/span'
8
+ import Icon from '@startupjs-ui/icon'
9
+
10
+ # Div
11
+
12
+ Div is a basic building block for layouts. It is a wrapper around `View` with some extra features.
13
+
14
+ ```jsx
15
+ import { Div } from 'startupjs-ui'
16
+ ```
17
+
18
+ ## Simple example
19
+ ```jsx example
20
+ return (
21
+ <Div>
22
+ <Span>Div</Span>
23
+ </Div>
24
+ )
25
+ ```
26
+
27
+ ## Clickable
28
+
29
+ ```jsx example
30
+ const divStyle = {
31
+ width: 150,
32
+ height: 100,
33
+ alignItems: 'center',
34
+ justifyContent: 'center',
35
+ backgroundColor: '#00AED6'
36
+ }
37
+ const [counter, setCounter] = useState(0)
38
+ return (
39
+ <Div
40
+ style={divStyle}
41
+ onLongPress={() => setCounter(counter + 10)}
42
+ onPress={() => setCounter(counter + 1)}
43
+ >
44
+ <Span>Clicked {counter} times</Span>
45
+ </Div>
46
+ )
47
+ ```
48
+
49
+ ## Layout
50
+
51
+ You can specify properties to manage layout:
52
+
53
+ - `row` - aligns children from left to right (default: `false`)
54
+ - `reverse` - aligns the children in reverse order depending on the direction (default: `false`)
55
+ - `wrap` - controls the wrapping of children into multiple rows (default: `false`)
56
+ - `align` - controls horizontal alignment (possible values: `left`, `center`, `right`)
57
+ - `vAlign` - controls vertical alignment (possible values: `top`, `center`, `bottom`)
58
+ - `gap` - defines the size of the gap between items in [units](/docs/tutorial/TricksWithStyles#units) or the value `true` is equivalent to `2`
59
+
60
+ ```jsx example
61
+ return (
62
+ <Div
63
+ style={{ height: u(10), backgroundColor: 'yellow' }}
64
+ row
65
+ align='center'
66
+ vAlign='center'
67
+ >
68
+ <Div style={{ width: u(5), height: u(5), backgroundColor: 'red' }} />
69
+ <Div style={{ width: u(5), height: u(5), backgroundColor: 'blue' }} pushed />
70
+ </Div>
71
+ )
72
+ ```
73
+
74
+ ## Full width (or height)
75
+
76
+ To make `Div` take all remaining space in the parent container (according to its `flex-direction`), pass the `full` property.
77
+
78
+ This property just sets `flex: 1` to it.
79
+
80
+ ```jsx example
81
+ return (
82
+ <Div row vAlign='center'>
83
+ <Div full>
84
+ <Span>Tesla Model S</Span>
85
+ </Div>
86
+ <Span description>1000 HP</Span>
87
+ </Div>
88
+ )
89
+ ```
90
+
91
+ ## Indentation between multiple Div
92
+
93
+ When displaying multiple Div(s) in a line spacing between them can be adjusted using `pushed` prop to specify indent from the previous Div. Possible values of `pushed` prop can be found in the `Sandbox` section at the bottom of the page. The `true` value of prop is equivalent to `m` value.
94
+
95
+ Though instead of pushing each individual Div, in most cases it is better to use `gap` prop on the parent Div to set spacing between all children at once.
96
+
97
+ ```jsx example
98
+ const divStyle = {
99
+ width: 100,
100
+ height: 80,
101
+ textAlign: 'center',
102
+ justifyContent: 'center',
103
+ backgroundColor: '#00AED6'
104
+ }
105
+ return (
106
+ <Div row>
107
+ <Div style={divStyle}>
108
+ <Span>Div 1</Span>
109
+ </Div>
110
+ <Div pushed='s' style={divStyle}>
111
+ <Span>Div 2</Span>
112
+ </Div>
113
+ <Div pushed style={divStyle}>
114
+ <Span>Div 3</Span>
115
+ </Div>
116
+ <Div pushed='l' style={divStyle}>
117
+ <Span>Div 4</Span>
118
+ </Div>
119
+ </Div>
120
+ )
121
+ ```
122
+
123
+ ## Different clickable states
124
+
125
+ There are two clickable variants of component, controlled by `variant` property.
126
+
127
+ `opacity` variant - on press down, opacity of the component is decreased.
128
+
129
+ `highlight` variant - on press down, the opacity of the component background is decreased.
130
+
131
+ ```jsx example
132
+ const divStyle = {
133
+ width: 100,
134
+ height: 80,
135
+ textAlign: 'center',
136
+ justifyContent: 'center',
137
+ backgroundColor: '#00AED6'
138
+ }
139
+ return (
140
+ <Div row>
141
+ <Div style={divStyle} onPress={() => {}}>
142
+ <Span>Div opacity variant</Span>
143
+ </Div>
144
+ <Div style={divStyle} variant='highlight' pushed onPress={() => {}}>
145
+ <Span>Div highlight variant</Span>
146
+ </Div>
147
+ </Div>
148
+ )
149
+ ```
150
+
151
+ ## Levels of emphasis
152
+
153
+ Div `level` property determines different levels of emphasis by adding shadow to component. Possible values of `level` prop can be found in the `Sandbox` section at the bottom of the page.
154
+ **IMPORTANT**: Shadow does not work without background color on mobile devices.
155
+
156
+ ```jsx example
157
+ const divStyle = {
158
+ width: 100,
159
+ height: 80,
160
+ textAlign: 'center',
161
+ justifyContent: 'center',
162
+ backgroundColor: 'white'
163
+ }
164
+ return (
165
+ <Div row>
166
+ <Div style={divStyle} level={1}>
167
+ <Span>Div level 1</Span>
168
+ </Div>
169
+ <Div style={divStyle} level={4} pushed>
170
+ <Span>Div level 4</Span>
171
+ </Div>
172
+ </Div>
173
+ )
174
+ ```
175
+
176
+ ## Tooltip
177
+
178
+ ```jsx example
179
+ return pug`
180
+ Div.root(tooltip='Tooltip content')
181
+ Icon(icon=faInfoCircle size='xl')
182
+ `
183
+
184
+ styl`
185
+ .root
186
+ align-self flex-start
187
+ `
188
+ ```
189
+
190
+ ## Sandbox
191
+
192
+ <Sandbox
193
+ Component={Div}
194
+ propsJsonSchema={DivPropsJsonSchema}
195
+ props={{
196
+ children: (
197
+ <Div style={{
198
+ height: 15 * 8,
199
+ width: 15 * 8,
200
+ margin: 'auto',
201
+ backgroundColor: '#00AED6'
202
+ }} />
203
+ ),
204
+ style: {
205
+ width: 160,
206
+ height: 160,
207
+ backgroundColor: '#2962FF'
208
+ },
209
+ onPress: () => alert('"onPress" event on "Div" component'),
210
+ onLongPress: () => alert('"onLongPress" event on "Div" component')
211
+ }}
212
+ />
@@ -0,0 +1,110 @@
1
+ // -----------------------------------------
2
+ // CONFIGURATION. $UI.Div
3
+ // -----------------------------------------
4
+
5
+ $this = merge({
6
+ defaultHoverOpacity: 0.8,
7
+ defaultActiveOpacity: 0.5
8
+ }, $UI.Div, true)
9
+
10
+ // -----------------------------------------
11
+ // COMPONENT
12
+ // -----------------------------------------
13
+
14
+ _pushedSizes = ('xs' 's' 'm' 'l' 'xl' 'xxl')
15
+
16
+ // skip level 0 for shadow
17
+ // because it needed only when you want to override shadow from style sheet
18
+ _shadowLevels = ('1' '2' '3' '4')
19
+
20
+ .root
21
+ outline-color var(--color-primary)
22
+
23
+ &.column
24
+ &.left
25
+ align-items stretch
26
+
27
+ &.center
28
+ align-items center
29
+
30
+ &.right
31
+ align-items flex-end
32
+
33
+ &.v_top
34
+ justify-content flex-start
35
+
36
+ &.v_center
37
+ justify-content center
38
+
39
+ &.v_bottom
40
+ justify-content flex-end
41
+
42
+ &.row
43
+ flex-direction row
44
+
45
+ &.left
46
+ justify-content flex-start
47
+
48
+ &.center
49
+ justify-content center
50
+
51
+ &.right
52
+ justify-content flex-end
53
+
54
+ &.v_top
55
+ align-items flex-start
56
+
57
+ &.v_center
58
+ align-items center
59
+
60
+ &.v_bottom
61
+ align-items flex-end
62
+
63
+ &.wrap
64
+ flex-wrap wrap
65
+
66
+ &.reverse
67
+ &.column
68
+ flex-direction column-reverse
69
+
70
+ &.row
71
+ flex-direction row-reverse
72
+
73
+ &.rounded
74
+ radius()
75
+
76
+ &.circle
77
+ radius(circle)
78
+
79
+ for size in _pushedSizes
80
+ &.pushed-{size}
81
+ margin-left: $UI.gutters[size]
82
+
83
+ for level in _shadowLevels
84
+ &.shadow-{level}
85
+ shadow(level)
86
+
87
+ &.bleed
88
+ bleed()
89
+
90
+ &.full
91
+ flex-grow 1
92
+ flex-shrink 1
93
+
94
+ &.clickable
95
+ cursor pointer
96
+ user-select none
97
+
98
+ &.disabled
99
+ opacity 0.5
100
+
101
+ +web()
102
+ cursor default
103
+
104
+ // -----------------------------------------
105
+ // JS EXPORTS
106
+ // -----------------------------------------
107
+
108
+ :export
109
+ config: $this
110
+ shadows: $UI.shadows
package/index.d.ts ADDED
@@ -0,0 +1,68 @@
1
+ /* eslint-disable */
2
+ // DO NOT MODIFY THIS FILE - IT IS AUTOMATICALLY GENERATED ON COMMITS.
3
+
4
+ import { type ReactNode, type RefObject } from 'react';
5
+ import { type StyleProp, type ViewStyle, type ViewProps, type AccessibilityRole } from 'react-native';
6
+ declare const _default: import("react").ComponentType<DivProps>;
7
+ export default _default;
8
+ export declare const _PropsJsonSchema: {};
9
+ export interface DivProps extends ViewProps {
10
+ /** Ref to access underlying <View> or <Pressable> */
11
+ ref?: RefObject<any>;
12
+ /** Custom styles applied to the root view */
13
+ style?: StyleProp<ViewStyle>;
14
+ /** Content rendered inside Div */
15
+ children?: ReactNode;
16
+ /** Visual feedback variant @default 'opacity' */
17
+ variant?: 'opacity' | 'highlight';
18
+ /** Render children in a horizontal row */
19
+ row?: boolean;
20
+ /** Allow wrapping when row is enabled */
21
+ wrap?: boolean;
22
+ /** Reverse children order for row layouts */
23
+ reverse?: boolean;
24
+ /** Horizontal alignment when using row/column */
25
+ align?: 'left' | 'center' | 'right';
26
+ /** Vertical alignment when using row/column */
27
+ vAlign?: 'top' | 'center' | 'bottom';
28
+ /** Spacing between children (true maps to default gap) */
29
+ gap?: boolean | number;
30
+ /** Enable press feedback styles (hover and active states) @default true */
31
+ feedback?: boolean;
32
+ /** Custom style for hover state */
33
+ hoverStyle?: StyleProp<ViewStyle>;
34
+ /** Custom style for active state */
35
+ activeStyle?: StyleProp<ViewStyle>;
36
+ /** Disable interactions and apply disabled styles */
37
+ disabled?: boolean;
38
+ /** Elevation level controlling shadow intensity */
39
+ level?: 0 | 1 | 2 | 3 | 4 | 5;
40
+ /** Shape of the container corners */
41
+ shape?: 'squared' | 'rounded' | 'circle';
42
+ /** Add more space from the previous sibling */
43
+ pushed?: boolean | 's' | 'm' | 'l';
44
+ /** Stretch container into negative spacing area */
45
+ bleed?: boolean;
46
+ /** Expand to take full available height (or width if 'row' is true) */
47
+ full?: boolean;
48
+ /** Simple tooltip text */
49
+ tooltip?: string;
50
+ /** Style overrides for tooltip element */
51
+ tooltipStyle?: StyleProp<ViewStyle>;
52
+ /** onPress handler */
53
+ onPress?: (e: any) => void;
54
+ /** onLongPress handler */
55
+ onLongPress?: (e: any) => void;
56
+ /** onPressIn handler */
57
+ onPressIn?: (e: any) => void;
58
+ /** onPressOut handler */
59
+ onPressOut?: (e: any) => void;
60
+ /** Whether view is accessible and focusable (if you can press it it's focusable by default) */
61
+ accessible?: boolean;
62
+ /** Accessibility role passed to native view (if you can press it it's a 'button') */
63
+ accessibilityRole?: AccessibilityRole;
64
+ /** Deprecated custom tooltip renderer @deprecated */
65
+ renderTooltip?: any;
66
+ /** Test ID for testing purposes */
67
+ 'data-testid'?: string;
68
+ }
package/index.tsx ADDED
@@ -0,0 +1,360 @@
1
+ import { useState, useRef, type ReactNode, type RefObject } from 'react'
2
+ import {
3
+ View,
4
+ Pressable,
5
+ Platform,
6
+ StyleSheet,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ type ViewProps,
10
+ type AccessibilityRole
11
+ } from 'react-native'
12
+ import { pug, observer, u, useDidUpdate } from 'startupjs'
13
+ import { colorToRGBA, getCssVariable, themed } from '@startupjs-ui/core'
14
+ import { useDecorateTooltipProps } from './useTooltip'
15
+ import STYLES from './index.cssx.styl'
16
+
17
+ const DEPRECATED_PUSHED_VALUES = ['xs', 'xl', 'xxl']
18
+ const PRESSABLE_PROPS = ['onPress', 'onLongPress', 'onPressIn', 'onPressOut']
19
+ const isWeb = Platform.OS === 'web'
20
+ const isNative = Platform.OS !== 'web'
21
+
22
+ const {
23
+ config: {
24
+ defaultHoverOpacity,
25
+ defaultActiveOpacity
26
+ }
27
+ } = STYLES
28
+
29
+ export default observer(themed('Div', Div))
30
+
31
+ export const _PropsJsonSchema = {/* DivProps */}
32
+
33
+ export interface DivProps extends ViewProps {
34
+ /** Ref to access underlying <View> or <Pressable> */
35
+ ref?: RefObject<any>
36
+ /** Custom styles applied to the root view */
37
+ style?: StyleProp<ViewStyle>
38
+ /** Content rendered inside Div */
39
+ children?: ReactNode
40
+ /** Visual feedback variant @default 'opacity' */
41
+ variant?: 'opacity' | 'highlight'
42
+ /** Render children in a horizontal row */
43
+ row?: boolean
44
+ /** Allow wrapping when row is enabled */
45
+ wrap?: boolean
46
+ /** Reverse children order for row layouts */
47
+ reverse?: boolean
48
+ /** Horizontal alignment when using row/column */
49
+ align?: 'left' | 'center' | 'right'
50
+ /** Vertical alignment when using row/column */
51
+ vAlign?: 'top' | 'center' | 'bottom'
52
+ /** Spacing between children (true maps to default gap) */
53
+ gap?: boolean | number
54
+ /** Enable press feedback styles (hover and active states) @default true */
55
+ feedback?: boolean
56
+ /** Custom style for hover state */
57
+ hoverStyle?: StyleProp<ViewStyle>
58
+ /** Custom style for active state */
59
+ activeStyle?: StyleProp<ViewStyle>
60
+ /** Disable interactions and apply disabled styles */
61
+ disabled?: boolean
62
+ /** Elevation level controlling shadow intensity */
63
+ level?: 0 | 1 | 2 | 3 | 4 | 5
64
+ /** Shape of the container corners */
65
+ shape?: 'squared' | 'rounded' | 'circle'
66
+ /** Add more space from the previous sibling */
67
+ pushed?: boolean | 's' | 'm' | 'l'
68
+ /** Stretch container into negative spacing area */
69
+ bleed?: boolean
70
+ /** Expand to take full available height (or width if 'row' is true) */
71
+ full?: boolean
72
+ /** Simple tooltip text */
73
+ tooltip?: string
74
+ /** Style overrides for tooltip element */
75
+ tooltipStyle?: StyleProp<ViewStyle>
76
+ /** onPress handler */
77
+ onPress?: (e: any) => void
78
+ /** onLongPress handler */
79
+ onLongPress?: (e: any) => void
80
+ /** onPressIn handler */
81
+ onPressIn?: (e: any) => void
82
+ /** onPressOut handler */
83
+ onPressOut?: (e: any) => void
84
+ /** Whether view is accessible and focusable (if you can press it it's focusable by default) */
85
+ accessible?: boolean
86
+ /** Accessibility role passed to native view (if you can press it it's a 'button') */
87
+ accessibilityRole?: AccessibilityRole
88
+ /** Deprecated custom tooltip renderer @deprecated */
89
+ renderTooltip?: any // Deprecated
90
+ /** Test ID for testing purposes */
91
+ 'data-testid'?: string
92
+ }
93
+
94
+ function Div ({
95
+ style = [],
96
+ children,
97
+ variant = 'opacity',
98
+ row,
99
+ wrap,
100
+ reverse,
101
+ align,
102
+ vAlign,
103
+ gap,
104
+ hoverStyle,
105
+ activeStyle,
106
+ feedback = true,
107
+ disabled,
108
+ level = 0,
109
+ shape,
110
+ pushed, // History: for some reason the prop 'push' was ignored
111
+ bleed,
112
+ full,
113
+ accessible,
114
+ accessibilityRole,
115
+ tooltip,
116
+ tooltipStyle,
117
+ renderTooltip,
118
+ ref,
119
+ ...props
120
+ }: DivProps): ReactNode {
121
+ assertDeprecatedValues({ pushed, renderTooltip })
122
+ style = StyleSheet.flatten(style)
123
+ // on RN row-reverse switches margins and paddings sides, so we switch them back
124
+ if (isNative && reverse) style = reverseMarginPaddingSides(style)
125
+ if (gap === true) gap = 2
126
+ const isPressable = hasPressHandler(props)
127
+ const fallbackRef = useRef<any>(null)
128
+ const rootRef = ref ?? fallbackRef
129
+
130
+ let pressableStyle: StyleProp<ViewStyle> = {}
131
+ ;({
132
+ props,
133
+ pressableStyle,
134
+ accessibilityRole
135
+ } = useDecoratePressableProps({
136
+ props,
137
+ style,
138
+ activeStyle,
139
+ hoverStyle,
140
+ variant,
141
+ isPressable,
142
+ disabled,
143
+ accessibilityRole,
144
+ feedback
145
+ }))
146
+
147
+ let tooltipElement
148
+ ;({
149
+ props,
150
+ tooltipElement
151
+ } = useDecorateTooltipProps({
152
+ props,
153
+ style: tooltipStyle,
154
+ anchorRef: rootRef,
155
+ tooltip
156
+ }))
157
+
158
+ let pushedModifier
159
+ if (pushed) {
160
+ if (typeof pushed === 'boolean') pushed = 'm'
161
+ pushedModifier = `pushed-${pushed}`
162
+ }
163
+
164
+ let levelModifier
165
+ if (level) levelModifier = `shadow-${level}`
166
+
167
+ if (!accessible) accessibilityRole = undefined
168
+
169
+ const Component = isPressable ? Pressable : View
170
+ const testID = props.testID ?? props['data-testid']
171
+ const divElement = pug`
172
+ Component.root(
173
+ ref=rootRef
174
+ style=[
175
+ gap ? { gap: u(gap) } : undefined,
176
+ style,
177
+ pressableStyle
178
+ ]
179
+ styleName=[
180
+ row ? 'row' : 'column',
181
+ { wrap, reverse },
182
+ align,
183
+ 'v_' + vAlign,
184
+ {
185
+ clickable: isWeb && isPressable,
186
+ bleed,
187
+ full,
188
+ disabled
189
+ },
190
+ shape,
191
+ pushedModifier,
192
+ levelModifier
193
+ ]
194
+ accessible=accessible
195
+ accessibilityRole=accessibilityRole
196
+ testID=testID
197
+ ...props
198
+ )= children
199
+ `
200
+
201
+ if (tooltipElement) {
202
+ return pug`
203
+ = divElement
204
+ = tooltipElement
205
+ `
206
+ } else return divElement
207
+ }
208
+
209
+ function useDecoratePressableProps ({
210
+ props,
211
+ style,
212
+ activeStyle,
213
+ hoverStyle,
214
+ variant,
215
+ isPressable,
216
+ disabled,
217
+ accessibilityRole,
218
+ feedback
219
+ }: {
220
+ props: Record<string, any>
221
+ style: StyleProp<ViewStyle>
222
+ activeStyle: StyleProp<ViewStyle>
223
+ hoverStyle: StyleProp<ViewStyle>
224
+ variant: 'opacity' | 'highlight'
225
+ isPressable: boolean
226
+ disabled?: boolean
227
+ accessibilityRole?: AccessibilityRole
228
+ feedback?: boolean
229
+ }): {
230
+ props: Record<string, any>
231
+ pressableStyle?: StyleProp<ViewStyle>
232
+ accessibilityRole?: AccessibilityRole
233
+ } {
234
+ let pressableStyle: StyleProp<ViewStyle> = {}
235
+ const [hover, setHover] = useState(false)
236
+ const [active, setActive] = useState(false)
237
+
238
+ // If component become not clickable, for example received 'disabled'
239
+ // prop while hover or active, state wouldn't update without this effect
240
+ useDidUpdate(() => {
241
+ if (!isPressable) return
242
+ if (disabled) {
243
+ if (hover) setHover(false)
244
+ if (active) setActive(false)
245
+ }
246
+ }, [isPressable, disabled])
247
+
248
+ // decorate the element state (hover, active) only if it's pressable
249
+ if (!isPressable) return { props }
250
+
251
+ accessibilityRole ??= 'button'
252
+ props.focusable ??= true
253
+
254
+ // setup hover and active states styles and props
255
+ if (feedback) {
256
+ const { onPressIn, onPressOut } = props
257
+
258
+ props.onPressIn = (...args: any[]) => {
259
+ setActive(true)
260
+ onPressIn?.(...args)
261
+ }
262
+ props.onPressOut = (...args: any[]) => {
263
+ setActive(false)
264
+ onPressOut?.(...args)
265
+ }
266
+
267
+ if (isWeb && !disabled) {
268
+ const { onMouseEnter, onMouseLeave } = props
269
+
270
+ props.onMouseEnter = (...args: any[]) => {
271
+ setHover(true)
272
+ onMouseEnter?.(...args)
273
+ }
274
+ props.onMouseLeave = (...args: any[]) => {
275
+ setHover(false)
276
+ onMouseLeave?.(...args)
277
+ }
278
+ }
279
+ // hover or active state styles
280
+ // active state takes precedence over hover state
281
+ if (active) {
282
+ pressableStyle = activeStyle ?? getDefaultStyle(style, 'active', variant)
283
+ } else if (hover) {
284
+ pressableStyle = hoverStyle ?? getDefaultStyle(style, 'hover', variant)
285
+ }
286
+ }
287
+
288
+ // if disabled, disable all press handlers
289
+ for (const prop of PRESSABLE_PROPS) {
290
+ const pressHandler = props[prop]
291
+ if (!pressHandler) continue
292
+ props[prop] = (...args: any[]) => {
293
+ if (disabled) return
294
+ pressHandler(...args)
295
+ }
296
+ }
297
+
298
+ return { props, pressableStyle, accessibilityRole }
299
+ }
300
+
301
+ function getDefaultStyle (
302
+ style: StyleProp<ViewStyle>,
303
+ type: 'hover' | 'active',
304
+ variant?: 'opacity' | 'highlight'
305
+ ): StyleProp<ViewStyle> | undefined {
306
+ if (variant === 'opacity') {
307
+ if (type === 'hover') return { opacity: defaultHoverOpacity }
308
+ if (type === 'active') return { opacity: defaultActiveOpacity }
309
+ } else {
310
+ style = StyleSheet.flatten(style)
311
+ let backgroundColor = style.backgroundColor
312
+ if (backgroundColor === 'transparent') backgroundColor = undefined
313
+
314
+ if (type === 'hover') {
315
+ if (backgroundColor) {
316
+ return { backgroundColor: colorToRGBA(backgroundColor as string, defaultHoverOpacity) }
317
+ } else {
318
+ // If no color exists, we treat it as a light background and just dim it a bit
319
+ return { backgroundColor: getCssVariable('--Div-hoverBg') as string | undefined }
320
+ }
321
+ }
322
+
323
+ if (type === 'active') {
324
+ if (backgroundColor) {
325
+ return { backgroundColor: colorToRGBA(backgroundColor as string, defaultActiveOpacity) }
326
+ } else {
327
+ // If no color exists, we treat it as a light background and just dim it a bit
328
+ return { backgroundColor: getCssVariable('--Div-activeBg') as string | undefined }
329
+ }
330
+ }
331
+ }
332
+ }
333
+
334
+ function hasPressHandler (props: Record<string, any>): boolean {
335
+ return PRESSABLE_PROPS.some(prop => props[prop])
336
+ }
337
+
338
+ function reverseMarginPaddingSides (style: StyleProp<ViewStyle>) {
339
+ style = StyleSheet.flatten(style)
340
+ const { paddingLeft, paddingRight, marginLeft, marginRight } = style
341
+ style.marginLeft = marginRight
342
+ style.marginRight = marginLeft
343
+ style.paddingLeft = paddingRight
344
+ style.paddingRight = paddingLeft
345
+ return style
346
+ }
347
+
348
+ function assertDeprecatedValues ({ pushed, renderTooltip }: { pushed?: any, renderTooltip?: any }) {
349
+ if (DEPRECATED_PUSHED_VALUES.includes(pushed)) console.warn(ERRORS.DEPRECATED_PUSHED(pushed))
350
+ if (renderTooltip) console.warn(ERRORS.DEPRECATED_RENDER_TOOLTIP)
351
+ }
352
+
353
+ const ERRORS = {
354
+ DEPRECATED_PUSHED: (pushed: string) => `
355
+ [@startupjs/ui] Div: variant='${pushed}' is DEPRECATED, use one of 's', 'm', 'l' instead.
356
+ `,
357
+ DEPRECATED_RENDER_TOOLTIP: `
358
+ [@startupjs/ui] Div: renderTooltip is DEPRECATED, use 'tooltip' property instead.
359
+ `
360
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@startupjs-ui/div",
3
+ "version": "0.1.3",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "main": "index.tsx",
8
+ "types": "index.d.ts",
9
+ "type": "module",
10
+ "exports": {
11
+ ".": "./index.tsx",
12
+ "./useTooltip": "./useTooltip.tsx"
13
+ },
14
+ "dependencies": {
15
+ "@startupjs-ui/abstract-popover": "^0.1.3",
16
+ "@startupjs-ui/core": "^0.1.3",
17
+ "@startupjs-ui/span": "^0.1.3"
18
+ },
19
+ "peerDependencies": {
20
+ "react": "*",
21
+ "react-native": "*",
22
+ "startupjs": "*"
23
+ },
24
+ "gitHead": "fd964ebc3892d3dd0a6c85438c0af619cc50c3f0"
25
+ }
package/useTooltip.tsx ADDED
@@ -0,0 +1,166 @@
1
+ import React, { useState, useEffect, useMemo, useCallback, type ReactNode, type RefObject } from 'react'
2
+ import { Platform, View } from 'react-native'
3
+ import { pug, styl } from 'startupjs'
4
+ import Span from '@startupjs-ui/span'
5
+ import AbstractPopover from '@startupjs-ui/abstract-popover'
6
+
7
+ const isWeb = Platform.OS === 'web'
8
+
9
+ const DEFAULT_TOOLTIP_PROPS = {
10
+ position: 'top',
11
+ attachment: 'center',
12
+ arrow: true
13
+ }
14
+
15
+ export interface UseTooltipProps {
16
+ style?: any
17
+ anchorRef: RefObject<any>
18
+ tooltip?: ReactNode | (() => ReactNode)
19
+ }
20
+
21
+ export type TooltipEventHandler = 'onMouseEnter' | 'onMouseLeave' | 'onLongPress' | 'onPressOut'
22
+ export type TooltipEventHandlers = Partial<Record<TooltipEventHandler, any>>
23
+ export const tooltipEventHandlersList: TooltipEventHandler[] = [
24
+ 'onMouseEnter',
25
+ 'onMouseLeave',
26
+ 'onLongPress',
27
+ 'onPressOut'
28
+ ]
29
+
30
+ interface TooltipResult {
31
+ tooltipElement?: ReactNode
32
+ tooltipEventHandlers: TooltipEventHandlers
33
+ }
34
+
35
+ export default function useTooltip ({ style, anchorRef, tooltip }: UseTooltipProps) {
36
+ const result: TooltipResult = { tooltipEventHandlers: {} }
37
+ const [visible, setVisible] = useState(false)
38
+
39
+ const cbSetVisibleTrue = useCallback(() => { setVisible(true) }, [])
40
+ const cbSetVisibleFalse = useCallback(() => { setVisible(false) }, [])
41
+
42
+ useEffect(() => {
43
+ if (!isWeb) return
44
+ if (!tooltip) return
45
+
46
+ window.addEventListener('wheel', cbSetVisibleFalse, true)
47
+ return () => {
48
+ window.removeEventListener('wheel', cbSetVisibleFalse, true)
49
+ }
50
+ }, [cbSetVisibleFalse, cbSetVisibleTrue, tooltip])
51
+
52
+ const tooltipEventHandlers = useMemo(() => {
53
+ const handlers: TooltipEventHandlers = {}
54
+ if (!tooltip) return handlers
55
+
56
+ if (isWeb) {
57
+ handlers.onMouseEnter = cbSetVisibleTrue
58
+ handlers.onMouseLeave = cbSetVisibleFalse
59
+ } else {
60
+ handlers.onLongPress = cbSetVisibleTrue
61
+ handlers.onPressOut = cbSetVisibleFalse
62
+ }
63
+
64
+ return handlers
65
+ }, [cbSetVisibleFalse, cbSetVisibleTrue, tooltip])
66
+
67
+ result.tooltipEventHandlers = tooltipEventHandlers
68
+
69
+ if (tooltip) {
70
+ result.tooltipElement = pug`
71
+ AbstractPopover.tooltip(
72
+ style=style
73
+ anchorRef=anchorRef
74
+ visible=visible
75
+ ...DEFAULT_TOOLTIP_PROPS
76
+ )
77
+ //- case for DEPRECATED renderTooltip property
78
+ if typeof tooltip === 'function'
79
+ = tooltip()
80
+ else
81
+ //- HACK
82
+ //- Wrap to row, because for small texts it does not correctly hyphenate in the text
83
+ //- For example $500,000, Copy, etc...
84
+ View(style={ flexDirection: 'row' })
85
+ Span.tooltip-text= tooltip
86
+ `
87
+ }
88
+
89
+ return result
90
+
91
+ styl`
92
+ .tooltip
93
+ background-color var(--Div-tooltipBg)
94
+ max-width 260px
95
+ radius()
96
+ shadow(3)
97
+ padding 1u 2u
98
+
99
+ +tablet()
100
+ max-width 320px
101
+
102
+ &-text
103
+ color var(--Div-tooltipText)
104
+ `
105
+ }
106
+
107
+ export interface UseDecorateTooltipProps {
108
+ props: Record<string, any>
109
+ style?: any
110
+ anchorRef: RefObject<any>
111
+ tooltip?: ReactNode | (() => ReactNode)
112
+ }
113
+
114
+ interface DecorateTooltipResult {
115
+ props: Record<string, any>
116
+ tooltipElement?: ReactNode
117
+ }
118
+
119
+ export function useDecorateTooltipProps ({
120
+ props,
121
+ style,
122
+ anchorRef,
123
+ tooltip
124
+ }: UseDecorateTooltipProps): DecorateTooltipResult {
125
+ const { tooltipElement, tooltipEventHandlers } = useTooltip({ style, anchorRef, tooltip })
126
+ const {
127
+ onMouseEnter,
128
+ onMouseLeave,
129
+ onLongPress,
130
+ onPressOut
131
+ } = props as TooltipEventHandlers
132
+
133
+ const extraEventHandlerProps: TooltipEventHandlers = useMemo(() => {
134
+ const res: TooltipEventHandlers = {}
135
+ const divHandlers: TooltipEventHandlers = {
136
+ onMouseEnter,
137
+ onMouseLeave,
138
+ onLongPress,
139
+ onPressOut
140
+ }
141
+
142
+ for (const handlerName of tooltipEventHandlersList) {
143
+ const tooltipHandler = tooltipEventHandlers[handlerName]
144
+ if (!tooltipHandler) continue
145
+ const divHandler = divHandlers[handlerName]
146
+
147
+ res[handlerName] = divHandler
148
+ ? (...args: any[]) => {
149
+ tooltipHandler(...args)
150
+ divHandler(...args)
151
+ }
152
+ : tooltipHandler
153
+ }
154
+ return res
155
+ }, [
156
+ tooltipEventHandlers,
157
+ onMouseEnter,
158
+ onMouseLeave,
159
+ onLongPress,
160
+ onPressOut
161
+ ])
162
+
163
+ Object.assign(props, extraEventHandlerProps)
164
+
165
+ return { props, tooltipElement }
166
+ }