@remix-run/ui 0.0.0 → 0.1.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.
Files changed (273) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -2
  3. package/dist/animation/animate-layout-mixin.d.ts +12 -0
  4. package/dist/animation/animate-layout-mixin.js +214 -0
  5. package/dist/animation/animate-layout-mixin.js.map +1 -0
  6. package/dist/animation/animate-mixins.d.ts +29 -0
  7. package/dist/animation/animate-mixins.js +235 -0
  8. package/dist/animation/animate-mixins.js.map +1 -0
  9. package/dist/animation/index.d.ts +6 -0
  10. package/dist/animation/index.js +5 -0
  11. package/dist/animation/index.js.map +1 -0
  12. package/dist/animation/spring.d.ts +75 -0
  13. package/dist/animation/spring.js +183 -0
  14. package/dist/animation/spring.js.map +1 -0
  15. package/dist/animation/tween.d.ts +70 -0
  16. package/dist/animation/tween.js +93 -0
  17. package/dist/animation/tween.js.map +1 -0
  18. package/dist/components/accordion/accordion.d.ts +92 -0
  19. package/dist/components/accordion/accordion.js +337 -0
  20. package/dist/components/accordion/accordion.js.map +1 -0
  21. package/dist/components/anchor/anchor.d.ts +13 -0
  22. package/dist/components/anchor/anchor.js +558 -0
  23. package/dist/components/anchor/anchor.js.map +1 -0
  24. package/dist/components/breadcrumbs/breadcrumbs.d.ts +11 -0
  25. package/dist/components/breadcrumbs/breadcrumbs.js +78 -0
  26. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -0
  27. package/dist/components/button/button.d.ts +23 -0
  28. package/dist/components/button/button.js +147 -0
  29. package/dist/components/button/button.js.map +1 -0
  30. package/dist/components/combobox/combobox.d.ts +101 -0
  31. package/dist/components/combobox/combobox.js +708 -0
  32. package/dist/components/combobox/combobox.js.map +1 -0
  33. package/dist/components/glyph/glyph.d.ts +14 -0
  34. package/dist/components/glyph/glyph.js +65 -0
  35. package/dist/components/glyph/glyph.js.map +1 -0
  36. package/dist/components/listbox/listbox.d.ts +67 -0
  37. package/dist/components/listbox/listbox.js +340 -0
  38. package/dist/components/listbox/listbox.js.map +1 -0
  39. package/dist/components/menu/hover-aim.d.ts +5 -0
  40. package/dist/components/menu/hover-aim.js +308 -0
  41. package/dist/components/menu/hover-aim.js.map +1 -0
  42. package/dist/components/menu/menu.d.ts +164 -0
  43. package/dist/components/menu/menu.js +1106 -0
  44. package/dist/components/menu/menu.js.map +1 -0
  45. package/dist/components/popover/popover.d.ts +35 -0
  46. package/dist/components/popover/popover.js +138 -0
  47. package/dist/components/popover/popover.js.map +1 -0
  48. package/dist/components/select/select.d.ts +67 -0
  49. package/dist/components/select/select.js +397 -0
  50. package/dist/components/select/select.js.map +1 -0
  51. package/dist/components/separator/separator.d.ts +2 -0
  52. package/dist/components/separator/separator.js +15 -0
  53. package/dist/components/separator/separator.js.map +1 -0
  54. package/dist/components/tabs/tabs.d.ts +78 -0
  55. package/dist/components/tabs/tabs.js +341 -0
  56. package/dist/components/tabs/tabs.js.map +1 -0
  57. package/dist/index.d.ts +28 -0
  58. package/dist/index.js +21 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/interactions/keydown/keydown.d.ts +1 -0
  61. package/dist/interactions/keydown/keydown.js +8 -0
  62. package/dist/interactions/keydown/keydown.js.map +1 -0
  63. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -0
  64. package/dist/interactions/outside-click/outside-click-mixin.js +29 -0
  65. package/dist/interactions/outside-click/outside-click-mixin.js.map +1 -0
  66. package/dist/interactions/typeahead/typeahead-mixin.d.ts +8 -0
  67. package/dist/interactions/typeahead/typeahead-mixin.js +59 -0
  68. package/dist/interactions/typeahead/typeahead-mixin.js.map +1 -0
  69. package/dist/jsx-runtime.d.ts +2 -0
  70. package/dist/jsx-runtime.js +3 -0
  71. package/dist/jsx-runtime.js.map +1 -0
  72. package/dist/runtime/client-entries.d.ts +91 -0
  73. package/dist/runtime/client-entries.js +40 -0
  74. package/dist/runtime/client-entries.js.map +1 -0
  75. package/dist/runtime/component.d.ts +223 -0
  76. package/dist/runtime/component.js +152 -0
  77. package/dist/runtime/component.js.map +1 -0
  78. package/dist/runtime/create-element.d.ts +10 -0
  79. package/dist/runtime/create-element.js +29 -0
  80. package/dist/runtime/create-element.js.map +1 -0
  81. package/dist/runtime/diff-dom.d.ts +2 -0
  82. package/dist/runtime/diff-dom.js +364 -0
  83. package/dist/runtime/diff-dom.js.map +1 -0
  84. package/dist/runtime/diff-props.d.ts +9 -0
  85. package/dist/runtime/diff-props.js +195 -0
  86. package/dist/runtime/diff-props.js.map +1 -0
  87. package/dist/runtime/document-state.d.ts +11 -0
  88. package/dist/runtime/document-state.js +106 -0
  89. package/dist/runtime/document-state.js.map +1 -0
  90. package/dist/runtime/dom.d.ts +3189 -0
  91. package/dist/runtime/dom.js +2 -0
  92. package/dist/runtime/dom.js.map +1 -0
  93. package/dist/runtime/error-event.d.ts +20 -0
  94. package/dist/runtime/error-event.js +19 -0
  95. package/dist/runtime/error-event.js.map +1 -0
  96. package/dist/runtime/event-listeners.d.ts +50 -0
  97. package/dist/runtime/event-listeners.js +31 -0
  98. package/dist/runtime/event-listeners.js.map +1 -0
  99. package/dist/runtime/frame.d.ts +119 -0
  100. package/dist/runtime/frame.js +915 -0
  101. package/dist/runtime/frame.js.map +1 -0
  102. package/dist/runtime/invariant.d.ts +23 -0
  103. package/dist/runtime/invariant.js +33 -0
  104. package/dist/runtime/invariant.js.map +1 -0
  105. package/dist/runtime/jsx.d.ts +320 -0
  106. package/dist/runtime/jsx.js +31 -0
  107. package/dist/runtime/jsx.js.map +1 -0
  108. package/dist/runtime/mixins/attrs-mixin.d.ts +9 -0
  109. package/dist/runtime/mixins/attrs-mixin.js +23 -0
  110. package/dist/runtime/mixins/attrs-mixin.js.map +1 -0
  111. package/dist/runtime/mixins/link-mixin.d.ts +17 -0
  112. package/dist/runtime/mixins/link-mixin.js +101 -0
  113. package/dist/runtime/mixins/link-mixin.js.map +1 -0
  114. package/dist/runtime/mixins/mixin.d.ts +145 -0
  115. package/dist/runtime/mixins/mixin.js +570 -0
  116. package/dist/runtime/mixins/mixin.js.map +1 -0
  117. package/dist/runtime/mixins/on-mixin.d.ts +16 -0
  118. package/dist/runtime/mixins/on-mixin.js +49 -0
  119. package/dist/runtime/mixins/on-mixin.js.map +1 -0
  120. package/dist/runtime/mixins/ref-mixin.d.ts +9 -0
  121. package/dist/runtime/mixins/ref-mixin.js +21 -0
  122. package/dist/runtime/mixins/ref-mixin.js.map +1 -0
  123. package/dist/runtime/navigation.d.ts +22 -0
  124. package/dist/runtime/navigation.js +102 -0
  125. package/dist/runtime/navigation.js.map +1 -0
  126. package/dist/runtime/reconcile.d.ts +11 -0
  127. package/dist/runtime/reconcile.js +1240 -0
  128. package/dist/runtime/reconcile.js.map +1 -0
  129. package/dist/runtime/render.d.ts +54 -0
  130. package/dist/runtime/render.js +50 -0
  131. package/dist/runtime/render.js.map +1 -0
  132. package/dist/runtime/run.d.ts +45 -0
  133. package/dist/runtime/run.js +68 -0
  134. package/dist/runtime/run.js.map +1 -0
  135. package/dist/runtime/scheduler.d.ts +30 -0
  136. package/dist/runtime/scheduler.js +185 -0
  137. package/dist/runtime/scheduler.js.map +1 -0
  138. package/dist/runtime/svg-attributes.d.ts +5 -0
  139. package/dist/runtime/svg-attributes.js +124 -0
  140. package/dist/runtime/svg-attributes.js.map +1 -0
  141. package/dist/runtime/to-vnode.d.ts +3 -0
  142. package/dist/runtime/to-vnode.js +43 -0
  143. package/dist/runtime/to-vnode.js.map +1 -0
  144. package/dist/runtime/typed-event-target.d.ts +50 -0
  145. package/dist/runtime/typed-event-target.js +6 -0
  146. package/dist/runtime/typed-event-target.js.map +1 -0
  147. package/dist/runtime/vdom.d.ts +55 -0
  148. package/dist/runtime/vdom.js +198 -0
  149. package/dist/runtime/vdom.js.map +1 -0
  150. package/dist/runtime/vnode.d.ts +79 -0
  151. package/dist/runtime/vnode.js +38 -0
  152. package/dist/runtime/vnode.js.map +1 -0
  153. package/dist/server/stream.d.ts +57 -0
  154. package/dist/server/stream.js +1007 -0
  155. package/dist/server/stream.js.map +1 -0
  156. package/dist/style/css-mixin.d.ts +8 -0
  157. package/dist/style/css-mixin.js +54 -0
  158. package/dist/style/css-mixin.js.map +1 -0
  159. package/dist/style/index.d.ts +5 -0
  160. package/dist/style/index.js +4 -0
  161. package/dist/style/index.js.map +1 -0
  162. package/dist/style/style.d.ts +24 -0
  163. package/dist/style/style.js +269 -0
  164. package/dist/style/style.js.map +1 -0
  165. package/dist/style/stylesheet.d.ts +11 -0
  166. package/dist/style/stylesheet.js +174 -0
  167. package/dist/style/stylesheet.js.map +1 -0
  168. package/dist/test.d.ts +2 -0
  169. package/dist/test.js +2 -0
  170. package/dist/test.js.map +1 -0
  171. package/dist/theme/contract.d.ts +259 -0
  172. package/dist/theme/contract.js +131 -0
  173. package/dist/theme/contract.js.map +1 -0
  174. package/dist/theme/glyph-contract.d.ts +13 -0
  175. package/dist/theme/glyph-contract.js +34 -0
  176. package/dist/theme/glyph-contract.js.map +1 -0
  177. package/dist/theme/presets/rmx-01/glyphs.d.ts +2 -0
  178. package/dist/theme/presets/rmx-01/glyphs.js +252 -0
  179. package/dist/theme/presets/rmx-01/glyphs.js.map +1 -0
  180. package/dist/theme/presets/rmx-01/index.d.ts +2 -0
  181. package/dist/theme/presets/rmx-01/index.js +119 -0
  182. package/dist/theme/presets/rmx-01/index.js.map +1 -0
  183. package/dist/theme/runtime.d.ts +2 -0
  184. package/dist/theme/runtime.js +81 -0
  185. package/dist/theme/runtime.js.map +1 -0
  186. package/dist/theme/theme.d.ts +6 -0
  187. package/dist/theme/theme.js +5 -0
  188. package/dist/theme/theme.js.map +1 -0
  189. package/dist/utils/flash-attribute.d.ts +1 -0
  190. package/dist/utils/flash-attribute.js +11 -0
  191. package/dist/utils/flash-attribute.js.map +1 -0
  192. package/dist/utils/scroll-lock.d.ts +3 -0
  193. package/dist/utils/scroll-lock.js +69 -0
  194. package/dist/utils/scroll-lock.js.map +1 -0
  195. package/dist/utils/wait-for-css-transition.d.ts +1 -0
  196. package/dist/utils/wait-for-css-transition.js +58 -0
  197. package/dist/utils/wait-for-css-transition.js.map +1 -0
  198. package/dist/utils/wait.d.ts +1 -0
  199. package/dist/utils/wait.js +6 -0
  200. package/dist/utils/wait.js.map +1 -0
  201. package/package.json +125 -5
  202. package/src/animation/animate-layout-mixin.ts +266 -0
  203. package/src/animation/animate-mixins.ts +292 -0
  204. package/src/animation/index.ts +6 -0
  205. package/src/animation/spring.ts +299 -0
  206. package/src/animation/tween.ts +134 -0
  207. package/src/components/accordion/accordion.tsx +565 -0
  208. package/src/components/anchor/anchor.ts +904 -0
  209. package/src/components/breadcrumbs/breadcrumbs.tsx +124 -0
  210. package/src/components/button/README.md +44 -0
  211. package/src/components/button/button.tsx +188 -0
  212. package/src/components/combobox/README.md +145 -0
  213. package/src/components/combobox/combobox.tsx +1014 -0
  214. package/src/components/glyph/glyph.tsx +110 -0
  215. package/src/components/listbox/listbox.ts +447 -0
  216. package/src/components/menu/hover-aim.ts +428 -0
  217. package/src/components/menu/menu.tsx +1547 -0
  218. package/src/components/popover/README.md +122 -0
  219. package/src/components/popover/popover.ts +201 -0
  220. package/src/components/select/select.tsx +585 -0
  221. package/src/components/separator/separator.ts +19 -0
  222. package/src/components/tabs/README.md +105 -0
  223. package/src/components/tabs/tabs.tsx +499 -0
  224. package/src/index.ts +72 -0
  225. package/src/interactions/keydown/keydown.ts +14 -0
  226. package/src/interactions/outside-click/outside-click-mixin.ts +55 -0
  227. package/src/interactions/typeahead/typeahead-mixin.ts +89 -0
  228. package/src/jsx-runtime.ts +2 -0
  229. package/src/runtime/client-entries.ts +137 -0
  230. package/src/runtime/component.ts +402 -0
  231. package/src/runtime/create-element.ts +38 -0
  232. package/src/runtime/diff-dom.ts +404 -0
  233. package/src/runtime/diff-props.ts +209 -0
  234. package/src/runtime/document-state.ts +130 -0
  235. package/src/runtime/dom.ts +3971 -0
  236. package/src/runtime/error-event.ts +26 -0
  237. package/src/runtime/event-listeners.ts +171 -0
  238. package/src/runtime/frame.ts +1211 -0
  239. package/src/runtime/invariant.ts +31 -0
  240. package/src/runtime/jsx.ts +398 -0
  241. package/src/runtime/mixins/attrs-mixin.ts +35 -0
  242. package/src/runtime/mixins/link-mixin.ts +131 -0
  243. package/src/runtime/mixins/mixin.ts +908 -0
  244. package/src/runtime/mixins/on-mixin.ts +89 -0
  245. package/src/runtime/mixins/ref-mixin.ts +32 -0
  246. package/src/runtime/navigation.ts +136 -0
  247. package/src/runtime/reconcile.ts +1722 -0
  248. package/src/runtime/render.ts +101 -0
  249. package/src/runtime/run.ts +100 -0
  250. package/src/runtime/scheduler.ts +262 -0
  251. package/src/runtime/svg-attributes.ts +133 -0
  252. package/src/runtime/to-vnode.ts +50 -0
  253. package/src/runtime/typed-event-target.ts +67 -0
  254. package/src/runtime/vdom.ts +295 -0
  255. package/src/runtime/vnode.ts +137 -0
  256. package/src/server/stream.ts +1342 -0
  257. package/src/style/css-mixin.ts +82 -0
  258. package/src/style/index.ts +10 -0
  259. package/src/style/style.ts +312 -0
  260. package/src/style/stylesheet.ts +209 -0
  261. package/src/test/setup.ts +7 -0
  262. package/src/test/utils.ts +45 -0
  263. package/src/test.ts +2 -0
  264. package/src/theme/contract.ts +183 -0
  265. package/src/theme/glyph-contract.ts +57 -0
  266. package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
  267. package/src/theme/presets/rmx-01/index.ts +121 -0
  268. package/src/theme/runtime.ts +116 -0
  269. package/src/theme/theme.ts +15 -0
  270. package/src/utils/flash-attribute.ts +11 -0
  271. package/src/utils/scroll-lock.ts +97 -0
  272. package/src/utils/wait-for-css-transition.ts +66 -0
  273. package/src/utils/wait.ts +5 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Attempt to find the t value for a given x on a cubic bezier curve.
