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":"Scheduler.js","names":[],"sources":["../../../src/components/Scheduler/Scheduler.tsx"],"sourcesContent":["import { type HTMLAttributes, type ReactNode, useEffect, useMemo, useState } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport {\n dayRange,\n type DayWindow,\n fractionOfWindow,\n hourMarks,\n isSameDay,\n layoutAllDay,\n layoutEvents,\n type SchedulerEvent,\n} from \"./scheduler-layout\";\nimport styles from \"./Scheduler.module.css\";\n\nexport interface SchedulerProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Events to place. Instants are read in the browser's local time. */\n events: readonly SchedulerEvent[];\n /** Any day within the range to show. Default: today. */\n anchor?: Date;\n /** How many consecutive days to render. `1` is a day view, `7` a week. Default `7`. */\n days?: number;\n /** First visible hour, `0`–`23`. Default `8`. */\n startHour?: number;\n /** Last visible hour, `1`–`24`. Default `20`. */\n endHour?: number;\n /** Minutes a click on empty space snaps to. Default `30`. */\n snapMinutes?: number;\n /** Called when an event is activated by click, `Enter` or `Space`. */\n onEventClick?: (event: SchedulerEvent) => void;\n /** Called with the snapped instant when empty space is clicked. */\n onSlotClick?: (start: Date) => void;\n /** Render an event's contents. Defaults to its title and start time. */\n renderEvent?: (event: SchedulerEvent) => ReactNode;\n /** Locale for the day and hour labels. Default `\"pt-BR\"`. */\n locale?: string;\n /** Draw the current-time line. Default `true`. */\n showCurrentTime?: boolean;\n /** Fixed \"now\" for the indicator. Default: the real clock, ticking each minute. */\n now?: Date;\n}\n\n/** Minutes between ticks of the current-time indicator. */\nconst TICK_MS = 60_000;\n\n/**\n * An agenda: events placed on a time grid across consecutive days.\n *\n * `Calendar` is a date *picker* — it answers \"which day?\". This answers \"what is on\n * those days, and when\", which needs a different structure entirely: a vertical time\n * axis, events sized by duration, and overlapping events sitting side by side.\n *\n * That last part is the one worth naming. Overlapping events are grouped into\n * clusters of mutual overlap and every event in a cluster shares one column count,\n * so widths line up; a column is reused the moment it frees, so `9–10`, `9–10`,\n * `10–11` takes two columns and not three. The layout is pure and lives in\n * `scheduler-layout.ts`.\n *\n * Times are local. An event crossing midnight is split into both day columns, and\n * the day range is built by incrementing the calendar day, so a DST boundary does\n * not duplicate or skip a date.\n *\n * @example\n * <Scheduler\n * events={bookings}\n * days={7}\n * startHour={7}\n * endHour={21}\n * onEventClick={(e) => open(e.id)}\n * onSlotClick={(start) => createAt(start)}\n * />\n */\nexport function Scheduler({\n events,\n anchor,\n days: dayCount = 7,\n startHour = 8,\n endHour = 20,\n snapMinutes = 30,\n onEventClick,\n onSlotClick,\n renderEvent,\n locale = \"pt-BR\",\n showCurrentTime = true,\n now,\n className,\n ...rest\n}: SchedulerProps) {\n const [clock, setClock] = useState<Date>(() => now ?? new Date());\n\n /**\n * Keep the current-time line moving.\n *\n * Skipped entirely when `now` is supplied: that is the hook tests and demos use\n * to be deterministic, and a timer would fight it.\n */\n useEffect(() => {\n if (now || !showCurrentTime) return;\n const id = setInterval(() => setClock(new Date()), TICK_MS);\n return () => clearInterval(id);\n }, [now, showCurrentTime]);\n\n const reference = now ?? clock;\n\n const window = useMemo<DayWindow>(\n () => ({ startMinute: startHour * 60, endMinute: endHour * 60 }),\n [startHour, endHour],\n );\n\n /**\n * The calendar day the range starts from, as a stable string.\n *\n * `reference` ticks every minute when the current-time line is live. Depending on\n * the Date itself would re-slice the day range — and therefore relayout every\n * event — once a minute; depending on the day only recomputes at midnight.\n */\n const anchorDay = (anchor ?? reference).toDateString();\n const dayList = useMemo(() => dayRange(new Date(anchorDay), dayCount), [anchorDay, dayCount]);\n\n const placed = useMemo(\n () => layoutEvents({ events, days: dayList, window }),\n [events, dayList, window],\n );\n const allDay = useMemo(() => layoutAllDay({ events, days: dayList }), [events, dayList]);\n const marks = useMemo(() => hourMarks(window), [window]);\n\n const dayLabel = new Intl.DateTimeFormat(locale, { weekday: \"short\", day: \"numeric\" });\n const timeLabel = new Intl.DateTimeFormat(locale, { hour: \"2-digit\", minute: \"2-digit\" });\n\n /** Turn a click's vertical position within a day column into a snapped instant. */\n const slotFromClick = (day: Date, event: React.MouseEvent<HTMLDivElement>): Date => {\n const rect = event.currentTarget.getBoundingClientRect();\n const fraction = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0;\n const raw = window.startMinute + fraction * (window.endMinute - window.startMinute);\n const snapped = Math.round(raw / snapMinutes) * snapMinutes;\n const clamped = Math.max(window.startMinute, Math.min(window.endMinute, snapped));\n const result = new Date(day);\n result.setHours(0, clamped, 0, 0);\n return result;\n };\n\n const currentFraction = showCurrentTime ? fractionOfWindow(reference, window) : null;\n const todayIndex = dayList.findIndex((day) => isSameDay(day, reference));\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n <div\n className={styles.head}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n >\n <span className={styles.gutterHead} />\n {dayList.map((day, index) => (\n <span\n key={day.toISOString()}\n className={cn(styles.dayHead, index === todayIndex && styles.today)}\n >\n {dayLabel.format(day)}\n </span>\n ))}\n </div>\n\n {allDay.length > 0 && (\n <div\n className={styles.allDayLane}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n >\n <span className={styles.gutterHead}>Dia inteiro</span>\n <div className={styles.allDayTrack}>\n {allDay.map(({ event, dayIndex, span }) => (\n <button\n key={event.id}\n type=\"button\"\n className={styles.allDayEvent}\n style={{ gridColumn: `${dayIndex + 1} / span ${span}` }}\n onClick={() => onEventClick?.(event)}\n disabled={!onEventClick}\n >\n {event.title}\n </button>\n ))}\n </div>\n </div>\n )}\n\n {/*\n * Focusable because it scrolls vertically: a scroll region that cannot be\n * focused is unreachable by keyboard, which is what `axe`'s\n * `scrollable-region-focusable` rule is about. It needs a name too, or the\n * new tab stop would announce nothing — but `group`, not `region`: a named\n * `region` is a landmark, and two schedulers on one page would then be two\n * identically-named landmarks (`landmark-unique`).\n */}\n <div\n className={styles.body}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n tabIndex={0}\n role=\"group\"\n aria-label=\"Grade de horários\"\n >\n <div className={styles.gutter}>\n {marks.map((minute) => (\n <span\n key={minute}\n className={styles.hourLabel}\n style={{\n top: `${((minute - window.startMinute) / (window.endMinute - window.startMinute)) * 100}%`,\n }}\n >\n {timeLabel.format(\n new Date(2026, 0, 1, Math.floor(minute / 60), minute % 60),\n )}\n </span>\n ))}\n </div>\n\n {/*\n * Not `role=\"grid\"`: that requires `row` children, and the events are\n * siblings of the day columns inside one CSS grid — a `row` wrapper\n * would stop the columns being grid items and collapse the layout.\n * A labelled group per day is the honest structure anyway: a screen\n * reader tabs the event buttons and the group name supplies the day.\n */}\n <div className={styles.grid} aria-label=\"Agenda\">\n {dayList.map((day, index) => (\n <div\n key={day.toISOString()}\n className={cn(\n styles.dayColumn,\n index === todayIndex && styles.todayColumn,\n )}\n role=\"group\"\n aria-label={dayLabel.format(day)}\n onClick={\n onSlotClick\n ? (event) => {\n // Only empty space creates: a click that\n // landed on an event is that event's.\n if (event.target !== event.currentTarget) return;\n onSlotClick(slotFromClick(day, event));\n }\n : undefined\n }\n >\n {marks.map((minute) => (\n <span\n key={minute}\n className={styles.hourLine}\n style={{\n top: `${((minute - window.startMinute) / (window.endMinute - window.startMinute)) * 100}%`,\n }}\n />\n ))}\n </div>\n ))}\n\n {placed.map((item) => (\n <button\n key={`${item.event.id}-${item.dayIndex}`}\n type=\"button\"\n className={styles.event}\n style={{\n gridColumn: item.dayIndex + 1,\n top: `${item.top * 100}%`,\n height: `${item.height * 100}%`,\n left: `${(item.column / item.columns) * 100}%`,\n width: `${(1 / item.columns) * 100}%`,\n }}\n onClick={() => onEventClick?.(item.event)}\n disabled={!onEventClick}\n title={`${item.event.title} — ${timeLabel.format(item.event.start)}`}\n >\n {renderEvent ? (\n renderEvent(item.event)\n ) : (\n <>\n <span className={styles.eventTime}>\n {timeLabel.format(item.event.start)}\n </span>\n <span className={styles.eventTitle}>{item.event.title}</span>\n </>\n )}\n </button>\n ))}\n\n {currentFraction !== null && todayIndex >= 0 && (\n <span\n className={styles.nowLine}\n style={{ top: `${currentFraction * 100}%` }}\n aria-hidden\n data-testid=\"scheduler-now\"\n />\n )}\n </div>\n </div>\n </div>\n );\n}\n\nexport type { SchedulerEvent };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Scheduler.js","names":[],"sources":["../../../src/components/Scheduler/Scheduler.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the grid is defined by\n * anchor, days, startHour, endHour and snapMinutes; the content by events,\n * renderEvent, onEventClick and onSlotClick; the reading by locale, showCurrentTime\n * and now. The body lays out overlapping events into columns, which needs the whole\n * day's events at once.\n */\nimport { type HTMLAttributes, type ReactNode, useEffect, useMemo, useState } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport {\n dayRange,\n type DayWindow,\n fractionOfWindow,\n hourMarks,\n isSameDay,\n layoutAllDay,\n layoutEvents,\n type SchedulerEvent,\n} from \"./scheduler-layout\";\nimport styles from \"./Scheduler.module.css\";\n\nexport interface SchedulerProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Events to place. Instants are read in the browser's local time. */\n events: readonly SchedulerEvent[];\n /** Any day within the range to show. Default: today. */\n anchor?: Date;\n /** How many consecutive days to render. `1` is a day view, `7` a week. Default `7`. */\n days?: number;\n /** First visible hour, `0`–`23`. Default `8`. */\n startHour?: number;\n /** Last visible hour, `1`–`24`. Default `20`. */\n endHour?: number;\n /** Minutes a click on empty space snaps to. Default `30`. */\n snapMinutes?: number;\n /** Called when an event is activated by click, `Enter` or `Space`. */\n onEventClick?: (event: SchedulerEvent) => void;\n /** Called with the snapped instant when empty space is clicked. */\n onSlotClick?: (start: Date) => void;\n /** Render an event's contents. Defaults to its title and start time. */\n renderEvent?: (event: SchedulerEvent) => ReactNode;\n /** Locale for the day and hour labels. Default `\"pt-BR\"`. */\n locale?: string;\n /** Draw the current-time line. Default `true`. */\n showCurrentTime?: boolean;\n /** Fixed \"now\" for the indicator. Default: the real clock, ticking each minute. */\n now?: Date;\n}\n\n/** Minutes between ticks of the current-time indicator. */\nconst TICK_MS = 60_000;\n\n/**\n * An agenda: events placed on a time grid across consecutive days.\n *\n * `Calendar` is a date *picker* — it answers \"which day?\". This answers \"what is on\n * those days, and when\", which needs a different structure entirely: a vertical time\n * axis, events sized by duration, and overlapping events sitting side by side.\n *\n * That last part is the one worth naming. Overlapping events are grouped into\n * clusters of mutual overlap and every event in a cluster shares one column count,\n * so widths line up; a column is reused the moment it frees, so `9–10`, `9–10`,\n * `10–11` takes two columns and not three. The layout is pure and lives in\n * `scheduler-layout.ts`.\n *\n * Times are local. An event crossing midnight is split into both day columns, and\n * the day range is built by incrementing the calendar day, so a DST boundary does\n * not duplicate or skip a date.\n *\n * @example\n * <Scheduler\n * events={bookings}\n * days={7}\n * startHour={7}\n * endHour={21}\n * onEventClick={(e) => open(e.id)}\n * onSlotClick={(start) => createAt(start)}\n * />\n */\nexport function Scheduler({\n events,\n anchor,\n days: dayCount = 7,\n startHour = 8,\n endHour = 20,\n snapMinutes = 30,\n onEventClick,\n onSlotClick,\n renderEvent,\n locale = \"pt-BR\",\n showCurrentTime = true,\n now,\n className,\n ...rest\n}: SchedulerProps) {\n const [clock, setClock] = useState<Date>(() => now ?? new Date());\n\n /**\n * Keep the current-time line moving.\n *\n * Skipped entirely when `now` is supplied: that is the hook tests and demos use\n * to be deterministic, and a timer would fight it.\n */\n useEffect(() => {\n if (now || !showCurrentTime) return;\n const id = setInterval(() => setClock(new Date()), TICK_MS);\n return () => clearInterval(id);\n }, [now, showCurrentTime]);\n\n const reference = now ?? clock;\n\n const window = useMemo<DayWindow>(\n () => ({ startMinute: startHour * 60, endMinute: endHour * 60 }),\n [startHour, endHour],\n );\n\n /**\n * The calendar day the range starts from, as a stable string.\n *\n * `reference` ticks every minute when the current-time line is live. Depending on\n * the Date itself would re-slice the day range — and therefore relayout every\n * event — once a minute; depending on the day only recomputes at midnight.\n */\n const anchorDay = (anchor ?? reference).toDateString();\n const dayList = useMemo(() => dayRange(new Date(anchorDay), dayCount), [anchorDay, dayCount]);\n\n const placed = useMemo(\n () => layoutEvents({ events, days: dayList, window }),\n [events, dayList, window],\n );\n const allDay = useMemo(() => layoutAllDay({ events, days: dayList }), [events, dayList]);\n const marks = useMemo(() => hourMarks(window), [window]);\n\n const dayLabel = new Intl.DateTimeFormat(locale, { weekday: \"short\", day: \"numeric\" });\n const timeLabel = new Intl.DateTimeFormat(locale, { hour: \"2-digit\", minute: \"2-digit\" });\n\n /** Turn a click's vertical position within a day column into a snapped instant. */\n const slotFromClick = (day: Date, event: React.MouseEvent<HTMLDivElement>): Date => {\n const rect = event.currentTarget.getBoundingClientRect();\n const fraction = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0;\n const raw = window.startMinute + fraction * (window.endMinute - window.startMinute);\n const snapped = Math.round(raw / snapMinutes) * snapMinutes;\n const clamped = Math.max(window.startMinute, Math.min(window.endMinute, snapped));\n const result = new Date(day);\n result.setHours(0, clamped, 0, 0);\n return result;\n };\n\n const currentFraction = showCurrentTime ? fractionOfWindow(reference, window) : null;\n const todayIndex = dayList.findIndex((day) => isSameDay(day, reference));\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n <div\n className={styles.head}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n >\n <span className={styles.gutterHead} />\n {dayList.map((day, index) => (\n <span\n key={day.toISOString()}\n className={cn(styles.dayHead, index === todayIndex && styles.today)}\n >\n {dayLabel.format(day)}\n </span>\n ))}\n </div>\n\n {allDay.length > 0 && (\n <div\n className={styles.allDayLane}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n >\n <span className={styles.gutterHead}>Dia inteiro</span>\n <div className={styles.allDayTrack}>\n {allDay.map(({ event, dayIndex, span }) => (\n <button\n key={event.id}\n type=\"button\"\n className={styles.allDayEvent}\n style={{ gridColumn: `${dayIndex + 1} / span ${span}` }}\n onClick={() => onEventClick?.(event)}\n disabled={!onEventClick}\n >\n {event.title}\n </button>\n ))}\n </div>\n </div>\n )}\n\n {/*\n * Focusable because it scrolls vertically: a scroll region that cannot be\n * focused is unreachable by keyboard, which is what `axe`'s\n * `scrollable-region-focusable` rule is about. It needs a name too, or the\n * new tab stop would announce nothing — but `group`, not `region`: a named\n * `region` is a landmark, and two schedulers on one page would then be two\n * identically-named landmarks (`landmark-unique`).\n */}\n <div\n className={styles.body}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n tabIndex={0}\n role=\"group\"\n aria-label=\"Grade de horários\"\n >\n <div className={styles.gutter}>\n {marks.map((minute) => (\n <span\n key={minute}\n className={styles.hourLabel}\n style={{\n top: `${((minute - window.startMinute) / (window.endMinute - window.startMinute)) * 100}%`,\n }}\n >\n {timeLabel.format(\n new Date(2026, 0, 1, Math.floor(minute / 60), minute % 60),\n )}\n </span>\n ))}\n </div>\n\n {/*\n * Not `role=\"grid\"`: that requires `row` children, and the events are\n * siblings of the day columns inside one CSS grid — a `row` wrapper\n * would stop the columns being grid items and collapse the layout.\n * A labelled group per day is the honest structure anyway: a screen\n * reader tabs the event buttons and the group name supplies the day.\n */}\n <div className={styles.grid} aria-label=\"Agenda\">\n {dayList.map((day, index) => (\n <div\n key={day.toISOString()}\n className={cn(\n styles.dayColumn,\n index === todayIndex && styles.todayColumn,\n )}\n role=\"group\"\n aria-label={dayLabel.format(day)}\n onClick={\n onSlotClick\n ? (event) => {\n // Only empty space creates: a click that\n // landed on an event is that event's.\n if (event.target !== event.currentTarget) return;\n onSlotClick(slotFromClick(day, event));\n }\n : undefined\n }\n >\n {marks.map((minute) => (\n <span\n key={minute}\n className={styles.hourLine}\n style={{\n top: `${((minute - window.startMinute) / (window.endMinute - window.startMinute)) * 100}%`,\n }}\n />\n ))}\n </div>\n ))}\n\n {placed.map((item) => (\n <button\n key={`${item.event.id}-${item.dayIndex}`}\n type=\"button\"\n className={styles.event}\n style={{\n gridColumn: item.dayIndex + 1,\n top: `${item.top * 100}%`,\n height: `${item.height * 100}%`,\n left: `${(item.column / item.columns) * 100}%`,\n width: `${(1 / item.columns) * 100}%`,\n }}\n onClick={() => onEventClick?.(item.event)}\n disabled={!onEventClick}\n title={`${item.event.title} — ${timeLabel.format(item.event.start)}`}\n >\n {renderEvent ? (\n renderEvent(item.event)\n ) : (\n <>\n <span className={styles.eventTime}>\n {timeLabel.format(item.event.start)}\n </span>\n <span className={styles.eventTitle}>{item.event.title}</span>\n </>\n )}\n </button>\n ))}\n\n {currentFraction !== null && todayIndex >= 0 && (\n <span\n className={styles.nowLine}\n style={{ top: `${currentFraction * 100}%` }}\n aria-hidden\n data-testid=\"scheduler-now\"\n />\n )}\n </div>\n </div>\n </div>\n );\n}\n\nexport type { SchedulerEvent };\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sidebar.cjs","names":[],"sources":["../../../src/components/Sidebar/Sidebar.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Sidebar.cjs","names":[],"sources":["../../../src/components/Sidebar/Sidebar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — header, items, footer are the three regions,\n * value/onChange the selection, and collapsed/width/collapsedWidth the two layout\n * modes it switches between.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Sidebar.module.css\";\n\nexport interface SidebarItem {\n key: string;\n label: ReactNode;\n icon?: ReactNode;\n badge?: ReactNode;\n disabled?: boolean;\n href?: string;\n}\n\nexport interface SidebarProps extends Omit<HTMLAttributes<HTMLElement>, \"onChange\"> {\n /** Top slot — typically the logo + brand. */\n header?: ReactNode;\n /** Navigation items. */\n items: SidebarItem[];\n /** Active item key. */\n value?: string;\n /** Fires when an item is clicked. Receives the item's `key`. */\n onChange?: (key: string) => void;\n /** Bottom slot — typically settings/profile/logout. */\n footer?: ReactNode;\n /** Collapsed mode — only icons visible. Default `false`. */\n collapsed?: boolean;\n /** Width when expanded, in pixels or any CSS length. Default `240px`. */\n width?: number | string;\n /** Width when collapsed, in pixels or any CSS length. Default `64px`. */\n collapsedWidth?: number | string;\n}\n\n/**\n * Desktop sidebar navigation. Pair with `<Show above=\"md\">` and a `Drawer`\n * for mobile.\n *\n * @example\n * const [tab, setTab] = useState(\"home\");\n * <Show above=\"md\">\n * <Sidebar\n * header={<Brand />}\n * items={[{ key: \"home\", label: \"Home\", icon: <Home /> }]}\n * value={tab}\n * onChange={setTab}\n * />\n * </Show>\n */\nexport function Sidebar({\n header,\n items,\n value,\n onChange,\n footer,\n collapsed = false,\n width = 240,\n collapsedWidth = 64,\n className,\n style,\n ...props\n}: SidebarProps) {\n const finalWidth =\n typeof (collapsed ? collapsedWidth : width) === \"number\"\n ? `${collapsed ? collapsedWidth : width}px`\n : collapsed\n ? collapsedWidth\n : width;\n return (\n <aside\n className={cn(styles.sidebar, collapsed && styles.collapsed, className)}\n style={{ width: finalWidth, ...style }}\n {...props}\n >\n {header && <div className={styles.header}>{header}</div>}\n <nav className={styles.nav} aria-label=\"Navegação lateral\">\n {items.map((item) => {\n const active = item.key === value;\n return (\n <button\n key={item.key}\n type=\"button\"\n className={cn(styles.item, active && styles.active)}\n aria-current={active ? \"page\" : undefined}\n disabled={item.disabled}\n onClick={() => onChange?.(item.key)}\n title={\n collapsed && typeof item.label === \"string\" ? item.label : undefined\n }\n >\n {item.icon && <span className={styles.icon}>{item.icon}</span>}\n {!collapsed && <span className={styles.label}>{item.label}</span>}\n {!collapsed && item.badge !== undefined && (\n <span className={styles.badge}>{item.badge}</span>\n )}\n </button>\n );\n })}\n </nav>\n {footer && <div className={styles.footer}>{footer}</div>}\n </aside>\n );\n}\n"],"mappings":"2GAoDA,SAAgB,EAAQ,CACpB,SACA,QACA,QACA,WACA,SACA,YAAY,GACZ,QAAQ,IACR,iBAAiB,GACjB,YACA,QACA,GAAG,GACU,CACb,IAAM,EACF,OAAQ,EAAY,EAAiB,IAAW,SAC1C,GAAG,EAAY,EAAiB,EAAM,IACtC,EACE,EACA,EACZ,OACI,EAAA,EAAA,KAAA,CAAC,QAAD,CACI,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,GAAa,EAAA,QAAO,UAAW,CAAS,EACtE,MAAO,CAAE,MAAO,EAAY,GAAG,CAAM,EACrC,GAAI,WAHR,CAKK,IAAU,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,gBAAS,CAAY,CAAA,GACvD,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,IAAK,aAAW,6BAClC,EAAM,IAAK,GAAS,CACjB,IAAM,EAAS,EAAK,MAAQ,EAC5B,OACI,EAAA,EAAA,KAAA,CAAC,SAAD,CAEI,KAAK,SACL,UAAW,EAAA,GAAG,EAAA,QAAO,KAAM,GAAU,EAAA,QAAO,MAAM,EAClD,eAAc,EAAS,OAAS,IAAA,GAChC,SAAU,EAAK,SACf,YAAe,IAAW,EAAK,GAAG,EAClC,MACI,GAAa,OAAO,EAAK,OAAU,SAAW,EAAK,MAAQ,IAAA,YARnE,CAWK,EAAK,OAAQ,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,cAAO,EAAK,IAAW,CAAA,EAC5D,CAAC,IAAa,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,eAAQ,EAAK,KAAY,CAAA,EAC/D,CAAC,GAAa,EAAK,QAAU,IAAA,KAC1B,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,eAAQ,EAAK,KAAY,CAAA,CAEjD,GAfC,EAAK,GAeN,CAEhB,CAAC,CACA,CAAA,EACJ,IAAU,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,gBAAS,CAAY,CAAA,CACpD,GAEf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sidebar.js","names":[],"sources":["../../../src/components/Sidebar/Sidebar.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Sidebar.js","names":[],"sources":["../../../src/components/Sidebar/Sidebar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — header, items, footer are the three regions,\n * value/onChange the selection, and collapsed/width/collapsedWidth the two layout\n * modes it switches between.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Sidebar.module.css\";\n\nexport interface SidebarItem {\n key: string;\n label: ReactNode;\n icon?: ReactNode;\n badge?: ReactNode;\n disabled?: boolean;\n href?: string;\n}\n\nexport interface SidebarProps extends Omit<HTMLAttributes<HTMLElement>, \"onChange\"> {\n /** Top slot — typically the logo + brand. */\n header?: ReactNode;\n /** Navigation items. */\n items: SidebarItem[];\n /** Active item key. */\n value?: string;\n /** Fires when an item is clicked. Receives the item's `key`. */\n onChange?: (key: string) => void;\n /** Bottom slot — typically settings/profile/logout. */\n footer?: ReactNode;\n /** Collapsed mode — only icons visible. Default `false`. */\n collapsed?: boolean;\n /** Width when expanded, in pixels or any CSS length. Default `240px`. */\n width?: number | string;\n /** Width when collapsed, in pixels or any CSS length. Default `64px`. */\n collapsedWidth?: number | string;\n}\n\n/**\n * Desktop sidebar navigation. Pair with `<Show above=\"md\">` and a `Drawer`\n * for mobile.\n *\n * @example\n * const [tab, setTab] = useState(\"home\");\n * <Show above=\"md\">\n * <Sidebar\n * header={<Brand />}\n * items={[{ key: \"home\", label: \"Home\", icon: <Home /> }]}\n * value={tab}\n * onChange={setTab}\n * />\n * </Show>\n */\nexport function Sidebar({\n header,\n items,\n value,\n onChange,\n footer,\n collapsed = false,\n width = 240,\n collapsedWidth = 64,\n className,\n style,\n ...props\n}: SidebarProps) {\n const finalWidth =\n typeof (collapsed ? collapsedWidth : width) === \"number\"\n ? `${collapsed ? collapsedWidth : width}px`\n : collapsed\n ? collapsedWidth\n : width;\n return (\n <aside\n className={cn(styles.sidebar, collapsed && styles.collapsed, className)}\n style={{ width: finalWidth, ...style }}\n {...props}\n >\n {header && <div className={styles.header}>{header}</div>}\n <nav className={styles.nav} aria-label=\"Navegação lateral\">\n {items.map((item) => {\n const active = item.key === value;\n return (\n <button\n key={item.key}\n type=\"button\"\n className={cn(styles.item, active && styles.active)}\n aria-current={active ? \"page\" : undefined}\n disabled={item.disabled}\n onClick={() => onChange?.(item.key)}\n title={\n collapsed && typeof item.label === \"string\" ? item.label : undefined\n }\n >\n {item.icon && <span className={styles.icon}>{item.icon}</span>}\n {!collapsed && <span className={styles.label}>{item.label}</span>}\n {!collapsed && item.badge !== undefined && (\n <span className={styles.badge}>{item.badge}</span>\n )}\n </button>\n );\n })}\n </nav>\n {footer && <div className={styles.footer}>{footer}</div>}\n </aside>\n );\n}\n"],"mappings":";;;;AAoDA,SAAgB,EAAQ,EACpB,WACA,UACA,UACA,aACA,WACA,eAAY,IACZ,WAAQ,KACR,oBAAiB,IACjB,cACA,UACA,GAAG,KACU;CACb,IAAM,IACF,QAAQ,IAAY,IAAiB,MAAW,WAC1C,GAAG,IAAY,IAAiB,EAAM,MACtC,IACE,IACA;CACZ,OACI,kBAAC,SAAD;EACI,WAAW,EAAG,EAAO,SAAS,KAAa,EAAO,WAAW,CAAS;EACtE,OAAO;GAAE,OAAO;GAAY,GAAG;EAAM;EACrC,GAAI;YAHR;GAKK,KAAU,kBAAC,OAAD;IAAK,WAAW,EAAO;cAAS;GAAY,CAAA;GACvD,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAK,cAAW;cAClC,EAAM,KAAK,MAAS;KACjB,IAAM,IAAS,EAAK,QAAQ;KAC5B,OACI,kBAAC,UAAD;MAEI,MAAK;MACL,WAAW,EAAG,EAAO,MAAM,KAAU,EAAO,MAAM;MAClD,gBAAc,IAAS,SAAS,KAAA;MAChC,UAAU,EAAK;MACf,eAAe,IAAW,EAAK,GAAG;MAClC,OACI,KAAa,OAAO,EAAK,SAAU,WAAW,EAAK,QAAQ,KAAA;gBARnE;OAWK,EAAK,QAAQ,kBAAC,QAAD;QAAM,WAAW,EAAO;kBAAO,EAAK;OAAW,CAAA;OAC5D,CAAC,KAAa,kBAAC,QAAD;QAAM,WAAW,EAAO;kBAAQ,EAAK;OAAY,CAAA;OAC/D,CAAC,KAAa,EAAK,UAAU,KAAA,KAC1B,kBAAC,QAAD;QAAM,WAAW,EAAO;kBAAQ,EAAK;OAAY,CAAA;MAEjD;QAfC,EAAK,GAeN;IAEhB,CAAC;GACA,CAAA;GACJ,KAAU,kBAAC,OAAD;IAAK,WAAW,EAAO;cAAS;GAAY,CAAA;EACpD;;AAEf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SignaturePad.cjs","names":[],"sources":["../../../src/components/SignaturePad/SignaturePad.tsx"],"sourcesContent":["import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from \"react\";\nimport type { PointerEvent as ReactPointerEvent } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Button } from \"../Button\";\nimport styles from \"./SignaturePad.module.css\";\n\n/** A single sampled point of a stroke, in CSS pixels relative to the canvas. */\ninterface Point {\n x: number;\n y: number;\n}\n\n/** Imperative handle exposed through `ref`. */\nexport interface SignaturePadHandle {\n /** Drop every stroke. */\n clear: () => void;\n /** Drop the last stroke. */\n undo: () => void;\n /** `true` while nothing has been drawn. */\n isEmpty: () => boolean;\n /** Export as a data URL. Returns `\"\"` when the canvas is unavailable. */\n toDataURL: (type?: string, quality?: number) => string;\n /** Export as a `Blob` — what you actually upload. `null` when unavailable. */\n toBlob: (type?: string, quality?: number) => Promise<Blob | null>;\n}\n\nexport interface SignaturePadProps {\n /** Drawing surface width in CSS pixels. Default `400`. */\n width?: number;\n /** Drawing surface height in CSS pixels. Default `160`. */\n height?: number;\n /**\n * Stroke color. Defaults to the canvas' computed `color`, which the\n * stylesheet binds to `--tempest-text` — so the signature follows the theme\n * (including dark mode) instead of being hardcoded black.\n */\n penColor?: string;\n /** Stroke width in CSS pixels. Default `2`. */\n penWidth?: number;\n /** Blocks drawing and dims the surface. */\n disabled?: boolean;\n /** Accessible name of the surface. Default `\"Signature\"`. */\n label?: string;\n /** Called on the first move of each stroke. */\n onBegin?: () => void;\n /** Called when a stroke ends, with the current image as a data URL. */\n onEnd?: (dataUrl: string) => void;\n /** Called whenever the emptiness changes — wire it to a submit button. */\n onEmptyChange?: (isEmpty: boolean) => void;\n /** Renders the Clear/Undo buttons. Default `true`. */\n showActions?: boolean;\n clearLabel?: string;\n undoLabel?: string;\n className?: string;\n}\n\n/**\n * Signature capture on a canvas — the \"sign here\" field of a delivery receipt,\n * a service order, a term of acceptance.\n *\n * Strokes are kept as point lists and the canvas is **redrawn** from them, which\n * is what makes `undo` possible at all: a canvas holds pixels, not history, so\n * removing the last stroke means replaying the rest.\n *\n * The backing store is scaled by `devicePixelRatio`, so the line is crisp on a\n * phone instead of the blurry 1x bitmap a naive canvas produces.\n *\n * @example\n * ```tsx\n * const pad = useRef<SignaturePadHandle>(null);\n * const [empty, setEmpty] = useState(true);\n *\n * async function submit() {\n * const blob = await pad.current?.toBlob(\"image/png\");\n * if (blob) await api.upload(\"/receipts/42/signature\", blob);\n * }\n *\n * <SignaturePad ref={pad} label=\"Assinatura do cliente\" onEmptyChange={setEmpty} />\n * <Button disabled={empty} onClick={submit}>Enviar</Button>\n * ```\n */\nexport const SignaturePad = forwardRef<SignaturePadHandle, SignaturePadProps>(function SignaturePad(\n {\n width = 400,\n height = 160,\n penColor,\n penWidth = 2,\n disabled = false,\n label = \"Signature\",\n onBegin,\n onEnd,\n onEmptyChange,\n showActions = true,\n clearLabel = \"Clear\",\n undoLabel = \"Undo\",\n className,\n },\n ref,\n) {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const strokesRef = useRef<Point[][]>([]);\n const drawingRef = useRef(false);\n const [isEmpty, setIsEmpty] = useState(true);\n\n const resolvePenColor = useCallback((): string => {\n if (penColor) return penColor;\n const canvas = canvasRef.current;\n if (!canvas || typeof window === \"undefined\") return \"#101828\";\n return window.getComputedStyle(canvas).color || \"#101828\";\n }, [penColor]);\n\n /**\n * Repaint the whole surface from the stroke list, honouring the pixel ratio.\n *\n * A one-point stroke (a tap) gets a line back to its own origin: `stroke()`\n * over a path with a single point paints nothing, so without it a tap would\n * silently produce no dot while still counting as \"not empty\".\n */\n const redraw = useCallback((): void => {\n const canvas = canvasRef.current;\n const context = canvas?.getContext(\"2d\");\n if (!canvas || !context) return;\n\n const ratio = typeof window === \"undefined\" ? 1 : window.devicePixelRatio || 1;\n canvas.width = width * ratio;\n canvas.height = height * ratio;\n context.setTransform(ratio, 0, 0, ratio, 0, 0);\n context.clearRect(0, 0, width, height);\n\n context.lineWidth = penWidth;\n context.lineCap = \"round\";\n context.lineJoin = \"round\";\n context.strokeStyle = resolvePenColor();\n\n for (const stroke of strokesRef.current) {\n if (stroke.length === 0) continue;\n context.beginPath();\n context.moveTo(stroke[0].x, stroke[0].y);\n for (const point of stroke.slice(1)) {\n context.lineTo(point.x, point.y);\n }\n if (stroke.length === 1) context.lineTo(stroke[0].x, stroke[0].y);\n context.stroke();\n }\n }, [height, penWidth, resolvePenColor, width]);\n\n useEffect(() => {\n redraw();\n }, [redraw]);\n\n const setEmptiness = useCallback(\n (next: boolean): void => {\n setIsEmpty((current) => {\n if (current !== next) onEmptyChange?.(next);\n return next;\n });\n },\n [onEmptyChange],\n );\n\n const pointFrom = useCallback((event: ReactPointerEvent<HTMLCanvasElement>): Point => {\n const rect = canvasRef.current?.getBoundingClientRect();\n return {\n x: event.clientX - (rect?.left ?? 0),\n y: event.clientY - (rect?.top ?? 0),\n };\n }, []);\n\n const handlePointerDown = useCallback(\n (event: ReactPointerEvent<HTMLCanvasElement>): void => {\n if (disabled) return;\n drawingRef.current = true;\n strokesRef.current = [...strokesRef.current, [pointFrom(event)]];\n canvasRef.current?.setPointerCapture?.(event.pointerId);\n onBegin?.();\n setEmptiness(false);\n redraw();\n },\n [disabled, onBegin, pointFrom, redraw, setEmptiness],\n );\n\n const handlePointerMove = useCallback(\n (event: ReactPointerEvent<HTMLCanvasElement>): void => {\n if (!drawingRef.current || disabled) return;\n const strokes = strokesRef.current;\n strokes[strokes.length - 1].push(pointFrom(event));\n redraw();\n },\n [disabled, pointFrom, redraw],\n );\n\n const handlePointerUp = useCallback(\n (event: ReactPointerEvent<HTMLCanvasElement>): void => {\n if (!drawingRef.current) return;\n drawingRef.current = false;\n canvasRef.current?.releasePointerCapture?.(event.pointerId);\n onEnd?.(canvasRef.current?.toDataURL() ?? \"\");\n },\n [onEnd],\n );\n\n const clear = useCallback((): void => {\n strokesRef.current = [];\n setEmptiness(true);\n redraw();\n }, [redraw, setEmptiness]);\n\n const undo = useCallback((): void => {\n strokesRef.current = strokesRef.current.slice(0, -1);\n setEmptiness(strokesRef.current.length === 0);\n redraw();\n }, [redraw, setEmptiness]);\n\n useImperativeHandle(\n ref,\n () => ({\n clear,\n undo,\n isEmpty: () => strokesRef.current.length === 0,\n toDataURL: (type, quality) => canvasRef.current?.toDataURL(type, quality) ?? \"\",\n toBlob: (type, quality) =>\n new Promise((resolve) => {\n const canvas = canvasRef.current;\n if (!canvas?.toBlob) {\n resolve(null);\n return;\n }\n canvas.toBlob((blob) => resolve(blob), type, quality);\n }),\n }),\n [clear, undo],\n );\n\n return (\n <div className={cn(styles.pad, disabled && styles.disabled, className)}>\n <canvas\n ref={canvasRef}\n role=\"img\"\n aria-label={label}\n aria-disabled={disabled || undefined}\n className={styles.canvas}\n style={{ width, height, touchAction: \"none\" }}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerUp}\n onPointerLeave={handlePointerUp}\n />\n {showActions ? (\n <div className={styles.actions}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={undo}\n disabled={disabled || isEmpty}\n >\n {undoLabel}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={clear}\n disabled={disabled || isEmpty}\n >\n {clearLabel}\n </Button>\n </div>\n ) : null}\n </div>\n );\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SignaturePad.cjs","names":[],"sources":["../../../src/components/SignaturePad/SignaturePad.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the canvas (width,\n * height, penColor, penWidth), the lifecycle an app needs to enable its save button\n * (onBegin, onEnd, onEmptyChange) and the built-in actions with their copy\n * (showActions, clearLabel, undoLabel, label). The body is the stroke buffer plus\n * undo history, which are the same array.\n */\nimport { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from \"react\";\nimport type { PointerEvent as ReactPointerEvent } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Button } from \"../Button\";\nimport styles from \"./SignaturePad.module.css\";\n\n/** A single sampled point of a stroke, in CSS pixels relative to the canvas. */\ninterface Point {\n x: number;\n y: number;\n}\n\n/** Imperative handle exposed through `ref`. */\nexport interface SignaturePadHandle {\n /** Drop every stroke. */\n clear: () => void;\n /** Drop the last stroke. */\n undo: () => void;\n /** `true` while nothing has been drawn. */\n isEmpty: () => boolean;\n /** Export as a data URL. Returns `\"\"` when the canvas is unavailable. */\n toDataURL: (type?: string, quality?: number) => string;\n /** Export as a `Blob` — what you actually upload. `null` when unavailable. */\n toBlob: (type?: string, quality?: number) => Promise<Blob | null>;\n}\n\nexport interface SignaturePadProps {\n /** Drawing surface width in CSS pixels. Default `400`. */\n width?: number;\n /** Drawing surface height in CSS pixels. Default `160`. */\n height?: number;\n /**\n * Stroke color. Defaults to the canvas' computed `color`, which the\n * stylesheet binds to `--tempest-text` — so the signature follows the theme\n * (including dark mode) instead of being hardcoded black.\n */\n penColor?: string;\n /** Stroke width in CSS pixels. Default `2`. */\n penWidth?: number;\n /** Blocks drawing and dims the surface. */\n disabled?: boolean;\n /** Accessible name of the surface. Default `\"Signature\"`. */\n label?: string;\n /** Called on the first move of each stroke. */\n onBegin?: () => void;\n /** Called when a stroke ends, with the current image as a data URL. */\n onEnd?: (dataUrl: string) => void;\n /** Called whenever the emptiness changes — wire it to a submit button. */\n onEmptyChange?: (isEmpty: boolean) => void;\n /** Renders the Clear/Undo buttons. Default `true`. */\n showActions?: boolean;\n clearLabel?: string;\n undoLabel?: string;\n className?: string;\n}\n\n/**\n * Signature capture on a canvas — the \"sign here\" field of a delivery receipt,\n * a service order, a term of acceptance.\n *\n * Strokes are kept as point lists and the canvas is **redrawn** from them, which\n * is what makes `undo` possible at all: a canvas holds pixels, not history, so\n * removing the last stroke means replaying the rest.\n *\n * The backing store is scaled by `devicePixelRatio`, so the line is crisp on a\n * phone instead of the blurry 1x bitmap a naive canvas produces.\n *\n * @example\n * ```tsx\n * const pad = useRef<SignaturePadHandle>(null);\n * const [empty, setEmpty] = useState(true);\n *\n * async function submit() {\n * const blob = await pad.current?.toBlob(\"image/png\");\n * if (blob) await api.upload(\"/receipts/42/signature\", blob);\n * }\n *\n * <SignaturePad ref={pad} label=\"Assinatura do cliente\" onEmptyChange={setEmpty} />\n * <Button disabled={empty} onClick={submit}>Enviar</Button>\n * ```\n */\nexport const SignaturePad = forwardRef<SignaturePadHandle, SignaturePadProps>(function SignaturePad(\n {\n width = 400,\n height = 160,\n penColor,\n penWidth = 2,\n disabled = false,\n label = \"Signature\",\n onBegin,\n onEnd,\n onEmptyChange,\n showActions = true,\n clearLabel = \"Clear\",\n undoLabel = \"Undo\",\n className,\n },\n ref,\n) {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const strokesRef = useRef<Point[][]>([]);\n const drawingRef = useRef(false);\n const [isEmpty, setIsEmpty] = useState(true);\n\n const resolvePenColor = useCallback((): string => {\n if (penColor) return penColor;\n const canvas = canvasRef.current;\n if (!canvas || typeof window === \"undefined\") return \"#101828\";\n return window.getComputedStyle(canvas).color || \"#101828\";\n }, [penColor]);\n\n /**\n * Repaint the whole surface from the stroke list, honouring the pixel ratio.\n *\n * A one-point stroke (a tap) gets a line back to its own origin: `stroke()`\n * over a path with a single point paints nothing, so without it a tap would\n * silently produce no dot while still counting as \"not empty\".\n */\n const redraw = useCallback((): void => {\n const canvas = canvasRef.current;\n const context = canvas?.getContext(\"2d\");\n if (!canvas || !context) return;\n\n const ratio = typeof window === \"undefined\" ? 1 : window.devicePixelRatio || 1;\n canvas.width = width * ratio;\n canvas.height = height * ratio;\n context.setTransform(ratio, 0, 0, ratio, 0, 0);\n context.clearRect(0, 0, width, height);\n\n context.lineWidth = penWidth;\n context.lineCap = \"round\";\n context.lineJoin = \"round\";\n context.strokeStyle = resolvePenColor();\n\n for (const stroke of strokesRef.current) {\n if (stroke.length === 0) continue;\n context.beginPath();\n context.moveTo(stroke[0].x, stroke[0].y);\n for (const point of stroke.slice(1)) {\n context.lineTo(point.x, point.y);\n }\n if (stroke.length === 1) context.lineTo(stroke[0].x, stroke[0].y);\n context.stroke();\n }\n }, [height, penWidth, resolvePenColor, width]);\n\n useEffect(() => {\n redraw();\n }, [redraw]);\n\n const setEmptiness = useCallback(\n (next: boolean): void => {\n setIsEmpty((current) => {\n if (current !== next) onEmptyChange?.(next);\n return next;\n });\n },\n [onEmptyChange],\n );\n\n const pointFrom = useCallback((event: ReactPointerEvent<HTMLCanvasElement>): Point => {\n const rect = canvasRef.current?.getBoundingClientRect();\n return {\n x: event.clientX - (rect?.left ?? 0),\n y: event.clientY - (rect?.top ?? 0),\n };\n }, []);\n\n const handlePointerDown = useCallback(\n (event: ReactPointerEvent<HTMLCanvasElement>): void => {\n if (disabled) return;\n drawingRef.current = true;\n strokesRef.current = [...strokesRef.current, [pointFrom(event)]];\n canvasRef.current?.setPointerCapture?.(event.pointerId);\n onBegin?.();\n setEmptiness(false);\n redraw();\n },\n [disabled, onBegin, pointFrom, redraw, setEmptiness],\n );\n\n const handlePointerMove = useCallback(\n (event: ReactPointerEvent<HTMLCanvasElement>): void => {\n if (!drawingRef.current || disabled) return;\n const strokes = strokesRef.current;\n strokes[strokes.length - 1].push(pointFrom(event));\n redraw();\n },\n [disabled, pointFrom, redraw],\n );\n\n const handlePointerUp = useCallback(\n (event: ReactPointerEvent<HTMLCanvasElement>): void => {\n if (!drawingRef.current) return;\n drawingRef.current = false;\n canvasRef.current?.releasePointerCapture?.(event.pointerId);\n onEnd?.(canvasRef.current?.toDataURL() ?? \"\");\n },\n [onEnd],\n );\n\n const clear = useCallback((): void => {\n strokesRef.current = [];\n setEmptiness(true);\n redraw();\n }, [redraw, setEmptiness]);\n\n const undo = useCallback((): void => {\n strokesRef.current = strokesRef.current.slice(0, -1);\n setEmptiness(strokesRef.current.length === 0);\n redraw();\n }, [redraw, setEmptiness]);\n\n useImperativeHandle(\n ref,\n () => ({\n clear,\n undo,\n isEmpty: () => strokesRef.current.length === 0,\n toDataURL: (type, quality) => canvasRef.current?.toDataURL(type, quality) ?? \"\",\n toBlob: (type, quality) =>\n new Promise((resolve) => {\n const canvas = canvasRef.current;\n if (!canvas?.toBlob) {\n resolve(null);\n return;\n }\n canvas.toBlob((blob) => resolve(blob), type, quality);\n }),\n }),\n [clear, undo],\n );\n\n return (\n <div className={cn(styles.pad, disabled && styles.disabled, className)}>\n <canvas\n ref={canvasRef}\n role=\"img\"\n aria-label={label}\n aria-disabled={disabled || undefined}\n className={styles.canvas}\n style={{ width, height, touchAction: \"none\" }}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerUp}\n onPointerLeave={handlePointerUp}\n />\n {showActions ? (\n <div className={styles.actions}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={undo}\n disabled={disabled || isEmpty}\n >\n {undoLabel}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={clear}\n disabled={disabled || isEmpty}\n >\n {clearLabel}\n </Button>\n </div>\n ) : null}\n </div>\n );\n});\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SignaturePad.js","names":[],"sources":["../../../src/components/SignaturePad/SignaturePad.tsx"],"sourcesContent":["import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from \"react\";\nimport type { PointerEvent as ReactPointerEvent } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Button } from \"../Button\";\nimport styles from \"./SignaturePad.module.css\";\n\n/** A single sampled point of a stroke, in CSS pixels relative to the canvas. */\ninterface Point {\n x: number;\n y: number;\n}\n\n/** Imperative handle exposed through `ref`. */\nexport interface SignaturePadHandle {\n /** Drop every stroke. */\n clear: () => void;\n /** Drop the last stroke. */\n undo: () => void;\n /** `true` while nothing has been drawn. */\n isEmpty: () => boolean;\n /** Export as a data URL. Returns `\"\"` when the canvas is unavailable. */\n toDataURL: (type?: string, quality?: number) => string;\n /** Export as a `Blob` — what you actually upload. `null` when unavailable. */\n toBlob: (type?: string, quality?: number) => Promise<Blob | null>;\n}\n\nexport interface SignaturePadProps {\n /** Drawing surface width in CSS pixels. Default `400`. */\n width?: number;\n /** Drawing surface height in CSS pixels. Default `160`. */\n height?: number;\n /**\n * Stroke color. Defaults to the canvas' computed `color`, which the\n * stylesheet binds to `--tempest-text` — so the signature follows the theme\n * (including dark mode) instead of being hardcoded black.\n */\n penColor?: string;\n /** Stroke width in CSS pixels. Default `2`. */\n penWidth?: number;\n /** Blocks drawing and dims the surface. */\n disabled?: boolean;\n /** Accessible name of the surface. Default `\"Signature\"`. */\n label?: string;\n /** Called on the first move of each stroke. */\n onBegin?: () => void;\n /** Called when a stroke ends, with the current image as a data URL. */\n onEnd?: (dataUrl: string) => void;\n /** Called whenever the emptiness changes — wire it to a submit button. */\n onEmptyChange?: (isEmpty: boolean) => void;\n /** Renders the Clear/Undo buttons. Default `true`. */\n showActions?: boolean;\n clearLabel?: string;\n undoLabel?: string;\n className?: string;\n}\n\n/**\n * Signature capture on a canvas — the \"sign here\" field of a delivery receipt,\n * a service order, a term of acceptance.\n *\n * Strokes are kept as point lists and the canvas is **redrawn** from them, which\n * is what makes `undo` possible at all: a canvas holds pixels, not history, so\n * removing the last stroke means replaying the rest.\n *\n * The backing store is scaled by `devicePixelRatio`, so the line is crisp on a\n * phone instead of the blurry 1x bitmap a naive canvas produces.\n *\n * @example\n * ```tsx\n * const pad = useRef<SignaturePadHandle>(null);\n * const [empty, setEmpty] = useState(true);\n *\n * async function submit() {\n * const blob = await pad.current?.toBlob(\"image/png\");\n * if (blob) await api.upload(\"/receipts/42/signature\", blob);\n * }\n *\n * <SignaturePad ref={pad} label=\"Assinatura do cliente\" onEmptyChange={setEmpty} />\n * <Button disabled={empty} onClick={submit}>Enviar</Button>\n * ```\n */\nexport const SignaturePad = forwardRef<SignaturePadHandle, SignaturePadProps>(function SignaturePad(\n {\n width = 400,\n height = 160,\n penColor,\n penWidth = 2,\n disabled = false,\n label = \"Signature\",\n onBegin,\n onEnd,\n onEmptyChange,\n showActions = true,\n clearLabel = \"Clear\",\n undoLabel = \"Undo\",\n className,\n },\n ref,\n) {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const strokesRef = useRef<Point[][]>([]);\n const drawingRef = useRef(false);\n const [isEmpty, setIsEmpty] = useState(true);\n\n const resolvePenColor = useCallback((): string => {\n if (penColor) return penColor;\n const canvas = canvasRef.current;\n if (!canvas || typeof window === \"undefined\") return \"#101828\";\n return window.getComputedStyle(canvas).color || \"#101828\";\n }, [penColor]);\n\n /**\n * Repaint the whole surface from the stroke list, honouring the pixel ratio.\n *\n * A one-point stroke (a tap) gets a line back to its own origin: `stroke()`\n * over a path with a single point paints nothing, so without it a tap would\n * silently produce no dot while still counting as \"not empty\".\n */\n const redraw = useCallback((): void => {\n const canvas = canvasRef.current;\n const context = canvas?.getContext(\"2d\");\n if (!canvas || !context) return;\n\n const ratio = typeof window === \"undefined\" ? 1 : window.devicePixelRatio || 1;\n canvas.width = width * ratio;\n canvas.height = height * ratio;\n context.setTransform(ratio, 0, 0, ratio, 0, 0);\n context.clearRect(0, 0, width, height);\n\n context.lineWidth = penWidth;\n context.lineCap = \"round\";\n context.lineJoin = \"round\";\n context.strokeStyle = resolvePenColor();\n\n for (const stroke of strokesRef.current) {\n if (stroke.length === 0) continue;\n context.beginPath();\n context.moveTo(stroke[0].x, stroke[0].y);\n for (const point of stroke.slice(1)) {\n context.lineTo(point.x, point.y);\n }\n if (stroke.length === 1) context.lineTo(stroke[0].x, stroke[0].y);\n context.stroke();\n }\n }, [height, penWidth, resolvePenColor, width]);\n\n useEffect(() => {\n redraw();\n }, [redraw]);\n\n const setEmptiness = useCallback(\n (next: boolean): void => {\n setIsEmpty((current) => {\n if (current !== next) onEmptyChange?.(next);\n return next;\n });\n },\n [onEmptyChange],\n );\n\n const pointFrom = useCallback((event: ReactPointerEvent<HTMLCanvasElement>): Point => {\n const rect = canvasRef.current?.getBoundingClientRect();\n return {\n x: event.clientX - (rect?.left ?? 0),\n y: event.clientY - (rect?.top ?? 0),\n };\n }, []);\n\n const handlePointerDown = useCallback(\n (event: ReactPointerEvent<HTMLCanvasElement>): void => {\n if (disabled) return;\n drawingRef.current = true;\n strokesRef.current = [...strokesRef.current, [pointFrom(event)]];\n canvasRef.current?.setPointerCapture?.(event.pointerId);\n onBegin?.();\n setEmptiness(false);\n redraw();\n },\n [disabled, onBegin, pointFrom, redraw, setEmptiness],\n );\n\n const handlePointerMove = useCallback(\n (event: ReactPointerEvent<HTMLCanvasElement>): void => {\n if (!drawingRef.current || disabled) return;\n const strokes = strokesRef.current;\n strokes[strokes.length - 1].push(pointFrom(event));\n redraw();\n },\n [disabled, pointFrom, redraw],\n );\n\n const handlePointerUp = useCallback(\n (event: ReactPointerEvent<HTMLCanvasElement>): void => {\n if (!drawingRef.current) return;\n drawingRef.current = false;\n canvasRef.current?.releasePointerCapture?.(event.pointerId);\n onEnd?.(canvasRef.current?.toDataURL() ?? \"\");\n },\n [onEnd],\n );\n\n const clear = useCallback((): void => {\n strokesRef.current = [];\n setEmptiness(true);\n redraw();\n }, [redraw, setEmptiness]);\n\n const undo = useCallback((): void => {\n strokesRef.current = strokesRef.current.slice(0, -1);\n setEmptiness(strokesRef.current.length === 0);\n redraw();\n }, [redraw, setEmptiness]);\n\n useImperativeHandle(\n ref,\n () => ({\n clear,\n undo,\n isEmpty: () => strokesRef.current.length === 0,\n toDataURL: (type, quality) => canvasRef.current?.toDataURL(type, quality) ?? \"\",\n toBlob: (type, quality) =>\n new Promise((resolve) => {\n const canvas = canvasRef.current;\n if (!canvas?.toBlob) {\n resolve(null);\n return;\n }\n canvas.toBlob((blob) => resolve(blob), type, quality);\n }),\n }),\n [clear, undo],\n );\n\n return (\n <div className={cn(styles.pad, disabled && styles.disabled, className)}>\n <canvas\n ref={canvasRef}\n role=\"img\"\n aria-label={label}\n aria-disabled={disabled || undefined}\n className={styles.canvas}\n style={{ width, height, touchAction: \"none\" }}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerUp}\n onPointerLeave={handlePointerUp}\n />\n {showActions ? (\n <div className={styles.actions}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={undo}\n disabled={disabled || isEmpty}\n >\n {undoLabel}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={clear}\n disabled={disabled || isEmpty}\n >\n {clearLabel}\n </Button>\n </div>\n ) : null}\n </div>\n );\n});\n"],"mappings":";;;;;;AAiFA,IAAa,IAAe,EAAkD,SAC1E,EACI,WAAQ,KACR,YAAS,KACT,aACA,cAAW,GACX,cAAW,IACX,WAAQ,aACR,YACA,UACA,kBACA,iBAAc,IACd,gBAAa,SACb,eAAY,QACZ,gBAEJ,GACF;CACE,IAAM,IAAY,EAA0B,IAAI,GAC1C,IAAa,EAAkB,CAAC,CAAC,GACjC,IAAa,EAAO,EAAK,GACzB,CAAC,GAAS,KAAc,EAAS,EAAI,GAErC,IAAkB,QAA0B;EAC9C,IAAI,GAAU,OAAO;EACrB,IAAM,IAAS,EAAU;EAEzB,OADI,CAAC,KAAU,OAAO,SAAW,MAAoB,YAC9C,OAAO,iBAAiB,CAAM,CAAC,CAAC,SAAS;CACpD,GAAG,CAAC,CAAQ,CAAC,GASP,IAAS,QAAwB;EACnC,IAAM,IAAS,EAAU,SACnB,IAAU,GAAQ,WAAW,IAAI;EACvC,IAAI,CAAC,KAAU,CAAC,GAAS;EAEzB,IAAM,IAAQ,OAAO,SAAW,MAAc,IAAI,OAAO,oBAAoB;EAS7E,AARA,EAAO,QAAQ,IAAQ,GACvB,EAAO,SAAS,IAAS,GACzB,EAAQ,aAAa,GAAO,GAAG,GAAG,GAAO,GAAG,CAAC,GAC7C,EAAQ,UAAU,GAAG,GAAG,GAAO,CAAM,GAErC,EAAQ,YAAY,GACpB,EAAQ,UAAU,SAClB,EAAQ,WAAW,SACnB,EAAQ,cAAc,EAAgB;EAEtC,KAAK,IAAM,KAAU,EAAW,SACxB,MAAO,WAAW,GAEtB;GADA,EAAQ,UAAU,GAClB,EAAQ,OAAO,EAAO,EAAE,CAAC,GAAG,EAAO,EAAE,CAAC,CAAC;GACvC,KAAK,IAAM,KAAS,EAAO,MAAM,CAAC,GAC9B,EAAQ,OAAO,EAAM,GAAG,EAAM,CAAC;GAGnC,AADI,EAAO,WAAW,KAAG,EAAQ,OAAO,EAAO,EAAE,CAAC,GAAG,EAAO,EAAE,CAAC,CAAC,GAChE,EAAQ,OAAO;EALwB;CAO/C,GAAG;EAAC;EAAQ;EAAU;EAAiB;CAAK,CAAC;CAE7C,QAAgB;EACZ,EAAO;CACX,GAAG,CAAC,CAAM,CAAC;CAEX,IAAM,IAAe,GAChB,MAAwB;EACrB,GAAY,OACJ,MAAY,KAAM,IAAgB,CAAI,GACnC,EACV;CACL,GACA,CAAC,CAAa,CAClB,GAEM,IAAY,GAAa,MAAuD;EAClF,IAAM,IAAO,EAAU,SAAS,sBAAsB;EACtD,OAAO;GACH,GAAG,EAAM,WAAW,GAAM,QAAQ;GAClC,GAAG,EAAM,WAAW,GAAM,OAAO;EACrC;CACJ,GAAG,CAAC,CAAC,GAEC,IAAoB,GACrB,MAAsD;EAC/C,MACJ,EAAW,UAAU,IACrB,EAAW,UAAU,CAAC,GAAG,EAAW,SAAS,CAAC,EAAU,CAAK,CAAC,CAAC,GAC/D,EAAU,SAAS,oBAAoB,EAAM,SAAS,GACtD,IAAU,GACV,EAAa,EAAK,GAClB,EAAO;CACX,GACA;EAAC;EAAU;EAAS;EAAW;EAAQ;CAAY,CACvD,GAEM,IAAoB,GACrB,MAAsD;EACnD,IAAI,CAAC,EAAW,WAAW,GAAU;EACrC,IAAM,IAAU,EAAW;EAE3B,AADA,EAAQ,EAAQ,SAAS,EAAE,CAAC,KAAK,EAAU,CAAK,CAAC,GACjD,EAAO;CACX,GACA;EAAC;EAAU;EAAW;CAAM,CAChC,GAEM,IAAkB,GACnB,MAAsD;EAC9C,EAAW,YAChB,EAAW,UAAU,IACrB,EAAU,SAAS,wBAAwB,EAAM,SAAS,GAC1D,IAAQ,EAAU,SAAS,UAAU,KAAK,EAAE;CAChD,GACA,CAAC,CAAK,CACV,GAEM,IAAQ,QAAwB;EAGlC,AAFA,EAAW,UAAU,CAAC,GACtB,EAAa,EAAI,GACjB,EAAO;CACX,GAAG,CAAC,GAAQ,CAAY,CAAC,GAEnB,IAAO,QAAwB;EAGjC,AAFA,EAAW,UAAU,EAAW,QAAQ,MAAM,GAAG,EAAE,GACnD,EAAa,EAAW,QAAQ,WAAW,CAAC,GAC5C,EAAO;CACX,GAAG,CAAC,GAAQ,CAAY,CAAC;CAsBzB,OApBA,EACI,UACO;EACH;EACA;EACA,eAAe,EAAW,QAAQ,WAAW;EAC7C,YAAY,GAAM,MAAY,EAAU,SAAS,UAAU,GAAM,CAAO,KAAK;EAC7E,SAAS,GAAM,MACX,IAAI,SAAS,MAAY;GACrB,IAAM,IAAS,EAAU;GACzB,IAAI,CAAC,GAAQ,QAAQ;IACjB,EAAQ,IAAI;IACZ;GACJ;GACA,EAAO,QAAQ,MAAS,EAAQ,CAAI,GAAG,GAAM,CAAO;EACxD,CAAC;CACT,IACA,CAAC,GAAO,CAAI,CAChB,GAGI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,KAAK,KAAY,EAAO,UAAU,CAAS;YAArE,CACI,kBAAC,UAAD;GACI,KAAK;GACL,MAAK;GACL,cAAY;GACZ,iBAAe,KAAY,KAAA;GAC3B,WAAW,EAAO;GAClB,OAAO;IAAE;IAAO;IAAQ,aAAa;GAAO;GAC5C,eAAe;GACf,eAAe;GACf,aAAa;GACb,gBAAgB;EACnB,CAAA,GACA,IACG,kBAAC,OAAD;GAAK,WAAW,EAAO;aAAvB,CACI,kBAAC,GAAD;IACI,MAAK;IACL,SAAQ;IACR,MAAK;IACL,SAAS;IACT,UAAU,KAAY;cAErB;GACG,CAAA,GACR,kBAAC,GAAD;IACI,MAAK;IACL,SAAQ;IACR,MAAK;IACL,SAAS;IACT,UAAU,KAAY;cAErB;GACG,CAAA,CACP;OACL,IACH;;AAEb,CAAC"}
|
|
1
|
+
{"version":3,"file":"SignaturePad.js","names":[],"sources":["../../../src/components/SignaturePad/SignaturePad.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the canvas (width,\n * height, penColor, penWidth), the lifecycle an app needs to enable its save button\n * (onBegin, onEnd, onEmptyChange) and the built-in actions with their copy\n * (showActions, clearLabel, undoLabel, label). The body is the stroke buffer plus\n * undo history, which are the same array.\n */\nimport { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from \"react\";\nimport type { PointerEvent as ReactPointerEvent } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Button } from \"../Button\";\nimport styles from \"./SignaturePad.module.css\";\n\n/** A single sampled point of a stroke, in CSS pixels relative to the canvas. */\ninterface Point {\n x: number;\n y: number;\n}\n\n/** Imperative handle exposed through `ref`. */\nexport interface SignaturePadHandle {\n /** Drop every stroke. */\n clear: () => void;\n /** Drop the last stroke. */\n undo: () => void;\n /** `true` while nothing has been drawn. */\n isEmpty: () => boolean;\n /** Export as a data URL. Returns `\"\"` when the canvas is unavailable. */\n toDataURL: (type?: string, quality?: number) => string;\n /** Export as a `Blob` — what you actually upload. `null` when unavailable. */\n toBlob: (type?: string, quality?: number) => Promise<Blob | null>;\n}\n\nexport interface SignaturePadProps {\n /** Drawing surface width in CSS pixels. Default `400`. */\n width?: number;\n /** Drawing surface height in CSS pixels. Default `160`. */\n height?: number;\n /**\n * Stroke color. Defaults to the canvas' computed `color`, which the\n * stylesheet binds to `--tempest-text` — so the signature follows the theme\n * (including dark mode) instead of being hardcoded black.\n */\n penColor?: string;\n /** Stroke width in CSS pixels. Default `2`. */\n penWidth?: number;\n /** Blocks drawing and dims the surface. */\n disabled?: boolean;\n /** Accessible name of the surface. Default `\"Signature\"`. */\n label?: string;\n /** Called on the first move of each stroke. */\n onBegin?: () => void;\n /** Called when a stroke ends, with the current image as a data URL. */\n onEnd?: (dataUrl: string) => void;\n /** Called whenever the emptiness changes — wire it to a submit button. */\n onEmptyChange?: (isEmpty: boolean) => void;\n /** Renders the Clear/Undo buttons. Default `true`. */\n showActions?: boolean;\n clearLabel?: string;\n undoLabel?: string;\n className?: string;\n}\n\n/**\n * Signature capture on a canvas — the \"sign here\" field of a delivery receipt,\n * a service order, a term of acceptance.\n *\n * Strokes are kept as point lists and the canvas is **redrawn** from them, which\n * is what makes `undo` possible at all: a canvas holds pixels, not history, so\n * removing the last stroke means replaying the rest.\n *\n * The backing store is scaled by `devicePixelRatio`, so the line is crisp on a\n * phone instead of the blurry 1x bitmap a naive canvas produces.\n *\n * @example\n * ```tsx\n * const pad = useRef<SignaturePadHandle>(null);\n * const [empty, setEmpty] = useState(true);\n *\n * async function submit() {\n * const blob = await pad.current?.toBlob(\"image/png\");\n * if (blob) await api.upload(\"/receipts/42/signature\", blob);\n * }\n *\n * <SignaturePad ref={pad} label=\"Assinatura do cliente\" onEmptyChange={setEmpty} />\n * <Button disabled={empty} onClick={submit}>Enviar</Button>\n * ```\n */\nexport const SignaturePad = forwardRef<SignaturePadHandle, SignaturePadProps>(function SignaturePad(\n {\n width = 400,\n height = 160,\n penColor,\n penWidth = 2,\n disabled = false,\n label = \"Signature\",\n onBegin,\n onEnd,\n onEmptyChange,\n showActions = true,\n clearLabel = \"Clear\",\n undoLabel = \"Undo\",\n className,\n },\n ref,\n) {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const strokesRef = useRef<Point[][]>([]);\n const drawingRef = useRef(false);\n const [isEmpty, setIsEmpty] = useState(true);\n\n const resolvePenColor = useCallback((): string => {\n if (penColor) return penColor;\n const canvas = canvasRef.current;\n if (!canvas || typeof window === \"undefined\") return \"#101828\";\n return window.getComputedStyle(canvas).color || \"#101828\";\n }, [penColor]);\n\n /**\n * Repaint the whole surface from the stroke list, honouring the pixel ratio.\n *\n * A one-point stroke (a tap) gets a line back to its own origin: `stroke()`\n * over a path with a single point paints nothing, so without it a tap would\n * silently produce no dot while still counting as \"not empty\".\n */\n const redraw = useCallback((): void => {\n const canvas = canvasRef.current;\n const context = canvas?.getContext(\"2d\");\n if (!canvas || !context) return;\n\n const ratio = typeof window === \"undefined\" ? 1 : window.devicePixelRatio || 1;\n canvas.width = width * ratio;\n canvas.height = height * ratio;\n context.setTransform(ratio, 0, 0, ratio, 0, 0);\n context.clearRect(0, 0, width, height);\n\n context.lineWidth = penWidth;\n context.lineCap = \"round\";\n context.lineJoin = \"round\";\n context.strokeStyle = resolvePenColor();\n\n for (const stroke of strokesRef.current) {\n if (stroke.length === 0) continue;\n context.beginPath();\n context.moveTo(stroke[0].x, stroke[0].y);\n for (const point of stroke.slice(1)) {\n context.lineTo(point.x, point.y);\n }\n if (stroke.length === 1) context.lineTo(stroke[0].x, stroke[0].y);\n context.stroke();\n }\n }, [height, penWidth, resolvePenColor, width]);\n\n useEffect(() => {\n redraw();\n }, [redraw]);\n\n const setEmptiness = useCallback(\n (next: boolean): void => {\n setIsEmpty((current) => {\n if (current !== next) onEmptyChange?.(next);\n return next;\n });\n },\n [onEmptyChange],\n );\n\n const pointFrom = useCallback((event: ReactPointerEvent<HTMLCanvasElement>): Point => {\n const rect = canvasRef.current?.getBoundingClientRect();\n return {\n x: event.clientX - (rect?.left ?? 0),\n y: event.clientY - (rect?.top ?? 0),\n };\n }, []);\n\n const handlePointerDown = useCallback(\n (event: ReactPointerEvent<HTMLCanvasElement>): void => {\n if (disabled) return;\n drawingRef.current = true;\n strokesRef.current = [...strokesRef.current, [pointFrom(event)]];\n canvasRef.current?.setPointerCapture?.(event.pointerId);\n onBegin?.();\n setEmptiness(false);\n redraw();\n },\n [disabled, onBegin, pointFrom, redraw, setEmptiness],\n );\n\n const handlePointerMove = useCallback(\n (event: ReactPointerEvent<HTMLCanvasElement>): void => {\n if (!drawingRef.current || disabled) return;\n const strokes = strokesRef.current;\n strokes[strokes.length - 1].push(pointFrom(event));\n redraw();\n },\n [disabled, pointFrom, redraw],\n );\n\n const handlePointerUp = useCallback(\n (event: ReactPointerEvent<HTMLCanvasElement>): void => {\n if (!drawingRef.current) return;\n drawingRef.current = false;\n canvasRef.current?.releasePointerCapture?.(event.pointerId);\n onEnd?.(canvasRef.current?.toDataURL() ?? \"\");\n },\n [onEnd],\n );\n\n const clear = useCallback((): void => {\n strokesRef.current = [];\n setEmptiness(true);\n redraw();\n }, [redraw, setEmptiness]);\n\n const undo = useCallback((): void => {\n strokesRef.current = strokesRef.current.slice(0, -1);\n setEmptiness(strokesRef.current.length === 0);\n redraw();\n }, [redraw, setEmptiness]);\n\n useImperativeHandle(\n ref,\n () => ({\n clear,\n undo,\n isEmpty: () => strokesRef.current.length === 0,\n toDataURL: (type, quality) => canvasRef.current?.toDataURL(type, quality) ?? \"\",\n toBlob: (type, quality) =>\n new Promise((resolve) => {\n const canvas = canvasRef.current;\n if (!canvas?.toBlob) {\n resolve(null);\n return;\n }\n canvas.toBlob((blob) => resolve(blob), type, quality);\n }),\n }),\n [clear, undo],\n );\n\n return (\n <div className={cn(styles.pad, disabled && styles.disabled, className)}>\n <canvas\n ref={canvasRef}\n role=\"img\"\n aria-label={label}\n aria-disabled={disabled || undefined}\n className={styles.canvas}\n style={{ width, height, touchAction: \"none\" }}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerUp}\n onPointerLeave={handlePointerUp}\n />\n {showActions ? (\n <div className={styles.actions}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={undo}\n disabled={disabled || isEmpty}\n >\n {undoLabel}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={clear}\n disabled={disabled || isEmpty}\n >\n {clearLabel}\n </Button>\n </div>\n ) : null}\n </div>\n );\n});\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Slider.cjs","names":[],"sources":["../../../src/components/Slider/Slider.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Slider.cjs","names":[],"sources":["../../../src/components/Slider/Slider.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — min, max, step, value, onChange, formatValue plus\n * the field chrome (label, helperText, disabled, className). Every one is used by\n * the range variant too — the pair is deliberately the same API.\n */\nimport { useCallback, useMemo } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Slider.module.css\";\n\nexport interface SliderProps {\n /** Current value. */\n value: number;\n /** Called with the new value on every change. */\n onChange: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n label?: string;\n helperText?: string;\n disabled?: boolean;\n /** Formatter for the value badge next to the label. Defaults to the raw number. */\n formatValue?: (value: number) => string;\n className?: string;\n}\n\n/**\n * Single-thumb slider built on a native `<input type=\"range\">`, so it stays\n * accessible (keyboard + screen reader) with no positioning libs. The active\n * fill is a percentage-width bar. For a two-thumb range, use `RangeSlider`.\n */\nexport function Slider({\n value,\n onChange,\n min = 0,\n max = 100,\n step = 1,\n label,\n helperText,\n disabled = false,\n formatValue,\n className,\n}: SliderProps) {\n const pct = useMemo(() => {\n const range = max - min;\n if (range <= 0) return 0;\n return Math.min(100, Math.max(0, ((value - min) / range) * 100));\n }, [value, min, max]);\n\n const valueText = formatValue?.(value) ?? String(value);\n\n const handleChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>): void => {\n onChange(Number(event.target.value));\n },\n [onChange],\n );\n\n return (\n <div className={cn(styles.wrapper, className)}>\n {label && (\n <div className={styles.label}>\n <span>{label}</span>\n <span className={styles.value}>{valueText}</span>\n </div>\n )}\n <div className={styles.field}>\n <div className={styles.track} />\n <div className={styles.fill} style={{ width: `${pct}%` }} />\n <input\n type=\"range\"\n className={styles.input}\n min={min}\n max={max}\n step={step}\n value={value}\n onChange={handleChange}\n disabled={disabled}\n aria-label={label ?? \"Slider\"}\n aria-valuetext={valueText}\n />\n </div>\n {helperText && <span className={styles.helper}>{helperText}</span>}\n </div>\n );\n}\n"],"mappings":"6HA8BA,SAAgB,EAAO,CACnB,QACA,WACA,MAAM,EACN,MAAM,IACN,OAAO,EACP,QACA,aACA,WAAW,GACX,cACA,aACY,CACZ,IAAM,GAAA,EAAA,EAAA,QAAA,KAAoB,CACtB,IAAM,EAAQ,EAAM,EAEpB,OADI,GAAS,EAAU,EAChB,KAAK,IAAI,IAAK,KAAK,IAAI,GAAK,EAAQ,GAAO,EAAS,GAAG,CAAC,CACnE,EAAG,CAAC,EAAO,EAAK,CAAG,CAAC,EAEd,EAAY,IAAc,CAAK,GAAK,OAAO,CAAK,EAEhD,GAAA,EAAA,EAAA,YAAA,CACD,GAAqD,CAClD,EAAS,OAAO,EAAM,OAAO,KAAK,CAAC,CACvC,EACA,CAAC,CAAQ,CACb,EAEA,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,CAAS,WAA5C,CACK,IACG,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,eAAvB,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,CAAY,CAAA,GACnB,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,eAAQ,CAAgB,CAAA,CAC/C,KAET,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,eAAvB,EACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAQ,CAAA,GAC/B,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAM,MAAO,CAAE,MAAO,GAAG,EAAI,EAAG,CAAI,CAAA,GAC3D,EAAA,EAAA,IAAA,CAAC,QAAD,CACI,KAAK,QACL,UAAW,EAAA,QAAO,MACb,MACA,MACC,OACC,QACP,SAAU,EACA,WACV,aAAY,GAAS,SACrB,iBAAgB,CACnB,CAAA,CACA,IACJ,IAAc,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,gBAAS,CAAiB,CAAA,CAChE,GAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Slider.js","names":[],"sources":["../../../src/components/Slider/Slider.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Slider.js","names":[],"sources":["../../../src/components/Slider/Slider.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — min, max, step, value, onChange, formatValue plus\n * the field chrome (label, helperText, disabled, className). Every one is used by\n * the range variant too — the pair is deliberately the same API.\n */\nimport { useCallback, useMemo } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Slider.module.css\";\n\nexport interface SliderProps {\n /** Current value. */\n value: number;\n /** Called with the new value on every change. */\n onChange: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n label?: string;\n helperText?: string;\n disabled?: boolean;\n /** Formatter for the value badge next to the label. Defaults to the raw number. */\n formatValue?: (value: number) => string;\n className?: string;\n}\n\n/**\n * Single-thumb slider built on a native `<input type=\"range\">`, so it stays\n * accessible (keyboard + screen reader) with no positioning libs. The active\n * fill is a percentage-width bar. For a two-thumb range, use `RangeSlider`.\n */\nexport function Slider({\n value,\n onChange,\n min = 0,\n max = 100,\n step = 1,\n label,\n helperText,\n disabled = false,\n formatValue,\n className,\n}: SliderProps) {\n const pct = useMemo(() => {\n const range = max - min;\n if (range <= 0) return 0;\n return Math.min(100, Math.max(0, ((value - min) / range) * 100));\n }, [value, min, max]);\n\n const valueText = formatValue?.(value) ?? String(value);\n\n const handleChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>): void => {\n onChange(Number(event.target.value));\n },\n [onChange],\n );\n\n return (\n <div className={cn(styles.wrapper, className)}>\n {label && (\n <div className={styles.label}>\n <span>{label}</span>\n <span className={styles.value}>{valueText}</span>\n </div>\n )}\n <div className={styles.field}>\n <div className={styles.track} />\n <div className={styles.fill} style={{ width: `${pct}%` }} />\n <input\n type=\"range\"\n className={styles.input}\n min={min}\n max={max}\n step={step}\n value={value}\n onChange={handleChange}\n disabled={disabled}\n aria-label={label ?? \"Slider\"}\n aria-valuetext={valueText}\n />\n </div>\n {helperText && <span className={styles.helper}>{helperText}</span>}\n </div>\n );\n}\n"],"mappings":";;;;;AA8BA,SAAgB,EAAO,EACnB,UACA,aACA,SAAM,GACN,SAAM,KACN,UAAO,GACP,UACA,eACA,cAAW,IACX,gBACA,gBACY;CACZ,IAAM,IAAM,QAAc;EACtB,IAAM,IAAQ,IAAM;EAEpB,OADI,KAAS,IAAU,IAChB,KAAK,IAAI,KAAK,KAAK,IAAI,IAAK,IAAQ,KAAO,IAAS,GAAG,CAAC;CACnE,GAAG;EAAC;EAAO;EAAK;CAAG,CAAC,GAEd,IAAY,IAAc,CAAK,KAAK,OAAO,CAAK,GAEhD,IAAe,GAChB,MAAqD;EAClD,EAAS,OAAO,EAAM,OAAO,KAAK,CAAC;CACvC,GACA,CAAC,CAAQ,CACb;CAEA,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,SAAS,CAAS;YAA5C;GACK,KACG,kBAAC,OAAD;IAAK,WAAW,EAAO;cAAvB,CACI,kBAAC,QAAD,EAAA,UAAO,EAAY,CAAA,GACnB,kBAAC,QAAD;KAAM,WAAW,EAAO;eAAQ;IAAgB,CAAA,CAC/C;;GAET,kBAAC,OAAD;IAAK,WAAW,EAAO;cAAvB;KACI,kBAAC,OAAD,EAAK,WAAW,EAAO,MAAQ,CAAA;KAC/B,kBAAC,OAAD;MAAK,WAAW,EAAO;MAAM,OAAO,EAAE,OAAO,GAAG,EAAI,GAAG;KAAI,CAAA;KAC3D,kBAAC,SAAD;MACI,MAAK;MACL,WAAW,EAAO;MACb;MACA;MACC;MACC;MACP,UAAU;MACA;MACV,cAAY,KAAS;MACrB,kBAAgB;KACnB,CAAA;IACA;;GACJ,KAAc,kBAAC,QAAD;IAAM,WAAW,EAAO;cAAS;GAAiB,CAAA;EAChE;;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sparkline.cjs","names":[],"sources":["../../../src/components/Sparkline/Sparkline.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Sparkline.cjs","names":[],"sources":["../../../src/components/Sparkline/Sparkline.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — an inline chart with no chart library: data plus its\n * scale (min, max), its box (width, height), its look (variant, color, showEnd) and\n * its accessibility (valueFormatter, label). The formatter and label matter because\n * a sparkline has no axes to read.\n */\nimport type { HTMLAttributes } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { areaPath, barRects, describeSeries, linePath, sparkPoints } from \"./sparkline-geometry\";\nimport styles from \"./Sparkline.module.css\";\n\nexport type SparklineVariant = \"line\" | \"area\" | \"bar\";\n\nexport interface SparklineProps extends Omit<HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** The series, in order. Non-finite entries are skipped. */\n data: readonly number[];\n /** Drawing width in px. Default `88`. */\n width?: number;\n /** Drawing height in px. Default `24`. */\n height?: number;\n /** Which mark to draw. Default `\"line\"`. */\n variant?: SparklineVariant;\n /** Any CSS colour. Defaults to the first chart series token. */\n color?: string;\n /** Draw a dot on the last point. Default `true` for line and area. */\n showEnd?: boolean;\n /** Force the low end of the value axis — use it to compare several sparklines. */\n min?: number;\n /** Force the high end. */\n max?: number;\n /** Render a value; used in the accessible description. */\n valueFormatter?: (value: number) => string;\n /**\n * Accessible name. Defaults to a sentence describing the series.\n *\n * Pass one when the surrounding text already says what is plotted — the default\n * still appends the shape, so a caller never ends up with an unlabelled image.\n */\n label?: string;\n}\n\n/**\n * A tiny inline chart: shape only, no axes, no legend.\n *\n * Sized to sit in a table cell or beside a number, which is why it is plain SVG on\n * the root entry rather than a recharts wrapper — a sparkline in a data table should\n * not oblige an app to install a charting library.\n *\n * It carries `role=\"img\"` and a spoken description of the series (direction, ends,\n * extremes). Without that it is an unlabelled image: a sparkline has no axis or\n * legend to fall back on, so a screen reader would reach it and read nothing. Pair it\n * with the number it annotates — the shape is context, never the only way to read the\n * value.\n *\n * @example\n * <Sparkline data={last30Days} />\n * <Sparkline data={revenue} variant=\"area\" width={120} height={32} />\n *\n * // Same axis across a column, so the rows are comparable\n * <Sparkline data={row.series} min={0} max={columnMax} />\n */\nexport function Sparkline({\n data,\n width = 88,\n height = 24,\n variant = \"line\",\n color = \"var(--tempest-chart-1)\",\n showEnd,\n min,\n max,\n valueFormatter,\n label,\n className,\n ...rest\n}: SparklineProps) {\n const points = sparkPoints({ values: data, width, height, min, max });\n const description = label ?? describeSeries(data, valueFormatter);\n const withEnd = showEnd ?? variant !== \"bar\";\n const last = points[points.length - 1];\n\n /*\n * Bars grow from the bottom of the box; line and area close against it. Both use\n * the same edge so a sparkline swapped between variants keeps its footprint.\n */\n const baselineY = height - 2;\n\n return (\n <span className={cn(styles.wrapper, className)} {...rest}>\n <svg\n className={styles.svg}\n width={width}\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n role=\"img\"\n aria-label={description}\n >\n {points.length > 0 && variant === \"area\" && (\n <path d={areaPath(points, baselineY)} fill={color} className={styles.area} />\n )}\n\n {points.length > 0 && variant !== \"bar\" && (\n <path\n d={linePath(points)}\n fill=\"none\"\n stroke={color}\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n )}\n\n {variant === \"bar\" &&\n barRects(points, { width, baselineY }).map((rect) => (\n <rect\n key={rect.point.index}\n x={rect.x}\n y={rect.y}\n width={rect.width}\n height={rect.height}\n rx={1}\n fill={color}\n />\n ))}\n\n {withEnd && last && (\n /*\n * The ring is drawn in the surface colour rather than as a stroke on\n * the dot: it keeps the marker legible where it sits on top of the\n * line without adding ink that is not data.\n */\n <circle\n cx={last.x}\n cy={last.y}\n r={3}\n fill={color}\n stroke=\"var(--tempest-bg)\"\n strokeWidth={2}\n />\n )}\n </svg>\n </span>\n );\n}\n"],"mappings":"mJA+DA,SAAgB,EAAU,CACtB,OACA,QAAQ,GACR,SAAS,GACT,UAAU,OACV,QAAQ,yBACR,UACA,MACA,MACA,iBACA,QACA,YACA,GAAG,GACY,CACf,IAAM,EAAS,EAAA,YAAY,CAAE,OAAQ,EAAM,QAAO,SAAQ,MAAK,KAAI,CAAC,EAC9D,EAAc,GAAS,EAAA,eAAe,EAAM,CAAc,EAC1D,EAAU,GAAW,IAAY,MACjC,EAAO,EAAO,EAAO,OAAS,GAM9B,EAAY,EAAS,EAE3B,OACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,CAAS,EAAG,GAAI,YAChD,EAAA,EAAA,KAAA,CAAC,MAAD,CACI,UAAW,EAAA,QAAO,IACX,QACC,SACR,QAAS,OAAO,EAAM,GAAG,IACzB,KAAK,MACL,aAAY,WANhB,CAQK,EAAO,OAAS,GAAK,IAAY,SAC9B,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,EAAG,EAAA,SAAS,EAAQ,CAAS,EAAG,KAAM,EAAO,UAAW,EAAA,QAAO,IAAO,CAAA,EAG/E,EAAO,OAAS,GAAK,IAAY,QAC9B,EAAA,EAAA,IAAA,CAAC,OAAD,CACI,EAAG,EAAA,SAAS,CAAM,EAClB,KAAK,OACL,OAAQ,EACR,YAAa,EACb,cAAc,QACd,eAAe,OAClB,CAAA,EAGJ,IAAY,OACT,EAAA,SAAS,EAAQ,CAAE,QAAO,WAAU,CAAC,CAAC,CAAC,IAAK,IACxC,EAAA,EAAA,IAAA,CAAC,OAAD,CAEI,EAAG,EAAK,EACR,EAAG,EAAK,EACR,MAAO,EAAK,MACZ,OAAQ,EAAK,OACb,GAAI,EACJ,KAAM,CACT,EAPQ,EAAK,MAAM,KAOnB,CACJ,EAEJ,GAAW,IAMR,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,GAAI,EAAK,EACT,GAAI,EAAK,EACT,EAAG,EACH,KAAM,EACN,OAAO,oBACP,YAAa,CAChB,CAAA,CAEJ,GACH,CAAA,CAEd"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sparkline.js","names":[],"sources":["../../../src/components/Sparkline/Sparkline.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Sparkline.js","names":[],"sources":["../../../src/components/Sparkline/Sparkline.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — an inline chart with no chart library: data plus its\n * scale (min, max), its box (width, height), its look (variant, color, showEnd) and\n * its accessibility (valueFormatter, label). The formatter and label matter because\n * a sparkline has no axes to read.\n */\nimport type { HTMLAttributes } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { areaPath, barRects, describeSeries, linePath, sparkPoints } from \"./sparkline-geometry\";\nimport styles from \"./Sparkline.module.css\";\n\nexport type SparklineVariant = \"line\" | \"area\" | \"bar\";\n\nexport interface SparklineProps extends Omit<HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** The series, in order. Non-finite entries are skipped. */\n data: readonly number[];\n /** Drawing width in px. Default `88`. */\n width?: number;\n /** Drawing height in px. Default `24`. */\n height?: number;\n /** Which mark to draw. Default `\"line\"`. */\n variant?: SparklineVariant;\n /** Any CSS colour. Defaults to the first chart series token. */\n color?: string;\n /** Draw a dot on the last point. Default `true` for line and area. */\n showEnd?: boolean;\n /** Force the low end of the value axis — use it to compare several sparklines. */\n min?: number;\n /** Force the high end. */\n max?: number;\n /** Render a value; used in the accessible description. */\n valueFormatter?: (value: number) => string;\n /**\n * Accessible name. Defaults to a sentence describing the series.\n *\n * Pass one when the surrounding text already says what is plotted — the default\n * still appends the shape, so a caller never ends up with an unlabelled image.\n */\n label?: string;\n}\n\n/**\n * A tiny inline chart: shape only, no axes, no legend.\n *\n * Sized to sit in a table cell or beside a number, which is why it is plain SVG on\n * the root entry rather than a recharts wrapper — a sparkline in a data table should\n * not oblige an app to install a charting library.\n *\n * It carries `role=\"img\"` and a spoken description of the series (direction, ends,\n * extremes). Without that it is an unlabelled image: a sparkline has no axis or\n * legend to fall back on, so a screen reader would reach it and read nothing. Pair it\n * with the number it annotates — the shape is context, never the only way to read the\n * value.\n *\n * @example\n * <Sparkline data={last30Days} />\n * <Sparkline data={revenue} variant=\"area\" width={120} height={32} />\n *\n * // Same axis across a column, so the rows are comparable\n * <Sparkline data={row.series} min={0} max={columnMax} />\n */\nexport function Sparkline({\n data,\n width = 88,\n height = 24,\n variant = \"line\",\n color = \"var(--tempest-chart-1)\",\n showEnd,\n min,\n max,\n valueFormatter,\n label,\n className,\n ...rest\n}: SparklineProps) {\n const points = sparkPoints({ values: data, width, height, min, max });\n const description = label ?? describeSeries(data, valueFormatter);\n const withEnd = showEnd ?? variant !== \"bar\";\n const last = points[points.length - 1];\n\n /*\n * Bars grow from the bottom of the box; line and area close against it. Both use\n * the same edge so a sparkline swapped between variants keeps its footprint.\n */\n const baselineY = height - 2;\n\n return (\n <span className={cn(styles.wrapper, className)} {...rest}>\n <svg\n className={styles.svg}\n width={width}\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n role=\"img\"\n aria-label={description}\n >\n {points.length > 0 && variant === \"area\" && (\n <path d={areaPath(points, baselineY)} fill={color} className={styles.area} />\n )}\n\n {points.length > 0 && variant !== \"bar\" && (\n <path\n d={linePath(points)}\n fill=\"none\"\n stroke={color}\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n )}\n\n {variant === \"bar\" &&\n barRects(points, { width, baselineY }).map((rect) => (\n <rect\n key={rect.point.index}\n x={rect.x}\n y={rect.y}\n width={rect.width}\n height={rect.height}\n rx={1}\n fill={color}\n />\n ))}\n\n {withEnd && last && (\n /*\n * The ring is drawn in the surface colour rather than as a stroke on\n * the dot: it keeps the marker legible where it sits on top of the\n * line without adding ink that is not data.\n */\n <circle\n cx={last.x}\n cy={last.y}\n r={3}\n fill={color}\n stroke=\"var(--tempest-bg)\"\n strokeWidth={2}\n />\n )}\n </svg>\n </span>\n );\n}\n"],"mappings":";;;;;AA+DA,SAAgB,EAAU,EACtB,SACA,WAAQ,IACR,YAAS,IACT,aAAU,QACV,WAAQ,0BACR,YACA,QACA,QACA,mBACA,UACA,cACA,GAAG,KACY;CACf,IAAM,IAAS,EAAY;EAAE,QAAQ;EAAM;EAAO;EAAQ;EAAK;CAAI,CAAC,GAC9D,IAAc,KAAS,EAAe,GAAM,CAAc,GAC1D,IAAU,KAAW,MAAY,OACjC,IAAO,EAAO,EAAO,SAAS,IAM9B,IAAY,IAAS;CAE3B,OACI,kBAAC,QAAD;EAAM,WAAW,EAAG,EAAO,SAAS,CAAS;EAAG,GAAI;YAChD,kBAAC,OAAD;GACI,WAAW,EAAO;GACX;GACC;GACR,SAAS,OAAO,EAAM,GAAG;GACzB,MAAK;GACL,cAAY;aANhB;IAQK,EAAO,SAAS,KAAK,MAAY,UAC9B,kBAAC,QAAD;KAAM,GAAG,EAAS,GAAQ,CAAS;KAAG,MAAM;KAAO,WAAW,EAAO;IAAO,CAAA;IAG/E,EAAO,SAAS,KAAK,MAAY,SAC9B,kBAAC,QAAD;KACI,GAAG,EAAS,CAAM;KAClB,MAAK;KACL,QAAQ;KACR,aAAa;KACb,eAAc;KACd,gBAAe;IAClB,CAAA;IAGJ,MAAY,SACT,EAAS,GAAQ;KAAE;KAAO;IAAU,CAAC,CAAC,CAAC,KAAK,MACxC,kBAAC,QAAD;KAEI,GAAG,EAAK;KACR,GAAG,EAAK;KACR,OAAO,EAAK;KACZ,QAAQ,EAAK;KACb,IAAI;KACJ,MAAM;IACT,GAPQ,EAAK,MAAM,KAOnB,CACJ;IAEJ,KAAW,KAMR,kBAAC,UAAD;KACI,IAAI,EAAK;KACT,IAAI,EAAK;KACT,GAAG;KACH,MAAM;KACN,QAAO;KACP,aAAa;IAChB,CAAA;GAEJ;;CACH,CAAA;AAEd"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StepperInput.cjs","names":[],"sources":["../../../src/components/StepperInput/StepperInput.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"StepperInput.cjs","names":[],"sources":["../../../src/components/StepperInput/StepperInput.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — value/onChange/min/max/step is the numeric contract,\n * format renders it, labels names the increment and decrement buttons for screen\n * readers, and size/disabled/label/className are the field. The button labels cannot\n * be defaulted: they are the only text those buttons have.\n */\nimport { forwardRef } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { clamp } from \"@/utils/numbers\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./StepperInput.module.css\";\n\nexport interface StepperInputProps {\n /** Current value. */\n value: number;\n /** Fires with the new value (already clamped to min/max). */\n onChange: (value: number) => void;\n /** Lower bound. Default `0`. */\n min?: number;\n /** Upper bound. Default `Number.POSITIVE_INFINITY`. */\n max?: number;\n /** Increment per click. Default `1`. */\n step?: number;\n /** Visual size. Default `\"md\"`. */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Disable the whole control. */\n disabled?: boolean;\n /** Optional label rendered above. */\n label?: ReactNode;\n /** Optional formatter for the displayed value. */\n format?: (value: number) => string;\n /** Custom button labels for accessibility. */\n labels?: { decrement: string; increment: string };\n className?: string;\n}\n\n/**\n * Numeric +/− stepper. Common in checkout quantity selectors and admin\n * forms. Clamps to `[min, max]` and emits already-bounded values.\n *\n * @example\n * <StepperInput value={qty} onChange={setQty} min={1} max={10} />\n */\nexport const StepperInput = forwardRef<HTMLDivElement, StepperInputProps>(function StepperInput(\n {\n value,\n onChange,\n min = 0,\n max = Number.POSITIVE_INFINITY,\n step = 1,\n size = \"md\",\n disabled = false,\n label,\n format,\n labels = { decrement: \"Diminuir\", increment: \"Aumentar\" },\n className,\n },\n ref,\n) {\n const decrement = (): void => {\n if (disabled) return;\n onChange(clamp(value - step, min, max));\n };\n const increment = (): void => {\n if (disabled) return;\n onChange(clamp(value + step, min, max));\n };\n\n const display = format ? format(value) : String(value);\n const canDecrement = !disabled && value > min;\n const canIncrement = !disabled && value < max;\n\n return (\n <div ref={ref} className={cn(styles.wrapper, className)}>\n {label && <label className={styles.label}>{label}</label>}\n <div className={cn(styles.row, styles[size], disabled && styles.disabled)}>\n <button\n type=\"button\"\n className={styles.btn}\n aria-label={labels.decrement}\n onClick={decrement}\n disabled={!canDecrement}\n >\n −\n </button>\n <span className={styles.value} role=\"status\" aria-live=\"polite\">\n {display}\n </span>\n <button\n type=\"button\"\n className={styles.btn}\n aria-label={labels.increment}\n onClick={increment}\n disabled={!canIncrement}\n >\n +\n </button>\n </div>\n </div>\n );\n});\n"],"mappings":"wKA2CA,IAAa,GAAA,EAAA,EAAA,WAAA,CAA6D,SACtE,CACI,QACA,WACA,MAAM,EACN,MAAM,IACN,OAAO,EACP,OAAO,KACP,WAAW,GACX,QACA,SACA,SAAS,CAAE,UAAW,WAAY,UAAW,UAAW,EACxD,aAEJ,EACF,CACE,IAAM,MAAwB,CACtB,GACJ,EAAS,EAAA,MAAM,EAAQ,EAAM,EAAK,CAAG,CAAC,CAC1C,EACM,MAAwB,CACtB,GACJ,EAAS,EAAA,MAAM,EAAQ,EAAM,EAAK,CAAG,CAAC,CAC1C,EAEM,EAAU,EAAS,EAAO,CAAK,EAAI,OAAO,CAAK,EAC/C,EAAe,CAAC,GAAY,EAAQ,EACpC,EAAe,CAAC,GAAY,EAAQ,EAE1C,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAU,MAAK,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,CAAS,WAAtD,CACK,IAAS,EAAA,EAAA,IAAA,CAAC,QAAD,CAAO,UAAW,EAAA,QAAO,eAAQ,CAAa,CAAA,GACxD,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,IAAK,EAAA,QAAO,GAAO,GAAY,EAAA,QAAO,QAAQ,WAAxE,EACI,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,IAClB,aAAY,EAAO,UACnB,QAAS,EACT,SAAU,CAAC,WACd,GAEO,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAO,KAAK,SAAS,YAAU,kBAClD,CACC,CAAA,GACN,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,IAClB,aAAY,EAAO,UACnB,QAAS,EACT,SAAU,CAAC,WACd,GAEO,CAAA,CACP,GACJ,GAEb,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StepperInput.js","names":[],"sources":["../../../src/components/StepperInput/StepperInput.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"StepperInput.js","names":[],"sources":["../../../src/components/StepperInput/StepperInput.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — value/onChange/min/max/step is the numeric contract,\n * format renders it, labels names the increment and decrement buttons for screen\n * readers, and size/disabled/label/className are the field. The button labels cannot\n * be defaulted: they are the only text those buttons have.\n */\nimport { forwardRef } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { clamp } from \"@/utils/numbers\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./StepperInput.module.css\";\n\nexport interface StepperInputProps {\n /** Current value. */\n value: number;\n /** Fires with the new value (already clamped to min/max). */\n onChange: (value: number) => void;\n /** Lower bound. Default `0`. */\n min?: number;\n /** Upper bound. Default `Number.POSITIVE_INFINITY`. */\n max?: number;\n /** Increment per click. Default `1`. */\n step?: number;\n /** Visual size. Default `\"md\"`. */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Disable the whole control. */\n disabled?: boolean;\n /** Optional label rendered above. */\n label?: ReactNode;\n /** Optional formatter for the displayed value. */\n format?: (value: number) => string;\n /** Custom button labels for accessibility. */\n labels?: { decrement: string; increment: string };\n className?: string;\n}\n\n/**\n * Numeric +/− stepper. Common in checkout quantity selectors and admin\n * forms. Clamps to `[min, max]` and emits already-bounded values.\n *\n * @example\n * <StepperInput value={qty} onChange={setQty} min={1} max={10} />\n */\nexport const StepperInput = forwardRef<HTMLDivElement, StepperInputProps>(function StepperInput(\n {\n value,\n onChange,\n min = 0,\n max = Number.POSITIVE_INFINITY,\n step = 1,\n size = \"md\",\n disabled = false,\n label,\n format,\n labels = { decrement: \"Diminuir\", increment: \"Aumentar\" },\n className,\n },\n ref,\n) {\n const decrement = (): void => {\n if (disabled) return;\n onChange(clamp(value - step, min, max));\n };\n const increment = (): void => {\n if (disabled) return;\n onChange(clamp(value + step, min, max));\n };\n\n const display = format ? format(value) : String(value);\n const canDecrement = !disabled && value > min;\n const canIncrement = !disabled && value < max;\n\n return (\n <div ref={ref} className={cn(styles.wrapper, className)}>\n {label && <label className={styles.label}>{label}</label>}\n <div className={cn(styles.row, styles[size], disabled && styles.disabled)}>\n <button\n type=\"button\"\n className={styles.btn}\n aria-label={labels.decrement}\n onClick={decrement}\n disabled={!canDecrement}\n >\n −\n </button>\n <span className={styles.value} role=\"status\" aria-live=\"polite\">\n {display}\n </span>\n <button\n type=\"button\"\n className={styles.btn}\n aria-label={labels.increment}\n onClick={increment}\n disabled={!canIncrement}\n >\n +\n </button>\n </div>\n </div>\n );\n});\n"],"mappings":";;;;;;AA2CA,IAAa,IAAe,EAA8C,SACtE,EACI,UACA,aACA,SAAM,GACN,SAAM,UACN,UAAO,GACP,UAAO,MACP,cAAW,IACX,UACA,WACA,YAAS;CAAE,WAAW;CAAY,WAAW;AAAW,GACxD,gBAEJ,GACF;CACE,IAAM,UAAwB;EACtB,KACJ,EAAS,EAAM,IAAQ,GAAM,GAAK,CAAG,CAAC;CAC1C,GACM,UAAwB;EACtB,KACJ,EAAS,EAAM,IAAQ,GAAM,GAAK,CAAG,CAAC;CAC1C,GAEM,IAAU,IAAS,EAAO,CAAK,IAAI,OAAO,CAAK,GAC/C,IAAe,CAAC,KAAY,IAAQ,GACpC,IAAe,CAAC,KAAY,IAAQ;CAE1C,OACI,kBAAC,OAAD;EAAU;EAAK,WAAW,EAAG,EAAO,SAAS,CAAS;YAAtD,CACK,KAAS,kBAAC,SAAD;GAAO,WAAW,EAAO;aAAQ;EAAa,CAAA,GACxD,kBAAC,OAAD;GAAK,WAAW,EAAG,EAAO,KAAK,EAAO,IAAO,KAAY,EAAO,QAAQ;aAAxE;IACI,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,cAAY,EAAO;KACnB,SAAS;KACT,UAAU,CAAC;eACd;IAEO,CAAA;IACR,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAO,MAAK;KAAS,aAAU;eAClD;IACC,CAAA;IACN,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,cAAY,EAAO;KACnB,SAAS;KACT,UAAU,CAAC;eACd;IAEO,CAAA;GACP;IACJ;;AAEb,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Table.cjs","names":[],"sources":["../../../src/components/Table/Table.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Table.cjs","names":[],"sources":["../../../src/components/Table/Table.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — columns, data and rowKey are the model; onRowClick\n * and emptyMessage the interaction and the empty state; stackOnMobile and\n * scrollLabel are the two ways a table survives a phone, and the label is required\n * because a scroll region needs an accessible name.\n */\nimport { useRef } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { useScrollOverflow } from \"@/hooks/use-scroll-overflow\";\nimport styles from \"./Table.module.css\";\n\nexport type TableAlign = \"left\" | \"right\" | \"center\";\nexport type TablePriority = \"always\" | \"tablet\" | \"desktop\";\n\nexport interface TableColumn<T> {\n key: string;\n header: ReactNode;\n /** Render the cell content. Defaults to `row[key]` if not provided. */\n render?: (row: T, index: number) => ReactNode;\n align?: TableAlign;\n width?: string | number;\n className?: string;\n /**\n * Visibility priority: `always` (default) shows on every viewport,\n * `tablet` hides below md (< 768px), `desktop` hides below lg (< 1024px).\n */\n priority?: TablePriority;\n}\n\nexport interface TableProps<T> {\n columns: TableColumn<T>[];\n data: T[];\n rowKey: (row: T, index: number) => string | number;\n onRowClick?: (row: T) => void;\n emptyMessage?: ReactNode;\n className?: string;\n /**\n * Stack mode — render rows as label/value cards on mobile (< md).\n * Better than horizontal scroll when each row has 3+ columns of dense data.\n */\n stackOnMobile?: boolean;\n /**\n * Accessible name for the scrollable region, used only while the table is\n * actually wider than its box. A focus stop that announces nothing is worse\n * than no focus stop, so name the table when the page holds several.\n */\n scrollLabel?: string;\n}\n\nfunction priorityClass(priority: TablePriority | undefined): string | undefined {\n if (priority === \"tablet\") return styles.priorityTablet;\n if (priority === \"desktop\") return styles.priorityDesktop;\n return undefined;\n}\n\n/**\n * Lightweight table with declarative columns + mobile niceties.\n *\n * - `priority` per column lets less-important data hide on narrow viewports.\n * - `stackOnMobile` re-renders each row as a label/value card on mobile,\n * avoiding horizontal scroll for dense data.\n */\nexport function Table<T>({\n columns,\n data,\n rowKey,\n onRowClick,\n emptyMessage = \"Nenhum registro encontrado.\",\n className,\n stackOnMobile = false,\n scrollLabel = \"Tabela rolável horizontalmente\",\n}: TableProps<T>) {\n const scrollRef = useRef<HTMLDivElement>(null);\n const scrollable = useScrollOverflow(scrollRef, \"horizontal\");\n\n return (\n <div\n ref={scrollRef}\n className={cn(styles.scroll, stackOnMobile && styles.stackable, className)}\n tabIndex={scrollable ? 0 : undefined}\n role={scrollable ? \"group\" : undefined}\n aria-label={scrollable ? scrollLabel : undefined}\n >\n <table className={styles.table}>\n <thead className={cn(stackOnMobile && styles.stackableHead)}>\n <tr>\n {columns.map((column) => (\n <th\n key={column.key}\n className={cn(\n styles.th,\n column.align === \"right\" && styles.alignRight,\n column.align === \"center\" && styles.alignCenter,\n priorityClass(column.priority),\n )}\n style={{ width: column.width }}\n >\n {column.header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {data.length === 0 ? (\n <tr>\n <td className={styles.emptyRow} colSpan={columns.length}>\n {emptyMessage}\n </td>\n </tr>\n ) : (\n data.map((row, index) => (\n <tr\n key={rowKey(row, index)}\n className={cn(\n styles.tr,\n onRowClick && styles.clickable,\n stackOnMobile && styles.stackableRow,\n )}\n onClick={onRowClick ? () => onRowClick(row) : undefined}\n >\n {columns.map((column) => {\n const content = column.render\n ? column.render(row, index)\n : (row as Record<string, ReactNode>)[column.key];\n return (\n <td\n key={column.key}\n className={cn(\n styles.td,\n column.align === \"right\" && styles.alignRight,\n column.align === \"center\" && styles.alignCenter,\n priorityClass(column.priority),\n column.className,\n )}\n data-label={\n typeof column.header === \"string\"\n ? column.header\n : undefined\n }\n >\n {content}\n </td>\n );\n })}\n </tr>\n ))\n )}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"6KAkDA,SAAS,EAAc,EAAyD,CAC5E,GAAI,IAAa,SAAU,OAAO,EAAA,QAAO,eACzC,GAAI,IAAa,UAAW,OAAO,EAAA,QAAO,eAE9C,CASA,SAAgB,EAAS,CACrB,UACA,OACA,SACA,aACA,eAAe,8BACf,YACA,gBAAgB,GAChB,cAAc,kCACA,CACd,IAAM,GAAA,EAAA,EAAA,OAAA,CAAmC,IAAI,EACvC,EAAa,EAAA,kBAAkB,EAAW,YAAY,EAE5D,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,IAAK,EACL,UAAW,EAAA,GAAG,EAAA,QAAO,OAAQ,GAAiB,EAAA,QAAO,UAAW,CAAS,EACzE,SAAU,EAAa,EAAI,IAAA,GAC3B,KAAM,EAAa,QAAU,IAAA,GAC7B,aAAY,EAAa,EAAc,IAAA,aAEvC,EAAA,EAAA,KAAA,CAAC,QAAD,CAAO,UAAW,EAAA,QAAO,eAAzB,EACI,EAAA,EAAA,IAAA,CAAC,QAAD,CAAO,UAAW,EAAA,GAAG,GAAiB,EAAA,QAAO,aAAa,YACtD,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,SACK,EAAQ,IAAK,IACV,EAAA,EAAA,IAAA,CAAC,KAAD,CAEI,UAAW,EAAA,GACP,EAAA,QAAO,GACP,EAAO,QAAU,SAAW,EAAA,QAAO,WACnC,EAAO,QAAU,UAAY,EAAA,QAAO,YACpC,EAAc,EAAO,QAAQ,CACjC,EACA,MAAO,CAAE,MAAO,EAAO,KAAM,WAE5B,EAAO,MACR,EAVK,EAAO,GAUZ,CACP,CACD,CAAA,CACD,CAAA,GACP,EAAA,EAAA,IAAA,CAAC,QAAD,CAAA,SACK,EAAK,SAAW,GACb,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,UACI,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,SAAU,QAAS,EAAQ,gBAC5C,CACD,CAAA,CACJ,CAAA,EAEJ,EAAK,KAAK,EAAK,KACX,EAAA,EAAA,IAAA,CAAC,KAAD,CAEI,UAAW,EAAA,GACP,EAAA,QAAO,GACP,GAAc,EAAA,QAAO,UACrB,GAAiB,EAAA,QAAO,YAC5B,EACA,QAAS,MAAmB,EAAW,CAAG,EAAI,IAAA,YAE7C,EAAQ,IAAK,GAAW,CACrB,IAAM,EAAU,EAAO,OACjB,EAAO,OAAO,EAAK,CAAK,EACvB,EAAkC,EAAO,KAChD,OACI,EAAA,EAAA,IAAA,CAAC,KAAD,CAEI,UAAW,EAAA,GACP,EAAA,QAAO,GACP,EAAO,QAAU,SAAW,EAAA,QAAO,WACnC,EAAO,QAAU,UAAY,EAAA,QAAO,YACpC,EAAc,EAAO,QAAQ,EAC7B,EAAO,SACX,EACA,aACI,OAAO,EAAO,QAAW,SACnB,EAAO,OACP,IAAA,YAGT,CACD,EAfK,EAAO,GAeZ,CAEZ,CAAC,CACD,EAhCK,EAAO,EAAK,CAAK,CAgCtB,CACP,CAEF,CAAA,CACJ,GACN,CAAA,CAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Table.js","names":[],"sources":["../../../src/components/Table/Table.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"Table.js","names":[],"sources":["../../../src/components/Table/Table.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — columns, data and rowKey are the model; onRowClick\n * and emptyMessage the interaction and the empty state; stackOnMobile and\n * scrollLabel are the two ways a table survives a phone, and the label is required\n * because a scroll region needs an accessible name.\n */\nimport { useRef } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { useScrollOverflow } from \"@/hooks/use-scroll-overflow\";\nimport styles from \"./Table.module.css\";\n\nexport type TableAlign = \"left\" | \"right\" | \"center\";\nexport type TablePriority = \"always\" | \"tablet\" | \"desktop\";\n\nexport interface TableColumn<T> {\n key: string;\n header: ReactNode;\n /** Render the cell content. Defaults to `row[key]` if not provided. */\n render?: (row: T, index: number) => ReactNode;\n align?: TableAlign;\n width?: string | number;\n className?: string;\n /**\n * Visibility priority: `always` (default) shows on every viewport,\n * `tablet` hides below md (< 768px), `desktop` hides below lg (< 1024px).\n */\n priority?: TablePriority;\n}\n\nexport interface TableProps<T> {\n columns: TableColumn<T>[];\n data: T[];\n rowKey: (row: T, index: number) => string | number;\n onRowClick?: (row: T) => void;\n emptyMessage?: ReactNode;\n className?: string;\n /**\n * Stack mode — render rows as label/value cards on mobile (< md).\n * Better than horizontal scroll when each row has 3+ columns of dense data.\n */\n stackOnMobile?: boolean;\n /**\n * Accessible name for the scrollable region, used only while the table is\n * actually wider than its box. A focus stop that announces nothing is worse\n * than no focus stop, so name the table when the page holds several.\n */\n scrollLabel?: string;\n}\n\nfunction priorityClass(priority: TablePriority | undefined): string | undefined {\n if (priority === \"tablet\") return styles.priorityTablet;\n if (priority === \"desktop\") return styles.priorityDesktop;\n return undefined;\n}\n\n/**\n * Lightweight table with declarative columns + mobile niceties.\n *\n * - `priority` per column lets less-important data hide on narrow viewports.\n * - `stackOnMobile` re-renders each row as a label/value card on mobile,\n * avoiding horizontal scroll for dense data.\n */\nexport function Table<T>({\n columns,\n data,\n rowKey,\n onRowClick,\n emptyMessage = \"Nenhum registro encontrado.\",\n className,\n stackOnMobile = false,\n scrollLabel = \"Tabela rolável horizontalmente\",\n}: TableProps<T>) {\n const scrollRef = useRef<HTMLDivElement>(null);\n const scrollable = useScrollOverflow(scrollRef, \"horizontal\");\n\n return (\n <div\n ref={scrollRef}\n className={cn(styles.scroll, stackOnMobile && styles.stackable, className)}\n tabIndex={scrollable ? 0 : undefined}\n role={scrollable ? \"group\" : undefined}\n aria-label={scrollable ? scrollLabel : undefined}\n >\n <table className={styles.table}>\n <thead className={cn(stackOnMobile && styles.stackableHead)}>\n <tr>\n {columns.map((column) => (\n <th\n key={column.key}\n className={cn(\n styles.th,\n column.align === \"right\" && styles.alignRight,\n column.align === \"center\" && styles.alignCenter,\n priorityClass(column.priority),\n )}\n style={{ width: column.width }}\n >\n {column.header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {data.length === 0 ? (\n <tr>\n <td className={styles.emptyRow} colSpan={columns.length}>\n {emptyMessage}\n </td>\n </tr>\n ) : (\n data.map((row, index) => (\n <tr\n key={rowKey(row, index)}\n className={cn(\n styles.tr,\n onRowClick && styles.clickable,\n stackOnMobile && styles.stackableRow,\n )}\n onClick={onRowClick ? () => onRowClick(row) : undefined}\n >\n {columns.map((column) => {\n const content = column.render\n ? column.render(row, index)\n : (row as Record<string, ReactNode>)[column.key];\n return (\n <td\n key={column.key}\n className={cn(\n styles.td,\n column.align === \"right\" && styles.alignRight,\n column.align === \"center\" && styles.alignCenter,\n priorityClass(column.priority),\n column.className,\n )}\n data-label={\n typeof column.header === \"string\"\n ? column.header\n : undefined\n }\n >\n {content}\n </td>\n );\n })}\n </tr>\n ))\n )}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":";;;;;;AAkDA,SAAS,EAAc,GAAyD;CAC5E,IAAI,MAAa,UAAU,OAAO,EAAO;CACzC,IAAI,MAAa,WAAW,OAAO,EAAO;AAE9C;AASA,SAAgB,EAAS,EACrB,YACA,SACA,WACA,eACA,kBAAe,+BACf,cACA,mBAAgB,IAChB,iBAAc,oCACA;CACd,IAAM,IAAY,EAAuB,IAAI,GACvC,IAAa,EAAkB,GAAW,YAAY;CAE5D,OACI,kBAAC,OAAD;EACI,KAAK;EACL,WAAW,EAAG,EAAO,QAAQ,KAAiB,EAAO,WAAW,CAAS;EACzE,UAAU,IAAa,IAAI,KAAA;EAC3B,MAAM,IAAa,UAAU,KAAA;EAC7B,cAAY,IAAa,IAAc,KAAA;YAEvC,kBAAC,SAAD;GAAO,WAAW,EAAO;aAAzB,CACI,kBAAC,SAAD;IAAO,WAAW,EAAG,KAAiB,EAAO,aAAa;cACtD,kBAAC,MAAD,EAAA,UACK,EAAQ,KAAK,MACV,kBAAC,MAAD;KAEI,WAAW,EACP,EAAO,IACP,EAAO,UAAU,WAAW,EAAO,YACnC,EAAO,UAAU,YAAY,EAAO,aACpC,EAAc,EAAO,QAAQ,CACjC;KACA,OAAO,EAAE,OAAO,EAAO,MAAM;eAE5B,EAAO;IACR,GAVK,EAAO,GAUZ,CACP,EACD,CAAA;GACD,CAAA,GACP,kBAAC,SAAD,EAAA,UACK,EAAK,WAAW,IACb,kBAAC,MAAD,EAAA,UACI,kBAAC,MAAD;IAAI,WAAW,EAAO;IAAU,SAAS,EAAQ;cAC5C;GACD,CAAA,EACJ,CAAA,IAEJ,EAAK,KAAK,GAAK,MACX,kBAAC,MAAD;IAEI,WAAW,EACP,EAAO,IACP,KAAc,EAAO,WACrB,KAAiB,EAAO,YAC5B;IACA,SAAS,UAAmB,EAAW,CAAG,IAAI,KAAA;cAE7C,EAAQ,KAAK,MAAW;KACrB,IAAM,IAAU,EAAO,SACjB,EAAO,OAAO,GAAK,CAAK,IACvB,EAAkC,EAAO;KAChD,OACI,kBAAC,MAAD;MAEI,WAAW,EACP,EAAO,IACP,EAAO,UAAU,WAAW,EAAO,YACnC,EAAO,UAAU,YAAY,EAAO,aACpC,EAAc,EAAO,QAAQ,GAC7B,EAAO,SACX;MACA,cACI,OAAO,EAAO,UAAW,WACnB,EAAO,SACP,KAAA;gBAGT;KACD,GAfK,EAAO,GAeZ;IAEZ,CAAC;GACD,GAhCK,EAAO,GAAK,CAAK,CAgCtB,CACP,EAEF,CAAA,CACJ;;CACN,CAAA;AAEb"}
|