@krizaka/ui 3.3.0 → 3.5.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 (477) hide show
  1. package/dist/activity-feed.d.ts +199 -0
  2. package/dist/activity-feed.js +155 -0
  3. package/dist/activity-feed.js.map +1 -0
  4. package/dist/chat.d.ts +187 -0
  5. package/dist/chat.js +132 -0
  6. package/dist/chat.js.map +1 -0
  7. package/dist/chunk-7Z4KHPFC.js +42 -0
  8. package/dist/chunk-7Z4KHPFC.js.map +1 -0
  9. package/dist/chunk-ECSCA3CN.js +128 -0
  10. package/dist/chunk-ECSCA3CN.js.map +1 -0
  11. package/dist/chunk-F2GKFIYK.js +47 -0
  12. package/dist/chunk-F2GKFIYK.js.map +1 -0
  13. package/dist/data-table.js +84 -231
  14. package/dist/data-table.js.map +1 -1
  15. package/dist/description-list.d.ts +105 -0
  16. package/dist/description-list.js +49 -0
  17. package/dist/description-list.js.map +1 -0
  18. package/dist/index.js +13 -66
  19. package/dist/index.js.map +1 -1
  20. package/dist/kanban.d.ts +132 -0
  21. package/dist/kanban.js +187 -0
  22. package/dist/kanban.js.map +1 -0
  23. package/dist/list.d.ts +153 -0
  24. package/dist/list.js +130 -0
  25. package/dist/list.js.map +1 -0
  26. package/dist/map.d.ts +463 -0
  27. package/dist/map.js +952 -0
  28. package/dist/map.js.map +1 -0
  29. package/dist/match-dialog.d.ts +30 -0
  30. package/dist/match-dialog.js +146 -0
  31. package/dist/match-dialog.js.map +1 -0
  32. package/dist/motion.css +12 -1
  33. package/dist/native/map.d.ts +200 -0
  34. package/dist/native/map.js +673 -0
  35. package/dist/native/map.js.map +1 -0
  36. package/dist/native/media-picker.d.ts +58 -0
  37. package/dist/native/media-picker.js +63 -0
  38. package/dist/native/media-picker.js.map +1 -0
  39. package/dist/native.d.ts +952 -75
  40. package/dist/native.js +2572 -889
  41. package/dist/native.js.map +1 -1
  42. package/dist/photo-grid.d.ts +122 -0
  43. package/dist/photo-grid.js +94 -0
  44. package/dist/photo-grid.js.map +1 -0
  45. package/dist/stat.d.ts +31 -2
  46. package/dist/stat.js +21 -4
  47. package/dist/stat.js.map +1 -1
  48. package/dist/step-progress.d.ts +20 -0
  49. package/dist/step-progress.js +17 -0
  50. package/dist/step-progress.js.map +1 -0
  51. package/dist/swipe-deck.d.ts +183 -0
  52. package/dist/swipe-deck.js +144 -0
  53. package/dist/swipe-deck.js.map +1 -0
  54. package/dist/tag-selector.d.ts +26 -0
  55. package/dist/tag-selector.js +21 -0
  56. package/dist/tag-selector.js.map +1 -0
  57. package/dist/tree-view.d.ts +111 -0
  58. package/dist/tree-view.js +154 -0
  59. package/dist/tree-view.js.map +1 -0
  60. package/dist/verified-badge.d.ts +16 -0
  61. package/dist/verified-badge.js +23 -0
  62. package/dist/verified-badge.js.map +1 -0
  63. package/package.json +245 -10
  64. package/registry/achievement.json +1 -1
  65. package/registry/action-bar.json +1 -1
  66. package/registry/action-sheet.json +121 -0
  67. package/registry/activity-feed.json +468 -0
  68. package/registry/alert.json +1 -1
  69. package/registry/app-shell.json +1 -1
  70. package/registry/argument-card.json +1 -1
  71. package/registry/audio-player.json +1 -1
  72. package/registry/auth-layout.json +1 -1
  73. package/registry/avatar.json +58 -3
  74. package/registry/badge.json +1 -1
  75. package/registry/button.json +1 -1
  76. package/registry/card-grid.json +1 -1
  77. package/registry/card.json +1 -1
  78. package/registry/catalogue-page.json +1 -1
  79. package/registry/changelog-page.json +1 -1
  80. package/registry/chat.json +917 -0
  81. package/registry/checkbox.json +1 -1
  82. package/registry/chip-link.json +1 -1
  83. package/registry/chip.json +1 -1
  84. package/registry/choice-grid.json +1 -1
  85. package/registry/code-block.json +1 -1
  86. package/registry/combobox.json +1 -1
  87. package/registry/command.json +1 -1
  88. package/registry/component-page.json +1 -1
  89. package/registry/confirm-button.json +1 -1
  90. package/registry/contact-page.json +1 -1
  91. package/registry/continue-prompt.json +1 -1
  92. package/registry/countdown.json +1 -1
  93. package/registry/cta-panel.json +1 -1
  94. package/registry/dashboard-layout.json +1 -1
  95. package/registry/data-table.json +1 -1
  96. package/registry/date-picker.json +2 -2
  97. package/registry/decision-list.json +1 -1
  98. package/registry/description-list.json +232 -0
  99. package/registry/detail-layout.json +1 -1
  100. package/registry/dialog.json +1 -1
  101. package/registry/docs-article-page.json +1 -1
  102. package/registry/docs-hub-page.json +1 -1
  103. package/registry/docs-layout.json +1 -1
  104. package/registry/dropdown-menu.json +1 -1
  105. package/registry/empty-state.json +1 -1
  106. package/registry/error-page.json +1 -1
  107. package/registry/examples/action-sheet/native/video-actions.d.ts +4 -0
  108. package/registry/examples/action-sheet/native/video-actions.dark.png +0 -0
  109. package/registry/examples/action-sheet/native/video-actions.js +30 -0
  110. package/registry/examples/action-sheet/native/video-actions.light.png +0 -0
  111. package/registry/examples/action-sheet/native/video-actions.tsx +24 -0
  112. package/registry/examples/activity-feed/job-activity.d.ts +4 -0
  113. package/registry/examples/activity-feed/job-activity.js +21 -0
  114. package/registry/examples/activity-feed/job-activity.tsx +17 -0
  115. package/registry/examples/activity-feed/live.d.ts +4 -0
  116. package/registry/examples/activity-feed/live.js +23 -0
  117. package/registry/examples/activity-feed/live.tsx +24 -0
  118. package/registry/examples/activity-feed/native/notifications.d.ts +4 -0
  119. package/registry/examples/activity-feed/native/notifications.dark.png +0 -0
  120. package/registry/examples/activity-feed/native/notifications.js +18 -0
  121. package/registry/examples/activity-feed/native/notifications.light.png +0 -0
  122. package/registry/examples/activity-feed/native/notifications.tsx +18 -0
  123. package/registry/examples/activity-feed/timeline.d.ts +4 -0
  124. package/registry/examples/activity-feed/timeline.js +25 -0
  125. package/registry/examples/activity-feed/timeline.tsx +19 -0
  126. package/registry/examples/avatar/native/presence.d.ts +4 -0
  127. package/registry/examples/avatar/native/presence.dark.png +0 -0
  128. package/registry/examples/avatar/native/presence.js +18 -0
  129. package/registry/examples/avatar/native/presence.light.png +0 -0
  130. package/registry/examples/avatar/native/presence.tsx +15 -0
  131. package/registry/examples/chat/conversation.d.ts +4 -0
  132. package/registry/examples/chat/conversation.js +22 -0
  133. package/registry/examples/chat/conversation.tsx +21 -0
  134. package/registry/examples/chat/inbox.d.ts +4 -0
  135. package/registry/examples/chat/inbox.js +15 -0
  136. package/registry/examples/chat/inbox.tsx +12 -0
  137. package/registry/examples/chat/native/conversation.d.ts +4 -0
  138. package/registry/examples/chat/native/conversation.dark.png +0 -0
  139. package/registry/examples/chat/native/conversation.js +23 -0
  140. package/registry/examples/chat/native/conversation.light.png +0 -0
  141. package/registry/examples/chat/native/conversation.tsx +20 -0
  142. package/registry/examples/chat/native/inbox.d.ts +4 -0
  143. package/registry/examples/chat/native/inbox.dark.png +0 -0
  144. package/registry/examples/chat/native/inbox.js +16 -0
  145. package/registry/examples/chat/native/inbox.light.png +0 -0
  146. package/registry/examples/chat/native/inbox.tsx +13 -0
  147. package/registry/examples/description-list/job-details.d.ts +4 -0
  148. package/registry/examples/description-list/job-details.js +23 -0
  149. package/registry/examples/description-list/job-details.tsx +17 -0
  150. package/registry/examples/description-list/key-value-grid.d.ts +4 -0
  151. package/registry/examples/description-list/key-value-grid.js +22 -0
  152. package/registry/examples/description-list/key-value-grid.tsx +16 -0
  153. package/registry/examples/description-list/native/job-details.d.ts +4 -0
  154. package/registry/examples/description-list/native/job-details.dark.png +0 -0
  155. package/registry/examples/description-list/native/job-details.js +33 -0
  156. package/registry/examples/description-list/native/job-details.light.png +0 -0
  157. package/registry/examples/description-list/native/job-details.tsx +24 -0
  158. package/registry/examples/description-list/receipt.d.ts +4 -0
  159. package/registry/examples/description-list/receipt.js +23 -0
  160. package/registry/examples/description-list/receipt.tsx +17 -0
  161. package/registry/examples/fab/native/extended.d.ts +4 -0
  162. package/registry/examples/fab/native/extended.dark.png +0 -0
  163. package/registry/examples/fab/native/extended.js +18 -0
  164. package/registry/examples/fab/native/extended.light.png +0 -0
  165. package/registry/examples/fab/native/extended.tsx +23 -0
  166. package/registry/examples/header/native/large-title.d.ts +4 -0
  167. package/registry/examples/header/native/large-title.dark.png +0 -0
  168. package/registry/examples/header/native/large-title.js +23 -0
  169. package/registry/examples/header/native/large-title.light.png +0 -0
  170. package/registry/examples/header/native/large-title.tsx +31 -0
  171. package/registry/examples/image-viewer/native/gallery.d.ts +4 -0
  172. package/registry/examples/image-viewer/native/gallery.dark.png +0 -0
  173. package/registry/examples/image-viewer/native/gallery.js +22 -0
  174. package/registry/examples/image-viewer/native/gallery.light.png +0 -0
  175. package/registry/examples/image-viewer/native/gallery.tsx +21 -0
  176. package/registry/examples/kanban/moderation.d.ts +4 -0
  177. package/registry/examples/kanban/moderation.js +35 -0
  178. package/registry/examples/kanban/moderation.tsx +37 -0
  179. package/registry/examples/list/jobs.d.ts +4 -0
  180. package/registry/examples/list/jobs.js +35 -0
  181. package/registry/examples/list/jobs.tsx +33 -0
  182. package/registry/examples/list/selectable.d.ts +4 -0
  183. package/registry/examples/list/selectable.js +33 -0
  184. package/registry/examples/list/selectable.tsx +30 -0
  185. package/registry/examples/list/virtualized.d.ts +4 -0
  186. package/registry/examples/list/virtualized.js +22 -0
  187. package/registry/examples/list/virtualized.tsx +17 -0
  188. package/registry/examples/loader/native/block.d.ts +4 -0
  189. package/registry/examples/loader/native/block.dark.png +0 -0
  190. package/registry/examples/loader/native/block.js +12 -0
  191. package/registry/examples/loader/native/block.light.png +0 -0
  192. package/registry/examples/loader/native/block.tsx +11 -0
  193. package/registry/examples/loader/native/job-wait.d.ts +4 -0
  194. package/registry/examples/loader/native/job-wait.dark.png +0 -0
  195. package/registry/examples/loader/native/job-wait.js +17 -0
  196. package/registry/examples/loader/native/job-wait.light.png +0 -0
  197. package/registry/examples/loader/native/job-wait.tsx +14 -0
  198. package/registry/examples/map/cluster.d.ts +4 -0
  199. package/registry/examples/map/cluster.js +16 -0
  200. package/registry/examples/map/cluster.tsx +18 -0
  201. package/registry/examples/map/location-picker.d.ts +4 -0
  202. package/registry/examples/map/location-picker.js +24 -0
  203. package/registry/examples/map/location-picker.tsx +22 -0
  204. package/registry/examples/map/native/location-picker.d.ts +4 -0
  205. package/registry/examples/map/native/location-picker.dark.png +0 -0
  206. package/registry/examples/map/native/location-picker.js +14 -0
  207. package/registry/examples/map/native/location-picker.light.png +0 -0
  208. package/registry/examples/map/native/location-picker.tsx +13 -0
  209. package/registry/examples/map/native/places.d.ts +4 -0
  210. package/registry/examples/map/native/places.dark.png +0 -0
  211. package/registry/examples/map/native/places.js +25 -0
  212. package/registry/examples/map/native/places.light.png +0 -0
  213. package/registry/examples/map/native/places.tsx +27 -0
  214. package/registry/examples/map/places.d.ts +4 -0
  215. package/registry/examples/map/places.js +28 -0
  216. package/registry/examples/map/places.tsx +28 -0
  217. package/registry/examples/map/static.d.ts +4 -0
  218. package/registry/examples/map/static.js +22 -0
  219. package/registry/examples/map/static.tsx +18 -0
  220. package/registry/examples/match-dialog/its-a-match.d.ts +4 -0
  221. package/registry/examples/match-dialog/its-a-match.js +29 -0
  222. package/registry/examples/match-dialog/its-a-match.tsx +25 -0
  223. package/registry/examples/match-dialog/native/its-a-match.d.ts +4 -0
  224. package/registry/examples/match-dialog/native/its-a-match.dark.png +0 -0
  225. package/registry/examples/match-dialog/native/its-a-match.js +28 -0
  226. package/registry/examples/match-dialog/native/its-a-match.light.png +0 -0
  227. package/registry/examples/match-dialog/native/its-a-match.tsx +22 -0
  228. package/registry/examples/media-picker/native/story.d.ts +4 -0
  229. package/registry/examples/media-picker/native/story.dark.png +0 -0
  230. package/registry/examples/media-picker/native/story.js +30 -0
  231. package/registry/examples/media-picker/native/story.light.png +0 -0
  232. package/registry/examples/media-picker/native/story.tsx +26 -0
  233. package/registry/examples/meter/native/credits.d.ts +4 -0
  234. package/registry/examples/meter/native/credits.dark.png +0 -0
  235. package/registry/examples/meter/native/credits.js +15 -0
  236. package/registry/examples/meter/native/credits.light.png +0 -0
  237. package/registry/examples/meter/native/credits.tsx +12 -0
  238. package/registry/examples/meter/native/split-ring.d.ts +4 -0
  239. package/registry/examples/meter/native/split-ring.dark.png +0 -0
  240. package/registry/examples/meter/native/split-ring.js +24 -0
  241. package/registry/examples/meter/native/split-ring.light.png +0 -0
  242. package/registry/examples/meter/native/split-ring.tsx +19 -0
  243. package/registry/examples/number-ticker/native/balance.d.ts +4 -0
  244. package/registry/examples/number-ticker/native/balance.dark.png +0 -0
  245. package/registry/examples/number-ticker/native/balance.js +18 -0
  246. package/registry/examples/number-ticker/native/balance.light.png +0 -0
  247. package/registry/examples/number-ticker/native/balance.tsx +17 -0
  248. package/registry/examples/onboarding/native/first-launch.d.ts +4 -0
  249. package/registry/examples/onboarding/native/first-launch.dark.png +0 -0
  250. package/registry/examples/onboarding/native/first-launch.js +23 -0
  251. package/registry/examples/onboarding/native/first-launch.light.png +0 -0
  252. package/registry/examples/onboarding/native/first-launch.tsx +19 -0
  253. package/registry/examples/otp-input/native/invalid.d.ts +4 -0
  254. package/registry/examples/otp-input/native/invalid.dark.png +0 -0
  255. package/registry/examples/otp-input/native/invalid.js +15 -0
  256. package/registry/examples/otp-input/native/invalid.light.png +0 -0
  257. package/registry/examples/otp-input/native/invalid.tsx +14 -0
  258. package/registry/examples/otp-input/native/verify.d.ts +4 -0
  259. package/registry/examples/otp-input/native/verify.dark.png +0 -0
  260. package/registry/examples/otp-input/native/verify.js +17 -0
  261. package/registry/examples/otp-input/native/verify.light.png +0 -0
  262. package/registry/examples/otp-input/native/verify.tsx +14 -0
  263. package/registry/examples/permission-prompt/native/blocked.d.ts +4 -0
  264. package/registry/examples/permission-prompt/native/blocked.dark.png +0 -0
  265. package/registry/examples/permission-prompt/native/blocked.js +25 -0
  266. package/registry/examples/permission-prompt/native/blocked.light.png +0 -0
  267. package/registry/examples/permission-prompt/native/blocked.tsx +21 -0
  268. package/registry/examples/permission-prompt/native/camera.d.ts +4 -0
  269. package/registry/examples/permission-prompt/native/camera.dark.png +0 -0
  270. package/registry/examples/permission-prompt/native/camera.js +25 -0
  271. package/registry/examples/permission-prompt/native/camera.light.png +0 -0
  272. package/registry/examples/permission-prompt/native/camera.tsx +21 -0
  273. package/registry/examples/photo-grid/native/profile-photos.d.ts +4 -0
  274. package/registry/examples/photo-grid/native/profile-photos.dark.png +0 -0
  275. package/registry/examples/photo-grid/native/profile-photos.js +27 -0
  276. package/registry/examples/photo-grid/native/profile-photos.light.png +0 -0
  277. package/registry/examples/photo-grid/native/profile-photos.tsx +24 -0
  278. package/registry/examples/photo-grid/profile-photos.d.ts +4 -0
  279. package/registry/examples/photo-grid/profile-photos.js +28 -0
  280. package/registry/examples/photo-grid/profile-photos.tsx +25 -0
  281. package/registry/examples/pull-to-refresh/native/feed.d.ts +4 -0
  282. package/registry/examples/pull-to-refresh/native/feed.dark.png +0 -0
  283. package/registry/examples/pull-to-refresh/native/feed.js +14 -0
  284. package/registry/examples/pull-to-refresh/native/feed.light.png +0 -0
  285. package/registry/examples/pull-to-refresh/native/feed.tsx +19 -0
  286. package/registry/examples/screen/native/app-shell.d.ts +4 -0
  287. package/registry/examples/screen/native/app-shell.dark.png +0 -0
  288. package/registry/examples/screen/native/app-shell.js +25 -0
  289. package/registry/examples/screen/native/app-shell.light.png +0 -0
  290. package/registry/examples/screen/native/app-shell.tsx +33 -0
  291. package/registry/examples/screen/native/auth.d.ts +4 -0
  292. package/registry/examples/screen/native/auth.dark.png +0 -0
  293. package/registry/examples/screen/native/auth.js +24 -0
  294. package/registry/examples/screen/native/auth.light.png +0 -0
  295. package/registry/examples/screen/native/auth.tsx +20 -0
  296. package/registry/examples/screen/native/chat.d.ts +4 -0
  297. package/registry/examples/screen/native/chat.dark.png +0 -0
  298. package/registry/examples/screen/native/chat.js +27 -0
  299. package/registry/examples/screen/native/chat.light.png +0 -0
  300. package/registry/examples/screen/native/chat.tsx +22 -0
  301. package/registry/examples/screen/native/feed.d.ts +4 -0
  302. package/registry/examples/screen/native/feed.dark.png +0 -0
  303. package/registry/examples/screen/native/feed.js +24 -0
  304. package/registry/examples/screen/native/feed.light.png +0 -0
  305. package/registry/examples/screen/native/feed.tsx +34 -0
  306. package/registry/examples/screen/native/profile.d.ts +4 -0
  307. package/registry/examples/screen/native/profile.dark.png +0 -0
  308. package/registry/examples/screen/native/profile.js +29 -0
  309. package/registry/examples/screen/native/profile.light.png +0 -0
  310. package/registry/examples/screen/native/profile.tsx +27 -0
  311. package/registry/examples/screen/native/settings.d.ts +4 -0
  312. package/registry/examples/screen/native/settings.dark.png +0 -0
  313. package/registry/examples/screen/native/settings.js +20 -0
  314. package/registry/examples/screen/native/settings.light.png +0 -0
  315. package/registry/examples/screen/native/settings.tsx +19 -0
  316. package/registry/examples/search-field/native/cancel.d.ts +4 -0
  317. package/registry/examples/search-field/native/cancel.dark.png +0 -0
  318. package/registry/examples/search-field/native/cancel.js +14 -0
  319. package/registry/examples/search-field/native/cancel.light.png +0 -0
  320. package/registry/examples/search-field/native/cancel.tsx +13 -0
  321. package/registry/examples/stat/grid.d.ts +4 -0
  322. package/registry/examples/stat/grid.js +17 -0
  323. package/registry/examples/stat/grid.tsx +14 -0
  324. package/registry/examples/step-progress/native/sign-up.d.ts +4 -0
  325. package/registry/examples/step-progress/native/sign-up.dark.png +0 -0
  326. package/registry/examples/step-progress/native/sign-up.js +12 -0
  327. package/registry/examples/step-progress/native/sign-up.light.png +0 -0
  328. package/registry/examples/step-progress/native/sign-up.tsx +11 -0
  329. package/registry/examples/step-progress/sign-up.d.ts +4 -0
  330. package/registry/examples/step-progress/sign-up.js +22 -0
  331. package/registry/examples/step-progress/sign-up.tsx +16 -0
  332. package/registry/examples/stepper/native/run.d.ts +4 -0
  333. package/registry/examples/stepper/native/run.dark.png +0 -0
  334. package/registry/examples/stepper/native/run.js +24 -0
  335. package/registry/examples/stepper/native/run.light.png +0 -0
  336. package/registry/examples/stepper/native/run.tsx +20 -0
  337. package/registry/examples/swipe-deck/discover.d.ts +4 -0
  338. package/registry/examples/swipe-deck/discover.js +41 -0
  339. package/registry/examples/swipe-deck/discover.tsx +37 -0
  340. package/registry/examples/swipe-deck/native/discover.d.ts +4 -0
  341. package/registry/examples/swipe-deck/native/discover.dark.png +0 -0
  342. package/registry/examples/swipe-deck/native/discover.js +39 -0
  343. package/registry/examples/swipe-deck/native/discover.light.png +0 -0
  344. package/registry/examples/swipe-deck/native/discover.tsx +35 -0
  345. package/registry/examples/swipeable-row/native/conversation.d.ts +4 -0
  346. package/registry/examples/swipeable-row/native/conversation.dark.png +0 -0
  347. package/registry/examples/swipeable-row/native/conversation.js +30 -0
  348. package/registry/examples/swipeable-row/native/conversation.light.png +0 -0
  349. package/registry/examples/swipeable-row/native/conversation.tsx +27 -0
  350. package/registry/examples/tab-bar/native/main-sections.d.ts +4 -0
  351. package/registry/examples/tab-bar/native/main-sections.dark.png +0 -0
  352. package/registry/examples/tab-bar/native/main-sections.js +29 -0
  353. package/registry/examples/tab-bar/native/main-sections.light.png +0 -0
  354. package/registry/examples/tab-bar/native/main-sections.tsx +30 -0
  355. package/registry/examples/tabs/native/sections.d.ts +4 -0
  356. package/registry/examples/tabs/native/sections.dark.png +0 -0
  357. package/registry/examples/tabs/native/sections.js +23 -0
  358. package/registry/examples/tabs/native/sections.light.png +0 -0
  359. package/registry/examples/tabs/native/sections.tsx +19 -0
  360. package/registry/examples/tabs/native/segmented.d.ts +4 -0
  361. package/registry/examples/tabs/native/segmented.dark.png +0 -0
  362. package/registry/examples/tabs/native/segmented.js +22 -0
  363. package/registry/examples/tabs/native/segmented.light.png +0 -0
  364. package/registry/examples/tabs/native/segmented.tsx +18 -0
  365. package/registry/examples/tag-selector/interests.d.ts +4 -0
  366. package/registry/examples/tag-selector/interests.js +14 -0
  367. package/registry/examples/tag-selector/interests.tsx +12 -0
  368. package/registry/examples/tag-selector/native/interests.d.ts +4 -0
  369. package/registry/examples/tag-selector/native/interests.dark.png +0 -0
  370. package/registry/examples/tag-selector/native/interests.js +15 -0
  371. package/registry/examples/tag-selector/native/interests.light.png +0 -0
  372. package/registry/examples/tag-selector/native/interests.tsx +15 -0
  373. package/registry/examples/toast/native/snackbar.d.ts +4 -0
  374. package/registry/examples/toast/native/snackbar.dark.png +0 -0
  375. package/registry/examples/toast/native/snackbar.js +23 -0
  376. package/registry/examples/toast/native/snackbar.light.png +0 -0
  377. package/registry/examples/toast/native/snackbar.tsx +21 -0
  378. package/registry/examples/tree-view/docs.d.ts +4 -0
  379. package/registry/examples/tree-view/docs.js +24 -0
  380. package/registry/examples/tree-view/docs.tsx +22 -0
  381. package/registry/examples/tree-view/lazy.d.ts +4 -0
  382. package/registry/examples/tree-view/lazy.js +21 -0
  383. package/registry/examples/tree-view/lazy.tsx +19 -0
  384. package/registry/examples/verified-badge/native/verified.d.ts +4 -0
  385. package/registry/examples/verified-badge/native/verified.dark.png +0 -0
  386. package/registry/examples/verified-badge/native/verified.js +18 -0
  387. package/registry/examples/verified-badge/native/verified.light.png +0 -0
  388. package/registry/examples/verified-badge/native/verified.tsx +15 -0
  389. package/registry/examples/verified-badge/verified.d.ts +4 -0
  390. package/registry/examples/verified-badge/verified.js +17 -0
  391. package/registry/examples/verified-badge/verified.tsx +13 -0
  392. package/registry/fab.json +122 -0
  393. package/registry/field.json +1 -1
  394. package/registry/file-upload.json +2 -2
  395. package/registry/filter-bar.json +1 -1
  396. package/registry/form.json +2 -2
  397. package/registry/gallery-page.json +1 -1
  398. package/registry/glow-card.json +1 -1
  399. package/registry/goal-gauge.json +1 -1
  400. package/registry/header.json +137 -0
  401. package/registry/how-it-works-page.json +1 -1
  402. package/registry/image-cropper.json +1 -1
  403. package/registry/image-viewer.json +107 -0
  404. package/registry/index.json +441 -25
  405. package/registry/iso.json +1 -1
  406. package/registry/kanban.json +253 -0
  407. package/registry/kbd.json +1 -1
  408. package/registry/landing-page.json +1 -1
  409. package/registry/landscape.json +1 -1
  410. package/registry/legal-page.json +1 -1
  411. package/registry/link-card.json +1 -1
  412. package/registry/list.json +367 -30
  413. package/registry/loader.json +157 -4
  414. package/registry/longform-page.json +1 -1
  415. package/registry/map.json +1368 -0
  416. package/registry/marketing-layout.json +1 -1
  417. package/registry/marquee.json +1 -1
  418. package/registry/mascot-quote.json +1 -1
  419. package/registry/mascot-state.json +1 -1
  420. package/registry/mascots.json +1 -1
  421. package/registry/match-dialog.json +300 -0
  422. package/registry/media-capture.json +1 -1
  423. package/registry/media-picker.json +129 -0
  424. package/registry/meter.json +136 -4
  425. package/registry/nav-bar.json +1 -1
  426. package/registry/number-ticker.json +96 -4
  427. package/registry/onboarding.json +95 -0
  428. package/registry/otp-input.json +150 -0
  429. package/registry/page-header.json +1 -1
  430. package/registry/page-hero.json +1 -1
  431. package/registry/page.json +1 -1
  432. package/registry/pagination.json +1 -1
  433. package/registry/permission-prompt.json +172 -0
  434. package/registry/photo-grid.json +302 -0
  435. package/registry/pointer-light.json +1 -1
  436. package/registry/popover.json +1 -1
  437. package/registry/pricing-page.json +1 -1
  438. package/registry/product-page.json +1 -1
  439. package/registry/profile-card.json +1 -1
  440. package/registry/progress.json +2 -2
  441. package/registry/promise-card.json +1 -1
  442. package/registry/pull-to-refresh.json +74 -0
  443. package/registry/radio-group.json +1 -1
  444. package/registry/screen.json +184 -0
  445. package/registry/scroll-stage.json +1 -1
  446. package/registry/search-field.json +30 -3
  447. package/registry/section-backdrop.json +1 -1
  448. package/registry/separator.json +1 -1
  449. package/registry/settings-layout.json +1 -1
  450. package/registry/skeleton.json +1 -1
  451. package/registry/slider.json +1 -1
  452. package/registry/slot.json +1 -1
  453. package/registry/spinner.json +1 -1
  454. package/registry/split-view.json +1 -1
  455. package/registry/stage-banner.json +1 -1
  456. package/registry/stat.json +68 -5
  457. package/registry/status-page.json +1 -1
  458. package/registry/step-progress.json +255 -0
  459. package/registry/stepper.json +75 -4
  460. package/registry/swipe-deck.json +512 -0
  461. package/registry/swipeable-row.json +124 -0
  462. package/registry/switch.json +1 -1
  463. package/registry/tab-bar.json +115 -0
  464. package/registry/tabs.json +94 -4
  465. package/registry/tag-input.json +1 -1
  466. package/registry/tag-selector.json +276 -0
  467. package/registry/terminal-replay.json +1 -1
  468. package/registry/theme.json +8 -1
  469. package/registry/toast.json +17 -3
  470. package/registry/toc.json +1 -1
  471. package/registry/tooltip.json +1 -1
  472. package/registry/tree-view.json +291 -0
  473. package/registry/verified-badge.json +202 -0
  474. package/registry/video-player.json +1 -1
  475. package/registry/video-trimmer.json +1 -1
  476. package/registry/virtual.json +1 -1
  477. package/skills/map/SKILL.md +35 -0
