@hadoku/task-ui-components 0.0.0-stage → 4.20.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.
Files changed (169) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +917 -2
  3. package/dist/app-header.css +288 -0
  4. package/dist/bento.css +143 -0
  5. package/dist/button.css +227 -0
  6. package/dist/components/AppHeader.d.ts +61 -0
  7. package/dist/components/AppHeader.d.ts.map +1 -0
  8. package/dist/components/AppHeader.js +23 -0
  9. package/dist/components/AppHeader.js.map +1 -0
  10. package/dist/components/BentoCard.d.ts +23 -0
  11. package/dist/components/BentoCard.d.ts.map +1 -0
  12. package/dist/components/BentoCard.js +49 -0
  13. package/dist/components/BentoCard.js.map +1 -0
  14. package/dist/components/BentoGrid.d.ts +24 -0
  15. package/dist/components/BentoGrid.d.ts.map +1 -0
  16. package/dist/components/BentoGrid.js +22 -0
  17. package/dist/components/BentoGrid.js.map +1 -0
  18. package/dist/components/Button.d.ts +28 -0
  19. package/dist/components/Button.d.ts.map +1 -0
  20. package/dist/components/Button.js +51 -0
  21. package/dist/components/Button.js.map +1 -0
  22. package/dist/components/ConnectedSettings.d.ts +53 -0
  23. package/dist/components/ConnectedSettings.d.ts.map +1 -0
  24. package/dist/components/ConnectedSettings.js +302 -0
  25. package/dist/components/ConnectedSettings.js.map +1 -0
  26. package/dist/components/ConnectedThemePicker.d.ts +52 -0
  27. package/dist/components/ConnectedThemePicker.d.ts.map +1 -0
  28. package/dist/components/ConnectedThemePicker.js +32 -0
  29. package/dist/components/ConnectedThemePicker.js.map +1 -0
  30. package/dist/components/ContextMenu.d.ts +22 -0
  31. package/dist/components/ContextMenu.d.ts.map +1 -0
  32. package/dist/components/ContextMenu.js +23 -0
  33. package/dist/components/ContextMenu.js.map +1 -0
  34. package/dist/components/HadokuThemePicker.d.ts +27 -0
  35. package/dist/components/HadokuThemePicker.d.ts.map +1 -0
  36. package/dist/components/HadokuThemePicker.js +19 -0
  37. package/dist/components/HadokuThemePicker.js.map +1 -0
  38. package/dist/components/LoadingSkeleton.d.ts +14 -0
  39. package/dist/components/LoadingSkeleton.d.ts.map +1 -0
  40. package/dist/components/LoadingSkeleton.js +62 -0
  41. package/dist/components/LoadingSkeleton.js.map +1 -0
  42. package/dist/components/Modal.d.ts +39 -0
  43. package/dist/components/Modal.d.ts.map +1 -0
  44. package/dist/components/Modal.js +106 -0
  45. package/dist/components/Modal.js.map +1 -0
  46. package/dist/components/NumberInput.d.ts +26 -0
  47. package/dist/components/NumberInput.d.ts.map +1 -0
  48. package/dist/components/NumberInput.js +131 -0
  49. package/dist/components/NumberInput.js.map +1 -0
  50. package/dist/components/Select.d.ts +52 -0
  51. package/dist/components/Select.d.ts.map +1 -0
  52. package/dist/components/Select.js +281 -0
  53. package/dist/components/Select.js.map +1 -0
  54. package/dist/components/SettingsModal.d.ts +69 -0
  55. package/dist/components/SettingsModal.d.ts.map +1 -0
  56. package/dist/components/SettingsModal.js +141 -0
  57. package/dist/components/SettingsModal.js.map +1 -0
  58. package/dist/components/ThemeIcons.d.ts +26 -0
  59. package/dist/components/ThemeIcons.d.ts.map +1 -0
  60. package/dist/components/ThemeIcons.js +74 -0
  61. package/dist/components/ThemeIcons.js.map +1 -0
  62. package/dist/components/ThemePicker.d.ts +28 -0
  63. package/dist/components/ThemePicker.d.ts.map +1 -0
  64. package/dist/components/ThemePicker.js +101 -0
  65. package/dist/components/ThemePicker.js.map +1 -0
  66. package/dist/components/Toast.d.ts +13 -0
  67. package/dist/components/Toast.d.ts.map +1 -0
  68. package/dist/components/Toast.js +70 -0
  69. package/dist/components/Toast.js.map +1 -0
  70. package/dist/components/Toaster.d.ts +19 -0
  71. package/dist/components/Toaster.d.ts.map +1 -0
  72. package/dist/components/Toaster.js +19 -0
  73. package/dist/components/Toaster.js.map +1 -0
  74. package/dist/components/Tooltip.d.ts +49 -0
  75. package/dist/components/Tooltip.d.ts.map +1 -0
  76. package/dist/components/Tooltip.js +143 -0
  77. package/dist/components/Tooltip.js.map +1 -0
  78. package/dist/context-menu.css +11 -0
  79. package/dist/controls.css +137 -0
  80. package/dist/hooks/useReducedMotion.d.ts +7 -0
  81. package/dist/hooks/useReducedMotion.d.ts.map +1 -0
  82. package/dist/hooks/useReducedMotion.js +25 -0
  83. package/dist/hooks/useReducedMotion.js.map +1 -0
  84. package/dist/hooks/useToast.d.ts +18 -0
  85. package/dist/hooks/useToast.d.ts.map +1 -0
  86. package/dist/hooks/useToast.js +28 -0
  87. package/dist/hooks/useToast.js.map +1 -0
  88. package/dist/icon-btn.css +64 -0
  89. package/dist/index.d.ts +49 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +72 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/lib/menuPlacement.d.ts +16 -0
  94. package/dist/lib/menuPlacement.d.ts.map +1 -0
  95. package/dist/lib/menuPlacement.js +19 -0
  96. package/dist/lib/menuPlacement.js.map +1 -0
  97. package/dist/lib/selectMenus.d.ts +30 -0
  98. package/dist/lib/selectMenus.d.ts.map +1 -0
  99. package/dist/lib/selectMenus.js +236 -0
  100. package/dist/lib/selectMenus.js.map +1 -0
  101. package/dist/lib/settingsClient.d.ts +120 -0
  102. package/dist/lib/settingsClient.d.ts.map +1 -0
  103. package/dist/lib/settingsClient.js +140 -0
  104. package/dist/lib/settingsClient.js.map +1 -0
  105. package/dist/lib/themeScope.d.ts +19 -0
  106. package/dist/lib/themeScope.d.ts.map +1 -0
  107. package/dist/lib/themeScope.js +10 -0
  108. package/dist/lib/themeScope.js.map +1 -0
  109. package/dist/lib/titleTooltips.d.ts +38 -0
  110. package/dist/lib/titleTooltips.d.ts.map +1 -0
  111. package/dist/lib/titleTooltips.js +166 -0
  112. package/dist/lib/titleTooltips.js.map +1 -0
  113. package/dist/lib/tooltipPlacement.d.ts +14 -0
  114. package/dist/lib/tooltipPlacement.d.ts.map +1 -0
  115. package/dist/lib/tooltipPlacement.js +34 -0
  116. package/dist/lib/tooltipPlacement.js.map +1 -0
  117. package/dist/modal.css +129 -0
  118. package/dist/number-input.css +131 -0
  119. package/dist/scrollbar.css +165 -0
  120. package/dist/select.css +251 -0
  121. package/dist/styles.css +1866 -0
  122. package/dist/task-ui-components.css +1866 -0
  123. package/dist/theme-picker.css +219 -0
  124. package/dist/toaster.css +159 -0
  125. package/dist/tooltip.css +103 -0
  126. package/dist/types/index.d.ts +37 -0
  127. package/dist/types/index.d.ts.map +1 -0
  128. package/package.json +60 -4
  129. package/src/app-header.css +288 -0
  130. package/src/bento.css +143 -0
  131. package/src/button.css +227 -0
  132. package/src/components/AppHeader.tsx +87 -0
  133. package/src/components/BentoCard.tsx +72 -0
  134. package/src/components/BentoGrid.tsx +48 -0
  135. package/src/components/Button.tsx +81 -0
  136. package/src/components/ConnectedSettings.tsx +465 -0
  137. package/src/components/ConnectedThemePicker.tsx +79 -0
  138. package/src/components/ContextMenu.tsx +48 -0
  139. package/src/components/HadokuThemePicker.tsx +39 -0
  140. package/src/components/LoadingSkeleton.tsx +84 -0
  141. package/src/components/Modal.tsx +171 -0
  142. package/src/components/NumberInput.tsx +161 -0
  143. package/src/components/Select.tsx +359 -0
  144. package/src/components/SettingsModal.tsx +235 -0
  145. package/src/components/ThemeIcons.tsx +127 -0
  146. package/src/components/ThemePicker.tsx +149 -0
  147. package/src/components/Toast.tsx +92 -0
  148. package/src/components/Toaster.tsx +43 -0
  149. package/src/components/Tooltip.tsx +212 -0
  150. package/src/context-menu.css +11 -0
  151. package/src/controls.css +137 -0
  152. package/src/hooks/useReducedMotion.ts +36 -0
  153. package/src/hooks/useToast.ts +57 -0
  154. package/src/icon-btn.css +64 -0
  155. package/src/index.ts +140 -0
  156. package/src/lib/menuPlacement.ts +31 -0
  157. package/src/lib/selectMenus.ts +303 -0
  158. package/src/lib/settingsClient.ts +377 -0
  159. package/src/lib/themeScope.ts +23 -0
  160. package/src/lib/titleTooltips.ts +236 -0
  161. package/src/lib/tooltipPlacement.ts +59 -0
  162. package/src/modal.css +129 -0
  163. package/src/number-input.css +131 -0
  164. package/src/scrollbar.css +165 -0
  165. package/src/select.css +251 -0
  166. package/src/theme-picker.css +219 -0
  167. package/src/toaster.css +159 -0
  168. package/src/tooltip.css +103 -0
  169. package/src/types/index.ts +43 -0
