@reekon-tools/boldr-cad 0.1.0 → 1.0.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 (547) hide show
  1. package/README.md +81 -9
  2. package/dist/app/edit.d.ts +12 -1
  3. package/dist/app/edit.js +96 -11
  4. package/dist/app/edit.js.map +1 -1
  5. package/dist/app/state.d.ts +31 -4
  6. package/dist/app/state.js +31 -2
  7. package/dist/app/state.js.map +1 -1
  8. package/dist/data/memory.d.ts +5 -0
  9. package/dist/data/memory.js +2 -2
  10. package/dist/data/memory.js.map +1 -1
  11. package/dist/data/provider.d.ts +19 -7
  12. package/dist/data/sync.d.ts +7 -2
  13. package/dist/data/sync.js +115 -64
  14. package/dist/data/sync.js.map +1 -1
  15. package/dist/editor/actions.d.ts +1 -2
  16. package/dist/editor/actions.js +391 -113
  17. package/dist/editor/actions.js.map +1 -1
  18. package/dist/editor/confirm.d.ts +23 -0
  19. package/dist/editor/confirm.js +35 -0
  20. package/dist/editor/confirm.js.map +1 -0
  21. package/dist/editor/elevation.d.ts +191 -90
  22. package/dist/editor/elevation.js +812 -616
  23. package/dist/editor/elevation.js.map +1 -1
  24. package/dist/editor/face.d.ts +73 -0
  25. package/dist/editor/face.js +269 -0
  26. package/dist/editor/face.js.map +1 -0
  27. package/dist/editor/files.d.ts +6 -0
  28. package/dist/editor/files.js +50 -4
  29. package/dist/editor/files.js.map +1 -1
  30. package/dist/editor/gestures.d.ts +18 -3
  31. package/dist/editor/gestures.js +145 -30
  32. package/dist/editor/gestures.js.map +1 -1
  33. package/dist/editor/keypad.d.ts +19 -4
  34. package/dist/editor/keypad.js +39 -9
  35. package/dist/editor/keypad.js.map +1 -1
  36. package/dist/editor/keys.js +36 -4
  37. package/dist/editor/keys.js.map +1 -1
  38. package/dist/editor/laser.d.ts +20 -4
  39. package/dist/editor/laser.js +104 -21
  40. package/dist/editor/laser.js.map +1 -1
  41. package/dist/editor/linkActions.d.ts +49 -0
  42. package/dist/editor/linkActions.js +210 -0
  43. package/dist/editor/linkActions.js.map +1 -0
  44. package/dist/editor/linkFollow.d.ts +2 -0
  45. package/dist/editor/linkFollow.js +53 -0
  46. package/dist/editor/linkFollow.js.map +1 -0
  47. package/dist/editor/linkWrite.d.ts +13 -0
  48. package/dist/editor/linkWrite.js +84 -0
  49. package/dist/editor/linkWrite.js.map +1 -0
  50. package/dist/editor/links.d.ts +84 -0
  51. package/dist/editor/links.js +130 -0
  52. package/dist/editor/links.js.map +1 -0
  53. package/dist/editor/markup.d.ts +29 -3
  54. package/dist/editor/markup.js +95 -10
  55. package/dist/editor/markup.js.map +1 -1
  56. package/dist/editor/mode.d.ts +40 -2
  57. package/dist/editor/mode.js +71 -4
  58. package/dist/editor/mode.js.map +1 -1
  59. package/dist/editor/ports.d.ts +36 -6
  60. package/dist/editor/ports.js +10 -1
  61. package/dist/editor/ports.js.map +1 -1
  62. package/dist/editor/project.d.ts +38 -6
  63. package/dist/editor/project.js +167 -27
  64. package/dist/editor/project.js.map +1 -1
  65. package/dist/editor/selection.d.ts +10 -0
  66. package/dist/editor/selection.js +62 -8
  67. package/dist/editor/selection.js.map +1 -1
  68. package/dist/editor/status.js +3 -0
  69. package/dist/editor/status.js.map +1 -1
  70. package/dist/editor/tooltips.d.ts +5 -0
  71. package/dist/editor/tooltips.js +48 -0
  72. package/dist/editor/tooltips.js.map +1 -0
  73. package/dist/editor/view3d.d.ts +17 -0
  74. package/dist/editor/view3d.js +135 -0
  75. package/dist/editor/view3d.js.map +1 -0
  76. package/dist/editor/view3dState.d.ts +39 -0
  77. package/dist/editor/view3dState.js +44 -0
  78. package/dist/editor/view3dState.js.map +1 -0
  79. package/dist/export/dxf.js +1 -1
  80. package/dist/export/dxf.js.map +1 -1
  81. package/dist/export/pdf.js +17 -12
  82. package/dist/export/pdf.js.map +1 -1
  83. package/dist/export/plan.d.ts +23 -6
  84. package/dist/export/plan.js +127 -44
  85. package/dist/export/plan.js.map +1 -1
  86. package/dist/index.d.ts +5 -0
  87. package/dist/index.js +2 -0
  88. package/dist/index.js.map +1 -1
  89. package/dist/model/constraints.js +15 -4
  90. package/dist/model/constraints.js.map +1 -1
  91. package/dist/model/demo.js +3 -3
  92. package/dist/model/demo.js.map +1 -1
  93. package/dist/model/elevation.d.ts +6 -9
  94. package/dist/model/elevation.js +5 -13
  95. package/dist/model/elevation.js.map +1 -1
  96. package/dist/model/faces.d.ts +58 -0
  97. package/dist/model/faces.js +197 -0
  98. package/dist/model/faces.js.map +1 -0
  99. package/dist/model/facesolve.d.ts +72 -0
  100. package/dist/model/facesolve.js +369 -0
  101. package/dist/model/facesolve.js.map +1 -0
  102. package/dist/model/history.d.ts +3 -2
  103. package/dist/model/history.js +17 -11
  104. package/dist/model/history.js.map +1 -1
  105. package/dist/model/images.d.ts +18 -0
  106. package/dist/model/images.js +26 -0
  107. package/dist/model/images.js.map +1 -0
  108. package/dist/model/links.d.ts +46 -0
  109. package/dist/model/links.js +73 -0
  110. package/dist/model/links.js.map +1 -0
  111. package/dist/model/markup.d.ts +46 -0
  112. package/dist/model/markup.js +71 -0
  113. package/dist/model/markup.js.map +1 -0
  114. package/dist/model/measurement.d.ts +11 -0
  115. package/dist/model/measurement.js +24 -0
  116. package/dist/model/measurement.js.map +1 -0
  117. package/dist/model/openings.d.ts +4 -0
  118. package/dist/model/openings.js +4 -0
  119. package/dist/model/openings.js.map +1 -1
  120. package/dist/model/planGeom.d.ts +72 -2
  121. package/dist/model/planGeom.js +184 -10
  122. package/dist/model/planGeom.js.map +1 -1
  123. package/dist/model/project.js +3 -2
  124. package/dist/model/project.js.map +1 -1
  125. package/dist/model/projectfile.js +2 -1
  126. package/dist/model/projectfile.js.map +1 -1
  127. package/dist/model/rooms.js +2 -2
  128. package/dist/model/rooms.js.map +1 -1
  129. package/dist/model/settings.d.ts +6 -0
  130. package/dist/model/settings.js +2 -2
  131. package/dist/model/settings.js.map +1 -1
  132. package/dist/model/sketch.d.ts +14 -0
  133. package/dist/model/sketch.js +143 -24
  134. package/dist/model/sketch.js.map +1 -1
  135. package/dist/model/smooth.d.ts +6 -0
  136. package/dist/model/smooth.js +52 -0
  137. package/dist/model/smooth.js.map +1 -0
  138. package/dist/model/store.d.ts +12 -0
  139. package/dist/model/store.js +35 -3
  140. package/dist/model/store.js.map +1 -1
  141. package/dist/model/types.d.ts +173 -8
  142. package/dist/model/wallfaces.d.ts +32 -0
  143. package/dist/model/wallfaces.js +133 -20
  144. package/dist/model/wallfaces.js.map +1 -1
  145. package/dist/model/wallshape.d.ts +15 -0
  146. package/dist/model/wallshape.js +89 -0
  147. package/dist/model/wallshape.js.map +1 -0
  148. package/dist/react/BottomBar.d.ts +9 -5
  149. package/dist/react/BottomBar.js +213 -24
  150. package/dist/react/BottomBar.js.map +1 -1
  151. package/dist/react/Buttons.d.ts +13 -2
  152. package/dist/react/Buttons.js +37 -5
  153. package/dist/react/Buttons.js.map +1 -1
  154. package/dist/react/CadEditor.d.ts +46 -3
  155. package/dist/react/CadEditor.js +77 -20
  156. package/dist/react/CadEditor.js.map +1 -1
  157. package/dist/react/ConfirmSheet.d.ts +4 -0
  158. package/dist/react/ConfirmSheet.js +31 -0
  159. package/dist/react/ConfirmSheet.js.map +1 -0
  160. package/dist/react/ConflictSheet.d.ts +2 -1
  161. package/dist/react/ConflictSheet.js +16 -8
  162. package/dist/react/ConflictSheet.js.map +1 -1
  163. package/dist/react/Drawer.d.ts +8 -2
  164. package/dist/react/Drawer.js +167 -38
  165. package/dist/react/Drawer.js.map +1 -1
  166. package/dist/react/Duck.d.ts +13 -0
  167. package/dist/react/Duck.js +45 -0
  168. package/dist/react/Duck.js.map +1 -0
  169. package/dist/react/Empty3D.d.ts +2 -0
  170. package/dist/react/Empty3D.js +39 -0
  171. package/dist/react/Empty3D.js.map +1 -0
  172. package/dist/react/FitScroll.d.ts +6 -0
  173. package/dist/react/FitScroll.js +14 -0
  174. package/dist/react/FitScroll.js.map +1 -0
  175. package/dist/react/Icon.native.d.ts +6 -0
  176. package/dist/react/Icon.native.js +4 -0
  177. package/dist/react/Icon.native.js.map +1 -1
  178. package/dist/react/Icon.web.d.ts +6 -0
  179. package/dist/react/Icon.web.js +4 -0
  180. package/dist/react/Icon.web.js.map +1 -1
  181. package/dist/react/KeypadSheet.d.ts +9 -2
  182. package/dist/react/KeypadSheet.js +174 -42
  183. package/dist/react/KeypadSheet.js.map +1 -1
  184. package/dist/react/LaserTray.js +8 -5
  185. package/dist/react/LaserTray.js.map +1 -1
  186. package/dist/react/MarkupPanel.d.ts +8 -0
  187. package/dist/react/MarkupPanel.js +67 -0
  188. package/dist/react/MarkupPanel.js.map +1 -0
  189. package/dist/react/PageThumb.native.d.ts +7 -2
  190. package/dist/react/PageThumb.native.js +19 -7
  191. package/dist/react/PageThumb.native.js.map +1 -1
  192. package/dist/react/PageThumb.web.d.ts +3 -2
  193. package/dist/react/PageThumb.web.js +4 -4
  194. package/dist/react/PageThumb.web.js.map +1 -1
  195. package/dist/react/PlanCanvas.native.d.ts +1 -1
  196. package/dist/react/PlanCanvas.native.js +134 -36
  197. package/dist/react/PlanCanvas.native.js.map +1 -1
  198. package/dist/react/PlanCanvas.web.js +16 -5
  199. package/dist/react/PlanCanvas.web.js.map +1 -1
  200. package/dist/react/Popovers.d.ts +12 -6
  201. package/dist/react/Popovers.js +53 -26
  202. package/dist/react/Popovers.js.map +1 -1
  203. package/dist/react/Rail.d.ts +24 -0
  204. package/dist/react/Rail.js +324 -34
  205. package/dist/react/Rail.js.map +1 -1
  206. package/dist/react/ShapeMenu.d.ts +7 -0
  207. package/dist/react/ShapeMenu.js +58 -0
  208. package/dist/react/ShapeMenu.js.map +1 -0
  209. package/dist/react/Sheet.d.ts +14 -4
  210. package/dist/react/Sheet.js +54 -9
  211. package/dist/react/Sheet.js.map +1 -1
  212. package/dist/react/Slider.d.ts +18 -0
  213. package/dist/react/Slider.js +27 -0
  214. package/dist/react/Slider.js.map +1 -0
  215. package/dist/react/Toast.js +14 -6
  216. package/dist/react/Toast.js.map +1 -1
  217. package/dist/react/ToolTip.d.ts +7 -0
  218. package/dist/react/ToolTip.js +43 -0
  219. package/dist/react/ToolTip.js.map +1 -0
  220. package/dist/react/TopBar.d.ts +6 -3
  221. package/dist/react/TopBar.js +48 -17
  222. package/dist/react/TopBar.js.map +1 -1
  223. package/dist/react/Tween.d.ts +7 -0
  224. package/dist/react/Tween.js +26 -0
  225. package/dist/react/Tween.js.map +1 -0
  226. package/dist/react/Tween.native.d.ts +6 -0
  227. package/dist/react/Tween.native.js +43 -0
  228. package/dist/react/Tween.native.js.map +1 -0
  229. package/dist/react/View3D.native.d.ts +4 -0
  230. package/dist/react/View3D.native.js +103 -0
  231. package/dist/react/View3D.native.js.map +1 -0
  232. package/dist/react/View3D.web.d.ts +4 -0
  233. package/dist/react/View3D.web.js +151 -0
  234. package/dist/react/View3D.web.js.map +1 -0
  235. package/dist/react/View3DHost.d.ts +143 -0
  236. package/dist/react/View3DHost.js +377 -0
  237. package/dist/react/View3DHost.js.map +1 -0
  238. package/dist/react/corners.d.ts +7 -0
  239. package/dist/react/corners.js +51 -0
  240. package/dist/react/corners.js.map +1 -0
  241. package/dist/react/easing.d.ts +15 -0
  242. package/dist/react/easing.js +10 -0
  243. package/dist/react/easing.js.map +1 -0
  244. package/dist/react/host.d.ts +7 -2
  245. package/dist/react/host.js +8 -3
  246. package/dist/react/host.js.map +1 -1
  247. package/dist/react/hostKeypad.d.ts +56 -0
  248. package/dist/react/hostKeypad.js +5 -0
  249. package/dist/react/hostKeypad.js.map +1 -0
  250. package/dist/react/motion.d.ts +77 -0
  251. package/dist/react/motion.js +173 -0
  252. package/dist/react/motion.js.map +1 -0
  253. package/dist/react/ports.js +90 -8
  254. package/dist/react/ports.js.map +1 -1
  255. package/dist/react/saving.d.ts +15 -0
  256. package/dist/react/saving.js +76 -0
  257. package/dist/react/saving.js.map +1 -0
  258. package/dist/react/store.d.ts +38 -1
  259. package/dist/react/store.js +31 -1
  260. package/dist/react/store.js.map +1 -1
  261. package/dist/react/theme.d.ts +10 -0
  262. package/dist/react/theme.js +8 -0
  263. package/dist/react/theme.js.map +1 -1
  264. package/dist/react/topRow.d.ts +22 -0
  265. package/dist/react/topRow.js +61 -0
  266. package/dist/react/topRow.js.map +1 -0
  267. package/dist/react/touch.d.ts +6 -0
  268. package/dist/react/touch.js +9 -0
  269. package/dist/react/touch.js.map +1 -0
  270. package/dist/render/ctx.d.ts +8 -0
  271. package/dist/render/draw.d.ts +33 -1
  272. package/dist/render/draw.js +593 -215
  273. package/dist/render/draw.js.map +1 -1
  274. package/dist/render/elevation.d.ts +65 -43
  275. package/dist/render/elevation.js +414 -140
  276. package/dist/render/elevation.js.map +1 -1
  277. package/dist/render/figmaIcons.d.ts +12 -0
  278. package/dist/render/figmaIcons.js +21 -0
  279. package/dist/render/figmaIcons.js.map +1 -0
  280. package/dist/render/hit.d.ts +22 -2
  281. package/dist/render/hit.js +168 -25
  282. package/dist/render/hit.js.map +1 -1
  283. package/dist/render/icons.js +69 -10
  284. package/dist/render/icons.js.map +1 -1
  285. package/dist/render/linkTile.d.ts +15 -0
  286. package/dist/render/linkTile.js +369 -0
  287. package/dist/render/linkTile.js.map +1 -0
  288. package/dist/render/markup.d.ts +11 -0
  289. package/dist/render/markup.js +112 -0
  290. package/dist/render/markup.js.map +1 -0
  291. package/dist/render/navigate.d.ts +88 -0
  292. package/dist/render/navigate.js +100 -0
  293. package/dist/render/navigate.js.map +1 -0
  294. package/dist/render/snap.d.ts +20 -1
  295. package/dist/render/snap.js +118 -25
  296. package/dist/render/snap.js.map +1 -1
  297. package/dist/render/squircle.d.ts +8 -0
  298. package/dist/render/squircle.js +71 -0
  299. package/dist/render/squircle.js.map +1 -0
  300. package/dist/render/view.d.ts +115 -2
  301. package/dist/render/view.js +244 -19
  302. package/dist/render/view.js.map +1 -1
  303. package/dist/skia/SkiaDrawCtx.d.ts +30 -2
  304. package/dist/skia/SkiaDrawCtx.js +198 -43
  305. package/dist/skia/SkiaDrawCtx.js.map +1 -1
  306. package/dist/skia/host.d.ts +18 -2
  307. package/dist/skia/host.js +46 -1
  308. package/dist/skia/host.js.map +1 -1
  309. package/dist/solver/equations.js +37 -2
  310. package/dist/solver/equations.js.map +1 -1
  311. package/dist/solver/gauss-newton.d.ts +17 -0
  312. package/dist/solver/gauss-newton.js +144 -36
  313. package/dist/solver/gauss-newton.js.map +1 -1
  314. package/dist/solver/linalg.d.ts +45 -2
  315. package/dist/solver/linalg.js +164 -10
  316. package/dist/solver/linalg.js.map +1 -1
  317. package/dist/solver/types.d.ts +5 -0
  318. package/dist/tools/annotate.d.ts +57 -1
  319. package/dist/tools/annotate.js +64 -43
  320. package/dist/tools/annotate.js.map +1 -1
  321. package/dist/tools/chamfer.d.ts +12 -0
  322. package/dist/tools/chamfer.js +114 -0
  323. package/dist/tools/chamfer.js.map +1 -0
  324. package/dist/tools/corner.d.ts +38 -0
  325. package/dist/tools/corner.js +99 -0
  326. package/dist/tools/corner.js.map +1 -0
  327. package/dist/tools/dim.d.ts +86 -9
  328. package/dist/tools/dim.js +581 -96
  329. package/dist/tools/dim.js.map +1 -1
  330. package/dist/tools/elevation.js +1 -1
  331. package/dist/tools/elevation.js.map +1 -1
  332. package/dist/tools/fillet.d.ts +9 -0
  333. package/dist/tools/fillet.js +86 -0
  334. package/dist/tools/fillet.js.map +1 -0
  335. package/dist/tools/freehand.d.ts +3 -2
  336. package/dist/tools/freehand.js +19 -12
  337. package/dist/tools/freehand.js.map +1 -1
  338. package/dist/tools/index.js +14 -4
  339. package/dist/tools/index.js.map +1 -1
  340. package/dist/tools/line.d.ts +8 -3
  341. package/dist/tools/line.js +66 -30
  342. package/dist/tools/line.js.map +1 -1
  343. package/dist/tools/modify.d.ts +13 -0
  344. package/dist/tools/modify.js +105 -25
  345. package/dist/tools/modify.js.map +1 -1
  346. package/dist/tools/openings.js +22 -15
  347. package/dist/tools/openings.js.map +1 -1
  348. package/dist/tools/registry.d.ts +8 -0
  349. package/dist/tools/registry.js +42 -2
  350. package/dist/tools/registry.js.map +1 -1
  351. package/dist/tools/select.d.ts +77 -3
  352. package/dist/tools/select.js +560 -74
  353. package/dist/tools/select.js.map +1 -1
  354. package/dist/tools/shapes.d.ts +14 -0
  355. package/dist/tools/shapes.js +154 -75
  356. package/dist/tools/shapes.js.map +1 -1
  357. package/dist/units/format.d.ts +10 -2
  358. package/dist/units/format.js +18 -3
  359. package/dist/units/format.js.map +1 -1
  360. package/dist/view3d/build.d.ts +35 -0
  361. package/dist/view3d/build.js +630 -0
  362. package/dist/view3d/build.js.map +1 -0
  363. package/dist/view3d/camera.d.ts +60 -0
  364. package/dist/view3d/camera.js +84 -0
  365. package/dist/view3d/camera.js.map +1 -0
  366. package/dist/view3d/fatLines.d.ts +11 -0
  367. package/dist/view3d/fatLines.js +83 -0
  368. package/dist/view3d/fatLines.js.map +1 -0
  369. package/dist/view3d/lines.d.ts +17 -0
  370. package/dist/view3d/lines.js +271 -0
  371. package/dist/view3d/lines.js.map +1 -0
  372. package/dist/view3d/mesh.d.ts +51 -0
  373. package/dist/view3d/mesh.js +101 -0
  374. package/dist/view3d/mesh.js.map +1 -0
  375. package/dist/view3d/poly.d.ts +16 -0
  376. package/dist/view3d/poly.js +109 -0
  377. package/dist/view3d/poly.js.map +1 -0
  378. package/dist/view3d/viewer.d.ts +86 -0
  379. package/dist/view3d/viewer.js +350 -0
  380. package/dist/view3d/viewer.js.map +1 -0
  381. package/package.json +15 -2
  382. package/src/app/edit.ts +83 -12
  383. package/src/app/main.ts +11 -5
  384. package/src/app/state.ts +38 -5
  385. package/src/data/memory.ts +2 -2
  386. package/src/data/provider.ts +15 -7
  387. package/src/data/sync.ts +87 -50
  388. package/src/editor/actions.ts +292 -83
  389. package/src/editor/confirm.ts +58 -0
  390. package/src/editor/elevation.ts +698 -478
  391. package/src/editor/face.ts +227 -0
  392. package/src/editor/files.ts +40 -4
  393. package/src/editor/gestures.ts +147 -31
  394. package/src/editor/keypad.ts +43 -11
  395. package/src/editor/keys.ts +25 -5
  396. package/src/editor/laser.ts +98 -20
  397. package/src/editor/linkActions.ts +194 -0
  398. package/src/editor/linkFollow.ts +43 -0
  399. package/src/editor/linkWrite.ts +77 -0
  400. package/src/editor/links.ts +181 -0
  401. package/src/editor/markup.ts +92 -8
  402. package/src/editor/mode.ts +83 -7
  403. package/src/editor/ports.ts +37 -6
  404. package/src/editor/project.ts +155 -26
  405. package/src/editor/selection.ts +54 -8
  406. package/src/editor/status.ts +2 -0
  407. package/src/editor/tooltips.ts +47 -0
  408. package/src/editor/view3d.ts +119 -0
  409. package/src/editor/view3dState.ts +61 -0
  410. package/src/export/dxf.ts +1 -1
  411. package/src/export/pdf.ts +15 -7
  412. package/src/export/plan.ts +119 -35
  413. package/src/index.ts +5 -0
  414. package/src/input/keyboard.ts +2 -4
  415. package/src/model/constraints.ts +13 -4
  416. package/src/model/demo.ts +3 -3
  417. package/src/model/elevation.ts +6 -16
  418. package/src/model/faces.ts +153 -0
  419. package/src/model/facesolve.ts +322 -0
  420. package/src/model/history.ts +19 -11
  421. package/src/model/images.ts +35 -0
  422. package/src/model/links.ts +77 -0
  423. package/src/model/markup.ts +80 -0
  424. package/src/model/measurement.ts +26 -0
  425. package/src/model/openings.ts +6 -0
  426. package/src/model/planGeom.ts +174 -8
  427. package/src/model/project.ts +3 -2
  428. package/src/model/projectfile.ts +2 -1
  429. package/src/model/rooms.ts +1 -1
  430. package/src/model/settings.ts +8 -2
  431. package/src/model/sketch.ts +101 -14
  432. package/src/model/smooth.ts +51 -0
  433. package/src/model/store.ts +35 -4
  434. package/src/model/types.ts +170 -8
  435. package/src/model/wallfaces.ts +126 -17
  436. package/src/model/wallshape.ts +97 -0
  437. package/src/react/BottomBar.tsx +263 -46
  438. package/src/react/Buttons.tsx +50 -9
  439. package/src/react/CadEditor.tsx +133 -30
  440. package/src/react/ConfirmSheet.tsx +38 -0
  441. package/src/react/ConflictSheet.tsx +19 -12
  442. package/src/react/Drawer.tsx +213 -110
  443. package/src/react/Duck.tsx +50 -0
  444. package/src/react/Empty3D.tsx +32 -0
  445. package/src/react/FitScroll.tsx +19 -0
  446. package/src/react/Icon.native.tsx +5 -0
  447. package/src/react/Icon.web.tsx +5 -0
  448. package/src/react/KeypadSheet.tsx +253 -60
  449. package/src/react/LaserTray.tsx +12 -8
  450. package/src/react/MarkupPanel.tsx +150 -0
  451. package/src/react/PageThumb.native.tsx +18 -7
  452. package/src/react/PageThumb.web.tsx +4 -4
  453. package/src/react/PlanCanvas.native.tsx +151 -34
  454. package/src/react/PlanCanvas.web.tsx +15 -5
  455. package/src/react/Popovers.tsx +116 -51
  456. package/src/react/Rail.tsx +360 -27
  457. package/src/react/ShapeMenu.tsx +66 -0
  458. package/src/react/Sheet.tsx +56 -11
  459. package/src/react/Slider.tsx +48 -0
  460. package/src/react/Toast.tsx +16 -9
  461. package/src/react/ToolTip.tsx +49 -0
  462. package/src/react/TopBar.tsx +97 -39
  463. package/src/react/Tween.native.tsx +36 -0
  464. package/src/react/Tween.tsx +24 -0
  465. package/src/react/View3D.native.tsx +131 -0
  466. package/src/react/View3D.web.tsx +131 -0
  467. package/src/react/View3DHost.ts +367 -0
  468. package/src/react/corners.ts +52 -0
  469. package/src/react/easing.ts +14 -0
  470. package/src/react/host.ts +8 -3
  471. package/src/react/hostKeypad.ts +66 -0
  472. package/src/react/motion.tsx +239 -0
  473. package/src/react/ports.ts +78 -9
  474. package/src/react/saving.ts +80 -0
  475. package/src/react/store.ts +35 -3
  476. package/src/react/theme.ts +8 -0
  477. package/src/react/topRow.ts +71 -0
  478. package/src/react/touch.ts +10 -0
  479. package/src/render/ctx.ts +8 -0
  480. package/src/render/draw.ts +437 -175
  481. package/src/render/elevation.ts +349 -143
  482. package/src/render/figmaIcons.ts +22 -0
  483. package/src/render/hit.ts +140 -21
  484. package/src/render/icons.ts +69 -10
  485. package/src/render/linkTile.ts +297 -0
  486. package/src/render/markup.ts +91 -0
  487. package/src/render/navigate.ts +114 -0
  488. package/src/render/snap.ts +105 -22
  489. package/src/render/squircle.ts +73 -0
  490. package/src/render/view.ts +255 -19
  491. package/src/skia/SkiaDrawCtx.ts +163 -41
  492. package/src/skia/host.ts +46 -1
  493. package/src/solver/equations.ts +37 -2
  494. package/src/solver/gauss-newton.ts +125 -28
  495. package/src/solver/linalg.ts +141 -5
  496. package/src/solver/types.ts +5 -0
  497. package/src/styles.css +91 -42
  498. package/src/tools/annotate.ts +58 -29
  499. package/src/tools/chamfer.ts +103 -0
  500. package/src/tools/corner.ts +99 -0
  501. package/src/tools/dim.ts +524 -92
  502. package/src/tools/elevation.ts +1 -1
  503. package/src/tools/fillet.ts +82 -0
  504. package/src/tools/freehand.ts +20 -16
  505. package/src/tools/index.ts +17 -4
  506. package/src/tools/line.ts +59 -23
  507. package/src/tools/modify.ts +86 -24
  508. package/src/tools/openings.ts +8 -13
  509. package/src/tools/registry.ts +27 -2
  510. package/src/tools/select.ts +424 -75
  511. package/src/tools/shapes.ts +119 -58
  512. package/src/ui/ctx.ts +2 -1
  513. package/src/ui/domPorts.ts +23 -4
  514. package/src/ui/drawer.ts +6 -26
  515. package/src/ui/elevation.ts +10 -101
  516. package/src/ui/keypad.ts +8 -14
  517. package/src/ui/laser.ts +2 -2
  518. package/src/ui/menu.ts +63 -17
  519. package/src/ui/noteinput.ts +3 -3
  520. package/src/ui/opacity.ts +108 -0
  521. package/src/ui/palette.ts +67 -12
  522. package/src/ui/rail.ts +21 -7
  523. package/src/ui/topbar.ts +4 -14
  524. package/src/units/format.ts +18 -3
  525. package/src/view3d/build.ts +607 -0
  526. package/src/view3d/camera.ts +113 -0
  527. package/src/view3d/fatLines.ts +83 -0
  528. package/src/view3d/lines.ts +258 -0
  529. package/src/view3d/mesh.ts +111 -0
  530. package/src/view3d/poly.ts +104 -0
  531. package/src/view3d/viewer.ts +348 -0
  532. package/dist/react/ElevCanvas.native.d.ts +0 -3
  533. package/dist/react/ElevCanvas.native.js +0 -58
  534. package/dist/react/ElevCanvas.native.js.map +0 -1
  535. package/dist/react/ElevCanvas.web.d.ts +0 -4
  536. package/dist/react/ElevCanvas.web.js +0 -44
  537. package/dist/react/ElevCanvas.web.js.map +0 -1
  538. package/dist/react/ElevationSheet.d.ts +0 -2
  539. package/dist/react/ElevationSheet.js +0 -38
  540. package/dist/react/ElevationSheet.js.map +0 -1
  541. package/dist/render/elevationView.d.ts +0 -3
  542. package/dist/render/elevationView.js +0 -100
  543. package/dist/render/elevationView.js.map +0 -1
  544. package/src/react/ElevCanvas.native.tsx +0 -68
  545. package/src/react/ElevCanvas.web.tsx +0 -42
  546. package/src/react/ElevationSheet.tsx +0 -88
  547. package/src/render/elevationView.ts +0 -73