3
+ * Uses Newton-Raphson iteration for fast convergence.
4
+ * @param x1 First control point x coordinate
5
+ * @param x2 Second control point x coordinate
6
+ * @param targetX The x value to solve for
7
+ * @returns The t parameter value
8
+ */
9
+ function solveCubicBezierX(x1: number, x2: number, targetX: number): number {
10
+ // Initial guess
11
+ let t = targetX
12
+
13
+ // Newton-Raphson iteration (usually converges in 4-8 iterations)
14
+ for (let i = 0; i < 8; i++) {
15
+ let currentX = cubicBezier(t, x1, x2)
16
+ let slope = cubicBezierDerivative(t, x1, x2)
17
+
18
+ if (Math.abs(slope) < 1e-6) break
19
+
20
+ let error = currentX - targetX
21
+ if (Math.abs(error) < 1e-6) break
22
+
23
+ t -= error / slope
24
+ }
25
+
26
+ return Math.max(0, Math.min(1, t))
27
+ }
28
+
29
+ /**
30
+ * Compute the value of a cubic bezier at parameter t.
31
+ * For CSS-style beziers, start is (0,0) and end is (1,1),
32
+ * so we only need the two middle control point coordinates.
33
+ * @param t The parameter value (0 to 1)
34
+ * @param p1 First control point coordinate
35
+ * @param p2 Second control point coordinate
36
+ * @returns The bezier value at t
37
+ */
38
+ function cubicBezier(t: number, p1: number, p2: number): number {
39
+ // B(t) = 3(1-t)²t·p1 + 3(1-t)t²·p2 + t³
40
+ let oneMinusT = 1 - t
41
+ return 3 * oneMinusT * oneMinusT * t * p1 + 3 * oneMinusT * t * t * p2 + t * t * t
42
+ }
43
+
44
+ /**
45
+ * Derivative of the cubic bezier function.
46
+ * @param t The parameter value (0 to 1)
47
+ * @param p1 First control point coordinate
48
+ * @param p2 Second control point coordinate
49
+ * @returns The derivative of the bezier at t
50
+ */
51
+ function cubicBezierDerivative(t: number, p1: number, p2: number): number {
52
+ // B'(t) = 3(1-t)²·p1 + 6(1-t)t·(p2-p1) + 3t²·(1-p2)
53
+ let oneMinusT = 1 - t
54
+ return 3 * oneMinusT * oneMinusT * p1 + 6 * oneMinusT * t * (p2 - p1) + 3 * t * t * (1 - p2)
55
+ }
56
+
57
+ /**
58
+ * Cubic-bezier control points used by {@link tween}.
59
+ */
60
+ export interface BezierCurve {
61
+ /** First control point x coordinate. */
62
+ x1: number
63
+ /** First control point y coordinate. */
64
+ y1: number
65
+ /** Second control point x coordinate. */
66
+ x2: number
67
+ /** Second control point y coordinate. */
68
+ y2: number
69
+ }
70
+
71
+ // Common easing presets
72
+ /**
73
+ * Common cubic-bezier presets for {@link tween}.
74
+ */
75
+ export const easings = {
76
+ linear: { x1: 0, y1: 0, x2: 1, y2: 1 },
77
+ ease: { x1: 0.25, y1: 0.1, x2: 0.25, y2: 1 },
78
+ easeIn: { x1: 0.42, y1: 0, x2: 1, y2: 1 },
79
+ easeOut: { x1: 0, y1: 0, x2: 0.58, y2: 1 },
80
+ easeInOut: { x1: 0.42, y1: 0, x2: 0.58, y2: 1 },
81
+ } as const
82
+
83
+ /**
84
+ * Options for generating tweened values over time.
85
+ */
86
+ export interface TweenOptions {
87
+ /** Starting value for the tween. */
88
+ from: number
89
+ /** Ending value for the tween. */
90
+ to: number
91
+ /** Total tween duration in milliseconds. */
92
+ duration: number
93
+ /** Cubic-bezier curve used to shape the interpolation. */
94
+ curve: BezierCurve
95
+ }
96
+
97
+ /**
98
+ * Generator that tweens a value over time using a cubic bezier curve.
99
+ * Yields the current value on each frame. Use the iterator's `done` property
100
+ * to check if the animation is complete.
101
+ * @param options The tween configuration
102
+ * @yields The current tweened value
103
+ * @returns The final value
104
+ */
105
+ export function* tween(options: TweenOptions): Generator<number, number, number> {
106
+ let { from, to, duration, curve } = options
107
+ let { x1, y1, x2, y2 } = curve
108
+
109
+ let startTime: number | null = null
110
+ let value = from
111
+
112
+ while (true) {
113
+ // Yield current value and receive the next timestamp
114
+ let timestamp: number = yield value
115
+
116
+ if (startTime === null) {
117
+ startTime = timestamp
118
+ }
119
+
120
+ let elapsed = timestamp - startTime
121
+ let linearProgress = Math.min(elapsed / duration, 1)
122
+
123
+ // Map linear progress through the bezier curve
124
+ // x-axis = time, y-axis = value
125
+ let t = solveCubicBezierX(x1, x2, linearProgress)
126
+ let easedProgress = cubicBezier(t, y1, y2)
127
+
128
+ value = from + (to - from) * easedProgress
129
+
130
+ if (linearProgress >= 1) {
131
+ return to
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,565 @@
1
+ // @jsxRuntime classic
2
+ // @jsx createElement
3
+ import {
4
+ css,
5
+ createElement,
6
+ on,
7
+ ref,
8
+ type CSSMixinDescriptor,
9
+ type Handle,
10
+ type Props,
11
+ type RemixNode,
12
+ } from '@remix-run/ui'
13
+
14
+ import { spring } from '@remix-run/ui/animation'
15
+ import { Glyph } from '../glyph/glyph.tsx'
16
+ import { theme } from '../../theme/theme.ts'
17
+
18
+ const ACCORDION_CHANGE_EVENT = 'rmx:accordion-change' as const
19
+
20
+ type AccordionChangeHandler = (
21
+ event: AccordionChangeEvent,
22
+ signal: AbortSignal,
23
+ ) => void | Promise<void>
24
+
25
+ declare global {
26
+ interface HTMLElementEventMap {
27
+ [ACCORDION_CHANGE_EVENT]: AccordionChangeEvent
28
+ }
29
+ }
30
+
31
+ export class AccordionChangeEvent extends Event {
32
+ accordionType: AccordionType
33
+ itemValue: string
34
+ value: string | null | string[]
35
+
36
+ constructor(
37
+ value: string | null | string[],
38
+ init: {
39
+ accordionType: AccordionType
40
+ itemValue: string
41
+ },
42
+ ) {
43
+ super(ACCORDION_CHANGE_EVENT, {
44
+ bubbles: true,
45
+ })
46
+ this.accordionType = init.accordionType
47
+ this.itemValue = init.itemValue
48
+ this.value = value
49
+ }
50
+ }
51
+
52
+ type AccordionType = 'single' | 'multiple'
53
+ type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6
54
+
55
+ type AccordionBaseProps = Omit<Props<'div'>, 'children'> & {
56
+ children?: RemixNode
57
+ disabled?: boolean
58
+ headingLevel?: HeadingLevel
59
+ }
60
+
61
+ export type AccordionSingleProps = AccordionBaseProps & {
62
+ type?: 'single'
63
+ value?: string | null
64
+ defaultValue?: string | null
65
+ onValueChange?: (value: string | null) => void
66
+ collapsible?: boolean
67
+ }
68
+
69
+ export type AccordionMultipleProps = AccordionBaseProps & {
70
+ type: 'multiple'
71
+ value?: string[]
72
+ defaultValue?: string[]
73
+ onValueChange?: (value: string[]) => void
74
+ }
75
+
76
+ export type AccordionProps = AccordionSingleProps | AccordionMultipleProps
77
+
78
+ export type AccordionItemProps = Omit<Props<'div'>, 'children'> & {
79
+ children?: RemixNode
80
+ disabled?: boolean
81
+ value: string
82
+ }
83
+
84
+ export type AccordionTriggerProps = Omit<Props<'button'>, 'children' | 'type'> & {
85
+ children?: RemixNode
86
+ indicator?: RemixNode | null
87
+ type?: 'button' | 'submit' | 'reset'
88
+ }
89
+
90
+ export type AccordionContentProps = Omit<Props<'div'>, 'children'> & {
91
+ children?: RemixNode
92
+ }
93
+
94
+ type RegisteredItem = {
95
+ disabled: boolean
96
+ getTriggerNode(): HTMLButtonElement | null
97
+ value: string
98
+ }
99
+
100
+ type AccordionContext = {
101
+ collapsible: boolean
102
+ disabled: boolean
103
+ focusItem(value: string, direction: FocusDirection): void
104
+ getPanelId(value: string): string | undefined
105
+ getTriggerId(value: string): string | undefined
106
+ headingLevel: HeadingLevel
107
+ isOpen(value: string): boolean
108
+ registerItem(item: RegisteredItem): void
109
+ toggleItem(value: string): void
110
+ type: AccordionType
111
+ }
112
+
113
+ type AccordionItemContext = {
114
+ disabled: boolean
115
+ headingLevel: HeadingLevel
116
+ lockedOpen: boolean
117
+ open: boolean
118
+ panelId: string
119
+ setTriggerNode(node: HTMLButtonElement | null): void
120
+ triggerId: string
121
+ value: string
122
+ }
123
+
124
+ type FocusDirection = 'first' | 'last' | 'next' | 'previous'
125
+
126
+ const accordionPanelClipCss = css({
127
+ minHeight: 0,
128
+ overflow: 'hidden',
129
+ })
130
+
131
+ const accordionTransition = spring()
132
+
133
+ const accordionRootCss: CSSMixinDescriptor = css({
134
+ display: 'flex',
135
+ flexDirection: 'column',
136
+ minWidth: 0,
137
+ })
138
+
139
+ const accordionItemCss: CSSMixinDescriptor = css({
140
+ minWidth: 0,
141
+ })
142
+
143
+ const accordionTriggerCss: CSSMixinDescriptor = css({
144
+ all: 'unset',
145
+ boxSizing: 'border-box',
146
+ cursor: 'revert',
147
+ display: 'grid',
148
+ gridTemplateColumns: 'minmax(0, 1fr) auto',
149
+ alignItems: 'center',
150
+ gap: theme.space.md,
151
+ width: '100%',
152
+ minHeight: theme.control.height.lg,
153
+ padding: `${theme.space.md} 0`,
154
+ color: theme.colors.text.primary,
155
+ fontFamily: theme.fontFamily.sans,
156
+ fontSize: theme.fontSize.sm,
157
+ lineHeight: theme.lineHeight.normal,
158
+ fontWeight: theme.fontWeight.medium,
159
+ textAlign: 'left',
160
+ '&:hover:not(:disabled)': {
161
+ backgroundColor: theme.surface.lvl1,
162
+ },
163
+ '&:focus-visible': {
164
+ outline: `2px solid ${theme.colors.focus.ring}`,
165
+ outlineOffset: '2px',
166
+ },
167
+ '&:disabled': {
168
+ opacity: 0.55,
169
+ },
170
+ '& > span:first-child': {
171
+ minWidth: 0,
172
+ },
173
+ })
174
+
175
+ const accordionIndicatorCss: CSSMixinDescriptor = css({
176
+ display: 'inline-flex',
177
+ alignItems: 'center',
178
+ justifyContent: 'center',
179
+ width: theme.fontSize.sm,
180
+ height: theme.fontSize.sm,
181
+ color: theme.colors.text.muted,
182
+ transition: `transform ${accordionTransition}`,
183
+ '& > svg': {
184
+ display: 'block',
185
+ width: '100%',
186
+ height: '100%',
187
+ },
188
+ '&[data-state="open"]': {
189
+ transform: 'rotate(90deg)',
190
+ },
191
+ })
192
+
193
+ const accordionPanelCss: CSSMixinDescriptor = css({
194
+ display: 'grid',
195
+ gridTemplateRows: '0fr',
196
+ transition: `grid-template-rows ${accordionTransition}`,
197
+ '&[data-state="open"]': {
198
+ gridTemplateRows: '1fr',
199
+ },
200
+ '&[data-state="closed"]': {
201
+ pointerEvents: 'none',
202
+ },
203
+ '@media (prefers-reduced-motion: reduce)': {
204
+ transition: 'none',
205
+ },
206
+ })
207
+
208
+ const accordionBodyCss: CSSMixinDescriptor = css({
209
+ display: 'flow-root',
210
+ minHeight: 0,
211
+ paddingBottom: theme.space.md,
212
+ color: theme.colors.text.secondary,
213
+ fontSize: theme.fontSize.sm,
214
+ lineHeight: theme.lineHeight.relaxed,
215
+ '& > :first-child': {
216
+ marginTop: 0,
217
+ },
218
+ '& > :last-child': {
219
+ marginBottom: 0,
220
+ },
221
+ })
222
+
223
+ export const rootStyle = accordionRootCss
224
+ export const itemStyle = accordionItemCss
225
+ export const triggerStyle = accordionTriggerCss
226
+ export const indicatorStyle = accordionIndicatorCss
227
+ export const panelStyle = accordionPanelCss
228
+ export const bodyStyle = accordionBodyCss
229
+
230
+ function isMultipleProps(props: AccordionProps | null): props is AccordionMultipleProps {
231
+ return props?.type === 'multiple'
232
+ }
233
+
234
+ function AccordionImpl(handle: Handle<AccordionProps, AccordionContext>) {
235
+ let rootNode: HTMLElement | null = null
236
+ let registeredItems: RegisteredItem[] = []
237
+ let uncontrolledSingleValue: string | null = null
238
+ let uncontrolledMultipleValue: string[] = []
239
+ let hasInitializedSingle = false
240
+ let hasInitializedMultiple = false
241
+
242
+ let getType = () => (isMultipleProps(handle.props) ? 'multiple' : 'single')
243
+
244
+ let getSingleValue = () => {
245
+ if (isMultipleProps(handle.props)) {
246
+ return null
247
+ }
248
+
249
+ if (handle.props.value !== undefined) {
250
+ return handle.props.value
251
+ }
252
+
253
+ if (!hasInitializedSingle) {
254
+ uncontrolledSingleValue = handle.props.defaultValue ?? null
255
+ hasInitializedSingle = true
256
+ }
257
+
258
+ return uncontrolledSingleValue
259
+ }
260
+
261
+ let getMultipleValue = () => {
262
+ if (!isMultipleProps(handle.props)) {
263
+ return []
264
+ }
265
+
266
+ if (handle.props.value !== undefined) {
267
+ return handle.props.value
268
+ }
269
+
270
+ if (!hasInitializedMultiple) {
271
+ uncontrolledMultipleValue = [...(handle.props.defaultValue ?? [])]
272
+ hasInitializedMultiple = true
273
+ }
274
+
275
+ return uncontrolledMultipleValue
276
+ }
277
+
278
+ let isOpen = (value: string) => {
279
+ if (getType() === 'multiple') {
280
+ return getMultipleValue().includes(value)
281
+ }
282
+
283
+ return getSingleValue() === value
284
+ }
285
+
286
+ let dispatchChange = (itemValue: string, value: string | null | string[]) => {
287
+ rootNode?.dispatchEvent(
288
+ new AccordionChangeEvent(value, {
289
+ accordionType: getType(),
290
+ itemValue,
291
+ }),
292
+ )
293
+ }
294
+
295
+ let toggleItem = (itemValue: string) => {
296
+ if (handle.props.disabled) {
297
+ return
298
+ }
299
+
300
+ if (isMultipleProps(handle.props)) {
301
+ let currentValue = getMultipleValue()
302
+ let nextValue = currentValue.includes(itemValue)
303
+ ? currentValue.filter((value) => value !== itemValue)
304
+ : [...currentValue, itemValue]
305
+
306
+ if (handle.props.value === undefined) {
307
+ uncontrolledMultipleValue = nextValue
308
+ void handle.update()
309
+ }
310
+
311
+ handle.props.onValueChange?.(nextValue)
312
+ dispatchChange(itemValue, nextValue)
313
+ return
314
+ }
315
+
316
+ let isCurrentItemOpen = getSingleValue() === itemValue
317
+ if (isCurrentItemOpen && !(handle.props.collapsible ?? true)) {
318
+ return
319
+ }
320
+
321
+ let nextValue = isCurrentItemOpen ? null : itemValue
322
+
323
+ if (handle.props.value === undefined) {
324
+ uncontrolledSingleValue = nextValue
325
+ void handle.update()
326
+ }
327
+
328
+ handle.props.onValueChange?.(nextValue)
329
+ dispatchChange(itemValue, nextValue)
330
+ }
331
+
332
+ let focusItem = (itemValue: string, direction: FocusDirection) => {
333
+ let items = registeredItems.filter((item) => !item.disabled && item.getTriggerNode() !== null)
334
+ if (items.length === 0) {
335
+ return
336
+ }
337
+
338
+ let currentIndex = items.findIndex((item) => item.value === itemValue)
339
+ let targetIndex = 0
340
+
341
+ switch (direction) {
342
+ case 'first':
343
+ targetIndex = 0
344
+ break
345
+ case 'last':
346
+ targetIndex = items.length - 1
347
+ break
348
+ case 'previous':
349
+ targetIndex = currentIndex <= 0 ? items.length - 1 : currentIndex - 1
350
+ break
351
+ case 'next':
352
+ targetIndex =
353
+ currentIndex === -1 || currentIndex === items.length - 1 ? 0 : currentIndex + 1
354
+ break
355
+ }
356
+
357
+ items[targetIndex]?.getTriggerNode()?.focus()
358
+ }
359
+
360
+ let registerItem = (item: RegisteredItem) => {
361
+ registeredItems.push(item)
362
+ }
363
+
364
+ let getTriggerId = (value: string) =>
365
+ registeredItems.find((item) => item.value === value)?.getTriggerNode()?.id
366
+
367
+ let getPanelId = (value: string) => `${handle.id}-${value}-panel`
368
+
369
+ return () => {
370
+ let collapsible = 'collapsible' in handle.props ? handle.props.collapsible : undefined
371
+ let {
372
+ children,
373
+ defaultValue,
374
+ disabled,
375
+ headingLevel,
376
+ mix,
377
+ onValueChange,
378
+ type,
379
+ value,
380
+ ...divProps
381
+ } = handle.props
382
+ void defaultValue
383
+ void onValueChange
384
+ void value
385
+ registeredItems = []
386
+
387
+ handle.context.set({
388
+ collapsible: type === 'multiple' ? true : (collapsible ?? true),
389
+ disabled: disabled ?? false,
390
+ focusItem,
391
+ getPanelId,
392
+ getTriggerId,
393
+ headingLevel: headingLevel ?? 3,
394
+ isOpen,
395
+ registerItem,
396
+ toggleItem,
397
+ type: type ?? 'single',
398
+ })
399
+
400
+ return (
401
+ <div
402
+ {...divProps}
403
+ data-disabled={disabled ? '' : undefined}
404
+ data-type={type ?? 'single'}
405
+ mix={[
406
+ rootStyle,
407
+ ref((node) => {
408
+ rootNode = node as HTMLElement
409
+ }),
410
+ ...(mix ?? []),
411
+ ]}
412
+ >
413
+ {children}
414
+ </div>
415
+ )
416
+ }
417
+ }
418
+
419
+ export function onAccordionChange(handler: AccordionChangeHandler, captureBoolean?: boolean) {
420
+ return on<HTMLElement, typeof ACCORDION_CHANGE_EVENT>(
421
+ ACCORDION_CHANGE_EVENT,
422
+ handler,
423
+ captureBoolean,
424
+ )
425
+ }
426
+
427
+ export const Accordion = AccordionImpl
428
+
429
+ export function AccordionItem(handle: Handle<AccordionItemProps, AccordionItemContext>) {
430
+ let triggerNode: HTMLButtonElement | null = null
431
+ let triggerId = `${handle.id}-trigger`
432
+ let panelId = `${handle.id}-panel`
433
+
434
+ return () => {
435
+ let { children, disabled: itemDisabled, mix, value, ...divProps } = handle.props
436
+ let accordion = handle.context.get(Accordion)
437
+ let disabled = accordion.disabled || itemDisabled === true
438
+ let open = accordion.isOpen(value)
439
+ let lockedOpen = accordion.type === 'single' && !accordion.collapsible && open
440
+
441
+ accordion.registerItem({
442
+ disabled,
443
+ getTriggerNode: () => triggerNode,
444
+ value,
445
+ })
446
+
447
+ handle.context.set({
448
+ disabled,
449
+ headingLevel: accordion.headingLevel,
450
+ lockedOpen,
451
+ open,
452
+ panelId,
453
+ setTriggerNode(node) {
454
+ triggerNode = node
455
+ },
456
+ triggerId,
457
+ value,
458
+ })
459
+
460
+ return (
461
+ <div
462
+ {...divProps}
463
+ data-disabled={disabled ? '' : undefined}
464
+ data-state={open ? 'open' : 'closed'}
465
+ mix={[itemStyle, ...(mix ?? [])]}
466
+ >
467
+ {children}
468
+ </div>
469
+ )
470
+ }
471
+ }
472
+
473
+ export function AccordionTrigger(handle: Handle<AccordionTriggerProps>) {
474
+ return () => {
475
+ let accordion = handle.context.get(Accordion)
476
+ let item = handle.context.get(AccordionItem)
477
+ let headingTag = `h${item.headingLevel}` as keyof JSX.IntrinsicElements
478
+ let { children, indicator, mix, type, ...buttonProps } = handle.props
479
+ let disabled = item.disabled || handle.props.disabled === true
480
+ let toggleItem = () => {
481
+ if (disabled || item.lockedOpen) {
482
+ return
483
+ }
484
+
485
+ accordion.toggleItem(item.value)
486
+ }
487
+
488
+ let button = (
489
+ <button
490
+ {...buttonProps}
491
+ aria-controls={item.panelId}
492
+ aria-disabled={item.lockedOpen ? true : undefined}
493
+ aria-expanded={item.open}
494
+ data-state={item.open ? 'open' : 'closed'}
495
+ disabled={disabled ? true : undefined}
496
+ id={item.triggerId}
497
+ mix={[
498
+ triggerStyle,
499
+ ref((node) => {
500
+ item.setTriggerNode(node as HTMLButtonElement)
501
+ }),
502
+ on('click', toggleItem),
503
+ on('keydown', (event) => {
504
+ switch (event.key) {
505
+ case 'ArrowDown':
506
+ event.preventDefault()
507
+ accordion.focusItem(item.value, 'next')
508
+ break
509
+ case 'ArrowUp':
510
+ event.preventDefault()
511
+ accordion.focusItem(item.value, 'previous')
512
+ break
513
+ case 'Home':
514
+ event.preventDefault()
515
+ accordion.focusItem(item.value, 'first')
516
+ break
517
+ case 'End':
518
+ event.preventDefault()
519
+ accordion.focusItem(item.value, 'last')
520
+ break
521
+ }
522
+ }),
523
+ mix,
524
+ ]}
525
+ type={type ?? 'button'}
526
+ >
527
+ <span>{children}</span>
528
+ {indicator === null ? null : (
529
+ <span
530
+ data-rmx-accordion-indicator=""
531
+ data-state={item.open ? 'open' : 'closed'}
532
+ mix={indicatorStyle}
533
+ >
534
+ {indicator ?? <Glyph name="chevronRight" />}
535
+ </span>
536
+ )}
537
+ </button>
538
+ )
539
+
540
+ return createElement(headingTag, {}, button)
541
+ }
542
+ }
543
+
544
+ export function AccordionContent(handle: Handle<AccordionContentProps>) {
545
+ return () => {
546
+ let item = handle.context.get(AccordionItem)
547
+ let { children, mix, ...panelProps } = handle.props
548
+
549
+ return (
550
+ <div
551
+ {...panelProps}
552
+ aria-hidden={item.open ? undefined : true}
553
+ aria-labelledby={item.triggerId}
554
+ data-state={item.open ? 'open' : 'closed'}
555
+ id={item.panelId}
556
+ inert={item.open ? undefined : true}
557
+ mix={[panelStyle, mix ?? []]}
558
+ >
559
+ <div mix={accordionPanelClipCss}>
560
+ <div mix={bodyStyle}>{children}</div>
561
+ </div>
562
+ </div>
563
+ )
564
+ }
565
+ }