@@ -0,0 +1,84 @@
1
+ /**
2
+ * LoadingSkeleton Component
3
+ * Displays a loading skeleton with theme support
4
+ * Can be customized with different layouts
5
+ */
6
+
7
+ import React from 'react'
8
+
9
+ export interface LoadingSkeletonProps {
10
+ isDarkTheme?: boolean
11
+ className?: string
12
+ layout?: 'default' | 'minimal' | 'custom'
13
+ customContent?: React.ReactNode
14
+ }
15
+
16
+ export function LoadingSkeleton({
17
+ isDarkTheme = false,
18
+ className = '',
19
+ layout = 'default',
20
+ customContent
21
+ }: LoadingSkeletonProps) {
22
+ if (layout === 'custom' && customContent) {
23
+ return (
24
+ <div
25
+ className={`loading-skeleton ${className}`}
26
+ data-dark-theme={isDarkTheme ? 'true' : 'false'}
27
+ >
28
+ {customContent}
29
+ </div>
30
+ )
31
+ }
32
+
33
+ if (layout === 'minimal') {
34
+ return (
35
+ <div
36
+ className={`loading-skeleton loading-skeleton--minimal ${className}`}
37
+ data-dark-theme={isDarkTheme ? 'true' : 'false'}
38
+ >
39
+ <div className="skeleton-spinner"></div>
40
+ </div>
41
+ )
42
+ }
43
+
44
+ // Default layout - task app specific but can be overridden
45
+ return (
46
+ <div
47
+ className={`loading-skeleton ${className}`}
48
+ data-dark-theme={isDarkTheme ? 'true' : 'false'}
49
+ >
50
+ <div className="loading-skeleton__content">
51
+ {/* Header */}
52
+ <div className="skeleton-header-row">
53
+ <div className="skeleton-header"></div>
54
+ <div className="skeleton-button"></div>
55
+ </div>
56
+
57
+ {/* Navigation */}
58
+ <div className="skeleton-nav">
59
+ <div className="skeleton-nav-item"></div>
60
+ <div className="skeleton-nav-item"></div>
61
+ <div className="skeleton-nav-item"></div>
62
+ </div>
63
+
64
+ {/* Input */}
65
+ <div className="skeleton-input"></div>
66
+
67
+ {/* Filters */}
68
+ <div className="skeleton-filters">
69
+ <div className="skeleton-filter"></div>
70
+ <div className="skeleton-filter"></div>
71
+ <div className="skeleton-filter"></div>
72
+ </div>
73
+
74
+ {/* Content */}
75
+ <div className="skeleton-content">
76
+ <div className="skeleton-content-header"></div>
77
+ <div className="skeleton-item"></div>
78
+ <div className="skeleton-item"></div>
79
+ <div className="skeleton-item"></div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ )
84
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Modal Component
3
+ * Generic modal dialog with keyboard shortcuts and customizable actions
4
+ *
5
+ * Styles ship with the package: import '@wolffm/task-ui-components/modal.css'
6
+ */
7
+
8
+ import React, { useEffect, useId, useRef } from 'react'
9
+
10
+ export interface ModalProps {
11
+ isOpen: boolean
12
+ title: string
13
+ onClose: () => void
14
+ onConfirm?: () => void | Promise<void>
15
+ children?: React.ReactNode
16
+ inputValue?: string
17
+ onInputChange?: (value: string) => void
18
+ inputPlaceholder?: string
19
+ confirmLabel?: string
20
+ cancelLabel?: string
21
+ confirmDisabled?: boolean
22
+ confirmDanger?: boolean
23
+ showCancel?: boolean
24
+ showConfirm?: boolean
25
+ className?: string
26
+ overlayClassName?: string
27
+ /**
28
+ * Replaces the whole action row. A wizard has Back/Cancel/Regenerate/Create,
29
+ * which the fixed Cancel+Confirm pair cannot express — and that is what
30
+ * pushes an app to hand-roll its own dialog instead of adopting this one.
31
+ *
32
+ * `null` means *no* row at all (a progress step, a viewer); omitting it means
33
+ * "use the default row". `??` collapses those two, so the check below is
34
+ * against `undefined` explicitly.
35
+ */
36
+ actions?: React.ReactNode
37
+ /** Rendered left of the title, e.g. a wizard's Back button. */
38
+ lead?: React.ReactNode
39
+ }
40
+
41
+ /**
42
+ * Open dialogs, outermost first. Each mounted-and-open Modal pushes a token
43
+ * and pops it on close, so the handler can tell whether it is the one the
44
+ * keypress is meant for.
45
+ */
46
+ const escapeStack: symbol[] = []
47
+
48
+ export function Modal({
49
+ isOpen,
50
+ title,
51
+ onClose,
52
+ onConfirm,
53
+ children,
54
+ inputValue,
55
+ onInputChange,
56
+ inputPlaceholder,
57
+ confirmLabel = 'Confirm',
58
+ cancelLabel = 'Cancel',
59
+ confirmDisabled = false,
60
+ confirmDanger = false,
61
+ showCancel = true,
62
+ showConfirm = true,
63
+ className = '',
64
+ overlayClassName = '',
65
+ actions,
66
+ lead
67
+ }: ModalProps) {
68
+ const titleId = useId()
69
+ const cardRef = useRef<HTMLDivElement>(null)
70
+
71
+ // Escape closes from anywhere in the dialog, not just from a focused input.
72
+ // Bound to the document rather than the card so it works before anything
73
+ // inside has been focused — previously the handler hung off the optional
74
+ // text input, so a dialog without one could not be dismissed by keyboard.
75
+ //
76
+ // Only the TOP-MOST open dialog answers, which is what makes a confirm
77
+ // nested inside another modal usable: the parent's document listener is
78
+ // still live underneath it, so without the stack one Escape closes both.
79
+ // onClose is read through a ref so a caller passing an unmemoised handler
80
+ // cannot re-run this effect and shuffle itself back to the top of the stack.
81
+ const onCloseRef = useRef(onClose)
82
+ onCloseRef.current = onClose
83
+ useEffect(() => {
84
+ if (!isOpen) return
85
+ const id = Symbol('modal')
86
+ escapeStack.push(id)
87
+ const onKey = (e: KeyboardEvent) => {
88
+ if (e.key !== 'Escape') return
89
+ if (escapeStack[escapeStack.length - 1] !== id) return
90
+ e.preventDefault()
91
+ onCloseRef.current()
92
+ }
93
+ document.addEventListener('keydown', onKey)
94
+ return () => {
95
+ document.removeEventListener('keydown', onKey)
96
+ const at = escapeStack.lastIndexOf(id)
97
+ if (at >= 0) escapeStack.splice(at, 1)
98
+ }
99
+ }, [isOpen])
100
+
101
+ // Move focus into the dialog on open, so the keyboard starts inside it.
102
+ useEffect(() => {
103
+ if (!isOpen) return
104
+ const card = cardRef.current
105
+ if (!card) return
106
+ const focusable = card.querySelector<HTMLElement>(
107
+ 'input:not([type=hidden]), textarea, select, button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])'
108
+ )
109
+ ;(focusable ?? card).focus()
110
+ }, [isOpen])
111
+
112
+ if (!isOpen) return null
113
+
114
+ return (
115
+ <div className={`modal-overlay ${overlayClassName}`.trim()} onClick={onClose}>
116
+ <div
117
+ ref={cardRef}
118
+ className={`modal-card ${className}`.trim()}
119
+ onClick={e => e.stopPropagation()}
120
+ role="dialog"
121
+ aria-modal="true"
122
+ aria-labelledby={titleId}
123
+ tabIndex={-1}
124
+ >
125
+ <div className="modal-head">
126
+ {lead}
127
+ <h3 id={titleId}>{title}</h3>
128
+ </div>
129
+
130
+ {children}
131
+
132
+ {onInputChange && (
133
+ <input
134
+ type="text"
135
+ className="modal-input"
136
+ value={inputValue || ''}
137
+ onChange={e => onInputChange(e.target.value)}
138
+ placeholder={inputPlaceholder}
139
+ onKeyDown={e => {
140
+ if (e.key === 'Enter' && !confirmDisabled && onConfirm) {
141
+ e.preventDefault()
142
+ void onConfirm()
143
+ }
144
+ }}
145
+ />
146
+ )}
147
+
148
+ {actions !== undefined ? (
149
+ actions
150
+ ) : (
151
+ <div className="modal-actions">
152
+ {showCancel && (
153
+ <button className="modal-button" onClick={onClose}>
154
+ {cancelLabel}
155
+ </button>
156
+ )}
157
+ {showConfirm && onConfirm && (
158
+ <button
159
+ className={`modal-button ${confirmDanger ? 'modal-button--danger' : 'modal-button--primary'}`}
160
+ onClick={() => void onConfirm()}
161
+ disabled={confirmDisabled}
162
+ >
163
+ {confirmLabel}
164
+ </button>
165
+ )}
166
+ </div>
167
+ )}
168
+ </div>
169
+ </div>
170
+ )
171
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * NumberInput Component
3
+ * A themed number field with its own stepper, for the OS spinner arrows that
4
+ * no CSS can style (controls.css removes them from a bare <input type=number>).
5
+ *
6
+ * It is a real <input type="number"> with every input prop passed through, so
7
+ * it works controlled (`value` + `onChange`) or uncontrolled (`defaultValue`),
8
+ * submits in a form, and keeps native min/max/step validation and the arrow
9
+ * keys. The buttons call the input's own stepUp()/stepDown() and then fire
10
+ * `input` and `change`, so `onChange` sees an ordinary edit: React's value
11
+ * tracker compares against what React last wrote, and stepUp() goes around it.
12
+ *
13
+ * Holding a button repeats, like the native spinner. The buttons are not tab
14
+ * stops — the arrow keys on the field already step — but they are labelled.
15
+ */
16
+
17
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
18
+ import { Icon } from '@wolffm/themes'
19
+ import '../number-input.css'
20
+
21
+ export type NumberInputSize = 'sm' | 'md' | 'lg'
22
+
23
+ export interface NumberInputProps
24
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'size'> {
25
+ size?: NumberInputSize
26
+ /** Labels for the two buttons; default "Increase" / "Decrease". */
27
+ increaseLabel?: string
28
+ decreaseLabel?: string
29
+ }
30
+
31
+ const REPEAT_DELAY_MS = 400
32
+ const REPEAT_EVERY_MS = 60
33
+ const ICON_SIZE: Record<NumberInputSize, number> = { sm: 12, md: 14, lg: 16 }
34
+
35
+ export const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
36
+ function NumberInput(
37
+ {
38
+ size = 'md',
39
+ increaseLabel = 'Increase',
40
+ decreaseLabel = 'Decrease',
41
+ className,
42
+ disabled,
43
+ readOnly,
44
+ onInput,
45
+ ...rest
46
+ },
47
+ ref
48
+ ) {
49
+ const inputRef = useRef<HTMLInputElement | null>(null)
50
+ const repeat = useRef<{
51
+ delay?: ReturnType<typeof setTimeout>
52
+ every?: ReturnType<typeof setInterval>
53
+ }>({})
54
+ // Mirrors the field so the buttons can disable at min/max, controlled or not.
55
+ const [current, setCurrent] = useState<string>(String(rest.value ?? rest.defaultValue ?? ''))
56
+ useEffect(() => {
57
+ if (rest.value !== undefined) setCurrent(String(rest.value))
58
+ }, [rest.value])
59
+
60
+ const setRef = useCallback(
61
+ (node: HTMLInputElement | null) => {
62
+ inputRef.current = node
63
+ if (typeof ref === 'function') ref(node)
64
+ else if (ref) ref.current = node
65
+ },
66
+ [ref]
67
+ )
68
+
69
+ const stop = useCallback(() => {
70
+ clearTimeout(repeat.current.delay)
71
+ clearInterval(repeat.current.every)
72
+ repeat.current = {}
73
+ }, [])
74
+ useEffect(() => stop, [stop])
75
+
76
+ const stepOnce = useCallback((dir: 1 | -1) => {
77
+ const input = inputRef.current
78
+ if (!input || input.disabled || input.readOnly) return false
79
+ const before = input.value
80
+ if (dir === 1) input.stepUp()
81
+ else input.stepDown()
82
+ if (input.value === before) return false
83
+ input.dispatchEvent(new Event('input', { bubbles: true }))
84
+ input.dispatchEvent(new Event('change', { bubbles: true }))
85
+ return true
86
+ }, [])
87
+
88
+ const start = (dir: 1 | -1) => (e: React.PointerEvent) => {
89
+ if (e.button !== 0) return
90
+ // Keep focus (and the caret) in the field, as the native spinner does.
91
+ e.preventDefault()
92
+ inputRef.current?.focus({ preventScroll: true })
93
+ stop()
94
+ if (!stepOnce(dir)) return
95
+ repeat.current.delay = setTimeout(() => {
96
+ repeat.current.every = setInterval(() => {
97
+ if (!stepOnce(dir)) stop()
98
+ }, REPEAT_EVERY_MS)
99
+ }, REPEAT_DELAY_MS)
100
+ }
101
+
102
+ const n = current === '' ? NaN : Number(current)
103
+ const min = rest.min === undefined || rest.min === '' ? -Infinity : Number(rest.min)
104
+ const max = rest.max === undefined || rest.max === '' ? Infinity : Number(rest.max)
105
+ const locked = disabled || readOnly
106
+
107
+ return (
108
+ <span
109
+ className={[
110
+ 'hdk-number',
111
+ `hdk-number--${size}`,
112
+ disabled && 'hdk-number--disabled',
113
+ className
114
+ ]
115
+ .filter(Boolean)
116
+ .join(' ')}
117
+ >
118
+ <input
119
+ ref={setRef}
120
+ type="number"
121
+ className="hdk-number__input"
122
+ disabled={disabled}
123
+ readOnly={readOnly}
124
+ onInput={e => {
125
+ setCurrent(e.currentTarget.value)
126
+ onInput?.(e)
127
+ }}
128
+ {...rest}
129
+ />
130
+ <span className="hdk-number__steps">
131
+ <button
132
+ type="button"
133
+ tabIndex={-1}
134
+ className="hdk-number__step"
135
+ aria-label={increaseLabel}
136
+ disabled={locked || n >= max}
137
+ onPointerDown={start(1)}
138
+ onPointerUp={stop}
139
+ onPointerLeave={stop}
140
+ onPointerCancel={stop}
141
+ >
142
+ <Icon name="chevron-up" size={ICON_SIZE[size]} />
143
+ </button>
144
+ <button
145
+ type="button"
146
+ tabIndex={-1}
147
+ className="hdk-number__step"
148
+ aria-label={decreaseLabel}
149
+ disabled={locked || n <= min}
150
+ onPointerDown={start(-1)}
151
+ onPointerUp={stop}
152
+ onPointerLeave={stop}
153
+ onPointerCancel={stop}
154
+ >
155
+ <Icon name="chevron-down" size={ICON_SIZE[size]} />
156
+ </button>
157
+ </span>
158
+ </span>
159
+ )
160
+ }
161
+ )