@@ -1,45 +1,378 @@
1
- import { Pressable, ScrollView, StyleSheet, View } from 'react-native';
1
+ import { toolTip } from '../editor/tooltips';
2
+ import { notify, ui, useEditorKey } from './store';
3
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { Animated, Dimensions, Platform, ScrollView, StyleSheet, View } from 'react-native';
2
5
  import { redo, undo } from '../app/edit';
3
6
  import { ed } from '../app/state';
4
- import { RAIL, toggleConstr } from '../editor/mode';
5
- import { openPalette, toast } from '../editor/ports';
7
+ import { groupTools, RAIL, railIcon, railItems, TOGGLES, TOOL_GROUPS, toolLabel, type RailItem } from '../editor/mode';
8
+ import { markupTarget } from '../editor/markup';
9
+ import { elevOpen } from '../editor/elevation';
10
+ import { openPalette } from '../editor/ports';
6
11
  import { setTool } from '../tools/registry';
12
+ import { addLinks, LINK_PLUS_LABEL, linkPlusVisible } from '../editor/linkActions';
13
+ import { canLink } from '../editor/links';
7
14
  import { hovered } from './Buttons';
8
15
  import { Icon } from './Icon';
9
16
  import { C, shadow } from './theme';
17
+ import { EASE, DURATION, Tap, TOGGLE_EASE, useEased } from './motion';
18
+ import { NATIVE } from './easing';
19
+ import { TweenView } from './Tween';
20
+ import { createSheet } from './corners';
21
+
22
+ /** A rail button's icon, fading from white to dark as it's selected (two icons, crossfaded). */
23
+ const RailIcon = memo(function RailIcon({ name, size, on }: { name: string; size: number; on: boolean }) {
24
+ const t = useEased(on), off = useMemo(() => t.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }), [t]);
25
+ return (
26
+ <View style={{ width: size, height: size }}>
27
+ <Animated.View style={[StyleSheet.absoluteFill, { opacity: off }]}><Icon name={name} size={size} color="#fff" /></Animated.View>
28
+ <Animated.View style={[StyleSheet.absoluteFill, { opacity: t }]}><Icon name={name} size={size} color={C.dark} /></Animated.View>
29
+ </View>
30
+ );
31
+ });
32
+
33
+ /**
34
+ * The selected tool's white disc: it glides from tool to tool (and fades when the tool isn't on the rail), moved by a
35
+ * transform so the glide runs on the native driver. Where it goes is worked out from the rail's own layout, not
36
+ * measured: a button that moves without changing size reports no new layout (react-native-web), which left the disc
37
+ * between buttons.
38
+ */
39
+ function useGlide(top: number | null) {
40
+ const y = useRef(new Animated.Value(0)).current, o = useRef(new Animated.Value(0)).current, seen = useRef(false);
41
+ useEffect(() => {
42
+ const go = (v: Animated.Value, to: number) => Animated.timing(v, { toValue: to, duration: DURATION, easing: EASE, useNativeDriver: NATIVE }).start();
43
+ if (top == null) { go(o, 0); return; }
44
+ if (!seen.current) { y.setValue(top); o.setValue(1); seen.current = true; return; }
45
+ go(y, top); go(o, 1);
46
+ }, [top]); // eslint-disable-line react-hooks/exhaustive-deps
47
+ return { y, o };
48
+ }
49
+
50
+ /**
51
+ * INSET: the space around the buttons, the same at the ends as at the sides, so the end buttons sit concentric in the
52
+ * pill. GAP: the space between buttons — kept, not spread to fill a tall screen (less only where the screen is short).
53
+ */
54
+ const BTN = 40, MIN_BTN = 32, DIV = 11, INSET = 6, PAD = 2 * INSET, GAP = 5;
55
+ /**
56
+ * On a tablet the rail is bigger than on a computer (44 pt buttons, easy to hit with a finger, the same eighth of a
57
+ * button apart) and keeps its proportions as it sizes: dividers, padding and icons follow the buttons. It's sized for
58
+ * the room it has in landscape and stays that size in portrait, so it looks the same whichever way the tablet is held:
59
+ * where landscape is short the space between buttons tightens first, to a twelfth of a button at least, and then the
60
+ * whole rail scales down — 44 pt buttons on a 13" iPad Pro, 41 pt on an 11" iPad, 36 pt on an iPad mini. A tablet's
61
+ * rail never scrolls: TABLET_MIN_BTN is below what the smallest iPad needs.
62
+ */
63
+ const TABLET_BTN = 44, TABLET_MIN_BTN = 30;
64
+ const PER_BTN = { gapMax: 0.12, gapMin: 0.08, div: 0.275, pad: (2 * INSET) / BTN, icon: 0.6 };
65
+
66
+ export interface TabletRail { size: number; gap: number; div: number; pad: number; icon: number }
67
+
68
+ /**
69
+ * The room a tablet's rail is sized for: its room in landscape — what it has when held that way, and in portrait less
70
+ * the difference between the window's sides. From one layout (the window read with the room): the same either way the
71
+ * tablet is held, so the rail doesn't change as it's turned.
72
+ */
73
+ export function landscapeRoom(height: number, winW: number, winH: number): number {
74
+ return height - Math.max(0, winH - Math.min(winW, winH));
75
+ }
76
+
77
+ /** The tablet rail for `height` of room: 44 pt buttons an eighth apart, tighter gaps then smaller to fit. */
78
+ export function tabletRailLayout(height: number, buttons: number, dividers: number): TabletRail {
79
+ const spaces = buttons + dividers - 1;
80
+ // height per point of button size, before the gaps: the buttons, the dividers and the padding
81
+ const base = buttons + dividers * PER_BTN.div + PER_BTN.pad;
82
+ let size = TABLET_BTN, gap = PER_BTN.gapMax;
83
+ if (height) {
84
+ const room = height / TABLET_BTN - base; // what's left for gaps at full size, per point of button
85
+ if (room < spaces * PER_BTN.gapMax) {
86
+ gap = Math.max(PER_BTN.gapMin, room / spaces);
87
+ if (room < spaces * PER_BTN.gapMin) size = Math.floor(height / (base + spaces * gap));
88
+ }
89
+ }
90
+ size = Math.max(TABLET_MIN_BTN, Math.min(TABLET_BTN, size));
91
+ return {
92
+ size,
93
+ gap: Math.round(size * gap),
94
+ div: Math.round(size * PER_BTN.div),
95
+ pad: Math.round(size * PER_BTN.pad),
96
+ icon: Math.round(size * PER_BTN.icon),
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Buttons and the space between them for a rail `height` tall: on a computer GAP apart, closer on a short screen, and
102
+ * on a shorter one still the buttons shrink to fit before it has to scroll. Phones keep the compact rail, scrolling
103
+ * when it must.
104
+ */
105
+ export function railLayout(height: number, buttons: number, dividers: number, phone: boolean): { size: number; gap: number } {
106
+ if (phone || !height || buttons < 2) return { size: BTN, gap: 0 };
107
+ const natural = buttons * BTN + dividers * DIV + PAD;
108
+ if (height < natural) {
109
+ return { size: Math.max(MIN_BTN, Math.floor((height - dividers * DIV - PAD) / buttons)), gap: 0 };
110
+ }
111
+ return { size: BTN, gap: Math.min(GAP, Math.floor((height - natural) / (buttons + dividers - 1))) };
112
+ }
113
+
114
+ /**
115
+ * The rail's room on a tablet or a computer: from 30 under the top bar's buttons (their 58) to 14 above the bottom.
116
+ * The rail hangs from the top of it, whatever its tools and whichever way a tablet is held: its first button (Select)
117
+ * never moves, and a shorter rail just ends sooner.
118
+ */
119
+ const RAIL_TOP = 58 + 30, RAIL_BOTTOM = 14;
120
+
121
+ /** How long the rail takes to change from one tool set to another (switching between Draft and Annotate). */
122
+ const MORPH = 240, FADE_OUT = 130, FADE_IN_AT = 70;
123
+ /** Annotate's + sits this far under the rail, as wide as the rail is. */
124
+ const PLUS_GAP = 10;
125
+
126
+ /** A value that follows `to`, easing to each new one (set at once the first time), on the native driver. */
127
+ function useMorphed(to: number): Animated.Value {
128
+ const v = useRef(new Animated.Value(to)).current;
129
+ useEffect(() => { Animated.timing(v, { toValue: to, duration: MORPH, easing: EASE, useNativeDriver: NATIVE }).start(); }, [to, v]);
130
+ return v;
131
+ }
132
+
133
+ /**
134
+ * What one place on the rail shows, all as plain values: a divider (`id` null), or a tool, toggle or action. A place
135
+ * re-renders only when one of them changes — picking a tool redraws the two places whose selection changed, not the
136
+ * whole rail (each of which took its share of a phone's frame).
137
+ */
138
+ interface SlotProps {
139
+ y: number; appear: boolean; leaving?: boolean;
140
+ id: string | null; label: string; kind?: 'toggle' | 'act' | 'group'; tool: string; on: boolean;
141
+ size: number; icon: number; unit: number; div: number;
142
+ /** the markup colour's dot (the Color place) */
143
+ dot: string | null;
144
+ /** where a shared place's menu opens: beside it */
145
+ menuX: number; menuY: number;
146
+ onAddImage: () => void;
147
+ }
148
+
149
+ /**
150
+ * One place on the rail. As the tool set changes it glides to where it now sits; a place new to this tool set fades
151
+ * (and grows a touch) in, and one that's gone fades out — so switching modes, the rail morphs rather than redraws.
152
+ * It moves by a transform, on the native driver.
153
+ */
154
+ const RailSlot = memo(function RailSlot(p: SlotProps) {
155
+ const top = useMorphed(p.y), vis = useRef(new Animated.Value(p.appear ? 0 : 1)).current;
156
+ const scale = useMemo(() => vis.interpolate({ inputRange: [0, 1], outputRange: [0.88, 1] }), [vis]);
157
+ // what goes clears quickly, and what comes fades in just behind it, so the two never sit on top of each other
158
+ useEffect(() => {
159
+ // (fades ease in and out: a fast start reads as a blink on something appearing or going)
160
+ Animated.timing(vis, p.leaving ? { toValue: 0, duration: FADE_OUT, easing: TOGGLE_EASE, useNativeDriver: NATIVE }
161
+ : { toValue: 1, duration: MORPH - FADE_IN_AT, delay: p.appear ? FADE_IN_AT : 0, easing: TOGGLE_EASE, useNativeDriver: NATIVE }).start();
162
+ }, [p.leaving, vis]); // eslint-disable-line react-hooks/exhaustive-deps
163
+ return (
164
+ <Animated.View pointerEvents={p.leaving ? 'none' : 'box-none'} aria-hidden={p.leaving || undefined}
165
+ style={[s.slot, { opacity: vis, transform: [{ translateY: top }, { scale }] }]}>
166
+ {p.id == null ? <View style={[s.div, { width: 28 * p.unit, marginVertical: (p.div - 1) / 2 }]} /> : <RailEntry {...p} />}
167
+ </Animated.View>
168
+ );
169
+ });
170
+
171
+ /**
172
+ * A tool, toggle or action on the rail, and what a press does. A place several tools share draws with the one it shows
173
+ * on a tap, and opens the menu of them all on a long press; with a mouse (a browser on a computer) a click opens the
174
+ * menu instead — a mouse has no long press to find. A browser on a phone or an iPad keeps the touch behaviour.
175
+ */
176
+ const MENU_ON_CLICK = Platform.OS === 'web' && typeof matchMedia === 'function' && matchMedia('(hover: hover) and (pointer: fine)').matches;
177
+ function RailEntry({ id, label, kind, tool, on, size, icon, unit, dot, menuX, menuY, onAddImage, leaving }: SlotProps) {
178
+ const group = kind === 'group';
179
+ const press = () => {
180
+ if (group && MENU_ON_CLICK) {
181
+ // a second click closes it again
182
+ if (ui.shapeMenu?.group === id) { ui.shapeMenu = null; notify(); } else openMenu();
183
+ return;
184
+ }
185
+ ui.shapeMenu = null;
186
+ if (kind === 'toggle') TOGGLES[id!]?.flip();
187
+ else if (kind === 'act') {
188
+ if (id === 'undo') undo(); else if (id === 'redo') redo(); else if (id === 'image') onAddImage(); else if (id === 'color') openPalette();
189
+ } else setTool(tool);
190
+ // what it does, in a line under the tabs (for a tool or a toggle; not Undo, a colour, an action)
191
+ const t = kind === 'act' ? null : toolTip(tool, label);
192
+ if (t) ui.toolTip = { text: t, seq: (ui.toolTip?.seq ?? 0) + 1 };
193
+ notify();
194
+ };
195
+ // the menu of the place's tools, beside the rail level with it
196
+ const openMenu = () => {
197
+ setTool(tool);
198
+ ui.menu = false; ui.palette = false;
199
+ ui.shapeMenu = { x: menuX, y: menuY, group: id! };
200
+ notify();
201
+ };
202
+ return (
203
+ <RailButton id={id!} label={label} on={on} toggle={kind === 'toggle'} size={size} onPress={press} testID={leaving ? undefined : 'tool-' + id}
204
+ onLongPress={group ? openMenu : undefined} hint={group ? (MENU_ON_CLICK ? 'Opens the other shapes' : 'Touch and hold for the other shapes') : undefined}>
205
+ {id === 'color'
206
+ ? <View style={[s.dot, { width: 20 * unit, height: 20 * unit, borderRadius: 10 * unit, backgroundColor: dot ?? undefined }]} />
207
+ : <RailIcon name={railIcon(tool)} size={icon} on={on} />}
208
+ </RailButton>
209
+ );
210
+ }
211
+
212
+ /** A rail item's place: a tool keeps its place across tool sets when it's the same tool (Select / Select markup). */
213
+ const slotKey = (it: RailItem, divider: number): string => (it === '|' ? 'd' + divider : it[0] === 'aselect' ? 'select' : it[0]);
214
+ interface Placed { key: string; y: number; it: RailItem }
10
215
 
11
216
  /** The tool rail on the left: the current mode's tools, toggles and actions. */
12
217
  export function Rail({ phone, onAddImage }: { phone: boolean; onAddImage?: () => void }) {
13
- const items = RAIL[ed.mode].filter(it => it === '|' || it[0] !== 'image' || onAddImage);
218
+ // its tools (the mode's or the wall view's), the one in use, the toggles, the markup colour, the shape
219
+ useEditorKey(() => [ed.mode, elevOpen(), ed.tool, ed.drawConstr, ed.drawWalls, markupTarget().color, JSON.stringify(ui.picked), canLink(), linkPlusVisible()].join('|'));
220
+ // (shapes share one place: Annotate's always, Draft's on a phone, which has no room for each)
221
+ const usable = (list: RailItem[]) => groupTools(list.filter(it => it === '|' || it[0] !== 'image' || onAddImage), phone);
222
+ const items = usable(railItems());
223
+ // the room it has, as last laid out, and the window it was laid out in — read at that same moment. A tablet turned
224
+ // changes the window at once and the room a layout later: the two taken together (a portrait room in a landscape
225
+ // window) shrank the rail to nothing and grew it back as it turned.
226
+ const [room, setRoom] = useState({ height: 0, y: 0, winW: 0, winH: 0 });
227
+ const { height, y: railY } = room;
228
+ // the tool a shared place draws with: the one in use, or the last picked
229
+ const pickedIn = (g: string) => (TOOL_GROUPS[g].tools.includes(ed.tool) ? ed.tool : ui.picked[g]);
230
+ const tablet = Platform.OS !== 'web' && !phone;
231
+ // a tablet's rail keeps its proportions as it sizes; elsewhere the 40 pt rail closes up or shrinks to fit
232
+ // the rail's room in landscape: the same as now when held that way; in portrait, less by the difference between the sides
233
+ const landscapeHeight = landscapeRoom(height, room.winW, room.winH);
234
+ const layoutOf = (list: RailItem[], less = 0) => {
235
+ const dividers = list.filter(it => it === '|').length, buttons = list.length - dividers;
236
+ return tablet ? tabletRailLayout(landscapeHeight - less, buttons, dividers) : { ...railLayout(height - less, buttons, dividers, phone), div: DIV, pad: PAD, icon: 24 };
237
+ };
238
+ type Layout = ReturnType<typeof layoutOf>;
239
+ const tightest = (ls: Layout[]) => ls.reduce((a, b) => (b.size < a.size || (b.size === a.size && b.gap < a.gap) ? b : a));
240
+ const lengthOf = (list: RailItem[], l: Layout) => list.reduce((h, it) => h + (it === '|' ? l.div : l.size) + l.gap, 0) - l.gap + 2 * INSET * (l.size / BTN);
241
+ const pillWidth = (l: Layout) => l.size + 2 * INSET * (l.size / BTN);
242
+ // one spacing for every tool set — the one the longest rail (Draft's) can fit — so a shorter rail is just shorter
243
+ let L = tightest([items, usable(RAIL.draft), usable(RAIL.annotate)].map(l => layoutOf(l)));
244
+ // with a host that links, Annotate's rail leaves room under it for the + — or, where even a smaller rail wouldn't
245
+ // (a short window), the + floats in the corner instead
246
+ let plusUnder = false;
247
+ if (!phone && canLink() && height > 0) {
248
+ const annotate = usable(RAIL.annotate), withPlus = tightest([L, layoutOf(annotate, PLUS_GAP + pillWidth(L))]);
249
+ if (lengthOf(annotate, withPlus) + PLUS_GAP + pillWidth(withPlus) <= height + 0.5) { L = withPlus; plusUnder = true; }
250
+ }
251
+ const fab = !phone && canLink() && height > 0 && !plusUnder;
252
+ useEffect(() => { if (ui.linkFab !== fab) { ui.linkFab = fab; notify(); } }, [fab]);
253
+ const { size, gap, icon } = L, unit = size / BTN;
254
+ // where each item sits: the rail's top padding, then each button or divider above it and the gap after it
255
+ // (the space above the first button is the space beside it: the pill's ends are concentric with the end buttons)
256
+ const inset = INSET * unit;
257
+ let y = inset, dn = 0;
258
+ const placed: Placed[] = items.map(it => { const p = { key: slotKey(it, dn), y, it }; if (it === '|') dn++; y += (it === '|' ? L.div : size) + gap; return p; });
259
+ const total = y - gap + inset, width = size + 2 * inset, radius = size / 2 + inset;
260
+ // what was on the rail and isn't now fades out where it was — worked out in this same render, so it never drops
261
+ // out for a frame before fading (that read as a flicker)
262
+ const prev = useRef<Placed[]>(placed), leaving = useRef<{ items: Placed[]; seq: number }>({ items: [], seq: 0 }), mounted = useRef(false);
263
+ const keys = placed.map(p => p.key).join(), [, rerender] = useState(0);
264
+ if (keys !== prev.current.map(p => p.key).join()) {
265
+ const now = new Set(placed.map(p => p.key));
266
+ const gone = [...prev.current.filter(p => !now.has(p.key)), ...leaving.current.items.filter(p => !now.has(p.key))];
267
+ leaving.current = { items: gone.filter((p, i) => gone.findIndex(q => q.key === p.key) === i), seq: leaving.current.seq + 1 };
268
+ prev.current = placed;
269
+ }
270
+ const seq = leaving.current.seq;
271
+ useEffect(() => {
272
+ if (!leaving.current.items.length) return;
273
+ const t = setTimeout(() => { if (leaving.current.seq === seq) { leaving.current = { items: [], seq }; rerender(n => n + 1); } }, MORPH + 20);
274
+ return () => clearTimeout(t);
275
+ }, [seq]);
276
+ // (what's on the rail when it's first shown is just there; what comes later fades in)
277
+ useEffect(() => { if (height) mounted.current = true; }, [height]);
278
+ const isOn = (id: string) => id === ed.tool || !!TOOL_GROUPS[id]?.tools.includes(ed.tool);
279
+ const sel = placed.find(p => p.it !== '|' && (!p.it[3] || p.it[3] === 'group') && isOn(p.it[0]));
280
+ const glide = useGlide(sel ? sel.y : null);
281
+ // (one function for every render, so the places' props stay equal: the host's own may be new each time)
282
+ const addImage = useRef(onAddImage);
283
+ addImage.current = onAddImage;
284
+ const onAdd = useCallback(() => addImage.current?.(), []);
285
+ /** A place's props: plain values only (see SlotProps). */
286
+ const slot = (p: Placed, isLeaving: boolean): SlotProps => {
287
+ const common = { y: p.y, appear: !isLeaving && mounted.current, leaving: isLeaving, size, icon, unit, div: L.div, onAddImage: onAdd };
288
+ if (p.it === '|') return { ...common, id: null, label: '', tool: '', on: false, dot: null, menuX: 0, menuY: 0 };
289
+ const [id, rawLabel, , kind] = p.it, group = kind === 'group';
290
+ // a shared place stands for the tool it'll draw with
291
+ const tool = group ? pickedIn(id) : id;
292
+ return {
293
+ ...common, id, kind, tool, label: group ? toolLabel(tool) : rawLabel,
294
+ on: !isLeaving && (kind === 'toggle' ? !!TOGGLES[id]?.on() : isOn(id)),
295
+ dot: id === 'color' ? markupTarget().color : null,
296
+ // (the rail's own place, plus where this sits on it)
297
+ menuX: group ? 12 + width + 10 : 0, menuY: group ? railY + p.y : 0,
298
+ };
299
+ };
14
300
  return (
15
- <View style={[s.rail, phone && { top: 162 }]}>
16
- <ScrollView contentContainerStyle={s.inner} showsVerticalScrollIndicator={false}>
17
- {items.map((it, i) => {
18
- if (it === '|') return <View key={'d' + i} style={s.div} />;
19
- const [id, label, , kind] = it;
20
- const on = id === ed.tool || (id === 'constr' && ed.drawConstr);
21
- const press = () => {
22
- if (kind === 'toggle') { toggleConstr(); toast(ed.drawConstr ? 'New lines are construction lines — guides that never make spaces.' : 'New lines are regular lines.'); }
23
- else if (kind === 'act') {
24
- if (id === 'undo') undo(); else if (id === 'redo') redo(); else if (id === 'image') onAddImage?.(); else if (id === 'color') openPalette();
25
- } else setTool(id);
26
- };
27
- return (
28
- <Pressable key={id} accessibilityRole="button" accessibilityLabel={label} accessibilityState={{ selected: on }} testID={'tool-' + id} onPress={press}
29
- style={st => [s.btn, on ? { backgroundColor: kind === 'toggle' ? C.lblue : '#fff' } : hovered(st) && { backgroundColor: 'rgba(255,255,255,0.08)' }]}>
30
- {id === 'color' ? <View style={[s.dot, { backgroundColor: ed.noteColor }]} /> : <Icon name={id} color={on ? C.dark : '#fff'} />}
31
- </Pressable>
32
- );
33
- })}
34
- </ScrollView>
301
+ // (on a phone the bar runs under it, so it stops above; elsewhere the bar is centred and the rail runs to the bottom)
302
+ <View style={[s.rail, phone && { top: 162, bottom: 84 }]} onLayout={e => {
303
+ const { height: h, y: top } = e.nativeEvent.layout, w = Dimensions.get('window');
304
+ setRoom({ height: h, y: top, winW: w.width, winH: w.height });
305
+ }}>
306
+ {/* the pill eases to its new length as the tool set changes (on the UI thread on a device: see TweenView). It's
307
+ shown once its room is known, at the size that room gives it: no settling as it opens. */}
308
+ {height > 0 && <TweenView testID="railPill" to={{ height: total }} duration={MORPH} style={[s.inner, { width, maxHeight: '100%', borderRadius: radius }]}>
309
+ {/* it scrolls only when its tools can't all fit (a phone): otherwise a drag down the rail would move it */}
310
+ <ScrollView style={{ flex: 1, borderRadius: radius }} contentContainerStyle={{ height: total }} showsVerticalScrollIndicator={false}
311
+ scrollEnabled={!!height && total > height + 1} bounces={false}>
312
+ {sel && <Animated.View pointerEvents="none" style={[s.disc, { left: inset, width: size, height: size, borderRadius: size / 2, opacity: glide.o, transform: [{ translateY: glide.y }] }]} />}
313
+ {leaving.current.items.map(p => <RailSlot key={'gone-' + p.key} {...slot(p, true)} />)}
314
+ {placed.map(p => <RailSlot key={p.key} {...slot(p, false)} />)}
315
+ </ScrollView>
316
+ </TweenView>}
317
+ {height > 0 && plusUnder && <RailPlus top={Math.min(total, height) + PLUS_GAP} size={width} shown={linkPlusVisible()} />}
35
318
  </View>
36
319
  );
37
320
  }
38
321
 
39
- const s = StyleSheet.create({
40
- rail: { position: 'absolute', left: 12, top: 112, bottom: 84, zIndex: 22, alignItems: 'flex-start' },
41
- inner: { backgroundColor: C.g2, borderRadius: 30, paddingVertical: 8, paddingHorizontal: 6, alignItems: 'center', ...shadow },
322
+ /**
323
+ * Annotate's +, under the rail: picks the job's files and measurements to put on the plan. It follows the rail's end as
324
+ * the rail changes length, and fades in and out with Annotate's tools (all on the native driver).
325
+ */
326
+ const RailPlus = memo(function RailPlus({ top, size, shown }: { top: number; size: number; shown: boolean }) {
327
+ const y = useMorphed(top), vis = useRef(new Animated.Value(shown ? 1 : 0)).current;
328
+ useEffect(() => {
329
+ Animated.timing(vis, shown ? { toValue: 1, duration: MORPH - FADE_IN_AT, delay: FADE_IN_AT, easing: TOGGLE_EASE, useNativeDriver: NATIVE }
330
+ : { toValue: 0, duration: FADE_OUT, easing: TOGGLE_EASE, useNativeDriver: NATIVE }).start();
331
+ }, [shown, vis]);
332
+ return (
333
+ <Animated.View pointerEvents={shown ? 'box-none' : 'none'} aria-hidden={!shown || undefined} style={[s.plusAt, { opacity: vis, transform: [{ translateY: y }] }]}>
334
+ <Tap accessibilityRole="button" accessibilityLabel={LINK_PLUS_LABEL} testID={shown ? 'linkPlus' : undefined} onPress={() => { void addLinks(); }}
335
+ style={[s.plus, { width: size, height: size, borderRadius: size / 2 }]}>
336
+ <Icon name="plus" size={Math.round(size * 0.5)} color={C.dark} />
337
+ </Tap>
338
+ </Animated.View>
339
+ );
340
+ });
341
+
342
+ /**
343
+ * A rail button: a tool sits on the gliding disc when selected; a toggle fades its own blue in and out. One that opens
344
+ * a menu on a long press has a small nub on its right edge, pointing where the menu opens (as in BOLDR's annotation rail).
345
+ */
346
+ function RailButton({ id, label, on, toggle, size, onPress, onLongPress, hint, children, testID }: {
347
+ id: string; label: string; on: boolean; toggle: boolean; size: number; onPress: () => void; onLongPress?: () => void; hint?: string;
348
+ children: React.ReactNode; testID?: string;
349
+ }) {
350
+ const t = useEased(on && toggle);
351
+ const fill = useMemo(() => t.interpolate({ inputRange: [0, 1], outputRange: ['rgba(80,166,255,0)', C.lblue] }), [t]);
352
+ return (
353
+ <Tap accessibilityRole="button" accessibilityLabel={label} accessibilityHint={hint} accessibilityState={{ selected: on }} aria-pressed={toggle ? on : undefined} testID={testID}
354
+ onPress={onPress} onLongPress={onLongPress}
355
+ style={st => [s.btn, { width: size, height: size, borderRadius: size / 2 },
356
+ toggle && { backgroundColor: fill as unknown as string },
357
+ !on && hovered(st) && { backgroundColor: 'rgba(255,255,255,0.08)' }]}>
358
+ {children}
359
+ {onLongPress && <View pointerEvents="none" style={[s.nub, { top: size / 2 - 4 }, on && { borderLeftColor: C.dark }]} />}
360
+ </Tap>
361
+ );
362
+ }
363
+
364
+ const s = createSheet({
365
+ // (both moved by a transform from the top: see useGlide, RailSlot)
366
+ disc: { position: 'absolute', top: 0, backgroundColor: '#fff' },
367
+ rail: { position: 'absolute', left: 12, top: RAIL_TOP, bottom: RAIL_BOTTOM, zIndex: 22, alignItems: 'flex-start' },
368
+ inner: { backgroundColor: C.g2, ...shadow },
369
+ slot: { position: 'absolute', top: 0, left: 0, right: 0, alignItems: 'center' },
42
370
  btn: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center' },
43
371
  div: { width: 28, height: 1, backgroundColor: C.g3, marginVertical: 5 },
372
+ // a right-pointing triangle, 8 tall, hugging the right edge
373
+ nub: { position: 'absolute', right: 3, width: 0, height: 0, borderTopWidth: 4, borderBottomWidth: 4, borderLeftWidth: 5, borderTopColor: 'transparent', borderBottomColor: 'transparent', borderLeftColor: 'rgba(255,255,255,0.75)' },
44
374
  dot: { width: 20, height: 20, borderRadius: 10, borderWidth: 2, borderColor: 'rgba(255,255,255,0.85)' },
375
+ // (moved by a transform from the rail's top: see RailPlus)
376
+ plusAt: { position: 'absolute', top: 0, left: 0 },
377
+ plus: { backgroundColor: C.yellow, alignItems: 'center', justifyContent: 'center', ...shadow },
45
378
  });
@@ -0,0 +1,66 @@
1
+ import { useRef } from 'react';
2
+ import { Animated, Text, useWindowDimensions, View } from 'react-native';
3
+ import { ed } from '../app/state';
4
+ import { railIcon, TOOL_GROUPS, toolLabel } from '../editor/mode';
5
+ import { toolTip } from '../editor/tooltips';
6
+ import { setTool } from '../tools/registry';
7
+ import { Icon } from './Icon';
8
+ import { notify, ui, useEditorKey } from './store';
9
+ import { C, FONT, shadow } from './theme';
10
+ import { Tap, usePopup } from './motion';
11
+ import { createSheet } from './corners';
12
+
13
+ const ROW = 44, GAP = 6, PAD = 8;
14
+ /** A menu's height for `n` rows: the rows, the gaps between them, its padding and border. */
15
+ const heightFor = (n: number) => n * ROW + (n - 1) * GAP + 2 * PAD + 2;
16
+
17
+ /**
18
+ * The menu of the tools sharing one place on the rail (a long press on it; a click in a browser) — Draft's shapes on a
19
+ * phone, Annotate's markup shapes: each by icon and name, the one in use on a blue disc, as BOLDR's annotation rail lays out its sub-menus. Picking one draws with it from then on, and the rail's
20
+ * place shows it. It sits beside the rail, level with the place; there's no scrim, and a tap on the drawing, the rail
21
+ * or the top bar closes it.
22
+ */
23
+ export function ShapeMenu() {
24
+ useEditorKey(() => (ui.shapeMenu ? [ui.shapeMenu.x, ui.shapeMenu.y, ui.shapeMenu.group, ed.mode, ed.tool].join('|') : ''));
25
+ const { height } = useWindowDimensions(), now = ui.shapeMenu, nowGroup = now ? TOOL_GROUPS[now.group] : null;
26
+ const open = !!now && !!nowGroup && ed.mode === nowGroup.mode;
27
+ // (it grows out of the rail's place, sideways; as it goes it keeps what it showed)
28
+ const last = useRef(now), pop = usePopup(open, { x: -8 }, 'left center');
29
+ if (open) last.current = now;
30
+ const at = last.current, group = at ? TOOL_GROUPS[at.group] : null;
31
+ if (!pop.present || !at || !group) return null;
32
+ // (lifted to stay on screen when the place sits low)
33
+ const top = Math.max(12, Math.min(at.y - PAD, height - heightFor(group.tools.length) - 96));
34
+ const pick = (id: string) => {
35
+ ui.picked[at.group] = id;
36
+ ui.shapeMenu = null;
37
+ setTool(id);
38
+ const t = toolTip(id, toolLabel(id));
39
+ if (t) ui.toolTip = { text: t, seq: (ui.toolTip?.seq ?? 0) + 1 };
40
+ notify();
41
+ };
42
+ return (
43
+ <Animated.View style={[s.menu, { left: at.x, top }, pop.style]} pointerEvents={open ? 'auto' : 'none'} accessibilityRole="menu" accessibilityLabel={group.label} testID={open ? 'shapeMenu' : undefined}>
44
+ {group.tools.map(id => {
45
+ const on = ed.tool === id;
46
+ return (
47
+ <Tap key={id} feel="dim" accessibilityRole="menuitem" accessibilityLabel={toolLabel(id)} accessibilityState={{ selected: on }}
48
+ onPress={() => pick(id)} style={s.row} testID={'shape-' + id}>
49
+ <View style={[s.disc, on && { backgroundColor: C.blue }]}><Icon name={railIcon(id)} color="#fff" /></View>
50
+ <Text style={s.label}>{toolLabel(id)}</Text>
51
+ </Tap>
52
+ );
53
+ })}
54
+ </Animated.View>
55
+ );
56
+ }
57
+
58
+ const s = createSheet({
59
+ menu: {
60
+ position: 'absolute', zIndex: 46, backgroundColor: C.g2, borderColor: C.g3, borderWidth: 1, borderRadius: 30,
61
+ padding: PAD, gap: GAP, ...shadow,
62
+ },
63
+ row: { flexDirection: 'row', alignItems: 'center', gap: 12, height: ROW, paddingRight: 24, borderRadius: ROW / 2 },
64
+ disc: { width: ROW, height: ROW, borderRadius: ROW / 2, alignItems: 'center', justifyContent: 'center' },
65
+ label: { fontFamily: FONT, fontSize: 16, fontWeight: '500', color: '#fff' },
66
+ });
@@ -1,12 +1,51 @@
1
- import type { ReactNode } from 'react';
2
- import { Pressable, StyleSheet, View } from 'react-native';
1
+ import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
2
+ import { Animated, Easing, Pressable, StyleSheet, View, type ViewStyle } from 'react-native';
3
+ import { EASE, NATIVE } from './easing';
4
+ import { POP_EASE_OUT, POP_IN, POP_OUT, reducedMotion } from './motion';
3
5
  import { ui } from './store';
4
6
  import { C } from './theme';
7
+ import { createSheet } from './corners';
5
8
 
6
9
  /** Taps on the backdrop right after a sheet opens are the opening tap's ghost click (touch screens): ignore them. */
7
10
  const GHOST_MS = 450;
8
11
 
9
- /** Dims the drawing behind a sheet on phones; a tap outside closes the sheet. */
12
+ /**
13
+ * How a sheet (the keypad, a conflict) comes and goes. On a tablet or a computer it's a card in the corner, and comes
14
+ * and goes as the popup menus do (motion.tsx usePopup): fading in and growing from just under its size out of its
15
+ * corner, rising a few points, in about 140 ms; fading and shrinking back, gathering pace, in about 100. On a phone it
16
+ * rises from the bottom as it fades in. A transform and opacity, on the native driver; with Reduce Motion on, it only
17
+ * fades. `present`: on screen — shown, or still going (it keeps what it showed till then).
18
+ */
19
+ const SHEET_IN = 200, SHEET_OUT = 160, SHEET_EASE_OUT = Easing.bezier(0.4, 0, 1, 1);
20
+ /** how far it rises on a phone, and a card on a wide screen (as a popup does) */
21
+ const RISE_BY = 80, CARD_RISE = 8;
22
+ export function useSheetMotion(shown: boolean, wide: boolean): { present: boolean; motion: Animated.WithAnimatedObject<Pick<ViewStyle, 'transform' | 'opacity'>> } {
23
+ const x = useRef(new Animated.Value(1)).current, [present, setPresent] = useState(shown);
24
+ const motion = useMemo(() => {
25
+ const opacity = x.interpolate({ inputRange: [0, 1], outputRange: [1, 0] });
26
+ if (reducedMotion()) return { opacity };
27
+ return (wide
28
+ ? {
29
+ opacity, transformOrigin: 'bottom right',
30
+ transform: [{ translateY: x.interpolate({ inputRange: [0, 1], outputRange: [0, CARD_RISE] }) }, { scale: x.interpolate({ inputRange: [0, 1], outputRange: [1, 0.96] }) }],
31
+ }
32
+ : { opacity, transform: [{ translateY: x.interpolate({ inputRange: [0, 1], outputRange: [0, RISE_BY] }) }] }) as Animated.WithAnimatedObject<Pick<ViewStyle, 'transform' | 'opacity'>>;
33
+ }, [wide, x]);
34
+ useEffect(() => {
35
+ if (shown) setPresent(true);
36
+ // (on the next frame: see Drawer — strict mode's second mount would otherwise stop it)
37
+ const frame = requestAnimationFrame(() => {
38
+ Animated.timing(x, shown
39
+ ? { toValue: 0, duration: wide ? POP_IN : SHEET_IN, easing: EASE, useNativeDriver: NATIVE }
40
+ : { toValue: 1, duration: wide ? POP_OUT : SHEET_OUT, easing: wide ? POP_EASE_OUT : SHEET_EASE_OUT, useNativeDriver: NATIVE })
41
+ .start(({ finished }) => { if (finished && !shown) setPresent(false); });
42
+ });
43
+ return () => cancelAnimationFrame(frame);
44
+ }, [shown, wide, x]);
45
+ return { present: shown || present, motion };
46
+ }
47
+
48
+ /** Dims the drawing behind a sheet on a phone; a tap outside closes the sheet. (Elsewhere the drawing stays in reach.) */
10
49
  export function Backdrop({ wide, onClose }: { wide: boolean; onClose: () => void }) {
11
50
  return (
12
51
  <Pressable accessibilityLabel="Close" testID="backdrop" style={[StyleSheet.absoluteFill, s.backdrop, wide && { backgroundColor: 'transparent' }]}
@@ -14,20 +53,26 @@ export function Backdrop({ wide, onClose }: { wide: boolean; onClose: () => void
14
53
  );
15
54
  }
16
55
 
17
- /** A sheet: a card in the bottom-right corner on wide screens, a bottom sheet on phones. */
18
- export function Sheet({ wide, children, label, testID }: { wide: boolean; children: ReactNode; label: string; testID?: string }) {
56
+ /**
57
+ * A sheet: a card in the bottom-right corner on wide screens, a bottom sheet on phones. `motion`: a transform (and
58
+ * opacity) it's moved by as it comes and goes; `live` false while it's going (nothing in it can be pressed).
59
+ */
60
+ export function Sheet({ wide, children, label, testID, motion, live = true }: {
61
+ wide: boolean; children: ReactNode; label: string; testID?: string;
62
+ motion?: Animated.WithAnimatedObject<Pick<ViewStyle, 'transform' | 'opacity'>>; live?: boolean;
63
+ }) {
19
64
  return (
20
- <View accessibilityLabel={label} testID={testID} style={[s.sheet, wide ? s.card : s.bottom]}>
65
+ <Animated.View accessibilityLabel={label} testID={testID} pointerEvents={live ? 'auto' : 'none'} style={[s.sheet, wide ? s.card : s.bottom, motion]}>
21
66
  {!wide && <View style={s.handle} />}
22
67
  {children}
23
- </View>
68
+ </Animated.View>
24
69
  );
25
70
  }
26
71
 
27
- const s = StyleSheet.create({
72
+ const s = createSheet({
28
73
  backdrop: { backgroundColor: 'rgba(0,0,0,0.35)', zIndex: 55 },
29
- sheet: { position: 'absolute', backgroundColor: C.g1, paddingTop: 12, paddingHorizontal: 18, paddingBottom: 18, zIndex: 60, maxHeight: '88%' },
30
- card: { right: 16, bottom: 16, width: 380, borderRadius: 20 },
31
- bottom: { left: 0, right: 0, bottom: 0, borderTopLeftRadius: 20, borderTopRightRadius: 20 },
74
+ sheet: { position: 'absolute', backgroundColor: C.g1, borderColor: C.g3, borderWidth: 1, paddingTop: 14, paddingHorizontal: 16, paddingBottom: 16, zIndex: 60, maxHeight: '88%', boxShadow: '0px 5px 15px rgba(0, 0, 0, 0.75)' },
75
+ card: { right: 16, bottom: 16, width: 360, borderRadius: 20, paddingTop: 16 },
76
+ bottom: { left: 0, right: 0, bottom: 0, borderTopLeftRadius: 20, borderTopRightRadius: 20, borderBottomWidth: 0 },
32
77
  handle: { width: 50, height: 5, borderRadius: 3, backgroundColor: C.g4, alignSelf: 'center', marginBottom: 14 },
33
78
  });
@@ -0,0 +1,48 @@
1
+ import { useRef, useState } from 'react';
2
+ import { View, type GestureResponderEvent, type LayoutChangeEvent } from 'react-native';
3
+ import { C } from './theme';
4
+ import { createSheet } from './corners';
5
+
6
+ /**
7
+ * A horizontal slider: drag (or tap) along the track; the screen reader's adjust gestures step it. `onStart` comes once
8
+ * as a drag begins (an undo step), `onChange` as the value moves, `onEnd` as it's let go. Values snap to `step`
9
+ * between `min` and `max`.
10
+ */
11
+ export function Slider({ value, min, max, step, onChange, onStart, onEnd, label, testID, format = v => v + '%' }: {
12
+ value: number; min: number; max: number; step: number; onChange: (v: number) => void; onStart?: () => void; onEnd?: () => void; label: string; testID?: string;
13
+ /** the value as a screen reader says it (percent unless given) */
14
+ format?: (v: number) => string;
15
+ }) {
16
+ const width = useRef(0), [, setW] = useState(0);
17
+ const snap = (v: number) => Math.min(max, Math.max(min, Math.round(v / step) * step));
18
+ // the thumb and fill take no touches, so a touch's x is always measured from the track's left edge
19
+ const at = (e: GestureResponderEvent) => snap(min + (e.nativeEvent.locationX / (width.current || 1)) * (max - min));
20
+ const t = (value - min) / (max - min);
21
+ return (
22
+ <View
23
+ accessibilityRole="adjustable" accessibilityLabel={label} testID={testID}
24
+ accessibilityValue={{ min, max, now: value, text: format(value) }}
25
+ onAccessibilityAction={e => { onChange(snap(value + (e.nativeEvent.actionName === 'increment' ? step : -step))); onEnd?.(); }}
26
+ accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
27
+ onLayout={(e: LayoutChangeEvent) => { width.current = e.nativeEvent.layout.width; setW(width.current); }}
28
+ onStartShouldSetResponder={() => true} onMoveShouldSetResponder={() => true} onResponderTerminationRequest={() => false}
29
+ onResponderGrant={e => { onStart?.(); onChange(at(e)); }}
30
+ onResponderMove={e => onChange(at(e))}
31
+ onResponderRelease={() => onEnd?.()} onResponderTerminate={() => onEnd?.()}
32
+ style={s.hit}>
33
+ <View style={s.track} pointerEvents="none">
34
+ <View style={[s.fill, { width: `${t * 100}%` }]} />
35
+ </View>
36
+ <View pointerEvents="none" style={[s.thumb, { left: `${t * 100}%` }]} />
37
+ </View>
38
+ );
39
+ }
40
+
41
+ const THUMB = 22;
42
+ const s = createSheet({
43
+ // a tall hit area around a thin track
44
+ hit: { height: 36, justifyContent: 'center', marginHorizontal: THUMB / 2 },
45
+ track: { height: 4, borderRadius: 2, backgroundColor: C.g3, overflow: 'hidden' },
46
+ fill: { height: 4, backgroundColor: '#fff' },
47
+ thumb: { position: 'absolute', top: (36 - THUMB) / 2, width: THUMB, height: THUMB, marginLeft: -THUMB / 2, borderRadius: THUMB / 2, backgroundColor: '#fff', boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.3)' },
48
+ });