@@ -0,0 +1,1368 @@
1
+ {
2
+ "name": "map",
3
+ "type": "primitive",
4
+ "title": "Map",
5
+ "summary": "Maps that cost nothing per view — MapLibre (open source) over Protomaps tiles on the Krizaka CDN, painted from the tokens in dark or light and in the product's accent — with markers, clusters, the person's position, a radius and a location picker.",
6
+ "description": "Maps that cost nothing per view — MapLibre (open source) over Protomaps tiles on the Krizaka CDN, painted from the tokens in dark or light and in the product's accent — with markers, clusters, the person's position, a radius and a location picker.",
7
+ "status": "beta",
8
+ "category": "data-display",
9
+ "platforms": "both",
10
+ "whenToUse": [
11
+ "To show where things are: pick-up points, creators near you, an event's venue.",
12
+ "`Cluster` for many points (dozens to thousands): they group while close and open by zooming in.",
13
+ "`LocationPicker` to choose a place: a meeting point, a delivery address, the centre of a search with its radius.",
14
+ "`StaticMap` where a live map is too much (a card, a list, an e-mail) and as the `fallback` of every `Map`."
15
+ ],
16
+ "whenNotToUse": [
17
+ {
18
+ "when": "For an address typed or searched: a text field (geocoding is a server's job — Nominatim or Photon self-hosted later), the map only confirms it.",
19
+ "use": "field"
20
+ },
21
+ {
22
+ "when": "For figures by country or region (a choropleth): a chart.",
23
+ "use": "stat"
24
+ }
25
+ ],
26
+ "bestPractices": [
27
+ "Give every map a `label` and every marker a `label`: they are read as the list of places in place of the map.",
28
+ "Always pass a `fallback` (`StaticMap` with the same centre and places): it shows while the engine loads and for good without WebGL.",
29
+ "Ask for the position only on a press (`LocationPicker`'s Near me), never on load; every refusal has its words.",
30
+ "Keep the attribution visible (OpenStreetMap's licence, the ODbL, asks for it): the components draw it.",
31
+ "Install the engine only where a map is shown: `maplibre-gl` (web) or `@maplibre/maplibre-react-native` (Expo) — optional peers."
32
+ ],
33
+ "accessibility": {
34
+ "keyboard": [
35
+ {
36
+ "keys": "Tab",
37
+ "action": "The map (a region named by `label`), then its buttons: zoom, the markers that do something, Near me."
38
+ },
39
+ {
40
+ "keys": "Arrow keys",
41
+ "action": "Pan the focused map."
42
+ },
43
+ {
44
+ "keys": "+ / −",
45
+ "action": "Zoom the focused map in or out."
46
+ }
47
+ ],
48
+ "notes": [
49
+ "The canvas is hidden from assistive technologies: the places are read as a list (each `Marker`, `Cluster` point and the person's position by its `label`).",
50
+ "A marker with `onPress` is a button named by its label (`aria-pressed` when it can be chosen); the chosen one shows its name above it.",
51
+ "Under reduced motion the camera jumps instead of flying and the position's pulse stands still.",
52
+ "Colours come from the tokens: the water is the accent softened, roads and labels the text roles — readable in both schemes."
53
+ ]
54
+ },
55
+ "related": [
56
+ "dialog",
57
+ "search-field",
58
+ "chip"
59
+ ],
60
+ "web": {
61
+ "entry": "@krizaka/ui/map",
62
+ "import": "import { Map, Marker, Cluster, UserPosition, RadiusCircle, LocationPicker, StaticMap, mapStyle, distanceMeters, formatDistance } from \"@krizaka/ui/map\";",
63
+ "props": [
64
+ {
65
+ "component": "MAP_GLYPHS",
66
+ "description": "The glyphs of the labels (Noto Sans, SIL OFL): `{fontstack}` and `{range}` filled by MapLibre.",
67
+ "props": []
68
+ },
69
+ {
70
+ "component": "LocationPicker",
71
+ "description": "A position picked by moving the map under a pin; its distance from the person; \"near me\" on request.",
72
+ "props": [
73
+ {
74
+ "name": "label",
75
+ "type": "string",
76
+ "default": null,
77
+ "description": "The picker's accessible name (\"Meeting point\"), passed translated.",
78
+ "required": true
79
+ },
80
+ {
81
+ "name": "locale",
82
+ "type": "string",
83
+ "default": null,
84
+ "description": "The BCP 47 locale of the distance.",
85
+ "required": true
86
+ },
87
+ {
88
+ "name": "onValueChange",
89
+ "type": "(value: LngLat) => void",
90
+ "default": null,
91
+ "description": "Called when the map rests on a new position.",
92
+ "required": true
93
+ },
94
+ {
95
+ "name": "value",
96
+ "type": "LngLat",
97
+ "default": null,
98
+ "description": "The chosen position (controlled): the centre of the map.",
99
+ "required": true
100
+ },
101
+ {
102
+ "name": "brand",
103
+ "type": "BrandId",
104
+ "default": null,
105
+ "description": "The product whose accent the map carries. Default `krizaka`.",
106
+ "required": false
107
+ },
108
+ {
109
+ "name": "className",
110
+ "type": "string",
111
+ "default": null,
112
+ "description": "Classes merged on the root (its height: `h-80`, `aspect-video`).",
113
+ "required": false
114
+ },
115
+ {
116
+ "name": "fallback",
117
+ "type": "ReactNode",
118
+ "default": null,
119
+ "description": "Shown in place of the map while its engine loads, and for good without WebGL: a `StaticMap`, a picture.",
120
+ "required": false
121
+ },
122
+ {
123
+ "name": "labels",
124
+ "type": "Partial<LocationPickerLabels>",
125
+ "default": null,
126
+ "description": "The words, passed translated; English by default.",
127
+ "required": false
128
+ },
129
+ {
130
+ "name": "lang",
131
+ "type": "string",
132
+ "default": null,
133
+ "description": "The language of the labels (`fr`, `en`). Default `en`.",
134
+ "required": false
135
+ },
136
+ {
137
+ "name": "origin",
138
+ "type": "LngLat | null",
139
+ "default": "null",
140
+ "description": "Where the person is, when the app already knows it (the distance chip shows at once).",
141
+ "required": false
142
+ },
143
+ {
144
+ "name": "radius",
145
+ "type": "number",
146
+ "default": null,
147
+ "description": "A radius in metres drawn around the chosen position (a delivery zone, a search radius).",
148
+ "required": false
149
+ },
150
+ {
151
+ "name": "region",
152
+ "type": "MapRegion | null",
153
+ "default": null,
154
+ "description": "The detailed region (PMTiles) drawn from zoom 6.5. Default: our Montréal–Québec extract (to zoom 14).",
155
+ "required": false
156
+ },
157
+ {
158
+ "name": "scheme",
159
+ "type": "\"light\" | \"dark\"",
160
+ "default": null,
161
+ "description": "The scheme. Default: the page's (`html.light`), followed.",
162
+ "required": false
163
+ },
164
+ {
165
+ "name": "world",
166
+ "type": "MapRegion | null",
167
+ "default": null,
168
+ "description": "The world's low zooms (PMTiles). Default: our world extract (to zoom 6).",
169
+ "required": false
170
+ },
171
+ {
172
+ "name": "zoom",
173
+ "type": "number",
174
+ "default": "14",
175
+ "description": "The zoom at first. Default 14.",
176
+ "required": false
177
+ }
178
+ ]
179
+ },
180
+ {
181
+ "component": "Cluster",
182
+ "description": "Many points grouped while they are close: a disc with the count, opened by zooming in; each point is listed for screen readers.",
183
+ "props": [
184
+ {
185
+ "name": "points",
186
+ "type": "readonly ClusterPoint[]",
187
+ "default": null,
188
+ "description": "The points: many of them (creators, pick-up points, events).",
189
+ "required": true
190
+ },
191
+ {
192
+ "name": "onPointPress",
193
+ "type": "((point: ClusterPoint) => void)",
194
+ "default": null,
195
+ "description": "Pressing a single point. A group zooms in to open itself.",
196
+ "required": false
197
+ },
198
+ {
199
+ "name": "radius",
200
+ "type": "number",
201
+ "default": "48",
202
+ "description": "The radius, in pixels, within which points group. Default 48.",
203
+ "required": false
204
+ }
205
+ ]
206
+ },
207
+ {
208
+ "component": "Map",
209
+ "description": "A map: MapLibre over our Protomaps tiles, styled from the tokens, with its places as a list for screen readers.",
210
+ "props": [
211
+ {
212
+ "name": "center",
213
+ "type": "LngLat",
214
+ "default": null,
215
+ "description": "Where the map looks at first: longitude, latitude.",
216
+ "required": true
217
+ },
218
+ {
219
+ "name": "label",
220
+ "type": "string",
221
+ "default": null,
222
+ "description": "The map's accessible name (\"Pick-up points near you\"), passed translated.",
223
+ "required": true
224
+ },
225
+ {
226
+ "name": "brand",
227
+ "type": "BrandId",
228
+ "default": null,
229
+ "description": "The product whose accent the map carries. Default `krizaka`.",
230
+ "required": false
231
+ },
232
+ {
233
+ "name": "children",
234
+ "type": "ReactNode",
235
+ "default": null,
236
+ "description": "Markers, circles, clusters, the user's position — and anything drawn over the map.",
237
+ "required": false
238
+ },
239
+ {
240
+ "name": "className",
241
+ "type": "string",
242
+ "default": null,
243
+ "description": "Classes merged on the root (its height: `h-80`, `aspect-video`).",
244
+ "required": false
245
+ },
246
+ {
247
+ "name": "controlled",
248
+ "type": "boolean",
249
+ "default": null,
250
+ "description": "The camera follows `center` and `zoom` when they change (flies, or jumps under reduced motion).",
251
+ "required": false
252
+ },
253
+ {
254
+ "name": "controls",
255
+ "type": "boolean",
256
+ "default": "true",
257
+ "description": "The zoom buttons (default) — the keyboard also zooms with + and − on the focused map.",
258
+ "required": false
259
+ },
260
+ {
261
+ "name": "fallback",
262
+ "type": "ReactNode",
263
+ "default": null,
264
+ "description": "Shown in place of the map while its engine loads, and for good without WebGL: a `StaticMap`, a picture.",
265
+ "required": false
266
+ },
267
+ {
268
+ "name": "interactive",
269
+ "type": "boolean",
270
+ "default": "true",
271
+ "description": "Moves with the hand (default); `false` makes a still picture of a place.",
272
+ "required": false
273
+ },
274
+ {
275
+ "name": "labels",
276
+ "type": "Partial<MapLabels>",
277
+ "default": null,
278
+ "description": "The words, passed translated; English by default.",
279
+ "required": false
280
+ },
281
+ {
282
+ "name": "lang",
283
+ "type": "string",
284
+ "default": null,
285
+ "description": "The language of the labels (`fr`, `en`). Default `en`.",
286
+ "required": false
287
+ },
288
+ {
289
+ "name": "onMoveEnd",
290
+ "type": "((view: { center: LngLat; zoom: number; }) => void)",
291
+ "default": null,
292
+ "description": "Called when the camera rests: its centre and zoom.",
293
+ "required": false
294
+ },
295
+ {
296
+ "name": "onPress",
297
+ "type": "((position: LngLat) => void)",
298
+ "default": null,
299
+ "description": "Called with the position pressed on the map (not on a marker).",
300
+ "required": false
301
+ },
302
+ {
303
+ "name": "region",
304
+ "type": "MapRegion | null",
305
+ "default": null,
306
+ "description": "The detailed region (PMTiles) drawn from zoom 6.5. Default: our Montréal–Québec extract (to zoom 14).",
307
+ "required": false
308
+ },
309
+ {
310
+ "name": "scheme",
311
+ "type": "\"light\" | \"dark\"",
312
+ "default": null,
313
+ "description": "The scheme. Default: the page's (`html.light`), followed.",
314
+ "required": false
315
+ },
316
+ {
317
+ "name": "world",
318
+ "type": "MapRegion | null",
319
+ "default": null,
320
+ "description": "The world's low zooms (PMTiles). Default: our world extract (to zoom 6).",
321
+ "required": false
322
+ },
323
+ {
324
+ "name": "zoom",
325
+ "type": "number",
326
+ "default": "12",
327
+ "description": "The zoom at first (0 the world, 12 a city, 16 a street). Default 12.",
328
+ "required": false
329
+ }
330
+ ]
331
+ },
332
+ {
333
+ "component": "Marker",
334
+ "description": "A place on the map: an accent dot (or your content) that is a button when it does something.",
335
+ "props": [
336
+ {
337
+ "name": "label",
338
+ "type": "string",
339
+ "default": null,
340
+ "description": "Its words: the place's name, read in the list of places and as the button's name (\"Café Olimpico, 350 m\").",
341
+ "required": true
342
+ },
343
+ {
344
+ "name": "position",
345
+ "type": "LngLat",
346
+ "default": null,
347
+ "description": "Where it stands.",
348
+ "required": true
349
+ },
350
+ {
351
+ "name": "children",
352
+ "type": "ReactNode",
353
+ "default": null,
354
+ "description": "What it draws instead of the accent dot (an avatar, an icon).",
355
+ "required": false
356
+ },
357
+ {
358
+ "name": "onPress",
359
+ "type": "(() => void)",
360
+ "default": null,
361
+ "description": "Pressing it (details in a panel, a sheet): the marker becomes a button.",
362
+ "required": false
363
+ },
364
+ {
365
+ "name": "selected",
366
+ "type": "boolean",
367
+ "default": null,
368
+ "description": "The chosen one: larger, its label shown above it.",
369
+ "required": false
370
+ },
371
+ {
372
+ "name": "size",
373
+ "type": "number",
374
+ "default": "18",
375
+ "description": "Its size in pixels. Default 18.",
376
+ "required": false
377
+ }
378
+ ]
379
+ },
380
+ {
381
+ "component": "RadiusCircle",
382
+ "description": "A circle on the ground: a delivery zone, a search radius — filled with the accent, its edge drawn.",
383
+ "props": [
384
+ {
385
+ "name": "center",
386
+ "type": "LngLat",
387
+ "default": null,
388
+ "description": "Its centre.",
389
+ "required": true
390
+ },
391
+ {
392
+ "name": "radius",
393
+ "type": "number",
394
+ "default": null,
395
+ "description": "Its radius in metres.",
396
+ "required": true
397
+ },
398
+ {
399
+ "name": "tone",
400
+ "type": "\"accent\" | \"soft\"",
401
+ "default": "accent",
402
+ "description": "`accent` (a zone that matters: a delivery area, a search radius) or `soft` (an accuracy).",
403
+ "required": false
404
+ }
405
+ ]
406
+ },
407
+ {
408
+ "component": "UserPosition",
409
+ "description": "The person's position: a dot that pulses (still under reduced motion), its accuracy as a soft disc.",
410
+ "props": [
411
+ {
412
+ "name": "label",
413
+ "type": "string",
414
+ "default": null,
415
+ "description": "Its words (\"You are here\"), read in the list of places.",
416
+ "required": true
417
+ },
418
+ {
419
+ "name": "position",
420
+ "type": "LngLat",
421
+ "default": null,
422
+ "description": "Where the person is.",
423
+ "required": true
424
+ },
425
+ {
426
+ "name": "accuracy",
427
+ "type": "number",
428
+ "default": null,
429
+ "description": "The accuracy in metres: a soft disc around the dot.",
430
+ "required": false
431
+ }
432
+ ]
433
+ },
434
+ {
435
+ "component": "StaticMap",
436
+ "description": "A still map: the places on the map's paper (or on a snapshot), named in words.",
437
+ "props": [
438
+ {
439
+ "name": "center",
440
+ "type": "LngLat",
441
+ "default": null,
442
+ "description": "The centre of the picture.",
443
+ "required": true
444
+ },
445
+ {
446
+ "name": "label",
447
+ "type": "string",
448
+ "default": null,
449
+ "description": "What the picture shows, in words (\"Three pick-up points in the Plateau\"), passed translated: its accessible name.",
450
+ "required": true
451
+ },
452
+ {
453
+ "name": "className",
454
+ "type": "string",
455
+ "default": null,
456
+ "description": "Classes merged on the root (its size).",
457
+ "required": false
458
+ },
459
+ {
460
+ "name": "image",
461
+ "type": "string",
462
+ "default": null,
463
+ "description": "A snapshot of the real map of the area (same centre and zoom), drawn under the places; the attribution then shows.",
464
+ "required": false
465
+ },
466
+ {
467
+ "name": "places",
468
+ "type": "readonly StaticMapPlace[]",
469
+ "default": "[]",
470
+ "description": "The places drawn on it (an accent dot each), listed in its name.",
471
+ "required": false
472
+ },
473
+ {
474
+ "name": "zoom",
475
+ "type": "number",
476
+ "default": "12",
477
+ "description": "The zoom of the picture. Default 12.",
478
+ "required": false
479
+ }
480
+ ]
481
+ },
482
+ {
483
+ "component": "REGION_FROM_ZOOM",
484
+ "description": "The zoom where the detailed region takes over from the world.",
485
+ "props": []
486
+ }
487
+ ],
488
+ "examples": [
489
+ {
490
+ "name": "places",
491
+ "title": "Places",
492
+ "description": "Pick-up points in Montréal: markers are buttons, the chosen one shows its name.",
493
+ "path": "examples/map/places.tsx",
494
+ "module": "@krizaka/ui/registry/examples/map/places",
495
+ "code": "\"use client\";\n\nimport { Map, Marker, StaticMap } from \"@krizaka/ui/map\";\nimport * as React from \"react\";\n\nconst SPOTS = [\n { id: \"olimpico\", label: \"Café Olimpico, Mile End\", position: [-73.6005, 45.5229] as const },\n { id: \"jean-talon\", label: \"Marché Jean-Talon\", position: [-73.6147, 45.5363] as const },\n { id: \"parc\", label: \"Parc La Fontaine\", position: [-73.5695, 45.5268] as const },\n];\n\n// Pick-up points in Montréal: markers are buttons, the chosen one shows its name; the places are a list for screen readers.\nexport default function MapPlaces() {\n const [chosen, setChosen] = React.useState(\"olimpico\");\n return (\n <Map\n label=\"Pick-up points in Montréal\"\n center={[-73.592, 45.528]}\n zoom={12.5}\n className=\"h-80 w-full max-w-xl\"\n fallback={<StaticMap label=\"Pick-up points in Montréal\" center={[-73.592, 45.528]} zoom={12.5} places={SPOTS} />}\n >\n {SPOTS.map((s) => (\n <Marker key={s.id} position={s.position} label={s.label} selected={s.id === chosen} onPress={() => setChosen(s.id)} />\n ))}\n </Map>\n );\n}\n"
496
+ },
497
+ {
498
+ "name": "cluster",
499
+ "title": "Cluster",
500
+ "description": "Sixty creators grouped while they are close; a group opens by zooming in.",
501
+ "path": "examples/map/cluster.tsx",
502
+ "module": "@krizaka/ui/registry/examples/map/cluster",
503
+ "code": "\"use client\";\n\nimport { Cluster, Map, StaticMap } from \"@krizaka/ui/map\";\n\n// 60 creators around Montréal, grouped while they are close; a group opens by zooming in.\nconst CREATORS = Array.from({ length: 60 }, (_, i) => ({\n id: String(i),\n label: `Creator ${i + 1}`,\n position: [-73.62 + ((i * 37) % 60) / 600, 45.48 + ((i * 53) % 70) / 700] as const,\n}));\n\nexport default function MapCluster() {\n return (\n <Map label=\"Creators near Montréal\" center={[-73.58, 45.53]} zoom={11} className=\"h-80 w-full max-w-xl\" fallback={<StaticMap label=\"Creators near Montréal\" center={[-73.58, 45.53]} zoom={11} />}>\n <Cluster points={CREATORS} />\n </Map>\n );\n}\n"
504
+ },
505
+ {
506
+ "name": "location-picker",
507
+ "title": "Location picker",
508
+ "description": "A meeting point under the pin, its distance from the person, a 1-km zone and Near me.",
509
+ "path": "examples/map/location-picker.tsx",
510
+ "module": "@krizaka/ui/registry/examples/map/location-picker",
511
+ "code": "\"use client\";\n\nimport { type LngLat, LocationPicker, StaticMap } from \"@krizaka/ui/map\";\nimport * as React from \"react\";\n\n// A meeting point picked by moving the map under the pin: its distance from the person, a 1-km zone, \"Near me\" on request.\nexport default function MapLocationPicker() {\n const [point, setPoint] = React.useState<LngLat>([-73.5673, 45.5017]);\n return (\n <div className=\"w-full max-w-xl\">\n <LocationPicker\n label=\"Meeting point\"\n locale=\"en-CA\"\n value={point}\n onValueChange={setPoint}\n origin={[-73.5794, 45.5048]}\n radius={1000}\n fallback={<StaticMap label=\"Meeting point\" center={point} zoom={14} places={[{ position: point, label: \"Meeting point\" }]} />}\n />\n </div>\n );\n}\n"
512
+ },
513
+ {
514
+ "name": "static",
515
+ "title": "Static map",
516
+ "description": "The places on the map's paper, without an engine: a card, a list item, a fallback.",
517
+ "path": "examples/map/static.tsx",
518
+ "module": "@krizaka/ui/registry/examples/map/static",
519
+ "code": "import { StaticMap } from \"@krizaka/ui/map\";\n\n// A still map without an engine: the places projected on the map's paper — a card, a list item, the fallback of a Map.\nexport default function MapStatic() {\n return (\n <div className=\"h-56 w-full max-w-md overflow-hidden rounded-2xl border border-border-default\">\n <StaticMap\n label=\"Your two pick-up points\"\n center={[-73.585, 45.524]}\n zoom={13}\n places={[\n { position: [-73.6005, 45.5229], label: \"Café Olimpico\" },\n { position: [-73.5695, 45.5268], label: \"Parc La Fontaine\" },\n ]}\n />\n </div>\n );\n}\n"
520
+ }
521
+ ]
522
+ },
523
+ "native": {
524
+ "entry": "@krizaka/ui/native/map",
525
+ "import": "import { Map, Marker, Cluster, UserPosition, RadiusCircle, LocationPicker } from \"@krizaka/ui/native/map\";",
526
+ "differences": [
527
+ "MapLibre Native (`@maplibre/maplibre-react-native`, an optional peer, with its Expo config plugin): the same style and tiles as the web — it reads `pmtiles://` itself.",
528
+ "A marker's details open in a `Sheet` (`@krizaka/ui/native`); markers and Near me play a haptic.",
529
+ "Near me asks the system's permission through MapLibre's `LocationManager` when pressed; the app declares the location usage in its config.",
530
+ "The map's name lists its places for VoiceOver and TalkBack; the screenshots of these stories are drawn by a stand-in (the markers placed, not the tiles)."
531
+ ],
532
+ "props": [
533
+ {
534
+ "component": "Map",
535
+ "description": "A map: MapLibre Native over our Protomaps tiles, styled from the tokens in the app's scheme; its places in its name.",
536
+ "props": [
537
+ {
538
+ "name": "center",
539
+ "type": "LngLat",
540
+ "default": null,
541
+ "description": "Where the map looks: longitude, latitude.",
542
+ "required": true
543
+ },
544
+ {
545
+ "name": "label",
546
+ "type": "string",
547
+ "default": null,
548
+ "description": "The map's accessible name (\"Pick-up points near you\"), passed translated; the places follow it.",
549
+ "required": true
550
+ },
551
+ {
552
+ "name": "brand",
553
+ "type": "BrandId",
554
+ "default": null,
555
+ "description": "The product whose accent the map carries. Default `krizaka`.",
556
+ "required": false
557
+ },
558
+ {
559
+ "name": "children",
560
+ "type": "ReactNode",
561
+ "default": null,
562
+ "description": "Markers, circles, clusters, the person's position.",
563
+ "required": false
564
+ },
565
+ {
566
+ "name": "interactive",
567
+ "type": "boolean",
568
+ "default": "true",
569
+ "description": "Moves with the hand (default).",
570
+ "required": false
571
+ },
572
+ {
573
+ "name": "lang",
574
+ "type": "string",
575
+ "default": null,
576
+ "description": "The language of the labels (`fr`, `en`). Default `en`.",
577
+ "required": false
578
+ },
579
+ {
580
+ "name": "onMoveEnd",
581
+ "type": "((view: { center: LngLat; zoom: number; }) => void)",
582
+ "default": null,
583
+ "description": "Called when the camera rests: its centre and zoom.",
584
+ "required": false
585
+ },
586
+ {
587
+ "name": "onPress",
588
+ "type": "((position: LngLat) => void)",
589
+ "default": null,
590
+ "description": "Called with the position pressed on the map (not on a marker).",
591
+ "required": false
592
+ },
593
+ {
594
+ "name": "region",
595
+ "type": "MapRegion | null",
596
+ "default": null,
597
+ "description": "The detailed region (PMTiles) drawn from zoom 6.5. Default: our Montréal–Québec extract (to zoom 14).",
598
+ "required": false
599
+ },
600
+ {
601
+ "name": "style",
602
+ "type": "StyleProp<ViewStyle>",
603
+ "default": null,
604
+ "description": "Styles merged last (its height).",
605
+ "required": false
606
+ },
607
+ {
608
+ "name": "world",
609
+ "type": "MapRegion | null",
610
+ "default": null,
611
+ "description": "The world's low zooms (PMTiles). Default: our world extract (to zoom 6).",
612
+ "required": false
613
+ },
614
+ {
615
+ "name": "zoom",
616
+ "type": "number",
617
+ "default": "12",
618
+ "description": "The zoom (0 the world, 12 a city, 16 a street). Default 12.",
619
+ "required": false
620
+ }
621
+ ]
622
+ },
623
+ {
624
+ "component": "Marker",
625
+ "description": "A place: an accent dot (or your view) that is a button when it does something.",
626
+ "props": [
627
+ {
628
+ "name": "label",
629
+ "type": "string",
630
+ "default": null,
631
+ "description": "Its words: the place's name (read in the map's name and as the button's name).",
632
+ "required": true
633
+ },
634
+ {
635
+ "name": "position",
636
+ "type": "LngLat",
637
+ "default": null,
638
+ "description": "Where it stands.",
639
+ "required": true
640
+ },
641
+ {
642
+ "name": "children",
643
+ "type": "ReactNode",
644
+ "default": null,
645
+ "description": "What it draws instead of the accent dot.",
646
+ "required": false
647
+ },
648
+ {
649
+ "name": "onPress",
650
+ "type": "(() => void)",
651
+ "default": null,
652
+ "description": "Pressing it: details in a `Sheet`.",
653
+ "required": false
654
+ },
655
+ {
656
+ "name": "selected",
657
+ "type": "boolean",
658
+ "default": null,
659
+ "description": "The chosen one: larger, its label above it.",
660
+ "required": false
661
+ }
662
+ ]
663
+ },
664
+ {
665
+ "component": "UserPosition",
666
+ "description": "The person's position: a dot that pulses (still when the system reduces motion).",
667
+ "props": [
668
+ {
669
+ "name": "label",
670
+ "type": "string",
671
+ "default": null,
672
+ "description": "Its words (\"You are here\").",
673
+ "required": true
674
+ },
675
+ {
676
+ "name": "position",
677
+ "type": "LngLat",
678
+ "default": null,
679
+ "description": "Where the person is.",
680
+ "required": true
681
+ },
682
+ {
683
+ "name": "accuracy",
684
+ "type": "number",
685
+ "default": null,
686
+ "description": "The accuracy in metres: a soft disc.",
687
+ "required": false
688
+ }
689
+ ]
690
+ },
691
+ {
692
+ "component": "RadiusCircle",
693
+ "description": "A circle on the ground: a delivery zone, a search radius.",
694
+ "props": [
695
+ {
696
+ "name": "center",
697
+ "type": "LngLat",
698
+ "default": null,
699
+ "description": "Its centre.",
700
+ "required": true
701
+ },
702
+ {
703
+ "name": "radius",
704
+ "type": "number",
705
+ "default": null,
706
+ "description": "Its radius in metres.",
707
+ "required": true
708
+ },
709
+ {
710
+ "name": "tone",
711
+ "type": "\"accent\" | \"soft\"",
712
+ "default": "accent",
713
+ "description": "`accent` (a zone) or `soft` (an accuracy).",
714
+ "required": false
715
+ }
716
+ ]
717
+ },
718
+ {
719
+ "component": "Cluster",
720
+ "description": "Many points grouped while they are close: a disc with the count; each point is part of the map's name.",
721
+ "props": [
722
+ {
723
+ "name": "points",
724
+ "type": "readonly ClusterPoint[]",
725
+ "default": null,
726
+ "description": "The points: many of them.",
727
+ "required": true
728
+ },
729
+ {
730
+ "name": "onPointPress",
731
+ "type": "((point: ClusterPoint) => void)",
732
+ "default": null,
733
+ "description": "Pressing a single point (details in a `Sheet`).",
734
+ "required": false
735
+ },
736
+ {
737
+ "name": "radius",
738
+ "type": "number",
739
+ "default": "48",
740
+ "description": "The radius, in points, within which points group. Default 48.",
741
+ "required": false
742
+ }
743
+ ]
744
+ },
745
+ {
746
+ "component": "LocationPicker",
747
+ "description": "A position picked by moving the map under a pin; its distance from the person; \"near me\" asks for the location only when pressed.",
748
+ "props": [
749
+ {
750
+ "name": "label",
751
+ "type": "string",
752
+ "default": null,
753
+ "description": "The picker's accessible name (\"Meeting point\").",
754
+ "required": true
755
+ },
756
+ {
757
+ "name": "locale",
758
+ "type": "string",
759
+ "default": null,
760
+ "description": "The BCP 47 locale of the distance.",
761
+ "required": true
762
+ },
763
+ {
764
+ "name": "onValueChange",
765
+ "type": "(value: LngLat) => void",
766
+ "default": null,
767
+ "description": "Called when the map rests on a new position.",
768
+ "required": true
769
+ },
770
+ {
771
+ "name": "value",
772
+ "type": "LngLat",
773
+ "default": null,
774
+ "description": "The chosen position (the centre of the map).",
775
+ "required": true
776
+ },
777
+ {
778
+ "name": "brand",
779
+ "type": "BrandId",
780
+ "default": null,
781
+ "description": "The product whose accent the map carries. Default `krizaka`.",
782
+ "required": false
783
+ },
784
+ {
785
+ "name": "labels",
786
+ "type": "Partial<LocationPickerLabels>",
787
+ "default": null,
788
+ "description": "The words, passed translated.",
789
+ "required": false
790
+ },
791
+ {
792
+ "name": "lang",
793
+ "type": "string",
794
+ "default": null,
795
+ "description": "The language of the labels (`fr`, `en`). Default `en`.",
796
+ "required": false
797
+ },
798
+ {
799
+ "name": "origin",
800
+ "type": "LngLat | null",
801
+ "default": "null",
802
+ "description": "Where the person is, when already known.",
803
+ "required": false
804
+ },
805
+ {
806
+ "name": "radius",
807
+ "type": "number",
808
+ "default": null,
809
+ "description": "A radius in metres around the chosen position.",
810
+ "required": false
811
+ },
812
+ {
813
+ "name": "region",
814
+ "type": "MapRegion | null",
815
+ "default": null,
816
+ "description": "The detailed region (PMTiles) drawn from zoom 6.5. Default: our Montréal–Québec extract (to zoom 14).",
817
+ "required": false
818
+ },
819
+ {
820
+ "name": "style",
821
+ "type": "StyleProp<ViewStyle>",
822
+ "default": null,
823
+ "description": "Styles merged last (its height).",
824
+ "required": false
825
+ },
826
+ {
827
+ "name": "world",
828
+ "type": "MapRegion | null",
829
+ "default": null,
830
+ "description": "The world's low zooms (PMTiles). Default: our world extract (to zoom 6).",
831
+ "required": false
832
+ },
833
+ {
834
+ "name": "zoom",
835
+ "type": "number",
836
+ "default": "14",
837
+ "description": "The zoom. Default 14.",
838
+ "required": false
839
+ }
840
+ ]
841
+ },
842
+ {
843
+ "component": "MAP_GLYPHS",
844
+ "description": "The glyphs of the labels (Noto Sans, SIL OFL): `{fontstack}` and `{range}` filled by MapLibre.",
845
+ "props": []
846
+ },
847
+ {
848
+ "component": "LngLat",
849
+ "description": "A position: longitude, latitude (GeoJSON's order, MapLibre's `LngLat`).",
850
+ "props": []
851
+ },
852
+ {
853
+ "component": "MapRegion",
854
+ "description": "A region of the basemap: its PMTiles file and the zoom up to which it has tiles.",
855
+ "props": []
856
+ }
857
+ ],
858
+ "examples": [
859
+ {
860
+ "name": "places",
861
+ "title": "Places",
862
+ "description": "Pick-up points: a marker pressed opens its details in a bottom sheet.",
863
+ "path": "examples/map/native/places.tsx",
864
+ "module": "@krizaka/ui/registry/examples/map/native/places",
865
+ "code": "import { Button, Sheet, Txt } from \"@krizaka/ui/native\";\nimport { Map, Marker } from \"@krizaka/ui/native/map\";\nimport * as React from \"react\";\nimport { View } from \"react-native\";\n\nconst SPOTS = [\n { id: \"olimpico\", label: \"Café Olimpico\", address: \"124 Rue Saint-Viateur O\", position: [-73.6005, 45.5229] as const },\n { id: \"parc\", label: \"Parc La Fontaine\", address: \"3933 Av. du Parc-La Fontaine\", position: [-73.5695, 45.5268] as const },\n];\n\n// Pick-up points: a marker pressed opens its details in a bottom sheet.\nexport default function NativeMapPlaces() {\n const [open, setOpen] = React.useState<(typeof SPOTS)[number] | null>(null);\n return (\n <View style={{ width: 340 }}>\n <Map label=\"Pick-up points in Montréal\" center={[-73.585, 45.524]} zoom={12.5}>\n {SPOTS.map((s) => (\n <Marker key={s.id} position={s.position} label={s.label} selected={open?.id === s.id} onPress={() => setOpen(s)} />\n ))}\n </Map>\n <Sheet open={Boolean(open)} onOpenChange={(o) => !o && setOpen(null)} title={open?.label}>\n <Txt tone=\"secondary\">{open?.address}</Txt>\n <Button label=\"Choose this point\" variant=\"primary\" onPress={() => setOpen(null)} />\n </Sheet>\n </View>\n );\n}\n",
866
+ "screenshots": {
867
+ "dark": "examples/map/native/places.dark.png",
868
+ "light": "examples/map/native/places.light.png"
869
+ }
870
+ },
871
+ {
872
+ "name": "location-picker",
873
+ "title": "Location picker",
874
+ "description": "A meeting point under the pin; Near me on request.",
875
+ "path": "examples/map/native/location-picker.tsx",
876
+ "module": "@krizaka/ui/registry/examples/map/native/location-picker",
877
+ "code": "import { type LngLat, LocationPicker } from \"@krizaka/ui/native/map\";\nimport * as React from \"react\";\nimport { View } from \"react-native\";\n\n// A meeting point under the pin; \"Near me\" asks for the location only when pressed, the distance follows.\nexport default function NativeMapLocationPicker() {\n const [point, setPoint] = React.useState<LngLat>([-73.5673, 45.5017]);\n return (\n <View style={{ width: 340 }}>\n <LocationPicker label=\"Meeting point\" locale=\"en-CA\" value={point} onValueChange={setPoint} origin={[-73.5794, 45.5048]} radius={1000} />\n </View>\n );\n}\n",
878
+ "screenshots": {
879
+ "dark": "examples/map/native/location-picker.dark.png",
880
+ "light": "examples/map/native/location-picker.light.png"
881
+ }
882
+ }
883
+ ]
884
+ },
885
+ "files": [
886
+ {
887
+ "path": "map/assets.ts",
888
+ "content": "// Where the basemap lives: Protomaps PMTiles extracts on the Krizaka CDN (Bunny), uploaded by tools/map-assets.mjs.\n// No API key, no metered service: one file per region, read by HTTP range requests. A new extract is a new file (its\n// build date in the name) and a new default here.\n\nconst CDN = \"https://krizaka-assets.b-cdn.net/maps/v1\";\n\n/** The basemap extracts: OpenStreetMap data built by Protomaps (ODbL). `world` up to zoom 6, `quebec` (Montréal–Québec) up to 14. */\nexport const MAP_TILES = {\n world: `${CDN}/tiles/world-20261012.pmtiles`,\n quebec: `${CDN}/tiles/quebec-20261012.pmtiles`,\n} as const;\n\n/** The glyphs of the labels (Noto Sans, SIL OFL): `{fontstack}` and `{range}` filled by MapLibre. */\nexport const MAP_GLYPHS = `${CDN}/fonts/{fontstack}/{range}.pbf`;\n\n/** The words every map shows, as the licences ask: OpenStreetMap's contributors (ODbL) and Protomaps. */\nexport const MAP_ATTRIBUTION = { osm: \"© OpenStreetMap contributors\", osmUrl: \"https://www.openstreetmap.org/copyright\", protomaps: \"Protomaps\", protomapsUrl: \"https://protomaps.com\" } as const;\n\nlet base: string | null = null;\n\n/**\n * Where the maps read the tiles and the glyphs, when not from the CDN itself: a same-origin path that proxies it\n * (`https://www.krizaka.com/maps/v1`, a rewrite of the site) — no cross-origin request at all. Absolute URL, no trailing slash.\n */\nexport function setMapAssetsBase(url: string | null) {\n base = url ? url.replace(/\\/+$/, \"\") : null;\n}\n\n/** The tiles and glyphs the maps read now: the CDN's, or under the base set by `setMapAssetsBase`. */\nexport function mapAssets() {\n const at = (u: string) => (base ? u.replace(CDN, base) : u);\n return { tiles: { world: at(MAP_TILES.world), quebec: at(MAP_TILES.quebec) }, glyphs: at(MAP_GLYPHS) };\n}\n"
889
+ },
890
+ {
891
+ "path": "map/geo.ts",
892
+ "content": "/// <reference types=\"geojson\" />\n\n// Plain geometry for the maps, web and native (no DOM, no React): distances, circles, the Web Mercator projection.\n\n/** A position: longitude, latitude (GeoJSON's order, MapLibre's `LngLat`). */\nexport type LngLat = readonly [lng: number, lat: number];\n\nconst R = 6_371_008.8;\nconst rad = (d: number) => (d * Math.PI) / 180;\n\n/** The great-circle distance between two positions, in metres (haversine). */\nexport function distanceMeters(a: LngLat, b: LngLat): number {\n const dLat = rad(b[1] - a[1]);\n const dLng = rad(b[0] - a[0]);\n const h = Math.sin(dLat / 2) ** 2 + Math.cos(rad(a[1])) * Math.cos(rad(b[1])) * Math.sin(dLng / 2) ** 2;\n return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)));\n}\n\n/** A circle of `radius` metres around `center` as a GeoJSON polygon (`steps` points): a delivery zone, a search radius. */\nexport function circlePolygon(center: LngLat, radius: number, steps = 64): GeoJSON.Feature<GeoJSON.Polygon> {\n const [lng, lat] = center;\n const d = radius / R;\n const ring: [number, number][] = [];\n for (let i = 0; i <= steps; i++) {\n const brg = (2 * Math.PI * i) / steps;\n const lat2 = Math.asin(Math.sin(rad(lat)) * Math.cos(d) + Math.cos(rad(lat)) * Math.sin(d) * Math.cos(brg));\n const lng2 = rad(lng) + Math.atan2(Math.sin(brg) * Math.sin(d) * Math.cos(rad(lat)), Math.cos(d) - Math.sin(rad(lat)) * Math.sin(lat2));\n ring.push([(lng2 * 180) / Math.PI, (lat2 * 180) / Math.PI]);\n }\n return { type: \"Feature\", properties: {}, geometry: { type: \"Polygon\", coordinates: [ring] } };\n}\n\n/** The Web Mercator pixel of a position at a zoom (256-px tiles): what a static map draws without an engine. */\nexport function project([lng, lat]: LngLat, zoom: number): [number, number] {\n const scale = 256 * 2 ** zoom;\n const s = Math.sin(rad(Math.max(-85.0511, Math.min(85.0511, lat))));\n return [((lng + 180) / 360) * scale, (0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI)) * scale];\n}\n\n/** A distance written the locale's way: metres under 1 km, kilometres (one decimal under 10) above. */\nexport function formatDistance(meters: number, locale: string): string {\n if (meters < 1000) return new Intl.NumberFormat(locale, { style: \"unit\", unit: \"meter\", maximumFractionDigits: 0 }).format(Math.round(meters / 10) * 10);\n return new Intl.NumberFormat(locale, { style: \"unit\", unit: \"kilometer\", maximumFractionDigits: meters < 10_000 ? 1 : 0 }).format(meters / 1000);\n}\n"
893
+ },
894
+ {
895
+ "path": "map/index.ts",
896
+ "content": "/**\n * @krizaka/ui/map — maps on MapLibre GL JS (open source) over Protomaps tiles on the Krizaka CDN, styled from the\n * tokens: `Map`, `Marker`, `Cluster`, `UserPosition`, `RadiusCircle`, `LocationPicker`, `StaticMap` (the fallback), the\n * style (`mapStyle`, `mapPalette`) and the geometry (`distanceMeters`, `circlePolygon`, `formatDistance`). maplibre-gl\n * and pmtiles are optional peers, loaded when a map is shown. Client (\"use client\").\n */\nexport { MAP_ATTRIBUTION, MAP_GLYPHS, MAP_TILES, mapAssets, setMapAssetsBase } from \"./assets\";\nexport { circlePolygon, distanceMeters, formatDistance, type LngLat, project } from \"./geo\";\nexport { LocationPicker, locationPicker, type LocationPickerLabels, locationPickerLabels, type LocationPickerProps } from \"./location-picker\";\nexport {\n Cluster,\n type ClusterPoint,\n type ClusterProps,\n configureMaps,\n Map,\n map,\n type MapLabels,\n mapLabels,\n type MapProps,\n Marker,\n type MarkerProps,\n RadiusCircle,\n type RadiusCircleProps,\n useMap,\n UserPosition,\n type UserPositionProps,\n} from \"./map\";\nexport { StaticMap, staticMap, type StaticMapPlace, type StaticMapProps } from \"./static-map\";\nexport { type MapBrand, mapFlavor, mapPalette, type MapRegion, mapStyle, type MapStyleOptions, mix, REGION_FROM_ZOOM } from \"./style\";\n"
897
+ },
898
+ {
899
+ "path": "map/location-picker.tsx",
900
+ "content": "// Client. A place chosen by moving the map under a fixed pin — a meeting point, a delivery address, the centre of a\n// search — with its distance from where the person is (a chip) and \"near me\": the browser asks for the position only when\n// the button is pressed, and every answer has its words (locating, refused, unavailable). The value is a position.\nimport * as React from \"react\";\n\nimport { tv } from \"../tv\";\nimport { distanceMeters, formatDistance, type LngLat } from \"./geo\";\nimport { Map, type MapProps, RadiusCircle, UserPosition } from \"./map\";\n\nexport const locationPicker = tv({\n slots: {\n root: \"flex flex-col gap-2\",\n frame: \"relative\",\n pin: \"pointer-events-none absolute left-1/2 top-1/2 z-10 -translate-x-1/2 -translate-y-full text-accent drop-shadow-md\",\n bar: \"absolute inset-x-3 bottom-8 z-10 flex flex-wrap items-center justify-between gap-2\",\n chip: \"rounded-full border border-border-default bg-surface-1 px-3 py-1.5 text-xs font-semibold tabular-nums text-fg shadow-floating\",\n near:\n \"inline-flex h-10 items-center gap-2 rounded-full border border-border-default bg-surface-1 bg-sheen px-4 text-sm font-semibold text-fg shadow-floating inset-shadow-edge \" +\n \"transition-colors hover:border-border-strong focus-visible:outline-hidden focus-visible:focus-ring disabled:opacity-60\",\n status: \"text-xs text-fg-secondary data-[tone=danger]:text-fg-danger\",\n },\n});\n\nexport type LocationPickerLabels = {\n /** The button that centres the map on the person (\"Near me\"). */\n nearMe: string;\n /** While the position is asked (\"Finding you…\"). */\n locating: string;\n /** The person refused (\"Location is off for this site. Move the map instead.\"). */\n denied: string;\n /** No position (no GPS, a timeout) (\"We could not find you. Move the map instead.\"). */\n unavailable: string;\n /** The person's place in the list of places (\"You are here\"). */\n you: string;\n /** The distance chip, from a written distance (\"1.2 km from you\"). */\n distance: (distance: string) => string;\n};\n\nexport const locationPickerLabels: LocationPickerLabels = {\n nearMe: \"Near me\",\n locating: \"Finding you…\",\n denied: \"Location is off for this site. Move the map instead.\",\n unavailable: \"We could not find you. Move the map instead.\",\n you: \"You are here\",\n distance: (d) => `${d} from you`,\n};\n\nexport type LocationPickerProps = Pick<MapProps, \"scheme\" | \"brand\" | \"lang\" | \"region\" | \"world\" | \"fallback\" | \"className\"> & {\n /** The picker's accessible name (\"Meeting point\"), passed translated. */\n label: string;\n /** The chosen position (controlled): the centre of the map. */\n value: LngLat;\n /** Called when the map rests on a new position. */\n onValueChange: (value: LngLat) => void;\n /** The BCP 47 locale of the distance. */\n locale: string;\n /** The zoom at first. Default 14. */\n zoom?: number;\n /** A radius in metres drawn around the chosen position (a delivery zone, a search radius). */\n radius?: number;\n /** Where the person is, when the app already knows it (the distance chip shows at once). */\n origin?: LngLat | null;\n /** The words, passed translated; English by default. */\n labels?: Partial<LocationPickerLabels>;\n};\n\ntype Status = \"idle\" | \"locating\" | \"denied\" | \"unavailable\";\n\n/** A position picked by moving the map under a pin; its distance from the person; \"near me\" on request. */\nexport function LocationPicker({ label, value, onValueChange, locale, zoom = 14, radius, origin: originProp = null, labels: given, className, ...map }: LocationPickerProps) {\n const labels = { ...locationPickerLabels, ...given };\n const s = locationPicker();\n const [origin, setOrigin] = React.useState<LngLat | null>(originProp);\n const [camera, setCamera] = React.useState<LngLat>(value);\n const [status, setStatus] = React.useState<Status>(\"idle\");\n const statusId = React.useId();\n const nearMe = () => {\n if (typeof navigator === \"undefined\" || !navigator.geolocation) return setStatus(\"unavailable\");\n setStatus(\"locating\");\n navigator.geolocation.getCurrentPosition(\n (p) => {\n const here: LngLat = [p.coords.longitude, p.coords.latitude];\n setOrigin(here);\n setCamera(here);\n onValueChange(here);\n setStatus(\"idle\");\n },\n (e) => setStatus(e.code === 1 ? \"denied\" : \"unavailable\"),\n { enableHighAccuracy: false, timeout: 10_000, maximumAge: 60_000 },\n );\n };\n const distance = origin ? formatDistance(distanceMeters(origin, value), locale) : null;\n const message = status === \"locating\" ? labels.locating : status === \"denied\" ? labels.denied : status === \"unavailable\" ? labels.unavailable : \"\";\n return (\n <div className={s.root({ className })} role=\"group\" aria-label={label}>\n <div className={s.frame()}>\n <Map {...map} label={label} center={camera} zoom={zoom} controlled onMoveEnd={(v) => onValueChange(v.center)} className=\"h-80\">\n {radius ? <RadiusCircle center={value} radius={radius} /> : null}\n {origin ? <UserPosition position={origin} label={labels.you} /> : null}\n </Map>\n <svg aria-hidden viewBox=\"0 0 24 32\" className={s.pin()} width=\"30\" height=\"40\">\n <path d=\"M12 0C5.4 0 0 5.2 0 11.7 0 20.4 12 32 12 32s12-11.6 12-20.3C24 5.2 18.6 0 12 0Z\" fill=\"currentColor\" />\n <circle cx=\"12\" cy=\"11.5\" r=\"4.5\" fill=\"var(--kz-on-accent)\" />\n </svg>\n <div className={s.bar()}>\n {distance ? (\n <span className={s.chip()} aria-live=\"polite\">\n {labels.distance(distance)}\n </span>\n ) : (\n <span />\n )}\n <button type=\"button\" className={s.near()} onClick={nearMe} disabled={status === \"locating\"} aria-describedby={message ? statusId : undefined}>\n <svg aria-hidden viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"3.5\" />\n <path d=\"M12 2v3M12 19v3M2 12h3M19 12h3\" />\n </svg>\n {status === \"locating\" ? labels.locating : labels.nearMe}\n </button>\n </div>\n </div>\n <p id={statusId} className={s.status()} data-tone={status === \"denied\" || status === \"unavailable\" ? \"danger\" : undefined} aria-live=\"polite\">\n {message}\n </p>\n </div>\n );\n}\n"
901
+ },
902
+ {
903
+ "path": "map/map.tsx",
904
+ "content": "// Client. A map of the Krizaka family on MapLibre GL JS (open source) over Protomaps tiles on our CDN — no API key, no\n// metered service. MapLibre and PMTiles are optional peers, loaded when a map is shown (a dynamic import: a page without\n// a map never pays for them). The style comes from the tokens (`mapStyle`), in the page's scheme, and follows it.\n//\n// What every map does: the canvas is decorative for assistive technologies — the map is a region named by `label`, its\n// places a list read in order (the text alternative, `MapPlaces`), each marker a button when it does something; the\n// attribution of OpenStreetMap (ODbL) and Protomaps is always shown; under reduced motion the camera jumps, never flies;\n// without WebGL (or while the engine loads) `fallback` stands in its place.\n/// <reference types=\"geojson\" />\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { usePrefersReducedMotion } from \"../media\";\nimport { tv } from \"../tv\";\nimport { MAP_ATTRIBUTION, setMapAssetsBase } from \"./assets\";\nimport { circlePolygon, type LngLat } from \"./geo\";\nimport { type MapBrand, mapPalette, type MapRegion, mapStyle } from \"./style\";\n\ntype MapLibre = typeof import(\"maplibre-gl\");\ntype MapInstance = import(\"maplibre-gl\").Map;\n\nexport const map = tv({\n slots: {\n root: \"relative isolate overflow-hidden rounded-2xl border border-border-default bg-surface-2 shadow-raised\",\n canvas: \"absolute inset-0 [&_.maplibregl-canvas]:outline-hidden\",\n fallback: \"absolute inset-0\",\n controls: \"absolute right-3 top-3 z-10 flex flex-col overflow-hidden rounded-xl border border-border-default bg-surface-1 shadow-floating\",\n control:\n \"inline-flex h-10 w-10 items-center justify-center text-lg font-semibold text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:focus-ring-inset disabled:opacity-40 [&+&]:border-t [&+&]:border-border-subtle\",\n attribution:\n \"absolute bottom-1.5 right-1.5 z-10 rounded-md bg-surface-1/85 px-1.5 py-0.5 text-[10px] text-fg-secondary backdrop-blur-sm [&_a]:underline-offset-2 hover:[&_a]:underline\",\n places: \"sr-only\",\n marker:\n \"group relative flex -translate-y-1/2 cursor-pointer items-center justify-center rounded-full border-2 border-on-accent bg-accent shadow-floating outline-hidden \" +\n \"transition-transform hover:scale-110 focus-visible:focus-ring motion-reduce:transition-none motion-reduce:hover:scale-100 data-[selected]:scale-125\",\n markerLabel:\n \"pointer-events-none absolute bottom-full mb-2 whitespace-nowrap rounded-lg border border-border-default bg-surface-1 px-2 py-1 text-xs font-semibold text-fg shadow-floating\",\n },\n});\n\ntype Registry = { add: (id: string, text: string) => void; remove: (id: string) => void };\ntype Ctx = { map: MapInstance | null; lib: MapLibre | null; ready: boolean; reduce: boolean; places: Registry };\nconst MapContext = React.createContext<Ctx | null>(null);\n\n/** The map a child is drawn on (`Marker`, `RadiusCircle`, `Cluster`…): the MapLibre instance once it is ready. */\nexport function useMap(): Ctx {\n const ctx = React.useContext(MapContext);\n if (!ctx) throw new Error(\"useMap: inside a <Map> only\");\n return ctx;\n}\n\n/** The scheme of the page (`html.light`), followed as it changes. */\nfunction usePageScheme(): \"dark\" | \"light\" {\n const read = () => (typeof document !== \"undefined\" && document.documentElement.classList.contains(\"light\") ? \"light\" : \"dark\");\n const [scheme, setScheme] = React.useState<\"dark\" | \"light\">(read);\n React.useEffect(() => {\n const observer = new MutationObserver(() => setScheme(read()));\n observer.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] });\n return () => observer.disconnect();\n }, []);\n return scheme;\n}\n\nlet protocolAdded = false;\nlet workerUrl: string | null = null;\n\n/**\n * Where MapLibre's worker comes from. A bundler (Next.js, Vite) renames MapLibre's module and leaves its worker behind:\n * by default the worker of the very version the app installed is imported from jsDelivr (free, open source, no key)\n * through a module worker of the page's own origin. An app with a strict CSP serves it itself —\n * `configureMaps({ workerUrl: \"/maplibre-gl-worker.mjs\" })` (with `maplibre-gl-shared.mjs` beside it), once, before a map.\n * `assetsBase` reads the tiles and glyphs through a same-origin proxy of the CDN (`setMapAssetsBase`).\n */\nexport function configureMaps(options: { workerUrl?: string; assetsBase?: string }) {\n if (options.workerUrl) workerUrl = options.workerUrl;\n if (options.assetsBase) setMapAssetsBase(options.assetsBase);\n}\n\n/** MapLibre, its worker and the PMTiles protocol, loaded once for the page. */\nasync function loadEngine(): Promise<MapLibre> {\n const [lib, pmtiles] = await Promise.all([import(\"maplibre-gl\"), import(\"pmtiles\")]);\n const ml = ((lib as unknown as { default?: MapLibre }).default ?? lib) as MapLibre;\n if (!protocolAdded) {\n protocolAdded = true;\n const remote = `https://cdn.jsdelivr.net/npm/maplibre-gl@${ml.getVersion()}/dist/maplibre-gl-worker.mjs`;\n ml.setWorkerUrl(workerUrl ?? URL.createObjectURL(new Blob([`import ${JSON.stringify(remote)};`], { type: \"text/javascript\" })));\n ml.addProtocol(\"pmtiles\", new pmtiles.Protocol().tile);\n }\n return ml;\n}\n\nconst hasWebGL = () => {\n try {\n const canvas = document.createElement(\"canvas\");\n return Boolean(canvas.getContext(\"webgl2\") ?? canvas.getContext(\"webgl\"));\n } catch {\n return false;\n }\n};\n\nexport type MapLabels = {\n /** The zoom-in button (\"Zoom in\"). */\n zoomIn: string;\n /** The zoom-out button (\"Zoom out\"). */\n zoomOut: string;\n /** Before the list of places read in place of the map (\"Places on the map\"). */\n places: string;\n};\n\nexport const mapLabels: MapLabels = { zoomIn: \"Zoom in\", zoomOut: \"Zoom out\", places: \"Places on the map\" };\n\nexport type MapProps = {\n /** The map's accessible name (\"Pick-up points near you\"), passed translated. */\n label: string;\n /** Where the map looks at first: longitude, latitude. */\n center: LngLat;\n /** The zoom at first (0 the world, 12 a city, 16 a street). Default 12. */\n zoom?: number;\n /** The camera follows `center` and `zoom` when they change (flies, or jumps under reduced motion). */\n controlled?: boolean;\n /** The scheme. Default: the page's (`html.light`), followed. */\n scheme?: \"dark\" | \"light\";\n /** The product whose accent the map carries. Default `krizaka`. */\n brand?: MapBrand;\n /** The language of the labels (`fr`, `en`). Default `en`. */\n lang?: string;\n /** The detailed region (PMTiles) drawn from zoom 6.5. Default: our Montréal–Québec extract (to zoom 14). */\n region?: MapRegion | null;\n /** The world's low zooms (PMTiles). Default: our world extract (to zoom 6). */\n world?: MapRegion | null;\n /** The zoom buttons (default) — the keyboard also zooms with + and − on the focused map. */\n controls?: boolean;\n /** Moves with the hand (default); `false` makes a still picture of a place. */\n interactive?: boolean;\n /** Shown in place of the map while its engine loads, and for good without WebGL: a `StaticMap`, a picture. */\n fallback?: React.ReactNode;\n /** Called when the camera rests: its centre and zoom. */\n onMoveEnd?: (view: { center: LngLat; zoom: number }) => void;\n /** Called with the position pressed on the map (not on a marker). */\n onPress?: (position: LngLat) => void;\n /** Markers, circles, clusters, the user's position — and anything drawn over the map. */\n children?: React.ReactNode;\n /** The words, passed translated; English by default. */\n labels?: Partial<MapLabels>;\n /** Classes merged on the root (its height: `h-80`, `aspect-video`). */\n className?: string;\n};\n\n/** A map: MapLibre over our Protomaps tiles, styled from the tokens, with its places as a list for screen readers. */\nexport function Map({\n label,\n center,\n zoom = 12,\n controlled,\n scheme: schemeProp,\n brand,\n lang,\n region,\n world,\n controls = true,\n interactive = true,\n fallback,\n onMoveEnd,\n onPress,\n children,\n labels: given,\n className,\n}: MapProps) {\n const labels = { ...mapLabels, ...given };\n const s = map();\n const pageScheme = usePageScheme();\n const scheme = schemeProp ?? pageScheme;\n const reduce = usePrefersReducedMotion();\n const host = React.useRef<HTMLDivElement>(null);\n const [instance, setInstance] = React.useState<MapInstance | null>(null);\n const [lib, setLib] = React.useState<MapLibre | null>(null);\n const [ready, setReady] = React.useState(false);\n const [failed, setFailed] = React.useState(false);\n const [places, setPlaces] = React.useState<Record<string, string>>({});\n const registry = React.useMemo<Registry>(\n () => ({\n add: (id, text) => setPlaces((p) => (p[id] === text ? p : { ...p, [id]: text })),\n remove: (id) =>\n setPlaces((p) => {\n const { [id]: _gone, ...rest } = p;\n return rest;\n }),\n }),\n [],\n );\n const latest = React.useRef({ onMoveEnd, onPress });\n React.useEffect(() => {\n latest.current = { onMoveEnd, onPress };\n });\n const styleOf = React.useCallback((sc: \"dark\" | \"light\") => mapStyle({ scheme: sc, brand, lang, ...(region !== undefined ? { region } : {}), ...(world !== undefined ? { world } : {}) }), [brand, lang, region, world]);\n\n // The engine and the map, once (the style follows the scheme below).\n React.useEffect(() => {\n if (!host.current) return;\n let alive = true;\n let created: MapInstance | null = null;\n (hasWebGL() ? loadEngine() : Promise.reject(new Error(\"no WebGL\")))\n .then((ml) => {\n if (!alive || !host.current) return;\n created = new ml.Map({\n container: host.current,\n style: styleOf(scheme) as never,\n center: [...center] as [number, number],\n zoom,\n interactive,\n attributionControl: false,\n fadeDuration: reduce ? 0 : 300,\n cooperativeGestures: false,\n });\n created.getCanvas().setAttribute(\"aria-hidden\", \"true\");\n created.getCanvas().setAttribute(\"tabindex\", \"-1\");\n created.on(\"load\", () => alive && setReady(true));\n // The basemap cannot be read (offline, a blocked request): the fallback comes back rather than an empty map.\n created.on(\"error\", (e) => {\n const source = (e as { sourceId?: string }).sourceId;\n if (alive && (source === \"world\" || source === \"region\")) setFailed(true);\n });\n created.on(\"moveend\", () => {\n const c = created!.getCenter();\n latest.current.onMoveEnd?.({ center: [c.lng, c.lat], zoom: created!.getZoom() });\n });\n created.on(\"click\", (e) => {\n if ((e.originalEvent.target as Element | null)?.closest?.(\"[data-kz-marker]\")) return;\n latest.current.onPress?.([e.lngLat.lng, e.lngLat.lat]);\n });\n setLib(ml);\n setInstance(created);\n })\n .catch(() => alive && setFailed(true));\n return () => {\n alive = false;\n created?.remove();\n };\n // Created once: the props that change afterwards are followed by the effects below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // The scheme (and the brand, the language) change the style, not the map.\n const first = React.useRef(true);\n React.useEffect(() => {\n if (!instance) return;\n if (first.current) {\n first.current = false;\n return;\n }\n instance.setStyle(styleOf(scheme) as never);\n }, [instance, scheme, styleOf]);\n\n // A controlled camera: it flies (or jumps) to the new view.\n React.useEffect(() => {\n if (!instance || !controlled) return;\n const view = { center: [...center] as [number, number], zoom };\n if (reduce) instance.jumpTo(view);\n else instance.flyTo({ ...view, speed: 1.4 });\n }, [instance, controlled, center[0], center[1], zoom, reduce]); // eslint-disable-line react-hooks/exhaustive-deps\n\n const zoomBy = (by: number) => instance && (reduce ? instance.setZoom(instance.getZoom() + by) : instance.zoomTo(instance.getZoom() + by, { duration: 250 }));\n const ctx = React.useMemo<Ctx>(() => ({ map: instance, lib, ready, reduce, places: registry }), [instance, lib, ready, reduce, registry]);\n const listed = Object.entries(places);\n return (\n <MapContext.Provider value={ctx}>\n <section\n aria-label={label}\n className={s.root({ className })}\n data-ready={ready ? \"\" : undefined}\n tabIndex={interactive ? 0 : undefined}\n onKeyDown={(e) => {\n if (e.target !== e.currentTarget || !instance) return;\n if (e.key === \"+\" || e.key === \"=\") zoomBy(1);\n else if (e.key === \"-\") zoomBy(-1);\n else if (e.key.startsWith(\"Arrow\")) {\n e.preventDefault();\n const step = 80;\n const by: Record<string, [number, number]> = { ArrowUp: [0, -step], ArrowDown: [0, step], ArrowLeft: [-step, 0], ArrowRight: [step, 0] };\n instance.panBy(by[e.key]!, { duration: reduce ? 0 : 200 });\n }\n }}\n >\n {!ready || failed ? <div className={s.fallback()}>{fallback}</div> : null}\n <div ref={host} className={s.canvas()} style={{ position: \"absolute\", inset: 0, visibility: failed ? \"hidden\" : undefined }} />\n {controls && interactive && !failed ? (\n <div className={s.controls()}>\n <button type=\"button\" className={s.control()} aria-label={labels.zoomIn} disabled={!instance} onClick={() => zoomBy(1)}>\n +\n </button>\n <button type=\"button\" className={s.control()} aria-label={labels.zoomOut} disabled={!instance} onClick={() => zoomBy(-1)}>\n −\n </button>\n </div>\n ) : null}\n <p className={s.attribution()}>\n <a href={MAP_ATTRIBUTION.protomapsUrl} target=\"_blank\" rel=\"noreferrer\">\n {MAP_ATTRIBUTION.protomaps}\n </a>\n {\" · \"}\n <a href={MAP_ATTRIBUTION.osmUrl} target=\"_blank\" rel=\"noreferrer\">\n {MAP_ATTRIBUTION.osm}\n </a>\n </p>\n {listed.length ? (\n <div className={s.places()}>\n <p>{labels.places}</p>\n <ul>\n {listed.map(([id, text]) => (\n <li key={id}>{text}</li>\n ))}\n </ul>\n </div>\n ) : null}\n {children}\n </section>\n </MapContext.Provider>\n );\n}\n\nexport type MarkerProps = {\n /** Where it stands. */\n position: LngLat;\n /** Its words: the place's name, read in the list of places and as the button's name (\"Café Olimpico, 350 m\"). */\n label: string;\n /** Pressing it (details in a panel, a sheet): the marker becomes a button. */\n onPress?: () => void;\n /** The chosen one: larger, its label shown above it. */\n selected?: boolean;\n /** Its size in pixels. Default 18. */\n size?: number;\n /** What it draws instead of the accent dot (an avatar, an icon). */\n children?: React.ReactNode;\n};\n\n/** A place on the map: an accent dot (or your content) that is a button when it does something. */\nexport function Marker({ position, label, onPress, selected, size = 18, children }: MarkerProps) {\n const { map: m, lib, places } = useMap();\n const s = map();\n const id = React.useId();\n const [el] = React.useState(() => (typeof document === \"undefined\" ? null : document.createElement(\"div\")));\n React.useEffect(() => {\n places.add(id, label);\n return () => places.remove(id);\n }, [id, label, places]);\n React.useEffect(() => {\n if (!m || !lib || !el) return;\n el.setAttribute(\"data-kz-marker\", \"\");\n const marker = new lib.Marker({ element: el, anchor: \"center\" }).setLngLat([...position] as [number, number]).addTo(m);\n return () => void marker.remove();\n }, [m, lib, el, position[0], position[1]]); // eslint-disable-line react-hooks/exhaustive-deps\n if (!el) return null;\n const Tag = onPress ? \"button\" : \"span\";\n return createPortal(\n <Tag\n type={onPress ? \"button\" : undefined}\n aria-label={onPress ? label : undefined}\n aria-hidden={onPress ? undefined : true}\n aria-pressed={onPress && selected !== undefined ? selected : undefined}\n data-selected={selected ? \"\" : undefined}\n className={s.marker()}\n style={children ? undefined : { width: size, height: size }}\n onClick={onPress}\n >\n {children}\n {selected ? <span className={s.markerLabel()}>{label}</span> : null}\n </Tag>,\n el,\n );\n}\n\nexport type UserPositionProps = {\n /** Where the person is. */\n position: LngLat;\n /** Its words (\"You are here\"), read in the list of places. */\n label: string;\n /** The accuracy in metres: a soft disc around the dot. */\n accuracy?: number;\n};\n\n/** The person's position: a dot that pulses (still under reduced motion), its accuracy as a soft disc. */\nexport function UserPosition({ position, label, accuracy }: UserPositionProps) {\n const { map: m, lib, places } = useMap();\n const id = React.useId();\n React.useEffect(() => {\n places.add(id, label);\n return () => places.remove(id);\n }, [id, label, places]);\n React.useEffect(() => {\n if (!m || !lib) return;\n const el = document.createElement(\"div\");\n el.className = \"kz-map-user\";\n el.setAttribute(\"aria-hidden\", \"true\");\n const marker = new lib.Marker({ element: el, anchor: \"center\" }).setLngLat([...position] as [number, number]).addTo(m);\n return () => void marker.remove();\n }, [m, lib, position[0], position[1]]); // eslint-disable-line react-hooks/exhaustive-deps\n return accuracy ? <RadiusCircle center={position} radius={accuracy} tone=\"soft\" /> : null;\n}\n\n/** Adds a GeoJSON source and its layers while the map has a style (again after a change of scheme). */\nfunction useGeoLayer(id: string, data: GeoJSON.GeoJSON, layers: (palette: ReturnType<typeof mapPalette>) => object[], source: object = {}) {\n const { map: m, ready } = useMap();\n const dataRef = React.useRef(data);\n React.useEffect(() => {\n dataRef.current = data;\n const src = m?.getSource(id) as { setData?: (d: GeoJSON.GeoJSON) => void } | undefined;\n src?.setData?.(data);\n }, [m, id, data]);\n React.useEffect(() => {\n if (!m || !ready) return;\n const add = () => {\n if (m.getSource(id)) return;\n const scheme = document.documentElement.classList.contains(\"light\") ? \"light\" : \"dark\";\n m.addSource(id, { type: \"geojson\", data: dataRef.current, ...source } as never);\n for (const layer of layers(mapPalette(scheme))) m.addLayer(layer as never);\n };\n add();\n m.on(\"styledata\", add);\n return () => {\n m.off(\"styledata\", add);\n if (!m.getStyle()) return;\n for (const layer of m.getStyle().layers.filter((l) => \"source\" in l && l.source === id)) m.removeLayer(layer.id);\n if (m.getSource(id)) m.removeSource(id);\n };\n }, [m, ready, id]); // eslint-disable-line react-hooks/exhaustive-deps\n}\n\nexport type RadiusCircleProps = {\n /** Its centre. */\n center: LngLat;\n /** Its radius in metres. */\n radius: number;\n /** `accent` (a zone that matters: a delivery area, a search radius) or `soft` (an accuracy). */\n tone?: \"accent\" | \"soft\";\n};\n\n/** A circle on the ground: a delivery zone, a search radius — filled with the accent, its edge drawn. */\nexport function RadiusCircle({ center, radius, tone = \"accent\" }: RadiusCircleProps) {\n const id = `kz-circle-${React.useId().replace(/[^a-zA-Z0-9]/g, \"\")}`;\n const data = React.useMemo(() => circlePolygon(center, radius), [center[0], center[1], radius]); // eslint-disable-line react-hooks/exhaustive-deps\n useGeoLayer(id, data, (c) => [\n { id: `${id}-fill`, type: \"fill\", source: id, paint: { \"fill-color\": c.accent, \"fill-opacity\": tone === \"soft\" ? 0.1 : 0.14 } },\n { id: `${id}-line`, type: \"line\", source: id, paint: { \"line-color\": c.accent, \"line-width\": tone === \"soft\" ? 1 : 2, \"line-opacity\": tone === \"soft\" ? 0.4 : 0.9, ...(tone === \"accent\" ? { \"line-dasharray\": [2, 1.5] } : {}) } },\n ]);\n return null;\n}\n\nexport type ClusterPoint = { id: string; position: LngLat; label: string };\n\nexport type ClusterProps = {\n /** The points: many of them (creators, pick-up points, events). */\n points: readonly ClusterPoint[];\n /** Pressing a single point. A group zooms in to open itself. */\n onPointPress?: (point: ClusterPoint) => void;\n /** The radius, in pixels, within which points group. Default 48. */\n radius?: number;\n};\n\n/** Many points grouped while they are close: a disc with the count, opened by zooming in; each point is listed for screen readers. */\nexport function Cluster({ points, onPointPress, radius = 48 }: ClusterProps) {\n const { map: m, ready, reduce, places } = useMap();\n const id = `kz-cluster-${React.useId().replace(/[^a-zA-Z0-9]/g, \"\")}`;\n const data = React.useMemo<GeoJSON.FeatureCollection>(\n () => ({ type: \"FeatureCollection\", features: points.map((p) => ({ type: \"Feature\", properties: { id: p.id, label: p.label }, geometry: { type: \"Point\", coordinates: [...p.position] } })) }),\n [points],\n );\n React.useEffect(() => {\n for (const p of points) places.add(`${id}-${p.id}`, p.label);\n return () => points.forEach((p) => places.remove(`${id}-${p.id}`));\n }, [points, places, id]);\n useGeoLayer(\n id,\n data,\n (c) => [\n { id: `${id}-groups`, type: \"circle\", source: id, filter: [\"has\", \"point_count\"], paint: { \"circle-color\": c.accent, \"circle-opacity\": 0.92, \"circle-radius\": [\"step\", [\"get\", \"point_count\"], 16, 10, 20, 50, 26], \"circle-stroke-width\": 3, \"circle-stroke-color\": c.halo } },\n { id: `${id}-count`, type: \"symbol\", source: id, filter: [\"has\", \"point_count\"], layout: { \"text-field\": [\"get\", \"point_count_abbreviated\"], \"text-font\": [\"Noto Sans Medium\"], \"text-size\": 13 }, paint: { \"text-color\": c.onAccent } },\n { id: `${id}-points`, type: \"circle\", source: id, filter: [\"!\", [\"has\", \"point_count\"]], paint: { \"circle-color\": c.accent, \"circle-radius\": 7, \"circle-stroke-width\": 2.5, \"circle-stroke-color\": c.halo } },\n ],\n { cluster: true, clusterRadius: radius, clusterMaxZoom: 15 },\n );\n React.useEffect(() => {\n if (!m || !ready) return;\n const openGroup = async (e: { features?: { properties: Record<string, unknown>; geometry: GeoJSON.Geometry }[] }) => {\n const f = e.features?.[0];\n if (!f) return;\n const src = m.getSource(id) as unknown as { getClusterExpansionZoom: (id: number) => Promise<number> };\n const z = await src.getClusterExpansionZoom(f.properties.cluster_id as number);\n const center = (f.geometry as GeoJSON.Point).coordinates as [number, number];\n if (reduce) m.jumpTo({ center, zoom: z });\n else m.easeTo({ center, zoom: z });\n };\n const pressPoint = (e: { features?: { properties: Record<string, unknown> }[] }) => {\n const pid = e.features?.[0]?.properties.id;\n const point = points.find((p) => p.id === pid);\n if (point) onPointPress?.(point);\n };\n m.on(\"click\", `${id}-groups`, openGroup as never);\n m.on(\"click\", `${id}-points`, pressPoint as never);\n return () => {\n m.off(\"click\", `${id}-groups`, openGroup as never);\n m.off(\"click\", `${id}-points`, pressPoint as never);\n };\n }, [m, ready, id, points, onPointPress, reduce]);\n return null;\n}\n"
905
+ },
906
+ {
907
+ "path": "map/meta.ts",
908
+ "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Map\",\n summary:\n \"Maps that cost nothing per view — MapLibre (open source) over Protomaps tiles on the Krizaka CDN, painted from the tokens in dark or light and in the product's accent — with markers, clusters, the person's position, a radius and a location picker.\",\n status: \"beta\",\n category: \"data-display\",\n platforms: \"both\",\n whenToUse: [\n \"To show where things are: pick-up points, creators near you, an event's venue.\",\n \"`Cluster` for many points (dozens to thousands): they group while close and open by zooming in.\",\n \"`LocationPicker` to choose a place: a meeting point, a delivery address, the centre of a search with its radius.\",\n \"`StaticMap` where a live map is too much (a card, a list, an e-mail) and as the `fallback` of every `Map`.\",\n ],\n whenNotToUse: [\n { when: \"For an address typed or searched: a text field (geocoding is a server's job — Nominatim or Photon self-hosted later), the map only confirms it.\", use: \"field\" },\n { when: \"For figures by country or region (a choropleth): a chart.\", use: \"stat\" },\n ],\n bestPractices: [\n \"Give every map a `label` and every marker a `label`: they are read as the list of places in place of the map.\",\n \"Always pass a `fallback` (`StaticMap` with the same centre and places): it shows while the engine loads and for good without WebGL.\",\n \"Ask for the position only on a press (`LocationPicker`'s Near me), never on load; every refusal has its words.\",\n \"Keep the attribution visible (OpenStreetMap's licence, the ODbL, asks for it): the components draw it.\",\n \"Install the engine only where a map is shown: `maplibre-gl` (web) or `@maplibre/maplibre-react-native` (Expo) — optional peers.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Tab\", action: \"The map (a region named by `label`), then its buttons: zoom, the markers that do something, Near me.\" },\n { keys: \"Arrow keys\", action: \"Pan the focused map.\" },\n { keys: \"+ / −\", action: \"Zoom the focused map in or out.\" },\n ],\n notes: [\n \"The canvas is hidden from assistive technologies: the places are read as a list (each `Marker`, `Cluster` point and the person's position by its `label`).\",\n \"A marker with `onPress` is a button named by its label (`aria-pressed` when it can be chosen); the chosen one shows its name above it.\",\n \"Under reduced motion the camera jumps instead of flying and the position's pulse stands still.\",\n \"Colours come from the tokens: the water is the accent softened, roads and labels the text roles — readable in both schemes.\",\n ],\n },\n related: [\"dialog\", \"search-field\", \"chip\"],\n web: {\n imports: [\"Map\", \"Marker\", \"Cluster\", \"UserPosition\", \"RadiusCircle\", \"LocationPicker\", \"StaticMap\", \"mapStyle\", \"distanceMeters\", \"formatDistance\"],\n examples: [\n { name: \"places\", title: \"Places\", description: \"Pick-up points in Montréal: markers are buttons, the chosen one shows its name.\" },\n { name: \"cluster\", title: \"Cluster\", description: \"Sixty creators grouped while they are close; a group opens by zooming in.\" },\n { name: \"location-picker\", title: \"Location picker\", description: \"A meeting point under the pin, its distance from the person, a 1-km zone and Near me.\" },\n { name: \"static\", title: \"Static map\", description: \"The places on the map's paper, without an engine: a card, a list item, a fallback.\" },\n ],\n },\n native: {\n entry: \"@krizaka/ui/native/map\",\n imports: [\"Map\", \"Marker\", \"Cluster\", \"UserPosition\", \"RadiusCircle\", \"LocationPicker\"],\n differences: [\n \"MapLibre Native (`@maplibre/maplibre-react-native`, an optional peer, with its Expo config plugin): the same style and tiles as the web — it reads `pmtiles://` itself.\",\n \"A marker's details open in a `Sheet` (`@krizaka/ui/native`); markers and Near me play a haptic.\",\n \"Near me asks the system's permission through MapLibre's `LocationManager` when pressed; the app declares the location usage in its config.\",\n \"The map's name lists its places for VoiceOver and TalkBack; the screenshots of these stories are drawn by a stand-in (the markers placed, not the tiles).\",\n ],\n examples: [\n { name: \"places\", title: \"Places\", description: \"Pick-up points: a marker pressed opens its details in a bottom sheet.\" },\n { name: \"location-picker\", title: \"Location picker\", description: \"A meeting point under the pin; Near me on request.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
909
+ },
910
+ {
911
+ "path": "map/static-map.tsx",
912
+ "content": "// Server-safe. A still map drawn without an engine: the places projected (Web Mercator) on the map's paper — or on a\n// picture of the area (`image`, a snapshot of the real map) — in the page's tokens. What a `Map` shows while its engine\n// loads and for good without WebGL; a picture for an e-mail, a card, a list item. An image (`role=\"img\"`) named by its\n// places.\n\n\nimport { tv } from \"../tv\";\nimport { MAP_ATTRIBUTION } from \"./assets\";\nimport { type LngLat, project } from \"./geo\";\n\nexport const staticMap = tv({\n slots: {\n root: \"relative block h-full w-full overflow-hidden bg-surface-2\",\n svg: \"absolute inset-0 h-full w-full\",\n attribution: \"absolute bottom-1.5 right-1.5 rounded-md bg-surface-1/85 px-1.5 py-0.5 text-[10px] text-fg-secondary\",\n },\n});\n\nexport type StaticMapPlace = { position: LngLat; label: string };\n\nexport type StaticMapProps = {\n /** What the picture shows, in words (\"Three pick-up points in the Plateau\"), passed translated: its accessible name. */\n label: string;\n /** The centre of the picture. */\n center: LngLat;\n /** The zoom of the picture. Default 12. */\n zoom?: number;\n /** The places drawn on it (an accent dot each), listed in its name. */\n places?: readonly StaticMapPlace[];\n /** A snapshot of the real map of the area (same centre and zoom), drawn under the places; the attribution then shows. */\n image?: string;\n /** Classes merged on the root (its size). */\n className?: string;\n};\n\nconst W = 640;\nconst H = 400;\n\n/** A still map: the places on the map's paper (or on a snapshot), named in words. */\nexport function StaticMap({ label, center, zoom = 12, places = [], image, className }: StaticMapProps) {\n const s = staticMap();\n const [cx, cy] = project(center, zoom);\n const at = (p: LngLat) => {\n const [x, y] = project(p, zoom);\n return [W / 2 + x - cx, H / 2 + y - cy] as const;\n };\n const name = places.length ? `${label}: ${places.map((p) => p.label).join(\", \")}` : label;\n return (\n <span role=\"img\" aria-label={name} className={s.root({ className })}>\n <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio=\"xMidYMid slice\" className={s.svg()} aria-hidden>\n <defs>\n <pattern id=\"kz-map-paper\" width=\"48\" height=\"48\" patternUnits=\"userSpaceOnUse\">\n <path d=\"M48 0H0V48\" fill=\"none\" stroke=\"var(--kz-border-subtle)\" strokeWidth=\"1\" />\n </pattern>\n </defs>\n <rect width={W} height={H} fill=\"var(--kz-surface-2)\" />\n {image ? <image href={image} x=\"0\" y=\"0\" width={W} height={H} preserveAspectRatio=\"xMidYMid slice\" /> : <rect width={W} height={H} fill=\"url(#kz-map-paper)\" />}\n {places.map((p) => {\n const [x, y] = at(p.position);\n return (\n <g key={`${p.position[0]},${p.position[1]}`}>\n <circle cx={x} cy={y} r=\"12\" fill=\"var(--kz-accent)\" opacity=\"0.18\" />\n <circle cx={x} cy={y} r=\"7\" fill=\"var(--kz-accent)\" stroke=\"var(--kz-on-accent)\" strokeWidth=\"2.5\" />\n </g>\n );\n })}\n </svg>\n {image ? (\n <span aria-hidden className={s.attribution()}>\n {MAP_ATTRIBUTION.protomaps} · {MAP_ATTRIBUTION.osm}\n </span>\n ) : null}\n </span>\n );\n}\n\n"
913
+ },
914
+ {
915
+ "path": "map/style.ts",
916
+ "content": "// The Krizaka map style, generated from the tokens (no hand-written colour): Protomaps' basemap layers\n// (@protomaps/basemaps) painted with a flavour drawn from `@krizaka/tokens` — the surfaces for the land, the accent\n// softened for the water, the text roles for the labels — in dark or light, the product's accent where a brand asks.\n// Platform-neutral: the web `Map` and the native `Map` both read it.\nimport { brands, themes } from \"@krizaka/tokens/native\";\nimport { type Flavor, layers, namedFlavor } from \"@protomaps/basemaps\";\n\nimport { MAP_ATTRIBUTION, mapAssets } from \"./assets\";\n\n/** A product of the family: its accent paints the map. */\nexport type MapBrand = keyof typeof brands;\n\n/** A colour mixed into another (`#rrggbb` only): `t` = 0 gives `a`, 1 gives `b`. */\nexport function mix(a: string, b: string, t: number): string {\n const p = (h: string) => [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16));\n const [x, y] = [p(a), p(b)];\n return `#${x.map((v, i) => Math.round(v + (y[i]! - v) * t).toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\n/** The colours of the map in a scheme (and a brand): what the style, the markers and the static map share. */\nexport function mapPalette(scheme: \"dark\" | \"light\", brand: MapBrand = \"krizaka\") {\n const t = { ...themes[scheme], ...(brands as Record<string, Partial<Record<\"dark\" | \"light\", Partial<typeof themes.dark>>>>)[brand]?.[scheme] };\n const dark = scheme === \"dark\";\n const land = dark ? t.surface1 : t.surface0;\n return {\n land,\n background: dark ? t.surface0 : t.surface2,\n water: mix(land, t.accent, dark ? 0.18 : 0.14),\n park: mix(land, t.success, dark ? 0.1 : 0.12),\n road: dark ? mix(land, t.textPrimary, 0.12) : \"#ffffff\",\n roadMajor: dark ? mix(land, t.textPrimary, 0.2) : mix(land, t.textPrimary, 0.06),\n casing: mix(land, t.textPrimary, dark ? 0.05 : 0.12),\n building: mix(land, t.textPrimary, dark ? 0.06 : 0.07),\n boundary: mix(land, t.textPrimary, 0.3),\n label: t.textSecondary,\n labelStrong: t.textPrimary,\n halo: land,\n accent: t.accent,\n onAccent: t.onAccent,\n };\n}\n\n/** The basemap flavour of a palette: every role of Protomaps' layers filled from the tokens. */\nexport function mapFlavor(scheme: \"dark\" | \"light\", brand?: MapBrand): Flavor {\n const c = mapPalette(scheme, brand);\n const base = namedFlavor(scheme);\n return {\n ...base,\n background: c.background,\n earth: c.land,\n park_a: c.park,\n park_b: c.park,\n wood_a: c.park,\n wood_b: c.park,\n scrub_a: c.park,\n scrub_b: c.park,\n glacier: c.land,\n sand: c.land,\n beach: c.land,\n zoo: c.park,\n hospital: c.land,\n industrial: c.land,\n school: c.land,\n military: c.land,\n aerodrome: c.land,\n pedestrian: c.land,\n runway: c.building,\n water: c.water,\n buildings: c.building,\n pier: c.building,\n boundaries: c.boundary,\n railway: c.casing,\n other: c.road,\n minor_service: c.road,\n minor_a: c.road,\n minor_b: c.road,\n link: c.road,\n major: c.roadMajor,\n highway: c.roadMajor,\n bridges_other: c.road,\n bridges_minor: c.road,\n bridges_link: c.road,\n bridges_major: c.roadMajor,\n bridges_highway: c.roadMajor,\n tunnel_other: c.road,\n tunnel_minor: c.road,\n tunnel_link: c.road,\n tunnel_major: c.roadMajor,\n tunnel_highway: c.roadMajor,\n minor_service_casing: c.casing,\n minor_casing: c.casing,\n link_casing: c.casing,\n major_casing_late: c.casing,\n major_casing_early: c.casing,\n highway_casing_late: c.casing,\n highway_casing_early: c.casing,\n bridges_other_casing: c.casing,\n bridges_minor_casing: c.casing,\n bridges_link_casing: c.casing,\n bridges_major_casing: c.casing,\n bridges_highway_casing: c.casing,\n tunnel_other_casing: c.casing,\n tunnel_minor_casing: c.casing,\n tunnel_link_casing: c.casing,\n tunnel_major_casing: c.casing,\n tunnel_highway_casing: c.casing,\n roads_label_minor: c.label,\n roads_label_minor_halo: c.halo,\n roads_label_major: c.label,\n roads_label_major_halo: c.halo,\n ocean_label: c.label,\n subplace_label: c.label,\n subplace_label_halo: c.halo,\n city_label: c.labelStrong,\n city_label_halo: c.halo,\n state_label: c.label,\n state_label_halo: c.halo,\n country_label: c.label,\n address_label: c.label,\n address_label_halo: c.halo,\n regular: \"Noto Sans Regular\",\n bold: \"Noto Sans Medium\",\n italic: \"Noto Sans Italic\",\n // No point-of-interest icons (no sprite to host): the app's own markers carry the places that matter.\n pois: undefined,\n landcover: undefined,\n };\n}\n\n/** A region of the basemap: its PMTiles file and the zoom up to which it has tiles. */\nexport type MapRegion = { url: string; maxzoom: number };\n\nexport type MapStyleOptions = {\n /** `dark` or `light`: the scheme of the page or the app. */\n scheme: \"dark\" | \"light\";\n /** The product whose accent the map carries (its markers, its water's tint). Default `krizaka`. */\n brand?: MapBrand;\n /** The language of the labels (`fr`, `en`): the place names in it where OpenStreetMap has them. Default `en`. */\n lang?: string;\n /** The low zooms of the whole world. Default: our world extract (zoom 0–6). */\n world?: MapRegion | null;\n /** The detailed region drawn from zoom 7. Default: Montréal–Québec (up to 14, overzoomed beyond). */\n region?: MapRegion | null;\n /** The glyphs' URL template. Default: our Noto Sans on the CDN. */\n glyphs?: string;\n};\n\n/** The zoom where the detailed region takes over from the world. */\nexport const REGION_FROM_ZOOM = 6.5;\n\n/**\n * The MapLibre style of a Krizaka map (a `StyleSpecification`): the world's low zooms, then the detailed region,\n * painted from the tokens. The `pmtiles://` sources need the PMTiles protocol (the web `Map` registers it; MapLibre\n * Native reads it as it is).\n */\nexport function mapStyle({ scheme, brand, lang = \"en\", world = { url: mapAssets().tiles.world, maxzoom: 6 }, region = { url: mapAssets().tiles.quebec, maxzoom: 14 }, glyphs = mapAssets().glyphs }: MapStyleOptions) {\n const flavor = mapFlavor(scheme, brand);\n const attribution = `<a href=\"${MAP_ATTRIBUTION.protomapsUrl}\">${MAP_ATTRIBUTION.protomaps}</a> · <a href=\"${MAP_ATTRIBUTION.osmUrl}\">${MAP_ATTRIBUTION.osm}</a>`;\n const sources: Record<string, unknown> = {};\n const all: { id: string; minzoom?: number; maxzoom?: number; [k: string]: unknown }[] = [];\n if (world) {\n sources.world = { type: \"vector\", url: `pmtiles://${world.url}`, attribution };\n for (const l of layers(\"world\", flavor, { lang }) as typeof all) {\n if (l.id === \"background\") all.push(l);\n else all.push({ ...l, id: `world-${l.id}`, maxzoom: region ? Math.min(l.maxzoom ?? 24, REGION_FROM_ZOOM) : l.maxzoom });\n }\n }\n if (region) {\n sources.region = { type: \"vector\", url: `pmtiles://${region.url}`, attribution };\n for (const l of layers(\"region\", flavor, { lang }) as typeof all) {\n if (l.id === \"background\") {\n if (!world) all.push(l);\n continue;\n }\n all.push({ ...l, id: `region-${l.id}`, minzoom: world ? Math.max(l.minzoom ?? 0, REGION_FROM_ZOOM) : l.minzoom });\n }\n }\n // No sprite is hosted: a layer that needs an icon (points of interest) is left out — the app's markers carry the places.\n const drawn = all.filter((l) => !(l.layout as Record<string, unknown> | undefined)?.[\"icon-image\"]);\n return { version: 8 as const, glyphs, sources, layers: drawn.map((l) => (l.maxzoom === undefined ? withoutKey(l, \"maxzoom\") : l)) };\n}\n\nconst withoutKey = <T extends Record<string, unknown>>(o: T, key: string) => Object.fromEntries(Object.entries(o).filter(([k]) => k !== key)) as T;\n"
917
+ },
918
+ {
919
+ "path": "media.ts",
920
+ "content": "// Shared by the data and media primitives (client): the clock format, the reduced-motion preference, the\n// in-view observer, a debounced callback. Never an entry of its own.\nimport * as React from \"react\";\n\n/** Seconds as a clock: `75` → `1:15`, `3725` → `1:02:05`. Negative or unknown values read `0:00`. */\nexport function formatClock(seconds: number): string {\n const s = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0;\n const h = Math.floor(s / 3600);\n const m = Math.floor((s % 3600) / 60);\n const rest = String(s % 60).padStart(2, \"0\");\n return h > 0 ? `${h}:${String(m).padStart(2, \"0\")}:${rest}` : `${m}:${rest}`;\n}\n\n/** Clamps `value` between `min` and `max`. */\nexport const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n\nconst REDUCE = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReduce = (notify: () => void) => {\n if (typeof window === \"undefined\" || !window.matchMedia) return () => undefined;\n const query = window.matchMedia(REDUCE);\n query.addEventListener?.(\"change\", notify);\n return () => query.removeEventListener?.(\"change\", notify);\n};\n\n/** `true` when the person asked for less motion (`prefers-reduced-motion: reduce`); follows a change of the setting. */\nexport function usePrefersReducedMotion(): boolean {\n return React.useSyncExternalStore(\n subscribeReduce,\n () => (typeof window !== \"undefined\" && window.matchMedia ? window.matchMedia(REDUCE).matches : false),\n () => false,\n );\n}\n\n/** The latest value of a callback, for an effect or an event that must not re-subscribe when it changes. */\nexport function useLatest<T>(value: T) {\n const ref = React.useRef(value);\n React.useLayoutEffect(() => {\n ref.current = value;\n });\n return ref;\n}\n\n/** Calls `onChange(visible)` when at least `threshold` of the element enters or leaves the viewport. */\nexport function useInView(ref: React.RefObject<Element | null>, onChange: (visible: boolean) => void, enabled = true, threshold = 0.25) {\n const latest = useLatest(onChange);\n React.useEffect(() => {\n const element = ref.current;\n if (!enabled || !element || typeof IntersectionObserver === \"undefined\") return;\n const observer = new IntersectionObserver(([entry]) => entry && latest.current(entry.isIntersecting), { threshold });\n observer.observe(element);\n return () => observer.disconnect();\n }, [ref, enabled, threshold, latest]);\n}\n\n/** A stable function that runs `callback` once `delay` ms have passed without a new call; `cancel()` drops the pending one. */\nexport function useDebouncedCallback<A extends unknown[]>(callback: (...args: A) => void, delay: number) {\n const latest = useLatest(callback);\n const [debounced] = React.useState(() => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let wait = delay;\n const run = (...args: A) => {\n clearTimeout(timer);\n if (wait <= 0) latest.current(...args);\n else timer = setTimeout(() => latest.current(...args), wait);\n };\n return Object.assign(run, {\n cancel: () => clearTimeout(timer),\n setDelay: (next: number) => {\n wait = next;\n },\n });\n });\n React.useEffect(() => debounced.setDelay(delay), [debounced, delay]);\n React.useEffect(() => () => debounced.cancel(), [debounced]);\n return debounced;\n}\n\n/** A value controlled by a prop when it is given, held inside otherwise; `set` calls `onChange` either way. */\nexport function useControllable<T>(value: T | undefined, defaultValue: T, onChange?: (next: T) => void) {\n const [inner, setInner] = React.useState(defaultValue);\n const controlled = value !== undefined;\n const current = controlled ? value : inner;\n const set = React.useCallback(\n (next: T) => {\n if (!controlled) setInner(next);\n onChange?.(next);\n },\n [controlled, onChange],\n );\n return [current, set] as const;\n}\n\n/** Starts a media element without an unhandled rejection (autoplay refused, the source replaced meanwhile). */\nexport function playSafely(media: HTMLMediaElement | null | undefined) {\n try {\n const started = media?.play() as Promise<void> | undefined;\n started?.catch?.(() => undefined);\n } catch {\n // Not playable here (no media support): the controls stay as they are.\n }\n}\n"
921
+ },
922
+ {
923
+ "path": "meta.ts",
924
+ "content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which krizaka.com renders as\n// /docs/ui/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"activity-feed\"\n | \"action-sheet\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"form\"\n | \"gallery-page\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"map\"\n | \"longform-page\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"skeleton\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"verified-badge\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/** `stable`: its API only changes in a major. `beta`: it may still change in a minor. */\nexport type Status = \"stable\" | \"beta\";\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\nexport interface Example {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its phone\n * version thought out), copied as a starting point — at most 160 lines, listed on krizaka.com/docs/ui/templates.\n */\n readonly template?: boolean;\n}\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
925
+ },
926
+ {
927
+ "path": "tv.ts",
928
+ "content": "import { createTV } from \"tailwind-variants\";\n\nimport { twMergeConfig } from \"./cn\";\n\n/** `tv` with the preset's utilities known to the merge engine (`twMergeConfig`): what a primitive wearing the material is styled with. */\nexport const tv = createTV({ twMergeConfig });\n"
929
+ }
930
+ ],
931
+ "dependencies": [
932
+ "@krizaka/tokens@^3.5.0",
933
+ "@protomaps/basemaps@5.7.2",
934
+ "tailwind-variants@^3.3.1"
935
+ ],
936
+ "registryDependencies": [
937
+ "cn"
938
+ ],
939
+ "demo": {
940
+ "path": "examples/map/places.tsx",
941
+ "content": "\"use client\";\n\nimport { Map, Marker, StaticMap } from \"@krizaka/ui/map\";\nimport * as React from \"react\";\n\nconst SPOTS = [\n { id: \"olimpico\", label: \"Café Olimpico, Mile End\", position: [-73.6005, 45.5229] as const },\n { id: \"jean-talon\", label: \"Marché Jean-Talon\", position: [-73.6147, 45.5363] as const },\n { id: \"parc\", label: \"Parc La Fontaine\", position: [-73.5695, 45.5268] as const },\n];\n\n// Pick-up points in Montréal: markers are buttons, the chosen one shows its name; the places are a list for screen readers.\nexport default function MapPlaces() {\n const [chosen, setChosen] = React.useState(\"olimpico\");\n return (\n <Map\n label=\"Pick-up points in Montréal\"\n center={[-73.592, 45.528]}\n zoom={12.5}\n className=\"h-80 w-full max-w-xl\"\n fallback={<StaticMap label=\"Pick-up points in Montréal\" center={[-73.592, 45.528]} zoom={12.5} places={SPOTS} />}\n >\n {SPOTS.map((s) => (\n <Marker key={s.id} position={s.position} label={s.label} selected={s.id === chosen} onPress={() => setChosen(s.id)} />\n ))}\n </Map>\n );\n}\n"
942
+ },
943
+ "props": [
944
+ {
945
+ "component": "MAP_GLYPHS",
946
+ "description": "The glyphs of the labels (Noto Sans, SIL OFL): `{fontstack}` and `{range}` filled by MapLibre.",
947
+ "props": []
948
+ },
949
+ {
950
+ "component": "LocationPicker",
951
+ "description": "A position picked by moving the map under a pin; its distance from the person; \"near me\" on request.",
952
+ "props": [
953
+ {
954
+ "name": "label",
955
+ "type": "string",
956
+ "default": null,
957
+ "description": "The picker's accessible name (\"Meeting point\"), passed translated.",
958
+ "required": true
959
+ },
960
+ {
961
+ "name": "locale",
962
+ "type": "string",
963
+ "default": null,
964
+ "description": "The BCP 47 locale of the distance.",
965
+ "required": true
966
+ },
967
+ {
968
+ "name": "onValueChange",
969
+ "type": "(value: LngLat) => void",
970
+ "default": null,
971
+ "description": "Called when the map rests on a new position.",
972
+ "required": true
973
+ },
974
+ {
975
+ "name": "value",
976
+ "type": "LngLat",
977
+ "default": null,
978
+ "description": "The chosen position (controlled): the centre of the map.",
979
+ "required": true
980
+ },
981
+ {
982
+ "name": "brand",
983
+ "type": "BrandId",
984
+ "default": null,
985
+ "description": "The product whose accent the map carries. Default `krizaka`.",
986
+ "required": false
987
+ },
988
+ {
989
+ "name": "className",
990
+ "type": "string",
991
+ "default": null,
992
+ "description": "Classes merged on the root (its height: `h-80`, `aspect-video`).",
993
+ "required": false
994
+ },
995
+ {
996
+ "name": "fallback",
997
+ "type": "ReactNode",
998
+ "default": null,
999
+ "description": "Shown in place of the map while its engine loads, and for good without WebGL: a `StaticMap`, a picture.",
1000
+ "required": false
1001
+ },
1002
+ {
1003
+ "name": "labels",
1004
+ "type": "Partial<LocationPickerLabels>",
1005
+ "default": null,
1006
+ "description": "The words, passed translated; English by default.",
1007
+ "required": false
1008
+ },
1009
+ {
1010
+ "name": "lang",
1011
+ "type": "string",
1012
+ "default": null,
1013
+ "description": "The language of the labels (`fr`, `en`). Default `en`.",
1014
+ "required": false
1015
+ },
1016
+ {
1017
+ "name": "origin",
1018
+ "type": "LngLat | null",
1019
+ "default": "null",
1020
+ "description": "Where the person is, when the app already knows it (the distance chip shows at once).",
1021
+ "required": false
1022
+ },
1023
+ {
1024
+ "name": "radius",
1025
+ "type": "number",
1026
+ "default": null,
1027
+ "description": "A radius in metres drawn around the chosen position (a delivery zone, a search radius).",
1028
+ "required": false
1029
+ },
1030
+ {
1031
+ "name": "region",
1032
+ "type": "MapRegion | null",
1033
+ "default": null,
1034
+ "description": "The detailed region (PMTiles) drawn from zoom 6.5. Default: our Montréal–Québec extract (to zoom 14).",
1035
+ "required": false
1036
+ },
1037
+ {
1038
+ "name": "scheme",
1039
+ "type": "\"light\" | \"dark\"",
1040
+ "default": null,
1041
+ "description": "The scheme. Default: the page's (`html.light`), followed.",
1042
+ "required": false
1043
+ },
1044
+ {
1045
+ "name": "world",
1046
+ "type": "MapRegion | null",
1047
+ "default": null,
1048
+ "description": "The world's low zooms (PMTiles). Default: our world extract (to zoom 6).",
1049
+ "required": false
1050
+ },
1051
+ {
1052
+ "name": "zoom",
1053
+ "type": "number",
1054
+ "default": "14",
1055
+ "description": "The zoom at first. Default 14.",
1056
+ "required": false
1057
+ }
1058
+ ]
1059
+ },
1060
+ {
1061
+ "component": "Cluster",
1062
+ "description": "Many points grouped while they are close: a disc with the count, opened by zooming in; each point is listed for screen readers.",
1063
+ "props": [
1064
+ {
1065
+ "name": "points",
1066
+ "type": "readonly ClusterPoint[]",
1067
+ "default": null,
1068
+ "description": "The points: many of them (creators, pick-up points, events).",
1069
+ "required": true
1070
+ },
1071
+ {
1072
+ "name": "onPointPress",
1073
+ "type": "((point: ClusterPoint) => void)",
1074
+ "default": null,
1075
+ "description": "Pressing a single point. A group zooms in to open itself.",
1076
+ "required": false
1077
+ },
1078
+ {
1079
+ "name": "radius",
1080
+ "type": "number",
1081
+ "default": "48",
1082
+ "description": "The radius, in pixels, within which points group. Default 48.",
1083
+ "required": false
1084
+ }
1085
+ ]
1086
+ },
1087
+ {
1088
+ "component": "Map",
1089
+ "description": "A map: MapLibre over our Protomaps tiles, styled from the tokens, with its places as a list for screen readers.",
1090
+ "props": [
1091
+ {
1092
+ "name": "center",
1093
+ "type": "LngLat",
1094
+ "default": null,
1095
+ "description": "Where the map looks at first: longitude, latitude.",
1096
+ "required": true
1097
+ },
1098
+ {
1099
+ "name": "label",
1100
+ "type": "string",
1101
+ "default": null,
1102
+ "description": "The map's accessible name (\"Pick-up points near you\"), passed translated.",
1103
+ "required": true
1104
+ },
1105
+ {
1106
+ "name": "brand",
1107
+ "type": "BrandId",
1108
+ "default": null,
1109
+ "description": "The product whose accent the map carries. Default `krizaka`.",
1110
+ "required": false
1111
+ },
1112
+ {
1113
+ "name": "children",
1114
+ "type": "ReactNode",
1115
+ "default": null,
1116
+ "description": "Markers, circles, clusters, the user's position — and anything drawn over the map.",
1117
+ "required": false
1118
+ },
1119
+ {
1120
+ "name": "className",
1121
+ "type": "string",
1122
+ "default": null,
1123
+ "description": "Classes merged on the root (its height: `h-80`, `aspect-video`).",
1124
+ "required": false
1125
+ },
1126
+ {
1127
+ "name": "controlled",
1128
+ "type": "boolean",
1129
+ "default": null,
1130
+ "description": "The camera follows `center` and `zoom` when they change (flies, or jumps under reduced motion).",
1131
+ "required": false
1132
+ },
1133
+ {
1134
+ "name": "controls",
1135
+ "type": "boolean",
1136
+ "default": "true",
1137
+ "description": "The zoom buttons (default) — the keyboard also zooms with + and − on the focused map.",
1138
+ "required": false
1139
+ },
1140
+ {
1141
+ "name": "fallback",
1142
+ "type": "ReactNode",
1143
+ "default": null,
1144
+ "description": "Shown in place of the map while its engine loads, and for good without WebGL: a `StaticMap`, a picture.",
1145
+ "required": false
1146
+ },
1147
+ {
1148
+ "name": "interactive",
1149
+ "type": "boolean",
1150
+ "default": "true",
1151
+ "description": "Moves with the hand (default); `false` makes a still picture of a place.",
1152
+ "required": false
1153
+ },
1154
+ {
1155
+ "name": "labels",
1156
+ "type": "Partial<MapLabels>",
1157
+ "default": null,
1158
+ "description": "The words, passed translated; English by default.",
1159
+ "required": false
1160
+ },
1161
+ {
1162
+ "name": "lang",
1163
+ "type": "string",
1164
+ "default": null,
1165
+ "description": "The language of the labels (`fr`, `en`). Default `en`.",
1166
+ "required": false
1167
+ },
1168
+ {
1169
+ "name": "onMoveEnd",
1170
+ "type": "((view: { center: LngLat; zoom: number; }) => void)",
1171
+ "default": null,
1172
+ "description": "Called when the camera rests: its centre and zoom.",
1173
+ "required": false
1174
+ },
1175
+ {
1176
+ "name": "onPress",
1177
+ "type": "((position: LngLat) => void)",
1178
+ "default": null,
1179
+ "description": "Called with the position pressed on the map (not on a marker).",
1180
+ "required": false
1181
+ },
1182
+ {
1183
+ "name": "region",
1184
+ "type": "MapRegion | null",
1185
+ "default": null,
1186
+ "description": "The detailed region (PMTiles) drawn from zoom 6.5. Default: our Montréal–Québec extract (to zoom 14).",
1187
+ "required": false
1188
+ },
1189
+ {
1190
+ "name": "scheme",
1191
+ "type": "\"light\" | \"dark\"",
1192
+ "default": null,
1193
+ "description": "The scheme. Default: the page's (`html.light`), followed.",
1194
+ "required": false
1195
+ },
1196
+ {
1197
+ "name": "world",
1198
+ "type": "MapRegion | null",
1199
+ "default": null,
1200
+ "description": "The world's low zooms (PMTiles). Default: our world extract (to zoom 6).",
1201
+ "required": false
1202
+ },
1203
+ {
1204
+ "name": "zoom",
1205
+ "type": "number",
1206
+ "default": "12",
1207
+ "description": "The zoom at first (0 the world, 12 a city, 16 a street). Default 12.",
1208
+ "required": false
1209
+ }
1210
+ ]
1211
+ },
1212
+ {
1213
+ "component": "Marker",
1214
+ "description": "A place on the map: an accent dot (or your content) that is a button when it does something.",
1215
+ "props": [
1216
+ {
1217
+ "name": "label",
1218
+ "type": "string",
1219
+ "default": null,
1220
+ "description": "Its words: the place's name, read in the list of places and as the button's name (\"Café Olimpico, 350 m\").",
1221
+ "required": true
1222
+ },
1223
+ {
1224
+ "name": "position",
1225
+ "type": "LngLat",
1226
+ "default": null,
1227
+ "description": "Where it stands.",
1228
+ "required": true
1229
+ },
1230
+ {
1231
+ "name": "children",
1232
+ "type": "ReactNode",
1233
+ "default": null,
1234
+ "description": "What it draws instead of the accent dot (an avatar, an icon).",
1235
+ "required": false
1236
+ },
1237
+ {
1238
+ "name": "onPress",
1239
+ "type": "(() => void)",
1240
+ "default": null,
1241
+ "description": "Pressing it (details in a panel, a sheet): the marker becomes a button.",
1242
+ "required": false
1243
+ },
1244
+ {
1245
+ "name": "selected",
1246
+ "type": "boolean",
1247
+ "default": null,
1248
+ "description": "The chosen one: larger, its label shown above it.",
1249
+ "required": false
1250
+ },
1251
+ {
1252
+ "name": "size",
1253
+ "type": "number",
1254
+ "default": "18",
1255
+ "description": "Its size in pixels. Default 18.",
1256
+ "required": false
1257
+ }
1258
+ ]
1259
+ },
1260
+ {
1261
+ "component": "RadiusCircle",
1262
+ "description": "A circle on the ground: a delivery zone, a search radius — filled with the accent, its edge drawn.",
1263
+ "props": [
1264
+ {
1265
+ "name": "center",
1266
+ "type": "LngLat",
1267
+ "default": null,
1268
+ "description": "Its centre.",
1269
+ "required": true
1270
+ },
1271
+ {
1272
+ "name": "radius",
1273
+ "type": "number",
1274
+ "default": null,
1275
+ "description": "Its radius in metres.",
1276
+ "required": true
1277
+ },
1278
+ {
1279
+ "name": "tone",
1280
+ "type": "\"accent\" | \"soft\"",
1281
+ "default": "accent",
1282
+ "description": "`accent` (a zone that matters: a delivery area, a search radius) or `soft` (an accuracy).",
1283
+ "required": false
1284
+ }
1285
+ ]
1286
+ },
1287
+ {
1288
+ "component": "UserPosition",
1289
+ "description": "The person's position: a dot that pulses (still under reduced motion), its accuracy as a soft disc.",
1290
+ "props": [
1291
+ {
1292
+ "name": "label",
1293
+ "type": "string",
1294
+ "default": null,
1295
+ "description": "Its words (\"You are here\"), read in the list of places.",
1296
+ "required": true
1297
+ },
1298
+ {
1299
+ "name": "position",
1300
+ "type": "LngLat",
1301
+ "default": null,
1302
+ "description": "Where the person is.",
1303
+ "required": true
1304
+ },
1305
+ {
1306
+ "name": "accuracy",
1307
+ "type": "number",
1308
+ "default": null,
1309
+ "description": "The accuracy in metres: a soft disc around the dot.",
1310
+ "required": false
1311
+ }
1312
+ ]
1313
+ },
1314
+ {
1315
+ "component": "StaticMap",
1316
+ "description": "A still map: the places on the map's paper (or on a snapshot), named in words.",
1317
+ "props": [
1318
+ {
1319
+ "name": "center",
1320
+ "type": "LngLat",
1321
+ "default": null,
1322
+ "description": "The centre of the picture.",
1323
+ "required": true
1324
+ },
1325
+ {
1326
+ "name": "label",
1327
+ "type": "string",
1328
+ "default": null,
1329
+ "description": "What the picture shows, in words (\"Three pick-up points in the Plateau\"), passed translated: its accessible name.",
1330
+ "required": true
1331
+ },
1332
+ {
1333
+ "name": "className",
1334
+ "type": "string",
1335
+ "default": null,
1336
+ "description": "Classes merged on the root (its size).",
1337
+ "required": false
1338
+ },
1339
+ {
1340
+ "name": "image",
1341
+ "type": "string",
1342
+ "default": null,
1343
+ "description": "A snapshot of the real map of the area (same centre and zoom), drawn under the places; the attribution then shows.",
1344
+ "required": false
1345
+ },
1346
+ {
1347
+ "name": "places",
1348
+ "type": "readonly StaticMapPlace[]",
1349
+ "default": "[]",
1350
+ "description": "The places drawn on it (an accent dot each), listed in its name.",
1351
+ "required": false
1352
+ },
1353
+ {
1354
+ "name": "zoom",
1355
+ "type": "number",
1356
+ "default": "12",
1357
+ "description": "The zoom of the picture. Default 12.",
1358
+ "required": false
1359
+ }
1360
+ ]
1361
+ },
1362
+ {
1363
+ "component": "REGION_FROM_ZOOM",
1364
+ "description": "The zoom where the detailed region takes over from the world.",
1365
+ "props": []
1366
+ }
1367
+ ]
1368
+ }