tempest-react-sdk 0.38.2 → 0.38.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/bin/lib/design/collect.mjs +19 -0
  2. package/bin/lib/design/functions.mjs +5 -0
  3. package/bin/lib/design/index.mjs +8 -3
  4. package/bin/lib/design/index.test.mjs +31 -0
  5. package/bin/lib/design/scan.mjs +34 -2
  6. package/bin/lib/design/scan.test.mjs +78 -0
  7. package/bin/lib/doctor/doctor.e2e.test.mjs +85 -0
  8. package/bin/tempest.mjs +90 -6
  9. package/dist/audio/level-meter.cjs.map +1 -1
  10. package/dist/audio/level-meter.js.map +1 -1
  11. package/dist/audio/use-audio-recorder.cjs.map +1 -1
  12. package/dist/audio/use-audio-recorder.js.map +1 -1
  13. package/dist/auth/passkey.cjs.map +1 -1
  14. package/dist/auth/passkey.js.map +1 -1
  15. package/dist/auth/use-passkey.cjs.map +1 -1
  16. package/dist/auth/use-passkey.js.map +1 -1
  17. package/dist/br/BrazilMap.cjs.map +1 -1
  18. package/dist/br/BrazilMap.js.map +1 -1
  19. package/dist/br/BrazilStateCitySelect.cjs.map +1 -1
  20. package/dist/br/BrazilStateCitySelect.js.map +1 -1
  21. package/dist/br/BrazilStateMap.cjs.map +1 -1
  22. package/dist/br/BrazilStateMap.js.map +1 -1
  23. package/dist/br/PixQRCode.cjs.map +1 -1
  24. package/dist/br/PixQRCode.js.map +1 -1
  25. package/dist/br/boleto.cjs.map +1 -1
  26. package/dist/br/boleto.js.map +1 -1
  27. package/dist/br/pix.cjs.map +1 -1
  28. package/dist/br/pix.js.map +1 -1
  29. package/dist/br.d.ts +8 -0
  30. package/dist/capture/media-recorder.cjs.map +1 -1
  31. package/dist/capture/media-recorder.js.map +1 -1
  32. package/dist/capture/use-barcode-scanner.cjs.map +1 -1
  33. package/dist/capture/use-barcode-scanner.js.map +1 -1
  34. package/dist/capture/use-screen-capture.cjs.map +1 -1
  35. package/dist/capture/use-screen-capture.js.map +1 -1
  36. package/dist/capture/use-speech-recognition.cjs.map +1 -1
  37. package/dist/capture/use-speech-recognition.js.map +1 -1
  38. package/dist/capture/use-video-recorder.cjs.map +1 -1
  39. package/dist/capture/use-video-recorder.js.map +1 -1
  40. package/dist/charts/types.cjs.map +1 -1
  41. package/dist/charts/types.js.map +1 -1
  42. package/dist/charts.d.ts +8 -0
  43. package/dist/components/AIChat/AIChat.cjs.map +1 -1
  44. package/dist/components/AIChat/AIChat.js.map +1 -1
  45. package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
  46. package/dist/components/AIChat/AIChatComposer.js.map +1 -1
  47. package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
  48. package/dist/components/AIChat/AIChatTurn.js.map +1 -1
  49. package/dist/components/AppBar/AppBar.cjs.map +1 -1
  50. package/dist/components/AppBar/AppBar.js.map +1 -1
  51. package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
  52. package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
  53. package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
  54. package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
  55. package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -1
  56. package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -1
  57. package/dist/components/Button/Button.cjs.map +1 -1
  58. package/dist/components/Button/Button.js.map +1 -1
  59. package/dist/components/Calendar/Calendar.cjs.map +1 -1
  60. package/dist/components/Calendar/Calendar.js.map +1 -1
  61. package/dist/components/Carousel/Carousel.cjs.map +1 -1
  62. package/dist/components/Carousel/Carousel.js.map +1 -1
  63. package/dist/components/Chat/Chat.cjs.map +1 -1
  64. package/dist/components/Chat/Chat.js.map +1 -1
  65. package/dist/components/ChipInput/ChipInput.cjs.map +1 -1
  66. package/dist/components/ChipInput/ChipInput.js.map +1 -1
  67. package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
  68. package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
  69. package/dist/components/Combobox/Combobox.cjs.map +1 -1
  70. package/dist/components/Combobox/Combobox.js.map +1 -1
  71. package/dist/components/Command/Command.cjs.map +1 -1
  72. package/dist/components/Command/Command.js.map +1 -1
  73. package/dist/components/ConfirmDialog/ConfirmDialog.cjs.map +1 -1
  74. package/dist/components/ConfirmDialog/ConfirmDialog.js.map +1 -1
  75. package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
  76. package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
  77. package/dist/components/CopyButton/CopyButton.cjs.map +1 -1
  78. package/dist/components/CopyButton/CopyButton.js.map +1 -1
  79. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  80. package/dist/components/DataTable/DataTable.js.map +1 -1
  81. package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
  82. package/dist/components/DataTable/DataTable.module.js.map +1 -1
  83. package/dist/components/DataTable/EditableCell.cjs.map +1 -1
  84. package/dist/components/DataTable/EditableCell.js.map +1 -1
  85. package/dist/components/DatePicker/DatePicker.cjs.map +1 -1
  86. package/dist/components/DatePicker/DatePicker.js.map +1 -1
  87. package/dist/components/DateRangePicker/DateRangePicker.cjs.map +1 -1
  88. package/dist/components/DateRangePicker/DateRangePicker.js.map +1 -1
  89. package/dist/components/Drawer/Drawer.cjs.map +1 -1
  90. package/dist/components/Drawer/Drawer.js.map +1 -1
  91. package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
  92. package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
  93. package/dist/components/Dropzone/Dropzone.cjs.map +1 -1
  94. package/dist/components/Dropzone/Dropzone.js.map +1 -1
  95. package/dist/components/FileUpload/FileUpload.cjs.map +1 -1
  96. package/dist/components/FileUpload/FileUpload.js.map +1 -1
  97. package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
  98. package/dist/components/FilterBar/FilterBar.js.map +1 -1
  99. package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
  100. package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
  101. package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
  102. package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
  103. package/dist/components/Kanban/Kanban.cjs.map +1 -1
  104. package/dist/components/Kanban/Kanban.js.map +1 -1
  105. package/dist/components/Lightbox/Lightbox.cjs.map +1 -1
  106. package/dist/components/Lightbox/Lightbox.js.map +1 -1
  107. package/dist/components/ListTile/ListTile.cjs.map +1 -1
  108. package/dist/components/ListTile/ListTile.js.map +1 -1
  109. package/dist/components/Markdown/Markdown.cjs.map +1 -1
  110. package/dist/components/Markdown/Markdown.js.map +1 -1
  111. package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
  112. package/dist/components/Markdown/markdown-parse.js.map +1 -1
  113. package/dist/components/Menubar/Menubar.cjs.map +1 -1
  114. package/dist/components/Menubar/Menubar.js.map +1 -1
  115. package/dist/components/Modal/Modal.cjs.map +1 -1
  116. package/dist/components/Modal/Modal.js.map +1 -1
  117. package/dist/components/ModalsManager/ModalsManager.cjs.map +1 -1
  118. package/dist/components/ModalsManager/ModalsManager.js.map +1 -1
  119. package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
  120. package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
  121. package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
  122. package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
  123. package/dist/components/NotificationCenter/NotificationCenter.cjs.map +1 -1
  124. package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
  125. package/dist/components/Page/Page.cjs.map +1 -1
  126. package/dist/components/Page/Page.js.map +1 -1
  127. package/dist/components/Pagination/Pagination.cjs.map +1 -1
  128. package/dist/components/Pagination/Pagination.js.map +1 -1
  129. package/dist/components/PasswordInput/PasswordInput.cjs.map +1 -1
  130. package/dist/components/PasswordInput/PasswordInput.js.map +1 -1
  131. package/dist/components/PinInput/PinInput.cjs.map +1 -1
  132. package/dist/components/PinInput/PinInput.js.map +1 -1
  133. package/dist/components/Popover/Popover.cjs.map +1 -1
  134. package/dist/components/Popover/Popover.js.map +1 -1
  135. package/dist/components/Progress/Progress.cjs.map +1 -1
  136. package/dist/components/Progress/Progress.js.map +1 -1
  137. package/dist/components/QRCode/qr-encode.cjs.map +1 -1
  138. package/dist/components/QRCode/qr-encode.js.map +1 -1
  139. package/dist/components/Radio/Radio.cjs.map +1 -1
  140. package/dist/components/Radio/Radio.js.map +1 -1
  141. package/dist/components/RangeSlider/RangeSlider.cjs.map +1 -1
  142. package/dist/components/RangeSlider/RangeSlider.js.map +1 -1
  143. package/dist/components/RatingStars/RatingStars.cjs.map +1 -1
  144. package/dist/components/RatingStars/RatingStars.js.map +1 -1
  145. package/dist/components/RefreshIndicator/RefreshIndicator.cjs.map +1 -1
  146. package/dist/components/RefreshIndicator/RefreshIndicator.js.map +1 -1
  147. package/dist/components/Resizable/Resizable.cjs.map +1 -1
  148. package/dist/components/Resizable/Resizable.js.map +1 -1
  149. package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
  150. package/dist/components/Scheduler/Scheduler.js.map +1 -1
  151. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  152. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  153. package/dist/components/SignaturePad/SignaturePad.cjs.map +1 -1
  154. package/dist/components/SignaturePad/SignaturePad.js.map +1 -1
  155. package/dist/components/Slider/Slider.cjs.map +1 -1
  156. package/dist/components/Slider/Slider.js.map +1 -1
  157. package/dist/components/Sparkline/Sparkline.cjs.map +1 -1
  158. package/dist/components/Sparkline/Sparkline.js.map +1 -1
  159. package/dist/components/StepperInput/StepperInput.cjs.map +1 -1
  160. package/dist/components/StepperInput/StepperInput.js.map +1 -1
  161. package/dist/components/Table/Table.cjs.map +1 -1
  162. package/dist/components/Table/Table.js.map +1 -1
  163. package/dist/components/TimePicker/TimePicker.cjs.map +1 -1
  164. package/dist/components/TimePicker/TimePicker.js.map +1 -1
  165. package/dist/components/Toast/ToastProvider.cjs.map +1 -1
  166. package/dist/components/Toast/ToastProvider.js.map +1 -1
  167. package/dist/components/Tour/Tour.cjs.map +1 -1
  168. package/dist/components/Tour/Tour.js.map +1 -1
  169. package/dist/components/Transfer/Transfer.cjs.map +1 -1
  170. package/dist/components/Transfer/Transfer.js.map +1 -1
  171. package/dist/components/TreeView/TreeView.cjs.map +1 -1
  172. package/dist/components/TreeView/TreeView.js.map +1 -1
  173. package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
  174. package/dist/components/VirtualList/VirtualList.js.map +1 -1
  175. package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
  176. package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
  177. package/dist/components/Wizard/Wizard.cjs.map +1 -1
  178. package/dist/components/Wizard/Wizard.js.map +1 -1
  179. package/dist/editor/RichTextEditor.cjs.map +1 -1
  180. package/dist/editor/RichTextEditor.js.map +1 -1
  181. package/dist/forms/FormField.cjs.map +1 -1
  182. package/dist/forms/FormField.js.map +1 -1
  183. package/dist/geo/TrajectoryMap.cjs.map +1 -1
  184. package/dist/geo/TrajectoryMap.js.map +1 -1
  185. package/dist/geo/estimate.cjs.map +1 -1
  186. package/dist/geo/estimate.js.map +1 -1
  187. package/dist/geo/projection.cjs.map +1 -1
  188. package/dist/geo/projection.js.map +1 -1
  189. package/dist/hooks/use-event-listener.cjs.map +1 -1
  190. package/dist/hooks/use-event-listener.js.map +1 -1
  191. package/dist/hooks/use-local-storage.cjs.map +1 -1
  192. package/dist/hooks/use-local-storage.js.map +1 -1
  193. package/dist/hooks/use-sortable.cjs.map +1 -1
  194. package/dist/hooks/use-sortable.js.map +1 -1
  195. package/dist/http/api-client.cjs.map +1 -1
  196. package/dist/http/api-client.js.map +1 -1
  197. package/dist/http/errors.cjs.map +1 -1
  198. package/dist/http/errors.js.map +1 -1
  199. package/dist/http/resumable-upload.cjs.map +1 -1
  200. package/dist/http/resumable-upload.js.map +1 -1
  201. package/dist/http/upload-with-progress.cjs.map +1 -1
  202. package/dist/http/upload-with-progress.js.map +1 -1
  203. package/dist/i18n/I18nProvider.cjs.map +1 -1
  204. package/dist/i18n/I18nProvider.js.map +1 -1
  205. package/dist/imaging/canvas.cjs.map +1 -1
  206. package/dist/imaging/canvas.js.map +1 -1
  207. package/dist/imaging.d.ts +5 -0
  208. package/dist/logger/logger.cjs.map +1 -1
  209. package/dist/logger/logger.js.map +1 -1
  210. package/dist/oauth/GoogleSignIn.cjs.map +1 -1
  211. package/dist/oauth/GoogleSignIn.js.map +1 -1
  212. package/dist/offline/create-offline-sync.cjs.map +1 -1
  213. package/dist/offline/create-offline-sync.js.map +1 -1
  214. package/dist/perf/cache-size.cjs +1 -1
  215. package/dist/perf/cache-size.cjs.map +1 -1
  216. package/dist/perf/cache-size.js +27 -6
  217. package/dist/perf/cache-size.js.map +1 -1
  218. package/dist/sse/create-event-stream.cjs.map +1 -1
  219. package/dist/sse/create-event-stream.js.map +1 -1
  220. package/dist/styles.css +1 -1
  221. package/dist/sw/background-sync.cjs.map +1 -1
  222. package/dist/sw/background-sync.js.map +1 -1
  223. package/dist/sw/cache.cjs.map +1 -1
  224. package/dist/sw/cache.js.map +1 -1
  225. package/dist/sw.d.ts +12 -0
  226. package/dist/tabular/compact.cjs.map +1 -1
  227. package/dist/tabular/compact.js.map +1 -1
  228. package/dist/tabular/predictor.cjs.map +1 -1
  229. package/dist/tabular/predictor.js.map +1 -1
  230. package/dist/tabular.d.ts +7 -2
  231. package/dist/tempest-react-sdk.d.ts +133 -6
  232. package/dist/theme/ThemeProvider.cjs.map +1 -1
  233. package/dist/theme/ThemeProvider.js.map +1 -1
  234. package/dist/theme/color.cjs.map +1 -1
  235. package/dist/theme/color.js.map +1 -1
  236. package/dist/theme/create-theme.cjs.map +1 -1
  237. package/dist/theme/create-theme.js.map +1 -1
  238. package/dist/theme/data-viz-ramps.cjs +1 -1
  239. package/dist/theme/data-viz-ramps.cjs.map +1 -1
  240. package/dist/theme/data-viz-ramps.js +1 -1
  241. package/dist/theme/data-viz-ramps.js.map +1 -1
  242. package/dist/utils/storage.cjs.map +1 -1
  243. package/dist/utils/storage.js.map +1 -1
  244. package/dist/vision/core/canvas.cjs.map +1 -1
  245. package/dist/vision/core/canvas.js.map +1 -1
  246. package/dist/vision/core/exceptions.cjs.map +1 -1
  247. package/dist/vision/core/exceptions.js.map +1 -1
  248. package/dist/vision/core/graph.cjs.map +1 -1
  249. package/dist/vision/core/graph.js.map +1 -1
  250. package/dist/vision/core/metadata.cjs.map +1 -1
  251. package/dist/vision/core/metadata.js.map +1 -1
  252. package/dist/vision/core/providers.cjs.map +1 -1
  253. package/dist/vision/core/providers.js.map +1 -1
  254. package/dist/vision/core/session.cjs.map +1 -1
  255. package/dist/vision/core/session.js.map +1 -1
  256. package/dist/vision/core/timing.cjs.map +1 -1
  257. package/dist/vision/core/timing.js.map +1 -1
  258. package/dist/vision/io/image.cjs.map +1 -1
  259. package/dist/vision/io/image.js.map +1 -1
  260. package/dist/vision/labels.cjs.map +1 -1
  261. package/dist/vision/labels.js.map +1 -1
  262. package/dist/vision/luminance.cjs.map +1 -1
  263. package/dist/vision/luminance.js.map +1 -1
  264. package/dist/vision/postprocess/classification.cjs.map +1 -1
  265. package/dist/vision/postprocess/classification.js.map +1 -1
  266. package/dist/vision/postprocess/detection.cjs.map +1 -1
  267. package/dist/vision/postprocess/detection.js.map +1 -1
  268. package/dist/vision/postprocess/segmentation.cjs.map +1 -1
  269. package/dist/vision/postprocess/segmentation.js.map +1 -1
  270. package/dist/vision/preprocess/image.cjs.map +1 -1
  271. package/dist/vision/preprocess/image.js.map +1 -1
  272. package/dist/vision/results.cjs.map +1 -1
  273. package/dist/vision/results.js.map +1 -1
  274. package/dist/vision/tasks/base.cjs.map +1 -1
  275. package/dist/vision/tasks/base.js.map +1 -1
  276. package/dist/vision/tasks/classifier.cjs.map +1 -1
  277. package/dist/vision/tasks/classifier.js.map +1 -1
  278. package/dist/vision/tasks/detector.cjs.map +1 -1
  279. package/dist/vision/tasks/detector.js.map +1 -1
  280. package/dist/vision/tasks/segmenter.cjs.map +1 -1
  281. package/dist/vision/tasks/segmenter.js.map +1 -1
  282. package/dist/vision/types.cjs.map +1 -1
  283. package/dist/vision/types.js.map +1 -1
  284. package/dist/vision.d.ts +6 -0
  285. package/dist/vite/tempest-icons.cjs.map +1 -1
  286. package/dist/vite/tempest-icons.js.map +1 -1
  287. package/dist/vite/tempest-pwa-icons.cjs.map +1 -1
  288. package/dist/vite/tempest-pwa-icons.js.map +1 -1
  289. package/dist/ws/create-web-socket.cjs.map +1 -1
  290. package/dist/ws/create-web-socket.js.map +1 -1
  291. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"Lightbox.js","names":[],"sources":["../../../src/components/Lightbox/Lightbox.tsx"],"sourcesContent":["import { ChevronLeft, ChevronRight, X } from \"lucide-react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useFocusTrap } from \"@/hooks/use-focus-trap\";\nimport { useScrollLock } from \"@/hooks/use-scroll-lock\";\nimport { cn } from \"@/utils/cn\";\nimport { Portal } from \"../Portal\";\nimport styles from \"./Lightbox.module.css\";\n\n/** One item of the gallery. */\nexport interface LightboxItem {\n /** Full-size image URL. */\n src: string;\n /** Alt text. Required: a gallery of unlabeled images is unusable with a screen reader. */\n alt: string;\n /** Caption rendered under the image. */\n caption?: string;\n /** Thumbnail URL for the strip. Falls back to `src`. */\n thumbnail?: string;\n}\n\nexport interface LightboxProps {\n items: LightboxItem[];\n /** Whether the overlay is open. */\n open: boolean;\n /** Index shown while open. Default `0`. */\n index?: number;\n onIndexChange?: (index: number) => void;\n onClose: () => void;\n /** Renders the thumbnail strip. Default `true` when there is more than one item. */\n showThumbnails?: boolean;\n /** Renders the `3 / 12` counter. Default `true`. */\n showCounter?: boolean;\n /**\n * Wrap around at the ends. Default `true` — in a photo viewer, hitting a dead\n * end at the last image reads as a bug more often than as a boundary.\n */\n loop?: boolean;\n closeLabel?: string;\n previousLabel?: string;\n nextLabel?: string;\n className?: string;\n}\n\n/**\n * Full-screen image viewer with keyboard navigation.\n *\n * `Esc` closes, `←`/`→` walk the gallery, `Home`/`End` jump to the ends. Focus is\n * trapped inside while open and the page behind is scroll-locked, so the overlay\n * behaves like the dialog it is instead of a fancy `<div>` the keyboard can escape.\n *\n * Only the current image is mounted; neighbours are **preloaded** via `Image()` so\n * pressing `→` does not flash an empty frame.\n *\n * @example\n * ```tsx\n * const [open, setOpen] = useState(false);\n * const [index, setIndex] = useState(0);\n *\n * <Lightbox\n * open={open}\n * items={photos.map((p) => ({ src: p.url, alt: p.description }))}\n * index={index}\n * onIndexChange={setIndex}\n * onClose={() => setOpen(false)}\n * />\n * ```\n */\nexport function Lightbox({\n items,\n open,\n index = 0,\n onIndexChange,\n onClose,\n showThumbnails,\n showCounter = true,\n loop = true,\n closeLabel = \"Close\",\n previousLabel = \"Previous image\",\n nextLabel = \"Next image\",\n className,\n}: LightboxProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [internalIndex, setInternalIndex] = useState(index);\n const isControlled = onIndexChange !== undefined;\n const current = isControlled ? index : internalIndex;\n\n useFocusTrap(containerRef, open);\n useScrollLock(open);\n\n const move = useCallback(\n (next: number): void => {\n if (items.length === 0) return;\n const target = loop\n ? (next + items.length) % items.length\n : Math.max(0, Math.min(next, items.length - 1));\n if (!isControlled) setInternalIndex(target);\n onIndexChange?.(target);\n },\n [isControlled, items.length, loop, onIndexChange],\n );\n\n useEffect(() => {\n if (!isControlled) setInternalIndex(index);\n }, [index, isControlled]);\n\n useEffect(() => {\n if (!open) return;\n function onKeyDown(event: KeyboardEvent): void {\n switch (event.key) {\n case \"Escape\":\n onClose();\n break;\n case \"ArrowRight\":\n move(current + 1);\n break;\n case \"ArrowLeft\":\n move(current - 1);\n break;\n case \"Home\":\n move(0);\n break;\n case \"End\":\n move(items.length - 1);\n break;\n default:\n return;\n }\n event.preventDefault();\n }\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [current, items.length, move, onClose, open]);\n\n useEffect(() => {\n if (!open || typeof Image === \"undefined\") return;\n for (const offset of [-1, 1]) {\n const neighbour = items[(current + offset + items.length) % items.length];\n if (!neighbour) continue;\n const preload = new Image();\n preload.src = neighbour.src;\n }\n }, [current, items, open]);\n\n if (!open || items.length === 0) return null;\n\n const item = items[Math.max(0, Math.min(current, items.length - 1))];\n const withThumbnails = showThumbnails ?? items.length > 1;\n const multiple = items.length > 1;\n\n return (\n <Portal>\n <div\n ref={containerRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={item.alt}\n className={cn(styles.overlay, className)}\n >\n <div className={styles.topBar}>\n {showCounter && multiple ? (\n <span className={styles.counter}>\n {current + 1} / {items.length}\n </span>\n ) : (\n <span />\n )}\n <button\n type=\"button\"\n className={styles.iconButton}\n aria-label={closeLabel}\n onClick={onClose}\n >\n <X size={20} />\n </button>\n </div>\n\n <div className={styles.stage}>\n {multiple ? (\n <button\n type=\"button\"\n className={cn(styles.iconButton, styles.nav)}\n aria-label={previousLabel}\n disabled={!loop && current === 0}\n onClick={() => move(current - 1)}\n >\n <ChevronLeft size={24} />\n </button>\n ) : null}\n\n <figure className={styles.figure}>\n <img src={item.src} alt={item.alt} className={styles.image} />\n {item.caption ? (\n <figcaption className={styles.caption}>{item.caption}</figcaption>\n ) : null}\n </figure>\n\n {multiple ? (\n <button\n type=\"button\"\n className={cn(styles.iconButton, styles.nav)}\n aria-label={nextLabel}\n disabled={!loop && current === items.length - 1}\n onClick={() => move(current + 1)}\n >\n <ChevronRight size={24} />\n </button>\n ) : null}\n </div>\n\n {withThumbnails && multiple ? (\n <div className={styles.thumbnails} role=\"tablist\" aria-label={item.alt}>\n {items.map((entry, entryIndex) => (\n <button\n key={`${entry.src}-${entryIndex}`}\n type=\"button\"\n role=\"tab\"\n aria-selected={entryIndex === current}\n aria-label={entry.alt}\n className={cn(\n styles.thumbnail,\n entryIndex === current && styles.thumbnailActive,\n )}\n onClick={() => move(entryIndex)}\n >\n <img\n src={entry.thumbnail ?? entry.src}\n alt=\"\"\n className={styles.thumbnailImage}\n />\n </button>\n ))}\n </div>\n ) : null}\n </div>\n </Portal>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Lightbox.js","names":[],"sources":["../../../src/components/Lightbox/Lightbox.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — closeLabel,\n * previousLabel and nextLabel are three separate strings an app has to translate,\n * showThumbnails/showCounter/loop are the chrome, and items/index/\n * onIndexChange/open/onClose are the controlled contract. The body is the focus\n * trap, the key handling and the swipe, which all move the same index.\n */\nimport { ChevronLeft, ChevronRight, X } from \"lucide-react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useFocusTrap } from \"@/hooks/use-focus-trap\";\nimport { useScrollLock } from \"@/hooks/use-scroll-lock\";\nimport { cn } from \"@/utils/cn\";\nimport { Portal } from \"../Portal\";\nimport styles from \"./Lightbox.module.css\";\n\n/** One item of the gallery. */\nexport interface LightboxItem {\n /** Full-size image URL. */\n src: string;\n /** Alt text. Required: a gallery of unlabeled images is unusable with a screen reader. */\n alt: string;\n /** Caption rendered under the image. */\n caption?: string;\n /** Thumbnail URL for the strip. Falls back to `src`. */\n thumbnail?: string;\n}\n\nexport interface LightboxProps {\n items: LightboxItem[];\n /** Whether the overlay is open. */\n open: boolean;\n /** Index shown while open. Default `0`. */\n index?: number;\n onIndexChange?: (index: number) => void;\n onClose: () => void;\n /** Renders the thumbnail strip. Default `true` when there is more than one item. */\n showThumbnails?: boolean;\n /** Renders the `3 / 12` counter. Default `true`. */\n showCounter?: boolean;\n /**\n * Wrap around at the ends. Default `true` — in a photo viewer, hitting a dead\n * end at the last image reads as a bug more often than as a boundary.\n */\n loop?: boolean;\n closeLabel?: string;\n previousLabel?: string;\n nextLabel?: string;\n className?: string;\n}\n\n/**\n * Full-screen image viewer with keyboard navigation.\n *\n * `Esc` closes, `←`/`→` walk the gallery, `Home`/`End` jump to the ends. Focus is\n * trapped inside while open and the page behind is scroll-locked, so the overlay\n * behaves like the dialog it is instead of a fancy `<div>` the keyboard can escape.\n *\n * Only the current image is mounted; neighbours are **preloaded** via `Image()` so\n * pressing `→` does not flash an empty frame.\n *\n * @example\n * ```tsx\n * const [open, setOpen] = useState(false);\n * const [index, setIndex] = useState(0);\n *\n * <Lightbox\n * open={open}\n * items={photos.map((p) => ({ src: p.url, alt: p.description }))}\n * index={index}\n * onIndexChange={setIndex}\n * onClose={() => setOpen(false)}\n * />\n * ```\n */\nexport function Lightbox({\n items,\n open,\n index = 0,\n onIndexChange,\n onClose,\n showThumbnails,\n showCounter = true,\n loop = true,\n closeLabel = \"Close\",\n previousLabel = \"Previous image\",\n nextLabel = \"Next image\",\n className,\n}: LightboxProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [internalIndex, setInternalIndex] = useState(index);\n const isControlled = onIndexChange !== undefined;\n const current = isControlled ? index : internalIndex;\n\n useFocusTrap(containerRef, open);\n useScrollLock(open);\n\n const move = useCallback(\n (next: number): void => {\n if (items.length === 0) return;\n const target = loop\n ? (next + items.length) % items.length\n : Math.max(0, Math.min(next, items.length - 1));\n if (!isControlled) setInternalIndex(target);\n onIndexChange?.(target);\n },\n [isControlled, items.length, loop, onIndexChange],\n );\n\n useEffect(() => {\n if (!isControlled) setInternalIndex(index);\n }, [index, isControlled]);\n\n useEffect(() => {\n if (!open) return;\n function onKeyDown(event: KeyboardEvent): void {\n switch (event.key) {\n case \"Escape\":\n onClose();\n break;\n case \"ArrowRight\":\n move(current + 1);\n break;\n case \"ArrowLeft\":\n move(current - 1);\n break;\n case \"Home\":\n move(0);\n break;\n case \"End\":\n move(items.length - 1);\n break;\n default:\n return;\n }\n event.preventDefault();\n }\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [current, items.length, move, onClose, open]);\n\n useEffect(() => {\n if (!open || typeof Image === \"undefined\") return;\n for (const offset of [-1, 1]) {\n const neighbour = items[(current + offset + items.length) % items.length];\n if (!neighbour) continue;\n const preload = new Image();\n preload.src = neighbour.src;\n }\n }, [current, items, open]);\n\n if (!open || items.length === 0) return null;\n\n const item = items[Math.max(0, Math.min(current, items.length - 1))];\n const withThumbnails = showThumbnails ?? items.length > 1;\n const multiple = items.length > 1;\n\n return (\n <Portal>\n <div\n ref={containerRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={item.alt}\n className={cn(styles.overlay, className)}\n >\n <div className={styles.topBar}>\n {showCounter && multiple ? (\n <span className={styles.counter}>\n {current + 1} / {items.length}\n </span>\n ) : (\n <span />\n )}\n <button\n type=\"button\"\n className={styles.iconButton}\n aria-label={closeLabel}\n onClick={onClose}\n >\n <X size={20} />\n </button>\n </div>\n\n <div className={styles.stage}>\n {multiple ? (\n <button\n type=\"button\"\n className={cn(styles.iconButton, styles.nav)}\n aria-label={previousLabel}\n disabled={!loop && current === 0}\n onClick={() => move(current - 1)}\n >\n <ChevronLeft size={24} />\n </button>\n ) : null}\n\n <figure className={styles.figure}>\n <img src={item.src} alt={item.alt} className={styles.image} />\n {item.caption ? (\n <figcaption className={styles.caption}>{item.caption}</figcaption>\n ) : null}\n </figure>\n\n {multiple ? (\n <button\n type=\"button\"\n className={cn(styles.iconButton, styles.nav)}\n aria-label={nextLabel}\n disabled={!loop && current === items.length - 1}\n onClick={() => move(current + 1)}\n >\n <ChevronRight size={24} />\n </button>\n ) : null}\n </div>\n\n {withThumbnails && multiple ? (\n <div className={styles.thumbnails} role=\"tablist\" aria-label={item.alt}>\n {items.map((entry, entryIndex) => (\n <button\n key={`${entry.src}-${entryIndex}`}\n type=\"button\"\n role=\"tab\"\n aria-selected={entryIndex === current}\n aria-label={entry.alt}\n className={cn(\n styles.thumbnail,\n entryIndex === current && styles.thumbnailActive,\n )}\n onClick={() => move(entryIndex)}\n >\n <img\n src={entry.thumbnail ?? entry.src}\n alt=\"\"\n className={styles.thumbnailImage}\n />\n </button>\n ))}\n </div>\n ) : null}\n </div>\n </Portal>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ListTile.cjs","names":[],"sources":["../../../src/components/ListTile/ListTile.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./ListTile.module.css\";\n\nexport interface ListTileProps {\n /** Left slot — typically an icon or avatar. */\n leading?: ReactNode;\n /** Primary line. Required. */\n title: ReactNode;\n /** Secondary line rendered under the title. */\n subtitle?: ReactNode;\n /** Right slot — typically an icon, switch or meta text. */\n trailing?: ReactNode;\n /** When provided, the tile becomes an interactive button. */\n onClick?: () => void;\n /** When true, the tile is dimmed and non-interactive. */\n disabled?: boolean;\n /** When true, the tile is highlighted as the active row. */\n selected?: boolean;\n /** Extra class names merged onto the root. */\n className?: string;\n}\n\n/**\n * The canonical Material list row: an optional leading slot, a title with an\n * optional subtitle, and an optional trailing slot. When `onClick` is supplied\n * the tile renders as a full-width, keyboard-accessible `<button>`; otherwise it\n * renders as a static `<div>`.\n *\n * @example\n * <ListTile\n * leading={<User />}\n * title=\"Maria Silva\"\n * subtitle=\"maria@example.com\"\n * trailing={<ChevronRight />}\n * onClick={() => open(user)}\n * />\n */\nexport function ListTile({\n leading,\n title,\n subtitle,\n trailing,\n onClick,\n disabled = false,\n selected = false,\n className,\n}: ListTileProps) {\n const content = (\n <>\n {leading !== undefined && <span className={styles.leading}>{leading}</span>}\n <span className={styles.body}>\n <span className={styles.title}>{title}</span>\n {subtitle !== undefined && <span className={styles.subtitle}>{subtitle}</span>}\n </span>\n {trailing !== undefined && <span className={styles.trailing}>{trailing}</span>}\n </>\n );\n\n const rootClassName = cn(\n styles.tile,\n selected && styles.selected,\n disabled && styles.disabled,\n className,\n );\n\n if (onClick) {\n return (\n <button\n type=\"button\"\n className={cn(rootClassName, styles.interactive)}\n aria-pressed={selected ? true : undefined}\n disabled={disabled}\n onClick={onClick}\n >\n {content}\n </button>\n );\n }\n\n return <div className={rootClassName}>{content}</div>;\n}\n"],"mappings":"4GAsCA,SAAgB,EAAS,CACrB,UACA,QACA,WACA,WACA,UACA,WAAW,GACX,WAAW,GACX,aACc,CACd,IAAM,GACF,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,CACK,IAAY,IAAA,KAAa,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,iBAAU,CAAc,CAAA,GAC1E,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,cAAxB,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,eAAQ,CAAY,CAAA,EAC3C,IAAa,IAAA,KAAa,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,kBAAW,CAAe,CAAA,CAC3E,IACL,IAAa,IAAA,KAAa,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,kBAAW,CAAe,CAAA,CAC/E,CAAA,CAAA,EAGA,EAAgB,EAAA,GAClB,EAAA,QAAO,KACP,GAAY,EAAA,QAAO,SACnB,GAAY,EAAA,QAAO,SACnB,CACJ,EAgBA,OAdI,GAEI,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,GAAG,EAAe,EAAA,QAAO,WAAW,EAC/C,eAAc,EAAW,GAAO,IAAA,GACtB,WACD,mBAER,CACG,CAAA,GAIT,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,WAAgB,CAAa,CAAA,CACxD"}
1
+ {"version":3,"file":"ListTile.cjs","names":[],"sources":["../../../src/components/ListTile/ListTile.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — leading, title, subtitle and trailing are the four\n * slots of a list row, and onClick/disabled/selected are what make it a row you can\n * pick. That is the whole component — there is no second one inside it.\n */\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./ListTile.module.css\";\n\nexport interface ListTileProps {\n /** Left slot — typically an icon or avatar. */\n leading?: ReactNode;\n /** Primary line. Required. */\n title: ReactNode;\n /** Secondary line rendered under the title. */\n subtitle?: ReactNode;\n /** Right slot — typically an icon, switch or meta text. */\n trailing?: ReactNode;\n /** When provided, the tile becomes an interactive button. */\n onClick?: () => void;\n /** When true, the tile is dimmed and non-interactive. */\n disabled?: boolean;\n /** When true, the tile is highlighted as the active row. */\n selected?: boolean;\n /** Extra class names merged onto the root. */\n className?: string;\n}\n\n/**\n * The canonical Material list row: an optional leading slot, a title with an\n * optional subtitle, and an optional trailing slot. When `onClick` is supplied\n * the tile renders as a full-width, keyboard-accessible `<button>`; otherwise it\n * renders as a static `<div>`.\n *\n * @example\n * <ListTile\n * leading={<User />}\n * title=\"Maria Silva\"\n * subtitle=\"maria@example.com\"\n * trailing={<ChevronRight />}\n * onClick={() => open(user)}\n * />\n */\nexport function ListTile({\n leading,\n title,\n subtitle,\n trailing,\n onClick,\n disabled = false,\n selected = false,\n className,\n}: ListTileProps) {\n const content = (\n <>\n {leading !== undefined && <span className={styles.leading}>{leading}</span>}\n <span className={styles.body}>\n <span className={styles.title}>{title}</span>\n {subtitle !== undefined && <span className={styles.subtitle}>{subtitle}</span>}\n </span>\n {trailing !== undefined && <span className={styles.trailing}>{trailing}</span>}\n </>\n );\n\n const rootClassName = cn(\n styles.tile,\n selected && styles.selected,\n disabled && styles.disabled,\n className,\n );\n\n if (onClick) {\n return (\n <button\n type=\"button\"\n className={cn(rootClassName, styles.interactive)}\n aria-pressed={selected ? true : undefined}\n disabled={disabled}\n onClick={onClick}\n >\n {content}\n </button>\n );\n }\n\n return <div className={rootClassName}>{content}</div>;\n}\n"],"mappings":"4GA2CA,SAAgB,EAAS,CACrB,UACA,QACA,WACA,WACA,UACA,WAAW,GACX,WAAW,GACX,aACc,CACd,IAAM,GACF,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,CACK,IAAY,IAAA,KAAa,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,iBAAU,CAAc,CAAA,GAC1E,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,cAAxB,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,eAAQ,CAAY,CAAA,EAC3C,IAAa,IAAA,KAAa,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,kBAAW,CAAe,CAAA,CAC3E,IACL,IAAa,IAAA,KAAa,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,kBAAW,CAAe,CAAA,CAC/E,CAAA,CAAA,EAGA,EAAgB,EAAA,GAClB,EAAA,QAAO,KACP,GAAY,EAAA,QAAO,SACnB,GAAY,EAAA,QAAO,SACnB,CACJ,EAgBA,OAdI,GAEI,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,GAAG,EAAe,EAAA,QAAO,WAAW,EAC/C,eAAc,EAAW,GAAO,IAAA,GACtB,WACD,mBAER,CACG,CAAA,GAIT,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,WAAgB,CAAa,CAAA,CACxD"}
@@ -1 +1 @@
1
- {"version":3,"file":"ListTile.js","names":[],"sources":["../../../src/components/ListTile/ListTile.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./ListTile.module.css\";\n\nexport interface ListTileProps {\n /** Left slot — typically an icon or avatar. */\n leading?: ReactNode;\n /** Primary line. Required. */\n title: ReactNode;\n /** Secondary line rendered under the title. */\n subtitle?: ReactNode;\n /** Right slot — typically an icon, switch or meta text. */\n trailing?: ReactNode;\n /** When provided, the tile becomes an interactive button. */\n onClick?: () => void;\n /** When true, the tile is dimmed and non-interactive. */\n disabled?: boolean;\n /** When true, the tile is highlighted as the active row. */\n selected?: boolean;\n /** Extra class names merged onto the root. */\n className?: string;\n}\n\n/**\n * The canonical Material list row: an optional leading slot, a title with an\n * optional subtitle, and an optional trailing slot. When `onClick` is supplied\n * the tile renders as a full-width, keyboard-accessible `<button>`; otherwise it\n * renders as a static `<div>`.\n *\n * @example\n * <ListTile\n * leading={<User />}\n * title=\"Maria Silva\"\n * subtitle=\"maria@example.com\"\n * trailing={<ChevronRight />}\n * onClick={() => open(user)}\n * />\n */\nexport function ListTile({\n leading,\n title,\n subtitle,\n trailing,\n onClick,\n disabled = false,\n selected = false,\n className,\n}: ListTileProps) {\n const content = (\n <>\n {leading !== undefined && <span className={styles.leading}>{leading}</span>}\n <span className={styles.body}>\n <span className={styles.title}>{title}</span>\n {subtitle !== undefined && <span className={styles.subtitle}>{subtitle}</span>}\n </span>\n {trailing !== undefined && <span className={styles.trailing}>{trailing}</span>}\n </>\n );\n\n const rootClassName = cn(\n styles.tile,\n selected && styles.selected,\n disabled && styles.disabled,\n className,\n );\n\n if (onClick) {\n return (\n <button\n type=\"button\"\n className={cn(rootClassName, styles.interactive)}\n aria-pressed={selected ? true : undefined}\n disabled={disabled}\n onClick={onClick}\n >\n {content}\n </button>\n );\n }\n\n return <div className={rootClassName}>{content}</div>;\n}\n"],"mappings":";;;;AAsCA,SAAgB,EAAS,EACrB,YACA,UACA,aACA,aACA,YACA,cAAW,IACX,cAAW,IACX,gBACc;CACd,IAAM,IACF,kBAAA,GAAA,EAAA,UAAA;EACK,MAAY,KAAA,KAAa,kBAAC,QAAD;GAAM,WAAW,EAAO;aAAU;EAAc,CAAA;EAC1E,kBAAC,QAAD;GAAM,WAAW,EAAO;aAAxB,CACI,kBAAC,QAAD;IAAM,WAAW,EAAO;cAAQ;GAAY,CAAA,GAC3C,MAAa,KAAA,KAAa,kBAAC,QAAD;IAAM,WAAW,EAAO;cAAW;GAAe,CAAA,CAC3E;;EACL,MAAa,KAAA,KAAa,kBAAC,QAAD;GAAM,WAAW,EAAO;aAAW;EAAe,CAAA;CAC/E,EAAA,CAAA,GAGA,IAAgB,EAClB,EAAO,MACP,KAAY,EAAO,UACnB,KAAY,EAAO,UACnB,CACJ;CAgBA,OAdI,IAEI,kBAAC,UAAD;EACI,MAAK;EACL,WAAW,EAAG,GAAe,EAAO,WAAW;EAC/C,gBAAc,IAAW,KAAO,KAAA;EACtB;EACD;YAER;CACG,CAAA,IAIT,kBAAC,OAAD;EAAK,WAAW;YAAgB;CAAa,CAAA;AACxD"}
1
+ {"version":3,"file":"ListTile.js","names":[],"sources":["../../../src/components/ListTile/ListTile.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — leading, title, subtitle and trailing are the four\n * slots of a list row, and onClick/disabled/selected are what make it a row you can\n * pick. That is the whole component — there is no second one inside it.\n */\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./ListTile.module.css\";\n\nexport interface ListTileProps {\n /** Left slot — typically an icon or avatar. */\n leading?: ReactNode;\n /** Primary line. Required. */\n title: ReactNode;\n /** Secondary line rendered under the title. */\n subtitle?: ReactNode;\n /** Right slot — typically an icon, switch or meta text. */\n trailing?: ReactNode;\n /** When provided, the tile becomes an interactive button. */\n onClick?: () => void;\n /** When true, the tile is dimmed and non-interactive. */\n disabled?: boolean;\n /** When true, the tile is highlighted as the active row. */\n selected?: boolean;\n /** Extra class names merged onto the root. */\n className?: string;\n}\n\n/**\n * The canonical Material list row: an optional leading slot, a title with an\n * optional subtitle, and an optional trailing slot. When `onClick` is supplied\n * the tile renders as a full-width, keyboard-accessible `<button>`; otherwise it\n * renders as a static `<div>`.\n *\n * @example\n * <ListTile\n * leading={<User />}\n * title=\"Maria Silva\"\n * subtitle=\"maria@example.com\"\n * trailing={<ChevronRight />}\n * onClick={() => open(user)}\n * />\n */\nexport function ListTile({\n leading,\n title,\n subtitle,\n trailing,\n onClick,\n disabled = false,\n selected = false,\n className,\n}: ListTileProps) {\n const content = (\n <>\n {leading !== undefined && <span className={styles.leading}>{leading}</span>}\n <span className={styles.body}>\n <span className={styles.title}>{title}</span>\n {subtitle !== undefined && <span className={styles.subtitle}>{subtitle}</span>}\n </span>\n {trailing !== undefined && <span className={styles.trailing}>{trailing}</span>}\n </>\n );\n\n const rootClassName = cn(\n styles.tile,\n selected && styles.selected,\n disabled && styles.disabled,\n className,\n );\n\n if (onClick) {\n return (\n <button\n type=\"button\"\n className={cn(rootClassName, styles.interactive)}\n aria-pressed={selected ? true : undefined}\n disabled={disabled}\n onClick={onClick}\n >\n {content}\n </button>\n );\n }\n\n return <div className={rootClassName}>{content}</div>;\n}\n"],"mappings":";;;;AA2CA,SAAgB,EAAS,EACrB,YACA,UACA,aACA,aACA,YACA,cAAW,IACX,cAAW,IACX,gBACc;CACd,IAAM,IACF,kBAAA,GAAA,EAAA,UAAA;EACK,MAAY,KAAA,KAAa,kBAAC,QAAD;GAAM,WAAW,EAAO;aAAU;EAAc,CAAA;EAC1E,kBAAC,QAAD;GAAM,WAAW,EAAO;aAAxB,CACI,kBAAC,QAAD;IAAM,WAAW,EAAO;cAAQ;GAAY,CAAA,GAC3C,MAAa,KAAA,KAAa,kBAAC,QAAD;IAAM,WAAW,EAAO;cAAW;GAAe,CAAA,CAC3E;;EACL,MAAa,KAAA,KAAa,kBAAC,QAAD;GAAM,WAAW,EAAO;aAAW;EAAe,CAAA;CAC/E,EAAA,CAAA,GAGA,IAAgB,EAClB,EAAO,MACP,KAAY,EAAO,UACnB,KAAY,EAAO,UACnB,CACJ;CAgBA,OAdI,IAEI,kBAAC,UAAD;EACI,MAAK;EACL,WAAW,EAAG,GAAe,EAAO,WAAW;EAC/C,gBAAc,IAAW,KAAO,KAAA;EACtB;EACD;YAER;CACG,CAAA,IAIT,kBAAC,OAAD;EAAK,WAAW;YAAgB;CAAa,CAAA;AACxD"}
@@ -1 +1 @@
1
- {"version":3,"file":"Markdown.cjs","names":[],"sources":["../../../src/components/Markdown/Markdown.tsx"],"sourcesContent":["import { Fragment, useMemo, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { useScrollOverflow } from \"@/hooks/use-scroll-overflow\";\nimport { cn } from \"@/utils/cn\";\n\nimport { CodeBlock } from \"../CodeBlock\";\nimport {\n parseMarkdown,\n type MarkdownAlign,\n type MarkdownBlock,\n type MarkdownInline,\n} from \"./markdown-parse\";\nimport styles from \"./Markdown.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\";\n\nexport interface MarkdownProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The Markdown source. */\n source: string;\n /**\n * Heading level the document's `#` maps to. Default 2.\n *\n * A comment body rendered inside a page whose `<h1>` is the page title must not\n * emit a second `<h1>`; shifting the whole scale keeps the outline valid.\n */\n headingOffset?: number;\n /** Render fenced code with `CodeBlock` (copy button, line numbers). Default `true`. */\n highlightCode?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n /** Extra props for every link — `target=\"_blank\"`, an analytics handler. */\n linkProps?: HTMLAttributes<HTMLAnchorElement> & { target?: string; rel?: string };\n}\n\n/** Render inline nodes. */\nfunction renderInline(nodes: readonly MarkdownInline[]): ReactNode {\n return nodes.map((node, index) => {\n const key = index;\n switch (node.type) {\n case \"text\":\n return <Fragment key={key}>{node.value}</Fragment>;\n case \"strong\":\n return <strong key={key}>{renderInline(node.children)}</strong>;\n case \"em\":\n return <em key={key}>{renderInline(node.children)}</em>;\n case \"del\":\n return <del key={key}>{renderInline(node.children)}</del>;\n case \"code\":\n return (\n <code key={key} className={styles.inlineCode}>\n {node.value}\n </code>\n );\n case \"image\":\n return <img key={key} className={styles.image} src={node.src} alt={node.alt} />;\n case \"break\":\n return <br key={key} />;\n default:\n return null;\n }\n });\n}\n\n/** CSS `text-align` for a table column. */\nfunction alignOf(align: MarkdownAlign): { textAlign: \"left\" | \"center\" | \"right\" } | undefined {\n return align ? { textAlign: align } : undefined;\n}\n\n/**\n * Rendered Markdown, from a node tree — never from an HTML string.\n *\n * `dangerouslySetInnerHTML` appears nowhere in this component. That is what makes\n * rendering somebody else's Markdown safe by construction rather than by a promise\n * about escaping: a `<script>` in a comment is four characters of text, because text\n * is all a React child can be.\n *\n * Link and image URLs go through a scheme **allowlist** (`http`, `https`, `mailto`,\n * `tel`, `sms`, plus relative), so `[click](javascript:alert(1))` renders as plain\n * text. A blocklist would have to enumerate every spelling of `javascript:` and\n * would miss one.\n *\n * @example\n * <Markdown source={comentario.corpo} linkProps={{ target: \"_blank\", rel: \"noreferrer\" }} />\n */\nexport function Markdown({\n source,\n headingOffset = 2,\n highlightCode = true,\n showLineNumbers = false,\n linkProps,\n className,\n ...rest\n}: MarkdownProps) {\n const blocks = useMemo(() => parseMarkdown(source), [source]);\n\n return (\n <div className={cn(styles.root, className)} {...rest}>\n {blocks.map((block, index) => (\n <Block\n key={index}\n block={block}\n headingOffset={headingOffset}\n highlightCode={highlightCode}\n showLineNumbers={showLineNumbers}\n linkProps={linkProps}\n />\n ))}\n </div>\n );\n}\n\ninterface BlockProps {\n block: MarkdownBlock;\n headingOffset: number;\n highlightCode: boolean;\n showLineNumbers: boolean;\n linkProps?: MarkdownProps[\"linkProps\"];\n}\n\n/** One block, and its children. */\nfunction Block({ block, headingOffset, highlightCode, showLineNumbers, linkProps }: BlockProps) {\n const children = (nodes: readonly MarkdownBlock[]): ReactNode =>\n nodes.map((child, index) => (\n <Block\n key={index}\n block={child}\n headingOffset={headingOffset}\n highlightCode={highlightCode}\n showLineNumbers={showLineNumbers}\n linkProps={linkProps}\n />\n ));\n\n const inline = (nodes: readonly MarkdownInline[]): ReactNode =>\n nodes.map((node, index) =>\n node.type === \"link\" ? (\n <a key={index} className={styles.link} href={node.href} {...linkProps}>\n {renderInline(node.children)}\n </a>\n ) : (\n <Fragment key={index}>{renderInline([node])}</Fragment>\n ),\n );\n\n switch (block.type) {\n case \"heading\": {\n const level = Math.min(6, Math.max(1, block.level + headingOffset - 1));\n const Tag = `h${level}` as \"h1\";\n return (\n <Tag className={styles[`h${level}` as keyof typeof styles]}>\n {inline(block.children)}\n </Tag>\n );\n }\n case \"paragraph\":\n return <p>{inline(block.children)}</p>;\n case \"code\":\n return highlightCode ? (\n <CodeBlock\n code={block.value}\n language={block.language ?? undefined}\n showLineNumbers={showLineNumbers}\n />\n ) : (\n <pre>\n <code>{block.value}</code>\n </pre>\n );\n case \"quote\":\n return <blockquote className={styles.quote}>{children(block.children)}</blockquote>;\n case \"rule\":\n return <hr className={styles.rule} />;\n case \"list\":\n return block.ordered ? (\n <ol className={styles.list} start={block.start === 1 ? undefined : block.start}>\n {block.items.map((item, index) => (\n <li key={index} className={styles.item}>\n {children(item)}\n </li>\n ))}\n </ol>\n ) : (\n <ul className={styles.list}>\n {block.items.map((item, index) => (\n <li key={index} className={styles.item}>\n {children(item)}\n </li>\n ))}\n </ul>\n );\n case \"table\":\n return <Table block={block} inline={inline} />;\n default:\n return null;\n }\n}\n\n/**\n * A table, in its own scroll box.\n *\n * The box only becomes a tab stop while it actually overflows: a scroll container\n * with nothing focusable inside cannot be reached by keyboard, and adding the stop\n * unconditionally would pollute the tab order with one entry per table on the page.\n */\nfunction Table({\n block,\n inline,\n}: {\n block: Extract<MarkdownBlock, { type: \"table\" }>;\n inline: (nodes: readonly MarkdownInline[]) => ReactNode;\n}) {\n const scroll = useRef<HTMLDivElement | null>(null);\n const overflowing = useScrollOverflow(scroll, \"horizontal\");\n\n return (\n <div\n ref={scroll}\n className={styles.tableScroll}\n tabIndex={overflowing ? 0 : undefined}\n role={overflowing ? \"region\" : undefined}\n aria-label={overflowing ? \"Tabela rolável\" : undefined}\n >\n <table className={styles.table}>\n <thead>\n <tr>\n {block.head.map((cell, index) => (\n <th key={index} style={alignOf(block.align[index] ?? null)}>\n {inline(cell)}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {block.rows.map((row, rowIndex) => (\n <tr key={rowIndex}>\n {row.map((cell, cellIndex) => (\n <td key={cellIndex} style={alignOf(block.align[cellIndex] ?? null)}>\n {inline(cell)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Markdown.cjs","names":[],"sources":["../../../src/components/Markdown/Markdown.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines — the renderer maps every block and inline node the\n * parser can emit to an element, and the length is that mapping — one arm per node\n * kind, no branching to extract.\n */\nimport { Fragment, useMemo, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { useScrollOverflow } from \"@/hooks/use-scroll-overflow\";\nimport { cn } from \"@/utils/cn\";\n\nimport { CodeBlock } from \"../CodeBlock\";\nimport {\n parseMarkdown,\n type MarkdownAlign,\n type MarkdownBlock,\n type MarkdownInline,\n} from \"./markdown-parse\";\nimport styles from \"./Markdown.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\";\n\nexport interface MarkdownProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The Markdown source. */\n source: string;\n /**\n * Heading level the document's `#` maps to. Default 2.\n *\n * A comment body rendered inside a page whose `<h1>` is the page title must not\n * emit a second `<h1>`; shifting the whole scale keeps the outline valid.\n */\n headingOffset?: number;\n /** Render fenced code with `CodeBlock` (copy button, line numbers). Default `true`. */\n highlightCode?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n /** Extra props for every link — `target=\"_blank\"`, an analytics handler. */\n linkProps?: HTMLAttributes<HTMLAnchorElement> & { target?: string; rel?: string };\n}\n\n/** Render inline nodes. */\nfunction renderInline(nodes: readonly MarkdownInline[]): ReactNode {\n return nodes.map((node, index) => {\n const key = index;\n switch (node.type) {\n case \"text\":\n return <Fragment key={key}>{node.value}</Fragment>;\n case \"strong\":\n return <strong key={key}>{renderInline(node.children)}</strong>;\n case \"em\":\n return <em key={key}>{renderInline(node.children)}</em>;\n case \"del\":\n return <del key={key}>{renderInline(node.children)}</del>;\n case \"code\":\n return (\n <code key={key} className={styles.inlineCode}>\n {node.value}\n </code>\n );\n case \"image\":\n return <img key={key} className={styles.image} src={node.src} alt={node.alt} />;\n case \"break\":\n return <br key={key} />;\n default:\n return null;\n }\n });\n}\n\n/** CSS `text-align` for a table column. */\nfunction alignOf(align: MarkdownAlign): { textAlign: \"left\" | \"center\" | \"right\" } | undefined {\n return align ? { textAlign: align } : undefined;\n}\n\n/**\n * Rendered Markdown, from a node tree — never from an HTML string.\n *\n * `dangerouslySetInnerHTML` appears nowhere in this component. That is what makes\n * rendering somebody else's Markdown safe by construction rather than by a promise\n * about escaping: a `<script>` in a comment is four characters of text, because text\n * is all a React child can be.\n *\n * Link and image URLs go through a scheme **allowlist** (`http`, `https`, `mailto`,\n * `tel`, `sms`, plus relative), so `[click](javascript:alert(1))` renders as plain\n * text. A blocklist would have to enumerate every spelling of `javascript:` and\n * would miss one.\n *\n * @example\n * <Markdown source={comentario.corpo} linkProps={{ target: \"_blank\", rel: \"noreferrer\" }} />\n */\nexport function Markdown({\n source,\n headingOffset = 2,\n highlightCode = true,\n showLineNumbers = false,\n linkProps,\n className,\n ...rest\n}: MarkdownProps) {\n const blocks = useMemo(() => parseMarkdown(source), [source]);\n\n return (\n <div className={cn(styles.root, className)} {...rest}>\n {blocks.map((block, index) => (\n <Block\n key={index}\n block={block}\n headingOffset={headingOffset}\n highlightCode={highlightCode}\n showLineNumbers={showLineNumbers}\n linkProps={linkProps}\n />\n ))}\n </div>\n );\n}\n\ninterface BlockProps {\n block: MarkdownBlock;\n headingOffset: number;\n highlightCode: boolean;\n showLineNumbers: boolean;\n linkProps?: MarkdownProps[\"linkProps\"];\n}\n\n/** One block, and its children. */\nfunction Block({ block, headingOffset, highlightCode, showLineNumbers, linkProps }: BlockProps) {\n const children = (nodes: readonly MarkdownBlock[]): ReactNode =>\n nodes.map((child, index) => (\n <Block\n key={index}\n block={child}\n headingOffset={headingOffset}\n highlightCode={highlightCode}\n showLineNumbers={showLineNumbers}\n linkProps={linkProps}\n />\n ));\n\n const inline = (nodes: readonly MarkdownInline[]): ReactNode =>\n nodes.map((node, index) =>\n node.type === \"link\" ? (\n <a key={index} className={styles.link} href={node.href} {...linkProps}>\n {renderInline(node.children)}\n </a>\n ) : (\n <Fragment key={index}>{renderInline([node])}</Fragment>\n ),\n );\n\n switch (block.type) {\n case \"heading\": {\n const level = Math.min(6, Math.max(1, block.level + headingOffset - 1));\n const Tag = `h${level}` as \"h1\";\n return (\n <Tag className={styles[`h${level}` as keyof typeof styles]}>\n {inline(block.children)}\n </Tag>\n );\n }\n case \"paragraph\":\n return <p>{inline(block.children)}</p>;\n case \"code\":\n return highlightCode ? (\n <CodeBlock\n code={block.value}\n language={block.language ?? undefined}\n showLineNumbers={showLineNumbers}\n />\n ) : (\n <pre>\n <code>{block.value}</code>\n </pre>\n );\n case \"quote\":\n return <blockquote className={styles.quote}>{children(block.children)}</blockquote>;\n case \"rule\":\n return <hr className={styles.rule} />;\n case \"list\":\n return block.ordered ? (\n <ol className={styles.list} start={block.start === 1 ? undefined : block.start}>\n {block.items.map((item, index) => (\n <li key={index} className={styles.item}>\n {children(item)}\n </li>\n ))}\n </ol>\n ) : (\n <ul className={styles.list}>\n {block.items.map((item, index) => (\n <li key={index} className={styles.item}>\n {children(item)}\n </li>\n ))}\n </ul>\n );\n case \"table\":\n return <Table block={block} inline={inline} />;\n default:\n return null;\n }\n}\n\n/**\n * A table, in its own scroll box.\n *\n * The box only becomes a tab stop while it actually overflows: a scroll container\n * with nothing focusable inside cannot be reached by keyboard, and adding the stop\n * unconditionally would pollute the tab order with one entry per table on the page.\n */\nfunction Table({\n block,\n inline,\n}: {\n block: Extract<MarkdownBlock, { type: \"table\" }>;\n inline: (nodes: readonly MarkdownInline[]) => ReactNode;\n}) {\n const scroll = useRef<HTMLDivElement | null>(null);\n const overflowing = useScrollOverflow(scroll, \"horizontal\");\n\n return (\n <div\n ref={scroll}\n className={styles.tableScroll}\n tabIndex={overflowing ? 0 : undefined}\n role={overflowing ? \"region\" : undefined}\n aria-label={overflowing ? \"Tabela rolável\" : undefined}\n >\n <table className={styles.table}>\n <thead>\n <tr>\n {block.head.map((cell, index) => (\n <th key={index} style={alignOf(block.align[index] ?? null)}>\n {inline(cell)}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {block.rows.map((row, rowIndex) => (\n <tr key={rowIndex}>\n {row.map((cell, cellIndex) => (\n <td key={cellIndex} style={alignOf(block.align[cellIndex] ?? null)}>\n {inline(cell)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Markdown.js","names":[],"sources":["../../../src/components/Markdown/Markdown.tsx"],"sourcesContent":["import { Fragment, useMemo, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { useScrollOverflow } from \"@/hooks/use-scroll-overflow\";\nimport { cn } from \"@/utils/cn\";\n\nimport { CodeBlock } from \"../CodeBlock\";\nimport {\n parseMarkdown,\n type MarkdownAlign,\n type MarkdownBlock,\n type MarkdownInline,\n} from \"./markdown-parse\";\nimport styles from \"./Markdown.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\";\n\nexport interface MarkdownProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The Markdown source. */\n source: string;\n /**\n * Heading level the document's `#` maps to. Default 2.\n *\n * A comment body rendered inside a page whose `<h1>` is the page title must not\n * emit a second `<h1>`; shifting the whole scale keeps the outline valid.\n */\n headingOffset?: number;\n /** Render fenced code with `CodeBlock` (copy button, line numbers). Default `true`. */\n highlightCode?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n /** Extra props for every link — `target=\"_blank\"`, an analytics handler. */\n linkProps?: HTMLAttributes<HTMLAnchorElement> & { target?: string; rel?: string };\n}\n\n/** Render inline nodes. */\nfunction renderInline(nodes: readonly MarkdownInline[]): ReactNode {\n return nodes.map((node, index) => {\n const key = index;\n switch (node.type) {\n case \"text\":\n return <Fragment key={key}>{node.value}</Fragment>;\n case \"strong\":\n return <strong key={key}>{renderInline(node.children)}</strong>;\n case \"em\":\n return <em key={key}>{renderInline(node.children)}</em>;\n case \"del\":\n return <del key={key}>{renderInline(node.children)}</del>;\n case \"code\":\n return (\n <code key={key} className={styles.inlineCode}>\n {node.value}\n </code>\n );\n case \"image\":\n return <img key={key} className={styles.image} src={node.src} alt={node.alt} />;\n case \"break\":\n return <br key={key} />;\n default:\n return null;\n }\n });\n}\n\n/** CSS `text-align` for a table column. */\nfunction alignOf(align: MarkdownAlign): { textAlign: \"left\" | \"center\" | \"right\" } | undefined {\n return align ? { textAlign: align } : undefined;\n}\n\n/**\n * Rendered Markdown, from a node tree — never from an HTML string.\n *\n * `dangerouslySetInnerHTML` appears nowhere in this component. That is what makes\n * rendering somebody else's Markdown safe by construction rather than by a promise\n * about escaping: a `<script>` in a comment is four characters of text, because text\n * is all a React child can be.\n *\n * Link and image URLs go through a scheme **allowlist** (`http`, `https`, `mailto`,\n * `tel`, `sms`, plus relative), so `[click](javascript:alert(1))` renders as plain\n * text. A blocklist would have to enumerate every spelling of `javascript:` and\n * would miss one.\n *\n * @example\n * <Markdown source={comentario.corpo} linkProps={{ target: \"_blank\", rel: \"noreferrer\" }} />\n */\nexport function Markdown({\n source,\n headingOffset = 2,\n highlightCode = true,\n showLineNumbers = false,\n linkProps,\n className,\n ...rest\n}: MarkdownProps) {\n const blocks = useMemo(() => parseMarkdown(source), [source]);\n\n return (\n <div className={cn(styles.root, className)} {...rest}>\n {blocks.map((block, index) => (\n <Block\n key={index}\n block={block}\n headingOffset={headingOffset}\n highlightCode={highlightCode}\n showLineNumbers={showLineNumbers}\n linkProps={linkProps}\n />\n ))}\n </div>\n );\n}\n\ninterface BlockProps {\n block: MarkdownBlock;\n headingOffset: number;\n highlightCode: boolean;\n showLineNumbers: boolean;\n linkProps?: MarkdownProps[\"linkProps\"];\n}\n\n/** One block, and its children. */\nfunction Block({ block, headingOffset, highlightCode, showLineNumbers, linkProps }: BlockProps) {\n const children = (nodes: readonly MarkdownBlock[]): ReactNode =>\n nodes.map((child, index) => (\n <Block\n key={index}\n block={child}\n headingOffset={headingOffset}\n highlightCode={highlightCode}\n showLineNumbers={showLineNumbers}\n linkProps={linkProps}\n />\n ));\n\n const inline = (nodes: readonly MarkdownInline[]): ReactNode =>\n nodes.map((node, index) =>\n node.type === \"link\" ? (\n <a key={index} className={styles.link} href={node.href} {...linkProps}>\n {renderInline(node.children)}\n </a>\n ) : (\n <Fragment key={index}>{renderInline([node])}</Fragment>\n ),\n );\n\n switch (block.type) {\n case \"heading\": {\n const level = Math.min(6, Math.max(1, block.level + headingOffset - 1));\n const Tag = `h${level}` as \"h1\";\n return (\n <Tag className={styles[`h${level}` as keyof typeof styles]}>\n {inline(block.children)}\n </Tag>\n );\n }\n case \"paragraph\":\n return <p>{inline(block.children)}</p>;\n case \"code\":\n return highlightCode ? (\n <CodeBlock\n code={block.value}\n language={block.language ?? undefined}\n showLineNumbers={showLineNumbers}\n />\n ) : (\n <pre>\n <code>{block.value}</code>\n </pre>\n );\n case \"quote\":\n return <blockquote className={styles.quote}>{children(block.children)}</blockquote>;\n case \"rule\":\n return <hr className={styles.rule} />;\n case \"list\":\n return block.ordered ? (\n <ol className={styles.list} start={block.start === 1 ? undefined : block.start}>\n {block.items.map((item, index) => (\n <li key={index} className={styles.item}>\n {children(item)}\n </li>\n ))}\n </ol>\n ) : (\n <ul className={styles.list}>\n {block.items.map((item, index) => (\n <li key={index} className={styles.item}>\n {children(item)}\n </li>\n ))}\n </ul>\n );\n case \"table\":\n return <Table block={block} inline={inline} />;\n default:\n return null;\n }\n}\n\n/**\n * A table, in its own scroll box.\n *\n * The box only becomes a tab stop while it actually overflows: a scroll container\n * with nothing focusable inside cannot be reached by keyboard, and adding the stop\n * unconditionally would pollute the tab order with one entry per table on the page.\n */\nfunction Table({\n block,\n inline,\n}: {\n block: Extract<MarkdownBlock, { type: \"table\" }>;\n inline: (nodes: readonly MarkdownInline[]) => ReactNode;\n}) {\n const scroll = useRef<HTMLDivElement | null>(null);\n const overflowing = useScrollOverflow(scroll, \"horizontal\");\n\n return (\n <div\n ref={scroll}\n className={styles.tableScroll}\n tabIndex={overflowing ? 0 : undefined}\n role={overflowing ? \"region\" : undefined}\n aria-label={overflowing ? \"Tabela rolável\" : undefined}\n >\n <table className={styles.table}>\n <thead>\n <tr>\n {block.head.map((cell, index) => (\n <th key={index} style={alignOf(block.align[index] ?? null)}>\n {inline(cell)}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {block.rows.map((row, rowIndex) => (\n <tr key={rowIndex}>\n {row.map((cell, cellIndex) => (\n <td key={cellIndex} style={alignOf(block.align[cellIndex] ?? null)}>\n {inline(cell)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Markdown.js","names":[],"sources":["../../../src/components/Markdown/Markdown.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines — the renderer maps every block and inline node the\n * parser can emit to an element, and the length is that mapping — one arm per node\n * kind, no branching to extract.\n */\nimport { Fragment, useMemo, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { useScrollOverflow } from \"@/hooks/use-scroll-overflow\";\nimport { cn } from \"@/utils/cn\";\n\nimport { CodeBlock } from \"../CodeBlock\";\nimport {\n parseMarkdown,\n type MarkdownAlign,\n type MarkdownBlock,\n type MarkdownInline,\n} from \"./markdown-parse\";\nimport styles from \"./Markdown.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\";\n\nexport interface MarkdownProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The Markdown source. */\n source: string;\n /**\n * Heading level the document's `#` maps to. Default 2.\n *\n * A comment body rendered inside a page whose `<h1>` is the page title must not\n * emit a second `<h1>`; shifting the whole scale keeps the outline valid.\n */\n headingOffset?: number;\n /** Render fenced code with `CodeBlock` (copy button, line numbers). Default `true`. */\n highlightCode?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n /** Extra props for every link — `target=\"_blank\"`, an analytics handler. */\n linkProps?: HTMLAttributes<HTMLAnchorElement> & { target?: string; rel?: string };\n}\n\n/** Render inline nodes. */\nfunction renderInline(nodes: readonly MarkdownInline[]): ReactNode {\n return nodes.map((node, index) => {\n const key = index;\n switch (node.type) {\n case \"text\":\n return <Fragment key={key}>{node.value}</Fragment>;\n case \"strong\":\n return <strong key={key}>{renderInline(node.children)}</strong>;\n case \"em\":\n return <em key={key}>{renderInline(node.children)}</em>;\n case \"del\":\n return <del key={key}>{renderInline(node.children)}</del>;\n case \"code\":\n return (\n <code key={key} className={styles.inlineCode}>\n {node.value}\n </code>\n );\n case \"image\":\n return <img key={key} className={styles.image} src={node.src} alt={node.alt} />;\n case \"break\":\n return <br key={key} />;\n default:\n return null;\n }\n });\n}\n\n/** CSS `text-align` for a table column. */\nfunction alignOf(align: MarkdownAlign): { textAlign: \"left\" | \"center\" | \"right\" } | undefined {\n return align ? { textAlign: align } : undefined;\n}\n\n/**\n * Rendered Markdown, from a node tree — never from an HTML string.\n *\n * `dangerouslySetInnerHTML` appears nowhere in this component. That is what makes\n * rendering somebody else's Markdown safe by construction rather than by a promise\n * about escaping: a `<script>` in a comment is four characters of text, because text\n * is all a React child can be.\n *\n * Link and image URLs go through a scheme **allowlist** (`http`, `https`, `mailto`,\n * `tel`, `sms`, plus relative), so `[click](javascript:alert(1))` renders as plain\n * text. A blocklist would have to enumerate every spelling of `javascript:` and\n * would miss one.\n *\n * @example\n * <Markdown source={comentario.corpo} linkProps={{ target: \"_blank\", rel: \"noreferrer\" }} />\n */\nexport function Markdown({\n source,\n headingOffset = 2,\n highlightCode = true,\n showLineNumbers = false,\n linkProps,\n className,\n ...rest\n}: MarkdownProps) {\n const blocks = useMemo(() => parseMarkdown(source), [source]);\n\n return (\n <div className={cn(styles.root, className)} {...rest}>\n {blocks.map((block, index) => (\n <Block\n key={index}\n block={block}\n headingOffset={headingOffset}\n highlightCode={highlightCode}\n showLineNumbers={showLineNumbers}\n linkProps={linkProps}\n />\n ))}\n </div>\n );\n}\n\ninterface BlockProps {\n block: MarkdownBlock;\n headingOffset: number;\n highlightCode: boolean;\n showLineNumbers: boolean;\n linkProps?: MarkdownProps[\"linkProps\"];\n}\n\n/** One block, and its children. */\nfunction Block({ block, headingOffset, highlightCode, showLineNumbers, linkProps }: BlockProps) {\n const children = (nodes: readonly MarkdownBlock[]): ReactNode =>\n nodes.map((child, index) => (\n <Block\n key={index}\n block={child}\n headingOffset={headingOffset}\n highlightCode={highlightCode}\n showLineNumbers={showLineNumbers}\n linkProps={linkProps}\n />\n ));\n\n const inline = (nodes: readonly MarkdownInline[]): ReactNode =>\n nodes.map((node, index) =>\n node.type === \"link\" ? (\n <a key={index} className={styles.link} href={node.href} {...linkProps}>\n {renderInline(node.children)}\n </a>\n ) : (\n <Fragment key={index}>{renderInline([node])}</Fragment>\n ),\n );\n\n switch (block.type) {\n case \"heading\": {\n const level = Math.min(6, Math.max(1, block.level + headingOffset - 1));\n const Tag = `h${level}` as \"h1\";\n return (\n <Tag className={styles[`h${level}` as keyof typeof styles]}>\n {inline(block.children)}\n </Tag>\n );\n }\n case \"paragraph\":\n return <p>{inline(block.children)}</p>;\n case \"code\":\n return highlightCode ? (\n <CodeBlock\n code={block.value}\n language={block.language ?? undefined}\n showLineNumbers={showLineNumbers}\n />\n ) : (\n <pre>\n <code>{block.value}</code>\n </pre>\n );\n case \"quote\":\n return <blockquote className={styles.quote}>{children(block.children)}</blockquote>;\n case \"rule\":\n return <hr className={styles.rule} />;\n case \"list\":\n return block.ordered ? (\n <ol className={styles.list} start={block.start === 1 ? undefined : block.start}>\n {block.items.map((item, index) => (\n <li key={index} className={styles.item}>\n {children(item)}\n </li>\n ))}\n </ol>\n ) : (\n <ul className={styles.list}>\n {block.items.map((item, index) => (\n <li key={index} className={styles.item}>\n {children(item)}\n </li>\n ))}\n </ul>\n );\n case \"table\":\n return <Table block={block} inline={inline} />;\n default:\n return null;\n }\n}\n\n/**\n * A table, in its own scroll box.\n *\n * The box only becomes a tab stop while it actually overflows: a scroll container\n * with nothing focusable inside cannot be reached by keyboard, and adding the stop\n * unconditionally would pollute the tab order with one entry per table on the page.\n */\nfunction Table({\n block,\n inline,\n}: {\n block: Extract<MarkdownBlock, { type: \"table\" }>;\n inline: (nodes: readonly MarkdownInline[]) => ReactNode;\n}) {\n const scroll = useRef<HTMLDivElement | null>(null);\n const overflowing = useScrollOverflow(scroll, \"horizontal\");\n\n return (\n <div\n ref={scroll}\n className={styles.tableScroll}\n tabIndex={overflowing ? 0 : undefined}\n role={overflowing ? \"region\" : undefined}\n aria-label={overflowing ? \"Tabela rolável\" : undefined}\n >\n <table className={styles.table}>\n <thead>\n <tr>\n {block.head.map((cell, index) => (\n <th key={index} style={alignOf(block.align[index] ?? null)}>\n {inline(cell)}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {block.rows.map((row, rowIndex) => (\n <tr key={rowIndex}>\n {row.map((cell, cellIndex) => (\n <td key={cellIndex} style={alignOf(block.align[cellIndex] ?? null)}>\n {inline(cell)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"markdown-parse.cjs","names":[],"sources":["../../../src/components/Markdown/markdown-parse.ts"],"sourcesContent":["// A small Markdown parser for the subset an app actually renders from untrusted\n// text: comments, ticket bodies, release notes, a product description.\n//\n// It produces a node tree, never an HTML string, and the renderer turns that into\n// React elements. No `dangerouslySetInnerHTML` exists anywhere in this component —\n// which is the property that makes rendering somebody else's Markdown safe, and it\n// is structural rather than a promise about escaping.\n//\n// Raw HTML in the input is therefore not \"sanitized\", it is **text**: `<script>` in\n// a comment renders as the four characters somebody typed. That is the whole point,\n// and it is also the documented limit — this is not a CommonMark implementation and\n// does not try to be.\n\nimport { safeImageUrl, safeLinkUrl } from \"./markdown-url\";\n\n/** Inline content. */\nexport type MarkdownInline =\n | { type: \"text\"; value: string }\n | { type: \"strong\"; children: MarkdownInline[] }\n | { type: \"em\"; children: MarkdownInline[] }\n | { type: \"del\"; children: MarkdownInline[] }\n | { type: \"code\"; value: string }\n | { type: \"link\"; href: string; children: MarkdownInline[] }\n | { type: \"image\"; src: string; alt: string }\n | { type: \"break\" };\n\n/** Column alignment of a table, from the delimiter row. */\nexport type MarkdownAlign = \"left\" | \"center\" | \"right\" | null;\n\n/** Block content. */\nexport type MarkdownBlock =\n | { type: \"heading\"; level: number; children: MarkdownInline[] }\n | { type: \"paragraph\"; children: MarkdownInline[] }\n | { type: \"code\"; language: string | null; value: string }\n | { type: \"quote\"; children: MarkdownBlock[] }\n | { type: \"list\"; ordered: boolean; start: number; items: MarkdownBlock[][] }\n | {\n type: \"table\";\n align: MarkdownAlign[];\n head: MarkdownInline[][];\n rows: MarkdownInline[][][];\n }\n | { type: \"rule\" };\n\nconst FENCE = /^\\s{0,3}(`{3,}|~{3,})\\s*([\\w+-]*)\\s*$/;\nconst HEADING = /^\\s{0,3}(#{1,6})\\s+(.*?)\\s*#*\\s*$/;\nconst RULE = /^\\s{0,3}([-*_])\\s*(?:\\1\\s*){2,}$/;\nconst QUOTE = /^\\s{0,3}>\\s?(.*)$/;\nconst BULLET = /^(\\s*)([-*+])\\s+(.*)$/;\nconst ORDERED = /^(\\s*)(\\d{1,9})[.)]\\s+(.*)$/;\n/**\n * A table's delimiter row.\n *\n * The `|` is required somewhere in the line, and that is what tells a one-column\n * table (`| --- |`) apart from a thematic break (`---`) — a single column is a\n * legitimate table, and the first version of this regex demanded two.\n */\nconst TABLE_DELIMITER = /^\\s{0,3}\\|[\\s|:-]*$|^\\s{0,3}:?-+:?(\\s*\\|\\s*:?-+:?)+\\s*\\|?\\s*$/;\n\n/**\n * Parse a Markdown document into blocks.\n *\n * @param source - Raw Markdown.\n * @returns Block nodes in document order.\n */\nexport function parseMarkdown(source: string): MarkdownBlock[] {\n const lines = source.replace(/\\r\\n?/g, \"\\n\").split(\"\\n\");\n return parseBlocks(lines);\n}\n\n/** Parse a run of lines into blocks. */\nfunction parseBlocks(lines: string[]): MarkdownBlock[] {\n const blocks: MarkdownBlock[] = [];\n let i = 0;\n\n while (i < lines.length) {\n const line = lines[i];\n\n if (line.trim() === \"\") {\n i += 1;\n continue;\n }\n\n const fence = FENCE.exec(line);\n if (fence) {\n const marker = fence[1][0];\n const body: string[] = [];\n i += 1;\n while (i < lines.length && !new RegExp(`^\\\\s{0,3}${marker}{3,}\\\\s*$`).test(lines[i])) {\n body.push(lines[i]);\n i += 1;\n }\n // An unterminated fence still yields a code block: the alternative is to\n // render the rest of the document as prose full of backticks.\n i += 1;\n blocks.push({ type: \"code\", language: fence[2] || null, value: body.join(\"\\n\") });\n continue;\n }\n\n const heading = HEADING.exec(line);\n if (heading) {\n blocks.push({\n type: \"heading\",\n level: heading[1].length,\n children: parseInline(heading[2]),\n });\n i += 1;\n continue;\n }\n\n if (RULE.test(line)) {\n blocks.push({ type: \"rule\" });\n i += 1;\n continue;\n }\n\n if (QUOTE.test(line)) {\n const inner: string[] = [];\n while (i < lines.length) {\n const match = QUOTE.exec(lines[i]);\n if (match) {\n inner.push(match[1]);\n i += 1;\n continue;\n }\n // A blank line ends the quote; a plain line continues it (lazy\n // continuation), which is how people actually write quotes.\n if (lines[i].trim() === \"\") break;\n inner.push(lines[i]);\n i += 1;\n }\n blocks.push({ type: \"quote\", children: parseBlocks(inner) });\n continue;\n }\n\n if (BULLET.test(line) || ORDERED.test(line)) {\n const [list, next] = parseList(lines, i);\n blocks.push(list);\n i = next;\n continue;\n }\n\n if (i + 1 < lines.length && line.includes(\"|\") && TABLE_DELIMITER.test(lines[i + 1])) {\n const [table, next] = parseTable(lines, i);\n blocks.push(table);\n i = next;\n continue;\n }\n\n const paragraph: string[] = [];\n while (i < lines.length && lines[i].trim() !== \"\") {\n const current = lines[i];\n if (\n HEADING.test(current) ||\n RULE.test(current) ||\n FENCE.test(current) ||\n QUOTE.test(current) ||\n BULLET.test(current) ||\n ORDERED.test(current)\n ) {\n break;\n }\n paragraph.push(current.trim());\n i += 1;\n }\n if (paragraph.length > 0) {\n blocks.push({ type: \"paragraph\", children: parseInline(paragraph.join(\"\\n\")) });\n }\n }\n\n return blocks;\n}\n\n/**\n * Parse a list, including nested lists and multi-line items.\n *\n * Nesting is decided by indentation relative to the **first** item's marker, so a\n * list indented inside a quote or another list still reads correctly.\n *\n * @returns The list node and the index of the first line after it.\n */\nfunction parseList(lines: string[], start: number): [MarkdownBlock, number] {\n const bulletStart = BULLET.exec(lines[start]);\n const orderedStart = ORDERED.exec(lines[start]);\n // The two markers are mutually exclusive — `-*+` versus digits — so whichever\n // matched decides the list kind.\n const ordered = orderedStart !== null;\n const first = orderedStart ?? bulletStart;\n const baseIndent = (first?.[1] ?? \"\").length;\n const startNumber = ordered ? Number(orderedStart?.[2] ?? 1) : 1;\n\n const items: MarkdownBlock[][] = [];\n let buffer: string[] = [];\n let i = start;\n\n const flush = (): void => {\n if (buffer.length === 0) return;\n items.push(parseBlocks(buffer));\n buffer = [];\n };\n\n while (i < lines.length) {\n const line = lines[i];\n if (line.trim() === \"\") {\n // A single blank line inside a list is a loose item, not the end. Two in\n // a row end it, which is what a blank-then-paragraph document means.\n if (i + 1 < lines.length && lines[i + 1].trim() !== \"\") {\n buffer.push(\"\");\n i += 1;\n continue;\n }\n break;\n }\n const bullet = BULLET.exec(line);\n const numbered = ORDERED.exec(line);\n const match = bullet ?? numbered;\n\n if (match && match[1].length <= baseIndent) {\n const sameKind = ordered ? Boolean(numbered) : Boolean(bullet);\n if (!sameKind) break;\n flush();\n buffer.push(match[3]);\n i += 1;\n continue;\n }\n if (match) {\n // Deeper marker: keep the indentation so the recursive call sees a list.\n buffer.push(line.slice(baseIndent));\n i += 1;\n continue;\n }\n if (line.search(/\\S/) > baseIndent) {\n buffer.push(line.trim());\n i += 1;\n continue;\n }\n break;\n }\n flush();\n\n return [{ type: \"list\", ordered, start: startNumber, items }, i];\n}\n\n/** Split a table row on unescaped pipes. */\nfunction splitRow(line: string): string[] {\n const trimmed = line.trim().replace(/^\\|/, \"\").replace(/\\|$/, \"\");\n const cells: string[] = [];\n let current = \"\";\n for (let i = 0; i < trimmed.length; i += 1) {\n if (trimmed[i] === \"\\\\\" && trimmed[i + 1] === \"|\") {\n current += \"|\";\n i += 1;\n continue;\n }\n if (trimmed[i] === \"|\") {\n cells.push(current);\n current = \"\";\n continue;\n }\n current += trimmed[i];\n }\n cells.push(current);\n return cells.map((cell) => cell.trim());\n}\n\n/**\n * Parse a GFM pipe table.\n *\n * @returns The table node and the index of the first line after it.\n */\nfunction parseTable(lines: string[], start: number): [MarkdownBlock, number] {\n const head = splitRow(lines[start]);\n const align: MarkdownAlign[] = splitRow(lines[start + 1]).map((cell) => {\n const left = cell.startsWith(\":\");\n const right = cell.endsWith(\":\");\n if (left && right) return \"center\";\n if (right) return \"right\";\n if (left) return \"left\";\n return null;\n });\n\n const rows: MarkdownInline[][][] = [];\n let i = start + 2;\n while (i < lines.length && lines[i].trim() !== \"\" && lines[i].includes(\"|\")) {\n rows.push(splitRow(lines[i]).map(parseInline));\n i += 1;\n }\n\n return [{ type: \"table\", align, head: head.map(parseInline), rows }, i];\n}\n\n/** Inline delimiters, longest marker first so `**` wins over `*`. */\nconst INLINE_RULES: Array<{\n pattern: RegExp;\n build: (match: RegExpExecArray) => MarkdownInline | null;\n}> = [\n {\n pattern: /^!\\[([^\\]]*)\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+\"[^\"]*\")?\\)/,\n build: (match) => {\n const src = safeImageUrl(match[2]);\n return src ? { type: \"image\", src, alt: match[1] } : { type: \"text\", value: match[1] };\n },\n },\n {\n pattern: /^\\[([^\\]]*)\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+\"[^\"]*\")?\\)/,\n build: (match) => {\n const href = safeLinkUrl(match[2]);\n const children = parseInline(match[1]);\n // A rejected URL keeps the label as plain text: dropping the text too\n // would silently delete words somebody wrote.\n return href ? { type: \"link\", href, children } : { type: \"text\", value: match[1] };\n },\n },\n { pattern: /^`([^`]+)`/, build: (match) => ({ type: \"code\", value: match[1] }) },\n {\n pattern: /^\\*\\*([\\s\\S]+?)\\*\\*/,\n build: (match) => ({ type: \"strong\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^__([\\s\\S]+?)__/,\n build: (match) => ({ type: \"strong\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^~~([\\s\\S]+?)~~/,\n build: (match) => ({ type: \"del\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^\\*([^*\\n]+)\\*/,\n build: (match) => ({ type: \"em\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^_([^_\\n]+)_/,\n build: (match) => ({ type: \"em\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^<((?:https?:\\/\\/|mailto:)[^>\\s]+)>/,\n build: (match) => {\n const href = safeLinkUrl(match[1]);\n return href\n ? { type: \"link\", href, children: [{ type: \"text\", value: match[1] }] }\n : { type: \"text\", value: match[1] };\n },\n },\n { pattern: /^ {2,}\\n/, build: () => ({ type: \"break\" }) },\n { pattern: /^\\\\\\n/, build: () => ({ type: \"break\" }) },\n];\n\n/**\n * Parse inline Markdown.\n *\n * A backslash escapes the next character, so `\\*not italic\\*` renders with the\n * asterisks. Anything that matches no rule is text — an unclosed `**` is two\n * asterisks, not a bold run to the end of the paragraph.\n *\n * @param source - One block's text.\n * @returns Inline nodes.\n */\nexport function parseInline(source: string): MarkdownInline[] {\n const nodes: MarkdownInline[] = [];\n let text = \"\";\n let i = 0;\n\n const flush = (): void => {\n if (text) nodes.push({ type: \"text\", value: text });\n text = \"\";\n };\n\n while (i < source.length) {\n if (source[i] === \"\\\\\" && i + 1 < source.length && source[i + 1] !== \"\\n\") {\n text += source[i + 1];\n i += 2;\n continue;\n }\n\n const rest = source.slice(i);\n let matched = false;\n for (const rule of INLINE_RULES) {\n const match = rule.pattern.exec(rest);\n if (!match) continue;\n const node = rule.build(match);\n if (!node) continue;\n flush();\n nodes.push(node);\n i += match[0].length;\n matched = true;\n break;\n }\n if (matched) continue;\n\n text += source[i];\n i += 1;\n }\n\n flush();\n return nodes;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"markdown-parse.cjs","names":[],"sources":["../../../src/components/Markdown/markdown-parse.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — a Markdown block parser is a state\n * machine over line kinds: paragraph, heading, fence, list, quote, table and\n * thematic break each need to see the lines around them to know where they end.\n * parseBlocks is that loop, and cutting it into per-kind functions would hand each\n * one the same lookahead cursor.\n */\n// A small Markdown parser for the subset an app actually renders from untrusted\n// text: comments, ticket bodies, release notes, a product description.\n//\n// It produces a node tree, never an HTML string, and the renderer turns that into\n// React elements. No `dangerouslySetInnerHTML` exists anywhere in this component —\n// which is the property that makes rendering somebody else's Markdown safe, and it\n// is structural rather than a promise about escaping.\n//\n// Raw HTML in the input is therefore not \"sanitized\", it is **text**: `<script>` in\n// a comment renders as the four characters somebody typed. That is the whole point,\n// and it is also the documented limit — this is not a CommonMark implementation and\n// does not try to be.\n\nimport { safeImageUrl, safeLinkUrl } from \"./markdown-url\";\n\n/** Inline content. */\nexport type MarkdownInline =\n | { type: \"text\"; value: string }\n | { type: \"strong\"; children: MarkdownInline[] }\n | { type: \"em\"; children: MarkdownInline[] }\n | { type: \"del\"; children: MarkdownInline[] }\n | { type: \"code\"; value: string }\n | { type: \"link\"; href: string; children: MarkdownInline[] }\n | { type: \"image\"; src: string; alt: string }\n | { type: \"break\" };\n\n/** Column alignment of a table, from the delimiter row. */\nexport type MarkdownAlign = \"left\" | \"center\" | \"right\" | null;\n\n/** Block content. */\nexport type MarkdownBlock =\n | { type: \"heading\"; level: number; children: MarkdownInline[] }\n | { type: \"paragraph\"; children: MarkdownInline[] }\n | { type: \"code\"; language: string | null; value: string }\n | { type: \"quote\"; children: MarkdownBlock[] }\n | { type: \"list\"; ordered: boolean; start: number; items: MarkdownBlock[][] }\n | {\n type: \"table\";\n align: MarkdownAlign[];\n head: MarkdownInline[][];\n rows: MarkdownInline[][][];\n }\n | { type: \"rule\" };\n\nconst FENCE = /^\\s{0,3}(`{3,}|~{3,})\\s*([\\w+-]*)\\s*$/;\nconst HEADING = /^\\s{0,3}(#{1,6})\\s+(.*?)\\s*#*\\s*$/;\nconst RULE = /^\\s{0,3}([-*_])\\s*(?:\\1\\s*){2,}$/;\nconst QUOTE = /^\\s{0,3}>\\s?(.*)$/;\nconst BULLET = /^(\\s*)([-*+])\\s+(.*)$/;\nconst ORDERED = /^(\\s*)(\\d{1,9})[.)]\\s+(.*)$/;\n/**\n * A table's delimiter row.\n *\n * The `|` is required somewhere in the line, and that is what tells a one-column\n * table (`| --- |`) apart from a thematic break (`---`) — a single column is a\n * legitimate table, and the first version of this regex demanded two.\n */\nconst TABLE_DELIMITER = /^\\s{0,3}\\|[\\s|:-]*$|^\\s{0,3}:?-+:?(\\s*\\|\\s*:?-+:?)+\\s*\\|?\\s*$/;\n\n/**\n * Parse a Markdown document into blocks.\n *\n * @param source - Raw Markdown.\n * @returns Block nodes in document order.\n */\nexport function parseMarkdown(source: string): MarkdownBlock[] {\n const lines = source.replace(/\\r\\n?/g, \"\\n\").split(\"\\n\");\n return parseBlocks(lines);\n}\n\n/** Parse a run of lines into blocks. */\nfunction parseBlocks(lines: string[]): MarkdownBlock[] {\n const blocks: MarkdownBlock[] = [];\n let i = 0;\n\n while (i < lines.length) {\n const line = lines[i];\n\n if (line.trim() === \"\") {\n i += 1;\n continue;\n }\n\n const fence = FENCE.exec(line);\n if (fence) {\n const marker = fence[1][0];\n const body: string[] = [];\n i += 1;\n while (i < lines.length && !new RegExp(`^\\\\s{0,3}${marker}{3,}\\\\s*$`).test(lines[i])) {\n body.push(lines[i]);\n i += 1;\n }\n // An unterminated fence still yields a code block: the alternative is to\n // render the rest of the document as prose full of backticks.\n i += 1;\n blocks.push({ type: \"code\", language: fence[2] || null, value: body.join(\"\\n\") });\n continue;\n }\n\n const heading = HEADING.exec(line);\n if (heading) {\n blocks.push({\n type: \"heading\",\n level: heading[1].length,\n children: parseInline(heading[2]),\n });\n i += 1;\n continue;\n }\n\n if (RULE.test(line)) {\n blocks.push({ type: \"rule\" });\n i += 1;\n continue;\n }\n\n if (QUOTE.test(line)) {\n const inner: string[] = [];\n while (i < lines.length) {\n const match = QUOTE.exec(lines[i]);\n if (match) {\n inner.push(match[1]);\n i += 1;\n continue;\n }\n // A blank line ends the quote; a plain line continues it (lazy\n // continuation), which is how people actually write quotes.\n if (lines[i].trim() === \"\") break;\n inner.push(lines[i]);\n i += 1;\n }\n blocks.push({ type: \"quote\", children: parseBlocks(inner) });\n continue;\n }\n\n if (BULLET.test(line) || ORDERED.test(line)) {\n const [list, next] = parseList(lines, i);\n blocks.push(list);\n i = next;\n continue;\n }\n\n if (i + 1 < lines.length && line.includes(\"|\") && TABLE_DELIMITER.test(lines[i + 1])) {\n const [table, next] = parseTable(lines, i);\n blocks.push(table);\n i = next;\n continue;\n }\n\n const paragraph: string[] = [];\n while (i < lines.length && lines[i].trim() !== \"\") {\n const current = lines[i];\n if (\n HEADING.test(current) ||\n RULE.test(current) ||\n FENCE.test(current) ||\n QUOTE.test(current) ||\n BULLET.test(current) ||\n ORDERED.test(current)\n ) {\n break;\n }\n paragraph.push(current.trim());\n i += 1;\n }\n if (paragraph.length > 0) {\n blocks.push({ type: \"paragraph\", children: parseInline(paragraph.join(\"\\n\")) });\n }\n }\n\n return blocks;\n}\n\n/**\n * Parse a list, including nested lists and multi-line items.\n *\n * Nesting is decided by indentation relative to the **first** item's marker, so a\n * list indented inside a quote or another list still reads correctly.\n *\n * @returns The list node and the index of the first line after it.\n */\nfunction parseList(lines: string[], start: number): [MarkdownBlock, number] {\n const bulletStart = BULLET.exec(lines[start]);\n const orderedStart = ORDERED.exec(lines[start]);\n // The two markers are mutually exclusive — `-*+` versus digits — so whichever\n // matched decides the list kind.\n const ordered = orderedStart !== null;\n const first = orderedStart ?? bulletStart;\n const baseIndent = (first?.[1] ?? \"\").length;\n const startNumber = ordered ? Number(orderedStart?.[2] ?? 1) : 1;\n\n const items: MarkdownBlock[][] = [];\n let buffer: string[] = [];\n let i = start;\n\n const flush = (): void => {\n if (buffer.length === 0) return;\n items.push(parseBlocks(buffer));\n buffer = [];\n };\n\n while (i < lines.length) {\n const line = lines[i];\n if (line.trim() === \"\") {\n // A single blank line inside a list is a loose item, not the end. Two in\n // a row end it, which is what a blank-then-paragraph document means.\n if (i + 1 < lines.length && lines[i + 1].trim() !== \"\") {\n buffer.push(\"\");\n i += 1;\n continue;\n }\n break;\n }\n const bullet = BULLET.exec(line);\n const numbered = ORDERED.exec(line);\n const match = bullet ?? numbered;\n\n if (match && match[1].length <= baseIndent) {\n const sameKind = ordered ? Boolean(numbered) : Boolean(bullet);\n if (!sameKind) break;\n flush();\n buffer.push(match[3]);\n i += 1;\n continue;\n }\n if (match) {\n // Deeper marker: keep the indentation so the recursive call sees a list.\n buffer.push(line.slice(baseIndent));\n i += 1;\n continue;\n }\n if (line.search(/\\S/) > baseIndent) {\n buffer.push(line.trim());\n i += 1;\n continue;\n }\n break;\n }\n flush();\n\n return [{ type: \"list\", ordered, start: startNumber, items }, i];\n}\n\n/** Split a table row on unescaped pipes. */\nfunction splitRow(line: string): string[] {\n const trimmed = line.trim().replace(/^\\|/, \"\").replace(/\\|$/, \"\");\n const cells: string[] = [];\n let current = \"\";\n for (let i = 0; i < trimmed.length; i += 1) {\n if (trimmed[i] === \"\\\\\" && trimmed[i + 1] === \"|\") {\n current += \"|\";\n i += 1;\n continue;\n }\n if (trimmed[i] === \"|\") {\n cells.push(current);\n current = \"\";\n continue;\n }\n current += trimmed[i];\n }\n cells.push(current);\n return cells.map((cell) => cell.trim());\n}\n\n/**\n * Parse a GFM pipe table.\n *\n * @returns The table node and the index of the first line after it.\n */\nfunction parseTable(lines: string[], start: number): [MarkdownBlock, number] {\n const head = splitRow(lines[start]);\n const align: MarkdownAlign[] = splitRow(lines[start + 1]).map((cell) => {\n const left = cell.startsWith(\":\");\n const right = cell.endsWith(\":\");\n if (left && right) return \"center\";\n if (right) return \"right\";\n if (left) return \"left\";\n return null;\n });\n\n const rows: MarkdownInline[][][] = [];\n let i = start + 2;\n while (i < lines.length && lines[i].trim() !== \"\" && lines[i].includes(\"|\")) {\n rows.push(splitRow(lines[i]).map(parseInline));\n i += 1;\n }\n\n return [{ type: \"table\", align, head: head.map(parseInline), rows }, i];\n}\n\n/** Inline delimiters, longest marker first so `**` wins over `*`. */\nconst INLINE_RULES: Array<{\n pattern: RegExp;\n build: (match: RegExpExecArray) => MarkdownInline | null;\n}> = [\n {\n pattern: /^!\\[([^\\]]*)\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+\"[^\"]*\")?\\)/,\n build: (match) => {\n const src = safeImageUrl(match[2]);\n return src ? { type: \"image\", src, alt: match[1] } : { type: \"text\", value: match[1] };\n },\n },\n {\n pattern: /^\\[([^\\]]*)\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+\"[^\"]*\")?\\)/,\n build: (match) => {\n const href = safeLinkUrl(match[2]);\n const children = parseInline(match[1]);\n // A rejected URL keeps the label as plain text: dropping the text too\n // would silently delete words somebody wrote.\n return href ? { type: \"link\", href, children } : { type: \"text\", value: match[1] };\n },\n },\n { pattern: /^`([^`]+)`/, build: (match) => ({ type: \"code\", value: match[1] }) },\n {\n pattern: /^\\*\\*([\\s\\S]+?)\\*\\*/,\n build: (match) => ({ type: \"strong\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^__([\\s\\S]+?)__/,\n build: (match) => ({ type: \"strong\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^~~([\\s\\S]+?)~~/,\n build: (match) => ({ type: \"del\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^\\*([^*\\n]+)\\*/,\n build: (match) => ({ type: \"em\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^_([^_\\n]+)_/,\n build: (match) => ({ type: \"em\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^<((?:https?:\\/\\/|mailto:)[^>\\s]+)>/,\n build: (match) => {\n const href = safeLinkUrl(match[1]);\n return href\n ? { type: \"link\", href, children: [{ type: \"text\", value: match[1] }] }\n : { type: \"text\", value: match[1] };\n },\n },\n { pattern: /^ {2,}\\n/, build: () => ({ type: \"break\" }) },\n { pattern: /^\\\\\\n/, build: () => ({ type: \"break\" }) },\n];\n\n/**\n * Parse inline Markdown.\n *\n * A backslash escapes the next character, so `\\*not italic\\*` renders with the\n * asterisks. Anything that matches no rule is text — an unclosed `**` is two\n * asterisks, not a bold run to the end of the paragraph.\n *\n * @param source - One block's text.\n * @returns Inline nodes.\n */\nexport function parseInline(source: string): MarkdownInline[] {\n const nodes: MarkdownInline[] = [];\n let text = \"\";\n let i = 0;\n\n const flush = (): void => {\n if (text) nodes.push({ type: \"text\", value: text });\n text = \"\";\n };\n\n while (i < source.length) {\n if (source[i] === \"\\\\\" && i + 1 < source.length && source[i + 1] !== \"\\n\") {\n text += source[i + 1];\n i += 2;\n continue;\n }\n\n const rest = source.slice(i);\n let matched = false;\n for (const rule of INLINE_RULES) {\n const match = rule.pattern.exec(rest);\n if (!match) continue;\n const node = rule.build(match);\n if (!node) continue;\n flush();\n nodes.push(node);\n i += match[0].length;\n matched = true;\n break;\n }\n if (matched) continue;\n\n text += source[i];\n i += 1;\n }\n\n flush();\n return nodes;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"markdown-parse.js","names":[],"sources":["../../../src/components/Markdown/markdown-parse.ts"],"sourcesContent":["// A small Markdown parser for the subset an app actually renders from untrusted\n// text: comments, ticket bodies, release notes, a product description.\n//\n// It produces a node tree, never an HTML string, and the renderer turns that into\n// React elements. No `dangerouslySetInnerHTML` exists anywhere in this component —\n// which is the property that makes rendering somebody else's Markdown safe, and it\n// is structural rather than a promise about escaping.\n//\n// Raw HTML in the input is therefore not \"sanitized\", it is **text**: `<script>` in\n// a comment renders as the four characters somebody typed. That is the whole point,\n// and it is also the documented limit — this is not a CommonMark implementation and\n// does not try to be.\n\nimport { safeImageUrl, safeLinkUrl } from \"./markdown-url\";\n\n/** Inline content. */\nexport type MarkdownInline =\n | { type: \"text\"; value: string }\n | { type: \"strong\"; children: MarkdownInline[] }\n | { type: \"em\"; children: MarkdownInline[] }\n | { type: \"del\"; children: MarkdownInline[] }\n | { type: \"code\"; value: string }\n | { type: \"link\"; href: string; children: MarkdownInline[] }\n | { type: \"image\"; src: string; alt: string }\n | { type: \"break\" };\n\n/** Column alignment of a table, from the delimiter row. */\nexport type MarkdownAlign = \"left\" | \"center\" | \"right\" | null;\n\n/** Block content. */\nexport type MarkdownBlock =\n | { type: \"heading\"; level: number; children: MarkdownInline[] }\n | { type: \"paragraph\"; children: MarkdownInline[] }\n | { type: \"code\"; language: string | null; value: string }\n | { type: \"quote\"; children: MarkdownBlock[] }\n | { type: \"list\"; ordered: boolean; start: number; items: MarkdownBlock[][] }\n | {\n type: \"table\";\n align: MarkdownAlign[];\n head: MarkdownInline[][];\n rows: MarkdownInline[][][];\n }\n | { type: \"rule\" };\n\nconst FENCE = /^\\s{0,3}(`{3,}|~{3,})\\s*([\\w+-]*)\\s*$/;\nconst HEADING = /^\\s{0,3}(#{1,6})\\s+(.*?)\\s*#*\\s*$/;\nconst RULE = /^\\s{0,3}([-*_])\\s*(?:\\1\\s*){2,}$/;\nconst QUOTE = /^\\s{0,3}>\\s?(.*)$/;\nconst BULLET = /^(\\s*)([-*+])\\s+(.*)$/;\nconst ORDERED = /^(\\s*)(\\d{1,9})[.)]\\s+(.*)$/;\n/**\n * A table's delimiter row.\n *\n * The `|` is required somewhere in the line, and that is what tells a one-column\n * table (`| --- |`) apart from a thematic break (`---`) — a single column is a\n * legitimate table, and the first version of this regex demanded two.\n */\nconst TABLE_DELIMITER = /^\\s{0,3}\\|[\\s|:-]*$|^\\s{0,3}:?-+:?(\\s*\\|\\s*:?-+:?)+\\s*\\|?\\s*$/;\n\n/**\n * Parse a Markdown document into blocks.\n *\n * @param source - Raw Markdown.\n * @returns Block nodes in document order.\n */\nexport function parseMarkdown(source: string): MarkdownBlock[] {\n const lines = source.replace(/\\r\\n?/g, \"\\n\").split(\"\\n\");\n return parseBlocks(lines);\n}\n\n/** Parse a run of lines into blocks. */\nfunction parseBlocks(lines: string[]): MarkdownBlock[] {\n const blocks: MarkdownBlock[] = [];\n let i = 0;\n\n while (i < lines.length) {\n const line = lines[i];\n\n if (line.trim() === \"\") {\n i += 1;\n continue;\n }\n\n const fence = FENCE.exec(line);\n if (fence) {\n const marker = fence[1][0];\n const body: string[] = [];\n i += 1;\n while (i < lines.length && !new RegExp(`^\\\\s{0,3}${marker}{3,}\\\\s*$`).test(lines[i])) {\n body.push(lines[i]);\n i += 1;\n }\n // An unterminated fence still yields a code block: the alternative is to\n // render the rest of the document as prose full of backticks.\n i += 1;\n blocks.push({ type: \"code\", language: fence[2] || null, value: body.join(\"\\n\") });\n continue;\n }\n\n const heading = HEADING.exec(line);\n if (heading) {\n blocks.push({\n type: \"heading\",\n level: heading[1].length,\n children: parseInline(heading[2]),\n });\n i += 1;\n continue;\n }\n\n if (RULE.test(line)) {\n blocks.push({ type: \"rule\" });\n i += 1;\n continue;\n }\n\n if (QUOTE.test(line)) {\n const inner: string[] = [];\n while (i < lines.length) {\n const match = QUOTE.exec(lines[i]);\n if (match) {\n inner.push(match[1]);\n i += 1;\n continue;\n }\n // A blank line ends the quote; a plain line continues it (lazy\n // continuation), which is how people actually write quotes.\n if (lines[i].trim() === \"\") break;\n inner.push(lines[i]);\n i += 1;\n }\n blocks.push({ type: \"quote\", children: parseBlocks(inner) });\n continue;\n }\n\n if (BULLET.test(line) || ORDERED.test(line)) {\n const [list, next] = parseList(lines, i);\n blocks.push(list);\n i = next;\n continue;\n }\n\n if (i + 1 < lines.length && line.includes(\"|\") && TABLE_DELIMITER.test(lines[i + 1])) {\n const [table, next] = parseTable(lines, i);\n blocks.push(table);\n i = next;\n continue;\n }\n\n const paragraph: string[] = [];\n while (i < lines.length && lines[i].trim() !== \"\") {\n const current = lines[i];\n if (\n HEADING.test(current) ||\n RULE.test(current) ||\n FENCE.test(current) ||\n QUOTE.test(current) ||\n BULLET.test(current) ||\n ORDERED.test(current)\n ) {\n break;\n }\n paragraph.push(current.trim());\n i += 1;\n }\n if (paragraph.length > 0) {\n blocks.push({ type: \"paragraph\", children: parseInline(paragraph.join(\"\\n\")) });\n }\n }\n\n return blocks;\n}\n\n/**\n * Parse a list, including nested lists and multi-line items.\n *\n * Nesting is decided by indentation relative to the **first** item's marker, so a\n * list indented inside a quote or another list still reads correctly.\n *\n * @returns The list node and the index of the first line after it.\n */\nfunction parseList(lines: string[], start: number): [MarkdownBlock, number] {\n const bulletStart = BULLET.exec(lines[start]);\n const orderedStart = ORDERED.exec(lines[start]);\n // The two markers are mutually exclusive — `-*+` versus digits — so whichever\n // matched decides the list kind.\n const ordered = orderedStart !== null;\n const first = orderedStart ?? bulletStart;\n const baseIndent = (first?.[1] ?? \"\").length;\n const startNumber = ordered ? Number(orderedStart?.[2] ?? 1) : 1;\n\n const items: MarkdownBlock[][] = [];\n let buffer: string[] = [];\n let i = start;\n\n const flush = (): void => {\n if (buffer.length === 0) return;\n items.push(parseBlocks(buffer));\n buffer = [];\n };\n\n while (i < lines.length) {\n const line = lines[i];\n if (line.trim() === \"\") {\n // A single blank line inside a list is a loose item, not the end. Two in\n // a row end it, which is what a blank-then-paragraph document means.\n if (i + 1 < lines.length && lines[i + 1].trim() !== \"\") {\n buffer.push(\"\");\n i += 1;\n continue;\n }\n break;\n }\n const bullet = BULLET.exec(line);\n const numbered = ORDERED.exec(line);\n const match = bullet ?? numbered;\n\n if (match && match[1].length <= baseIndent) {\n const sameKind = ordered ? Boolean(numbered) : Boolean(bullet);\n if (!sameKind) break;\n flush();\n buffer.push(match[3]);\n i += 1;\n continue;\n }\n if (match) {\n // Deeper marker: keep the indentation so the recursive call sees a list.\n buffer.push(line.slice(baseIndent));\n i += 1;\n continue;\n }\n if (line.search(/\\S/) > baseIndent) {\n buffer.push(line.trim());\n i += 1;\n continue;\n }\n break;\n }\n flush();\n\n return [{ type: \"list\", ordered, start: startNumber, items }, i];\n}\n\n/** Split a table row on unescaped pipes. */\nfunction splitRow(line: string): string[] {\n const trimmed = line.trim().replace(/^\\|/, \"\").replace(/\\|$/, \"\");\n const cells: string[] = [];\n let current = \"\";\n for (let i = 0; i < trimmed.length; i += 1) {\n if (trimmed[i] === \"\\\\\" && trimmed[i + 1] === \"|\") {\n current += \"|\";\n i += 1;\n continue;\n }\n if (trimmed[i] === \"|\") {\n cells.push(current);\n current = \"\";\n continue;\n }\n current += trimmed[i];\n }\n cells.push(current);\n return cells.map((cell) => cell.trim());\n}\n\n/**\n * Parse a GFM pipe table.\n *\n * @returns The table node and the index of the first line after it.\n */\nfunction parseTable(lines: string[], start: number): [MarkdownBlock, number] {\n const head = splitRow(lines[start]);\n const align: MarkdownAlign[] = splitRow(lines[start + 1]).map((cell) => {\n const left = cell.startsWith(\":\");\n const right = cell.endsWith(\":\");\n if (left && right) return \"center\";\n if (right) return \"right\";\n if (left) return \"left\";\n return null;\n });\n\n const rows: MarkdownInline[][][] = [];\n let i = start + 2;\n while (i < lines.length && lines[i].trim() !== \"\" && lines[i].includes(\"|\")) {\n rows.push(splitRow(lines[i]).map(parseInline));\n i += 1;\n }\n\n return [{ type: \"table\", align, head: head.map(parseInline), rows }, i];\n}\n\n/** Inline delimiters, longest marker first so `**` wins over `*`. */\nconst INLINE_RULES: Array<{\n pattern: RegExp;\n build: (match: RegExpExecArray) => MarkdownInline | null;\n}> = [\n {\n pattern: /^!\\[([^\\]]*)\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+\"[^\"]*\")?\\)/,\n build: (match) => {\n const src = safeImageUrl(match[2]);\n return src ? { type: \"image\", src, alt: match[1] } : { type: \"text\", value: match[1] };\n },\n },\n {\n pattern: /^\\[([^\\]]*)\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+\"[^\"]*\")?\\)/,\n build: (match) => {\n const href = safeLinkUrl(match[2]);\n const children = parseInline(match[1]);\n // A rejected URL keeps the label as plain text: dropping the text too\n // would silently delete words somebody wrote.\n return href ? { type: \"link\", href, children } : { type: \"text\", value: match[1] };\n },\n },\n { pattern: /^`([^`]+)`/, build: (match) => ({ type: \"code\", value: match[1] }) },\n {\n pattern: /^\\*\\*([\\s\\S]+?)\\*\\*/,\n build: (match) => ({ type: \"strong\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^__([\\s\\S]+?)__/,\n build: (match) => ({ type: \"strong\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^~~([\\s\\S]+?)~~/,\n build: (match) => ({ type: \"del\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^\\*([^*\\n]+)\\*/,\n build: (match) => ({ type: \"em\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^_([^_\\n]+)_/,\n build: (match) => ({ type: \"em\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^<((?:https?:\\/\\/|mailto:)[^>\\s]+)>/,\n build: (match) => {\n const href = safeLinkUrl(match[1]);\n return href\n ? { type: \"link\", href, children: [{ type: \"text\", value: match[1] }] }\n : { type: \"text\", value: match[1] };\n },\n },\n { pattern: /^ {2,}\\n/, build: () => ({ type: \"break\" }) },\n { pattern: /^\\\\\\n/, build: () => ({ type: \"break\" }) },\n];\n\n/**\n * Parse inline Markdown.\n *\n * A backslash escapes the next character, so `\\*not italic\\*` renders with the\n * asterisks. Anything that matches no rule is text — an unclosed `**` is two\n * asterisks, not a bold run to the end of the paragraph.\n *\n * @param source - One block's text.\n * @returns Inline nodes.\n */\nexport function parseInline(source: string): MarkdownInline[] {\n const nodes: MarkdownInline[] = [];\n let text = \"\";\n let i = 0;\n\n const flush = (): void => {\n if (text) nodes.push({ type: \"text\", value: text });\n text = \"\";\n };\n\n while (i < source.length) {\n if (source[i] === \"\\\\\" && i + 1 < source.length && source[i + 1] !== \"\\n\") {\n text += source[i + 1];\n i += 2;\n continue;\n }\n\n const rest = source.slice(i);\n let matched = false;\n for (const rule of INLINE_RULES) {\n const match = rule.pattern.exec(rest);\n if (!match) continue;\n const node = rule.build(match);\n if (!node) continue;\n flush();\n nodes.push(node);\n i += match[0].length;\n matched = true;\n break;\n }\n if (matched) continue;\n\n text += source[i];\n i += 1;\n }\n\n flush();\n return nodes;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"markdown-parse.js","names":[],"sources":["../../../src/components/Markdown/markdown-parse.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — a Markdown block parser is a state\n * machine over line kinds: paragraph, heading, fence, list, quote, table and\n * thematic break each need to see the lines around them to know where they end.\n * parseBlocks is that loop, and cutting it into per-kind functions would hand each\n * one the same lookahead cursor.\n */\n// A small Markdown parser for the subset an app actually renders from untrusted\n// text: comments, ticket bodies, release notes, a product description.\n//\n// It produces a node tree, never an HTML string, and the renderer turns that into\n// React elements. No `dangerouslySetInnerHTML` exists anywhere in this component —\n// which is the property that makes rendering somebody else's Markdown safe, and it\n// is structural rather than a promise about escaping.\n//\n// Raw HTML in the input is therefore not \"sanitized\", it is **text**: `<script>` in\n// a comment renders as the four characters somebody typed. That is the whole point,\n// and it is also the documented limit — this is not a CommonMark implementation and\n// does not try to be.\n\nimport { safeImageUrl, safeLinkUrl } from \"./markdown-url\";\n\n/** Inline content. */\nexport type MarkdownInline =\n | { type: \"text\"; value: string }\n | { type: \"strong\"; children: MarkdownInline[] }\n | { type: \"em\"; children: MarkdownInline[] }\n | { type: \"del\"; children: MarkdownInline[] }\n | { type: \"code\"; value: string }\n | { type: \"link\"; href: string; children: MarkdownInline[] }\n | { type: \"image\"; src: string; alt: string }\n | { type: \"break\" };\n\n/** Column alignment of a table, from the delimiter row. */\nexport type MarkdownAlign = \"left\" | \"center\" | \"right\" | null;\n\n/** Block content. */\nexport type MarkdownBlock =\n | { type: \"heading\"; level: number; children: MarkdownInline[] }\n | { type: \"paragraph\"; children: MarkdownInline[] }\n | { type: \"code\"; language: string | null; value: string }\n | { type: \"quote\"; children: MarkdownBlock[] }\n | { type: \"list\"; ordered: boolean; start: number; items: MarkdownBlock[][] }\n | {\n type: \"table\";\n align: MarkdownAlign[];\n head: MarkdownInline[][];\n rows: MarkdownInline[][][];\n }\n | { type: \"rule\" };\n\nconst FENCE = /^\\s{0,3}(`{3,}|~{3,})\\s*([\\w+-]*)\\s*$/;\nconst HEADING = /^\\s{0,3}(#{1,6})\\s+(.*?)\\s*#*\\s*$/;\nconst RULE = /^\\s{0,3}([-*_])\\s*(?:\\1\\s*){2,}$/;\nconst QUOTE = /^\\s{0,3}>\\s?(.*)$/;\nconst BULLET = /^(\\s*)([-*+])\\s+(.*)$/;\nconst ORDERED = /^(\\s*)(\\d{1,9})[.)]\\s+(.*)$/;\n/**\n * A table's delimiter row.\n *\n * The `|` is required somewhere in the line, and that is what tells a one-column\n * table (`| --- |`) apart from a thematic break (`---`) — a single column is a\n * legitimate table, and the first version of this regex demanded two.\n */\nconst TABLE_DELIMITER = /^\\s{0,3}\\|[\\s|:-]*$|^\\s{0,3}:?-+:?(\\s*\\|\\s*:?-+:?)+\\s*\\|?\\s*$/;\n\n/**\n * Parse a Markdown document into blocks.\n *\n * @param source - Raw Markdown.\n * @returns Block nodes in document order.\n */\nexport function parseMarkdown(source: string): MarkdownBlock[] {\n const lines = source.replace(/\\r\\n?/g, \"\\n\").split(\"\\n\");\n return parseBlocks(lines);\n}\n\n/** Parse a run of lines into blocks. */\nfunction parseBlocks(lines: string[]): MarkdownBlock[] {\n const blocks: MarkdownBlock[] = [];\n let i = 0;\n\n while (i < lines.length) {\n const line = lines[i];\n\n if (line.trim() === \"\") {\n i += 1;\n continue;\n }\n\n const fence = FENCE.exec(line);\n if (fence) {\n const marker = fence[1][0];\n const body: string[] = [];\n i += 1;\n while (i < lines.length && !new RegExp(`^\\\\s{0,3}${marker}{3,}\\\\s*$`).test(lines[i])) {\n body.push(lines[i]);\n i += 1;\n }\n // An unterminated fence still yields a code block: the alternative is to\n // render the rest of the document as prose full of backticks.\n i += 1;\n blocks.push({ type: \"code\", language: fence[2] || null, value: body.join(\"\\n\") });\n continue;\n }\n\n const heading = HEADING.exec(line);\n if (heading) {\n blocks.push({\n type: \"heading\",\n level: heading[1].length,\n children: parseInline(heading[2]),\n });\n i += 1;\n continue;\n }\n\n if (RULE.test(line)) {\n blocks.push({ type: \"rule\" });\n i += 1;\n continue;\n }\n\n if (QUOTE.test(line)) {\n const inner: string[] = [];\n while (i < lines.length) {\n const match = QUOTE.exec(lines[i]);\n if (match) {\n inner.push(match[1]);\n i += 1;\n continue;\n }\n // A blank line ends the quote; a plain line continues it (lazy\n // continuation), which is how people actually write quotes.\n if (lines[i].trim() === \"\") break;\n inner.push(lines[i]);\n i += 1;\n }\n blocks.push({ type: \"quote\", children: parseBlocks(inner) });\n continue;\n }\n\n if (BULLET.test(line) || ORDERED.test(line)) {\n const [list, next] = parseList(lines, i);\n blocks.push(list);\n i = next;\n continue;\n }\n\n if (i + 1 < lines.length && line.includes(\"|\") && TABLE_DELIMITER.test(lines[i + 1])) {\n const [table, next] = parseTable(lines, i);\n blocks.push(table);\n i = next;\n continue;\n }\n\n const paragraph: string[] = [];\n while (i < lines.length && lines[i].trim() !== \"\") {\n const current = lines[i];\n if (\n HEADING.test(current) ||\n RULE.test(current) ||\n FENCE.test(current) ||\n QUOTE.test(current) ||\n BULLET.test(current) ||\n ORDERED.test(current)\n ) {\n break;\n }\n paragraph.push(current.trim());\n i += 1;\n }\n if (paragraph.length > 0) {\n blocks.push({ type: \"paragraph\", children: parseInline(paragraph.join(\"\\n\")) });\n }\n }\n\n return blocks;\n}\n\n/**\n * Parse a list, including nested lists and multi-line items.\n *\n * Nesting is decided by indentation relative to the **first** item's marker, so a\n * list indented inside a quote or another list still reads correctly.\n *\n * @returns The list node and the index of the first line after it.\n */\nfunction parseList(lines: string[], start: number): [MarkdownBlock, number] {\n const bulletStart = BULLET.exec(lines[start]);\n const orderedStart = ORDERED.exec(lines[start]);\n // The two markers are mutually exclusive — `-*+` versus digits — so whichever\n // matched decides the list kind.\n const ordered = orderedStart !== null;\n const first = orderedStart ?? bulletStart;\n const baseIndent = (first?.[1] ?? \"\").length;\n const startNumber = ordered ? Number(orderedStart?.[2] ?? 1) : 1;\n\n const items: MarkdownBlock[][] = [];\n let buffer: string[] = [];\n let i = start;\n\n const flush = (): void => {\n if (buffer.length === 0) return;\n items.push(parseBlocks(buffer));\n buffer = [];\n };\n\n while (i < lines.length) {\n const line = lines[i];\n if (line.trim() === \"\") {\n // A single blank line inside a list is a loose item, not the end. Two in\n // a row end it, which is what a blank-then-paragraph document means.\n if (i + 1 < lines.length && lines[i + 1].trim() !== \"\") {\n buffer.push(\"\");\n i += 1;\n continue;\n }\n break;\n }\n const bullet = BULLET.exec(line);\n const numbered = ORDERED.exec(line);\n const match = bullet ?? numbered;\n\n if (match && match[1].length <= baseIndent) {\n const sameKind = ordered ? Boolean(numbered) : Boolean(bullet);\n if (!sameKind) break;\n flush();\n buffer.push(match[3]);\n i += 1;\n continue;\n }\n if (match) {\n // Deeper marker: keep the indentation so the recursive call sees a list.\n buffer.push(line.slice(baseIndent));\n i += 1;\n continue;\n }\n if (line.search(/\\S/) > baseIndent) {\n buffer.push(line.trim());\n i += 1;\n continue;\n }\n break;\n }\n flush();\n\n return [{ type: \"list\", ordered, start: startNumber, items }, i];\n}\n\n/** Split a table row on unescaped pipes. */\nfunction splitRow(line: string): string[] {\n const trimmed = line.trim().replace(/^\\|/, \"\").replace(/\\|$/, \"\");\n const cells: string[] = [];\n let current = \"\";\n for (let i = 0; i < trimmed.length; i += 1) {\n if (trimmed[i] === \"\\\\\" && trimmed[i + 1] === \"|\") {\n current += \"|\";\n i += 1;\n continue;\n }\n if (trimmed[i] === \"|\") {\n cells.push(current);\n current = \"\";\n continue;\n }\n current += trimmed[i];\n }\n cells.push(current);\n return cells.map((cell) => cell.trim());\n}\n\n/**\n * Parse a GFM pipe table.\n *\n * @returns The table node and the index of the first line after it.\n */\nfunction parseTable(lines: string[], start: number): [MarkdownBlock, number] {\n const head = splitRow(lines[start]);\n const align: MarkdownAlign[] = splitRow(lines[start + 1]).map((cell) => {\n const left = cell.startsWith(\":\");\n const right = cell.endsWith(\":\");\n if (left && right) return \"center\";\n if (right) return \"right\";\n if (left) return \"left\";\n return null;\n });\n\n const rows: MarkdownInline[][][] = [];\n let i = start + 2;\n while (i < lines.length && lines[i].trim() !== \"\" && lines[i].includes(\"|\")) {\n rows.push(splitRow(lines[i]).map(parseInline));\n i += 1;\n }\n\n return [{ type: \"table\", align, head: head.map(parseInline), rows }, i];\n}\n\n/** Inline delimiters, longest marker first so `**` wins over `*`. */\nconst INLINE_RULES: Array<{\n pattern: RegExp;\n build: (match: RegExpExecArray) => MarkdownInline | null;\n}> = [\n {\n pattern: /^!\\[([^\\]]*)\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+\"[^\"]*\")?\\)/,\n build: (match) => {\n const src = safeImageUrl(match[2]);\n return src ? { type: \"image\", src, alt: match[1] } : { type: \"text\", value: match[1] };\n },\n },\n {\n pattern: /^\\[([^\\]]*)\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+\"[^\"]*\")?\\)/,\n build: (match) => {\n const href = safeLinkUrl(match[2]);\n const children = parseInline(match[1]);\n // A rejected URL keeps the label as plain text: dropping the text too\n // would silently delete words somebody wrote.\n return href ? { type: \"link\", href, children } : { type: \"text\", value: match[1] };\n },\n },\n { pattern: /^`([^`]+)`/, build: (match) => ({ type: \"code\", value: match[1] }) },\n {\n pattern: /^\\*\\*([\\s\\S]+?)\\*\\*/,\n build: (match) => ({ type: \"strong\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^__([\\s\\S]+?)__/,\n build: (match) => ({ type: \"strong\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^~~([\\s\\S]+?)~~/,\n build: (match) => ({ type: \"del\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^\\*([^*\\n]+)\\*/,\n build: (match) => ({ type: \"em\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^_([^_\\n]+)_/,\n build: (match) => ({ type: \"em\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^<((?:https?:\\/\\/|mailto:)[^>\\s]+)>/,\n build: (match) => {\n const href = safeLinkUrl(match[1]);\n return href\n ? { type: \"link\", href, children: [{ type: \"text\", value: match[1] }] }\n : { type: \"text\", value: match[1] };\n },\n },\n { pattern: /^ {2,}\\n/, build: () => ({ type: \"break\" }) },\n { pattern: /^\\\\\\n/, build: () => ({ type: \"break\" }) },\n];\n\n/**\n * Parse inline Markdown.\n *\n * A backslash escapes the next character, so `\\*not italic\\*` renders with the\n * asterisks. Anything that matches no rule is text — an unclosed `**` is two\n * asterisks, not a bold run to the end of the paragraph.\n *\n * @param source - One block's text.\n * @returns Inline nodes.\n */\nexport function parseInline(source: string): MarkdownInline[] {\n const nodes: MarkdownInline[] = [];\n let text = \"\";\n let i = 0;\n\n const flush = (): void => {\n if (text) nodes.push({ type: \"text\", value: text });\n text = \"\";\n };\n\n while (i < source.length) {\n if (source[i] === \"\\\\\" && i + 1 < source.length && source[i + 1] !== \"\\n\") {\n text += source[i + 1];\n i += 2;\n continue;\n }\n\n const rest = source.slice(i);\n let matched = false;\n for (const rule of INLINE_RULES) {\n const match = rule.pattern.exec(rest);\n if (!match) continue;\n const node = rule.build(match);\n if (!node) continue;\n flush();\n nodes.push(node);\n i += match[0].length;\n matched = true;\n break;\n }\n if (matched) continue;\n\n text += source[i];\n i += 1;\n }\n\n flush();\n return nodes;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Menubar.cjs","names":[],"sources":["../../../src/components/Menubar/Menubar.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Menubar.module.css\";\n\nexport type MenubarItem =\n | {\n /** Visible label for the action. */\n label: ReactNode;\n /** Invoked when the action is selected. */\n onSelect?: () => void;\n /** Disable the action. */\n disabled?: boolean;\n /** Optional shortcut hint rendered right-aligned (e.g. `\"⌘S\"`). */\n shortcut?: string;\n }\n | { separator: true };\n\nexport interface MenubarMenu {\n /** Top-level menu label (e.g. `\"File\"`). */\n label: ReactNode;\n /** Items shown in the menu's dropdown. */\n items: MenubarItem[];\n}\n\nexport interface MenubarProps extends HTMLAttributes<HTMLDivElement> {\n /** Top-level menus rendered left-to-right. */\n menus: MenubarMenu[];\n}\n\nfunction isSeparator(item: MenubarItem): item is { separator: true } {\n return \"separator\" in item;\n}\n\n/**\n * Application menubar (File / Edit-style).\n *\n * - `role=\"menubar\"`; each menu is a button that opens a dropdown.\n * - Arrow Left/Right moves between menus (wrapping); opening one closes others.\n * - Closes on outside click, Escape, or selecting an item.\n * - Items may carry a `shortcut` (right-aligned) or be a `{ separator: true }`.\n *\n * @example\n * <Menubar\n * menus={[\n * {\n * label: \"File\",\n * items: [\n * { label: \"New\", shortcut: \"⌘N\", onSelect: () => create() },\n * { separator: true },\n * { label: \"Quit\", onSelect: () => quit() },\n * ],\n * },\n * ]}\n * />\n */\nexport function Menubar({ menus, className, ...props }: MenubarProps) {\n const [openIndex, setOpenIndex] = useState<number>(-1);\n const baseId = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const triggerRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n const close = useCallback((): void => {\n setOpenIndex(-1);\n }, []);\n\n useEffect(() => {\n if (openIndex === -1) return;\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") {\n close();\n triggerRefs.current[openIndex]?.focus();\n return;\n }\n if (event.key === \"ArrowRight\") {\n event.preventDefault();\n const next = (openIndex + 1) % menus.length;\n setOpenIndex(next);\n triggerRefs.current[next]?.focus();\n }\n if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n const prev = (openIndex - 1 + menus.length) % menus.length;\n setOpenIndex(prev);\n triggerRefs.current[prev]?.focus();\n }\n };\n const onDown = (event: MouseEvent): void => {\n if (rootRef.current && !rootRef.current.contains(event.target as Node)) close();\n };\n window.addEventListener(\"keydown\", onKey);\n window.addEventListener(\"mousedown\", onDown);\n return () => {\n window.removeEventListener(\"keydown\", onKey);\n window.removeEventListener(\"mousedown\", onDown);\n };\n }, [openIndex, menus.length, close]);\n\n return (\n <div ref={rootRef} role=\"menubar\" className={cn(styles.root, className)} {...props}>\n {menus.map((menu, index) => {\n const open = openIndex === index;\n const panelId = `${baseId}-panel-${index}`;\n return (\n <div key={index} className={styles.menu}>\n <button\n ref={(el) => {\n triggerRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"menuitem\"\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-controls={open ? panelId : undefined}\n className={cn(styles.trigger, open && styles.triggerOpen)}\n onClick={() => setOpenIndex((prev) => (prev === index ? -1 : index))}\n >\n {menu.label}\n </button>\n {open && (\n <ul id={panelId} role=\"menu\" className={styles.panel}>\n {menu.items.map((item, itemIndex) => {\n if (isSeparator(item)) {\n return (\n <li\n key={itemIndex}\n role=\"separator\"\n className={styles.separator}\n aria-hidden\n />\n );\n }\n return (\n <li key={itemIndex} role=\"none\">\n <button\n type=\"button\"\n role=\"menuitem\"\n className={styles.item}\n disabled={item.disabled}\n onClick={() => {\n item.onSelect?.();\n close();\n }}\n >\n <span className={styles.itemLabel}>\n {item.label}\n </span>\n {item.shortcut && (\n <span className={styles.shortcut}>\n {item.shortcut}\n </span>\n )}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Menubar.cjs","names":[],"sources":["../../../src/components/Menubar/Menubar.tsx"],"sourcesContent":["/**\n * @tempest-limits function-lines — a menubar has two keyboard planes — arrow keys\n * move between menus, arrows inside one move between items — and the body owns both\n * because a key press is routed by which plane is open.\n */\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Menubar.module.css\";\n\nexport type MenubarItem =\n | {\n /** Visible label for the action. */\n label: ReactNode;\n /** Invoked when the action is selected. */\n onSelect?: () => void;\n /** Disable the action. */\n disabled?: boolean;\n /** Optional shortcut hint rendered right-aligned (e.g. `\"⌘S\"`). */\n shortcut?: string;\n }\n | { separator: true };\n\nexport interface MenubarMenu {\n /** Top-level menu label (e.g. `\"File\"`). */\n label: ReactNode;\n /** Items shown in the menu's dropdown. */\n items: MenubarItem[];\n}\n\nexport interface MenubarProps extends HTMLAttributes<HTMLDivElement> {\n /** Top-level menus rendered left-to-right. */\n menus: MenubarMenu[];\n}\n\nfunction isSeparator(item: MenubarItem): item is { separator: true } {\n return \"separator\" in item;\n}\n\n/**\n * Application menubar (File / Edit-style).\n *\n * - `role=\"menubar\"`; each menu is a button that opens a dropdown.\n * - Arrow Left/Right moves between menus (wrapping); opening one closes others.\n * - Closes on outside click, Escape, or selecting an item.\n * - Items may carry a `shortcut` (right-aligned) or be a `{ separator: true }`.\n *\n * @example\n * <Menubar\n * menus={[\n * {\n * label: \"File\",\n * items: [\n * { label: \"New\", shortcut: \"⌘N\", onSelect: () => create() },\n * { separator: true },\n * { label: \"Quit\", onSelect: () => quit() },\n * ],\n * },\n * ]}\n * />\n */\nexport function Menubar({ menus, className, ...props }: MenubarProps) {\n const [openIndex, setOpenIndex] = useState<number>(-1);\n const baseId = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const triggerRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n const close = useCallback((): void => {\n setOpenIndex(-1);\n }, []);\n\n useEffect(() => {\n if (openIndex === -1) return;\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") {\n close();\n triggerRefs.current[openIndex]?.focus();\n return;\n }\n if (event.key === \"ArrowRight\") {\n event.preventDefault();\n const next = (openIndex + 1) % menus.length;\n setOpenIndex(next);\n triggerRefs.current[next]?.focus();\n }\n if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n const prev = (openIndex - 1 + menus.length) % menus.length;\n setOpenIndex(prev);\n triggerRefs.current[prev]?.focus();\n }\n };\n const onDown = (event: MouseEvent): void => {\n if (rootRef.current && !rootRef.current.contains(event.target as Node)) close();\n };\n window.addEventListener(\"keydown\", onKey);\n window.addEventListener(\"mousedown\", onDown);\n return () => {\n window.removeEventListener(\"keydown\", onKey);\n window.removeEventListener(\"mousedown\", onDown);\n };\n }, [openIndex, menus.length, close]);\n\n return (\n <div ref={rootRef} role=\"menubar\" className={cn(styles.root, className)} {...props}>\n {menus.map((menu, index) => {\n const open = openIndex === index;\n const panelId = `${baseId}-panel-${index}`;\n return (\n <div key={index} className={styles.menu}>\n <button\n ref={(el) => {\n triggerRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"menuitem\"\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-controls={open ? panelId : undefined}\n className={cn(styles.trigger, open && styles.triggerOpen)}\n onClick={() => setOpenIndex((prev) => (prev === index ? -1 : index))}\n >\n {menu.label}\n </button>\n {open && (\n <ul id={panelId} role=\"menu\" className={styles.panel}>\n {menu.items.map((item, itemIndex) => {\n if (isSeparator(item)) {\n return (\n <li\n key={itemIndex}\n role=\"separator\"\n className={styles.separator}\n aria-hidden\n />\n );\n }\n return (\n <li key={itemIndex} role=\"none\">\n <button\n type=\"button\"\n role=\"menuitem\"\n className={styles.item}\n disabled={item.disabled}\n onClick={() => {\n item.onSelect?.();\n close();\n }}\n >\n <span className={styles.itemLabel}>\n {item.label}\n </span>\n {item.shortcut && (\n <span className={styles.shortcut}>\n {item.shortcut}\n </span>\n )}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Menubar.js","names":[],"sources":["../../../src/components/Menubar/Menubar.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Menubar.module.css\";\n\nexport type MenubarItem =\n | {\n /** Visible label for the action. */\n label: ReactNode;\n /** Invoked when the action is selected. */\n onSelect?: () => void;\n /** Disable the action. */\n disabled?: boolean;\n /** Optional shortcut hint rendered right-aligned (e.g. `\"⌘S\"`). */\n shortcut?: string;\n }\n | { separator: true };\n\nexport interface MenubarMenu {\n /** Top-level menu label (e.g. `\"File\"`). */\n label: ReactNode;\n /** Items shown in the menu's dropdown. */\n items: MenubarItem[];\n}\n\nexport interface MenubarProps extends HTMLAttributes<HTMLDivElement> {\n /** Top-level menus rendered left-to-right. */\n menus: MenubarMenu[];\n}\n\nfunction isSeparator(item: MenubarItem): item is { separator: true } {\n return \"separator\" in item;\n}\n\n/**\n * Application menubar (File / Edit-style).\n *\n * - `role=\"menubar\"`; each menu is a button that opens a dropdown.\n * - Arrow Left/Right moves between menus (wrapping); opening one closes others.\n * - Closes on outside click, Escape, or selecting an item.\n * - Items may carry a `shortcut` (right-aligned) or be a `{ separator: true }`.\n *\n * @example\n * <Menubar\n * menus={[\n * {\n * label: \"File\",\n * items: [\n * { label: \"New\", shortcut: \"⌘N\", onSelect: () => create() },\n * { separator: true },\n * { label: \"Quit\", onSelect: () => quit() },\n * ],\n * },\n * ]}\n * />\n */\nexport function Menubar({ menus, className, ...props }: MenubarProps) {\n const [openIndex, setOpenIndex] = useState<number>(-1);\n const baseId = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const triggerRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n const close = useCallback((): void => {\n setOpenIndex(-1);\n }, []);\n\n useEffect(() => {\n if (openIndex === -1) return;\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") {\n close();\n triggerRefs.current[openIndex]?.focus();\n return;\n }\n if (event.key === \"ArrowRight\") {\n event.preventDefault();\n const next = (openIndex + 1) % menus.length;\n setOpenIndex(next);\n triggerRefs.current[next]?.focus();\n }\n if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n const prev = (openIndex - 1 + menus.length) % menus.length;\n setOpenIndex(prev);\n triggerRefs.current[prev]?.focus();\n }\n };\n const onDown = (event: MouseEvent): void => {\n if (rootRef.current && !rootRef.current.contains(event.target as Node)) close();\n };\n window.addEventListener(\"keydown\", onKey);\n window.addEventListener(\"mousedown\", onDown);\n return () => {\n window.removeEventListener(\"keydown\", onKey);\n window.removeEventListener(\"mousedown\", onDown);\n };\n }, [openIndex, menus.length, close]);\n\n return (\n <div ref={rootRef} role=\"menubar\" className={cn(styles.root, className)} {...props}>\n {menus.map((menu, index) => {\n const open = openIndex === index;\n const panelId = `${baseId}-panel-${index}`;\n return (\n <div key={index} className={styles.menu}>\n <button\n ref={(el) => {\n triggerRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"menuitem\"\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-controls={open ? panelId : undefined}\n className={cn(styles.trigger, open && styles.triggerOpen)}\n onClick={() => setOpenIndex((prev) => (prev === index ? -1 : index))}\n >\n {menu.label}\n </button>\n {open && (\n <ul id={panelId} role=\"menu\" className={styles.panel}>\n {menu.items.map((item, itemIndex) => {\n if (isSeparator(item)) {\n return (\n <li\n key={itemIndex}\n role=\"separator\"\n className={styles.separator}\n aria-hidden\n />\n );\n }\n return (\n <li key={itemIndex} role=\"none\">\n <button\n type=\"button\"\n role=\"menuitem\"\n className={styles.item}\n disabled={item.disabled}\n onClick={() => {\n item.onSelect?.();\n close();\n }}\n >\n <span className={styles.itemLabel}>\n {item.label}\n </span>\n {item.shortcut && (\n <span className={styles.shortcut}>\n {item.shortcut}\n </span>\n )}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;;;AA8BA,SAAS,EAAY,GAAgD;CACjE,OAAO,eAAe;AAC1B;AAwBA,SAAgB,EAAQ,EAAE,UAAO,cAAW,GAAG,KAAuB;CAClE,IAAM,CAAC,GAAW,KAAgB,EAAiB,EAAE,GAC/C,IAAS,EAAM,GACf,IAAU,EAAuB,IAAI,GACrC,IAAc,EAAwC,CAAC,CAAC,GAExD,IAAQ,QAAwB;EAClC,EAAa,EAAE;CACnB,GAAG,CAAC,CAAC;CAkCL,OAhCA,QAAgB;EACZ,IAAI,MAAc,IAAI;EACtB,IAAM,KAAS,MAA+B;GAC1C,IAAI,EAAM,QAAQ,UAAU;IAExB,AADA,EAAM,GACN,EAAY,QAAQ,EAAU,EAAE,MAAM;IACtC;GACJ;GACA,IAAI,EAAM,QAAQ,cAAc;IAC5B,EAAM,eAAe;IACrB,IAAM,KAAQ,IAAY,KAAK,EAAM;IAErC,AADA,EAAa,CAAI,GACjB,EAAY,QAAQ,EAAK,EAAE,MAAM;GACrC;GACA,IAAI,EAAM,QAAQ,aAAa;IAC3B,EAAM,eAAe;IACrB,IAAM,KAAQ,IAAY,IAAI,EAAM,UAAU,EAAM;IAEpD,AADA,EAAa,CAAI,GACjB,EAAY,QAAQ,EAAK,EAAE,MAAM;GACrC;EACJ,GACM,KAAU,MAA4B;GACxC,AAAI,EAAQ,WAAW,CAAC,EAAQ,QAAQ,SAAS,EAAM,MAAc,KAAG,EAAM;EAClF;EAGA,OAFA,OAAO,iBAAiB,WAAW,CAAK,GACxC,OAAO,iBAAiB,aAAa,CAAM,SAC9B;GAET,AADA,OAAO,oBAAoB,WAAW,CAAK,GAC3C,OAAO,oBAAoB,aAAa,CAAM;EAClD;CACJ,GAAG;EAAC;EAAW,EAAM;EAAQ;CAAK,CAAC,GAG/B,kBAAC,OAAD;EAAK,KAAK;EAAS,MAAK;EAAU,WAAW,EAAG,EAAO,MAAM,CAAS;EAAG,GAAI;YACxE,EAAM,KAAK,GAAM,MAAU;GACxB,IAAM,IAAO,MAAc,GACrB,IAAU,GAAG,EAAO,SAAS;GACnC,OACI,kBAAC,OAAD;IAAiB,WAAW,EAAO;cAAnC,CACI,kBAAC,UAAD;KACI,MAAM,MAAO;MACT,EAAY,QAAQ,KAAS;KACjC;KACA,MAAK;KACL,MAAK;KACL,iBAAc;KACd,iBAAe;KACf,iBAAe,IAAO,IAAU,KAAA;KAChC,WAAW,EAAG,EAAO,SAAS,KAAQ,EAAO,WAAW;KACxD,eAAe,GAAc,MAAU,MAAS,IAAQ,KAAK,CAAM;eAElE,EAAK;IACF,CAAA,GACP,KACG,kBAAC,MAAD;KAAI,IAAI;KAAS,MAAK;KAAO,WAAW,EAAO;eAC1C,EAAK,MAAM,KAAK,GAAM,MACf,EAAY,CAAI,IAEZ,kBAAC,MAAD;MAEI,MAAK;MACL,WAAW,EAAO;MAClB,eAAA;KACH,GAJQ,CAIR,IAIL,kBAAC,MAAD;MAAoB,MAAK;gBACrB,kBAAC,UAAD;OACI,MAAK;OACL,MAAK;OACL,WAAW,EAAO;OAClB,UAAU,EAAK;OACf,eAAe;QAEX,AADA,EAAK,WAAW,GAChB,EAAM;OACV;iBARJ,CAUI,kBAAC,QAAD;QAAM,WAAW,EAAO;kBACnB,EAAK;OACJ,CAAA,GACL,EAAK,YACF,kBAAC,QAAD;QAAM,WAAW,EAAO;kBACnB,EAAK;OACJ,CAAA,CAEN;;KACR,GApBK,CAoBL,CAEX;IACD,CAAA,CAEP;MAtDK,CAsDL;EAEb,CAAC;CACA,CAAA;AAEb"}
1
+ {"version":3,"file":"Menubar.js","names":[],"sources":["../../../src/components/Menubar/Menubar.tsx"],"sourcesContent":["/**\n * @tempest-limits function-lines — a menubar has two keyboard planes — arrow keys\n * move between menus, arrows inside one move between items — and the body owns both\n * because a key press is routed by which plane is open.\n */\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Menubar.module.css\";\n\nexport type MenubarItem =\n | {\n /** Visible label for the action. */\n label: ReactNode;\n /** Invoked when the action is selected. */\n onSelect?: () => void;\n /** Disable the action. */\n disabled?: boolean;\n /** Optional shortcut hint rendered right-aligned (e.g. `\"⌘S\"`). */\n shortcut?: string;\n }\n | { separator: true };\n\nexport interface MenubarMenu {\n /** Top-level menu label (e.g. `\"File\"`). */\n label: ReactNode;\n /** Items shown in the menu's dropdown. */\n items: MenubarItem[];\n}\n\nexport interface MenubarProps extends HTMLAttributes<HTMLDivElement> {\n /** Top-level menus rendered left-to-right. */\n menus: MenubarMenu[];\n}\n\nfunction isSeparator(item: MenubarItem): item is { separator: true } {\n return \"separator\" in item;\n}\n\n/**\n * Application menubar (File / Edit-style).\n *\n * - `role=\"menubar\"`; each menu is a button that opens a dropdown.\n * - Arrow Left/Right moves between menus (wrapping); opening one closes others.\n * - Closes on outside click, Escape, or selecting an item.\n * - Items may carry a `shortcut` (right-aligned) or be a `{ separator: true }`.\n *\n * @example\n * <Menubar\n * menus={[\n * {\n * label: \"File\",\n * items: [\n * { label: \"New\", shortcut: \"⌘N\", onSelect: () => create() },\n * { separator: true },\n * { label: \"Quit\", onSelect: () => quit() },\n * ],\n * },\n * ]}\n * />\n */\nexport function Menubar({ menus, className, ...props }: MenubarProps) {\n const [openIndex, setOpenIndex] = useState<number>(-1);\n const baseId = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const triggerRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n const close = useCallback((): void => {\n setOpenIndex(-1);\n }, []);\n\n useEffect(() => {\n if (openIndex === -1) return;\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") {\n close();\n triggerRefs.current[openIndex]?.focus();\n return;\n }\n if (event.key === \"ArrowRight\") {\n event.preventDefault();\n const next = (openIndex + 1) % menus.length;\n setOpenIndex(next);\n triggerRefs.current[next]?.focus();\n }\n if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n const prev = (openIndex - 1 + menus.length) % menus.length;\n setOpenIndex(prev);\n triggerRefs.current[prev]?.focus();\n }\n };\n const onDown = (event: MouseEvent): void => {\n if (rootRef.current && !rootRef.current.contains(event.target as Node)) close();\n };\n window.addEventListener(\"keydown\", onKey);\n window.addEventListener(\"mousedown\", onDown);\n return () => {\n window.removeEventListener(\"keydown\", onKey);\n window.removeEventListener(\"mousedown\", onDown);\n };\n }, [openIndex, menus.length, close]);\n\n return (\n <div ref={rootRef} role=\"menubar\" className={cn(styles.root, className)} {...props}>\n {menus.map((menu, index) => {\n const open = openIndex === index;\n const panelId = `${baseId}-panel-${index}`;\n return (\n <div key={index} className={styles.menu}>\n <button\n ref={(el) => {\n triggerRefs.current[index] = el;\n }}\n type=\"button\"\n role=\"menuitem\"\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-controls={open ? panelId : undefined}\n className={cn(styles.trigger, open && styles.triggerOpen)}\n onClick={() => setOpenIndex((prev) => (prev === index ? -1 : index))}\n >\n {menu.label}\n </button>\n {open && (\n <ul id={panelId} role=\"menu\" className={styles.panel}>\n {menu.items.map((item, itemIndex) => {\n if (isSeparator(item)) {\n return (\n <li\n key={itemIndex}\n role=\"separator\"\n className={styles.separator}\n aria-hidden\n />\n );\n }\n return (\n <li key={itemIndex} role=\"none\">\n <button\n type=\"button\"\n role=\"menuitem\"\n className={styles.item}\n disabled={item.disabled}\n onClick={() => {\n item.onSelect?.();\n close();\n }}\n >\n <span className={styles.itemLabel}>\n {item.label}\n </span>\n {item.shortcut && (\n <span className={styles.shortcut}>\n {item.shortcut}\n </span>\n )}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;;;AAmCA,SAAS,EAAY,GAAgD;CACjE,OAAO,eAAe;AAC1B;AAwBA,SAAgB,EAAQ,EAAE,UAAO,cAAW,GAAG,KAAuB;CAClE,IAAM,CAAC,GAAW,KAAgB,EAAiB,EAAE,GAC/C,IAAS,EAAM,GACf,IAAU,EAAuB,IAAI,GACrC,IAAc,EAAwC,CAAC,CAAC,GAExD,IAAQ,QAAwB;EAClC,EAAa,EAAE;CACnB,GAAG,CAAC,CAAC;CAkCL,OAhCA,QAAgB;EACZ,IAAI,MAAc,IAAI;EACtB,IAAM,KAAS,MAA+B;GAC1C,IAAI,EAAM,QAAQ,UAAU;IAExB,AADA,EAAM,GACN,EAAY,QAAQ,EAAU,EAAE,MAAM;IACtC;GACJ;GACA,IAAI,EAAM,QAAQ,cAAc;IAC5B,EAAM,eAAe;IACrB,IAAM,KAAQ,IAAY,KAAK,EAAM;IAErC,AADA,EAAa,CAAI,GACjB,EAAY,QAAQ,EAAK,EAAE,MAAM;GACrC;GACA,IAAI,EAAM,QAAQ,aAAa;IAC3B,EAAM,eAAe;IACrB,IAAM,KAAQ,IAAY,IAAI,EAAM,UAAU,EAAM;IAEpD,AADA,EAAa,CAAI,GACjB,EAAY,QAAQ,EAAK,EAAE,MAAM;GACrC;EACJ,GACM,KAAU,MAA4B;GACxC,AAAI,EAAQ,WAAW,CAAC,EAAQ,QAAQ,SAAS,EAAM,MAAc,KAAG,EAAM;EAClF;EAGA,OAFA,OAAO,iBAAiB,WAAW,CAAK,GACxC,OAAO,iBAAiB,aAAa,CAAM,SAC9B;GAET,AADA,OAAO,oBAAoB,WAAW,CAAK,GAC3C,OAAO,oBAAoB,aAAa,CAAM;EAClD;CACJ,GAAG;EAAC;EAAW,EAAM;EAAQ;CAAK,CAAC,GAG/B,kBAAC,OAAD;EAAK,KAAK;EAAS,MAAK;EAAU,WAAW,EAAG,EAAO,MAAM,CAAS;EAAG,GAAI;YACxE,EAAM,KAAK,GAAM,MAAU;GACxB,IAAM,IAAO,MAAc,GACrB,IAAU,GAAG,EAAO,SAAS;GACnC,OACI,kBAAC,OAAD;IAAiB,WAAW,EAAO;cAAnC,CACI,kBAAC,UAAD;KACI,MAAM,MAAO;MACT,EAAY,QAAQ,KAAS;KACjC;KACA,MAAK;KACL,MAAK;KACL,iBAAc;KACd,iBAAe;KACf,iBAAe,IAAO,IAAU,KAAA;KAChC,WAAW,EAAG,EAAO,SAAS,KAAQ,EAAO,WAAW;KACxD,eAAe,GAAc,MAAU,MAAS,IAAQ,KAAK,CAAM;eAElE,EAAK;IACF,CAAA,GACP,KACG,kBAAC,MAAD;KAAI,IAAI;KAAS,MAAK;KAAO,WAAW,EAAO;eAC1C,EAAK,MAAM,KAAK,GAAM,MACf,EAAY,CAAI,IAEZ,kBAAC,MAAD;MAEI,MAAK;MACL,WAAW,EAAO;MAClB,eAAA;KACH,GAJQ,CAIR,IAIL,kBAAC,MAAD;MAAoB,MAAK;gBACrB,kBAAC,UAAD;OACI,MAAK;OACL,MAAK;OACL,WAAW,EAAO;OAClB,UAAU,EAAK;OACf,eAAe;QAEX,AADA,EAAK,WAAW,GAChB,EAAM;OACV;iBARJ,CAUI,kBAAC,QAAD;QAAM,WAAW,EAAO;kBACnB,EAAK;OACJ,CAAA,GACL,EAAK,YACF,kBAAC,QAAD;QAAM,WAAW,EAAO;kBACnB,EAAK;OACJ,CAAA,CAEN;;KACR,GApBK,CAoBL,CAEX;IACD,CAAA,CAEP;MAtDK,CAsDL;EAEb,CAAC;CACA,CAAA;AAEb"}
@@ -1 +1 @@
1
- {"version":3,"file":"Modal.cjs","names":[],"sources":["../../../src/components/Modal/Modal.tsx"],"sourcesContent":["import { useEffect, useId } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Modal.module.css\";\n\nexport type ModalSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\";\n\nexport interface ModalProps {\n open: boolean;\n onClose: () => void;\n title?: ReactNode;\n children?: ReactNode;\n footer?: ReactNode;\n size?: ModalSize;\n closeOnBackdrop?: boolean;\n closeOnEsc?: boolean;\n className?: string;\n hideCloseButton?: boolean;\n /** Force fullscreen at all breakpoints. */\n fullscreen?: boolean;\n /** Auto-fullscreen on mobile viewports (< 640px). Default `false`. */\n fullscreenOnMobile?: boolean;\n /**\n * Accessible name for the dialog. Only needed when there is no `title` —\n * with a `title` the dialog is named by it through `aria-labelledby`.\n */\n \"aria-label\"?: string;\n}\n\n/**\n * Portal-rendered modal dialog with backdrop, Esc handler, and slots for\n * header/body/footer. Locks body scroll while open.\n *\n * The `dialog` role needs an accessible name: a `title` supplies it via\n * `aria-labelledby`, and a titleless dialog should pass `aria-label`. The\n * header heading is only rendered when there is a title, so a close-button-only\n * header does not leave an empty `h3` behind.\n */\nexport function Modal({\n open,\n onClose,\n title,\n children,\n footer,\n size = \"md\",\n closeOnBackdrop = true,\n closeOnEsc = true,\n className,\n hideCloseButton = false,\n fullscreen = false,\n fullscreenOnMobile = false,\n \"aria-label\": ariaLabel,\n}: ModalProps) {\n const titleId = useId();\n useEffect(() => {\n if (!open) return;\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n\n const handleKey = (event: KeyboardEvent): void => {\n if (closeOnEsc && event.key === \"Escape\") onClose();\n };\n window.addEventListener(\"keydown\", handleKey);\n\n return () => {\n document.body.style.overflow = previousOverflow;\n window.removeEventListener(\"keydown\", handleKey);\n };\n }, [open, closeOnEsc, onClose]);\n\n if (!open || typeof document === \"undefined\") return null;\n\n return createPortal(\n <div\n className={styles.overlay}\n role=\"presentation\"\n onClick={() => {\n if (closeOnBackdrop) onClose();\n }}\n >\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={title ? titleId : undefined}\n aria-label={title ? undefined : ariaLabel}\n className={cn(\n styles.dialog,\n sizeClassName(size),\n fullscreen && styles.fullscreen,\n fullscreenOnMobile && styles.fullscreenOnMobile,\n className,\n )}\n onClick={(event) => event.stopPropagation()}\n >\n {(title || !hideCloseButton) && (\n <header className={styles.header}>\n {title && (\n <h3 id={titleId} className={styles.title}>\n {title}\n </h3>\n )}\n {!hideCloseButton && (\n <button\n type=\"button\"\n className={styles.close}\n aria-label=\"Fechar\"\n onClick={onClose}\n >\n <CloseIcon />\n </button>\n )}\n </header>\n )}\n <div className={styles.body}>{children}</div>\n {footer && <footer className={styles.footer}>{footer}</footer>}\n </div>\n </div>,\n document.body,\n );\n}\n\nfunction sizeClassName(size: ModalSize): string | undefined {\n if (size === \"2xl\") return styles.size2xl;\n if (size === \"3xl\") return styles.size3xl;\n return styles[size];\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 6l12 12M6 18L18 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"mJAuCA,SAAgB,EAAM,CAClB,OACA,UACA,QACA,WACA,SACA,OAAO,KACP,kBAAkB,GAClB,aAAa,GACb,YACA,kBAAkB,GAClB,aAAa,GACb,qBAAqB,GACrB,aAAc,GACH,CACX,IAAM,GAAA,EAAA,EAAA,MAAA,CAAgB,EAmBtB,OAlBA,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,EAAM,OACX,IAAM,EAAmB,SAAS,KAAK,MAAM,SAC7C,SAAS,KAAK,MAAM,SAAW,SAE/B,IAAM,EAAa,GAA+B,CAC1C,GAAc,EAAM,MAAQ,UAAU,EAAQ,CACtD,EAGA,OAFA,OAAO,iBAAiB,UAAW,CAAS,MAE/B,CACT,SAAS,KAAK,MAAM,SAAW,EAC/B,OAAO,oBAAoB,UAAW,CAAS,CACnD,CACJ,EAAG,CAAC,EAAM,EAAY,CAAO,CAAC,EAE1B,CAAC,GAAQ,OAAO,SAAa,IAAoB,MAErD,EAAA,EAAA,aAAA,EACI,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,UAAW,EAAA,QAAO,QAClB,KAAK,eACL,YAAe,CACP,GAAiB,EAAQ,CACjC,YAEA,EAAA,EAAA,KAAA,CAAC,MAAD,CACI,KAAK,SACL,aAAW,OACX,kBAAiB,EAAQ,EAAU,IAAA,GACnC,aAAY,EAAQ,IAAA,GAAY,EAChC,UAAW,EAAA,GACP,EAAA,QAAO,OACP,EAAc,CAAI,EAClB,GAAc,EAAA,QAAO,WACrB,GAAsB,EAAA,QAAO,mBAC7B,CACJ,EACA,QAAU,GAAU,EAAM,gBAAgB,WAZ9C,EAcM,GAAS,CAAC,KACR,EAAA,EAAA,KAAA,CAAC,SAAD,CAAQ,UAAW,EAAA,QAAO,gBAA1B,CACK,IACG,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,GAAI,EAAS,UAAW,EAAA,QAAO,eAC9B,CACD,CAAA,EAEP,CAAC,IACE,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,MAClB,aAAW,SACX,QAAS,YAET,EAAA,EAAA,IAAA,CAAC,EAAD,CAAY,CAAA,CACR,CAAA,CAER,KAEZ,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAO,UAAc,CAAA,EAC3C,IAAU,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,UAAW,EAAA,QAAO,gBAAS,CAAe,CAAA,CAC5D,GACJ,CAAA,EACL,SAAS,IACb,CACJ,CAEA,SAAS,EAAc,EAAqC,CAGxD,OAFI,IAAS,MAAc,EAAA,QAAO,QAC9B,IAAS,MAAc,EAAA,QAAO,QAC3B,EAAA,QAAO,EAClB,CAEA,SAAS,GAAY,CACjB,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,MAAM,KAAK,OAAO,KAAK,QAAQ,YAAY,KAAK,iBACjD,EAAA,EAAA,IAAA,CAAC,OAAD,CACI,EAAE,uBACF,OAAO,eACP,YAAY,IACZ,cAAc,OACjB,CAAA,CACA,CAAA,CAEb"}
1
+ {"version":3,"file":"Modal.cjs","names":[],"sources":["../../../src/components/Modal/Modal.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — size, fullscreen and\n * fullscreenOnMobile are three distinct decisions (a fixed width, always full, full\n * only on a phone), and the rest is the dialog contract: open, onClose, title,\n * children, footer, closeOnBackdrop, closeOnEsc, hideCloseButton. The body is the\n * focus trap and the scroll lock, which must be installed and torn down together.\n */\nimport { useEffect, useId } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Modal.module.css\";\n\nexport type ModalSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\";\n\nexport interface ModalProps {\n open: boolean;\n onClose: () => void;\n title?: ReactNode;\n children?: ReactNode;\n footer?: ReactNode;\n size?: ModalSize;\n closeOnBackdrop?: boolean;\n closeOnEsc?: boolean;\n className?: string;\n hideCloseButton?: boolean;\n /** Force fullscreen at all breakpoints. */\n fullscreen?: boolean;\n /** Auto-fullscreen on mobile viewports (< 640px). Default `false`. */\n fullscreenOnMobile?: boolean;\n /**\n * Accessible name for the dialog. Only needed when there is no `title` —\n * with a `title` the dialog is named by it through `aria-labelledby`.\n */\n \"aria-label\"?: string;\n}\n\n/**\n * Portal-rendered modal dialog with backdrop, Esc handler, and slots for\n * header/body/footer. Locks body scroll while open.\n *\n * The `dialog` role needs an accessible name: a `title` supplies it via\n * `aria-labelledby`, and a titleless dialog should pass `aria-label`. The\n * header heading is only rendered when there is a title, so a close-button-only\n * header does not leave an empty `h3` behind.\n */\nexport function Modal({\n open,\n onClose,\n title,\n children,\n footer,\n size = \"md\",\n closeOnBackdrop = true,\n closeOnEsc = true,\n className,\n hideCloseButton = false,\n fullscreen = false,\n fullscreenOnMobile = false,\n \"aria-label\": ariaLabel,\n}: ModalProps) {\n const titleId = useId();\n useEffect(() => {\n if (!open) return;\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n\n const handleKey = (event: KeyboardEvent): void => {\n if (closeOnEsc && event.key === \"Escape\") onClose();\n };\n window.addEventListener(\"keydown\", handleKey);\n\n return () => {\n document.body.style.overflow = previousOverflow;\n window.removeEventListener(\"keydown\", handleKey);\n };\n }, [open, closeOnEsc, onClose]);\n\n if (!open || typeof document === \"undefined\") return null;\n\n return createPortal(\n <div\n className={styles.overlay}\n role=\"presentation\"\n onClick={() => {\n if (closeOnBackdrop) onClose();\n }}\n >\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={title ? titleId : undefined}\n aria-label={title ? undefined : ariaLabel}\n className={cn(\n styles.dialog,\n sizeClassName(size),\n fullscreen && styles.fullscreen,\n fullscreenOnMobile && styles.fullscreenOnMobile,\n className,\n )}\n onClick={(event) => event.stopPropagation()}\n >\n {(title || !hideCloseButton) && (\n <header className={styles.header}>\n {title && (\n <h3 id={titleId} className={styles.title}>\n {title}\n </h3>\n )}\n {!hideCloseButton && (\n <button\n type=\"button\"\n className={styles.close}\n aria-label=\"Fechar\"\n onClick={onClose}\n >\n <CloseIcon />\n </button>\n )}\n </header>\n )}\n <div className={styles.body}>{children}</div>\n {footer && <footer className={styles.footer}>{footer}</footer>}\n </div>\n </div>,\n document.body,\n );\n}\n\nfunction sizeClassName(size: ModalSize): string | undefined {\n if (size === \"2xl\") return styles.size2xl;\n if (size === \"3xl\") return styles.size3xl;\n return styles[size];\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 6l12 12M6 18L18 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"mJA8CA,SAAgB,EAAM,CAClB,OACA,UACA,QACA,WACA,SACA,OAAO,KACP,kBAAkB,GAClB,aAAa,GACb,YACA,kBAAkB,GAClB,aAAa,GACb,qBAAqB,GACrB,aAAc,GACH,CACX,IAAM,GAAA,EAAA,EAAA,MAAA,CAAgB,EAmBtB,OAlBA,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,EAAM,OACX,IAAM,EAAmB,SAAS,KAAK,MAAM,SAC7C,SAAS,KAAK,MAAM,SAAW,SAE/B,IAAM,EAAa,GAA+B,CAC1C,GAAc,EAAM,MAAQ,UAAU,EAAQ,CACtD,EAGA,OAFA,OAAO,iBAAiB,UAAW,CAAS,MAE/B,CACT,SAAS,KAAK,MAAM,SAAW,EAC/B,OAAO,oBAAoB,UAAW,CAAS,CACnD,CACJ,EAAG,CAAC,EAAM,EAAY,CAAO,CAAC,EAE1B,CAAC,GAAQ,OAAO,SAAa,IAAoB,MAErD,EAAA,EAAA,aAAA,EACI,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,UAAW,EAAA,QAAO,QAClB,KAAK,eACL,YAAe,CACP,GAAiB,EAAQ,CACjC,YAEA,EAAA,EAAA,KAAA,CAAC,MAAD,CACI,KAAK,SACL,aAAW,OACX,kBAAiB,EAAQ,EAAU,IAAA,GACnC,aAAY,EAAQ,IAAA,GAAY,EAChC,UAAW,EAAA,GACP,EAAA,QAAO,OACP,EAAc,CAAI,EAClB,GAAc,EAAA,QAAO,WACrB,GAAsB,EAAA,QAAO,mBAC7B,CACJ,EACA,QAAU,GAAU,EAAM,gBAAgB,WAZ9C,EAcM,GAAS,CAAC,KACR,EAAA,EAAA,KAAA,CAAC,SAAD,CAAQ,UAAW,EAAA,QAAO,gBAA1B,CACK,IACG,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,GAAI,EAAS,UAAW,EAAA,QAAO,eAC9B,CACD,CAAA,EAEP,CAAC,IACE,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,MAClB,aAAW,SACX,QAAS,YAET,EAAA,EAAA,IAAA,CAAC,EAAD,CAAY,CAAA,CACR,CAAA,CAER,KAEZ,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAO,UAAc,CAAA,EAC3C,IAAU,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,UAAW,EAAA,QAAO,gBAAS,CAAe,CAAA,CAC5D,GACJ,CAAA,EACL,SAAS,IACb,CACJ,CAEA,SAAS,EAAc,EAAqC,CAGxD,OAFI,IAAS,MAAc,EAAA,QAAO,QAC9B,IAAS,MAAc,EAAA,QAAO,QAC3B,EAAA,QAAO,EAClB,CAEA,SAAS,GAAY,CACjB,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,MAAM,KAAK,OAAO,KAAK,QAAQ,YAAY,KAAK,iBACjD,EAAA,EAAA,IAAA,CAAC,OAAD,CACI,EAAE,uBACF,OAAO,eACP,YAAY,IACZ,cAAc,OACjB,CAAA,CACA,CAAA,CAEb"}