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":"AudioRecorder.js","names":[],"sources":["../../../src/components/AudioRecorder/AudioRecorder.tsx"],"sourcesContent":["import { Mic, Pause, Play, RotateCcw, Square } from \"lucide-react\";\nimport { useEffect, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { isAudioRecordingSupported } from \"@/audio/audio-recorder\";\nimport { useAudioRecorder } from \"@/audio/use-audio-recorder\";\nimport { useMicrophone } from \"@/audio/use-microphone\";\nimport { blobToWav, type WavOptions } from \"@/audio/wav\";\nimport type { AudioRecording } from \"@/audio/audio-recorder\";\nimport { useMediaPermission } from \"@/hooks/use-media-permission\";\nimport { cn } from \"@/utils/cn\";\n\nimport { AudioPlayer } from \"../AudioPlayer\";\nimport styles from \"./AudioRecorder.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface AudioRecorderProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /**\n * Called once a recording is finished and (when asked) converted.\n *\n * Fires on stop and on hitting `maxDurationMs`, never on cancel.\n */\n onRecorded?: (recording: AudioRecording) => void;\n /** Stop automatically after this long. Strongly recommended on anything public. */\n maxDurationMs?: number;\n /** Specific microphone, from `useMediaDevices().audioInputs`. */\n deviceId?: string;\n /**\n * Output container.\n *\n * `\"native\"` (default) keeps what the browser produced — Opus on Chromium and\n * Firefox, AAC on Safari — which is the smallest and needs no work. `\"wav\"`\n * converts on stop, for a backend that only accepts WAV; see `wavOptions`, and\n * expect roughly ten times the bytes.\n */\n format?: \"native\" | \"wav\";\n /** Passed to `blobToWav` when `format=\"wav\"`. `{ mono: true, sampleRate: 16000 }` suits speech. */\n wavOptions?: WavOptions;\n /** Target bitrate. 32000–64000 is plenty for speech in Opus. */\n audioBitsPerSecond?: number;\n /** Offer playback of the recording before handing it over. Default `true`. */\n review?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered under the controls — a hint, a character count, a legal notice. */\n footer?: ReactNode;\n /** Recorder-level failure, or a failed WAV conversion. */\n onError?: (error: unknown) => void;\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n record: \"Gravar\",\n stop: \"Parar\",\n pause: \"Pausar\",\n resume: \"Continuar\",\n retake: \"Gravar de novo\",\n level: \"Nível do microfone\",\n asking: \"Pedindo acesso ao microfone…\",\n denied: \"Acesso ao microfone bloqueado. Libere nas configurações do site e recarregue.\",\n unsupported: \"Este navegador não grava áudio.\",\n idleHint: \"Toque em Gravar para começar.\",\n recording: \"Gravando\",\n paused: \"Pausado\",\n converting: \"Convertendo…\",\n },\n en: {\n record: \"Record\",\n stop: \"Stop\",\n pause: \"Pause\",\n resume: \"Resume\",\n retake: \"Record again\",\n level: \"Microphone level\",\n asking: \"Requesting microphone access…\",\n denied: \"Microphone access blocked. Allow it in site settings and reload.\",\n unsupported: \"This browser cannot record audio.\",\n idleHint: \"Press Record to start.\",\n recording: \"Recording\",\n paused: \"Paused\",\n converting: \"Converting…\",\n },\n} as const;\n\n/**\n * Record a voice note: one button to start, a live level meter, a clock, and a\n * playback review before the audio leaves the component.\n *\n * The permission flow is the part worth reading. The microphone is **not** opened on\n * mount — the prompt fires on the first press of Record, because a prompt the user did\n * not provoke is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever asking again. When the permission is already\n * `\"denied\"`, the component says so and how to fix it instead of offering a button\n * that cannot work.\n *\n * The level meter is not decoration either: a muted OS input or a headset with its mic\n * arm folded up produces a perfectly successful recording of silence, and without a\n * visible level the user only finds out after they finish talking.\n *\n * @example\n * <AudioRecorder\n * maxDurationMs={120_000}\n * format=\"wav\"\n * wavOptions={{ mono: true, sampleRate: 16000 }}\n * onRecorded={({ blob }) => upload(blob)}\n * />\n */\nexport function AudioRecorder({\n onRecorded,\n maxDurationMs,\n deviceId,\n format = \"native\",\n wavOptions,\n audioBitsPerSecond,\n review = true,\n locale = \"pt-BR\",\n footer,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioRecorderProps) {\n const strings = STRINGS[locale];\n const permission = useMediaPermission(\"microphone\");\n const mic = useMicrophone({ deviceId });\n const [converting, setConverting] = useState(false);\n const [result, setResult] = useState<AudioRecording | null>(null);\n const [supported, setSupported] = useState(true);\n\n useEffect(() => setSupported(isAudioRecordingSupported()), []);\n\n /**\n * Convert if asked, then publish.\n *\n * The conversion is awaited here rather than left to the caller so that\n * `onRecorded` only ever fires with the format the caller asked for — a callback\n * that sometimes hands over WebM and sometimes WAV is a bug waiting in the upload\n * path.\n */\n const finish = async (recording: AudioRecording): Promise<void> => {\n if (format !== \"wav\") {\n setResult(recording);\n onRecorded?.(recording);\n return;\n }\n setConverting(true);\n try {\n const blob = await blobToWav(recording.blob, wavOptions);\n const converted: AudioRecording = { ...recording, blob, mimeType: \"audio/wav\" };\n setResult(converted);\n onRecorded?.(converted);\n } catch (error) {\n onError?.(error);\n setResult(recording);\n onRecorded?.(recording);\n } finally {\n setConverting(false);\n }\n };\n\n const recorder = useAudioRecorder(mic.stream, {\n maxDurationMs,\n audioBitsPerSecond,\n onRecorded: (recording) => void finish(recording),\n onError,\n });\n\n /**\n * Start recording as soon as the stream lands.\n *\n * The user pressed Record before the permission prompt existed, so the press has\n * to survive the round-trip: `mic.start()` opens the device, the stream arrives a\n * few hundred milliseconds later, and recording begins then. Waiting for a second\n * press would make the first one look broken.\n */\n const [armed, setArmed] = useState(false);\n useEffect(() => {\n if (!armed || !recorder.ready || recorder.status !== \"idle\") return;\n setArmed(false);\n recorder.start();\n }, [armed, recorder.ready, recorder.status, recorder]);\n\n const begin = (): void => {\n setResult(null);\n if (mic.stream && recorder.ready) {\n recorder.start();\n return;\n }\n setArmed(true);\n mic.start();\n };\n\n const retake = (): void => {\n setResult(null);\n recorder.cancel();\n begin();\n };\n\n const busy = recorder.status === \"recording\" || recorder.status === \"paused\";\n const blocked = permission.state === \"denied\";\n\n if (!supported) {\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n </div>\n );\n }\n\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <div className={styles.row}>\n {!busy && (\n <button\n type=\"button\"\n className={styles.record}\n onClick={result ? retake : begin}\n disabled={disabled || blocked || converting}\n aria-label={result ? strings.retake : strings.record}\n >\n {result ? (\n <RotateCcw size={16} aria-hidden />\n ) : (\n <Mic size={16} aria-hidden />\n )}\n <span>{result ? strings.retake : strings.record}</span>\n </button>\n )}\n\n {busy && (\n <>\n <button\n type=\"button\"\n className={styles.stop}\n onClick={() => void recorder.stop()}\n aria-label={strings.stop}\n >\n <Square size={14} aria-hidden />\n <span>{strings.stop}</span>\n </button>\n <button\n type=\"button\"\n className={styles.secondary}\n onClick={\n recorder.status === \"paused\" ? recorder.resume : recorder.pause\n }\n aria-label={\n recorder.status === \"paused\" ? strings.resume : strings.pause\n }\n >\n {recorder.status === \"paused\" ? (\n <Play size={14} aria-hidden />\n ) : (\n <Pause size={14} aria-hidden />\n )}\n </button>\n </>\n )}\n\n {busy && (\n <div\n className={styles.meter}\n role=\"meter\"\n aria-label={strings.level}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(recorder.level * 100)}\n >\n <span\n className={styles.meterFill}\n style={{ transform: `scaleX(${recorder.level})` }}\n />\n </div>\n )}\n\n <span className={styles.clock}>\n {formatDuration(busy ? recorder.durationMs : (result?.durationMs ?? 0))}\n {maxDurationMs !== undefined && (\n <span className={styles.max}> / {formatDuration(maxDurationMs)}</span>\n )}\n </span>\n </div>\n\n <p className={styles.status} role=\"status\">\n {converting\n ? strings.converting\n : mic.status === \"requesting\"\n ? strings.asking\n : blocked\n ? strings.denied\n : mic.error\n ? mic.error.message\n : recorder.status === \"recording\"\n ? strings.recording\n : recorder.status === \"paused\"\n ? strings.paused\n : result\n ? \"\"\n : strings.idleHint}\n </p>\n\n {review && result && (\n <AudioPlayer\n src={result.blob}\n durationMs={result.durationMs}\n locale={locale}\n onError={onError}\n />\n )}\n\n {footer && <div className={styles.footer}>{footer}</div>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AudioRecorder.js","names":[],"sources":["../../../src/components/AudioRecorder/AudioRecorder.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — recording has three\n * separate configurations that a caller genuinely picks between: the capture\n * (deviceId, audioBitsPerSecond, maxDurationMs), the output (format, wavOptions) and\n * the review step (review, footer, locale). The body holds permission, recording,\n * review and error as one state machine; splitting it would thread the same recorder\n * handle through props.\n */\nimport { Mic, Pause, Play, RotateCcw, Square } from \"lucide-react\";\nimport { useEffect, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { isAudioRecordingSupported } from \"@/audio/audio-recorder\";\nimport { useAudioRecorder } from \"@/audio/use-audio-recorder\";\nimport { useMicrophone } from \"@/audio/use-microphone\";\nimport { blobToWav, type WavOptions } from \"@/audio/wav\";\nimport type { AudioRecording } from \"@/audio/audio-recorder\";\nimport { useMediaPermission } from \"@/hooks/use-media-permission\";\nimport { cn } from \"@/utils/cn\";\n\nimport { AudioPlayer } from \"../AudioPlayer\";\nimport styles from \"./AudioRecorder.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface AudioRecorderProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /**\n * Called once a recording is finished and (when asked) converted.\n *\n * Fires on stop and on hitting `maxDurationMs`, never on cancel.\n */\n onRecorded?: (recording: AudioRecording) => void;\n /** Stop automatically after this long. Strongly recommended on anything public. */\n maxDurationMs?: number;\n /** Specific microphone, from `useMediaDevices().audioInputs`. */\n deviceId?: string;\n /**\n * Output container.\n *\n * `\"native\"` (default) keeps what the browser produced — Opus on Chromium and\n * Firefox, AAC on Safari — which is the smallest and needs no work. `\"wav\"`\n * converts on stop, for a backend that only accepts WAV; see `wavOptions`, and\n * expect roughly ten times the bytes.\n */\n format?: \"native\" | \"wav\";\n /** Passed to `blobToWav` when `format=\"wav\"`. `{ mono: true, sampleRate: 16000 }` suits speech. */\n wavOptions?: WavOptions;\n /** Target bitrate. 32000–64000 is plenty for speech in Opus. */\n audioBitsPerSecond?: number;\n /** Offer playback of the recording before handing it over. Default `true`. */\n review?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered under the controls — a hint, a character count, a legal notice. */\n footer?: ReactNode;\n /** Recorder-level failure, or a failed WAV conversion. */\n onError?: (error: unknown) => void;\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n record: \"Gravar\",\n stop: \"Parar\",\n pause: \"Pausar\",\n resume: \"Continuar\",\n retake: \"Gravar de novo\",\n level: \"Nível do microfone\",\n asking: \"Pedindo acesso ao microfone…\",\n denied: \"Acesso ao microfone bloqueado. Libere nas configurações do site e recarregue.\",\n unsupported: \"Este navegador não grava áudio.\",\n idleHint: \"Toque em Gravar para começar.\",\n recording: \"Gravando\",\n paused: \"Pausado\",\n converting: \"Convertendo…\",\n },\n en: {\n record: \"Record\",\n stop: \"Stop\",\n pause: \"Pause\",\n resume: \"Resume\",\n retake: \"Record again\",\n level: \"Microphone level\",\n asking: \"Requesting microphone access…\",\n denied: \"Microphone access blocked. Allow it in site settings and reload.\",\n unsupported: \"This browser cannot record audio.\",\n idleHint: \"Press Record to start.\",\n recording: \"Recording\",\n paused: \"Paused\",\n converting: \"Converting…\",\n },\n} as const;\n\n/**\n * Record a voice note: one button to start, a live level meter, a clock, and a\n * playback review before the audio leaves the component.\n *\n * The permission flow is the part worth reading. The microphone is **not** opened on\n * mount — the prompt fires on the first press of Record, because a prompt the user did\n * not provoke is the most reliable way to earn a permanent block, after which\n * `getUserMedia` rejects without ever asking again. When the permission is already\n * `\"denied\"`, the component says so and how to fix it instead of offering a button\n * that cannot work.\n *\n * The level meter is not decoration either: a muted OS input or a headset with its mic\n * arm folded up produces a perfectly successful recording of silence, and without a\n * visible level the user only finds out after they finish talking.\n *\n * @example\n * <AudioRecorder\n * maxDurationMs={120_000}\n * format=\"wav\"\n * wavOptions={{ mono: true, sampleRate: 16000 }}\n * onRecorded={({ blob }) => upload(blob)}\n * />\n */\nexport function AudioRecorder({\n onRecorded,\n maxDurationMs,\n deviceId,\n format = \"native\",\n wavOptions,\n audioBitsPerSecond,\n review = true,\n locale = \"pt-BR\",\n footer,\n onError,\n disabled = false,\n className,\n ...rest\n}: AudioRecorderProps) {\n const strings = STRINGS[locale];\n const permission = useMediaPermission(\"microphone\");\n const mic = useMicrophone({ deviceId });\n const [converting, setConverting] = useState(false);\n const [result, setResult] = useState<AudioRecording | null>(null);\n const [supported, setSupported] = useState(true);\n\n useEffect(() => setSupported(isAudioRecordingSupported()), []);\n\n /**\n * Convert if asked, then publish.\n *\n * The conversion is awaited here rather than left to the caller so that\n * `onRecorded` only ever fires with the format the caller asked for — a callback\n * that sometimes hands over WebM and sometimes WAV is a bug waiting in the upload\n * path.\n */\n const finish = async (recording: AudioRecording): Promise<void> => {\n if (format !== \"wav\") {\n setResult(recording);\n onRecorded?.(recording);\n return;\n }\n setConverting(true);\n try {\n const blob = await blobToWav(recording.blob, wavOptions);\n const converted: AudioRecording = { ...recording, blob, mimeType: \"audio/wav\" };\n setResult(converted);\n onRecorded?.(converted);\n } catch (error) {\n onError?.(error);\n setResult(recording);\n onRecorded?.(recording);\n } finally {\n setConverting(false);\n }\n };\n\n const recorder = useAudioRecorder(mic.stream, {\n maxDurationMs,\n audioBitsPerSecond,\n onRecorded: (recording) => void finish(recording),\n onError,\n });\n\n /**\n * Start recording as soon as the stream lands.\n *\n * The user pressed Record before the permission prompt existed, so the press has\n * to survive the round-trip: `mic.start()` opens the device, the stream arrives a\n * few hundred milliseconds later, and recording begins then. Waiting for a second\n * press would make the first one look broken.\n */\n const [armed, setArmed] = useState(false);\n useEffect(() => {\n if (!armed || !recorder.ready || recorder.status !== \"idle\") return;\n setArmed(false);\n recorder.start();\n }, [armed, recorder.ready, recorder.status, recorder]);\n\n const begin = (): void => {\n setResult(null);\n if (mic.stream && recorder.ready) {\n recorder.start();\n return;\n }\n setArmed(true);\n mic.start();\n };\n\n const retake = (): void => {\n setResult(null);\n recorder.cancel();\n begin();\n };\n\n const busy = recorder.status === \"recording\" || recorder.status === \"paused\";\n const blocked = permission.state === \"denied\";\n\n if (!supported) {\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n </div>\n );\n }\n\n return (\n <div className={cn(styles.recorder, className)} {...rest}>\n <div className={styles.row}>\n {!busy && (\n <button\n type=\"button\"\n className={styles.record}\n onClick={result ? retake : begin}\n disabled={disabled || blocked || converting}\n aria-label={result ? strings.retake : strings.record}\n >\n {result ? (\n <RotateCcw size={16} aria-hidden />\n ) : (\n <Mic size={16} aria-hidden />\n )}\n <span>{result ? strings.retake : strings.record}</span>\n </button>\n )}\n\n {busy && (\n <>\n <button\n type=\"button\"\n className={styles.stop}\n onClick={() => void recorder.stop()}\n aria-label={strings.stop}\n >\n <Square size={14} aria-hidden />\n <span>{strings.stop}</span>\n </button>\n <button\n type=\"button\"\n className={styles.secondary}\n onClick={\n recorder.status === \"paused\" ? recorder.resume : recorder.pause\n }\n aria-label={\n recorder.status === \"paused\" ? strings.resume : strings.pause\n }\n >\n {recorder.status === \"paused\" ? (\n <Play size={14} aria-hidden />\n ) : (\n <Pause size={14} aria-hidden />\n )}\n </button>\n </>\n )}\n\n {busy && (\n <div\n className={styles.meter}\n role=\"meter\"\n aria-label={strings.level}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(recorder.level * 100)}\n >\n <span\n className={styles.meterFill}\n style={{ transform: `scaleX(${recorder.level})` }}\n />\n </div>\n )}\n\n <span className={styles.clock}>\n {formatDuration(busy ? recorder.durationMs : (result?.durationMs ?? 0))}\n {maxDurationMs !== undefined && (\n <span className={styles.max}> / {formatDuration(maxDurationMs)}</span>\n )}\n </span>\n </div>\n\n <p className={styles.status} role=\"status\">\n {converting\n ? strings.converting\n : mic.status === \"requesting\"\n ? strings.asking\n : blocked\n ? strings.denied\n : mic.error\n ? mic.error.message\n : recorder.status === \"recording\"\n ? strings.recording\n : recorder.status === \"paused\"\n ? strings.paused\n : result\n ? \"\"\n : strings.idleHint}\n </p>\n\n {review && result && (\n <AudioPlayer\n src={result.blob}\n durationMs={result.durationMs}\n locale={locale}\n onError={onError}\n />\n )}\n\n {footer && <div className={styles.footer}>{footer}</div>}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BarcodeScanner.cjs","names":[],"sources":["../../../src/components/BarcodeScanner/BarcodeScanner.tsx"],"sourcesContent":["import { Flashlight, FlashlightOff, ScanLine } from \"lucide-react\";\nimport { type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { useBarcodeScanner } from \"@/capture/use-barcode-scanner\";\nimport type { BarcodeDetectorLike, BarcodeFormat, BarcodeScanResult } from \"@/capture/barcode\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./BarcodeScanner.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface BarcodeScannerProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /** Called for every accepted read — repeats of the same value are suppressed. */\n onScan: (result: BarcodeScanResult) => void;\n /** Symbologies to look for. Defaults to QR + EAN-13 + Code 128. */\n formats?: readonly BarcodeFormat[];\n /** Stop looking without releasing the camera — set it while a confirmation is open. */\n paused?: boolean;\n /** A decoder to use instead of the native one, for Safari and Firefox. */\n detector?: BarcodeDetectorLike;\n /** How often a frame is examined, in ms. Default 200. */\n intervalMs?: number;\n /** Ignore the same value again for this long, in ms. Default 2500. */\n repeatDelayMs?: number;\n /** Offer the torch toggle when the camera has a lamp. Default `true`. */\n torch?: boolean;\n /** Viewport aspect ratio, `width / height`. Default `4 / 3`. */\n aspectRatio?: number;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Under the viewport — an instruction, the code just read, a manual-entry link. */\n footer?: ReactNode;\n /**\n * Rendered instead of the camera when there is no decoder.\n *\n * Worth filling in: on iOS and Firefox this is the **only** thing the user sees, so\n * the fallback is usually a plain text field for typing the code.\n */\n unsupported?: ReactNode;\n /** A frame the engine refused to decode. Routine and usually transient. */\n onError?: (error: unknown) => void;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n viewport: \"Visor da câmera\",\n scanning: \"Procurando código…\",\n paused: \"Leitura pausada\",\n opening: \"Abrindo a câmera…\",\n hint: \"Aponte a câmera para o código.\",\n found: (value: string) => `Código lido: ${value}`,\n torchOn: \"Desligar lanterna\",\n torchOff: \"Ligar lanterna\",\n unsupported: \"Este navegador não decodifica códigos de barras.\",\n retry: \"Tentar de novo\",\n },\n en: {\n viewport: \"Camera viewport\",\n scanning: \"Looking for a code…\",\n paused: \"Scanning paused\",\n opening: \"Opening the camera…\",\n hint: \"Point the camera at the code.\",\n found: (value: string) => `Code read: ${value}`,\n torchOn: \"Turn torch off\",\n torchOff: \"Turn torch on\",\n unsupported: \"This browser cannot decode barcodes.\",\n retry: \"Try again\",\n },\n} as const;\n\n/**\n * Point the camera at a barcode and get its value.\n *\n * The counterpart to {@link QRCode}, which only encodes. Under it are\n * `useBarcodeScanner` (the detect loop and repeat suppression), `useCameraStream` (the\n * stream and its classified errors) and `useTorch`.\n *\n * **Mounting this opens the camera**, so mount it when the user asks to scan rather\n * than on a page that happens to contain a scanner: a permission prompt nobody\n * provoked is the surest way to earn a permanent block, and after that `getUserMedia`\n * rejects without ever prompting again. The usual shape is a button that reveals it.\n *\n * The `unsupported` slot is not a nicety. `BarcodeDetector` is Chromium-only — absent\n * on Firefox, on every browser on iOS, and on Chromium for Windows and Linux — so on a\n * large share of real devices the fallback *is* the feature. Give it a text field, or\n * inject a polyfill through `detector`.\n *\n * The preview itself is `aria-hidden`: a live camera frame has nothing to announce and\n * no audio to caption, so what a screen reader gets is the `role=\"status\"` line, which\n * says whether scanning is running and reads out each accepted code.\n *\n * @example\n * <BarcodeScanner\n * formats={[\"ean_13\"]}\n * onScan={({ rawValue }) => addToCart(rawValue)}\n * footer={<small>Aponte para o código de barras da embalagem.</small>}\n * unsupported={<ManualCodeInput onSubmit={addToCart} />}\n * />\n */\nexport function BarcodeScanner({\n onScan,\n formats,\n paused = false,\n detector,\n intervalMs,\n repeatDelayMs,\n torch = true,\n aspectRatio = 4 / 3,\n locale = \"pt-BR\",\n footer,\n unsupported,\n onError,\n className,\n ...rest\n}: BarcodeScannerProps) {\n const strings = STRINGS[locale];\n const {\n videoRef,\n status,\n error,\n supported,\n scanning,\n result,\n torch: lamp,\n retry,\n } = useBarcodeScanner({\n formats,\n paused,\n detector,\n intervalMs,\n repeatDelayMs,\n onScan,\n onError,\n });\n\n if (!supported) {\n return (\n <div className={cn(styles.scanner, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n {unsupported}\n </div>\n );\n }\n\n const message = result\n ? strings.found(result.rawValue)\n : paused\n ? strings.paused\n : scanning\n ? strings.scanning\n : strings.opening;\n\n return (\n <div className={cn(styles.scanner, className)} {...rest}>\n <div\n className={styles.viewport}\n style={{ aspectRatio: String(aspectRatio) }}\n aria-label={strings.viewport}\n role=\"group\"\n >\n <video\n ref={videoRef}\n className={styles.video}\n muted\n playsInline\n aria-hidden=\"true\"\n />\n\n {status === \"ready\" && (\n <div className={styles.frame} aria-hidden=\"true\">\n <span className={cn(styles.laser, scanning && styles.laserActive)} />\n </div>\n )}\n\n {torch && lamp.supported && (\n <button\n type=\"button\"\n className={styles.torch}\n onClick={() => void lamp.toggle()}\n aria-pressed={lamp.on}\n aria-label={lamp.on ? strings.torchOn : strings.torchOff}\n >\n {lamp.on ? (\n <FlashlightOff size={18} aria-hidden />\n ) : (\n <Flashlight size={18} aria-hidden />\n )}\n </button>\n )}\n\n {error && (\n <div className={styles.overlay} role=\"alert\">\n <p className={styles.overlayText}>{error.message}</p>\n <button type=\"button\" className={styles.retry} onClick={retry}>\n {strings.retry}\n </button>\n </div>\n )}\n </div>\n\n {!error && (\n <p className={styles.status} role=\"status\">\n <ScanLine size={14} aria-hidden className={styles.statusIcon} />\n {message}\n </p>\n )}\n\n {footer ? <div className={styles.footer}>{footer}</div> : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BarcodeScanner.cjs","names":[],"sources":["../../../src/components/BarcodeScanner/BarcodeScanner.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the scan loop is tuned\n * by the caller because the right values depend on the symbology and the device:\n * formats, intervalMs, repeatDelayMs, detector, paused, torch, aspectRatio. The rest\n * are the surfaces to fill when the API is missing (unsupported, footer, locale) and\n * the two outputs (onScan, onError).\n */\nimport { Flashlight, FlashlightOff, ScanLine } from \"lucide-react\";\nimport { type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { useBarcodeScanner } from \"@/capture/use-barcode-scanner\";\nimport type { BarcodeDetectorLike, BarcodeFormat, BarcodeScanResult } from \"@/capture/barcode\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./BarcodeScanner.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface BarcodeScannerProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /** Called for every accepted read — repeats of the same value are suppressed. */\n onScan: (result: BarcodeScanResult) => void;\n /** Symbologies to look for. Defaults to QR + EAN-13 + Code 128. */\n formats?: readonly BarcodeFormat[];\n /** Stop looking without releasing the camera — set it while a confirmation is open. */\n paused?: boolean;\n /** A decoder to use instead of the native one, for Safari and Firefox. */\n detector?: BarcodeDetectorLike;\n /** How often a frame is examined, in ms. Default 200. */\n intervalMs?: number;\n /** Ignore the same value again for this long, in ms. Default 2500. */\n repeatDelayMs?: number;\n /** Offer the torch toggle when the camera has a lamp. Default `true`. */\n torch?: boolean;\n /** Viewport aspect ratio, `width / height`. Default `4 / 3`. */\n aspectRatio?: number;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Under the viewport — an instruction, the code just read, a manual-entry link. */\n footer?: ReactNode;\n /**\n * Rendered instead of the camera when there is no decoder.\n *\n * Worth filling in: on iOS and Firefox this is the **only** thing the user sees, so\n * the fallback is usually a plain text field for typing the code.\n */\n unsupported?: ReactNode;\n /** A frame the engine refused to decode. Routine and usually transient. */\n onError?: (error: unknown) => void;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n viewport: \"Visor da câmera\",\n scanning: \"Procurando código…\",\n paused: \"Leitura pausada\",\n opening: \"Abrindo a câmera…\",\n hint: \"Aponte a câmera para o código.\",\n found: (value: string) => `Código lido: ${value}`,\n torchOn: \"Desligar lanterna\",\n torchOff: \"Ligar lanterna\",\n unsupported: \"Este navegador não decodifica códigos de barras.\",\n retry: \"Tentar de novo\",\n },\n en: {\n viewport: \"Camera viewport\",\n scanning: \"Looking for a code…\",\n paused: \"Scanning paused\",\n opening: \"Opening the camera…\",\n hint: \"Point the camera at the code.\",\n found: (value: string) => `Code read: ${value}`,\n torchOn: \"Turn torch off\",\n torchOff: \"Turn torch on\",\n unsupported: \"This browser cannot decode barcodes.\",\n retry: \"Try again\",\n },\n} as const;\n\n/**\n * Point the camera at a barcode and get its value.\n *\n * The counterpart to {@link QRCode}, which only encodes. Under it are\n * `useBarcodeScanner` (the detect loop and repeat suppression), `useCameraStream` (the\n * stream and its classified errors) and `useTorch`.\n *\n * **Mounting this opens the camera**, so mount it when the user asks to scan rather\n * than on a page that happens to contain a scanner: a permission prompt nobody\n * provoked is the surest way to earn a permanent block, and after that `getUserMedia`\n * rejects without ever prompting again. The usual shape is a button that reveals it.\n *\n * The `unsupported` slot is not a nicety. `BarcodeDetector` is Chromium-only — absent\n * on Firefox, on every browser on iOS, and on Chromium for Windows and Linux — so on a\n * large share of real devices the fallback *is* the feature. Give it a text field, or\n * inject a polyfill through `detector`.\n *\n * The preview itself is `aria-hidden`: a live camera frame has nothing to announce and\n * no audio to caption, so what a screen reader gets is the `role=\"status\"` line, which\n * says whether scanning is running and reads out each accepted code.\n *\n * @example\n * <BarcodeScanner\n * formats={[\"ean_13\"]}\n * onScan={({ rawValue }) => addToCart(rawValue)}\n * footer={<small>Aponte para o código de barras da embalagem.</small>}\n * unsupported={<ManualCodeInput onSubmit={addToCart} />}\n * />\n */\nexport function BarcodeScanner({\n onScan,\n formats,\n paused = false,\n detector,\n intervalMs,\n repeatDelayMs,\n torch = true,\n aspectRatio = 4 / 3,\n locale = \"pt-BR\",\n footer,\n unsupported,\n onError,\n className,\n ...rest\n}: BarcodeScannerProps) {\n const strings = STRINGS[locale];\n const {\n videoRef,\n status,\n error,\n supported,\n scanning,\n result,\n torch: lamp,\n retry,\n } = useBarcodeScanner({\n formats,\n paused,\n detector,\n intervalMs,\n repeatDelayMs,\n onScan,\n onError,\n });\n\n if (!supported) {\n return (\n <div className={cn(styles.scanner, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n {unsupported}\n </div>\n );\n }\n\n const message = result\n ? strings.found(result.rawValue)\n : paused\n ? strings.paused\n : scanning\n ? strings.scanning\n : strings.opening;\n\n return (\n <div className={cn(styles.scanner, className)} {...rest}>\n <div\n className={styles.viewport}\n style={{ aspectRatio: String(aspectRatio) }}\n aria-label={strings.viewport}\n role=\"group\"\n >\n <video\n ref={videoRef}\n className={styles.video}\n muted\n playsInline\n aria-hidden=\"true\"\n />\n\n {status === \"ready\" && (\n <div className={styles.frame} aria-hidden=\"true\">\n <span className={cn(styles.laser, scanning && styles.laserActive)} />\n </div>\n )}\n\n {torch && lamp.supported && (\n <button\n type=\"button\"\n className={styles.torch}\n onClick={() => void lamp.toggle()}\n aria-pressed={lamp.on}\n aria-label={lamp.on ? strings.torchOn : strings.torchOff}\n >\n {lamp.on ? (\n <FlashlightOff size={18} aria-hidden />\n ) : (\n <Flashlight size={18} aria-hidden />\n )}\n </button>\n )}\n\n {error && (\n <div className={styles.overlay} role=\"alert\">\n <p className={styles.overlayText}>{error.message}</p>\n <button type=\"button\" className={styles.retry} onClick={retry}>\n {strings.retry}\n </button>\n </div>\n )}\n </div>\n\n {!error && (\n <p className={styles.status} role=\"status\">\n <ScanLine size={14} aria-hidden className={styles.statusIcon} />\n {message}\n </p>\n )}\n\n {footer ? <div className={styles.footer}>{footer}</div> : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BarcodeScanner.js","names":[],"sources":["../../../src/components/BarcodeScanner/BarcodeScanner.tsx"],"sourcesContent":["import { Flashlight, FlashlightOff, ScanLine } from \"lucide-react\";\nimport { type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { useBarcodeScanner } from \"@/capture/use-barcode-scanner\";\nimport type { BarcodeDetectorLike, BarcodeFormat, BarcodeScanResult } from \"@/capture/barcode\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./BarcodeScanner.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface BarcodeScannerProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /** Called for every accepted read — repeats of the same value are suppressed. */\n onScan: (result: BarcodeScanResult) => void;\n /** Symbologies to look for. Defaults to QR + EAN-13 + Code 128. */\n formats?: readonly BarcodeFormat[];\n /** Stop looking without releasing the camera — set it while a confirmation is open. */\n paused?: boolean;\n /** A decoder to use instead of the native one, for Safari and Firefox. */\n detector?: BarcodeDetectorLike;\n /** How often a frame is examined, in ms. Default 200. */\n intervalMs?: number;\n /** Ignore the same value again for this long, in ms. Default 2500. */\n repeatDelayMs?: number;\n /** Offer the torch toggle when the camera has a lamp. Default `true`. */\n torch?: boolean;\n /** Viewport aspect ratio, `width / height`. Default `4 / 3`. */\n aspectRatio?: number;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Under the viewport — an instruction, the code just read, a manual-entry link. */\n footer?: ReactNode;\n /**\n * Rendered instead of the camera when there is no decoder.\n *\n * Worth filling in: on iOS and Firefox this is the **only** thing the user sees, so\n * the fallback is usually a plain text field for typing the code.\n */\n unsupported?: ReactNode;\n /** A frame the engine refused to decode. Routine and usually transient. */\n onError?: (error: unknown) => void;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n viewport: \"Visor da câmera\",\n scanning: \"Procurando código…\",\n paused: \"Leitura pausada\",\n opening: \"Abrindo a câmera…\",\n hint: \"Aponte a câmera para o código.\",\n found: (value: string) => `Código lido: ${value}`,\n torchOn: \"Desligar lanterna\",\n torchOff: \"Ligar lanterna\",\n unsupported: \"Este navegador não decodifica códigos de barras.\",\n retry: \"Tentar de novo\",\n },\n en: {\n viewport: \"Camera viewport\",\n scanning: \"Looking for a code…\",\n paused: \"Scanning paused\",\n opening: \"Opening the camera…\",\n hint: \"Point the camera at the code.\",\n found: (value: string) => `Code read: ${value}`,\n torchOn: \"Turn torch off\",\n torchOff: \"Turn torch on\",\n unsupported: \"This browser cannot decode barcodes.\",\n retry: \"Try again\",\n },\n} as const;\n\n/**\n * Point the camera at a barcode and get its value.\n *\n * The counterpart to {@link QRCode}, which only encodes. Under it are\n * `useBarcodeScanner` (the detect loop and repeat suppression), `useCameraStream` (the\n * stream and its classified errors) and `useTorch`.\n *\n * **Mounting this opens the camera**, so mount it when the user asks to scan rather\n * than on a page that happens to contain a scanner: a permission prompt nobody\n * provoked is the surest way to earn a permanent block, and after that `getUserMedia`\n * rejects without ever prompting again. The usual shape is a button that reveals it.\n *\n * The `unsupported` slot is not a nicety. `BarcodeDetector` is Chromium-only — absent\n * on Firefox, on every browser on iOS, and on Chromium for Windows and Linux — so on a\n * large share of real devices the fallback *is* the feature. Give it a text field, or\n * inject a polyfill through `detector`.\n *\n * The preview itself is `aria-hidden`: a live camera frame has nothing to announce and\n * no audio to caption, so what a screen reader gets is the `role=\"status\"` line, which\n * says whether scanning is running and reads out each accepted code.\n *\n * @example\n * <BarcodeScanner\n * formats={[\"ean_13\"]}\n * onScan={({ rawValue }) => addToCart(rawValue)}\n * footer={<small>Aponte para o código de barras da embalagem.</small>}\n * unsupported={<ManualCodeInput onSubmit={addToCart} />}\n * />\n */\nexport function BarcodeScanner({\n onScan,\n formats,\n paused = false,\n detector,\n intervalMs,\n repeatDelayMs,\n torch = true,\n aspectRatio = 4 / 3,\n locale = \"pt-BR\",\n footer,\n unsupported,\n onError,\n className,\n ...rest\n}: BarcodeScannerProps) {\n const strings = STRINGS[locale];\n const {\n videoRef,\n status,\n error,\n supported,\n scanning,\n result,\n torch: lamp,\n retry,\n } = useBarcodeScanner({\n formats,\n paused,\n detector,\n intervalMs,\n repeatDelayMs,\n onScan,\n onError,\n });\n\n if (!supported) {\n return (\n <div className={cn(styles.scanner, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n {unsupported}\n </div>\n );\n }\n\n const message = result\n ? strings.found(result.rawValue)\n : paused\n ? strings.paused\n : scanning\n ? strings.scanning\n : strings.opening;\n\n return (\n <div className={cn(styles.scanner, className)} {...rest}>\n <div\n className={styles.viewport}\n style={{ aspectRatio: String(aspectRatio) }}\n aria-label={strings.viewport}\n role=\"group\"\n >\n <video\n ref={videoRef}\n className={styles.video}\n muted\n playsInline\n aria-hidden=\"true\"\n />\n\n {status === \"ready\" && (\n <div className={styles.frame} aria-hidden=\"true\">\n <span className={cn(styles.laser, scanning && styles.laserActive)} />\n </div>\n )}\n\n {torch && lamp.supported && (\n <button\n type=\"button\"\n className={styles.torch}\n onClick={() => void lamp.toggle()}\n aria-pressed={lamp.on}\n aria-label={lamp.on ? strings.torchOn : strings.torchOff}\n >\n {lamp.on ? (\n <FlashlightOff size={18} aria-hidden />\n ) : (\n <Flashlight size={18} aria-hidden />\n )}\n </button>\n )}\n\n {error && (\n <div className={styles.overlay} role=\"alert\">\n <p className={styles.overlayText}>{error.message}</p>\n <button type=\"button\" className={styles.retry} onClick={retry}>\n {strings.retry}\n </button>\n </div>\n )}\n </div>\n\n {!error && (\n <p className={styles.status} role=\"status\">\n <ScanLine size={14} aria-hidden className={styles.statusIcon} />\n {message}\n </p>\n )}\n\n {footer ? <div className={styles.footer}>{footer}</div> : null}\n </div>\n );\n}\n"],"mappings":";;;;;;;AA+CA,IAAM,IAAU;CACZ,SAAS;EACL,UAAU;EACV,UAAU;EACV,QAAQ;EACR,SAAS;EACT,MAAM;EACN,QAAQ,MAAkB,gBAAgB;EAC1C,SAAS;EACT,UAAU;EACV,aAAa;EACb,OAAO;CACX;CACA,IAAI;EACA,UAAU;EACV,UAAU;EACV,QAAQ;EACR,SAAS;EACT,MAAM;EACN,QAAQ,MAAkB,cAAc;EACxC,SAAS;EACT,UAAU;EACV,aAAa;EACb,OAAO;CACX;AACJ;AA+BA,SAAgB,EAAe,EAC3B,WACA,YACA,YAAS,IACT,aACA,eACA,kBACA,WAAQ,IACR,iBAAc,IAAI,GAClB,YAAS,SACT,WACA,gBACA,YACA,cACA,GAAG,KACiB;CACpB,IAAM,IAAU,EAAQ,IAClB,EACF,aACA,WACA,UACA,cACA,aACA,WACA,OAAO,GACP,aACA,EAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;CACJ,CAAC;CAED,IAAI,CAAC,GACD,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,SAAS,CAAS;EAAG,GAAI;YAAnD,CACI,kBAAC,KAAD;GAAG,WAAW,EAAO;aAAS,EAAQ;EAAe,CAAA,GACpD,CACA;;CAIb,IAAM,IAAU,IACV,EAAQ,MAAM,EAAO,QAAQ,IAC7B,IACE,EAAQ,SACR,IACE,EAAQ,WACR,EAAQ;CAElB,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,SAAS,CAAS;EAAG,GAAI;YAAnD;GACI,kBAAC,OAAD;IACI,WAAW,EAAO;IAClB,OAAO,EAAE,aAAa,OAAO,CAAW,EAAE;IAC1C,cAAY,EAAQ;IACpB,MAAK;cAJT;KAMI,kBAAC,SAAD;MACI,KAAK;MACL,WAAW,EAAO;MAClB,OAAA;MACA,aAAA;MACA,eAAY;KACf,CAAA;KAEA,MAAW,WACR,kBAAC,OAAD;MAAK,WAAW,EAAO;MAAO,eAAY;gBACtC,kBAAC,QAAD,EAAM,WAAW,EAAG,EAAO,OAAO,KAAY,EAAO,WAAW,EAAI,CAAA;KACnE,CAAA;KAGR,KAAS,EAAK,aACX,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,eAAe,KAAK,EAAK,OAAO;MAChC,gBAAc,EAAK;MACnB,cAAY,EAAK,KAAK,EAAQ,UAAU,EAAQ;gBAE/C,EAAK,KACF,kBAAC,GAAD;OAAe,MAAM;OAAI,eAAA;MAAa,CAAA,IAEtC,kBAAC,GAAD;OAAY,MAAM;OAAI,eAAA;MAAa,CAAA;KAEnC,CAAA;KAGX,KACG,kBAAC,OAAD;MAAK,WAAW,EAAO;MAAS,MAAK;gBAArC,CACI,kBAAC,KAAD;OAAG,WAAW,EAAO;iBAAc,EAAM;MAAW,CAAA,GACpD,kBAAC,UAAD;OAAQ,MAAK;OAAS,WAAW,EAAO;OAAO,SAAS;iBACnD,EAAQ;MACL,CAAA,CACP;;IAER;;GAEJ,CAAC,KACE,kBAAC,KAAD;IAAG,WAAW,EAAO;IAAQ,MAAK;cAAlC,CACI,kBAAC,GAAD;KAAU,MAAM;KAAI,eAAA;KAAY,WAAW,EAAO;IAAa,CAAA,GAC9D,CACF;;GAGN,IAAS,kBAAC,OAAD;IAAK,WAAW,EAAO;cAAS;GAAY,CAAA,IAAI;EACzD;;AAEb"}
|
|
1
|
+
{"version":3,"file":"BarcodeScanner.js","names":[],"sources":["../../../src/components/BarcodeScanner/BarcodeScanner.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the scan loop is tuned\n * by the caller because the right values depend on the symbology and the device:\n * formats, intervalMs, repeatDelayMs, detector, paused, torch, aspectRatio. The rest\n * are the surfaces to fill when the API is missing (unsupported, footer, locale) and\n * the two outputs (onScan, onError).\n */\nimport { Flashlight, FlashlightOff, ScanLine } from \"lucide-react\";\nimport { type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { useBarcodeScanner } from \"@/capture/use-barcode-scanner\";\nimport type { BarcodeDetectorLike, BarcodeFormat, BarcodeScanResult } from \"@/capture/barcode\";\nimport { cn } from \"@/utils/cn\";\n\nimport styles from \"./BarcodeScanner.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onError\";\n\nexport interface BarcodeScannerProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n OverriddenDomProps\n> {\n /** Called for every accepted read — repeats of the same value are suppressed. */\n onScan: (result: BarcodeScanResult) => void;\n /** Symbologies to look for. Defaults to QR + EAN-13 + Code 128. */\n formats?: readonly BarcodeFormat[];\n /** Stop looking without releasing the camera — set it while a confirmation is open. */\n paused?: boolean;\n /** A decoder to use instead of the native one, for Safari and Firefox. */\n detector?: BarcodeDetectorLike;\n /** How often a frame is examined, in ms. Default 200. */\n intervalMs?: number;\n /** Ignore the same value again for this long, in ms. Default 2500. */\n repeatDelayMs?: number;\n /** Offer the torch toggle when the camera has a lamp. Default `true`. */\n torch?: boolean;\n /** Viewport aspect ratio, `width / height`. Default `4 / 3`. */\n aspectRatio?: number;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Under the viewport — an instruction, the code just read, a manual-entry link. */\n footer?: ReactNode;\n /**\n * Rendered instead of the camera when there is no decoder.\n *\n * Worth filling in: on iOS and Firefox this is the **only** thing the user sees, so\n * the fallback is usually a plain text field for typing the code.\n */\n unsupported?: ReactNode;\n /** A frame the engine refused to decode. Routine and usually transient. */\n onError?: (error: unknown) => void;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n viewport: \"Visor da câmera\",\n scanning: \"Procurando código…\",\n paused: \"Leitura pausada\",\n opening: \"Abrindo a câmera…\",\n hint: \"Aponte a câmera para o código.\",\n found: (value: string) => `Código lido: ${value}`,\n torchOn: \"Desligar lanterna\",\n torchOff: \"Ligar lanterna\",\n unsupported: \"Este navegador não decodifica códigos de barras.\",\n retry: \"Tentar de novo\",\n },\n en: {\n viewport: \"Camera viewport\",\n scanning: \"Looking for a code…\",\n paused: \"Scanning paused\",\n opening: \"Opening the camera…\",\n hint: \"Point the camera at the code.\",\n found: (value: string) => `Code read: ${value}`,\n torchOn: \"Turn torch off\",\n torchOff: \"Turn torch on\",\n unsupported: \"This browser cannot decode barcodes.\",\n retry: \"Try again\",\n },\n} as const;\n\n/**\n * Point the camera at a barcode and get its value.\n *\n * The counterpart to {@link QRCode}, which only encodes. Under it are\n * `useBarcodeScanner` (the detect loop and repeat suppression), `useCameraStream` (the\n * stream and its classified errors) and `useTorch`.\n *\n * **Mounting this opens the camera**, so mount it when the user asks to scan rather\n * than on a page that happens to contain a scanner: a permission prompt nobody\n * provoked is the surest way to earn a permanent block, and after that `getUserMedia`\n * rejects without ever prompting again. The usual shape is a button that reveals it.\n *\n * The `unsupported` slot is not a nicety. `BarcodeDetector` is Chromium-only — absent\n * on Firefox, on every browser on iOS, and on Chromium for Windows and Linux — so on a\n * large share of real devices the fallback *is* the feature. Give it a text field, or\n * inject a polyfill through `detector`.\n *\n * The preview itself is `aria-hidden`: a live camera frame has nothing to announce and\n * no audio to caption, so what a screen reader gets is the `role=\"status\"` line, which\n * says whether scanning is running and reads out each accepted code.\n *\n * @example\n * <BarcodeScanner\n * formats={[\"ean_13\"]}\n * onScan={({ rawValue }) => addToCart(rawValue)}\n * footer={<small>Aponte para o código de barras da embalagem.</small>}\n * unsupported={<ManualCodeInput onSubmit={addToCart} />}\n * />\n */\nexport function BarcodeScanner({\n onScan,\n formats,\n paused = false,\n detector,\n intervalMs,\n repeatDelayMs,\n torch = true,\n aspectRatio = 4 / 3,\n locale = \"pt-BR\",\n footer,\n unsupported,\n onError,\n className,\n ...rest\n}: BarcodeScannerProps) {\n const strings = STRINGS[locale];\n const {\n videoRef,\n status,\n error,\n supported,\n scanning,\n result,\n torch: lamp,\n retry,\n } = useBarcodeScanner({\n formats,\n paused,\n detector,\n intervalMs,\n repeatDelayMs,\n onScan,\n onError,\n });\n\n if (!supported) {\n return (\n <div className={cn(styles.scanner, className)} {...rest}>\n <p className={styles.notice}>{strings.unsupported}</p>\n {unsupported}\n </div>\n );\n }\n\n const message = result\n ? strings.found(result.rawValue)\n : paused\n ? strings.paused\n : scanning\n ? strings.scanning\n : strings.opening;\n\n return (\n <div className={cn(styles.scanner, className)} {...rest}>\n <div\n className={styles.viewport}\n style={{ aspectRatio: String(aspectRatio) }}\n aria-label={strings.viewport}\n role=\"group\"\n >\n <video\n ref={videoRef}\n className={styles.video}\n muted\n playsInline\n aria-hidden=\"true\"\n />\n\n {status === \"ready\" && (\n <div className={styles.frame} aria-hidden=\"true\">\n <span className={cn(styles.laser, scanning && styles.laserActive)} />\n </div>\n )}\n\n {torch && lamp.supported && (\n <button\n type=\"button\"\n className={styles.torch}\n onClick={() => void lamp.toggle()}\n aria-pressed={lamp.on}\n aria-label={lamp.on ? strings.torchOn : strings.torchOff}\n >\n {lamp.on ? (\n <FlashlightOff size={18} aria-hidden />\n ) : (\n <Flashlight size={18} aria-hidden />\n )}\n </button>\n )}\n\n {error && (\n <div className={styles.overlay} role=\"alert\">\n <p className={styles.overlayText}>{error.message}</p>\n <button type=\"button\" className={styles.retry} onClick={retry}>\n {strings.retry}\n </button>\n </div>\n )}\n </div>\n\n {!error && (\n <p className={styles.status} role=\"status\">\n <ScanLine size={14} aria-hidden className={styles.statusIcon} />\n {message}\n </p>\n )}\n\n {footer ? <div className={styles.footer}>{footer}</div> : null}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAsDA,IAAM,IAAU;CACZ,SAAS;EACL,UAAU;EACV,UAAU;EACV,QAAQ;EACR,SAAS;EACT,MAAM;EACN,QAAQ,MAAkB,gBAAgB;EAC1C,SAAS;EACT,UAAU;EACV,aAAa;EACb,OAAO;CACX;CACA,IAAI;EACA,UAAU;EACV,UAAU;EACV,QAAQ;EACR,SAAS;EACT,MAAM;EACN,QAAQ,MAAkB,cAAc;EACxC,SAAS;EACT,UAAU;EACV,aAAa;EACb,OAAO;CACX;AACJ;AA+BA,SAAgB,EAAe,EAC3B,WACA,YACA,YAAS,IACT,aACA,eACA,kBACA,WAAQ,IACR,iBAAc,IAAI,GAClB,YAAS,SACT,WACA,gBACA,YACA,cACA,GAAG,KACiB;CACpB,IAAM,IAAU,EAAQ,IAClB,EACF,aACA,WACA,UACA,cACA,aACA,WACA,OAAO,GACP,aACA,EAAkB;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;CACJ,CAAC;CAED,IAAI,CAAC,GACD,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,SAAS,CAAS;EAAG,GAAI;YAAnD,CACI,kBAAC,KAAD;GAAG,WAAW,EAAO;aAAS,EAAQ;EAAe,CAAA,GACpD,CACA;;CAIb,IAAM,IAAU,IACV,EAAQ,MAAM,EAAO,QAAQ,IAC7B,IACE,EAAQ,SACR,IACE,EAAQ,WACR,EAAQ;CAElB,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,SAAS,CAAS;EAAG,GAAI;YAAnD;GACI,kBAAC,OAAD;IACI,WAAW,EAAO;IAClB,OAAO,EAAE,aAAa,OAAO,CAAW,EAAE;IAC1C,cAAY,EAAQ;IACpB,MAAK;cAJT;KAMI,kBAAC,SAAD;MACI,KAAK;MACL,WAAW,EAAO;MAClB,OAAA;MACA,aAAA;MACA,eAAY;KACf,CAAA;KAEA,MAAW,WACR,kBAAC,OAAD;MAAK,WAAW,EAAO;MAAO,eAAY;gBACtC,kBAAC,QAAD,EAAM,WAAW,EAAG,EAAO,OAAO,KAAY,EAAO,WAAW,EAAI,CAAA;KACnE,CAAA;KAGR,KAAS,EAAK,aACX,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,eAAe,KAAK,EAAK,OAAO;MAChC,gBAAc,EAAK;MACnB,cAAY,EAAK,KAAK,EAAQ,UAAU,EAAQ;gBAE/C,EAAK,KACF,kBAAC,GAAD;OAAe,MAAM;OAAI,eAAA;MAAa,CAAA,IAEtC,kBAAC,GAAD;OAAY,MAAM;OAAI,eAAA;MAAa,CAAA;KAEnC,CAAA;KAGX,KACG,kBAAC,OAAD;MAAK,WAAW,EAAO;MAAS,MAAK;gBAArC,CACI,kBAAC,KAAD;OAAG,WAAW,EAAO;iBAAc,EAAM;MAAW,CAAA,GACpD,kBAAC,UAAD;OAAQ,MAAK;OAAS,WAAW,EAAO;OAAO,SAAS;iBACnD,EAAQ;MACL,CAAA,CACP;;IAER;;GAEJ,CAAC,KACE,kBAAC,KAAD;IAAG,WAAW,EAAO;IAAQ,MAAK;cAAlC,CACI,kBAAC,GAAD;KAAU,MAAM;KAAI,eAAA;KAAY,WAAW,EAAO;IAAa,CAAA,GAC9D,CACF;;GAGN,IAAS,kBAAC,OAAD;IAAK,WAAW,EAAO;cAAS;GAAY,CAAA,IAAI;EACzD;;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Button.cjs","names":[],"sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Button.cjs","names":[],"sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — this is the design system's button: variant, size,\n * loading, fullWidth, iconOnly, pill, leftIcon and rightIcon are the whole\n * vocabulary every screen composes from. Eight orthogonal knobs on the most-used\n * component is the API working — the count rule is looking for two components fused\n * together, and there is only one here.\n */\nimport type { ButtonHTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Button.module.css\";\n\nexport type ButtonVariant =\n \"primary\" | \"secondary\" | \"danger\" | \"success\" | \"ghost\" | \"soft\" | \"outline\" | \"link\";\nexport type ButtonSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\n\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n variant?: ButtonVariant;\n size?: ButtonSize;\n loading?: boolean;\n fullWidth?: boolean;\n /** Removes horizontal padding and forces a square footprint (use with `aria-label`). */\n iconOnly?: boolean;\n /** Pill-shaped border radius. */\n pill?: boolean;\n leftIcon?: ReactNode;\n rightIcon?: ReactNode;\n}\n\n/**\n * Primary action button with variants, sizes and a loading state that\n * preserves layout via an absolutely-positioned spinner.\n *\n * Variants: `primary` (solid), `secondary` (neutral), `danger`, `success`,\n * `ghost` (transparent), `soft` (tinted), `outline` (bordered), `link`.\n *\n * Sizes are density-aware — they read from `--tempest-control-height-*`\n * tokens which respond to the `data-tempest-density` attribute.\n */\nexport function Button({\n variant = \"primary\",\n size = \"md\",\n loading = false,\n fullWidth = false,\n iconOnly = false,\n pill = false,\n leftIcon,\n rightIcon,\n disabled,\n className,\n children,\n ...props\n}: ButtonProps) {\n return (\n <button\n className={cn(\n styles.button,\n styles[variant],\n styles[size],\n iconOnly && styles.iconOnly,\n pill && styles.pill,\n loading && styles.loading,\n fullWidth && styles.fullWidth,\n className,\n )}\n disabled={disabled || loading}\n {...props}\n >\n {loading && (\n <span className={styles.spinner} aria-hidden>\n <SpinnerIcon />\n </span>\n )}\n <span className={cn(styles.content, loading && styles.hiddenText)}>\n {leftIcon}\n {children}\n {rightIcon}\n </span>\n </button>\n );\n}\n\nfunction SpinnerIcon() {\n return (\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M21 12a9 9 0 1 1-6.219-8.56\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"0GAsCA,SAAgB,EAAO,CACnB,UAAU,UACV,OAAO,KACP,UAAU,GACV,YAAY,GACZ,WAAW,GACX,OAAO,GACP,WACA,YACA,WACA,YACA,WACA,GAAG,GACS,CACZ,OACI,EAAA,EAAA,KAAA,CAAC,SAAD,CACI,UAAW,EAAA,GACP,EAAA,QAAO,OACP,EAAA,QAAO,GACP,EAAA,QAAO,GACP,GAAY,EAAA,QAAO,SACnB,GAAQ,EAAA,QAAO,KACf,GAAW,EAAA,QAAO,QAClB,GAAa,EAAA,QAAO,UACpB,CACJ,EACA,SAAU,GAAY,EACtB,GAAI,WAZR,CAcK,IACG,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,QAAS,cAAA,aAC7B,EAAA,EAAA,IAAA,CAAC,EAAD,CAAc,CAAA,CACZ,CAAA,GAEV,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,GAAW,EAAA,QAAO,UAAU,WAAhE,CACK,EACA,EACA,CACC,GACF,GAEhB,CAEA,SAAS,GAAc,CACnB,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,MAAM,KACN,OAAO,KACP,QAAQ,YACR,KAAK,OACL,MAAM,uCAEN,EAAA,EAAA,IAAA,CAAC,OAAD,CACI,EAAE,8BACF,OAAO,eACP,YAAY,IACZ,cAAc,OACjB,CAAA,CACA,CAAA,CAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Button.js","names":[],"sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Button.js","names":[],"sources":["../../../src/components/Button/Button.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — this is the design system's button: variant, size,\n * loading, fullWidth, iconOnly, pill, leftIcon and rightIcon are the whole\n * vocabulary every screen composes from. Eight orthogonal knobs on the most-used\n * component is the API working — the count rule is looking for two components fused\n * together, and there is only one here.\n */\nimport type { ButtonHTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Button.module.css\";\n\nexport type ButtonVariant =\n \"primary\" | \"secondary\" | \"danger\" | \"success\" | \"ghost\" | \"soft\" | \"outline\" | \"link\";\nexport type ButtonSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\n\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n variant?: ButtonVariant;\n size?: ButtonSize;\n loading?: boolean;\n fullWidth?: boolean;\n /** Removes horizontal padding and forces a square footprint (use with `aria-label`). */\n iconOnly?: boolean;\n /** Pill-shaped border radius. */\n pill?: boolean;\n leftIcon?: ReactNode;\n rightIcon?: ReactNode;\n}\n\n/**\n * Primary action button with variants, sizes and a loading state that\n * preserves layout via an absolutely-positioned spinner.\n *\n * Variants: `primary` (solid), `secondary` (neutral), `danger`, `success`,\n * `ghost` (transparent), `soft` (tinted), `outline` (bordered), `link`.\n *\n * Sizes are density-aware — they read from `--tempest-control-height-*`\n * tokens which respond to the `data-tempest-density` attribute.\n */\nexport function Button({\n variant = \"primary\",\n size = \"md\",\n loading = false,\n fullWidth = false,\n iconOnly = false,\n pill = false,\n leftIcon,\n rightIcon,\n disabled,\n className,\n children,\n ...props\n}: ButtonProps) {\n return (\n <button\n className={cn(\n styles.button,\n styles[variant],\n styles[size],\n iconOnly && styles.iconOnly,\n pill && styles.pill,\n loading && styles.loading,\n fullWidth && styles.fullWidth,\n className,\n )}\n disabled={disabled || loading}\n {...props}\n >\n {loading && (\n <span className={styles.spinner} aria-hidden>\n <SpinnerIcon />\n </span>\n )}\n <span className={cn(styles.content, loading && styles.hiddenText)}>\n {leftIcon}\n {children}\n {rightIcon}\n </span>\n </button>\n );\n}\n\nfunction SpinnerIcon() {\n return (\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M21 12a9 9 0 1 1-6.219-8.56\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":";;;;AAsCA,SAAgB,EAAO,EACnB,aAAU,WACV,UAAO,MACP,aAAU,IACV,eAAY,IACZ,cAAW,IACX,UAAO,IACP,aACA,cACA,aACA,cACA,aACA,GAAG,KACS;CACZ,OACI,kBAAC,UAAD;EACI,WAAW,EACP,EAAO,QACP,EAAO,IACP,EAAO,IACP,KAAY,EAAO,UACnB,KAAQ,EAAO,MACf,KAAW,EAAO,SAClB,KAAa,EAAO,WACpB,CACJ;EACA,UAAU,KAAY;EACtB,GAAI;YAZR,CAcK,KACG,kBAAC,QAAD;GAAM,WAAW,EAAO;GAAS,eAAA;aAC7B,kBAAC,GAAD,CAAc,CAAA;EACZ,CAAA,GAEV,kBAAC,QAAD;GAAM,WAAW,EAAG,EAAO,SAAS,KAAW,EAAO,UAAU;aAAhE;IACK;IACA;IACA;GACC;IACF;;AAEhB;AAEA,SAAS,IAAc;CACnB,OACI,kBAAC,OAAD;EACI,OAAM;EACN,QAAO;EACP,SAAQ;EACR,MAAK;EACL,OAAM;YAEN,kBAAC,QAAD;GACI,GAAE;GACF,QAAO;GACP,aAAY;GACZ,eAAc;EACjB,CAAA;CACA,CAAA;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Calendar.cjs","names":[],"sources":["../../../src/components/Calendar/Calendar.tsx"],"sourcesContent":["import { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { HTMLAttributes, KeyboardEvent } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Calendar.module.css\";\n\nexport type WeekStart = 0 | 1;\n\nexport interface CalendarProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n \"onChange\" | \"defaultValue\"\n> {\n /** Controlled selected date. */\n value?: Date;\n /** Initial selected date for the uncontrolled case. */\n defaultValue?: Date;\n /** Called with the newly selected date. */\n onChange?: (date: Date) => void;\n /** Controlled visible month (any day within it). */\n month?: Date;\n /** Called when the visible month changes (prev/next). */\n onMonthChange?: (month: Date) => void;\n /** Earliest selectable date (inclusive). */\n minDate?: Date;\n /** Latest selectable date (inclusive). */\n maxDate?: Date;\n /** First column of the week — `0` Sunday (default) or `1` Monday. */\n weekStartsOn?: WeekStart;\n}\n\nconst WEEKDAY_LABELS = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\nconst MONTH_LABELS = [\n \"January\",\n \"February\",\n \"March\",\n \"April\",\n \"May\",\n \"June\",\n \"July\",\n \"August\",\n \"September\",\n \"October\",\n \"November\",\n \"December\",\n];\n\nconst DAYS_IN_GRID = 42;\n\n/**\n * Build a `Date` at midnight local time for the given year/month/day.\n *\n * @param year - Full year.\n * @param month - Zero-based month index.\n * @param day - Day of the month.\n * @returns A normalized `Date`.\n */\nfunction makeDate(year: number, month: number, day: number): Date {\n return new Date(year, month, day);\n}\n\n/**\n * Strip the time portion so two dates can be compared by calendar day.\n *\n * @param date - The date to normalize.\n * @returns A new `Date` at local midnight.\n */\nfunction startOfDay(date: Date): Date {\n return new Date(date.getFullYear(), date.getMonth(), date.getDate());\n}\n\n/**\n * Whether two dates fall on the same calendar day.\n *\n * @param a - First date.\n * @param b - Second date.\n * @returns `true` when year, month and day match.\n */\nfunction isSameDay(a: Date, b: Date): boolean {\n return (\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate()\n );\n}\n\n/**\n * Whether a date is outside the inclusive `[minDate, maxDate]` window.\n *\n * @param date - The candidate day.\n * @param minDate - Optional lower bound.\n * @param maxDate - Optional upper bound.\n * @returns `true` when the date is disabled.\n */\nfunction isOutOfRange(date: Date, minDate?: Date, maxDate?: Date): boolean {\n const day = startOfDay(date).getTime();\n if (minDate && day < startOfDay(minDate).getTime()) return true;\n if (maxDate && day > startOfDay(maxDate).getTime()) return true;\n return false;\n}\n\n/**\n * Compute the 42-cell (6x7) grid of dates covering the given month, padded with\n * leading/trailing days from the adjacent months.\n *\n * @param month - Any date within the target month.\n * @param weekStartsOn - First column of the week.\n * @returns An array of 42 `Date` objects.\n */\nfunction buildGrid(month: Date, weekStartsOn: WeekStart): Date[] {\n const year = month.getFullYear();\n const monthIndex = month.getMonth();\n const firstOfMonth = makeDate(year, monthIndex, 1);\n const firstWeekday = firstOfMonth.getDay();\n const leading = (firstWeekday - weekStartsOn + 7) % 7;\n const start = makeDate(year, monthIndex, 1 - leading);\n\n const cells: Date[] = [];\n for (let i = 0; i < DAYS_IN_GRID; i += 1) {\n cells.push(makeDate(start.getFullYear(), start.getMonth(), start.getDate() + i));\n }\n return cells;\n}\n\n/**\n * Standalone month-grid date picker. Renders a header with the month/year and\n * prev/next buttons, a weekday row, and a 6x7 grid of day buttons. Selection and\n * the visible month can each be controlled or uncontrolled. Grid arithmetic uses\n * plain `Date` math — no external date libraries.\n *\n * Keyboard: arrow keys move focus by day (left/right) or week (up/down), and\n * Enter/Space selects the focused day.\n */\nexport function Calendar({\n value,\n defaultValue,\n onChange,\n month,\n onMonthChange,\n minDate,\n maxDate,\n weekStartsOn = 0,\n className,\n ...props\n}: CalendarProps) {\n const isSelectionControlled = value !== undefined;\n const [internalSelected, setInternalSelected] = useState<Date | undefined>(defaultValue);\n const selected = isSelectionControlled ? value : internalSelected;\n\n const isMonthControlled = month !== undefined;\n const [internalMonth, setInternalMonth] = useState<Date>(() => {\n const base = month ?? value ?? defaultValue ?? new Date();\n return makeDate(base.getFullYear(), base.getMonth(), 1);\n });\n const visibleMonth = isMonthControlled\n ? makeDate(month.getFullYear(), month.getMonth(), 1)\n : internalMonth;\n\n const [focusedDay, setFocusedDay] = useState<number | null>(null);\n const gridRef = useRef<HTMLDivElement | null>(null);\n\n const today = useMemo(() => startOfDay(new Date()), []);\n const cells = useMemo(\n () => buildGrid(visibleMonth, weekStartsOn),\n [visibleMonth, weekStartsOn],\n );\n\n const weekdayLabels = useMemo(() => {\n return Array.from({ length: 7 }, (_, i) => WEEKDAY_LABELS[(i + weekStartsOn) % 7]);\n }, [weekStartsOn]);\n\n const changeMonth = useCallback(\n (offset: number): void => {\n const next = makeDate(visibleMonth.getFullYear(), visibleMonth.getMonth() + offset, 1);\n if (onMonthChange) onMonthChange(next);\n if (!isMonthControlled) setInternalMonth(next);\n },\n [visibleMonth, onMonthChange, isMonthControlled],\n );\n\n const selectDate = useCallback(\n (date: Date): void => {\n if (isOutOfRange(date, minDate, maxDate)) return;\n if (onChange) onChange(date);\n if (!isSelectionControlled) setInternalSelected(date);\n },\n [minDate, maxDate, onChange, isSelectionControlled],\n );\n\n const focusCell = useCallback((index: number): void => {\n const clamped = Math.max(0, Math.min(DAYS_IN_GRID - 1, index));\n setFocusedDay(clamped);\n const grid = gridRef.current;\n if (!grid) return;\n const buttons = grid.querySelectorAll<HTMLButtonElement>(\"button[data-day]\");\n buttons[clamped]?.focus();\n }, []);\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLButtonElement>, index: number): void => {\n switch (event.key) {\n case \"ArrowLeft\":\n event.preventDefault();\n focusCell(index - 1);\n break;\n case \"ArrowRight\":\n event.preventDefault();\n focusCell(index + 1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n focusCell(index - 7);\n break;\n case \"ArrowDown\":\n event.preventDefault();\n focusCell(index + 7);\n break;\n case \"Enter\":\n case \" \":\n event.preventDefault();\n selectDate(cells[index]);\n break;\n default:\n break;\n }\n },\n [focusCell, selectDate, cells],\n );\n\n return (\n <div className={cn(styles.root, className)} {...props}>\n <div className={styles.header}>\n <button\n type=\"button\"\n className={styles.nav}\n aria-label=\"Previous month\"\n onClick={() => changeMonth(-1)}\n >\n {\"‹\"}\n </button>\n <span className={styles.title} aria-live=\"polite\">\n {MONTH_LABELS[visibleMonth.getMonth()]} {visibleMonth.getFullYear()}\n </span>\n <button\n type=\"button\"\n className={styles.nav}\n aria-label=\"Next month\"\n onClick={() => changeMonth(1)}\n >\n {\"›\"}\n </button>\n </div>\n\n <div className={styles.weekdays} role=\"row\">\n {weekdayLabels.map((label) => (\n <span key={label} className={styles.weekday} role=\"columnheader\">\n {label}\n </span>\n ))}\n </div>\n\n <div className={styles.grid} role=\"grid\" ref={gridRef}>\n {cells.map((date, index) => {\n const inMonth = date.getMonth() === visibleMonth.getMonth();\n const isSelected = selected ? isSameDay(date, selected) : false;\n const isToday = isSameDay(date, today);\n const disabled = isOutOfRange(date, minDate, maxDate);\n const isFocusTarget =\n focusedDay === null\n ? inMonth && date.getDate() === 1\n : focusedDay === index;\n return (\n <button\n key={index}\n type=\"button\"\n data-day\n role=\"gridcell\"\n className={cn(\n styles.day,\n !inMonth && styles.outside,\n isSelected && styles.selected,\n isToday && styles.today,\n )}\n aria-pressed={isSelected}\n aria-selected={isSelected}\n aria-current={isToday ? \"date\" : undefined}\n disabled={disabled}\n tabIndex={isFocusTarget ? 0 : -1}\n onClick={() => selectDate(date)}\n onKeyDown={(event) => handleKeyDown(event, index)}\n >\n {date.getDate()}\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Calendar.cjs","names":[],"sources":["../../../src/components/Calendar/Calendar.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — a date grid is\n * controlled two ways at once — the selected day (value, defaultValue, onChange) and\n * the visible month (month, onMonthChange) — inside the bounds (minDate, maxDate,\n * weekStartsOn). The body builds the grid and owns roving focus and keyboard\n * navigation, which read the same cursor date.\n */\nimport { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { HTMLAttributes, KeyboardEvent } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Calendar.module.css\";\n\nexport type WeekStart = 0 | 1;\n\nexport interface CalendarProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n \"onChange\" | \"defaultValue\"\n> {\n /** Controlled selected date. */\n value?: Date;\n /** Initial selected date for the uncontrolled case. */\n defaultValue?: Date;\n /** Called with the newly selected date. */\n onChange?: (date: Date) => void;\n /** Controlled visible month (any day within it). */\n month?: Date;\n /** Called when the visible month changes (prev/next). */\n onMonthChange?: (month: Date) => void;\n /** Earliest selectable date (inclusive). */\n minDate?: Date;\n /** Latest selectable date (inclusive). */\n maxDate?: Date;\n /** First column of the week — `0` Sunday (default) or `1` Monday. */\n weekStartsOn?: WeekStart;\n}\n\nconst WEEKDAY_LABELS = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\nconst MONTH_LABELS = [\n \"January\",\n \"February\",\n \"March\",\n \"April\",\n \"May\",\n \"June\",\n \"July\",\n \"August\",\n \"September\",\n \"October\",\n \"November\",\n \"December\",\n];\n\nconst DAYS_IN_GRID = 42;\n\n/**\n * Build a `Date` at midnight local time for the given year/month/day.\n *\n * @param year - Full year.\n * @param month - Zero-based month index.\n * @param day - Day of the month.\n * @returns A normalized `Date`.\n */\nfunction makeDate(year: number, month: number, day: number): Date {\n return new Date(year, month, day);\n}\n\n/**\n * Strip the time portion so two dates can be compared by calendar day.\n *\n * @param date - The date to normalize.\n * @returns A new `Date` at local midnight.\n */\nfunction startOfDay(date: Date): Date {\n return new Date(date.getFullYear(), date.getMonth(), date.getDate());\n}\n\n/**\n * Whether two dates fall on the same calendar day.\n *\n * @param a - First date.\n * @param b - Second date.\n * @returns `true` when year, month and day match.\n */\nfunction isSameDay(a: Date, b: Date): boolean {\n return (\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate()\n );\n}\n\n/**\n * Whether a date is outside the inclusive `[minDate, maxDate]` window.\n *\n * @param date - The candidate day.\n * @param minDate - Optional lower bound.\n * @param maxDate - Optional upper bound.\n * @returns `true` when the date is disabled.\n */\nfunction isOutOfRange(date: Date, minDate?: Date, maxDate?: Date): boolean {\n const day = startOfDay(date).getTime();\n if (minDate && day < startOfDay(minDate).getTime()) return true;\n if (maxDate && day > startOfDay(maxDate).getTime()) return true;\n return false;\n}\n\n/**\n * Compute the 42-cell (6x7) grid of dates covering the given month, padded with\n * leading/trailing days from the adjacent months.\n *\n * @param month - Any date within the target month.\n * @param weekStartsOn - First column of the week.\n * @returns An array of 42 `Date` objects.\n */\nfunction buildGrid(month: Date, weekStartsOn: WeekStart): Date[] {\n const year = month.getFullYear();\n const monthIndex = month.getMonth();\n const firstOfMonth = makeDate(year, monthIndex, 1);\n const firstWeekday = firstOfMonth.getDay();\n const leading = (firstWeekday - weekStartsOn + 7) % 7;\n const start = makeDate(year, monthIndex, 1 - leading);\n\n const cells: Date[] = [];\n for (let i = 0; i < DAYS_IN_GRID; i += 1) {\n cells.push(makeDate(start.getFullYear(), start.getMonth(), start.getDate() + i));\n }\n return cells;\n}\n\n/**\n * Standalone month-grid date picker. Renders a header with the month/year and\n * prev/next buttons, a weekday row, and a 6x7 grid of day buttons. Selection and\n * the visible month can each be controlled or uncontrolled. Grid arithmetic uses\n * plain `Date` math — no external date libraries.\n *\n * Keyboard: arrow keys move focus by day (left/right) or week (up/down), and\n * Enter/Space selects the focused day.\n */\nexport function Calendar({\n value,\n defaultValue,\n onChange,\n month,\n onMonthChange,\n minDate,\n maxDate,\n weekStartsOn = 0,\n className,\n ...props\n}: CalendarProps) {\n const isSelectionControlled = value !== undefined;\n const [internalSelected, setInternalSelected] = useState<Date | undefined>(defaultValue);\n const selected = isSelectionControlled ? value : internalSelected;\n\n const isMonthControlled = month !== undefined;\n const [internalMonth, setInternalMonth] = useState<Date>(() => {\n const base = month ?? value ?? defaultValue ?? new Date();\n return makeDate(base.getFullYear(), base.getMonth(), 1);\n });\n const visibleMonth = isMonthControlled\n ? makeDate(month.getFullYear(), month.getMonth(), 1)\n : internalMonth;\n\n const [focusedDay, setFocusedDay] = useState<number | null>(null);\n const gridRef = useRef<HTMLDivElement | null>(null);\n\n const today = useMemo(() => startOfDay(new Date()), []);\n const cells = useMemo(\n () => buildGrid(visibleMonth, weekStartsOn),\n [visibleMonth, weekStartsOn],\n );\n\n const weekdayLabels = useMemo(() => {\n return Array.from({ length: 7 }, (_, i) => WEEKDAY_LABELS[(i + weekStartsOn) % 7]);\n }, [weekStartsOn]);\n\n const changeMonth = useCallback(\n (offset: number): void => {\n const next = makeDate(visibleMonth.getFullYear(), visibleMonth.getMonth() + offset, 1);\n if (onMonthChange) onMonthChange(next);\n if (!isMonthControlled) setInternalMonth(next);\n },\n [visibleMonth, onMonthChange, isMonthControlled],\n );\n\n const selectDate = useCallback(\n (date: Date): void => {\n if (isOutOfRange(date, minDate, maxDate)) return;\n if (onChange) onChange(date);\n if (!isSelectionControlled) setInternalSelected(date);\n },\n [minDate, maxDate, onChange, isSelectionControlled],\n );\n\n const focusCell = useCallback((index: number): void => {\n const clamped = Math.max(0, Math.min(DAYS_IN_GRID - 1, index));\n setFocusedDay(clamped);\n const grid = gridRef.current;\n if (!grid) return;\n const buttons = grid.querySelectorAll<HTMLButtonElement>(\"button[data-day]\");\n buttons[clamped]?.focus();\n }, []);\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLButtonElement>, index: number): void => {\n switch (event.key) {\n case \"ArrowLeft\":\n event.preventDefault();\n focusCell(index - 1);\n break;\n case \"ArrowRight\":\n event.preventDefault();\n focusCell(index + 1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n focusCell(index - 7);\n break;\n case \"ArrowDown\":\n event.preventDefault();\n focusCell(index + 7);\n break;\n case \"Enter\":\n case \" \":\n event.preventDefault();\n selectDate(cells[index]);\n break;\n default:\n break;\n }\n },\n [focusCell, selectDate, cells],\n );\n\n return (\n <div className={cn(styles.root, className)} {...props}>\n <div className={styles.header}>\n <button\n type=\"button\"\n className={styles.nav}\n aria-label=\"Previous month\"\n onClick={() => changeMonth(-1)}\n >\n {\"‹\"}\n </button>\n <span className={styles.title} aria-live=\"polite\">\n {MONTH_LABELS[visibleMonth.getMonth()]} {visibleMonth.getFullYear()}\n </span>\n <button\n type=\"button\"\n className={styles.nav}\n aria-label=\"Next month\"\n onClick={() => changeMonth(1)}\n >\n {\"›\"}\n </button>\n </div>\n\n <div className={styles.weekdays} role=\"row\">\n {weekdayLabels.map((label) => (\n <span key={label} className={styles.weekday} role=\"columnheader\">\n {label}\n </span>\n ))}\n </div>\n\n <div className={styles.grid} role=\"grid\" ref={gridRef}>\n {cells.map((date, index) => {\n const inMonth = date.getMonth() === visibleMonth.getMonth();\n const isSelected = selected ? isSameDay(date, selected) : false;\n const isToday = isSameDay(date, today);\n const disabled = isOutOfRange(date, minDate, maxDate);\n const isFocusTarget =\n focusedDay === null\n ? inMonth && date.getDate() === 1\n : focusedDay === index;\n return (\n <button\n key={index}\n type=\"button\"\n data-day\n role=\"gridcell\"\n className={cn(\n styles.day,\n !inMonth && styles.outside,\n isSelected && styles.selected,\n isToday && styles.today,\n )}\n aria-pressed={isSelected}\n aria-selected={isSelected}\n aria-current={isToday ? \"date\" : undefined}\n disabled={disabled}\n tabIndex={isFocusTarget ? 0 : -1}\n onClick={() => selectDate(date)}\n onKeyDown={(event) => handleKeyDown(event, index)}\n >\n {date.getDate()}\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Calendar.js","names":[],"sources":["../../../src/components/Calendar/Calendar.tsx"],"sourcesContent":["import { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { HTMLAttributes, KeyboardEvent } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Calendar.module.css\";\n\nexport type WeekStart = 0 | 1;\n\nexport interface CalendarProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n \"onChange\" | \"defaultValue\"\n> {\n /** Controlled selected date. */\n value?: Date;\n /** Initial selected date for the uncontrolled case. */\n defaultValue?: Date;\n /** Called with the newly selected date. */\n onChange?: (date: Date) => void;\n /** Controlled visible month (any day within it). */\n month?: Date;\n /** Called when the visible month changes (prev/next). */\n onMonthChange?: (month: Date) => void;\n /** Earliest selectable date (inclusive). */\n minDate?: Date;\n /** Latest selectable date (inclusive). */\n maxDate?: Date;\n /** First column of the week — `0` Sunday (default) or `1` Monday. */\n weekStartsOn?: WeekStart;\n}\n\nconst WEEKDAY_LABELS = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\nconst MONTH_LABELS = [\n \"January\",\n \"February\",\n \"March\",\n \"April\",\n \"May\",\n \"June\",\n \"July\",\n \"August\",\n \"September\",\n \"October\",\n \"November\",\n \"December\",\n];\n\nconst DAYS_IN_GRID = 42;\n\n/**\n * Build a `Date` at midnight local time for the given year/month/day.\n *\n * @param year - Full year.\n * @param month - Zero-based month index.\n * @param day - Day of the month.\n * @returns A normalized `Date`.\n */\nfunction makeDate(year: number, month: number, day: number): Date {\n return new Date(year, month, day);\n}\n\n/**\n * Strip the time portion so two dates can be compared by calendar day.\n *\n * @param date - The date to normalize.\n * @returns A new `Date` at local midnight.\n */\nfunction startOfDay(date: Date): Date {\n return new Date(date.getFullYear(), date.getMonth(), date.getDate());\n}\n\n/**\n * Whether two dates fall on the same calendar day.\n *\n * @param a - First date.\n * @param b - Second date.\n * @returns `true` when year, month and day match.\n */\nfunction isSameDay(a: Date, b: Date): boolean {\n return (\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate()\n );\n}\n\n/**\n * Whether a date is outside the inclusive `[minDate, maxDate]` window.\n *\n * @param date - The candidate day.\n * @param minDate - Optional lower bound.\n * @param maxDate - Optional upper bound.\n * @returns `true` when the date is disabled.\n */\nfunction isOutOfRange(date: Date, minDate?: Date, maxDate?: Date): boolean {\n const day = startOfDay(date).getTime();\n if (minDate && day < startOfDay(minDate).getTime()) return true;\n if (maxDate && day > startOfDay(maxDate).getTime()) return true;\n return false;\n}\n\n/**\n * Compute the 42-cell (6x7) grid of dates covering the given month, padded with\n * leading/trailing days from the adjacent months.\n *\n * @param month - Any date within the target month.\n * @param weekStartsOn - First column of the week.\n * @returns An array of 42 `Date` objects.\n */\nfunction buildGrid(month: Date, weekStartsOn: WeekStart): Date[] {\n const year = month.getFullYear();\n const monthIndex = month.getMonth();\n const firstOfMonth = makeDate(year, monthIndex, 1);\n const firstWeekday = firstOfMonth.getDay();\n const leading = (firstWeekday - weekStartsOn + 7) % 7;\n const start = makeDate(year, monthIndex, 1 - leading);\n\n const cells: Date[] = [];\n for (let i = 0; i < DAYS_IN_GRID; i += 1) {\n cells.push(makeDate(start.getFullYear(), start.getMonth(), start.getDate() + i));\n }\n return cells;\n}\n\n/**\n * Standalone month-grid date picker. Renders a header with the month/year and\n * prev/next buttons, a weekday row, and a 6x7 grid of day buttons. Selection and\n * the visible month can each be controlled or uncontrolled. Grid arithmetic uses\n * plain `Date` math — no external date libraries.\n *\n * Keyboard: arrow keys move focus by day (left/right) or week (up/down), and\n * Enter/Space selects the focused day.\n */\nexport function Calendar({\n value,\n defaultValue,\n onChange,\n month,\n onMonthChange,\n minDate,\n maxDate,\n weekStartsOn = 0,\n className,\n ...props\n}: CalendarProps) {\n const isSelectionControlled = value !== undefined;\n const [internalSelected, setInternalSelected] = useState<Date | undefined>(defaultValue);\n const selected = isSelectionControlled ? value : internalSelected;\n\n const isMonthControlled = month !== undefined;\n const [internalMonth, setInternalMonth] = useState<Date>(() => {\n const base = month ?? value ?? defaultValue ?? new Date();\n return makeDate(base.getFullYear(), base.getMonth(), 1);\n });\n const visibleMonth = isMonthControlled\n ? makeDate(month.getFullYear(), month.getMonth(), 1)\n : internalMonth;\n\n const [focusedDay, setFocusedDay] = useState<number | null>(null);\n const gridRef = useRef<HTMLDivElement | null>(null);\n\n const today = useMemo(() => startOfDay(new Date()), []);\n const cells = useMemo(\n () => buildGrid(visibleMonth, weekStartsOn),\n [visibleMonth, weekStartsOn],\n );\n\n const weekdayLabels = useMemo(() => {\n return Array.from({ length: 7 }, (_, i) => WEEKDAY_LABELS[(i + weekStartsOn) % 7]);\n }, [weekStartsOn]);\n\n const changeMonth = useCallback(\n (offset: number): void => {\n const next = makeDate(visibleMonth.getFullYear(), visibleMonth.getMonth() + offset, 1);\n if (onMonthChange) onMonthChange(next);\n if (!isMonthControlled) setInternalMonth(next);\n },\n [visibleMonth, onMonthChange, isMonthControlled],\n );\n\n const selectDate = useCallback(\n (date: Date): void => {\n if (isOutOfRange(date, minDate, maxDate)) return;\n if (onChange) onChange(date);\n if (!isSelectionControlled) setInternalSelected(date);\n },\n [minDate, maxDate, onChange, isSelectionControlled],\n );\n\n const focusCell = useCallback((index: number): void => {\n const clamped = Math.max(0, Math.min(DAYS_IN_GRID - 1, index));\n setFocusedDay(clamped);\n const grid = gridRef.current;\n if (!grid) return;\n const buttons = grid.querySelectorAll<HTMLButtonElement>(\"button[data-day]\");\n buttons[clamped]?.focus();\n }, []);\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLButtonElement>, index: number): void => {\n switch (event.key) {\n case \"ArrowLeft\":\n event.preventDefault();\n focusCell(index - 1);\n break;\n case \"ArrowRight\":\n event.preventDefault();\n focusCell(index + 1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n focusCell(index - 7);\n break;\n case \"ArrowDown\":\n event.preventDefault();\n focusCell(index + 7);\n break;\n case \"Enter\":\n case \" \":\n event.preventDefault();\n selectDate(cells[index]);\n break;\n default:\n break;\n }\n },\n [focusCell, selectDate, cells],\n );\n\n return (\n <div className={cn(styles.root, className)} {...props}>\n <div className={styles.header}>\n <button\n type=\"button\"\n className={styles.nav}\n aria-label=\"Previous month\"\n onClick={() => changeMonth(-1)}\n >\n {\"‹\"}\n </button>\n <span className={styles.title} aria-live=\"polite\">\n {MONTH_LABELS[visibleMonth.getMonth()]} {visibleMonth.getFullYear()}\n </span>\n <button\n type=\"button\"\n className={styles.nav}\n aria-label=\"Next month\"\n onClick={() => changeMonth(1)}\n >\n {\"›\"}\n </button>\n </div>\n\n <div className={styles.weekdays} role=\"row\">\n {weekdayLabels.map((label) => (\n <span key={label} className={styles.weekday} role=\"columnheader\">\n {label}\n </span>\n ))}\n </div>\n\n <div className={styles.grid} role=\"grid\" ref={gridRef}>\n {cells.map((date, index) => {\n const inMonth = date.getMonth() === visibleMonth.getMonth();\n const isSelected = selected ? isSameDay(date, selected) : false;\n const isToday = isSameDay(date, today);\n const disabled = isOutOfRange(date, minDate, maxDate);\n const isFocusTarget =\n focusedDay === null\n ? inMonth && date.getDate() === 1\n : focusedDay === index;\n return (\n <button\n key={index}\n type=\"button\"\n data-day\n role=\"gridcell\"\n className={cn(\n styles.day,\n !inMonth && styles.outside,\n isSelected && styles.selected,\n isToday && styles.today,\n )}\n aria-pressed={isSelected}\n aria-selected={isSelected}\n aria-current={isToday ? \"date\" : undefined}\n disabled={disabled}\n tabIndex={isFocusTarget ? 0 : -1}\n onClick={() => selectDate(date)}\n onKeyDown={(event) => handleKeyDown(event, index)}\n >\n {date.getDate()}\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Calendar.js","names":[],"sources":["../../../src/components/Calendar/Calendar.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — a date grid is\n * controlled two ways at once — the selected day (value, defaultValue, onChange) and\n * the visible month (month, onMonthChange) — inside the bounds (minDate, maxDate,\n * weekStartsOn). The body builds the grid and owns roving focus and keyboard\n * navigation, which read the same cursor date.\n */\nimport { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { HTMLAttributes, KeyboardEvent } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Calendar.module.css\";\n\nexport type WeekStart = 0 | 1;\n\nexport interface CalendarProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n \"onChange\" | \"defaultValue\"\n> {\n /** Controlled selected date. */\n value?: Date;\n /** Initial selected date for the uncontrolled case. */\n defaultValue?: Date;\n /** Called with the newly selected date. */\n onChange?: (date: Date) => void;\n /** Controlled visible month (any day within it). */\n month?: Date;\n /** Called when the visible month changes (prev/next). */\n onMonthChange?: (month: Date) => void;\n /** Earliest selectable date (inclusive). */\n minDate?: Date;\n /** Latest selectable date (inclusive). */\n maxDate?: Date;\n /** First column of the week — `0` Sunday (default) or `1` Monday. */\n weekStartsOn?: WeekStart;\n}\n\nconst WEEKDAY_LABELS = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\nconst MONTH_LABELS = [\n \"January\",\n \"February\",\n \"March\",\n \"April\",\n \"May\",\n \"June\",\n \"July\",\n \"August\",\n \"September\",\n \"October\",\n \"November\",\n \"December\",\n];\n\nconst DAYS_IN_GRID = 42;\n\n/**\n * Build a `Date` at midnight local time for the given year/month/day.\n *\n * @param year - Full year.\n * @param month - Zero-based month index.\n * @param day - Day of the month.\n * @returns A normalized `Date`.\n */\nfunction makeDate(year: number, month: number, day: number): Date {\n return new Date(year, month, day);\n}\n\n/**\n * Strip the time portion so two dates can be compared by calendar day.\n *\n * @param date - The date to normalize.\n * @returns A new `Date` at local midnight.\n */\nfunction startOfDay(date: Date): Date {\n return new Date(date.getFullYear(), date.getMonth(), date.getDate());\n}\n\n/**\n * Whether two dates fall on the same calendar day.\n *\n * @param a - First date.\n * @param b - Second date.\n * @returns `true` when year, month and day match.\n */\nfunction isSameDay(a: Date, b: Date): boolean {\n return (\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate()\n );\n}\n\n/**\n * Whether a date is outside the inclusive `[minDate, maxDate]` window.\n *\n * @param date - The candidate day.\n * @param minDate - Optional lower bound.\n * @param maxDate - Optional upper bound.\n * @returns `true` when the date is disabled.\n */\nfunction isOutOfRange(date: Date, minDate?: Date, maxDate?: Date): boolean {\n const day = startOfDay(date).getTime();\n if (minDate && day < startOfDay(minDate).getTime()) return true;\n if (maxDate && day > startOfDay(maxDate).getTime()) return true;\n return false;\n}\n\n/**\n * Compute the 42-cell (6x7) grid of dates covering the given month, padded with\n * leading/trailing days from the adjacent months.\n *\n * @param month - Any date within the target month.\n * @param weekStartsOn - First column of the week.\n * @returns An array of 42 `Date` objects.\n */\nfunction buildGrid(month: Date, weekStartsOn: WeekStart): Date[] {\n const year = month.getFullYear();\n const monthIndex = month.getMonth();\n const firstOfMonth = makeDate(year, monthIndex, 1);\n const firstWeekday = firstOfMonth.getDay();\n const leading = (firstWeekday - weekStartsOn + 7) % 7;\n const start = makeDate(year, monthIndex, 1 - leading);\n\n const cells: Date[] = [];\n for (let i = 0; i < DAYS_IN_GRID; i += 1) {\n cells.push(makeDate(start.getFullYear(), start.getMonth(), start.getDate() + i));\n }\n return cells;\n}\n\n/**\n * Standalone month-grid date picker. Renders a header with the month/year and\n * prev/next buttons, a weekday row, and a 6x7 grid of day buttons. Selection and\n * the visible month can each be controlled or uncontrolled. Grid arithmetic uses\n * plain `Date` math — no external date libraries.\n *\n * Keyboard: arrow keys move focus by day (left/right) or week (up/down), and\n * Enter/Space selects the focused day.\n */\nexport function Calendar({\n value,\n defaultValue,\n onChange,\n month,\n onMonthChange,\n minDate,\n maxDate,\n weekStartsOn = 0,\n className,\n ...props\n}: CalendarProps) {\n const isSelectionControlled = value !== undefined;\n const [internalSelected, setInternalSelected] = useState<Date | undefined>(defaultValue);\n const selected = isSelectionControlled ? value : internalSelected;\n\n const isMonthControlled = month !== undefined;\n const [internalMonth, setInternalMonth] = useState<Date>(() => {\n const base = month ?? value ?? defaultValue ?? new Date();\n return makeDate(base.getFullYear(), base.getMonth(), 1);\n });\n const visibleMonth = isMonthControlled\n ? makeDate(month.getFullYear(), month.getMonth(), 1)\n : internalMonth;\n\n const [focusedDay, setFocusedDay] = useState<number | null>(null);\n const gridRef = useRef<HTMLDivElement | null>(null);\n\n const today = useMemo(() => startOfDay(new Date()), []);\n const cells = useMemo(\n () => buildGrid(visibleMonth, weekStartsOn),\n [visibleMonth, weekStartsOn],\n );\n\n const weekdayLabels = useMemo(() => {\n return Array.from({ length: 7 }, (_, i) => WEEKDAY_LABELS[(i + weekStartsOn) % 7]);\n }, [weekStartsOn]);\n\n const changeMonth = useCallback(\n (offset: number): void => {\n const next = makeDate(visibleMonth.getFullYear(), visibleMonth.getMonth() + offset, 1);\n if (onMonthChange) onMonthChange(next);\n if (!isMonthControlled) setInternalMonth(next);\n },\n [visibleMonth, onMonthChange, isMonthControlled],\n );\n\n const selectDate = useCallback(\n (date: Date): void => {\n if (isOutOfRange(date, minDate, maxDate)) return;\n if (onChange) onChange(date);\n if (!isSelectionControlled) setInternalSelected(date);\n },\n [minDate, maxDate, onChange, isSelectionControlled],\n );\n\n const focusCell = useCallback((index: number): void => {\n const clamped = Math.max(0, Math.min(DAYS_IN_GRID - 1, index));\n setFocusedDay(clamped);\n const grid = gridRef.current;\n if (!grid) return;\n const buttons = grid.querySelectorAll<HTMLButtonElement>(\"button[data-day]\");\n buttons[clamped]?.focus();\n }, []);\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLButtonElement>, index: number): void => {\n switch (event.key) {\n case \"ArrowLeft\":\n event.preventDefault();\n focusCell(index - 1);\n break;\n case \"ArrowRight\":\n event.preventDefault();\n focusCell(index + 1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n focusCell(index - 7);\n break;\n case \"ArrowDown\":\n event.preventDefault();\n focusCell(index + 7);\n break;\n case \"Enter\":\n case \" \":\n event.preventDefault();\n selectDate(cells[index]);\n break;\n default:\n break;\n }\n },\n [focusCell, selectDate, cells],\n );\n\n return (\n <div className={cn(styles.root, className)} {...props}>\n <div className={styles.header}>\n <button\n type=\"button\"\n className={styles.nav}\n aria-label=\"Previous month\"\n onClick={() => changeMonth(-1)}\n >\n {\"‹\"}\n </button>\n <span className={styles.title} aria-live=\"polite\">\n {MONTH_LABELS[visibleMonth.getMonth()]} {visibleMonth.getFullYear()}\n </span>\n <button\n type=\"button\"\n className={styles.nav}\n aria-label=\"Next month\"\n onClick={() => changeMonth(1)}\n >\n {\"›\"}\n </button>\n </div>\n\n <div className={styles.weekdays} role=\"row\">\n {weekdayLabels.map((label) => (\n <span key={label} className={styles.weekday} role=\"columnheader\">\n {label}\n </span>\n ))}\n </div>\n\n <div className={styles.grid} role=\"grid\" ref={gridRef}>\n {cells.map((date, index) => {\n const inMonth = date.getMonth() === visibleMonth.getMonth();\n const isSelected = selected ? isSameDay(date, selected) : false;\n const isToday = isSameDay(date, today);\n const disabled = isOutOfRange(date, minDate, maxDate);\n const isFocusTarget =\n focusedDay === null\n ? inMonth && date.getDate() === 1\n : focusedDay === index;\n return (\n <button\n key={index}\n type=\"button\"\n data-day\n role=\"gridcell\"\n className={cn(\n styles.day,\n !inMonth && styles.outside,\n isSelected && styles.selected,\n isToday && styles.today,\n )}\n aria-pressed={isSelected}\n aria-selected={isSelected}\n aria-current={isToday ? \"date\" : undefined}\n disabled={disabled}\n tabIndex={isFocusTarget ? 0 : -1}\n onClick={() => selectDate(date)}\n onKeyDown={(event) => handleKeyDown(event, index)}\n >\n {date.getDate()}\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;AAoCA,IAAM,IAAiB;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;CAAO;AAAK,GACjE,IAAe;CACjB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ,GAEM,IAAe;AAUrB,SAAS,EAAS,GAAc,GAAe,GAAmB;CAC9D,OAAO,IAAI,KAAK,GAAM,GAAO,CAAG;AACpC;AAQA,SAAS,EAAW,GAAkB;CAClC,OAAO,IAAI,KAAK,EAAK,YAAY,GAAG,EAAK,SAAS,GAAG,EAAK,QAAQ,CAAC;AACvE;AASA,SAAS,EAAU,GAAS,GAAkB;CAC1C,OACI,EAAE,YAAY,MAAM,EAAE,YAAY,KAClC,EAAE,SAAS,MAAM,EAAE,SAAS,KAC5B,EAAE,QAAQ,MAAM,EAAE,QAAQ;AAElC;AAUA,SAAS,EAAa,GAAY,GAAgB,GAAyB;CACvE,IAAM,IAAM,EAAW,CAAI,CAAC,CAAC,QAAQ;CAGrC,OADA,GADI,KAAW,IAAM,EAAW,CAAO,CAAC,CAAC,QAAQ,KAC7C,KAAW,IAAM,EAAW,CAAO,CAAC,CAAC,QAAQ;AAErD;AAUA,SAAS,EAAU,GAAa,GAAiC;CAC7D,IAAM,IAAO,EAAM,YAAY,GACzB,IAAa,EAAM,SAAS,GAI5B,IAAQ,EAAS,GAAM,GAAY,KAHpB,EAAS,GAAM,GAAY,CAC3B,CAAA,CAAa,OACjB,IAAe,IAAe,KAAK,CACA,GAE9C,IAAgB,CAAC;CACvB,KAAK,IAAI,IAAI,GAAG,IAAI,GAAc,KAAK,GACnC,EAAM,KAAK,EAAS,EAAM,YAAY,GAAG,EAAM,SAAS,GAAG,EAAM,QAAQ,IAAI,CAAC,CAAC;CAEnF,OAAO;AACX;AAWA,SAAgB,EAAS,EACrB,UACA,iBACA,aACA,UACA,kBACA,YACA,YACA,kBAAe,GACf,cACA,GAAG,KACW;CACd,IAAM,IAAwB,MAAU,KAAA,GAClC,CAAC,GAAkB,KAAuB,EAA2B,CAAY,GACjF,IAAW,IAAwB,IAAQ,GAE3C,IAAoB,MAAU,KAAA,GAC9B,CAAC,GAAe,KAAoB,QAAqB;EAC3D,IAAM,IAAO,KAAS,KAAS,qBAAgB,IAAI,KAAK;EACxD,OAAO,EAAS,EAAK,YAAY,GAAG,EAAK,SAAS,GAAG,CAAC;CAC1D,CAAC,GACK,IAAe,IACf,EAAS,EAAM,YAAY,GAAG,EAAM,SAAS,GAAG,CAAC,IACjD,GAEA,CAAC,GAAY,KAAiB,EAAwB,IAAI,GAC1D,IAAU,EAA8B,IAAI,GAE5C,IAAQ,QAAc,kBAAW,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,GAChD,IAAQ,QACJ,EAAU,GAAc,CAAY,GAC1C,CAAC,GAAc,CAAY,CAC/B,GAEM,IAAgB,QACX,MAAM,KAAK,EAAE,QAAQ,EAAE,IAAI,GAAG,MAAM,GAAgB,IAAI,KAAgB,EAAE,GAClF,CAAC,CAAY,CAAC,GAEX,IAAc,GACf,MAAyB;EACtB,IAAM,IAAO,EAAS,EAAa,YAAY,GAAG,EAAa,SAAS,IAAI,GAAQ,CAAC;EAErF,AADI,KAAe,EAAc,CAAI,GAChC,KAAmB,EAAiB,CAAI;CACjD,GACA;EAAC;EAAc;EAAe;CAAiB,CACnD,GAEM,IAAa,GACd,MAAqB;EACd,EAAa,GAAM,GAAS,CAAO,MACnC,KAAU,EAAS,CAAI,GACtB,KAAuB,EAAoB,CAAI;CACxD,GACA;EAAC;EAAS;EAAS;EAAU;CAAqB,CACtD,GAEM,IAAY,GAAa,MAAwB;EACnD,IAAM,IAAU,KAAK,IAAI,GAAG,KAAK,IAAI,IAAe,GAAG,CAAK,CAAC;EAC7D,EAAc,CAAO;EACrB,IAAM,IAAO,EAAQ;EAChB,KAEL,EADqB,iBAAoC,kBACzD,CAAA,CAAQ,EAAQ,EAAE,MAAM;CAC5B,GAAG,CAAC,CAAC,GAEC,IAAgB,GACjB,GAAyC,MAAwB;EAC9D,QAAQ,EAAM,KAAd;GACI,KAAK;IAED,AADA,EAAM,eAAe,GACrB,EAAU,IAAQ,CAAC;IACnB;GACJ,KAAK;IAED,AADA,EAAM,eAAe,GACrB,EAAU,IAAQ,CAAC;IACnB;GACJ,KAAK;IAED,AADA,EAAM,eAAe,GACrB,EAAU,IAAQ,CAAC;IACnB;GACJ,KAAK;IAED,AADA,EAAM,eAAe,GACrB,EAAU,IAAQ,CAAC;IACnB;GACJ,KAAK;GACL,KAAK;IAED,AADA,EAAM,eAAe,GACrB,EAAW,EAAM,EAAM;IACvB;GACJ,SACI;EACR;CACJ,GACA;EAAC;EAAW;EAAY;CAAK,CACjC;CAEA,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,MAAM,CAAS;EAAG,GAAI;YAAhD;GACI,kBAAC,OAAD;IAAK,WAAW,EAAO;cAAvB;KACI,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,cAAW;MACX,eAAe,EAAY,EAAE;gBAE5B;KACG,CAAA;KACR,kBAAC,QAAD;MAAM,WAAW,EAAO;MAAO,aAAU;gBAAzC;OACK,EAAa,EAAa,SAAS;OAAG;OAAE,EAAa,YAAY;MAChE;;KACN,kBAAC,UAAD;MACI,MAAK;MACL,WAAW,EAAO;MAClB,cAAW;MACX,eAAe,EAAY,CAAC;gBAE3B;KACG,CAAA;IACP;;GAEL,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAU,MAAK;cACjC,EAAc,KAAK,MAChB,kBAAC,QAAD;KAAkB,WAAW,EAAO;KAAS,MAAK;eAC7C;IACC,GAFK,CAEL,CACT;GACA,CAAA;GAEL,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAM,MAAK;IAAO,KAAK;cACzC,EAAM,KAAK,GAAM,MAAU;KACxB,IAAM,IAAU,EAAK,SAAS,MAAM,EAAa,SAAS,GACpD,IAAa,IAAW,EAAU,GAAM,CAAQ,IAAI,IACpD,IAAU,EAAU,GAAM,CAAK,GAC/B,IAAW,EAAa,GAAM,GAAS,CAAO,GAC9C,IACF,MAAe,OACT,KAAW,EAAK,QAAQ,MAAM,IAC9B,MAAe;KACzB,OACI,kBAAC,UAAD;MAEI,MAAK;MACL,YAAA;MACA,MAAK;MACL,WAAW,EACP,EAAO,KACP,CAAC,KAAW,EAAO,SACnB,KAAc,EAAO,UACrB,KAAW,EAAO,KACtB;MACA,gBAAc;MACd,iBAAe;MACf,gBAAc,IAAU,SAAS,KAAA;MACvB;MACV,UAAU,IAAgB,IAAI;MAC9B,eAAe,EAAW,CAAI;MAC9B,YAAY,MAAU,EAAc,GAAO,CAAK;gBAE/C,EAAK,QAAQ;KACV,GAnBC,CAmBD;IAEhB,CAAC;GACA,CAAA;EACJ;;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Carousel.cjs","names":[],"sources":["../../../src/components/Carousel/Carousel.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Carousel.cjs","names":[],"sources":["../../../src/components/Carousel/Carousel.tsx"],"sourcesContent":["/**\n * @tempest-limits function-lines — the body owns the scroll snap position, the\n * swipe, the autoplay timer that must pause on interaction and the dot indicators\n * that read the same index.\n */\nimport { useCallback, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Carousel.module.css\";\n\nexport interface CarouselProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Slides — one rendered at a time. */\n children: ReactNode[];\n /** Wrap around at the ends instead of stopping. Default `false`. */\n loop?: boolean;\n /** Show prev/next arrow buttons. Default `true`. */\n showArrows?: boolean;\n /** Show dot indicators. Default `true`. */\n showDots?: boolean;\n /** Controlled active index. */\n index?: number;\n /** Initial index for uncontrolled use. Default `0`. */\n defaultIndex?: number;\n /** Called whenever the active index changes. */\n onIndexChange?: (index: number) => void;\n}\n\n/**\n * Horizontal content slider showing one slide at a time.\n *\n * - Track translates by the active index (`translateX(-index * 100%)`).\n * - Prev/next arrows (disabled at the ends unless `loop`); dot indicators jump.\n * - Controlled (`index`) or uncontrolled (`defaultIndex`).\n * - Arrow Left/Right on the focused region navigates.\n *\n * @example\n * <Carousel loop>\n * <img src=\"/1.jpg\" alt=\"\" />\n * <img src=\"/2.jpg\" alt=\"\" />\n * <img src=\"/3.jpg\" alt=\"\" />\n * </Carousel>\n */\nexport function Carousel({\n children,\n loop = false,\n showArrows = true,\n showDots = true,\n index,\n defaultIndex = 0,\n onIndexChange,\n className,\n ...props\n}: CarouselProps) {\n const count = children.length;\n const isControlled = index !== undefined;\n const [internalIndex, setInternalIndex] = useState<number>(defaultIndex);\n const current = isControlled ? index : internalIndex;\n\n const goTo = useCallback(\n (next: number): void => {\n let target = next;\n if (target < 0) target = loop ? count - 1 : 0;\n else if (target >= count) target = loop ? 0 : count - 1;\n if (target === current) return;\n if (!isControlled) setInternalIndex(target);\n onIndexChange?.(target);\n },\n [count, loop, current, isControlled, onIndexChange],\n );\n\n const atStart = current <= 0;\n const atEnd = current >= count - 1;\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {\n if (event.key === \"ArrowRight\") {\n event.preventDefault();\n goTo(current + 1);\n } else if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n goTo(current - 1);\n }\n };\n\n return (\n <div\n role=\"region\"\n aria-roledescription=\"carousel\"\n tabIndex={0}\n className={cn(styles.root, className)}\n onKeyDown={handleKeyDown}\n {...props}\n >\n <div className={styles.viewport}>\n <div\n className={styles.track}\n style={{ transform: `translateX(-${current * 100}%)` }}\n >\n {children.map((slide, slideIndex) => (\n <div\n key={slideIndex}\n role=\"group\"\n aria-roledescription=\"slide\"\n aria-label={`${slideIndex + 1} of ${count}`}\n aria-hidden={slideIndex !== current}\n className={styles.slide}\n >\n {slide}\n </div>\n ))}\n </div>\n </div>\n\n {showArrows && count > 1 && (\n <>\n <button\n type=\"button\"\n className={cn(styles.arrow, styles.prev)}\n aria-label=\"Previous slide\"\n disabled={!loop && atStart}\n onClick={() => goTo(current - 1)}\n >\n ‹\n </button>\n <button\n type=\"button\"\n className={cn(styles.arrow, styles.next)}\n aria-label=\"Next slide\"\n disabled={!loop && atEnd}\n onClick={() => goTo(current + 1)}\n >\n ›\n </button>\n </>\n )}\n\n {showDots && count > 1 && (\n <div className={styles.dots} role=\"tablist\" aria-label=\"Slides\">\n {children.map((_, dotIndex) => (\n <button\n key={dotIndex}\n type=\"button\"\n role=\"tab\"\n aria-label={`Go to slide ${dotIndex + 1}`}\n aria-selected={dotIndex === current}\n className={cn(styles.dot, dotIndex === current && styles.dotActive)}\n onClick={() => goTo(dotIndex)}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"+HA0CA,SAAgB,EAAS,CACrB,WACA,OAAO,GACP,aAAa,GACb,WAAW,GACX,QACA,eAAe,EACf,gBACA,YACA,GAAG,GACW,CACd,IAAM,EAAQ,EAAS,OACjB,EAAe,IAAU,IAAA,GACzB,CAAC,EAAe,IAAA,EAAA,EAAA,SAAA,CAAqC,CAAY,EACjE,EAAU,EAAe,EAAQ,EAEjC,GAAA,EAAA,EAAA,YAAA,CACD,GAAuB,CACpB,IAAI,EAAS,EACT,EAAS,EAAG,EAAS,EAAO,EAAQ,EAAI,EACnC,GAAU,IAAO,EAAS,EAAO,EAAI,EAAQ,GAClD,IAAW,IACV,GAAc,EAAiB,CAAM,EAC1C,IAAgB,CAAM,EAC1B,EACA,CAAC,EAAO,EAAM,EAAS,EAAc,CAAa,CACtD,EAEM,EAAU,GAAW,EACrB,EAAQ,GAAW,EAAQ,EAYjC,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CACI,KAAK,SACL,uBAAqB,WACrB,SAAU,EACV,UAAW,EAAA,GAAG,EAAA,QAAO,KAAM,CAAS,EACpC,UAhBe,GAAqD,CACpE,EAAM,MAAQ,cACd,EAAM,eAAe,EACrB,EAAK,EAAU,CAAC,GACT,EAAM,MAAQ,cACrB,EAAM,eAAe,EACrB,EAAK,EAAU,CAAC,EAExB,EASQ,GAAI,WANR,EAQI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,mBACnB,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,UAAW,EAAA,QAAO,MAClB,MAAO,CAAE,UAAW,eAAe,EAAU,IAAI,GAAI,WAEpD,EAAS,KAAK,EAAO,KAClB,EAAA,EAAA,IAAA,CAAC,MAAD,CAEI,KAAK,QACL,uBAAqB,QACrB,aAAY,GAAG,EAAa,EAAE,MAAM,IACpC,cAAa,IAAe,EAC5B,UAAW,EAAA,QAAO,eAEjB,CACA,EARI,CAQJ,CACR,CACA,CAAA,CACJ,CAAA,EAEJ,GAAc,EAAQ,IACnB,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,GAAG,EAAA,QAAO,MAAO,EAAA,QAAO,IAAI,EACvC,aAAW,iBACX,SAAU,CAAC,GAAQ,EACnB,YAAe,EAAK,EAAU,CAAC,WAClC,GAEO,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,GAAG,EAAA,QAAO,MAAO,EAAA,QAAO,IAAI,EACvC,aAAW,aACX,SAAU,CAAC,GAAQ,EACnB,YAAe,EAAK,EAAU,CAAC,WAClC,GAEO,CAAA,CACV,CAAA,CAAA,EAGL,GAAY,EAAQ,IACjB,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAM,KAAK,UAAU,aAAW,kBAClD,EAAS,KAAK,EAAG,KACd,EAAA,EAAA,IAAA,CAAC,SAAD,CAEI,KAAK,SACL,KAAK,MACL,aAAY,eAAe,EAAW,IACtC,gBAAe,IAAa,EAC5B,UAAW,EAAA,GAAG,EAAA,QAAO,IAAK,IAAa,GAAW,EAAA,QAAO,SAAS,EAClE,YAAe,EAAK,CAAQ,CAC/B,EAPQ,CAOR,CACJ,CACA,CAAA,CAER,GAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Carousel.js","names":[],"sources":["../../../src/components/Carousel/Carousel.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Carousel.js","names":[],"sources":["../../../src/components/Carousel/Carousel.tsx"],"sourcesContent":["/**\n * @tempest-limits function-lines — the body owns the scroll snap position, the\n * swipe, the autoplay timer that must pause on interaction and the dot indicators\n * that read the same index.\n */\nimport { useCallback, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Carousel.module.css\";\n\nexport interface CarouselProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Slides — one rendered at a time. */\n children: ReactNode[];\n /** Wrap around at the ends instead of stopping. Default `false`. */\n loop?: boolean;\n /** Show prev/next arrow buttons. Default `true`. */\n showArrows?: boolean;\n /** Show dot indicators. Default `true`. */\n showDots?: boolean;\n /** Controlled active index. */\n index?: number;\n /** Initial index for uncontrolled use. Default `0`. */\n defaultIndex?: number;\n /** Called whenever the active index changes. */\n onIndexChange?: (index: number) => void;\n}\n\n/**\n * Horizontal content slider showing one slide at a time.\n *\n * - Track translates by the active index (`translateX(-index * 100%)`).\n * - Prev/next arrows (disabled at the ends unless `loop`); dot indicators jump.\n * - Controlled (`index`) or uncontrolled (`defaultIndex`).\n * - Arrow Left/Right on the focused region navigates.\n *\n * @example\n * <Carousel loop>\n * <img src=\"/1.jpg\" alt=\"\" />\n * <img src=\"/2.jpg\" alt=\"\" />\n * <img src=\"/3.jpg\" alt=\"\" />\n * </Carousel>\n */\nexport function Carousel({\n children,\n loop = false,\n showArrows = true,\n showDots = true,\n index,\n defaultIndex = 0,\n onIndexChange,\n className,\n ...props\n}: CarouselProps) {\n const count = children.length;\n const isControlled = index !== undefined;\n const [internalIndex, setInternalIndex] = useState<number>(defaultIndex);\n const current = isControlled ? index : internalIndex;\n\n const goTo = useCallback(\n (next: number): void => {\n let target = next;\n if (target < 0) target = loop ? count - 1 : 0;\n else if (target >= count) target = loop ? 0 : count - 1;\n if (target === current) return;\n if (!isControlled) setInternalIndex(target);\n onIndexChange?.(target);\n },\n [count, loop, current, isControlled, onIndexChange],\n );\n\n const atStart = current <= 0;\n const atEnd = current >= count - 1;\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {\n if (event.key === \"ArrowRight\") {\n event.preventDefault();\n goTo(current + 1);\n } else if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n goTo(current - 1);\n }\n };\n\n return (\n <div\n role=\"region\"\n aria-roledescription=\"carousel\"\n tabIndex={0}\n className={cn(styles.root, className)}\n onKeyDown={handleKeyDown}\n {...props}\n >\n <div className={styles.viewport}>\n <div\n className={styles.track}\n style={{ transform: `translateX(-${current * 100}%)` }}\n >\n {children.map((slide, slideIndex) => (\n <div\n key={slideIndex}\n role=\"group\"\n aria-roledescription=\"slide\"\n aria-label={`${slideIndex + 1} of ${count}`}\n aria-hidden={slideIndex !== current}\n className={styles.slide}\n >\n {slide}\n </div>\n ))}\n </div>\n </div>\n\n {showArrows && count > 1 && (\n <>\n <button\n type=\"button\"\n className={cn(styles.arrow, styles.prev)}\n aria-label=\"Previous slide\"\n disabled={!loop && atStart}\n onClick={() => goTo(current - 1)}\n >\n ‹\n </button>\n <button\n type=\"button\"\n className={cn(styles.arrow, styles.next)}\n aria-label=\"Next slide\"\n disabled={!loop && atEnd}\n onClick={() => goTo(current + 1)}\n >\n ›\n </button>\n </>\n )}\n\n {showDots && count > 1 && (\n <div className={styles.dots} role=\"tablist\" aria-label=\"Slides\">\n {children.map((_, dotIndex) => (\n <button\n key={dotIndex}\n type=\"button\"\n role=\"tab\"\n aria-label={`Go to slide ${dotIndex + 1}`}\n aria-selected={dotIndex === current}\n className={cn(styles.dot, dotIndex === current && styles.dotActive)}\n onClick={() => goTo(dotIndex)}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;AA0CA,SAAgB,EAAS,EACrB,aACA,UAAO,IACP,gBAAa,IACb,cAAW,IACX,UACA,kBAAe,GACf,kBACA,cACA,GAAG,KACW;CACd,IAAM,IAAQ,EAAS,QACjB,IAAe,MAAU,KAAA,GACzB,CAAC,GAAe,KAAoB,EAAiB,CAAY,GACjE,IAAU,IAAe,IAAQ,GAEjC,IAAO,GACR,MAAuB;EACpB,IAAI,IAAS;EACb,AAAI,IAAS,IAAG,IAAS,IAAO,IAAQ,IAAI,IACnC,KAAU,MAAO,IAAS,IAAO,IAAI,IAAQ,IAClD,MAAW,MACV,KAAc,EAAiB,CAAM,GAC1C,IAAgB,CAAM;CAC1B,GACA;EAAC;EAAO;EAAM;EAAS;EAAc;CAAa,CACtD,GAEM,IAAU,KAAW,GACrB,IAAQ,KAAW,IAAQ;CAYjC,OACI,kBAAC,OAAD;EACI,MAAK;EACL,wBAAqB;EACrB,UAAU;EACV,WAAW,EAAG,EAAO,MAAM,CAAS;EACpC,YAhBe,MAAqD;GACxE,AAAI,EAAM,QAAQ,gBACd,EAAM,eAAe,GACrB,EAAK,IAAU,CAAC,KACT,EAAM,QAAQ,gBACrB,EAAM,eAAe,GACrB,EAAK,IAAU,CAAC;EAExB;EASQ,GAAI;YANR;GAQI,kBAAC,OAAD;IAAK,WAAW,EAAO;cACnB,kBAAC,OAAD;KACI,WAAW,EAAO;KAClB,OAAO,EAAE,WAAW,eAAe,IAAU,IAAI,IAAI;eAEpD,EAAS,KAAK,GAAO,MAClB,kBAAC,OAAD;MAEI,MAAK;MACL,wBAAqB;MACrB,cAAY,GAAG,IAAa,EAAE,MAAM;MACpC,eAAa,MAAe;MAC5B,WAAW,EAAO;gBAEjB;KACA,GARI,CAQJ,CACR;IACA,CAAA;GACJ,CAAA;GAEJ,KAAc,IAAQ,KACnB,kBAAA,GAAA,EAAA,UAAA,CACI,kBAAC,UAAD;IACI,MAAK;IACL,WAAW,EAAG,EAAO,OAAO,EAAO,IAAI;IACvC,cAAW;IACX,UAAU,CAAC,KAAQ;IACnB,eAAe,EAAK,IAAU,CAAC;cAClC;GAEO,CAAA,GACR,kBAAC,UAAD;IACI,MAAK;IACL,WAAW,EAAG,EAAO,OAAO,EAAO,IAAI;IACvC,cAAW;IACX,UAAU,CAAC,KAAQ;IACnB,eAAe,EAAK,IAAU,CAAC;cAClC;GAEO,CAAA,CACV,EAAA,CAAA;GAGL,KAAY,IAAQ,KACjB,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAM,MAAK;IAAU,cAAW;cAClD,EAAS,KAAK,GAAG,MACd,kBAAC,UAAD;KAEI,MAAK;KACL,MAAK;KACL,cAAY,eAAe,IAAW;KACtC,iBAAe,MAAa;KAC5B,WAAW,EAAG,EAAO,KAAK,MAAa,KAAW,EAAO,SAAS;KAClE,eAAe,EAAK,CAAQ;IAC/B,GAPQ,CAOR,CACJ;GACA,CAAA;EAER;;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chat.cjs","names":[],"sources":["../../../src/components/Chat/Chat.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport { ChatComposer, type ChatComposerHandle } from \"./ChatComposer\";\nimport {\n chatStrings,\n dayLabel,\n groupMessages,\n timeLabel,\n typingLabel,\n type ChatMessage,\n} from \"./chat-groups\";\nimport styles from \"./Chat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface ChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The thread, **oldest first**. Never reordered by the component. */\n messages: readonly ChatMessage[];\n /** Author id treated as \"own\" — decides side, colour and status ticks. */\n currentUserId?: string;\n /** Renders the composer when given. Receives the trimmed text. */\n onSend?: (text: string) => void | Promise<void>;\n /** Enables the retry control on a `\"failed\"` message. */\n onRetry?: (message: ChatMessage) => void;\n /** Names currently typing. One, two or a count is phrased for you. */\n typing?: readonly string[];\n /** Avatar for the first message of a run — an `<Avatar>`, an `<Icon>`. */\n renderAvatar?: (message: ChatMessage) => ReactNode;\n /** Rendered above the thread, inside the panel. */\n header?: ReactNode;\n /** Shown when there are no messages. */\n emptyState?: ReactNode;\n /** Gap that still keeps consecutive messages in one run. Default 5 min. */\n groupWindowMs?: number;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Reference instant for \"Hoje\"/\"Ontem\". Default: now, at render time. */\n now?: number;\n /** Placeholder for the composer. */\n placeholder?: string;\n /** Extra controls inside the composer, before the send button. */\n composerActions?: ReactNode;\n /** Disable the composer — no permission, thread archived, offline. */\n composerDisabled?: boolean;\n /** Called when `onSend` rejects. The draft stays in the field either way. */\n onSendError?: (error: unknown) => void;\n}\n\n/** Status glyph and label for an outgoing message. */\nconst STATUS_GLYPH: Record<NonNullable<ChatMessage[\"status\"]>, string> = {\n sending: \"◌\",\n sent: \"✓\",\n read: \"✓✓\",\n failed: \"!\",\n};\n\n/**\n * A message thread: grouped by author and by day, own messages on one side, with\n * delivery state, a typing indicator and an optional composer.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and\n * emits intent (`onSend`, `onRetry`). Where messages come from — REST, the SDK's\n * `createWebSocket`, an SSE stream — and how an optimistic insert is done stay with\n * the app, because those differ per backend and a component that assumed one would\n * be wrong for most.\n *\n * It works as a comment thread too: that is the same component with\n * `currentUserId` set and no `typing`.\n *\n * @example\n * <Chat\n * messages={messages}\n * currentUserId={me.id}\n * typing={typingNames}\n * onSend={(text) => send({ text })}\n * onRetry={(message) => resend(message.id)}\n * />\n */\nexport function Chat({\n messages,\n currentUserId,\n onSend,\n onRetry,\n typing = [],\n renderAvatar,\n header,\n emptyState,\n groupWindowMs,\n locale = \"pt-BR\",\n now,\n placeholder,\n composerActions,\n composerDisabled,\n onSendError,\n className,\n ...rest\n}: ChatProps) {\n const strings = chatStrings(locale);\n const sections = groupMessages({ messages, currentUserId, windowMs: groupWindowMs });\n const composer = useRef<ChatComposerHandle | null>(null);\n const thread = useRef<HTMLDivElement | null>(null);\n const stuckToBottom = useRef(true);\n\n /**\n * Remember whether the reader is at the bottom, before the next batch lands.\n *\n * A thread that always scrolls to the newest message yanks somebody out of the\n * history they were reading, every time anyone types. So the jump only happens\n * when they were already at the bottom — the same rule every chat app converges\n * on. The 48px slack covers a partially visible last row.\n */\n const trackPosition = (): void => {\n const node = thread.current;\n if (!node) return;\n const distance = node.scrollHeight - node.scrollTop - node.clientHeight;\n stuckToBottom.current = distance < 48;\n };\n\n useLayoutEffect(() => {\n const node = thread.current;\n if (!node || !stuckToBottom.current) return;\n node.scrollTop = node.scrollHeight;\n }, [messages, typing]);\n\n useEffect(() => {\n const node = thread.current;\n if (!node) return;\n node.scrollTop = node.scrollHeight;\n // Mount lands on the newest message; from then on `trackPosition` decides.\n }, []);\n\n const typingText = typingLabel(typing, locale);\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {header && <header className={styles.header}>{header}</header>}\n\n <div\n ref={thread}\n className={styles.thread}\n onScroll={trackPosition}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n tabIndex={0}\n aria-label={strings.thread}\n >\n {messages.length === 0\n ? (emptyState ?? <EmptyState title={strings.empty} />)\n : sections.map((section) =>\n section.kind === \"day\" ? (\n <div key={section.key} className={styles.day}>\n <span className={styles.dayLabel}>\n {dayLabel(section.date, { locale, now })}\n </span>\n </div>\n ) : (\n <div\n key={section.key}\n className={cn(styles.run, section.own && styles.ownRun)}\n >\n {renderAvatar && (\n <div className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(section.messages[0])}\n </div>\n )}\n <div className={styles.runBody}>\n <span className={styles.author}>\n {section.own\n ? strings.you\n : (section.authorName ?? section.authorId)}\n </span>\n <ul className={styles.bubbles}>\n {section.messages.map((message) => (\n <li key={message.id} className={styles.bubbleRow}>\n <div\n className={cn(\n styles.bubble,\n section.own && styles.ownBubble,\n message.status === \"failed\" &&\n styles.failedBubble,\n )}\n >\n <div className={styles.body}>\n {message.body}\n </div>\n <div className={styles.meta}>\n <time\n dateTime={new Date(\n message.sentAt,\n ).toISOString()}\n >\n {timeLabel(message.sentAt, locale)}\n </time>\n {section.own && message.status && (\n <span\n className={cn(\n styles.status,\n message.status === \"failed\" &&\n styles.statusFailed,\n )}\n title={strings[message.status]}\n >\n <span aria-hidden=\"true\">\n {STATUS_GLYPH[message.status]}\n </span>\n <VisuallyHidden>\n {strings[message.status]}\n </VisuallyHidden>\n </span>\n )}\n </div>\n </div>\n {message.status === \"failed\" && onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </li>\n ))}\n </ul>\n </div>\n </div>\n ),\n )}\n </div>\n\n {typingText && (\n <p className={styles.typing} aria-live=\"polite\">\n {typingText}\n </p>\n )}\n\n {onSend && (\n <ChatComposer\n ref={composer}\n onSend={onSend}\n locale={locale}\n placeholder={placeholder}\n actions={composerActions}\n disabled={composerDisabled}\n onError={onSendError}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Chat.cjs","names":[],"sources":["../../../src/components/Chat/Chat.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — message grouping needs\n * more context than a list does: currentUserId to decide sides, groupWindowMs and\n * now to decide where a bubble group breaks, typing for the indicator. The remaining\n * props are the composer's (placeholder, composerActions, composerDisabled, onSend,\n * onSendError) and the slots (renderAvatar, header, emptyState).\n */\nimport { useEffect, useLayoutEffect, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport { ChatComposer, type ChatComposerHandle } from \"./ChatComposer\";\nimport {\n chatStrings,\n dayLabel,\n groupMessages,\n timeLabel,\n typingLabel,\n type ChatMessage,\n} from \"./chat-groups\";\nimport styles from \"./Chat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface ChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The thread, **oldest first**. Never reordered by the component. */\n messages: readonly ChatMessage[];\n /** Author id treated as \"own\" — decides side, colour and status ticks. */\n currentUserId?: string;\n /** Renders the composer when given. Receives the trimmed text. */\n onSend?: (text: string) => void | Promise<void>;\n /** Enables the retry control on a `\"failed\"` message. */\n onRetry?: (message: ChatMessage) => void;\n /** Names currently typing. One, two or a count is phrased for you. */\n typing?: readonly string[];\n /** Avatar for the first message of a run — an `<Avatar>`, an `<Icon>`. */\n renderAvatar?: (message: ChatMessage) => ReactNode;\n /** Rendered above the thread, inside the panel. */\n header?: ReactNode;\n /** Shown when there are no messages. */\n emptyState?: ReactNode;\n /** Gap that still keeps consecutive messages in one run. Default 5 min. */\n groupWindowMs?: number;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Reference instant for \"Hoje\"/\"Ontem\". Default: now, at render time. */\n now?: number;\n /** Placeholder for the composer. */\n placeholder?: string;\n /** Extra controls inside the composer, before the send button. */\n composerActions?: ReactNode;\n /** Disable the composer — no permission, thread archived, offline. */\n composerDisabled?: boolean;\n /** Called when `onSend` rejects. The draft stays in the field either way. */\n onSendError?: (error: unknown) => void;\n}\n\n/** Status glyph and label for an outgoing message. */\nconst STATUS_GLYPH: Record<NonNullable<ChatMessage[\"status\"]>, string> = {\n sending: \"◌\",\n sent: \"✓\",\n read: \"✓✓\",\n failed: \"!\",\n};\n\n/**\n * A message thread: grouped by author and by day, own messages on one side, with\n * delivery state, a typing indicator and an optional composer.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and\n * emits intent (`onSend`, `onRetry`). Where messages come from — REST, the SDK's\n * `createWebSocket`, an SSE stream — and how an optimistic insert is done stay with\n * the app, because those differ per backend and a component that assumed one would\n * be wrong for most.\n *\n * It works as a comment thread too: that is the same component with\n * `currentUserId` set and no `typing`.\n *\n * @example\n * <Chat\n * messages={messages}\n * currentUserId={me.id}\n * typing={typingNames}\n * onSend={(text) => send({ text })}\n * onRetry={(message) => resend(message.id)}\n * />\n */\nexport function Chat({\n messages,\n currentUserId,\n onSend,\n onRetry,\n typing = [],\n renderAvatar,\n header,\n emptyState,\n groupWindowMs,\n locale = \"pt-BR\",\n now,\n placeholder,\n composerActions,\n composerDisabled,\n onSendError,\n className,\n ...rest\n}: ChatProps) {\n const strings = chatStrings(locale);\n const sections = groupMessages({ messages, currentUserId, windowMs: groupWindowMs });\n const composer = useRef<ChatComposerHandle | null>(null);\n const thread = useRef<HTMLDivElement | null>(null);\n const stuckToBottom = useRef(true);\n\n /**\n * Remember whether the reader is at the bottom, before the next batch lands.\n *\n * A thread that always scrolls to the newest message yanks somebody out of the\n * history they were reading, every time anyone types. So the jump only happens\n * when they were already at the bottom — the same rule every chat app converges\n * on. The 48px slack covers a partially visible last row.\n */\n const trackPosition = (): void => {\n const node = thread.current;\n if (!node) return;\n const distance = node.scrollHeight - node.scrollTop - node.clientHeight;\n stuckToBottom.current = distance < 48;\n };\n\n useLayoutEffect(() => {\n const node = thread.current;\n if (!node || !stuckToBottom.current) return;\n node.scrollTop = node.scrollHeight;\n }, [messages, typing]);\n\n useEffect(() => {\n const node = thread.current;\n if (!node) return;\n node.scrollTop = node.scrollHeight;\n // Mount lands on the newest message; from then on `trackPosition` decides.\n }, []);\n\n const typingText = typingLabel(typing, locale);\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {header && <header className={styles.header}>{header}</header>}\n\n <div\n ref={thread}\n className={styles.thread}\n onScroll={trackPosition}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n tabIndex={0}\n aria-label={strings.thread}\n >\n {messages.length === 0\n ? (emptyState ?? <EmptyState title={strings.empty} />)\n : sections.map((section) =>\n section.kind === \"day\" ? (\n <div key={section.key} className={styles.day}>\n <span className={styles.dayLabel}>\n {dayLabel(section.date, { locale, now })}\n </span>\n </div>\n ) : (\n <div\n key={section.key}\n className={cn(styles.run, section.own && styles.ownRun)}\n >\n {renderAvatar && (\n <div className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(section.messages[0])}\n </div>\n )}\n <div className={styles.runBody}>\n <span className={styles.author}>\n {section.own\n ? strings.you\n : (section.authorName ?? section.authorId)}\n </span>\n <ul className={styles.bubbles}>\n {section.messages.map((message) => (\n <li key={message.id} className={styles.bubbleRow}>\n <div\n className={cn(\n styles.bubble,\n section.own && styles.ownBubble,\n message.status === \"failed\" &&\n styles.failedBubble,\n )}\n >\n <div className={styles.body}>\n {message.body}\n </div>\n <div className={styles.meta}>\n <time\n dateTime={new Date(\n message.sentAt,\n ).toISOString()}\n >\n {timeLabel(message.sentAt, locale)}\n </time>\n {section.own && message.status && (\n <span\n className={cn(\n styles.status,\n message.status === \"failed\" &&\n styles.statusFailed,\n )}\n title={strings[message.status]}\n >\n <span aria-hidden=\"true\">\n {STATUS_GLYPH[message.status]}\n </span>\n <VisuallyHidden>\n {strings[message.status]}\n </VisuallyHidden>\n </span>\n )}\n </div>\n </div>\n {message.status === \"failed\" && onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </li>\n ))}\n </ul>\n </div>\n </div>\n ),\n )}\n </div>\n\n {typingText && (\n <p className={styles.typing} aria-live=\"polite\">\n {typingText}\n </p>\n )}\n\n {onSend && (\n <ChatComposer\n ref={composer}\n onSend={onSend}\n locale={locale}\n placeholder={placeholder}\n actions={composerActions}\n disabled={composerDisabled}\n onError={onSendError}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|