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.
- package/bin/lib/design/collect.mjs +19 -0
- package/bin/lib/design/functions.mjs +5 -0
- package/bin/lib/design/index.mjs +8 -3
- package/bin/lib/design/index.test.mjs +31 -0
- package/bin/lib/design/scan.mjs +34 -2
- package/bin/lib/design/scan.test.mjs +78 -0
- package/bin/lib/doctor/doctor.e2e.test.mjs +85 -0
- package/bin/tempest.mjs +90 -6
- package/dist/audio/level-meter.cjs.map +1 -1
- package/dist/audio/level-meter.js.map +1 -1
- package/dist/audio/use-audio-recorder.cjs.map +1 -1
- package/dist/audio/use-audio-recorder.js.map +1 -1
- package/dist/auth/passkey.cjs.map +1 -1
- package/dist/auth/passkey.js.map +1 -1
- package/dist/auth/use-passkey.cjs.map +1 -1
- package/dist/auth/use-passkey.js.map +1 -1
- package/dist/br/BrazilMap.cjs.map +1 -1
- package/dist/br/BrazilMap.js.map +1 -1
- package/dist/br/BrazilStateCitySelect.cjs.map +1 -1
- package/dist/br/BrazilStateCitySelect.js.map +1 -1
- package/dist/br/BrazilStateMap.cjs.map +1 -1
- package/dist/br/BrazilStateMap.js.map +1 -1
- package/dist/br/PixQRCode.cjs.map +1 -1
- package/dist/br/PixQRCode.js.map +1 -1
- package/dist/br/boleto.cjs.map +1 -1
- package/dist/br/boleto.js.map +1 -1
- package/dist/br/pix.cjs.map +1 -1
- package/dist/br/pix.js.map +1 -1
- package/dist/br.d.ts +8 -0
- package/dist/capture/media-recorder.cjs.map +1 -1
- package/dist/capture/media-recorder.js.map +1 -1
- package/dist/capture/use-barcode-scanner.cjs.map +1 -1
- package/dist/capture/use-barcode-scanner.js.map +1 -1
- package/dist/capture/use-screen-capture.cjs.map +1 -1
- package/dist/capture/use-screen-capture.js.map +1 -1
- package/dist/capture/use-speech-recognition.cjs.map +1 -1
- package/dist/capture/use-speech-recognition.js.map +1 -1
- package/dist/capture/use-video-recorder.cjs.map +1 -1
- package/dist/capture/use-video-recorder.js.map +1 -1
- package/dist/charts/types.cjs.map +1 -1
- package/dist/charts/types.js.map +1 -1
- package/dist/charts.d.ts +8 -0
- package/dist/components/AIChat/AIChat.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.js.map +1 -1
- package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
- package/dist/components/AIChat/AIChatComposer.js.map +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
- package/dist/components/AIChat/AIChatTurn.js.map +1 -1
- package/dist/components/AppBar/AppBar.cjs.map +1 -1
- package/dist/components/AppBar/AppBar.js.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -1
- package/dist/components/Button/Button.cjs.map +1 -1
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Calendar/Calendar.cjs.map +1 -1
- package/dist/components/Calendar/Calendar.js.map +1 -1
- package/dist/components/Carousel/Carousel.cjs.map +1 -1
- package/dist/components/Carousel/Carousel.js.map +1 -1
- package/dist/components/Chat/Chat.cjs.map +1 -1
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/ChipInput/ChipInput.cjs.map +1 -1
- package/dist/components/ChipInput/ChipInput.js.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
- package/dist/components/Combobox/Combobox.cjs.map +1 -1
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/Command/Command.cjs.map +1 -1
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.cjs.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/CopyButton/CopyButton.cjs.map +1 -1
- package/dist/components/CopyButton/CopyButton.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.module.js.map +1 -1
- package/dist/components/DataTable/EditableCell.cjs.map +1 -1
- package/dist/components/DataTable/EditableCell.js.map +1 -1
- package/dist/components/DatePicker/DatePicker.cjs.map +1 -1
- package/dist/components/DatePicker/DatePicker.js.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.cjs.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.js.map +1 -1
- package/dist/components/Drawer/Drawer.cjs.map +1 -1
- package/dist/components/Drawer/Drawer.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/Dropzone/Dropzone.cjs.map +1 -1
- package/dist/components/Dropzone/Dropzone.js.map +1 -1
- package/dist/components/FileUpload/FileUpload.cjs.map +1 -1
- package/dist/components/FileUpload/FileUpload.js.map +1 -1
- package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
- package/dist/components/FilterBar/FilterBar.js.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
- package/dist/components/Kanban/Kanban.cjs.map +1 -1
- package/dist/components/Kanban/Kanban.js.map +1 -1
- package/dist/components/Lightbox/Lightbox.cjs.map +1 -1
- package/dist/components/Lightbox/Lightbox.js.map +1 -1
- package/dist/components/ListTile/ListTile.cjs.map +1 -1
- package/dist/components/ListTile/ListTile.js.map +1 -1
- package/dist/components/Markdown/Markdown.cjs.map +1 -1
- package/dist/components/Markdown/Markdown.js.map +1 -1
- package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
- package/dist/components/Markdown/markdown-parse.js.map +1 -1
- package/dist/components/Menubar/Menubar.cjs.map +1 -1
- package/dist/components/Menubar/Menubar.js.map +1 -1
- package/dist/components/Modal/Modal.cjs.map +1 -1
- package/dist/components/Modal/Modal.js.map +1 -1
- package/dist/components/ModalsManager/ModalsManager.cjs.map +1 -1
- package/dist/components/ModalsManager/ModalsManager.js.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.cjs.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
- package/dist/components/Page/Page.cjs.map +1 -1
- package/dist/components/Page/Page.js.map +1 -1
- package/dist/components/Pagination/Pagination.cjs.map +1 -1
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.cjs.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.js.map +1 -1
- package/dist/components/PinInput/PinInput.cjs.map +1 -1
- package/dist/components/PinInput/PinInput.js.map +1 -1
- package/dist/components/Popover/Popover.cjs.map +1 -1
- package/dist/components/Popover/Popover.js.map +1 -1
- package/dist/components/Progress/Progress.cjs.map +1 -1
- package/dist/components/Progress/Progress.js.map +1 -1
- package/dist/components/QRCode/qr-encode.cjs.map +1 -1
- package/dist/components/QRCode/qr-encode.js.map +1 -1
- package/dist/components/Radio/Radio.cjs.map +1 -1
- package/dist/components/Radio/Radio.js.map +1 -1
- package/dist/components/RangeSlider/RangeSlider.cjs.map +1 -1
- package/dist/components/RangeSlider/RangeSlider.js.map +1 -1
- package/dist/components/RatingStars/RatingStars.cjs.map +1 -1
- package/dist/components/RatingStars/RatingStars.js.map +1 -1
- package/dist/components/RefreshIndicator/RefreshIndicator.cjs.map +1 -1
- package/dist/components/RefreshIndicator/RefreshIndicator.js.map +1 -1
- package/dist/components/Resizable/Resizable.cjs.map +1 -1
- package/dist/components/Resizable/Resizable.js.map +1 -1
- package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
- package/dist/components/Scheduler/Scheduler.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.js.map +1 -1
- package/dist/components/SignaturePad/SignaturePad.cjs.map +1 -1
- package/dist/components/SignaturePad/SignaturePad.js.map +1 -1
- package/dist/components/Slider/Slider.cjs.map +1 -1
- package/dist/components/Slider/Slider.js.map +1 -1
- package/dist/components/Sparkline/Sparkline.cjs.map +1 -1
- package/dist/components/Sparkline/Sparkline.js.map +1 -1
- package/dist/components/StepperInput/StepperInput.cjs.map +1 -1
- package/dist/components/StepperInput/StepperInput.js.map +1 -1
- package/dist/components/Table/Table.cjs.map +1 -1
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/TimePicker/TimePicker.cjs.map +1 -1
- package/dist/components/TimePicker/TimePicker.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/components/Tour/Tour.cjs.map +1 -1
- package/dist/components/Tour/Tour.js.map +1 -1
- package/dist/components/Transfer/Transfer.cjs.map +1 -1
- package/dist/components/Transfer/Transfer.js.map +1 -1
- package/dist/components/TreeView/TreeView.cjs.map +1 -1
- package/dist/components/TreeView/TreeView.js.map +1 -1
- package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
- package/dist/components/VirtualList/VirtualList.js.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
- package/dist/components/Wizard/Wizard.cjs.map +1 -1
- package/dist/components/Wizard/Wizard.js.map +1 -1
- package/dist/editor/RichTextEditor.cjs.map +1 -1
- package/dist/editor/RichTextEditor.js.map +1 -1
- package/dist/forms/FormField.cjs.map +1 -1
- package/dist/forms/FormField.js.map +1 -1
- package/dist/geo/TrajectoryMap.cjs.map +1 -1
- package/dist/geo/TrajectoryMap.js.map +1 -1
- package/dist/geo/estimate.cjs.map +1 -1
- package/dist/geo/estimate.js.map +1 -1
- package/dist/geo/projection.cjs.map +1 -1
- package/dist/geo/projection.js.map +1 -1
- package/dist/hooks/use-event-listener.cjs.map +1 -1
- package/dist/hooks/use-event-listener.js.map +1 -1
- package/dist/hooks/use-local-storage.cjs.map +1 -1
- package/dist/hooks/use-local-storage.js.map +1 -1
- package/dist/hooks/use-sortable.cjs.map +1 -1
- package/dist/hooks/use-sortable.js.map +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js.map +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/upload-with-progress.cjs.map +1 -1
- package/dist/http/upload-with-progress.js.map +1 -1
- package/dist/i18n/I18nProvider.cjs.map +1 -1
- package/dist/i18n/I18nProvider.js.map +1 -1
- package/dist/imaging/canvas.cjs.map +1 -1
- package/dist/imaging/canvas.js.map +1 -1
- package/dist/imaging.d.ts +5 -0
- package/dist/logger/logger.cjs.map +1 -1
- package/dist/logger/logger.js.map +1 -1
- package/dist/oauth/GoogleSignIn.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.js.map +1 -1
- package/dist/offline/create-offline-sync.cjs.map +1 -1
- package/dist/offline/create-offline-sync.js.map +1 -1
- package/dist/perf/cache-size.cjs +1 -1
- package/dist/perf/cache-size.cjs.map +1 -1
- package/dist/perf/cache-size.js +27 -6
- package/dist/perf/cache-size.js.map +1 -1
- package/dist/sse/create-event-stream.cjs.map +1 -1
- package/dist/sse/create-event-stream.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/sw/background-sync.cjs.map +1 -1
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw.d.ts +12 -0
- package/dist/tabular/compact.cjs.map +1 -1
- package/dist/tabular/compact.js.map +1 -1
- package/dist/tabular/predictor.cjs.map +1 -1
- package/dist/tabular/predictor.js.map +1 -1
- package/dist/tabular.d.ts +7 -2
- package/dist/tempest-react-sdk.d.ts +133 -6
- package/dist/theme/ThemeProvider.cjs.map +1 -1
- package/dist/theme/ThemeProvider.js.map +1 -1
- package/dist/theme/color.cjs.map +1 -1
- package/dist/theme/color.js.map +1 -1
- package/dist/theme/create-theme.cjs.map +1 -1
- package/dist/theme/create-theme.js.map +1 -1
- package/dist/theme/data-viz-ramps.cjs +1 -1
- package/dist/theme/data-viz-ramps.cjs.map +1 -1
- package/dist/theme/data-viz-ramps.js +1 -1
- package/dist/theme/data-viz-ramps.js.map +1 -1
- package/dist/utils/storage.cjs.map +1 -1
- package/dist/utils/storage.js.map +1 -1
- package/dist/vision/core/canvas.cjs.map +1 -1
- package/dist/vision/core/canvas.js.map +1 -1
- package/dist/vision/core/exceptions.cjs.map +1 -1
- package/dist/vision/core/exceptions.js.map +1 -1
- package/dist/vision/core/graph.cjs.map +1 -1
- package/dist/vision/core/graph.js.map +1 -1
- package/dist/vision/core/metadata.cjs.map +1 -1
- package/dist/vision/core/metadata.js.map +1 -1
- package/dist/vision/core/providers.cjs.map +1 -1
- package/dist/vision/core/providers.js.map +1 -1
- package/dist/vision/core/session.cjs.map +1 -1
- package/dist/vision/core/session.js.map +1 -1
- package/dist/vision/core/timing.cjs.map +1 -1
- package/dist/vision/core/timing.js.map +1 -1
- package/dist/vision/io/image.cjs.map +1 -1
- package/dist/vision/io/image.js.map +1 -1
- package/dist/vision/labels.cjs.map +1 -1
- package/dist/vision/labels.js.map +1 -1
- package/dist/vision/luminance.cjs.map +1 -1
- package/dist/vision/luminance.js.map +1 -1
- package/dist/vision/postprocess/classification.cjs.map +1 -1
- package/dist/vision/postprocess/classification.js.map +1 -1
- package/dist/vision/postprocess/detection.cjs.map +1 -1
- package/dist/vision/postprocess/detection.js.map +1 -1
- package/dist/vision/postprocess/segmentation.cjs.map +1 -1
- package/dist/vision/postprocess/segmentation.js.map +1 -1
- package/dist/vision/preprocess/image.cjs.map +1 -1
- package/dist/vision/preprocess/image.js.map +1 -1
- package/dist/vision/results.cjs.map +1 -1
- package/dist/vision/results.js.map +1 -1
- package/dist/vision/tasks/base.cjs.map +1 -1
- package/dist/vision/tasks/base.js.map +1 -1
- package/dist/vision/tasks/classifier.cjs.map +1 -1
- package/dist/vision/tasks/classifier.js.map +1 -1
- package/dist/vision/tasks/detector.cjs.map +1 -1
- package/dist/vision/tasks/detector.js.map +1 -1
- package/dist/vision/tasks/segmenter.cjs.map +1 -1
- package/dist/vision/tasks/segmenter.js.map +1 -1
- package/dist/vision/types.cjs.map +1 -1
- package/dist/vision/types.js.map +1 -1
- package/dist/vision.d.ts +6 -0
- package/dist/vite/tempest-icons.cjs.map +1 -1
- package/dist/vite/tempest-icons.js.map +1 -1
- package/dist/vite/tempest-pwa-icons.cjs.map +1 -1
- package/dist/vite/tempest-pwa-icons.js.map +1 -1
- package/dist/ws/create-web-socket.cjs.map +1 -1
- package/dist/ws/create-web-socket.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Modal.js","names":[],"sources":["../../../src/components/Modal/Modal.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Modal.js","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":";;;;;;AA8CA,SAAgB,EAAM,EAClB,SACA,YACA,UACA,aACA,WACA,UAAO,MACP,qBAAkB,IAClB,gBAAa,IACb,cACA,qBAAkB,IAClB,gBAAa,IACb,wBAAqB,IACrB,cAAc,KACH;CACX,IAAM,IAAU,EAAM;CAmBtB,OAlBA,QAAgB;EACZ,IAAI,CAAC,GAAM;EACX,IAAM,IAAmB,SAAS,KAAK,MAAM;EAC7C,SAAS,KAAK,MAAM,WAAW;EAE/B,IAAM,KAAa,MAA+B;GAC9C,AAAI,KAAc,EAAM,QAAQ,YAAU,EAAQ;EACtD;EAGA,OAFA,OAAO,iBAAiB,WAAW,CAAS,SAE/B;GAET,AADA,SAAS,KAAK,MAAM,WAAW,GAC/B,OAAO,oBAAoB,WAAW,CAAS;EACnD;CACJ,GAAG;EAAC;EAAM;EAAY;CAAO,CAAC,GAE1B,CAAC,KAAQ,OAAO,WAAa,MAAoB,OAE9C,EACH,kBAAC,OAAD;EACI,WAAW,EAAO;EAClB,MAAK;EACL,eAAe;GACX,AAAI,KAAiB,EAAQ;EACjC;YAEA,kBAAC,OAAD;GACI,MAAK;GACL,cAAW;GACX,mBAAiB,IAAQ,IAAU,KAAA;GACnC,cAAY,IAAQ,KAAA,IAAY;GAChC,WAAW,EACP,EAAO,QACP,EAAc,CAAI,GAClB,KAAc,EAAO,YACrB,KAAsB,EAAO,oBAC7B,CACJ;GACA,UAAU,MAAU,EAAM,gBAAgB;aAZ9C;KAcM,KAAS,CAAC,MACR,kBAAC,UAAD;KAAQ,WAAW,EAAO;eAA1B,CACK,KACG,kBAAC,MAAD;MAAI,IAAI;MAAS,WAAW,EAAO;gBAC9B;KACD,CAAA,GAEP,CAAC,KACE,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,cAAW;MACX,SAAS;gBAET,kBAAC,GAAD,CAAY,CAAA;KACR,CAAA,CAER;;IAEZ,kBAAC,OAAD;KAAK,WAAW,EAAO;KAAO;IAAc,CAAA;IAC3C,KAAU,kBAAC,UAAD;KAAQ,WAAW,EAAO;eAAS;IAAe,CAAA;GAC5D;;CACJ,CAAA,GACL,SAAS,IACb;AACJ;AAEA,SAAS,EAAc,GAAqC;CAGxD,OAFI,MAAS,QAAc,EAAO,UAC9B,MAAS,QAAc,EAAO,UAC3B,EAAO;AAClB;AAEA,SAAS,IAAY;CACjB,OACI,kBAAC,OAAD;EAAK,OAAM;EAAK,QAAO;EAAK,SAAQ;EAAY,MAAK;YACjD,kBAAC,QAAD;GACI,GAAE;GACF,QAAO;GACP,aAAY;GACZ,eAAc;EACjB,CAAA;CACA,CAAA;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ModalsManager.cjs","names":[],"sources":["../../../src/components/ModalsManager/ModalsManager.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"ModalsManager.cjs","names":[],"sources":["../../../src/components/ModalsManager/ModalsManager.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines — the imperative API (open, close, confirm, prompt) and\n * the stack that renders it live in one file so the returned promise and the mounted\n * element cannot drift apart.\n */\nimport { createContext, useCallback, useContext, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { ConfirmDialog } from \"@/components/ConfirmDialog\";\nimport { Modal } from \"@/components/Modal\";\nimport type { ModalSize } from \"@/components/Modal\";\nimport styles from \"./ModalsManager.module.css\";\n\n/** Options accepted by {@link ModalsApi.open}. */\nexport interface OpenModalOptions {\n /** Header title. */\n title?: ReactNode;\n /** Modal body content. */\n children: ReactNode;\n /** Dialog size. Default `md`. */\n size?: ModalSize;\n /** Allow closing by clicking the backdrop. Default `true`. */\n closeOnBackdrop?: boolean;\n /** Allow closing with the Esc key. Default `true`. */\n closeOnEsc?: boolean;\n /** Hide the header close button. */\n hideCloseButton?: boolean;\n /** Called after the modal is removed from the stack. */\n onClose?: () => void;\n}\n\n/** Options accepted by {@link ModalsApi.confirm}. */\nexport interface ConfirmModalOptions {\n /** Header title. */\n title?: ReactNode;\n /** Prompt message rendered in the dialog body. */\n message: ReactNode;\n /** Confirm button label. Default `Confirmar`. */\n confirmLabel?: string;\n /** Cancel button label. Default `Cancelar`. */\n cancelLabel?: string;\n /** Render the confirm button in the danger variant. */\n danger?: boolean;\n /** Called when the user confirms. */\n onConfirm?: () => void | Promise<void>;\n /** Called when the user cancels or dismisses. */\n onCancel?: () => void;\n}\n\n/** Imperative API returned by {@link useModals}. */\nexport interface ModalsApi {\n /** Push a content modal. Returns its stack id. */\n open: (options: OpenModalOptions) => string;\n /** Push a confirmation dialog. Returns its stack id. */\n confirm: (options: ConfirmModalOptions) => string;\n /** Remove the modal with the given id. */\n close: (id: string) => void;\n /** Remove every modal from the stack. */\n closeAll: () => void;\n}\n\ninterface ContentEntry {\n kind: \"content\";\n id: string;\n options: OpenModalOptions;\n}\n\ninterface ConfirmEntry {\n kind: \"confirm\";\n id: string;\n options: ConfirmModalOptions;\n}\n\ntype StackEntry = ContentEntry | ConfirmEntry;\n\nconst ModalsContext = createContext<ModalsApi | null>(null);\n\n/** Props for {@link ModalsProvider}. */\nexport interface ModalsProviderProps {\n children: ReactNode;\n}\n\n/**\n * Provides imperative modal control via {@link useModals} and renders the open\n * modal stack. Mount once near the app root.\n *\n * @example\n * <ModalsProvider>\n * <App />\n * </ModalsProvider>\n */\nexport function ModalsProvider({ children }: ModalsProviderProps) {\n const [stack, setStack] = useState<StackEntry[]>([]);\n const counter = useRef(0);\n\n const nextId = useCallback((): string => {\n counter.current += 1;\n return `modal-${counter.current}`;\n }, []);\n\n const close = useCallback((id: string): void => {\n setStack((current) => current.filter((entry) => entry.id !== id));\n }, []);\n\n const closeAll = useCallback((): void => {\n setStack([]);\n }, []);\n\n const open = useCallback(\n (options: OpenModalOptions): string => {\n const id = nextId();\n setStack((current) => [...current, { kind: \"content\", id, options }]);\n return id;\n },\n [nextId],\n );\n\n const confirm = useCallback(\n (options: ConfirmModalOptions): string => {\n const id = nextId();\n setStack((current) => [...current, { kind: \"confirm\", id, options }]);\n return id;\n },\n [nextId],\n );\n\n const api = useMemo<ModalsApi>(\n () => ({ open, confirm, close, closeAll }),\n [open, confirm, close, closeAll],\n );\n\n return (\n <ModalsContext.Provider value={api}>\n {children}\n <div className={styles.stack}>\n {stack.map((entry) =>\n entry.kind === \"content\" ? (\n <ContentModal key={entry.id} entry={entry} close={close} />\n ) : (\n <ConfirmModal key={entry.id} entry={entry} close={close} />\n ),\n )}\n </div>\n </ModalsContext.Provider>\n );\n}\n\ninterface ContentModalProps {\n entry: ContentEntry;\n close: (id: string) => void;\n}\n\nfunction ContentModal({ entry, close }: ContentModalProps) {\n const { options, id } = entry;\n const handleClose = (): void => {\n options.onClose?.();\n close(id);\n };\n return (\n <Modal\n open\n onClose={handleClose}\n title={options.title}\n size={options.size}\n closeOnBackdrop={options.closeOnBackdrop}\n closeOnEsc={options.closeOnEsc}\n hideCloseButton={options.hideCloseButton}\n >\n {options.children}\n </Modal>\n );\n}\n\ninterface ConfirmModalProps {\n entry: ConfirmEntry;\n close: (id: string) => void;\n}\n\nfunction ConfirmModal({ entry, close }: ConfirmModalProps) {\n const { options, id } = entry;\n const [loading, setLoading] = useState(false);\n\n const handleConfirm = async (): Promise<void> => {\n try {\n setLoading(true);\n await options.onConfirm?.();\n } finally {\n setLoading(false);\n close(id);\n }\n };\n\n const handleCancel = (): void => {\n options.onCancel?.();\n close(id);\n };\n\n return (\n <ConfirmDialog\n open\n title={options.title ?? \"\"}\n description={options.message}\n confirmLabel={options.confirmLabel}\n cancelLabel={options.cancelLabel}\n variant={options.danger ? \"danger\" : \"primary\"}\n loading={loading}\n onConfirm={handleConfirm}\n onCancel={handleCancel}\n />\n );\n}\n\n/**\n * Access the imperative modals API. Must be used within a {@link ModalsProvider}.\n *\n * @example\n * const modals = useModals();\n * modals.confirm({ message: \"Excluir item?\", danger: true, onConfirm: del });\n *\n * @throws Error when called outside a {@link ModalsProvider}.\n */\nexport function useModals(): ModalsApi {\n const api = useContext(ModalsContext);\n if (api === null) {\n throw new Error(\"useModals must be used within a <ModalsProvider>\");\n }\n return api;\n}\n"],"mappings":"oLA0EA,IAAM,GAAA,EAAA,EAAA,cAAA,CAAgD,IAAI,EAgB1D,SAAgB,EAAe,CAAE,YAAiC,CAC9D,GAAM,CAAC,EAAO,IAAA,EAAA,EAAA,SAAA,CAAmC,CAAC,CAAC,EAC7C,GAAA,EAAA,EAAA,OAAA,CAAiB,CAAC,EAElB,GAAA,EAAA,EAAA,YAAA,MACF,EAAQ,SAAW,EACZ,SAAS,EAAQ,WACzB,CAAC,CAAC,EAEC,GAAA,EAAA,EAAA,YAAA,CAAqB,GAAqB,CAC5C,EAAU,GAAY,EAAQ,OAAQ,GAAU,EAAM,KAAO,CAAE,CAAC,CACpE,EAAG,CAAC,CAAC,EAEC,GAAA,EAAA,EAAA,YAAA,KAAmC,CACrC,EAAS,CAAC,CAAC,CACf,EAAG,CAAC,CAAC,EAEC,GAAA,EAAA,EAAA,YAAA,CACD,GAAsC,CACnC,IAAM,EAAK,EAAO,EAElB,OADA,EAAU,GAAY,CAAC,GAAG,EAAS,CAAE,KAAM,UAAW,KAAI,SAAQ,CAAC,CAAC,EAC7D,CACX,EACA,CAAC,CAAM,CACX,EAEM,GAAA,EAAA,EAAA,YAAA,CACD,GAAyC,CACtC,IAAM,EAAK,EAAO,EAElB,OADA,EAAU,GAAY,CAAC,GAAG,EAAS,CAAE,KAAM,UAAW,KAAI,SAAQ,CAAC,CAAC,EAC7D,CACX,EACA,CAAC,CAAM,CACX,EAEM,GAAA,EAAA,EAAA,QAAA,MACK,CAAE,OAAM,UAAS,QAAO,UAAS,GACxC,CAAC,EAAM,EAAS,EAAO,CAAQ,CACnC,EAEA,OACI,EAAA,EAAA,KAAA,CAAC,EAAc,SAAf,CAAwB,MAAO,WAA/B,CACK,GACD,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,eAClB,EAAM,IAAK,GACR,EAAM,OAAS,WACX,EAAA,EAAA,IAAA,CAAC,EAAD,CAAoC,QAAc,OAAQ,EAAvC,EAAM,EAAiC,GAE1D,EAAA,EAAA,IAAA,CAAC,EAAD,CAAoC,QAAc,OAAQ,EAAvC,EAAM,EAAiC,CAElE,CACC,CAAA,CACe,GAEhC,CAOA,SAAS,EAAa,CAAE,QAAO,SAA4B,CACvD,GAAM,CAAE,UAAS,MAAO,EAKxB,OACI,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CACI,KAAA,GACA,YAPwB,CAC5B,EAAQ,UAAU,EAClB,EAAM,CAAE,CACZ,EAKQ,MAAO,EAAQ,MACf,KAAM,EAAQ,KACd,gBAAiB,EAAQ,gBACzB,WAAY,EAAQ,WACpB,gBAAiB,EAAQ,yBAExB,EAAQ,QACN,CAAA,CAEf,CAOA,SAAS,EAAa,CAAE,QAAO,SAA4B,CACvD,GAAM,CAAE,UAAS,MAAO,EAClB,CAAC,EAAS,IAAA,EAAA,EAAA,SAAA,CAAuB,EAAK,EAiB5C,OACI,EAAA,EAAA,IAAA,CAAC,EAAA,cAAD,CACI,KAAA,GACA,MAAO,EAAQ,OAAS,GACxB,YAAa,EAAQ,QACrB,aAAc,EAAQ,aACtB,YAAa,EAAQ,YACrB,QAAS,EAAQ,OAAS,SAAW,UAC5B,UACT,UAAW,SAxB8B,CAC7C,GAAI,CACA,EAAW,EAAI,EACf,MAAM,EAAQ,YAAY,CAC9B,QAAU,CACN,EAAW,EAAK,EAChB,EAAM,CAAE,CACZ,CACJ,EAiBQ,aAfyB,CAC7B,EAAQ,WAAW,EACnB,EAAM,CAAE,CACZ,CAaK,CAAA,CAET,CAWA,SAAgB,GAAuB,CACnC,IAAM,GAAA,EAAA,EAAA,WAAA,CAAiB,CAAa,EACpC,GAAI,IAAQ,KACR,MAAU,MAAM,kDAAkD,EAEtE,OAAO,CACX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ModalsManager.js","names":[],"sources":["../../../src/components/ModalsManager/ModalsManager.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"ModalsManager.js","names":[],"sources":["../../../src/components/ModalsManager/ModalsManager.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines — the imperative API (open, close, confirm, prompt) and\n * the stack that renders it live in one file so the returned promise and the mounted\n * element cannot drift apart.\n */\nimport { createContext, useCallback, useContext, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { ConfirmDialog } from \"@/components/ConfirmDialog\";\nimport { Modal } from \"@/components/Modal\";\nimport type { ModalSize } from \"@/components/Modal\";\nimport styles from \"./ModalsManager.module.css\";\n\n/** Options accepted by {@link ModalsApi.open}. */\nexport interface OpenModalOptions {\n /** Header title. */\n title?: ReactNode;\n /** Modal body content. */\n children: ReactNode;\n /** Dialog size. Default `md`. */\n size?: ModalSize;\n /** Allow closing by clicking the backdrop. Default `true`. */\n closeOnBackdrop?: boolean;\n /** Allow closing with the Esc key. Default `true`. */\n closeOnEsc?: boolean;\n /** Hide the header close button. */\n hideCloseButton?: boolean;\n /** Called after the modal is removed from the stack. */\n onClose?: () => void;\n}\n\n/** Options accepted by {@link ModalsApi.confirm}. */\nexport interface ConfirmModalOptions {\n /** Header title. */\n title?: ReactNode;\n /** Prompt message rendered in the dialog body. */\n message: ReactNode;\n /** Confirm button label. Default `Confirmar`. */\n confirmLabel?: string;\n /** Cancel button label. Default `Cancelar`. */\n cancelLabel?: string;\n /** Render the confirm button in the danger variant. */\n danger?: boolean;\n /** Called when the user confirms. */\n onConfirm?: () => void | Promise<void>;\n /** Called when the user cancels or dismisses. */\n onCancel?: () => void;\n}\n\n/** Imperative API returned by {@link useModals}. */\nexport interface ModalsApi {\n /** Push a content modal. Returns its stack id. */\n open: (options: OpenModalOptions) => string;\n /** Push a confirmation dialog. Returns its stack id. */\n confirm: (options: ConfirmModalOptions) => string;\n /** Remove the modal with the given id. */\n close: (id: string) => void;\n /** Remove every modal from the stack. */\n closeAll: () => void;\n}\n\ninterface ContentEntry {\n kind: \"content\";\n id: string;\n options: OpenModalOptions;\n}\n\ninterface ConfirmEntry {\n kind: \"confirm\";\n id: string;\n options: ConfirmModalOptions;\n}\n\ntype StackEntry = ContentEntry | ConfirmEntry;\n\nconst ModalsContext = createContext<ModalsApi | null>(null);\n\n/** Props for {@link ModalsProvider}. */\nexport interface ModalsProviderProps {\n children: ReactNode;\n}\n\n/**\n * Provides imperative modal control via {@link useModals} and renders the open\n * modal stack. Mount once near the app root.\n *\n * @example\n * <ModalsProvider>\n * <App />\n * </ModalsProvider>\n */\nexport function ModalsProvider({ children }: ModalsProviderProps) {\n const [stack, setStack] = useState<StackEntry[]>([]);\n const counter = useRef(0);\n\n const nextId = useCallback((): string => {\n counter.current += 1;\n return `modal-${counter.current}`;\n }, []);\n\n const close = useCallback((id: string): void => {\n setStack((current) => current.filter((entry) => entry.id !== id));\n }, []);\n\n const closeAll = useCallback((): void => {\n setStack([]);\n }, []);\n\n const open = useCallback(\n (options: OpenModalOptions): string => {\n const id = nextId();\n setStack((current) => [...current, { kind: \"content\", id, options }]);\n return id;\n },\n [nextId],\n );\n\n const confirm = useCallback(\n (options: ConfirmModalOptions): string => {\n const id = nextId();\n setStack((current) => [...current, { kind: \"confirm\", id, options }]);\n return id;\n },\n [nextId],\n );\n\n const api = useMemo<ModalsApi>(\n () => ({ open, confirm, close, closeAll }),\n [open, confirm, close, closeAll],\n );\n\n return (\n <ModalsContext.Provider value={api}>\n {children}\n <div className={styles.stack}>\n {stack.map((entry) =>\n entry.kind === \"content\" ? (\n <ContentModal key={entry.id} entry={entry} close={close} />\n ) : (\n <ConfirmModal key={entry.id} entry={entry} close={close} />\n ),\n )}\n </div>\n </ModalsContext.Provider>\n );\n}\n\ninterface ContentModalProps {\n entry: ContentEntry;\n close: (id: string) => void;\n}\n\nfunction ContentModal({ entry, close }: ContentModalProps) {\n const { options, id } = entry;\n const handleClose = (): void => {\n options.onClose?.();\n close(id);\n };\n return (\n <Modal\n open\n onClose={handleClose}\n title={options.title}\n size={options.size}\n closeOnBackdrop={options.closeOnBackdrop}\n closeOnEsc={options.closeOnEsc}\n hideCloseButton={options.hideCloseButton}\n >\n {options.children}\n </Modal>\n );\n}\n\ninterface ConfirmModalProps {\n entry: ConfirmEntry;\n close: (id: string) => void;\n}\n\nfunction ConfirmModal({ entry, close }: ConfirmModalProps) {\n const { options, id } = entry;\n const [loading, setLoading] = useState(false);\n\n const handleConfirm = async (): Promise<void> => {\n try {\n setLoading(true);\n await options.onConfirm?.();\n } finally {\n setLoading(false);\n close(id);\n }\n };\n\n const handleCancel = (): void => {\n options.onCancel?.();\n close(id);\n };\n\n return (\n <ConfirmDialog\n open\n title={options.title ?? \"\"}\n description={options.message}\n confirmLabel={options.confirmLabel}\n cancelLabel={options.cancelLabel}\n variant={options.danger ? \"danger\" : \"primary\"}\n loading={loading}\n onConfirm={handleConfirm}\n onCancel={handleCancel}\n />\n );\n}\n\n/**\n * Access the imperative modals API. Must be used within a {@link ModalsProvider}.\n *\n * @example\n * const modals = useModals();\n * modals.confirm({ message: \"Excluir item?\", danger: true, onConfirm: del });\n *\n * @throws Error when called outside a {@link ModalsProvider}.\n */\nexport function useModals(): ModalsApi {\n const api = useContext(ModalsContext);\n if (api === null) {\n throw new Error(\"useModals must be used within a <ModalsProvider>\");\n }\n return api;\n}\n"],"mappings":";;;;;;AA0EA,IAAM,IAAgB,EAAgC,IAAI;AAgB1D,SAAgB,EAAe,EAAE,eAAiC;CAC9D,IAAM,CAAC,GAAO,KAAY,EAAuB,CAAC,CAAC,GAC7C,IAAU,EAAO,CAAC,GAElB,IAAS,SACX,EAAQ,WAAW,GACZ,SAAS,EAAQ,YACzB,CAAC,CAAC,GAEC,IAAQ,GAAa,MAAqB;EAC5C,GAAU,MAAY,EAAQ,QAAQ,MAAU,EAAM,OAAO,CAAE,CAAC;CACpE,GAAG,CAAC,CAAC,GAEC,IAAW,QAAwB;EACrC,EAAS,CAAC,CAAC;CACf,GAAG,CAAC,CAAC,GAEC,IAAO,GACR,MAAsC;EACnC,IAAM,IAAK,EAAO;EAElB,OADA,GAAU,MAAY,CAAC,GAAG,GAAS;GAAE,MAAM;GAAW;GAAI;EAAQ,CAAC,CAAC,GAC7D;CACX,GACA,CAAC,CAAM,CACX,GAEM,IAAU,GACX,MAAyC;EACtC,IAAM,IAAK,EAAO;EAElB,OADA,GAAU,MAAY,CAAC,GAAG,GAAS;GAAE,MAAM;GAAW;GAAI;EAAQ,CAAC,CAAC,GAC7D;CACX,GACA,CAAC,CAAM,CACX,GAEM,IAAM,SACD;EAAE;EAAM;EAAS;EAAO;CAAS,IACxC;EAAC;EAAM;EAAS;EAAO;CAAQ,CACnC;CAEA,OACI,kBAAC,EAAc,UAAf;EAAwB,OAAO;YAA/B,CACK,GACD,kBAAC,OAAD;GAAK,WAAW,EAAO;aAClB,EAAM,KAAK,MACR,EAAM,SAAS,YACX,kBAAC,GAAD;IAAoC;IAAc;GAAQ,GAAvC,EAAM,EAAiC,IAE1D,kBAAC,GAAD;IAAoC;IAAc;GAAQ,GAAvC,EAAM,EAAiC,CAElE;EACC,CAAA,CACe;;AAEhC;AAOA,SAAS,EAAa,EAAE,UAAO,YAA4B;CACvD,IAAM,EAAE,YAAS,UAAO;CAKxB,OACI,kBAAC,GAAD;EACI,MAAA;EACA,eAPwB;GAE5B,AADA,EAAQ,UAAU,GAClB,EAAM,CAAE;EACZ;EAKQ,OAAO,EAAQ;EACf,MAAM,EAAQ;EACd,iBAAiB,EAAQ;EACzB,YAAY,EAAQ;EACpB,iBAAiB,EAAQ;YAExB,EAAQ;CACN,CAAA;AAEf;AAOA,SAAS,EAAa,EAAE,UAAO,YAA4B;CACvD,IAAM,EAAE,YAAS,UAAO,GAClB,CAAC,GAAS,KAAc,EAAS,EAAK;CAiB5C,OACI,kBAAC,GAAD;EACI,MAAA;EACA,OAAO,EAAQ,SAAS;EACxB,aAAa,EAAQ;EACrB,cAAc,EAAQ;EACtB,aAAa,EAAQ;EACrB,SAAS,EAAQ,SAAS,WAAW;EAC5B;EACT,WAAW,YAxB8B;GAC7C,IAAI;IAEA,AADA,EAAW,EAAI,GACf,MAAM,EAAQ,YAAY;GAC9B,UAAU;IAEN,AADA,EAAW,EAAK,GAChB,EAAM,CAAE;GACZ;EACJ;EAiBQ,gBAfyB;GAE7B,AADA,EAAQ,WAAW,GACnB,EAAM,CAAE;EACZ;CAaK,CAAA;AAET;AAWA,SAAgB,IAAuB;CACnC,IAAM,IAAM,EAAW,CAAa;CACpC,IAAI,MAAQ,MACR,MAAU,MAAM,kDAAkD;CAEtE,OAAO;AACX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MultiSelect.cjs","names":[],"sources":["../../../src/components/MultiSelect/MultiSelect.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./MultiSelect.module.css\";\n\nexport interface MultiSelectOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface MultiSelectProps {\n options: MultiSelectOption[];\n /** Currently selected values. */\n value: string[];\n onChange: (value: string[]) => void;\n label?: string;\n placeholder?: string;\n helperText?: string;\n error?: string;\n disabled?: boolean;\n /** Cap the number of selectable items. */\n maxItems?: number;\n /** Custom filter — return true to keep the option. Default: case-insensitive substring on label. */\n filter?: (option: MultiSelectOption, query: string) => boolean;\n /** Message shown when no option matches. */\n emptyMessage?: string;\n className?: string;\n}\n\nfunction defaultFilter(option: MultiSelectOption, query: string): boolean {\n if (!query) return true;\n return option.label.toLowerCase().includes(query.toLowerCase());\n}\n\n/**\n * MultiSelect — a filterable dropdown that selects many options, shown as\n * removable chips inside the field. Selecting toggles a value; Backspace on an\n * empty query removes the last chip.\n *\n * Keyboard: ArrowUp/ArrowDown navigate, Enter toggles the active option, Esc\n * closes, Backspace (empty input) pops the last chip.\n */\nexport function MultiSelect({\n options,\n value,\n onChange,\n label,\n placeholder = \"Selecione\",\n helperText,\n error,\n disabled,\n maxItems,\n filter = defaultFilter,\n emptyMessage = \"Nenhuma opção encontrada\",\n className,\n}: MultiSelectProps) {\n const id = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n\n const selectedSet = useMemo(() => new Set(value), [value]);\n const selectedOptions = useMemo(\n () =>\n value\n .map((v) => options.find((o) => o.value === v))\n .filter((o): o is MultiSelectOption => Boolean(o)),\n [value, options],\n );\n const filtered = useMemo(\n () => options.filter((o) => filter(o, query)),\n [options, filter, query],\n );\n\n const atMax = maxItems !== undefined && value.length >= maxItems;\n\n const toggle = useCallback(\n (option: MultiSelectOption): void => {\n if (option.disabled) return;\n if (selectedSet.has(option.value)) {\n onChange(value.filter((v) => v !== option.value));\n } else {\n if (atMax) return;\n onChange([...value, option.value]);\n }\n setQuery(\"\");\n inputRef.current?.focus();\n },\n [selectedSet, onChange, value, atMax],\n );\n\n const removeAt = useCallback(\n (optionValue: string): void => {\n onChange(value.filter((v) => v !== optionValue));\n },\n [onChange, value],\n );\n\n useEffect(() => {\n if (!open) return;\n const onDown = (event: MouseEvent): void => {\n if (rootRef.current && !rootRef.current.contains(event.target as Node)) {\n setOpen(false);\n setQuery(\"\");\n }\n };\n window.addEventListener(\"mousedown\", onDown);\n return () => window.removeEventListener(\"mousedown\", onDown);\n }, [open]);\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n setOpen(true);\n setActiveIndex((current) => Math.min(filtered.length - 1, current + 1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((current) => Math.max(0, current - 1));\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const option = filtered[activeIndex];\n if (option) toggle(option);\n } else if (event.key === \"Escape\") {\n setOpen(false);\n setQuery(\"\");\n } else if (event.key === \"Backspace\" && query === \"\" && value.length > 0) {\n removeAt(value[value.length - 1]);\n }\n };\n\n return (\n <div ref={rootRef} className={cn(styles.wrapper, error && styles.error, className)}>\n {label && (\n <label htmlFor={id} className={styles.label}>\n {label}\n </label>\n )}\n <div\n className={cn(styles.field, disabled && styles.disabled)}\n onMouseDown={(event) => {\n if (disabled) return;\n // Keep focus on the input when clicking empty field space.\n if (event.target === event.currentTarget) {\n event.preventDefault();\n inputRef.current?.focus();\n setOpen(true);\n }\n }}\n >\n {selectedOptions.map((option) => (\n <span key={option.value} className={styles.chip}>\n {option.label}\n <button\n type=\"button\"\n className={styles.chipRemove}\n aria-label={`Remover ${option.label}`}\n disabled={disabled}\n onMouseDown={(event) => {\n event.preventDefault();\n removeAt(option.value);\n }}\n >\n ×\n </button>\n </span>\n ))}\n <input\n id={id}\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={`${id}-listbox`}\n aria-autocomplete=\"list\"\n className={styles.input}\n placeholder={value.length === 0 ? placeholder : \"\"}\n disabled={disabled}\n value={query}\n onFocus={() => setOpen(true)}\n onChange={(event) => {\n setQuery(event.target.value);\n setOpen(true);\n setActiveIndex(0);\n }}\n onKeyDown={handleKeyDown}\n />\n </div>\n {open && (\n <ul\n id={`${id}-listbox`}\n role=\"listbox\"\n aria-multiselectable\n className={styles.menu}\n >\n {filtered.length === 0 ? (\n <li className={styles.empty}>{emptyMessage}</li>\n ) : (\n filtered.map((option, index) => {\n const isSelected = selectedSet.has(option.value);\n return (\n <li\n key={option.value}\n role=\"option\"\n aria-selected={isSelected}\n aria-disabled={option.disabled || (!isSelected && atMax)}\n className={cn(\n styles.option,\n index === activeIndex && styles.active,\n isSelected && styles.selected,\n )}\n onMouseEnter={() => setActiveIndex(index)}\n onMouseDown={(event) => {\n event.preventDefault();\n toggle(option);\n }}\n >\n <span className={styles.check} aria-hidden>\n {isSelected ? \"✓\" : \"\"}\n </span>\n {option.label}\n </li>\n );\n })\n )}\n </ul>\n )}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MultiSelect.cjs","names":[],"sources":["../../../src/components/MultiSelect/MultiSelect.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — options/value/onChange\n * plus the shared field contract, and the three knobs that make it multi: maxItems,\n * filter, emptyMessage. The body is the listbox with chip removal, type-ahead and\n * the ARIA active-descendant wiring reading one active index.\n */\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./MultiSelect.module.css\";\n\nexport interface MultiSelectOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface MultiSelectProps {\n options: MultiSelectOption[];\n /** Currently selected values. */\n value: string[];\n onChange: (value: string[]) => void;\n label?: string;\n placeholder?: string;\n helperText?: string;\n error?: string;\n disabled?: boolean;\n /** Cap the number of selectable items. */\n maxItems?: number;\n /** Custom filter — return true to keep the option. Default: case-insensitive substring on label. */\n filter?: (option: MultiSelectOption, query: string) => boolean;\n /** Message shown when no option matches. */\n emptyMessage?: string;\n className?: string;\n}\n\nfunction defaultFilter(option: MultiSelectOption, query: string): boolean {\n if (!query) return true;\n return option.label.toLowerCase().includes(query.toLowerCase());\n}\n\n/**\n * MultiSelect — a filterable dropdown that selects many options, shown as\n * removable chips inside the field. Selecting toggles a value; Backspace on an\n * empty query removes the last chip.\n *\n * Keyboard: ArrowUp/ArrowDown navigate, Enter toggles the active option, Esc\n * closes, Backspace (empty input) pops the last chip.\n */\nexport function MultiSelect({\n options,\n value,\n onChange,\n label,\n placeholder = \"Selecione\",\n helperText,\n error,\n disabled,\n maxItems,\n filter = defaultFilter,\n emptyMessage = \"Nenhuma opção encontrada\",\n className,\n}: MultiSelectProps) {\n const id = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n\n const selectedSet = useMemo(() => new Set(value), [value]);\n const selectedOptions = useMemo(\n () =>\n value\n .map((v) => options.find((o) => o.value === v))\n .filter((o): o is MultiSelectOption => Boolean(o)),\n [value, options],\n );\n const filtered = useMemo(\n () => options.filter((o) => filter(o, query)),\n [options, filter, query],\n );\n\n const atMax = maxItems !== undefined && value.length >= maxItems;\n\n const toggle = useCallback(\n (option: MultiSelectOption): void => {\n if (option.disabled) return;\n if (selectedSet.has(option.value)) {\n onChange(value.filter((v) => v !== option.value));\n } else {\n if (atMax) return;\n onChange([...value, option.value]);\n }\n setQuery(\"\");\n inputRef.current?.focus();\n },\n [selectedSet, onChange, value, atMax],\n );\n\n const removeAt = useCallback(\n (optionValue: string): void => {\n onChange(value.filter((v) => v !== optionValue));\n },\n [onChange, value],\n );\n\n useEffect(() => {\n if (!open) return;\n const onDown = (event: MouseEvent): void => {\n if (rootRef.current && !rootRef.current.contains(event.target as Node)) {\n setOpen(false);\n setQuery(\"\");\n }\n };\n window.addEventListener(\"mousedown\", onDown);\n return () => window.removeEventListener(\"mousedown\", onDown);\n }, [open]);\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n setOpen(true);\n setActiveIndex((current) => Math.min(filtered.length - 1, current + 1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((current) => Math.max(0, current - 1));\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const option = filtered[activeIndex];\n if (option) toggle(option);\n } else if (event.key === \"Escape\") {\n setOpen(false);\n setQuery(\"\");\n } else if (event.key === \"Backspace\" && query === \"\" && value.length > 0) {\n removeAt(value[value.length - 1]);\n }\n };\n\n return (\n <div ref={rootRef} className={cn(styles.wrapper, error && styles.error, className)}>\n {label && (\n <label htmlFor={id} className={styles.label}>\n {label}\n </label>\n )}\n <div\n className={cn(styles.field, disabled && styles.disabled)}\n onMouseDown={(event) => {\n if (disabled) return;\n // Keep focus on the input when clicking empty field space.\n if (event.target === event.currentTarget) {\n event.preventDefault();\n inputRef.current?.focus();\n setOpen(true);\n }\n }}\n >\n {selectedOptions.map((option) => (\n <span key={option.value} className={styles.chip}>\n {option.label}\n <button\n type=\"button\"\n className={styles.chipRemove}\n aria-label={`Remover ${option.label}`}\n disabled={disabled}\n onMouseDown={(event) => {\n event.preventDefault();\n removeAt(option.value);\n }}\n >\n ×\n </button>\n </span>\n ))}\n <input\n id={id}\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={`${id}-listbox`}\n aria-autocomplete=\"list\"\n className={styles.input}\n placeholder={value.length === 0 ? placeholder : \"\"}\n disabled={disabled}\n value={query}\n onFocus={() => setOpen(true)}\n onChange={(event) => {\n setQuery(event.target.value);\n setOpen(true);\n setActiveIndex(0);\n }}\n onKeyDown={handleKeyDown}\n />\n </div>\n {open && (\n <ul\n id={`${id}-listbox`}\n role=\"listbox\"\n aria-multiselectable\n className={styles.menu}\n >\n {filtered.length === 0 ? (\n <li className={styles.empty}>{emptyMessage}</li>\n ) : (\n filtered.map((option, index) => {\n const isSelected = selectedSet.has(option.value);\n return (\n <li\n key={option.value}\n role=\"option\"\n aria-selected={isSelected}\n aria-disabled={option.disabled || (!isSelected && atMax)}\n className={cn(\n styles.option,\n index === activeIndex && styles.active,\n isSelected && styles.selected,\n )}\n onMouseEnter={() => setActiveIndex(index)}\n onMouseDown={(event) => {\n event.preventDefault();\n toggle(option);\n }}\n >\n <span className={styles.check} aria-hidden>\n {isSelected ? \"✓\" : \"\"}\n </span>\n {option.label}\n </li>\n );\n })\n )}\n </ul>\n )}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MultiSelect.js","names":[],"sources":["../../../src/components/MultiSelect/MultiSelect.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./MultiSelect.module.css\";\n\nexport interface MultiSelectOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface MultiSelectProps {\n options: MultiSelectOption[];\n /** Currently selected values. */\n value: string[];\n onChange: (value: string[]) => void;\n label?: string;\n placeholder?: string;\n helperText?: string;\n error?: string;\n disabled?: boolean;\n /** Cap the number of selectable items. */\n maxItems?: number;\n /** Custom filter — return true to keep the option. Default: case-insensitive substring on label. */\n filter?: (option: MultiSelectOption, query: string) => boolean;\n /** Message shown when no option matches. */\n emptyMessage?: string;\n className?: string;\n}\n\nfunction defaultFilter(option: MultiSelectOption, query: string): boolean {\n if (!query) return true;\n return option.label.toLowerCase().includes(query.toLowerCase());\n}\n\n/**\n * MultiSelect — a filterable dropdown that selects many options, shown as\n * removable chips inside the field. Selecting toggles a value; Backspace on an\n * empty query removes the last chip.\n *\n * Keyboard: ArrowUp/ArrowDown navigate, Enter toggles the active option, Esc\n * closes, Backspace (empty input) pops the last chip.\n */\nexport function MultiSelect({\n options,\n value,\n onChange,\n label,\n placeholder = \"Selecione\",\n helperText,\n error,\n disabled,\n maxItems,\n filter = defaultFilter,\n emptyMessage = \"Nenhuma opção encontrada\",\n className,\n}: MultiSelectProps) {\n const id = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n\n const selectedSet = useMemo(() => new Set(value), [value]);\n const selectedOptions = useMemo(\n () =>\n value\n .map((v) => options.find((o) => o.value === v))\n .filter((o): o is MultiSelectOption => Boolean(o)),\n [value, options],\n );\n const filtered = useMemo(\n () => options.filter((o) => filter(o, query)),\n [options, filter, query],\n );\n\n const atMax = maxItems !== undefined && value.length >= maxItems;\n\n const toggle = useCallback(\n (option: MultiSelectOption): void => {\n if (option.disabled) return;\n if (selectedSet.has(option.value)) {\n onChange(value.filter((v) => v !== option.value));\n } else {\n if (atMax) return;\n onChange([...value, option.value]);\n }\n setQuery(\"\");\n inputRef.current?.focus();\n },\n [selectedSet, onChange, value, atMax],\n );\n\n const removeAt = useCallback(\n (optionValue: string): void => {\n onChange(value.filter((v) => v !== optionValue));\n },\n [onChange, value],\n );\n\n useEffect(() => {\n if (!open) return;\n const onDown = (event: MouseEvent): void => {\n if (rootRef.current && !rootRef.current.contains(event.target as Node)) {\n setOpen(false);\n setQuery(\"\");\n }\n };\n window.addEventListener(\"mousedown\", onDown);\n return () => window.removeEventListener(\"mousedown\", onDown);\n }, [open]);\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n setOpen(true);\n setActiveIndex((current) => Math.min(filtered.length - 1, current + 1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((current) => Math.max(0, current - 1));\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const option = filtered[activeIndex];\n if (option) toggle(option);\n } else if (event.key === \"Escape\") {\n setOpen(false);\n setQuery(\"\");\n } else if (event.key === \"Backspace\" && query === \"\" && value.length > 0) {\n removeAt(value[value.length - 1]);\n }\n };\n\n return (\n <div ref={rootRef} className={cn(styles.wrapper, error && styles.error, className)}>\n {label && (\n <label htmlFor={id} className={styles.label}>\n {label}\n </label>\n )}\n <div\n className={cn(styles.field, disabled && styles.disabled)}\n onMouseDown={(event) => {\n if (disabled) return;\n // Keep focus on the input when clicking empty field space.\n if (event.target === event.currentTarget) {\n event.preventDefault();\n inputRef.current?.focus();\n setOpen(true);\n }\n }}\n >\n {selectedOptions.map((option) => (\n <span key={option.value} className={styles.chip}>\n {option.label}\n <button\n type=\"button\"\n className={styles.chipRemove}\n aria-label={`Remover ${option.label}`}\n disabled={disabled}\n onMouseDown={(event) => {\n event.preventDefault();\n removeAt(option.value);\n }}\n >\n ×\n </button>\n </span>\n ))}\n <input\n id={id}\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={`${id}-listbox`}\n aria-autocomplete=\"list\"\n className={styles.input}\n placeholder={value.length === 0 ? placeholder : \"\"}\n disabled={disabled}\n value={query}\n onFocus={() => setOpen(true)}\n onChange={(event) => {\n setQuery(event.target.value);\n setOpen(true);\n setActiveIndex(0);\n }}\n onKeyDown={handleKeyDown}\n />\n </div>\n {open && (\n <ul\n id={`${id}-listbox`}\n role=\"listbox\"\n aria-multiselectable\n className={styles.menu}\n >\n {filtered.length === 0 ? (\n <li className={styles.empty}>{emptyMessage}</li>\n ) : (\n filtered.map((option, index) => {\n const isSelected = selectedSet.has(option.value);\n return (\n <li\n key={option.value}\n role=\"option\"\n aria-selected={isSelected}\n aria-disabled={option.disabled || (!isSelected && atMax)}\n className={cn(\n styles.option,\n index === activeIndex && styles.active,\n isSelected && styles.selected,\n )}\n onMouseEnter={() => setActiveIndex(index)}\n onMouseDown={(event) => {\n event.preventDefault();\n toggle(option);\n }}\n >\n <span className={styles.check} aria-hidden>\n {isSelected ? \"✓\" : \"\"}\n </span>\n {option.label}\n </li>\n );\n })\n )}\n </ul>\n )}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;AA6BA,SAAS,EAAc,GAA2B,GAAwB;CAEtE,OADA,CAAK,KACE,EAAO,MAAM,YAAY,CAAC,CAAC,SAAS,EAAM,YAAY,CAAC;AAClE;AAUA,SAAgB,EAAY,EACxB,YACA,UACA,aACA,UACA,iBAAc,aACd,eACA,UACA,aACA,aACA,YAAS,GACT,kBAAe,4BACf,gBACiB;CACjB,IAAM,IAAK,EAAM,GACX,IAAU,EAAuB,IAAI,GACrC,IAAW,EAAyB,IAAI,GACxC,CAAC,GAAM,KAAW,EAAS,EAAK,GAChC,CAAC,GAAO,KAAY,EAAS,EAAE,GAC/B,CAAC,GAAa,KAAkB,EAAS,CAAC,GAE1C,IAAc,QAAc,IAAI,IAAI,CAAK,GAAG,CAAC,CAAK,CAAC,GACnD,IAAkB,QAEhB,EACK,KAAK,MAAM,EAAQ,MAAM,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,CAC9C,QAAQ,MAA8B,EAAQ,CAAE,GACzD,CAAC,GAAO,CAAO,CACnB,GACM,IAAW,QACP,EAAQ,QAAQ,MAAM,EAAO,GAAG,CAAK,CAAC,GAC5C;EAAC;EAAS;EAAQ;CAAK,CAC3B,GAEM,IAAQ,MAAa,KAAA,KAAa,EAAM,UAAU,GAElD,IAAS,GACV,MAAoC;EAC7B,OAAO,UACX;OAAI,EAAY,IAAI,EAAO,KAAK,GAC5B,EAAS,EAAM,QAAQ,MAAM,MAAM,EAAO,KAAK,CAAC;QAC7C;IACH,IAAI,GAAO;IACX,EAAS,CAAC,GAAG,GAAO,EAAO,KAAK,CAAC;GACrC;GAEA,AADA,EAAS,EAAE,GACX,EAAS,SAAS,MAAM;EAFxB;CAGJ,GACA;EAAC;EAAa;EAAU;EAAO;CAAK,CACxC,GAEM,IAAW,GACZ,MAA8B;EAC3B,EAAS,EAAM,QAAQ,MAAM,MAAM,CAAW,CAAC;CACnD,GACA,CAAC,GAAU,CAAK,CACpB;CAkCA,OAhCA,QAAgB;EACZ,IAAI,CAAC,GAAM;EACX,IAAM,KAAU,MAA4B;GACxC,AAAI,EAAQ,WAAW,CAAC,EAAQ,QAAQ,SAAS,EAAM,MAAc,MACjE,EAAQ,EAAK,GACb,EAAS,EAAE;EAEnB;EAEA,OADA,OAAO,iBAAiB,aAAa,CAAM,SAC9B,OAAO,oBAAoB,aAAa,CAAM;CAC/D,GAAG,CAAC,CAAI,CAAC,GAuBL,kBAAC,OAAD;EAAK,KAAK;EAAS,WAAW,EAAG,EAAO,SAAS,KAAS,EAAO,OAAO,CAAS;YAAjF;GACK,KACG,kBAAC,SAAD;IAAO,SAAS;IAAI,WAAW,EAAO;cACjC;GACE,CAAA;GAEX,kBAAC,OAAD;IACI,WAAW,EAAG,EAAO,OAAO,KAAY,EAAO,QAAQ;IACvD,cAAc,MAAU;KAChB,KAEA,EAAM,WAAW,EAAM,kBACvB,EAAM,eAAe,GACrB,EAAS,SAAS,MAAM,GACxB,EAAQ,EAAI;IAEpB;cAVJ,CAYK,EAAgB,KAAK,MAClB,kBAAC,QAAD;KAAyB,WAAW,EAAO;eAA3C,CACK,EAAO,OACR,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,cAAY,WAAW,EAAO;MACpB;MACV,cAAc,MAAU;OAEpB,AADA,EAAM,eAAe,GACrB,EAAS,EAAO,KAAK;MACzB;gBACH;KAEO,CAAA,CACN;OAdK,EAAO,KAcZ,CACT,GACD,kBAAC,SAAD;KACQ;KACJ,KAAK;KACL,MAAK;KACL,MAAK;KACL,iBAAe;KACf,iBAAe,GAAG,EAAG;KACrB,qBAAkB;KAClB,WAAW,EAAO;KAClB,aAAa,EAAM,WAAW,IAAI,IAAc;KACtC;KACV,OAAO;KACP,eAAe,EAAQ,EAAI;KAC3B,WAAW,MAAU;MAGjB,AAFA,EAAS,EAAM,OAAO,KAAK,GAC3B,EAAQ,EAAI,GACZ,EAAe,CAAC;KACpB;KACA,YA1EO,MAAuD;MAC1E,IAAI,EAAM,QAAQ,aAGd,AAFA,EAAM,eAAe,GACrB,EAAQ,EAAI,GACZ,GAAgB,MAAY,KAAK,IAAI,EAAS,SAAS,GAAG,IAAU,CAAC,CAAC;WACnE,IAAI,EAAM,QAAQ,WAErB,AADA,EAAM,eAAe,GACrB,GAAgB,MAAY,KAAK,IAAI,GAAG,IAAU,CAAC,CAAC;WACjD,IAAI,EAAM,QAAQ,SAAS;OAC9B,EAAM,eAAe;OACrB,IAAM,IAAS,EAAS;OACxB,AAAI,KAAQ,EAAO,CAAM;MAC7B,OAAO,AAAI,EAAM,QAAQ,YACrB,EAAQ,EAAK,GACb,EAAS,EAAE,KACJ,EAAM,QAAQ,eAAe,MAAU,MAAM,EAAM,SAAS,KACnE,EAAS,EAAM,EAAM,SAAS,EAAE;KAExC;IAyDa,CAAA,CACA;;GACJ,KACG,kBAAC,MAAD;IACI,IAAI,GAAG,EAAG;IACV,MAAK;IACL,wBAAA;IACA,WAAW,EAAO;cAEjB,EAAS,WAAW,IACjB,kBAAC,MAAD;KAAI,WAAW,EAAO;eAAQ;IAAiB,CAAA,IAE/C,EAAS,KAAK,GAAQ,MAAU;KAC5B,IAAM,IAAa,EAAY,IAAI,EAAO,KAAK;KAC/C,OACI,kBAAC,MAAD;MAEI,MAAK;MACL,iBAAe;MACf,iBAAe,EAAO,YAAa,CAAC,KAAc;MAClD,WAAW,EACP,EAAO,QACP,MAAU,KAAe,EAAO,QAChC,KAAc,EAAO,QACzB;MACA,oBAAoB,EAAe,CAAK;MACxC,cAAc,MAAU;OAEpB,AADA,EAAM,eAAe,GACrB,EAAO,CAAM;MACjB;gBAdJ,CAgBI,kBAAC,QAAD;OAAM,WAAW,EAAO;OAAO,eAAA;iBAC1B,IAAa,MAAM;MAClB,CAAA,GACL,EAAO,KACR;QAnBK,EAAO,KAmBZ;IAEZ,CAAC;GAEL,CAAA;GAEP,IACG,kBAAC,QAAD;IAAM,WAAW,EAAO;cAAY;GAAY,CAAA,IAChD,IACA,kBAAC,QAAD;IAAM,WAAW,EAAO;cAAS;GAAiB,CAAA,IAClD;EACH;;AAEb"}
|
|
1
|
+
{"version":3,"file":"MultiSelect.js","names":[],"sources":["../../../src/components/MultiSelect/MultiSelect.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — options/value/onChange\n * plus the shared field contract, and the three knobs that make it multi: maxItems,\n * filter, emptyMessage. The body is the listbox with chip removal, type-ahead and\n * the ARIA active-descendant wiring reading one active index.\n */\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./MultiSelect.module.css\";\n\nexport interface MultiSelectOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface MultiSelectProps {\n options: MultiSelectOption[];\n /** Currently selected values. */\n value: string[];\n onChange: (value: string[]) => void;\n label?: string;\n placeholder?: string;\n helperText?: string;\n error?: string;\n disabled?: boolean;\n /** Cap the number of selectable items. */\n maxItems?: number;\n /** Custom filter — return true to keep the option. Default: case-insensitive substring on label. */\n filter?: (option: MultiSelectOption, query: string) => boolean;\n /** Message shown when no option matches. */\n emptyMessage?: string;\n className?: string;\n}\n\nfunction defaultFilter(option: MultiSelectOption, query: string): boolean {\n if (!query) return true;\n return option.label.toLowerCase().includes(query.toLowerCase());\n}\n\n/**\n * MultiSelect — a filterable dropdown that selects many options, shown as\n * removable chips inside the field. Selecting toggles a value; Backspace on an\n * empty query removes the last chip.\n *\n * Keyboard: ArrowUp/ArrowDown navigate, Enter toggles the active option, Esc\n * closes, Backspace (empty input) pops the last chip.\n */\nexport function MultiSelect({\n options,\n value,\n onChange,\n label,\n placeholder = \"Selecione\",\n helperText,\n error,\n disabled,\n maxItems,\n filter = defaultFilter,\n emptyMessage = \"Nenhuma opção encontrada\",\n className,\n}: MultiSelectProps) {\n const id = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n\n const selectedSet = useMemo(() => new Set(value), [value]);\n const selectedOptions = useMemo(\n () =>\n value\n .map((v) => options.find((o) => o.value === v))\n .filter((o): o is MultiSelectOption => Boolean(o)),\n [value, options],\n );\n const filtered = useMemo(\n () => options.filter((o) => filter(o, query)),\n [options, filter, query],\n );\n\n const atMax = maxItems !== undefined && value.length >= maxItems;\n\n const toggle = useCallback(\n (option: MultiSelectOption): void => {\n if (option.disabled) return;\n if (selectedSet.has(option.value)) {\n onChange(value.filter((v) => v !== option.value));\n } else {\n if (atMax) return;\n onChange([...value, option.value]);\n }\n setQuery(\"\");\n inputRef.current?.focus();\n },\n [selectedSet, onChange, value, atMax],\n );\n\n const removeAt = useCallback(\n (optionValue: string): void => {\n onChange(value.filter((v) => v !== optionValue));\n },\n [onChange, value],\n );\n\n useEffect(() => {\n if (!open) return;\n const onDown = (event: MouseEvent): void => {\n if (rootRef.current && !rootRef.current.contains(event.target as Node)) {\n setOpen(false);\n setQuery(\"\");\n }\n };\n window.addEventListener(\"mousedown\", onDown);\n return () => window.removeEventListener(\"mousedown\", onDown);\n }, [open]);\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n setOpen(true);\n setActiveIndex((current) => Math.min(filtered.length - 1, current + 1));\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n setActiveIndex((current) => Math.max(0, current - 1));\n } else if (event.key === \"Enter\") {\n event.preventDefault();\n const option = filtered[activeIndex];\n if (option) toggle(option);\n } else if (event.key === \"Escape\") {\n setOpen(false);\n setQuery(\"\");\n } else if (event.key === \"Backspace\" && query === \"\" && value.length > 0) {\n removeAt(value[value.length - 1]);\n }\n };\n\n return (\n <div ref={rootRef} className={cn(styles.wrapper, error && styles.error, className)}>\n {label && (\n <label htmlFor={id} className={styles.label}>\n {label}\n </label>\n )}\n <div\n className={cn(styles.field, disabled && styles.disabled)}\n onMouseDown={(event) => {\n if (disabled) return;\n // Keep focus on the input when clicking empty field space.\n if (event.target === event.currentTarget) {\n event.preventDefault();\n inputRef.current?.focus();\n setOpen(true);\n }\n }}\n >\n {selectedOptions.map((option) => (\n <span key={option.value} className={styles.chip}>\n {option.label}\n <button\n type=\"button\"\n className={styles.chipRemove}\n aria-label={`Remover ${option.label}`}\n disabled={disabled}\n onMouseDown={(event) => {\n event.preventDefault();\n removeAt(option.value);\n }}\n >\n ×\n </button>\n </span>\n ))}\n <input\n id={id}\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={`${id}-listbox`}\n aria-autocomplete=\"list\"\n className={styles.input}\n placeholder={value.length === 0 ? placeholder : \"\"}\n disabled={disabled}\n value={query}\n onFocus={() => setOpen(true)}\n onChange={(event) => {\n setQuery(event.target.value);\n setOpen(true);\n setActiveIndex(0);\n }}\n onKeyDown={handleKeyDown}\n />\n </div>\n {open && (\n <ul\n id={`${id}-listbox`}\n role=\"listbox\"\n aria-multiselectable\n className={styles.menu}\n >\n {filtered.length === 0 ? (\n <li className={styles.empty}>{emptyMessage}</li>\n ) : (\n filtered.map((option, index) => {\n const isSelected = selectedSet.has(option.value);\n return (\n <li\n key={option.value}\n role=\"option\"\n aria-selected={isSelected}\n aria-disabled={option.disabled || (!isSelected && atMax)}\n className={cn(\n styles.option,\n index === activeIndex && styles.active,\n isSelected && styles.selected,\n )}\n onMouseEnter={() => setActiveIndex(index)}\n onMouseDown={(event) => {\n event.preventDefault();\n toggle(option);\n }}\n >\n <span className={styles.check} aria-hidden>\n {isSelected ? \"✓\" : \"\"}\n </span>\n {option.label}\n </li>\n );\n })\n )}\n </ul>\n )}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavigationMenu.cjs","names":[],"sources":["../../../src/components/NavigationMenu/NavigationMenu.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"NavigationMenu.cjs","names":[],"sources":["../../../src/components/NavigationMenu/NavigationMenu.tsx"],"sourcesContent":["/**\n * @tempest-limits function-lines — the body resolves the active item from the\n * current path, owns the open submenu and the hover-intent delay, all of which\n * decide the same highlighted row.\n */\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./NavigationMenu.module.css\";\n\nexport interface NavigationMenuItem {\n /** Visible label for the entry. */\n label: ReactNode;\n /** When set, the entry renders as an anchor. */\n href?: string;\n /** Invoked when the entry is activated (click / Enter). */\n onSelect?: () => void;\n /** Nested entries — presence turns the item into a submenu trigger. */\n children?: NavigationMenuItem[];\n}\n\nexport interface NavigationMenuProps extends HTMLAttributes<HTMLElement> {\n /** Top-level navigation entries. */\n items: NavigationMenuItem[];\n}\n\n/**\n * Horizontal navigation menu with hover/click/focus dropdown submenus.\n *\n * - Top-level items render in a `<nav><ul>`.\n * - Items with `children` open a submenu panel (`role=\"menu\"`) on hover, focus,\n * or click. Only one panel is open at a time.\n * - Closes on outside click, Escape, or selecting a leaf entry.\n *\n * @example\n * <NavigationMenu\n * items={[\n * { label: \"Home\", href: \"/\" },\n * {\n * label: \"Products\",\n * children: [\n * { label: \"Analytics\", href: \"/analytics\" },\n * { label: \"Billing\", onSelect: () => openBilling() },\n * ],\n * },\n * ]}\n * />\n */\nexport function NavigationMenu({ items, className, ...props }: NavigationMenuProps) {\n const [clickedIndex, setClickedIndex] = useState<number>(-1);\n const [hovered, setHovered] = useState<number>(-1);\n const baseId = useId();\n const rootRef = useRef<HTMLElement>(null);\n\n const close = useCallback((): void => {\n setClickedIndex(-1);\n setHovered(-1);\n }, []);\n\n const openIndex = clickedIndex !== -1 ? clickedIndex : hovered;\n\n useEffect(() => {\n if (openIndex === -1) return;\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") close();\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, close]);\n\n const renderLeaf = (item: NavigationMenuItem, role: \"none\" | \"menuitem\"): ReactNode => {\n const handleClick = (): void => {\n item.onSelect?.();\n close();\n };\n if (item.href) {\n return (\n <a\n href={item.href}\n role={role === \"menuitem\" ? \"menuitem\" : undefined}\n className={styles.link}\n onClick={handleClick}\n >\n {item.label}\n </a>\n );\n }\n return (\n <button\n type=\"button\"\n role={role === \"menuitem\" ? \"menuitem\" : undefined}\n className={styles.link}\n onClick={handleClick}\n >\n {item.label}\n </button>\n );\n };\n\n return (\n <nav ref={rootRef} className={cn(styles.root, className)} {...props}>\n <ul className={styles.list}>\n {items.map((item, index) => {\n const hasChildren = !!item.children && item.children.length > 0;\n if (!hasChildren) {\n return (\n <li key={index} className={styles.item}>\n {renderLeaf(item, \"none\")}\n </li>\n );\n }\n const open = openIndex === index;\n const panelId = `${baseId}-panel-${index}`;\n return (\n <li\n key={index}\n className={styles.item}\n onMouseEnter={() => setHovered(index)}\n onMouseLeave={() => setHovered((prev) => (prev === index ? -1 : prev))}\n >\n <button\n type=\"button\"\n className={styles.trigger}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-controls={panelId}\n onClick={() =>\n setClickedIndex((prev) => (prev === index ? -1 : index))\n }\n onFocus={() => setHovered(index)}\n >\n {item.label}\n </button>\n {open && (\n <ul id={panelId} role=\"menu\" className={styles.panel}>\n {item.children!.map((child, childIndex) => (\n <li key={childIndex} role=\"none\" className={styles.subItem}>\n {renderLeaf(child, \"menuitem\")}\n </li>\n ))}\n </ul>\n )}\n </li>\n );\n })}\n </ul>\n </nav>\n );\n}\n"],"mappings":"qIAgDA,SAAgB,EAAe,CAAE,QAAO,YAAW,GAAG,GAA8B,CAChF,GAAM,CAAC,EAAc,IAAA,EAAA,EAAA,SAAA,CAAoC,EAAE,EACrD,CAAC,EAAS,IAAA,EAAA,EAAA,SAAA,CAA+B,EAAE,EAC3C,GAAA,EAAA,EAAA,MAAA,CAAe,EACf,GAAA,EAAA,EAAA,OAAA,CAA8B,IAAI,EAElC,GAAA,EAAA,EAAA,YAAA,KAAgC,CAClC,EAAgB,EAAE,EAClB,EAAW,EAAE,CACjB,EAAG,CAAC,CAAC,EAEC,EAAY,IAAiB,GAAoB,EAAf,GAExC,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,IAAc,GAAI,OACtB,IAAM,EAAS,GAA+B,CACtC,EAAM,MAAQ,UAAU,EAAM,CACtC,EACM,EAAU,GAA4B,CACpC,EAAQ,SAAW,CAAC,EAAQ,QAAQ,SAAS,EAAM,MAAc,GAAG,EAAM,CAClF,EAGA,OAFA,OAAO,iBAAiB,UAAW,CAAK,EACxC,OAAO,iBAAiB,YAAa,CAAM,MAC9B,CACT,OAAO,oBAAoB,UAAW,CAAK,EAC3C,OAAO,oBAAoB,YAAa,CAAM,CAClD,CACJ,EAAG,CAAC,EAAW,CAAK,CAAC,EAErB,IAAM,GAAc,EAA0B,IAAyC,CACnF,IAAM,MAA0B,CAC5B,EAAK,WAAW,EAChB,EAAM,CACV,EAaA,OAZI,EAAK,MAED,EAAA,EAAA,IAAA,CAAC,IAAD,CACI,KAAM,EAAK,KACX,KAAM,IAAS,WAAa,WAAa,IAAA,GACzC,UAAW,EAAA,QAAO,KAClB,QAAS,WAER,EAAK,KACP,CAAA,GAIP,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,KAAM,IAAS,WAAa,WAAa,IAAA,GACzC,UAAW,EAAA,QAAO,KAClB,QAAS,WAER,EAAK,KACF,CAAA,CAEhB,EAEA,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,IAAK,EAAS,UAAW,EAAA,GAAG,EAAA,QAAO,KAAM,CAAS,EAAG,GAAI,YAC1D,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,cACjB,EAAM,KAAK,EAAM,IAAU,CAExB,GAAI,EADkB,EAAK,UAAY,EAAK,SAAS,OAAS,GAE1D,OACI,EAAA,EAAA,IAAA,CAAC,KAAD,CAAgB,UAAW,EAAA,QAAO,cAC7B,EAAW,EAAM,MAAM,CACxB,EAFK,CAEL,EAGZ,IAAM,EAAO,IAAc,EACrB,EAAU,GAAG,EAAO,SAAS,IACnC,OACI,EAAA,EAAA,KAAA,CAAC,KAAD,CAEI,UAAW,EAAA,QAAO,KAClB,iBAAoB,EAAW,CAAK,EACpC,iBAAoB,EAAY,GAAU,IAAS,EAAQ,GAAK,CAAK,WAJzE,EAMI,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,gBAAc,OACd,gBAAe,EACf,gBAAe,EACf,YACI,EAAiB,GAAU,IAAS,EAAQ,GAAK,CAAM,EAE3D,YAAe,EAAW,CAAK,WAE9B,EAAK,KACF,CAAA,EACP,IACG,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,GAAI,EAAS,KAAK,OAAO,UAAW,EAAA,QAAO,eAC1C,EAAK,SAAU,KAAK,EAAO,KACxB,EAAA,EAAA,IAAA,CAAC,KAAD,CAAqB,KAAK,OAAO,UAAW,EAAA,QAAO,iBAC9C,EAAW,EAAO,UAAU,CAC7B,EAFK,CAEL,CACP,CACD,CAAA,CAER,GA3BK,CA2BL,CAEZ,CAAC,CACD,CAAA,CACH,CAAA,CAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NavigationMenu.js","names":[],"sources":["../../../src/components/NavigationMenu/NavigationMenu.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"NavigationMenu.js","names":[],"sources":["../../../src/components/NavigationMenu/NavigationMenu.tsx"],"sourcesContent":["/**\n * @tempest-limits function-lines — the body resolves the active item from the\n * current path, owns the open submenu and the hover-intent delay, all of which\n * decide the same highlighted row.\n */\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./NavigationMenu.module.css\";\n\nexport interface NavigationMenuItem {\n /** Visible label for the entry. */\n label: ReactNode;\n /** When set, the entry renders as an anchor. */\n href?: string;\n /** Invoked when the entry is activated (click / Enter). */\n onSelect?: () => void;\n /** Nested entries — presence turns the item into a submenu trigger. */\n children?: NavigationMenuItem[];\n}\n\nexport interface NavigationMenuProps extends HTMLAttributes<HTMLElement> {\n /** Top-level navigation entries. */\n items: NavigationMenuItem[];\n}\n\n/**\n * Horizontal navigation menu with hover/click/focus dropdown submenus.\n *\n * - Top-level items render in a `<nav><ul>`.\n * - Items with `children` open a submenu panel (`role=\"menu\"`) on hover, focus,\n * or click. Only one panel is open at a time.\n * - Closes on outside click, Escape, or selecting a leaf entry.\n *\n * @example\n * <NavigationMenu\n * items={[\n * { label: \"Home\", href: \"/\" },\n * {\n * label: \"Products\",\n * children: [\n * { label: \"Analytics\", href: \"/analytics\" },\n * { label: \"Billing\", onSelect: () => openBilling() },\n * ],\n * },\n * ]}\n * />\n */\nexport function NavigationMenu({ items, className, ...props }: NavigationMenuProps) {\n const [clickedIndex, setClickedIndex] = useState<number>(-1);\n const [hovered, setHovered] = useState<number>(-1);\n const baseId = useId();\n const rootRef = useRef<HTMLElement>(null);\n\n const close = useCallback((): void => {\n setClickedIndex(-1);\n setHovered(-1);\n }, []);\n\n const openIndex = clickedIndex !== -1 ? clickedIndex : hovered;\n\n useEffect(() => {\n if (openIndex === -1) return;\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") close();\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, close]);\n\n const renderLeaf = (item: NavigationMenuItem, role: \"none\" | \"menuitem\"): ReactNode => {\n const handleClick = (): void => {\n item.onSelect?.();\n close();\n };\n if (item.href) {\n return (\n <a\n href={item.href}\n role={role === \"menuitem\" ? \"menuitem\" : undefined}\n className={styles.link}\n onClick={handleClick}\n >\n {item.label}\n </a>\n );\n }\n return (\n <button\n type=\"button\"\n role={role === \"menuitem\" ? \"menuitem\" : undefined}\n className={styles.link}\n onClick={handleClick}\n >\n {item.label}\n </button>\n );\n };\n\n return (\n <nav ref={rootRef} className={cn(styles.root, className)} {...props}>\n <ul className={styles.list}>\n {items.map((item, index) => {\n const hasChildren = !!item.children && item.children.length > 0;\n if (!hasChildren) {\n return (\n <li key={index} className={styles.item}>\n {renderLeaf(item, \"none\")}\n </li>\n );\n }\n const open = openIndex === index;\n const panelId = `${baseId}-panel-${index}`;\n return (\n <li\n key={index}\n className={styles.item}\n onMouseEnter={() => setHovered(index)}\n onMouseLeave={() => setHovered((prev) => (prev === index ? -1 : prev))}\n >\n <button\n type=\"button\"\n className={styles.trigger}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-controls={panelId}\n onClick={() =>\n setClickedIndex((prev) => (prev === index ? -1 : index))\n }\n onFocus={() => setHovered(index)}\n >\n {item.label}\n </button>\n {open && (\n <ul id={panelId} role=\"menu\" className={styles.panel}>\n {item.children!.map((child, childIndex) => (\n <li key={childIndex} role=\"none\" className={styles.subItem}>\n {renderLeaf(child, \"menuitem\")}\n </li>\n ))}\n </ul>\n )}\n </li>\n );\n })}\n </ul>\n </nav>\n );\n}\n"],"mappings":";;;;;AAgDA,SAAgB,EAAe,EAAE,UAAO,cAAW,GAAG,KAA8B;CAChF,IAAM,CAAC,GAAc,KAAmB,EAAiB,EAAE,GACrD,CAAC,GAAS,KAAc,EAAiB,EAAE,GAC3C,IAAS,EAAM,GACf,IAAU,EAAoB,IAAI,GAElC,IAAQ,QAAwB;EAElC,AADA,EAAgB,EAAE,GAClB,EAAW,EAAE;CACjB,GAAG,CAAC,CAAC,GAEC,IAAY,MAAiB,KAAoB,IAAf;CAExC,QAAgB;EACZ,IAAI,MAAc,IAAI;EACtB,IAAM,KAAS,MAA+B;GAC1C,AAAI,EAAM,QAAQ,YAAU,EAAM;EACtC,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,CAAC,GAAW,CAAK,CAAC;CAErB,IAAM,KAAc,GAA0B,MAAyC;EACnF,IAAM,UAA0B;GAE5B,AADA,EAAK,WAAW,GAChB,EAAM;EACV;EAaA,OAZI,EAAK,OAED,kBAAC,KAAD;GACI,MAAM,EAAK;GACX,MAAM,MAAS,aAAa,aAAa,KAAA;GACzC,WAAW,EAAO;GAClB,SAAS;aAER,EAAK;EACP,CAAA,IAIP,kBAAC,UAAD;GACI,MAAK;GACL,MAAM,MAAS,aAAa,aAAa,KAAA;GACzC,WAAW,EAAO;GAClB,SAAS;aAER,EAAK;EACF,CAAA;CAEhB;CAEA,OACI,kBAAC,OAAD;EAAK,KAAK;EAAS,WAAW,EAAG,EAAO,MAAM,CAAS;EAAG,GAAI;YAC1D,kBAAC,MAAD;GAAI,WAAW,EAAO;aACjB,EAAM,KAAK,GAAM,MAAU;IAExB,IAAI,EADkB,EAAK,YAAY,EAAK,SAAS,SAAS,IAE1D,OACI,kBAAC,MAAD;KAAgB,WAAW,EAAO;eAC7B,EAAW,GAAM,MAAM;IACxB,GAFK,CAEL;IAGZ,IAAM,IAAO,MAAc,GACrB,IAAU,GAAG,EAAO,SAAS;IACnC,OACI,kBAAC,MAAD;KAEI,WAAW,EAAO;KAClB,oBAAoB,EAAW,CAAK;KACpC,oBAAoB,GAAY,MAAU,MAAS,IAAQ,KAAK,CAAK;eAJzE,CAMI,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,iBAAc;MACd,iBAAe;MACf,iBAAe;MACf,eACI,GAAiB,MAAU,MAAS,IAAQ,KAAK,CAAM;MAE3D,eAAe,EAAW,CAAK;gBAE9B,EAAK;KACF,CAAA,GACP,KACG,kBAAC,MAAD;MAAI,IAAI;MAAS,MAAK;MAAO,WAAW,EAAO;gBAC1C,EAAK,SAAU,KAAK,GAAO,MACxB,kBAAC,MAAD;OAAqB,MAAK;OAAO,WAAW,EAAO;iBAC9C,EAAW,GAAO,UAAU;MAC7B,GAFK,CAEL,CACP;KACD,CAAA,CAER;OA3BK,CA2BL;GAEZ,CAAC;EACD,CAAA;CACH,CAAA;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotificationCenter.cjs","names":[],"sources":["../../../src/components/NotificationCenter/NotificationCenter.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { relativeTime, type RelativeTimeLocale } from \"@/utils/relative-time\";\n\nimport { Badge } from \"../Badge\";\nimport { EmptyState } from \"../EmptyState\";\nimport type { NotificationItem } from \"./use-notification-inbox\";\nimport styles from \"./NotificationCenter.module.css\";\n\n/**\n * DOM attributes this component redefines.\n *\n * `title` becomes a rendered heading node instead of a tooltip string, and\n * `onSelect` becomes \"an entry was activated\" instead of the DOM text-selection\n * event. Both would be type errors if merged, and silently confusing if allowed\n * through.\n */\ntype OverriddenDomProps = \"children\" | \"title\" | \"onSelect\";\n\nexport interface NotificationCenterProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /** Entries to show. Rendered in the order given — `useNotificationInbox` sorts newest first. */\n items: readonly NotificationItem[];\n /**\n * Panel heading. Pass `null` to render the list with no header.\n *\n * Widened from the DOM `title` attribute (a `string`) on purpose: this is a\n * rendered heading, not a tooltip, so it takes a node.\n */\n title?: ReactNode;\n /** Activate an entry: click, `Enter` or `Space`. Also marks it read when `onMarkRead` is set. */\n onSelect?: (item: NotificationItem) => void;\n /** Mark one entry as read. Enables the per-item read control. */\n onMarkRead?: (id: string) => void;\n /** Mark every entry as read. Enables the header action. */\n onMarkAllRead?: () => void;\n /** Remove one entry. Enables the per-item dismiss control. */\n onDismiss?: (id: string) => void;\n /** Icon rendered at the leading edge of each row — an `<Icon name={…} />`, say. */\n renderIcon?: (item: NotificationItem) => ReactNode;\n /** Locale for the relative timestamps. Default `\"pt-BR\"`. */\n locale?: RelativeTimeLocale;\n /** Shown when `items` is empty. */\n emptyState?: ReactNode;\n /** Reference point for the relative timestamps. Default: now, at render time. */\n now?: number;\n}\n\n/**\n * Inbox panel for notifications the app has received.\n *\n * Deliberately presentational and controlled: it takes a list and emits intent.\n * Where the list comes from — an API, a service-worker push, `localStorage` — and\n * where read state is written are app decisions, and a component that assumed any\n * of them would be wrong for most apps. `useNotificationInbox` is the companion\n * that holds the state, including the service-worker bridge.\n *\n * It is also just the panel, not a popover: apps mount it inside their own\n * `Popover`, `Drawer` or route. That keeps one component from owning both the\n * inbox and a positioning strategy.\n *\n * @example\n * const inbox = useNotificationInbox();\n *\n * <Popover trigger={<Button>Notificações ({inbox.unreadCount})</Button>}>\n * <NotificationCenter\n * items={inbox.items}\n * onMarkRead={inbox.markRead}\n * onMarkAllRead={inbox.markAllRead}\n * onDismiss={inbox.remove}\n * onSelect={(item) => item.url && navigate(item.url)}\n * />\n * </Popover>\n */\nexport function NotificationCenter({\n items,\n title = \"Notificações\",\n onSelect,\n onMarkRead,\n onMarkAllRead,\n onDismiss,\n renderIcon,\n locale = \"pt-BR\",\n emptyState,\n now,\n className,\n ...rest\n}: NotificationCenterProps) {\n const unreadCount = items.filter((item) => !item.read).length;\n const reference = now ?? new Date().getTime();\n\n /**\n * Activate an entry.\n *\n * Reading a notification is implied by opening it, so activation marks it read\n * as well — otherwise every app would have to remember to call both, and the\n * unread badge would keep counting something the user already saw.\n */\n const activate = (item: NotificationItem): void => {\n if (!item.read) onMarkRead?.(item.id);\n onSelect?.(item);\n };\n\n const interactive = Boolean(onSelect || onMarkRead);\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {(title || (onMarkAllRead && unreadCount > 0)) && (\n <header className={styles.header}>\n {title ? (\n <h2 className={styles.title}>\n {title}\n {unreadCount > 0 && (\n <Badge variant=\"primary\" aria-label={`${unreadCount} não lidas`}>\n {unreadCount}\n </Badge>\n )}\n </h2>\n ) : (\n <span />\n )}\n {onMarkAllRead && unreadCount > 0 && (\n <button\n type=\"button\"\n className={styles.headerAction}\n onClick={onMarkAllRead}\n >\n Marcar todas como lidas\n </button>\n )}\n </header>\n )}\n\n {items.length === 0 ? (\n (emptyState ?? <EmptyState title=\"Nenhuma notificação\" />)\n ) : (\n <ul className={styles.list}>\n {items.map((item) => (\n <li\n key={item.id}\n className={cn(styles.item, !item.read && styles.unread)}\n aria-current={!item.read ? \"true\" : undefined}\n >\n {renderIcon && <span className={styles.icon}>{renderIcon(item)}</span>}\n\n <div className={styles.body}>\n {interactive ? (\n <button\n type=\"button\"\n className={styles.trigger}\n onClick={() => activate(item)}\n >\n <span className={styles.itemTitle}>{item.title}</span>\n {item.body && (\n <span className={styles.itemBody}>{item.body}</span>\n )}\n </button>\n ) : (\n <>\n <span className={styles.itemTitle}>{item.title}</span>\n {item.body && (\n <span className={styles.itemBody}>{item.body}</span>\n )}\n </>\n )}\n <time\n className={styles.time}\n dateTime={new Date(item.receivedAt).toISOString()}\n >\n {relativeTime(item.receivedAt, { locale, now: reference })}\n </time>\n </div>\n\n {onDismiss && (\n <button\n type=\"button\"\n className={styles.dismiss}\n onClick={() => onDismiss(item.id)}\n aria-label={`Descartar: ${item.title}`}\n >\n ×\n </button>\n )}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":"2OA6EA,SAAgB,EAAmB,CAC/B,QACA,QAAQ,eACR,WACA,aACA,gBACA,YACA,aACA,SAAS,QACT,aACA,MACA,YACA,GAAG,GACqB,CACxB,IAAM,EAAc,EAAM,OAAQ,GAAS,CAAC,EAAK,IAAI,CAAC,CAAC,OACjD,EAAY,GAAO,IAAI,KAAK,CAAA,CAAE,QAAQ,EAStC,EAAY,GAAiC,CAC1C,EAAK,MAAM,IAAa,EAAK,EAAE,EACpC,IAAW,CAAI,CACnB,EAEM,EAAc,GAAQ,GAAY,GAExC,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,MAAO,CAAS,EAAG,GAAI,WAAjD,EACM,GAAU,GAAiB,EAAc,KACvC,EAAA,EAAA,KAAA,CAAC,SAAD,CAAQ,UAAW,EAAA,QAAO,gBAA1B,CACK,GACG,EAAA,EAAA,KAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,eAAtB,CACK,EACA,EAAc,IACX,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CAAO,QAAQ,UAAU,aAAY,GAAG,EAAY,qBAC/C,CACE,CAAA,CAEX,KAEJ,EAAA,EAAA,IAAA,CAAC,OAAD,CAAO,CAAA,EAEV,GAAiB,EAAc,IAC5B,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,aAClB,QAAS,WACZ,yBAEO,CAAA,CAER,IAGX,EAAM,SAAW,EACb,IAAc,EAAA,EAAA,IAAA,CAAC,EAAA,WAAD,CAAY,MAAM,qBAAuB,CAAA,GAExD,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,cACjB,EAAM,IAAK,IACR,EAAA,EAAA,KAAA,CAAC,KAAD,CAEI,UAAW,EAAA,GAAG,EAAA,QAAO,KAAM,CAAC,EAAK,MAAQ,EAAA,QAAO,MAAM,EACtD,eAAe,EAAK,KAAgB,IAAA,GAAT,gBAH/B,CAKK,IAAc,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,cAAO,EAAW,CAAI,CAAQ,CAAA,GAErE,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,cAAvB,CACK,GACG,EAAA,EAAA,KAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,YAAe,EAAS,CAAI,WAHhC,EAKI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,mBAAY,EAAK,KAAY,CAAA,EACpD,EAAK,OACF,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,kBAAW,EAAK,IAAW,CAAA,CAEnD,KAER,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,mBAAY,EAAK,KAAY,CAAA,EACpD,EAAK,OACF,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,kBAAW,EAAK,IAAW,CAAA,CAEzD,CAAA,CAAA,GAEN,EAAA,EAAA,IAAA,CAAC,OAAD,CACI,UAAW,EAAA,QAAO,KAClB,SAAU,IAAI,KAAK,EAAK,UAAU,CAAC,CAAC,YAAY,WAE/C,EAAA,aAAa,EAAK,WAAY,CAAE,SAAQ,IAAK,CAAU,CAAC,CACvD,CAAA,CACL,IAEJ,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,YAAe,EAAU,EAAK,EAAE,EAChC,aAAY,cAAc,EAAK,iBAClC,GAEO,CAAA,CAEZ,GA5CK,EAAK,EA4CV,CACP,CACD,CAAA,CAEP,GAEb"}
|
|
1
|
+
{"version":3,"file":"NotificationCenter.cjs","names":[],"sources":["../../../src/components/NotificationCenter/NotificationCenter.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — an inbox has four actions the app\n * must implement (onSelect, onMarkRead, onMarkAllRead, onDismiss) over one list,\n * plus the presentation it needs to render a readable feed: renderIcon, locale, now\n * for relative timestamps, emptyState and title.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { relativeTime, type RelativeTimeLocale } from \"@/utils/relative-time\";\n\nimport { Badge } from \"../Badge\";\nimport { EmptyState } from \"../EmptyState\";\nimport type { NotificationItem } from \"./use-notification-inbox\";\nimport styles from \"./NotificationCenter.module.css\";\n\n/**\n * DOM attributes this component redefines.\n *\n * `title` becomes a rendered heading node instead of a tooltip string, and\n * `onSelect` becomes \"an entry was activated\" instead of the DOM text-selection\n * event. Both would be type errors if merged, and silently confusing if allowed\n * through.\n */\ntype OverriddenDomProps = \"children\" | \"title\" | \"onSelect\";\n\nexport interface NotificationCenterProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /** Entries to show. Rendered in the order given — `useNotificationInbox` sorts newest first. */\n items: readonly NotificationItem[];\n /**\n * Panel heading. Pass `null` to render the list with no header.\n *\n * Widened from the DOM `title` attribute (a `string`) on purpose: this is a\n * rendered heading, not a tooltip, so it takes a node.\n */\n title?: ReactNode;\n /** Activate an entry: click, `Enter` or `Space`. Also marks it read when `onMarkRead` is set. */\n onSelect?: (item: NotificationItem) => void;\n /** Mark one entry as read. Enables the per-item read control. */\n onMarkRead?: (id: string) => void;\n /** Mark every entry as read. Enables the header action. */\n onMarkAllRead?: () => void;\n /** Remove one entry. Enables the per-item dismiss control. */\n onDismiss?: (id: string) => void;\n /** Icon rendered at the leading edge of each row — an `<Icon name={…} />`, say. */\n renderIcon?: (item: NotificationItem) => ReactNode;\n /** Locale for the relative timestamps. Default `\"pt-BR\"`. */\n locale?: RelativeTimeLocale;\n /** Shown when `items` is empty. */\n emptyState?: ReactNode;\n /** Reference point for the relative timestamps. Default: now, at render time. */\n now?: number;\n}\n\n/**\n * Inbox panel for notifications the app has received.\n *\n * Deliberately presentational and controlled: it takes a list and emits intent.\n * Where the list comes from — an API, a service-worker push, `localStorage` — and\n * where read state is written are app decisions, and a component that assumed any\n * of them would be wrong for most apps. `useNotificationInbox` is the companion\n * that holds the state, including the service-worker bridge.\n *\n * It is also just the panel, not a popover: apps mount it inside their own\n * `Popover`, `Drawer` or route. That keeps one component from owning both the\n * inbox and a positioning strategy.\n *\n * @example\n * const inbox = useNotificationInbox();\n *\n * <Popover trigger={<Button>Notificações ({inbox.unreadCount})</Button>}>\n * <NotificationCenter\n * items={inbox.items}\n * onMarkRead={inbox.markRead}\n * onMarkAllRead={inbox.markAllRead}\n * onDismiss={inbox.remove}\n * onSelect={(item) => item.url && navigate(item.url)}\n * />\n * </Popover>\n */\nexport function NotificationCenter({\n items,\n title = \"Notificações\",\n onSelect,\n onMarkRead,\n onMarkAllRead,\n onDismiss,\n renderIcon,\n locale = \"pt-BR\",\n emptyState,\n now,\n className,\n ...rest\n}: NotificationCenterProps) {\n const unreadCount = items.filter((item) => !item.read).length;\n const reference = now ?? new Date().getTime();\n\n /**\n * Activate an entry.\n *\n * Reading a notification is implied by opening it, so activation marks it read\n * as well — otherwise every app would have to remember to call both, and the\n * unread badge would keep counting something the user already saw.\n */\n const activate = (item: NotificationItem): void => {\n if (!item.read) onMarkRead?.(item.id);\n onSelect?.(item);\n };\n\n const interactive = Boolean(onSelect || onMarkRead);\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {(title || (onMarkAllRead && unreadCount > 0)) && (\n <header className={styles.header}>\n {title ? (\n <h2 className={styles.title}>\n {title}\n {unreadCount > 0 && (\n <Badge variant=\"primary\" aria-label={`${unreadCount} não lidas`}>\n {unreadCount}\n </Badge>\n )}\n </h2>\n ) : (\n <span />\n )}\n {onMarkAllRead && unreadCount > 0 && (\n <button\n type=\"button\"\n className={styles.headerAction}\n onClick={onMarkAllRead}\n >\n Marcar todas como lidas\n </button>\n )}\n </header>\n )}\n\n {items.length === 0 ? (\n (emptyState ?? <EmptyState title=\"Nenhuma notificação\" />)\n ) : (\n <ul className={styles.list}>\n {items.map((item) => (\n <li\n key={item.id}\n className={cn(styles.item, !item.read && styles.unread)}\n aria-current={!item.read ? \"true\" : undefined}\n >\n {renderIcon && <span className={styles.icon}>{renderIcon(item)}</span>}\n\n <div className={styles.body}>\n {interactive ? (\n <button\n type=\"button\"\n className={styles.trigger}\n onClick={() => activate(item)}\n >\n <span className={styles.itemTitle}>{item.title}</span>\n {item.body && (\n <span className={styles.itemBody}>{item.body}</span>\n )}\n </button>\n ) : (\n <>\n <span className={styles.itemTitle}>{item.title}</span>\n {item.body && (\n <span className={styles.itemBody}>{item.body}</span>\n )}\n </>\n )}\n <time\n className={styles.time}\n dateTime={new Date(item.receivedAt).toISOString()}\n >\n {relativeTime(item.receivedAt, { locale, now: reference })}\n </time>\n </div>\n\n {onDismiss && (\n <button\n type=\"button\"\n className={styles.dismiss}\n onClick={() => onDismiss(item.id)}\n aria-label={`Descartar: ${item.title}`}\n >\n ×\n </button>\n )}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":"2OAmFA,SAAgB,EAAmB,CAC/B,QACA,QAAQ,eACR,WACA,aACA,gBACA,YACA,aACA,SAAS,QACT,aACA,MACA,YACA,GAAG,GACqB,CACxB,IAAM,EAAc,EAAM,OAAQ,GAAS,CAAC,EAAK,IAAI,CAAC,CAAC,OACjD,EAAY,GAAO,IAAI,KAAK,CAAA,CAAE,QAAQ,EAStC,EAAY,GAAiC,CAC1C,EAAK,MAAM,IAAa,EAAK,EAAE,EACpC,IAAW,CAAI,CACnB,EAEM,EAAc,GAAQ,GAAY,GAExC,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,MAAO,CAAS,EAAG,GAAI,WAAjD,EACM,GAAU,GAAiB,EAAc,KACvC,EAAA,EAAA,KAAA,CAAC,SAAD,CAAQ,UAAW,EAAA,QAAO,gBAA1B,CACK,GACG,EAAA,EAAA,KAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,eAAtB,CACK,EACA,EAAc,IACX,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CAAO,QAAQ,UAAU,aAAY,GAAG,EAAY,qBAC/C,CACE,CAAA,CAEX,KAEJ,EAAA,EAAA,IAAA,CAAC,OAAD,CAAO,CAAA,EAEV,GAAiB,EAAc,IAC5B,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,aAClB,QAAS,WACZ,yBAEO,CAAA,CAER,IAGX,EAAM,SAAW,EACb,IAAc,EAAA,EAAA,IAAA,CAAC,EAAA,WAAD,CAAY,MAAM,qBAAuB,CAAA,GAExD,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,cACjB,EAAM,IAAK,IACR,EAAA,EAAA,KAAA,CAAC,KAAD,CAEI,UAAW,EAAA,GAAG,EAAA,QAAO,KAAM,CAAC,EAAK,MAAQ,EAAA,QAAO,MAAM,EACtD,eAAe,EAAK,KAAgB,IAAA,GAAT,gBAH/B,CAKK,IAAc,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,cAAO,EAAW,CAAI,CAAQ,CAAA,GAErE,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,cAAvB,CACK,GACG,EAAA,EAAA,KAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,YAAe,EAAS,CAAI,WAHhC,EAKI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,mBAAY,EAAK,KAAY,CAAA,EACpD,EAAK,OACF,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,kBAAW,EAAK,IAAW,CAAA,CAEnD,KAER,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,mBAAY,EAAK,KAAY,CAAA,EACpD,EAAK,OACF,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,kBAAW,EAAK,IAAW,CAAA,CAEzD,CAAA,CAAA,GAEN,EAAA,EAAA,IAAA,CAAC,OAAD,CACI,UAAW,EAAA,QAAO,KAClB,SAAU,IAAI,KAAK,EAAK,UAAU,CAAC,CAAC,YAAY,WAE/C,EAAA,aAAa,EAAK,WAAY,CAAE,SAAQ,IAAK,CAAU,CAAC,CACvD,CAAA,CACL,IAEJ,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,YAAe,EAAU,EAAK,EAAE,EAChC,aAAY,cAAc,EAAK,iBAClC,GAEO,CAAA,CAEZ,GA5CK,EAAK,EA4CV,CACP,CACD,CAAA,CAEP,GAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotificationCenter.js","names":[],"sources":["../../../src/components/NotificationCenter/NotificationCenter.tsx"],"sourcesContent":["import type { HTMLAttributes, ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { relativeTime, type RelativeTimeLocale } from \"@/utils/relative-time\";\n\nimport { Badge } from \"../Badge\";\nimport { EmptyState } from \"../EmptyState\";\nimport type { NotificationItem } from \"./use-notification-inbox\";\nimport styles from \"./NotificationCenter.module.css\";\n\n/**\n * DOM attributes this component redefines.\n *\n * `title` becomes a rendered heading node instead of a tooltip string, and\n * `onSelect` becomes \"an entry was activated\" instead of the DOM text-selection\n * event. Both would be type errors if merged, and silently confusing if allowed\n * through.\n */\ntype OverriddenDomProps = \"children\" | \"title\" | \"onSelect\";\n\nexport interface NotificationCenterProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /** Entries to show. Rendered in the order given — `useNotificationInbox` sorts newest first. */\n items: readonly NotificationItem[];\n /**\n * Panel heading. Pass `null` to render the list with no header.\n *\n * Widened from the DOM `title` attribute (a `string`) on purpose: this is a\n * rendered heading, not a tooltip, so it takes a node.\n */\n title?: ReactNode;\n /** Activate an entry: click, `Enter` or `Space`. Also marks it read when `onMarkRead` is set. */\n onSelect?: (item: NotificationItem) => void;\n /** Mark one entry as read. Enables the per-item read control. */\n onMarkRead?: (id: string) => void;\n /** Mark every entry as read. Enables the header action. */\n onMarkAllRead?: () => void;\n /** Remove one entry. Enables the per-item dismiss control. */\n onDismiss?: (id: string) => void;\n /** Icon rendered at the leading edge of each row — an `<Icon name={…} />`, say. */\n renderIcon?: (item: NotificationItem) => ReactNode;\n /** Locale for the relative timestamps. Default `\"pt-BR\"`. */\n locale?: RelativeTimeLocale;\n /** Shown when `items` is empty. */\n emptyState?: ReactNode;\n /** Reference point for the relative timestamps. Default: now, at render time. */\n now?: number;\n}\n\n/**\n * Inbox panel for notifications the app has received.\n *\n * Deliberately presentational and controlled: it takes a list and emits intent.\n * Where the list comes from — an API, a service-worker push, `localStorage` — and\n * where read state is written are app decisions, and a component that assumed any\n * of them would be wrong for most apps. `useNotificationInbox` is the companion\n * that holds the state, including the service-worker bridge.\n *\n * It is also just the panel, not a popover: apps mount it inside their own\n * `Popover`, `Drawer` or route. That keeps one component from owning both the\n * inbox and a positioning strategy.\n *\n * @example\n * const inbox = useNotificationInbox();\n *\n * <Popover trigger={<Button>Notificações ({inbox.unreadCount})</Button>}>\n * <NotificationCenter\n * items={inbox.items}\n * onMarkRead={inbox.markRead}\n * onMarkAllRead={inbox.markAllRead}\n * onDismiss={inbox.remove}\n * onSelect={(item) => item.url && navigate(item.url)}\n * />\n * </Popover>\n */\nexport function NotificationCenter({\n items,\n title = \"Notificações\",\n onSelect,\n onMarkRead,\n onMarkAllRead,\n onDismiss,\n renderIcon,\n locale = \"pt-BR\",\n emptyState,\n now,\n className,\n ...rest\n}: NotificationCenterProps) {\n const unreadCount = items.filter((item) => !item.read).length;\n const reference = now ?? new Date().getTime();\n\n /**\n * Activate an entry.\n *\n * Reading a notification is implied by opening it, so activation marks it read\n * as well — otherwise every app would have to remember to call both, and the\n * unread badge would keep counting something the user already saw.\n */\n const activate = (item: NotificationItem): void => {\n if (!item.read) onMarkRead?.(item.id);\n onSelect?.(item);\n };\n\n const interactive = Boolean(onSelect || onMarkRead);\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {(title || (onMarkAllRead && unreadCount > 0)) && (\n <header className={styles.header}>\n {title ? (\n <h2 className={styles.title}>\n {title}\n {unreadCount > 0 && (\n <Badge variant=\"primary\" aria-label={`${unreadCount} não lidas`}>\n {unreadCount}\n </Badge>\n )}\n </h2>\n ) : (\n <span />\n )}\n {onMarkAllRead && unreadCount > 0 && (\n <button\n type=\"button\"\n className={styles.headerAction}\n onClick={onMarkAllRead}\n >\n Marcar todas como lidas\n </button>\n )}\n </header>\n )}\n\n {items.length === 0 ? (\n (emptyState ?? <EmptyState title=\"Nenhuma notificação\" />)\n ) : (\n <ul className={styles.list}>\n {items.map((item) => (\n <li\n key={item.id}\n className={cn(styles.item, !item.read && styles.unread)}\n aria-current={!item.read ? \"true\" : undefined}\n >\n {renderIcon && <span className={styles.icon}>{renderIcon(item)}</span>}\n\n <div className={styles.body}>\n {interactive ? (\n <button\n type=\"button\"\n className={styles.trigger}\n onClick={() => activate(item)}\n >\n <span className={styles.itemTitle}>{item.title}</span>\n {item.body && (\n <span className={styles.itemBody}>{item.body}</span>\n )}\n </button>\n ) : (\n <>\n <span className={styles.itemTitle}>{item.title}</span>\n {item.body && (\n <span className={styles.itemBody}>{item.body}</span>\n )}\n </>\n )}\n <time\n className={styles.time}\n dateTime={new Date(item.receivedAt).toISOString()}\n >\n {relativeTime(item.receivedAt, { locale, now: reference })}\n </time>\n </div>\n\n {onDismiss && (\n <button\n type=\"button\"\n className={styles.dismiss}\n onClick={() => onDismiss(item.id)}\n aria-label={`Descartar: ${item.title}`}\n >\n ×\n </button>\n )}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;AA6EA,SAAgB,EAAmB,EAC/B,UACA,WAAQ,gBACR,aACA,eACA,kBACA,cACA,eACA,YAAS,SACT,eACA,QACA,cACA,GAAG,KACqB;CACxB,IAAM,IAAc,EAAM,QAAQ,MAAS,CAAC,EAAK,IAAI,CAAC,CAAC,QACjD,IAAY,sBAAO,IAAI,KAAK,EAAA,CAAE,QAAQ,GAStC,KAAY,MAAiC;EAE/C,AADK,EAAK,QAAM,IAAa,EAAK,EAAE,GACpC,IAAW,CAAI;CACnB,GAEM,IAAc,GAAQ,KAAY;CAExC,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,OAAO,CAAS;EAAG,GAAI;YAAjD,EACM,KAAU,KAAiB,IAAc,MACvC,kBAAC,UAAD;GAAQ,WAAW,EAAO;aAA1B,CACK,IACG,kBAAC,MAAD;IAAI,WAAW,EAAO;cAAtB,CACK,GACA,IAAc,KACX,kBAAC,GAAD;KAAO,SAAQ;KAAU,cAAY,GAAG,EAAY;eAC/C;IACE,CAAA,CAEX;QAEJ,kBAAC,QAAD,CAAO,CAAA,GAEV,KAAiB,IAAc,KAC5B,kBAAC,UAAD;IACI,MAAK;IACL,WAAW,EAAO;IAClB,SAAS;cACZ;GAEO,CAAA,CAER;MAGX,EAAM,WAAW,IACb,KAAc,kBAAC,GAAD,EAAY,OAAM,sBAAuB,CAAA,IAExD,kBAAC,MAAD;GAAI,WAAW,EAAO;aACjB,EAAM,KAAK,MACR,kBAAC,MAAD;IAEI,WAAW,EAAG,EAAO,MAAM,CAAC,EAAK,QAAQ,EAAO,MAAM;IACtD,gBAAe,EAAK,OAAgB,KAAA,IAAT;cAH/B;KAKK,KAAc,kBAAC,QAAD;MAAM,WAAW,EAAO;gBAAO,EAAW,CAAI;KAAQ,CAAA;KAErE,kBAAC,OAAD;MAAK,WAAW,EAAO;gBAAvB,CACK,IACG,kBAAC,UAAD;OACI,MAAK;OACL,WAAW,EAAO;OAClB,eAAe,EAAS,CAAI;iBAHhC,CAKI,kBAAC,QAAD;QAAM,WAAW,EAAO;kBAAY,EAAK;OAAY,CAAA,GACpD,EAAK,QACF,kBAAC,QAAD;QAAM,WAAW,EAAO;kBAAW,EAAK;OAAW,CAAA,CAEnD;WAER,kBAAA,GAAA,EAAA,UAAA,CACI,kBAAC,QAAD;OAAM,WAAW,EAAO;iBAAY,EAAK;MAAY,CAAA,GACpD,EAAK,QACF,kBAAC,QAAD;OAAM,WAAW,EAAO;iBAAW,EAAK;MAAW,CAAA,CAEzD,EAAA,CAAA,GAEN,kBAAC,QAAD;OACI,WAAW,EAAO;OAClB,UAAU,IAAI,KAAK,EAAK,UAAU,CAAC,CAAC,YAAY;iBAE/C,EAAa,EAAK,YAAY;QAAE;QAAQ,KAAK;OAAU,CAAC;MACvD,CAAA,CACL;;KAEJ,KACG,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,eAAe,EAAU,EAAK,EAAE;MAChC,cAAY,cAAc,EAAK;gBAClC;KAEO,CAAA;IAEZ;MA5CK,EAAK,EA4CV,CACP;EACD,CAAA,CAEP;;AAEb"}
|
|
1
|
+
{"version":3,"file":"NotificationCenter.js","names":[],"sources":["../../../src/components/NotificationCenter/NotificationCenter.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — an inbox has four actions the app\n * must implement (onSelect, onMarkRead, onMarkAllRead, onDismiss) over one list,\n * plus the presentation it needs to render a readable feed: renderIcon, locale, now\n * for relative timestamps, emptyState and title.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { relativeTime, type RelativeTimeLocale } from \"@/utils/relative-time\";\n\nimport { Badge } from \"../Badge\";\nimport { EmptyState } from \"../EmptyState\";\nimport type { NotificationItem } from \"./use-notification-inbox\";\nimport styles from \"./NotificationCenter.module.css\";\n\n/**\n * DOM attributes this component redefines.\n *\n * `title` becomes a rendered heading node instead of a tooltip string, and\n * `onSelect` becomes \"an entry was activated\" instead of the DOM text-selection\n * event. Both would be type errors if merged, and silently confusing if allowed\n * through.\n */\ntype OverriddenDomProps = \"children\" | \"title\" | \"onSelect\";\n\nexport interface NotificationCenterProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /** Entries to show. Rendered in the order given — `useNotificationInbox` sorts newest first. */\n items: readonly NotificationItem[];\n /**\n * Panel heading. Pass `null` to render the list with no header.\n *\n * Widened from the DOM `title` attribute (a `string`) on purpose: this is a\n * rendered heading, not a tooltip, so it takes a node.\n */\n title?: ReactNode;\n /** Activate an entry: click, `Enter` or `Space`. Also marks it read when `onMarkRead` is set. */\n onSelect?: (item: NotificationItem) => void;\n /** Mark one entry as read. Enables the per-item read control. */\n onMarkRead?: (id: string) => void;\n /** Mark every entry as read. Enables the header action. */\n onMarkAllRead?: () => void;\n /** Remove one entry. Enables the per-item dismiss control. */\n onDismiss?: (id: string) => void;\n /** Icon rendered at the leading edge of each row — an `<Icon name={…} />`, say. */\n renderIcon?: (item: NotificationItem) => ReactNode;\n /** Locale for the relative timestamps. Default `\"pt-BR\"`. */\n locale?: RelativeTimeLocale;\n /** Shown when `items` is empty. */\n emptyState?: ReactNode;\n /** Reference point for the relative timestamps. Default: now, at render time. */\n now?: number;\n}\n\n/**\n * Inbox panel for notifications the app has received.\n *\n * Deliberately presentational and controlled: it takes a list and emits intent.\n * Where the list comes from — an API, a service-worker push, `localStorage` — and\n * where read state is written are app decisions, and a component that assumed any\n * of them would be wrong for most apps. `useNotificationInbox` is the companion\n * that holds the state, including the service-worker bridge.\n *\n * It is also just the panel, not a popover: apps mount it inside their own\n * `Popover`, `Drawer` or route. That keeps one component from owning both the\n * inbox and a positioning strategy.\n *\n * @example\n * const inbox = useNotificationInbox();\n *\n * <Popover trigger={<Button>Notificações ({inbox.unreadCount})</Button>}>\n * <NotificationCenter\n * items={inbox.items}\n * onMarkRead={inbox.markRead}\n * onMarkAllRead={inbox.markAllRead}\n * onDismiss={inbox.remove}\n * onSelect={(item) => item.url && navigate(item.url)}\n * />\n * </Popover>\n */\nexport function NotificationCenter({\n items,\n title = \"Notificações\",\n onSelect,\n onMarkRead,\n onMarkAllRead,\n onDismiss,\n renderIcon,\n locale = \"pt-BR\",\n emptyState,\n now,\n className,\n ...rest\n}: NotificationCenterProps) {\n const unreadCount = items.filter((item) => !item.read).length;\n const reference = now ?? new Date().getTime();\n\n /**\n * Activate an entry.\n *\n * Reading a notification is implied by opening it, so activation marks it read\n * as well — otherwise every app would have to remember to call both, and the\n * unread badge would keep counting something the user already saw.\n */\n const activate = (item: NotificationItem): void => {\n if (!item.read) onMarkRead?.(item.id);\n onSelect?.(item);\n };\n\n const interactive = Boolean(onSelect || onMarkRead);\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {(title || (onMarkAllRead && unreadCount > 0)) && (\n <header className={styles.header}>\n {title ? (\n <h2 className={styles.title}>\n {title}\n {unreadCount > 0 && (\n <Badge variant=\"primary\" aria-label={`${unreadCount} não lidas`}>\n {unreadCount}\n </Badge>\n )}\n </h2>\n ) : (\n <span />\n )}\n {onMarkAllRead && unreadCount > 0 && (\n <button\n type=\"button\"\n className={styles.headerAction}\n onClick={onMarkAllRead}\n >\n Marcar todas como lidas\n </button>\n )}\n </header>\n )}\n\n {items.length === 0 ? (\n (emptyState ?? <EmptyState title=\"Nenhuma notificação\" />)\n ) : (\n <ul className={styles.list}>\n {items.map((item) => (\n <li\n key={item.id}\n className={cn(styles.item, !item.read && styles.unread)}\n aria-current={!item.read ? \"true\" : undefined}\n >\n {renderIcon && <span className={styles.icon}>{renderIcon(item)}</span>}\n\n <div className={styles.body}>\n {interactive ? (\n <button\n type=\"button\"\n className={styles.trigger}\n onClick={() => activate(item)}\n >\n <span className={styles.itemTitle}>{item.title}</span>\n {item.body && (\n <span className={styles.itemBody}>{item.body}</span>\n )}\n </button>\n ) : (\n <>\n <span className={styles.itemTitle}>{item.title}</span>\n {item.body && (\n <span className={styles.itemBody}>{item.body}</span>\n )}\n </>\n )}\n <time\n className={styles.time}\n dateTime={new Date(item.receivedAt).toISOString()}\n >\n {relativeTime(item.receivedAt, { locale, now: reference })}\n </time>\n </div>\n\n {onDismiss && (\n <button\n type=\"button\"\n className={styles.dismiss}\n onClick={() => onDismiss(item.id)}\n aria-label={`Descartar: ${item.title}`}\n >\n ×\n </button>\n )}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAmFA,SAAgB,EAAmB,EAC/B,UACA,WAAQ,gBACR,aACA,eACA,kBACA,cACA,eACA,YAAS,SACT,eACA,QACA,cACA,GAAG,KACqB;CACxB,IAAM,IAAc,EAAM,QAAQ,MAAS,CAAC,EAAK,IAAI,CAAC,CAAC,QACjD,IAAY,sBAAO,IAAI,KAAK,EAAA,CAAE,QAAQ,GAStC,KAAY,MAAiC;EAE/C,AADK,EAAK,QAAM,IAAa,EAAK,EAAE,GACpC,IAAW,CAAI;CACnB,GAEM,IAAc,GAAQ,KAAY;CAExC,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,OAAO,CAAS;EAAG,GAAI;YAAjD,EACM,KAAU,KAAiB,IAAc,MACvC,kBAAC,UAAD;GAAQ,WAAW,EAAO;aAA1B,CACK,IACG,kBAAC,MAAD;IAAI,WAAW,EAAO;cAAtB,CACK,GACA,IAAc,KACX,kBAAC,GAAD;KAAO,SAAQ;KAAU,cAAY,GAAG,EAAY;eAC/C;IACE,CAAA,CAEX;QAEJ,kBAAC,QAAD,CAAO,CAAA,GAEV,KAAiB,IAAc,KAC5B,kBAAC,UAAD;IACI,MAAK;IACL,WAAW,EAAO;IAClB,SAAS;cACZ;GAEO,CAAA,CAER;MAGX,EAAM,WAAW,IACb,KAAc,kBAAC,GAAD,EAAY,OAAM,sBAAuB,CAAA,IAExD,kBAAC,MAAD;GAAI,WAAW,EAAO;aACjB,EAAM,KAAK,MACR,kBAAC,MAAD;IAEI,WAAW,EAAG,EAAO,MAAM,CAAC,EAAK,QAAQ,EAAO,MAAM;IACtD,gBAAe,EAAK,OAAgB,KAAA,IAAT;cAH/B;KAKK,KAAc,kBAAC,QAAD;MAAM,WAAW,EAAO;gBAAO,EAAW,CAAI;KAAQ,CAAA;KAErE,kBAAC,OAAD;MAAK,WAAW,EAAO;gBAAvB,CACK,IACG,kBAAC,UAAD;OACI,MAAK;OACL,WAAW,EAAO;OAClB,eAAe,EAAS,CAAI;iBAHhC,CAKI,kBAAC,QAAD;QAAM,WAAW,EAAO;kBAAY,EAAK;OAAY,CAAA,GACpD,EAAK,QACF,kBAAC,QAAD;QAAM,WAAW,EAAO;kBAAW,EAAK;OAAW,CAAA,CAEnD;WAER,kBAAA,GAAA,EAAA,UAAA,CACI,kBAAC,QAAD;OAAM,WAAW,EAAO;iBAAY,EAAK;MAAY,CAAA,GACpD,EAAK,QACF,kBAAC,QAAD;OAAM,WAAW,EAAO;iBAAW,EAAK;MAAW,CAAA,CAEzD,EAAA,CAAA,GAEN,kBAAC,QAAD;OACI,WAAW,EAAO;OAClB,UAAU,IAAI,KAAK,EAAK,UAAU,CAAC,CAAC,YAAY;iBAE/C,EAAa,EAAK,YAAY;QAAE;QAAQ,KAAK;OAAU,CAAC;MACvD,CAAA,CACL;;KAEJ,KACG,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,eAAe,EAAU,EAAK,EAAE;MAChC,cAAY,cAAc,EAAK;gBAClC;KAEO,CAAA;IAEZ;MA5CK,EAAK,EA4CV,CACP;EACD,CAAA,CAEP;;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Page.cjs","names":[],"sources":["../../../src/components/Page/Page.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Page.cjs","names":[],"sources":["../../../src/components/Page/Page.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — a page shell is slots: eyebrow, title, description,\n * actions, toolbar, footer and children, with padded as the one behavioural switch.\n * Slots are the component.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Page.module.css\";\n\nexport interface PageProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n /** Page title rendered as `<h1>`. */\n title?: ReactNode;\n /** Optional subtitle / breadcrumbs slot rendered above the title. */\n eyebrow?: ReactNode;\n /** Optional description rendered below the title. */\n description?: ReactNode;\n /** Right-side actions slot in the header (buttons, menus). */\n actions?: ReactNode;\n /** Sticky tab bar / filter row rendered just below the header. */\n toolbar?: ReactNode;\n /** Footer slot rendered at the bottom of the content area. */\n footer?: ReactNode;\n /** Page-level padding. Default `true`. */\n padded?: boolean;\n children?: ReactNode;\n}\n\n/**\n * Page wrapper with header + (optional) toolbar + content + footer. Pairs\n * with `Container` when you want a max-width content well.\n *\n * @example\n * <Page title=\"Pedidos\" description=\"Acompanhe seus pedidos\" actions={<Button>Novo</Button>}>\n * <Table {...} />\n * </Page>\n */\nexport function Page({\n title,\n eyebrow,\n description,\n actions,\n toolbar,\n footer,\n padded = true,\n className,\n children,\n ...props\n}: PageProps) {\n const hasHeader = title || eyebrow || description || actions;\n return (\n <main className={cn(styles.page, padded && styles.padded, className)} {...props}>\n {hasHeader && (\n <header className={styles.header}>\n <div className={styles.headerText}>\n {eyebrow && <div className={styles.eyebrow}>{eyebrow}</div>}\n {title && <h1 className={styles.title}>{title}</h1>}\n {description && <p className={styles.description}>{description}</p>}\n </div>\n {actions && <div className={styles.actions}>{actions}</div>}\n </header>\n )}\n {toolbar && <div className={styles.toolbar}>{toolbar}</div>}\n <div className={styles.content}>{children}</div>\n {footer && <footer className={styles.footer}>{footer}</footer>}\n </main>\n );\n}\n"],"mappings":"wGAoCA,SAAgB,EAAK,CACjB,QACA,UACA,cACA,UACA,UACA,SACA,SAAS,GACT,YACA,WACA,GAAG,GACO,CACV,IAAM,EAAY,GAAS,GAAW,GAAe,EACrD,OACI,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,GAAG,EAAA,QAAO,KAAM,GAAU,EAAA,QAAO,OAAQ,CAAS,EAAG,GAAI,WAA1E,CACK,IACG,EAAA,EAAA,KAAA,CAAC,SAAD,CAAQ,UAAW,EAAA,QAAO,gBAA1B,EACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,oBAAvB,CACK,IAAW,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,iBAAU,CAAa,CAAA,EACzD,IAAS,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,eAAQ,CAAU,CAAA,EACjD,IAAe,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,UAAW,EAAA,QAAO,qBAAc,CAAe,CAAA,CACjE,IACJ,IAAW,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,iBAAU,CAAa,CAAA,CACtD,IAEX,IAAW,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,iBAAU,CAAa,CAAA,GAC1D,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAU,UAAc,CAAA,EAC9C,IAAU,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,UAAW,EAAA,QAAO,gBAAS,CAAe,CAAA,CAC3D,GAEd"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Page.js","names":[],"sources":["../../../src/components/Page/Page.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Page.js","names":[],"sources":["../../../src/components/Page/Page.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — a page shell is slots: eyebrow, title, description,\n * actions, toolbar, footer and children, with padded as the one behavioural switch.\n * Slots are the component.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Page.module.css\";\n\nexport interface PageProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n /** Page title rendered as `<h1>`. */\n title?: ReactNode;\n /** Optional subtitle / breadcrumbs slot rendered above the title. */\n eyebrow?: ReactNode;\n /** Optional description rendered below the title. */\n description?: ReactNode;\n /** Right-side actions slot in the header (buttons, menus). */\n actions?: ReactNode;\n /** Sticky tab bar / filter row rendered just below the header. */\n toolbar?: ReactNode;\n /** Footer slot rendered at the bottom of the content area. */\n footer?: ReactNode;\n /** Page-level padding. Default `true`. */\n padded?: boolean;\n children?: ReactNode;\n}\n\n/**\n * Page wrapper with header + (optional) toolbar + content + footer. Pairs\n * with `Container` when you want a max-width content well.\n *\n * @example\n * <Page title=\"Pedidos\" description=\"Acompanhe seus pedidos\" actions={<Button>Novo</Button>}>\n * <Table {...} />\n * </Page>\n */\nexport function Page({\n title,\n eyebrow,\n description,\n actions,\n toolbar,\n footer,\n padded = true,\n className,\n children,\n ...props\n}: PageProps) {\n const hasHeader = title || eyebrow || description || actions;\n return (\n <main className={cn(styles.page, padded && styles.padded, className)} {...props}>\n {hasHeader && (\n <header className={styles.header}>\n <div className={styles.headerText}>\n {eyebrow && <div className={styles.eyebrow}>{eyebrow}</div>}\n {title && <h1 className={styles.title}>{title}</h1>}\n {description && <p className={styles.description}>{description}</p>}\n </div>\n {actions && <div className={styles.actions}>{actions}</div>}\n </header>\n )}\n {toolbar && <div className={styles.toolbar}>{toolbar}</div>}\n <div className={styles.content}>{children}</div>\n {footer && <footer className={styles.footer}>{footer}</footer>}\n </main>\n );\n}\n"],"mappings":";;;;AAoCA,SAAgB,EAAK,EACjB,UACA,YACA,gBACA,YACA,YACA,WACA,YAAS,IACT,cACA,aACA,GAAG,KACO;CACV,IAAM,IAAY,KAAS,KAAW,KAAe;CACrD,OACI,kBAAC,QAAD;EAAM,WAAW,EAAG,EAAO,MAAM,KAAU,EAAO,QAAQ,CAAS;EAAG,GAAI;YAA1E;GACK,KACG,kBAAC,UAAD;IAAQ,WAAW,EAAO;cAA1B,CACI,kBAAC,OAAD;KAAK,WAAW,EAAO;eAAvB;MACK,KAAW,kBAAC,OAAD;OAAK,WAAW,EAAO;iBAAU;MAAa,CAAA;MACzD,KAAS,kBAAC,MAAD;OAAI,WAAW,EAAO;iBAAQ;MAAU,CAAA;MACjD,KAAe,kBAAC,KAAD;OAAG,WAAW,EAAO;iBAAc;MAAe,CAAA;KACjE;QACJ,KAAW,kBAAC,OAAD;KAAK,WAAW,EAAO;eAAU;IAAa,CAAA,CACtD;;GAEX,KAAW,kBAAC,OAAD;IAAK,WAAW,EAAO;cAAU;GAAa,CAAA;GAC1D,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAU;GAAc,CAAA;GAC9C,KAAU,kBAAC,UAAD;IAAQ,WAAW,EAAO;cAAS;GAAe,CAAA;EAC3D;;AAEd"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pagination.cjs","names":[],"sources":["../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Pagination.cjs","names":[],"sources":["../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — page/totalPages/onPageChange is the pager,\n * pageSize/onPageSizeChange/ pageSizeOptions is the size control that sits in the\n * same bar, and totalItems plus siblingCount decide the copy and how many page\n * buttons are rendered. They ship together because a footer that pages but cannot\n * resize is half a footer.\n */\nimport { useMemo } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Pagination.module.css\";\n\nexport interface PaginationProps {\n page: number;\n totalPages: number;\n onPageChange: (page: number) => void;\n pageSize?: number;\n onPageSizeChange?: (size: number) => void;\n pageSizeOptions?: number[];\n /** Total items count; if provided, renders the summary text. */\n totalItems?: number;\n /** Max number of numbered page buttons to show. Default 7. */\n siblingCount?: number;\n className?: string;\n}\n\nfunction buildRange(page: number, totalPages: number, siblings: number): (number | \"...\")[] {\n const totalSlots = siblings + 4;\n if (totalPages <= totalSlots) {\n return Array.from({ length: totalPages }, (_, i) => i + 1);\n }\n const start = Math.max(2, page - Math.floor(siblings / 2));\n const end = Math.min(totalPages - 1, start + siblings - 1);\n const range: (number | \"...\")[] = [1];\n if (start > 2) range.push(\"...\");\n for (let i = start; i <= end; i++) range.push(i);\n if (end < totalPages - 1) range.push(\"...\");\n range.push(totalPages);\n return range;\n}\n\n/**\n * Numeric pagination controls. Pair with {@link usePagination} for state.\n */\nexport function Pagination({\n page,\n totalPages,\n onPageChange,\n pageSize,\n onPageSizeChange,\n pageSizeOptions = [10, 25, 50, 100],\n totalItems,\n siblingCount = 3,\n className,\n}: PaginationProps) {\n const pages = useMemo(\n () => buildRange(page, totalPages, siblingCount),\n [page, totalPages, siblingCount],\n );\n\n if (totalPages <= 1 && !onPageSizeChange) return null;\n\n return (\n <div className={cn(styles.wrapper, className)}>\n <div className={styles.summary}>\n {typeof totalItems === \"number\"\n ? `${totalItems} resultado${totalItems === 1 ? \"\" : \"s\"}`\n : `Página ${page} de ${totalPages}`}\n </div>\n <div className={styles.controls}>\n <button\n type=\"button\"\n className={styles.page}\n onClick={() => onPageChange(Math.max(1, page - 1))}\n disabled={page <= 1}\n aria-label=\"Página anterior\"\n >\n ‹\n </button>\n {pages.map((entry, index) =>\n entry === \"...\" ? (\n <span\n key={`ellipsis-${index}`}\n className={cn(styles.ellipsis, styles.numeric)}\n >\n …\n </span>\n ) : (\n <button\n type=\"button\"\n key={entry}\n className={cn(\n styles.page,\n styles.numeric,\n entry === page && styles.active,\n )}\n onClick={() => onPageChange(entry)}\n aria-current={entry === page ? \"page\" : undefined}\n >\n {entry}\n </button>\n ),\n )}\n <button\n type=\"button\"\n className={styles.page}\n onClick={() => onPageChange(Math.min(totalPages, page + 1))}\n disabled={page >= totalPages}\n aria-label=\"Próxima página\"\n >\n ›\n </button>\n {onPageSizeChange && (\n <select\n className={styles.sizeSelect}\n value={pageSize}\n onChange={(event) => onPageSizeChange(Number(event.target.value))}\n aria-label=\"Itens por página\"\n >\n {pageSizeOptions.map((opt) => (\n <option key={opt} value={opt}>\n {opt} / página\n </option>\n ))}\n </select>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"iIAyBA,SAAS,EAAW,EAAc,EAAoB,EAAsC,CAExF,GAAI,GADe,EAAW,EAE1B,OAAO,MAAM,KAAK,CAAE,OAAQ,CAAW,GAAI,EAAG,IAAM,EAAI,CAAC,EAE7D,IAAM,EAAQ,KAAK,IAAI,EAAG,EAAO,KAAK,MAAM,EAAW,CAAC,CAAC,EACnD,EAAM,KAAK,IAAI,EAAa,EAAG,EAAQ,EAAW,CAAC,EACnD,EAA4B,CAAC,CAAC,EAChC,EAAQ,GAAG,EAAM,KAAK,KAAK,EAC/B,IAAK,IAAI,EAAI,EAAO,GAAK,EAAK,IAAK,EAAM,KAAK,CAAC,EAG/C,OAFI,EAAM,EAAa,GAAG,EAAM,KAAK,KAAK,EAC1C,EAAM,KAAK,CAAU,EACd,CACX,CAKA,SAAgB,EAAW,CACvB,OACA,aACA,eACA,WACA,mBACA,kBAAkB,CAAC,GAAI,GAAI,GAAI,GAAG,EAClC,aACA,eAAe,EACf,aACgB,CAChB,IAAM,GAAA,EAAA,EAAA,QAAA,KACI,EAAW,EAAM,EAAY,CAAY,EAC/C,CAAC,EAAM,EAAY,CAAY,CACnC,EAIA,OAFI,GAAc,GAAK,CAAC,EAAyB,MAG7C,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,CAAS,WAA5C,EACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,iBAClB,OAAO,GAAe,SACjB,GAAG,EAAW,YAAY,IAAe,EAAI,GAAK,MAClD,UAAU,EAAK,MAAM,GAC1B,CAAA,GACL,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,kBAAvB,EACI,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,KAClB,YAAe,EAAa,KAAK,IAAI,EAAG,EAAO,CAAC,CAAC,EACjD,SAAU,GAAQ,EAClB,aAAW,2BACd,GAEO,CAAA,EACP,EAAM,KAAK,EAAO,IACf,IAAU,OACN,EAAA,EAAA,IAAA,CAAC,OAAD,CAEI,UAAW,EAAA,GAAG,EAAA,QAAO,SAAU,EAAA,QAAO,OAAO,WAChD,GAEK,EAJG,YAAY,GAIf,GAEN,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SAEL,UAAW,EAAA,GACP,EAAA,QAAO,KACP,EAAA,QAAO,QACP,IAAU,GAAQ,EAAA,QAAO,MAC7B,EACA,YAAe,EAAa,CAAK,EACjC,eAAc,IAAU,EAAO,OAAS,IAAA,YAEvC,CACG,EAVC,CAUD,CAEhB,GACA,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,KAClB,YAAe,EAAa,KAAK,IAAI,EAAY,EAAO,CAAC,CAAC,EAC1D,SAAU,GAAQ,EAClB,aAAW,0BACd,GAEO,CAAA,EACP,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,UAAW,EAAA,QAAO,WAClB,MAAO,EACP,SAAW,GAAU,EAAiB,OAAO,EAAM,OAAO,KAAK,CAAC,EAChE,aAAW,4BAEV,EAAgB,IAAK,IAClB,EAAA,EAAA,KAAA,CAAC,SAAD,CAAkB,MAAO,WAAzB,CACK,EAAI,WACD,GAFK,CAEL,CACX,CACG,CAAA,CAEX,GACJ,GAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pagination.js","names":[],"sources":["../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Pagination.js","names":[],"sources":["../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — page/totalPages/onPageChange is the pager,\n * pageSize/onPageSizeChange/ pageSizeOptions is the size control that sits in the\n * same bar, and totalItems plus siblingCount decide the copy and how many page\n * buttons are rendered. They ship together because a footer that pages but cannot\n * resize is half a footer.\n */\nimport { useMemo } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Pagination.module.css\";\n\nexport interface PaginationProps {\n page: number;\n totalPages: number;\n onPageChange: (page: number) => void;\n pageSize?: number;\n onPageSizeChange?: (size: number) => void;\n pageSizeOptions?: number[];\n /** Total items count; if provided, renders the summary text. */\n totalItems?: number;\n /** Max number of numbered page buttons to show. Default 7. */\n siblingCount?: number;\n className?: string;\n}\n\nfunction buildRange(page: number, totalPages: number, siblings: number): (number | \"...\")[] {\n const totalSlots = siblings + 4;\n if (totalPages <= totalSlots) {\n return Array.from({ length: totalPages }, (_, i) => i + 1);\n }\n const start = Math.max(2, page - Math.floor(siblings / 2));\n const end = Math.min(totalPages - 1, start + siblings - 1);\n const range: (number | \"...\")[] = [1];\n if (start > 2) range.push(\"...\");\n for (let i = start; i <= end; i++) range.push(i);\n if (end < totalPages - 1) range.push(\"...\");\n range.push(totalPages);\n return range;\n}\n\n/**\n * Numeric pagination controls. Pair with {@link usePagination} for state.\n */\nexport function Pagination({\n page,\n totalPages,\n onPageChange,\n pageSize,\n onPageSizeChange,\n pageSizeOptions = [10, 25, 50, 100],\n totalItems,\n siblingCount = 3,\n className,\n}: PaginationProps) {\n const pages = useMemo(\n () => buildRange(page, totalPages, siblingCount),\n [page, totalPages, siblingCount],\n );\n\n if (totalPages <= 1 && !onPageSizeChange) return null;\n\n return (\n <div className={cn(styles.wrapper, className)}>\n <div className={styles.summary}>\n {typeof totalItems === \"number\"\n ? `${totalItems} resultado${totalItems === 1 ? \"\" : \"s\"}`\n : `Página ${page} de ${totalPages}`}\n </div>\n <div className={styles.controls}>\n <button\n type=\"button\"\n className={styles.page}\n onClick={() => onPageChange(Math.max(1, page - 1))}\n disabled={page <= 1}\n aria-label=\"Página anterior\"\n >\n ‹\n </button>\n {pages.map((entry, index) =>\n entry === \"...\" ? (\n <span\n key={`ellipsis-${index}`}\n className={cn(styles.ellipsis, styles.numeric)}\n >\n …\n </span>\n ) : (\n <button\n type=\"button\"\n key={entry}\n className={cn(\n styles.page,\n styles.numeric,\n entry === page && styles.active,\n )}\n onClick={() => onPageChange(entry)}\n aria-current={entry === page ? \"page\" : undefined}\n >\n {entry}\n </button>\n ),\n )}\n <button\n type=\"button\"\n className={styles.page}\n onClick={() => onPageChange(Math.min(totalPages, page + 1))}\n disabled={page >= totalPages}\n aria-label=\"Próxima página\"\n >\n ›\n </button>\n {onPageSizeChange && (\n <select\n className={styles.sizeSelect}\n value={pageSize}\n onChange={(event) => onPageSizeChange(Number(event.target.value))}\n aria-label=\"Itens por página\"\n >\n {pageSizeOptions.map((opt) => (\n <option key={opt} value={opt}>\n {opt} / página\n </option>\n ))}\n </select>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;AAyBA,SAAS,EAAW,GAAc,GAAoB,GAAsC;CAExF,IAAI,KADe,IAAW,GAE1B,OAAO,MAAM,KAAK,EAAE,QAAQ,EAAW,IAAI,GAAG,MAAM,IAAI,CAAC;CAE7D,IAAM,IAAQ,KAAK,IAAI,GAAG,IAAO,KAAK,MAAM,IAAW,CAAC,CAAC,GACnD,IAAM,KAAK,IAAI,IAAa,GAAG,IAAQ,IAAW,CAAC,GACnD,IAA4B,CAAC,CAAC;CACpC,AAAI,IAAQ,KAAG,EAAM,KAAK,KAAK;CAC/B,KAAK,IAAI,IAAI,GAAO,KAAK,GAAK,KAAK,EAAM,KAAK,CAAC;CAG/C,OAFI,IAAM,IAAa,KAAG,EAAM,KAAK,KAAK,GAC1C,EAAM,KAAK,CAAU,GACd;AACX;AAKA,SAAgB,EAAW,EACvB,SACA,eACA,iBACA,aACA,qBACA,qBAAkB;CAAC;CAAI;CAAI;CAAI;AAAG,GAClC,eACA,kBAAe,GACf,gBACgB;CAChB,IAAM,IAAQ,QACJ,EAAW,GAAM,GAAY,CAAY,GAC/C;EAAC;EAAM;EAAY;CAAY,CACnC;CAIA,OAFI,KAAc,KAAK,CAAC,IAAyB,OAG7C,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,SAAS,CAAS;YAA5C,CACI,kBAAC,OAAD;GAAK,WAAW,EAAO;aAClB,OAAO,KAAe,WACjB,GAAG,EAAW,YAAY,MAAe,IAAI,KAAK,QAClD,UAAU,EAAK,MAAM;EAC1B,CAAA,GACL,kBAAC,OAAD;GAAK,WAAW,EAAO;aAAvB;IACI,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,eAAe,EAAa,KAAK,IAAI,GAAG,IAAO,CAAC,CAAC;KACjD,UAAU,KAAQ;KAClB,cAAW;eACd;IAEO,CAAA;IACP,EAAM,KAAK,GAAO,MACf,MAAU,QACN,kBAAC,QAAD;KAEI,WAAW,EAAG,EAAO,UAAU,EAAO,OAAO;eAChD;IAEK,GAJG,YAAY,GAIf,IAEN,kBAAC,UAAD;KACI,MAAK;KAEL,WAAW,EACP,EAAO,MACP,EAAO,SACP,MAAU,KAAQ,EAAO,MAC7B;KACA,eAAe,EAAa,CAAK;KACjC,gBAAc,MAAU,IAAO,SAAS,KAAA;eAEvC;IACG,GAVC,CAUD,CAEhB;IACA,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,eAAe,EAAa,KAAK,IAAI,GAAY,IAAO,CAAC,CAAC;KAC1D,UAAU,KAAQ;KAClB,cAAW;eACd;IAEO,CAAA;IACP,KACG,kBAAC,UAAD;KACI,WAAW,EAAO;KAClB,OAAO;KACP,WAAW,MAAU,EAAiB,OAAO,EAAM,OAAO,KAAK,CAAC;KAChE,cAAW;eAEV,EAAgB,KAAK,MAClB,kBAAC,UAAD;MAAkB,OAAO;gBAAzB,CACK,GAAI,WACD;QAFK,CAEL,CACX;IACG,CAAA;GAEX;IACJ;;AAEb"}
|