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":"TreeView.cjs","names":[],"sources":["../../../src/components/TreeView/TreeView.tsx"],"sourcesContent":["import { ChevronRight } from \"lucide-react\";\nimport { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./TreeView.module.css\";\n\n/** One node of the tree. Children make it a branch; no children makes it a leaf. */\nexport interface TreeNode {\n /** Stable identifier, unique across the whole tree. */\n id: string;\n /** Rendered label. */\n label: ReactNode;\n /** Child nodes. An empty array still renders as a branch (an empty folder). */\n children?: TreeNode[];\n /** Icon rendered before the label. */\n icon?: ReactNode;\n /** Blocks selection and expansion, and skips the node in keyboard navigation. */\n disabled?: boolean;\n}\n\nexport interface TreeViewProps {\n /** Root nodes. */\n nodes: TreeNode[];\n /** Controlled expanded ids. */\n expandedIds?: string[];\n /** Uncontrolled initial expanded ids. */\n defaultExpandedIds?: string[];\n onExpandedChange?: (expandedIds: string[]) => void;\n /** Controlled selected id. `null` means nothing selected. */\n selectedId?: string | null;\n /** Uncontrolled initial selection. */\n defaultSelectedId?: string | null;\n onSelect?: (node: TreeNode) => void;\n /**\n * Selecting a branch also toggles it. Default `true` — matches how a file\n * explorer behaves; set `false` when a branch is itself a meaningful choice\n * (a category that owns items, for instance).\n */\n toggleOnSelect?: boolean;\n /** Accessible name for the tree. */\n label?: string;\n className?: string;\n}\n\ninterface FlatNode {\n node: TreeNode;\n depth: number;\n parentId: string | null;\n hasChildren: boolean;\n expanded: boolean;\n}\n\n/**\n * Walk the tree into the list of *currently visible* rows.\n *\n * Keyboard navigation works on this flattened view, which is what makes\n * ArrowDown from the last child of a collapsed-sibling branch land on the right\n * row: rows that are not rendered simply are not in the list.\n */\nfunction flatten(\n nodes: TreeNode[],\n expandedIds: Set<string>,\n depth = 0,\n parentId: string | null = null,\n out: FlatNode[] = [],\n): FlatNode[] {\n for (const node of nodes) {\n const hasChildren = Array.isArray(node.children);\n const expanded = hasChildren && expandedIds.has(node.id);\n out.push({ node, depth, parentId, hasChildren, expanded });\n if (expanded && node.children) {\n flatten(node.children, expandedIds, depth + 1, node.id, out);\n }\n }\n return out;\n}\n\n/**\n * Accessible tree for hierarchical data — categories, permissions, folders, an\n * org chart.\n *\n * Implements the `tree` role with **roving tabindex**: exactly one row is\n * tabbable, and the arrow keys move focus within the widget. That is what keeps a\n * 500-node tree from adding 500 stops to the page's tab order.\n *\n * Keyboard map: `↓`/`↑` move, `→` expands (or descends), `←` collapses (or goes to\n * the parent), `Home`/`End` jump to the first/last visible row, `Enter`/`Space`\n * select.\n *\n * The chevron is decoration (`aria-hidden`), not a button: the row itself carries\n * `aria-expanded`, so a second focusable control there would only add noise for a\n * screen reader while duplicating an action the keyboard map already has. It still\n * accepts a click, with the event stopped so it toggles without also selecting.\n *\n * @example\n * ```tsx\n * const nodes: TreeNode[] = [\n * {\n * id: \"vendas\",\n * label: \"Vendas\",\n * children: [\n * { id: \"vendas.ler\", label: \"Ler\" },\n * { id: \"vendas.editar\", label: \"Editar\" },\n * ],\n * },\n * { id: \"config\", label: \"Configurações\", children: [] },\n * ];\n *\n * <TreeView nodes={nodes} defaultExpandedIds={[\"vendas\"]} onSelect={(node) => console.log(node.id)} />\n * ```\n */\nexport function TreeView({\n nodes,\n expandedIds,\n defaultExpandedIds = [],\n onExpandedChange,\n selectedId,\n defaultSelectedId = null,\n onSelect,\n toggleOnSelect = true,\n label,\n className,\n}: TreeViewProps) {\n const expandedControlled = expandedIds !== undefined;\n const [internalExpanded, setInternalExpanded] = useState<string[]>(defaultExpandedIds);\n const expanded = expandedControlled ? expandedIds : internalExpanded;\n\n const selectionControlled = selectedId !== undefined;\n const [internalSelected, setInternalSelected] = useState<string | null>(defaultSelectedId);\n const selected = selectionControlled ? selectedId : internalSelected;\n\n const expandedSet = useMemo(() => new Set(expanded), [expanded]);\n const rows = useMemo(() => flatten(nodes, expandedSet), [nodes, expandedSet]);\n\n const [focusedId, setFocusedId] = useState<string | null>(null);\n const containerRef = useRef<HTMLUListElement>(null);\n\n const activeId =\n focusedId ?? selected ?? rows.find((row) => !row.node.disabled)?.node.id ?? null;\n\n const setExpanded = useCallback(\n (next: string[]): void => {\n if (!expandedControlled) setInternalExpanded(next);\n onExpandedChange?.(next);\n },\n [expandedControlled, onExpandedChange],\n );\n\n const toggle = useCallback(\n (id: string): void => {\n setExpanded(\n expanded.includes(id) ? expanded.filter((x) => x !== id) : [...expanded, id],\n );\n },\n [expanded, setExpanded],\n );\n\n const select = useCallback(\n (row: FlatNode): void => {\n if (row.node.disabled) return;\n if (!selectionControlled) setInternalSelected(row.node.id);\n setFocusedId(row.node.id);\n onSelect?.(row.node);\n if (row.hasChildren && toggleOnSelect) toggle(row.node.id);\n },\n [onSelect, selectionControlled, toggle, toggleOnSelect],\n );\n\n /** Move DOM focus to a row by id, so the roving tabindex actually roves. */\n const focusRow = useCallback((id: string): void => {\n setFocusedId(id);\n const element = containerRef.current?.querySelector<HTMLElement>(`[data-tree-id=\"${id}\"]`);\n element?.focus();\n }, []);\n\n const moveFocus = useCallback(\n (from: number, delta: number): void => {\n for (let index = from + delta; index >= 0 && index < rows.length; index += delta) {\n if (!rows[index].node.disabled) {\n focusRow(rows[index].node.id);\n return;\n }\n }\n },\n [rows, focusRow],\n );\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>, row: FlatNode, index: number): void => {\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n moveFocus(index, 1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n moveFocus(index, -1);\n break;\n case \"ArrowRight\":\n event.preventDefault();\n if (row.hasChildren && !row.expanded) toggle(row.node.id);\n else if (row.expanded) moveFocus(index, 1);\n break;\n case \"ArrowLeft\":\n event.preventDefault();\n if (row.expanded) {\n toggle(row.node.id);\n } else if (row.parentId) {\n focusRow(row.parentId);\n }\n break;\n case \"Home\":\n event.preventDefault();\n moveFocus(-1, 1);\n break;\n case \"End\":\n event.preventDefault();\n moveFocus(rows.length, -1);\n break;\n case \"Enter\":\n case \" \":\n event.preventDefault();\n select(row);\n break;\n default:\n break;\n }\n },\n [moveFocus, rows.length, select, toggle, focusRow],\n );\n\n return (\n <ul\n ref={containerRef}\n role=\"tree\"\n aria-label={label}\n className={cn(styles.tree, className)}\n >\n {rows.map((row, index) => {\n const isSelected = selected === row.node.id;\n return (\n <li\n key={row.node.id}\n role=\"treeitem\"\n aria-expanded={row.hasChildren ? row.expanded : undefined}\n aria-selected={isSelected}\n aria-level={row.depth + 1}\n aria-disabled={row.node.disabled || undefined}\n className={styles.item}\n >\n <div\n data-tree-id={row.node.id}\n className={cn(\n styles.row,\n isSelected && styles.selected,\n row.node.disabled && styles.disabled,\n )}\n style={{\n paddingInlineStart: `calc(${row.depth} * var(--tempest-space-5))`,\n }}\n tabIndex={activeId === row.node.id && !row.node.disabled ? 0 : -1}\n onClick={() => select(row)}\n onFocus={() => setFocusedId(row.node.id)}\n onKeyDown={(event) => handleKeyDown(event, row, index)}\n >\n {row.hasChildren ? (\n <span\n className={cn(\n styles.chevron,\n row.expanded && styles.chevronOpen,\n )}\n aria-hidden=\"true\"\n onClick={(event) => {\n event.stopPropagation();\n if (!row.node.disabled) toggle(row.node.id);\n }}\n >\n <ChevronRight size={14} />\n </span>\n ) : (\n <span className={styles.chevronPlaceholder} aria-hidden=\"true\" />\n )}\n {row.node.icon ? (\n <span className={styles.icon} aria-hidden=\"true\">\n {row.node.icon}\n </span>\n ) : null}\n <span className={styles.label}>{row.node.label}</span>\n </div>\n </li>\n );\n })}\n </ul>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TreeView.cjs","names":[],"sources":["../../../src/components/TreeView/TreeView.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — expansion and selection\n * are two independent controlled states, each with its uncontrolled twin\n * (expandedIds/defaultExpandedIds/onExpandedChange, selectedId/\n * defaultSelectedId/onSelect), plus toggleOnSelect for the case where they should\n * move together. The body is the flattened visible list and the roving focus over\n * it.\n */\nimport { ChevronRight } from \"lucide-react\";\nimport { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./TreeView.module.css\";\n\n/** One node of the tree. Children make it a branch; no children makes it a leaf. */\nexport interface TreeNode {\n /** Stable identifier, unique across the whole tree. */\n id: string;\n /** Rendered label. */\n label: ReactNode;\n /** Child nodes. An empty array still renders as a branch (an empty folder). */\n children?: TreeNode[];\n /** Icon rendered before the label. */\n icon?: ReactNode;\n /** Blocks selection and expansion, and skips the node in keyboard navigation. */\n disabled?: boolean;\n}\n\nexport interface TreeViewProps {\n /** Root nodes. */\n nodes: TreeNode[];\n /** Controlled expanded ids. */\n expandedIds?: string[];\n /** Uncontrolled initial expanded ids. */\n defaultExpandedIds?: string[];\n onExpandedChange?: (expandedIds: string[]) => void;\n /** Controlled selected id. `null` means nothing selected. */\n selectedId?: string | null;\n /** Uncontrolled initial selection. */\n defaultSelectedId?: string | null;\n onSelect?: (node: TreeNode) => void;\n /**\n * Selecting a branch also toggles it. Default `true` — matches how a file\n * explorer behaves; set `false` when a branch is itself a meaningful choice\n * (a category that owns items, for instance).\n */\n toggleOnSelect?: boolean;\n /** Accessible name for the tree. */\n label?: string;\n className?: string;\n}\n\ninterface FlatNode {\n node: TreeNode;\n depth: number;\n parentId: string | null;\n hasChildren: boolean;\n expanded: boolean;\n}\n\n/**\n * Walk the tree into the list of *currently visible* rows.\n *\n * Keyboard navigation works on this flattened view, which is what makes\n * ArrowDown from the last child of a collapsed-sibling branch land on the right\n * row: rows that are not rendered simply are not in the list.\n */\nfunction flatten(\n nodes: TreeNode[],\n expandedIds: Set<string>,\n depth = 0,\n parentId: string | null = null,\n out: FlatNode[] = [],\n): FlatNode[] {\n for (const node of nodes) {\n const hasChildren = Array.isArray(node.children);\n const expanded = hasChildren && expandedIds.has(node.id);\n out.push({ node, depth, parentId, hasChildren, expanded });\n if (expanded && node.children) {\n flatten(node.children, expandedIds, depth + 1, node.id, out);\n }\n }\n return out;\n}\n\n/**\n * Accessible tree for hierarchical data — categories, permissions, folders, an\n * org chart.\n *\n * Implements the `tree` role with **roving tabindex**: exactly one row is\n * tabbable, and the arrow keys move focus within the widget. That is what keeps a\n * 500-node tree from adding 500 stops to the page's tab order.\n *\n * Keyboard map: `↓`/`↑` move, `→` expands (or descends), `←` collapses (or goes to\n * the parent), `Home`/`End` jump to the first/last visible row, `Enter`/`Space`\n * select.\n *\n * The chevron is decoration (`aria-hidden`), not a button: the row itself carries\n * `aria-expanded`, so a second focusable control there would only add noise for a\n * screen reader while duplicating an action the keyboard map already has. It still\n * accepts a click, with the event stopped so it toggles without also selecting.\n *\n * @example\n * ```tsx\n * const nodes: TreeNode[] = [\n * {\n * id: \"vendas\",\n * label: \"Vendas\",\n * children: [\n * { id: \"vendas.ler\", label: \"Ler\" },\n * { id: \"vendas.editar\", label: \"Editar\" },\n * ],\n * },\n * { id: \"config\", label: \"Configurações\", children: [] },\n * ];\n *\n * <TreeView nodes={nodes} defaultExpandedIds={[\"vendas\"]} onSelect={(node) => console.log(node.id)} />\n * ```\n */\nexport function TreeView({\n nodes,\n expandedIds,\n defaultExpandedIds = [],\n onExpandedChange,\n selectedId,\n defaultSelectedId = null,\n onSelect,\n toggleOnSelect = true,\n label,\n className,\n}: TreeViewProps) {\n const expandedControlled = expandedIds !== undefined;\n const [internalExpanded, setInternalExpanded] = useState<string[]>(defaultExpandedIds);\n const expanded = expandedControlled ? expandedIds : internalExpanded;\n\n const selectionControlled = selectedId !== undefined;\n const [internalSelected, setInternalSelected] = useState<string | null>(defaultSelectedId);\n const selected = selectionControlled ? selectedId : internalSelected;\n\n const expandedSet = useMemo(() => new Set(expanded), [expanded]);\n const rows = useMemo(() => flatten(nodes, expandedSet), [nodes, expandedSet]);\n\n const [focusedId, setFocusedId] = useState<string | null>(null);\n const containerRef = useRef<HTMLUListElement>(null);\n\n const activeId =\n focusedId ?? selected ?? rows.find((row) => !row.node.disabled)?.node.id ?? null;\n\n const setExpanded = useCallback(\n (next: string[]): void => {\n if (!expandedControlled) setInternalExpanded(next);\n onExpandedChange?.(next);\n },\n [expandedControlled, onExpandedChange],\n );\n\n const toggle = useCallback(\n (id: string): void => {\n setExpanded(\n expanded.includes(id) ? expanded.filter((x) => x !== id) : [...expanded, id],\n );\n },\n [expanded, setExpanded],\n );\n\n const select = useCallback(\n (row: FlatNode): void => {\n if (row.node.disabled) return;\n if (!selectionControlled) setInternalSelected(row.node.id);\n setFocusedId(row.node.id);\n onSelect?.(row.node);\n if (row.hasChildren && toggleOnSelect) toggle(row.node.id);\n },\n [onSelect, selectionControlled, toggle, toggleOnSelect],\n );\n\n /** Move DOM focus to a row by id, so the roving tabindex actually roves. */\n const focusRow = useCallback((id: string): void => {\n setFocusedId(id);\n const element = containerRef.current?.querySelector<HTMLElement>(`[data-tree-id=\"${id}\"]`);\n element?.focus();\n }, []);\n\n const moveFocus = useCallback(\n (from: number, delta: number): void => {\n for (let index = from + delta; index >= 0 && index < rows.length; index += delta) {\n if (!rows[index].node.disabled) {\n focusRow(rows[index].node.id);\n return;\n }\n }\n },\n [rows, focusRow],\n );\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>, row: FlatNode, index: number): void => {\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n moveFocus(index, 1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n moveFocus(index, -1);\n break;\n case \"ArrowRight\":\n event.preventDefault();\n if (row.hasChildren && !row.expanded) toggle(row.node.id);\n else if (row.expanded) moveFocus(index, 1);\n break;\n case \"ArrowLeft\":\n event.preventDefault();\n if (row.expanded) {\n toggle(row.node.id);\n } else if (row.parentId) {\n focusRow(row.parentId);\n }\n break;\n case \"Home\":\n event.preventDefault();\n moveFocus(-1, 1);\n break;\n case \"End\":\n event.preventDefault();\n moveFocus(rows.length, -1);\n break;\n case \"Enter\":\n case \" \":\n event.preventDefault();\n select(row);\n break;\n default:\n break;\n }\n },\n [moveFocus, rows.length, select, toggle, focusRow],\n );\n\n return (\n <ul\n ref={containerRef}\n role=\"tree\"\n aria-label={label}\n className={cn(styles.tree, className)}\n >\n {rows.map((row, index) => {\n const isSelected = selected === row.node.id;\n return (\n <li\n key={row.node.id}\n role=\"treeitem\"\n aria-expanded={row.hasChildren ? row.expanded : undefined}\n aria-selected={isSelected}\n aria-level={row.depth + 1}\n aria-disabled={row.node.disabled || undefined}\n className={styles.item}\n >\n <div\n data-tree-id={row.node.id}\n className={cn(\n styles.row,\n isSelected && styles.selected,\n row.node.disabled && styles.disabled,\n )}\n style={{\n paddingInlineStart: `calc(${row.depth} * var(--tempest-space-5))`,\n }}\n tabIndex={activeId === row.node.id && !row.node.disabled ? 0 : -1}\n onClick={() => select(row)}\n onFocus={() => setFocusedId(row.node.id)}\n onKeyDown={(event) => handleKeyDown(event, row, index)}\n >\n {row.hasChildren ? (\n <span\n className={cn(\n styles.chevron,\n row.expanded && styles.chevronOpen,\n )}\n aria-hidden=\"true\"\n onClick={(event) => {\n event.stopPropagation();\n if (!row.node.disabled) toggle(row.node.id);\n }}\n >\n <ChevronRight size={14} />\n </span>\n ) : (\n <span className={styles.chevronPlaceholder} aria-hidden=\"true\" />\n )}\n {row.node.icon ? (\n <span className={styles.icon} aria-hidden=\"true\">\n {row.node.icon}\n </span>\n ) : null}\n <span className={styles.label}>{row.node.label}</span>\n </div>\n </li>\n );\n })}\n </ul>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TreeView.js","names":[],"sources":["../../../src/components/TreeView/TreeView.tsx"],"sourcesContent":["import { ChevronRight } from \"lucide-react\";\nimport { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./TreeView.module.css\";\n\n/** One node of the tree. Children make it a branch; no children makes it a leaf. */\nexport interface TreeNode {\n /** Stable identifier, unique across the whole tree. */\n id: string;\n /** Rendered label. */\n label: ReactNode;\n /** Child nodes. An empty array still renders as a branch (an empty folder). */\n children?: TreeNode[];\n /** Icon rendered before the label. */\n icon?: ReactNode;\n /** Blocks selection and expansion, and skips the node in keyboard navigation. */\n disabled?: boolean;\n}\n\nexport interface TreeViewProps {\n /** Root nodes. */\n nodes: TreeNode[];\n /** Controlled expanded ids. */\n expandedIds?: string[];\n /** Uncontrolled initial expanded ids. */\n defaultExpandedIds?: string[];\n onExpandedChange?: (expandedIds: string[]) => void;\n /** Controlled selected id. `null` means nothing selected. */\n selectedId?: string | null;\n /** Uncontrolled initial selection. */\n defaultSelectedId?: string | null;\n onSelect?: (node: TreeNode) => void;\n /**\n * Selecting a branch also toggles it. Default `true` — matches how a file\n * explorer behaves; set `false` when a branch is itself a meaningful choice\n * (a category that owns items, for instance).\n */\n toggleOnSelect?: boolean;\n /** Accessible name for the tree. */\n label?: string;\n className?: string;\n}\n\ninterface FlatNode {\n node: TreeNode;\n depth: number;\n parentId: string | null;\n hasChildren: boolean;\n expanded: boolean;\n}\n\n/**\n * Walk the tree into the list of *currently visible* rows.\n *\n * Keyboard navigation works on this flattened view, which is what makes\n * ArrowDown from the last child of a collapsed-sibling branch land on the right\n * row: rows that are not rendered simply are not in the list.\n */\nfunction flatten(\n nodes: TreeNode[],\n expandedIds: Set<string>,\n depth = 0,\n parentId: string | null = null,\n out: FlatNode[] = [],\n): FlatNode[] {\n for (const node of nodes) {\n const hasChildren = Array.isArray(node.children);\n const expanded = hasChildren && expandedIds.has(node.id);\n out.push({ node, depth, parentId, hasChildren, expanded });\n if (expanded && node.children) {\n flatten(node.children, expandedIds, depth + 1, node.id, out);\n }\n }\n return out;\n}\n\n/**\n * Accessible tree for hierarchical data — categories, permissions, folders, an\n * org chart.\n *\n * Implements the `tree` role with **roving tabindex**: exactly one row is\n * tabbable, and the arrow keys move focus within the widget. That is what keeps a\n * 500-node tree from adding 500 stops to the page's tab order.\n *\n * Keyboard map: `↓`/`↑` move, `→` expands (or descends), `←` collapses (or goes to\n * the parent), `Home`/`End` jump to the first/last visible row, `Enter`/`Space`\n * select.\n *\n * The chevron is decoration (`aria-hidden`), not a button: the row itself carries\n * `aria-expanded`, so a second focusable control there would only add noise for a\n * screen reader while duplicating an action the keyboard map already has. It still\n * accepts a click, with the event stopped so it toggles without also selecting.\n *\n * @example\n * ```tsx\n * const nodes: TreeNode[] = [\n * {\n * id: \"vendas\",\n * label: \"Vendas\",\n * children: [\n * { id: \"vendas.ler\", label: \"Ler\" },\n * { id: \"vendas.editar\", label: \"Editar\" },\n * ],\n * },\n * { id: \"config\", label: \"Configurações\", children: [] },\n * ];\n *\n * <TreeView nodes={nodes} defaultExpandedIds={[\"vendas\"]} onSelect={(node) => console.log(node.id)} />\n * ```\n */\nexport function TreeView({\n nodes,\n expandedIds,\n defaultExpandedIds = [],\n onExpandedChange,\n selectedId,\n defaultSelectedId = null,\n onSelect,\n toggleOnSelect = true,\n label,\n className,\n}: TreeViewProps) {\n const expandedControlled = expandedIds !== undefined;\n const [internalExpanded, setInternalExpanded] = useState<string[]>(defaultExpandedIds);\n const expanded = expandedControlled ? expandedIds : internalExpanded;\n\n const selectionControlled = selectedId !== undefined;\n const [internalSelected, setInternalSelected] = useState<string | null>(defaultSelectedId);\n const selected = selectionControlled ? selectedId : internalSelected;\n\n const expandedSet = useMemo(() => new Set(expanded), [expanded]);\n const rows = useMemo(() => flatten(nodes, expandedSet), [nodes, expandedSet]);\n\n const [focusedId, setFocusedId] = useState<string | null>(null);\n const containerRef = useRef<HTMLUListElement>(null);\n\n const activeId =\n focusedId ?? selected ?? rows.find((row) => !row.node.disabled)?.node.id ?? null;\n\n const setExpanded = useCallback(\n (next: string[]): void => {\n if (!expandedControlled) setInternalExpanded(next);\n onExpandedChange?.(next);\n },\n [expandedControlled, onExpandedChange],\n );\n\n const toggle = useCallback(\n (id: string): void => {\n setExpanded(\n expanded.includes(id) ? expanded.filter((x) => x !== id) : [...expanded, id],\n );\n },\n [expanded, setExpanded],\n );\n\n const select = useCallback(\n (row: FlatNode): void => {\n if (row.node.disabled) return;\n if (!selectionControlled) setInternalSelected(row.node.id);\n setFocusedId(row.node.id);\n onSelect?.(row.node);\n if (row.hasChildren && toggleOnSelect) toggle(row.node.id);\n },\n [onSelect, selectionControlled, toggle, toggleOnSelect],\n );\n\n /** Move DOM focus to a row by id, so the roving tabindex actually roves. */\n const focusRow = useCallback((id: string): void => {\n setFocusedId(id);\n const element = containerRef.current?.querySelector<HTMLElement>(`[data-tree-id=\"${id}\"]`);\n element?.focus();\n }, []);\n\n const moveFocus = useCallback(\n (from: number, delta: number): void => {\n for (let index = from + delta; index >= 0 && index < rows.length; index += delta) {\n if (!rows[index].node.disabled) {\n focusRow(rows[index].node.id);\n return;\n }\n }\n },\n [rows, focusRow],\n );\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>, row: FlatNode, index: number): void => {\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n moveFocus(index, 1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n moveFocus(index, -1);\n break;\n case \"ArrowRight\":\n event.preventDefault();\n if (row.hasChildren && !row.expanded) toggle(row.node.id);\n else if (row.expanded) moveFocus(index, 1);\n break;\n case \"ArrowLeft\":\n event.preventDefault();\n if (row.expanded) {\n toggle(row.node.id);\n } else if (row.parentId) {\n focusRow(row.parentId);\n }\n break;\n case \"Home\":\n event.preventDefault();\n moveFocus(-1, 1);\n break;\n case \"End\":\n event.preventDefault();\n moveFocus(rows.length, -1);\n break;\n case \"Enter\":\n case \" \":\n event.preventDefault();\n select(row);\n break;\n default:\n break;\n }\n },\n [moveFocus, rows.length, select, toggle, focusRow],\n );\n\n return (\n <ul\n ref={containerRef}\n role=\"tree\"\n aria-label={label}\n className={cn(styles.tree, className)}\n >\n {rows.map((row, index) => {\n const isSelected = selected === row.node.id;\n return (\n <li\n key={row.node.id}\n role=\"treeitem\"\n aria-expanded={row.hasChildren ? row.expanded : undefined}\n aria-selected={isSelected}\n aria-level={row.depth + 1}\n aria-disabled={row.node.disabled || undefined}\n className={styles.item}\n >\n <div\n data-tree-id={row.node.id}\n className={cn(\n styles.row,\n isSelected && styles.selected,\n row.node.disabled && styles.disabled,\n )}\n style={{\n paddingInlineStart: `calc(${row.depth} * var(--tempest-space-5))`,\n }}\n tabIndex={activeId === row.node.id && !row.node.disabled ? 0 : -1}\n onClick={() => select(row)}\n onFocus={() => setFocusedId(row.node.id)}\n onKeyDown={(event) => handleKeyDown(event, row, index)}\n >\n {row.hasChildren ? (\n <span\n className={cn(\n styles.chevron,\n row.expanded && styles.chevronOpen,\n )}\n aria-hidden=\"true\"\n onClick={(event) => {\n event.stopPropagation();\n if (!row.node.disabled) toggle(row.node.id);\n }}\n >\n <ChevronRight size={14} />\n </span>\n ) : (\n <span className={styles.chevronPlaceholder} aria-hidden=\"true\" />\n )}\n {row.node.icon ? (\n <span className={styles.icon} aria-hidden=\"true\">\n {row.node.icon}\n </span>\n ) : null}\n <span className={styles.label}>{row.node.label}</span>\n </div>\n </li>\n );\n })}\n </ul>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TreeView.js","names":[],"sources":["../../../src/components/TreeView/TreeView.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — expansion and selection\n * are two independent controlled states, each with its uncontrolled twin\n * (expandedIds/defaultExpandedIds/onExpandedChange, selectedId/\n * defaultSelectedId/onSelect), plus toggleOnSelect for the case where they should\n * move together. The body is the flattened visible list and the roving focus over\n * it.\n */\nimport { ChevronRight } from \"lucide-react\";\nimport { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./TreeView.module.css\";\n\n/** One node of the tree. Children make it a branch; no children makes it a leaf. */\nexport interface TreeNode {\n /** Stable identifier, unique across the whole tree. */\n id: string;\n /** Rendered label. */\n label: ReactNode;\n /** Child nodes. An empty array still renders as a branch (an empty folder). */\n children?: TreeNode[];\n /** Icon rendered before the label. */\n icon?: ReactNode;\n /** Blocks selection and expansion, and skips the node in keyboard navigation. */\n disabled?: boolean;\n}\n\nexport interface TreeViewProps {\n /** Root nodes. */\n nodes: TreeNode[];\n /** Controlled expanded ids. */\n expandedIds?: string[];\n /** Uncontrolled initial expanded ids. */\n defaultExpandedIds?: string[];\n onExpandedChange?: (expandedIds: string[]) => void;\n /** Controlled selected id. `null` means nothing selected. */\n selectedId?: string | null;\n /** Uncontrolled initial selection. */\n defaultSelectedId?: string | null;\n onSelect?: (node: TreeNode) => void;\n /**\n * Selecting a branch also toggles it. Default `true` — matches how a file\n * explorer behaves; set `false` when a branch is itself a meaningful choice\n * (a category that owns items, for instance).\n */\n toggleOnSelect?: boolean;\n /** Accessible name for the tree. */\n label?: string;\n className?: string;\n}\n\ninterface FlatNode {\n node: TreeNode;\n depth: number;\n parentId: string | null;\n hasChildren: boolean;\n expanded: boolean;\n}\n\n/**\n * Walk the tree into the list of *currently visible* rows.\n *\n * Keyboard navigation works on this flattened view, which is what makes\n * ArrowDown from the last child of a collapsed-sibling branch land on the right\n * row: rows that are not rendered simply are not in the list.\n */\nfunction flatten(\n nodes: TreeNode[],\n expandedIds: Set<string>,\n depth = 0,\n parentId: string | null = null,\n out: FlatNode[] = [],\n): FlatNode[] {\n for (const node of nodes) {\n const hasChildren = Array.isArray(node.children);\n const expanded = hasChildren && expandedIds.has(node.id);\n out.push({ node, depth, parentId, hasChildren, expanded });\n if (expanded && node.children) {\n flatten(node.children, expandedIds, depth + 1, node.id, out);\n }\n }\n return out;\n}\n\n/**\n * Accessible tree for hierarchical data — categories, permissions, folders, an\n * org chart.\n *\n * Implements the `tree` role with **roving tabindex**: exactly one row is\n * tabbable, and the arrow keys move focus within the widget. That is what keeps a\n * 500-node tree from adding 500 stops to the page's tab order.\n *\n * Keyboard map: `↓`/`↑` move, `→` expands (or descends), `←` collapses (or goes to\n * the parent), `Home`/`End` jump to the first/last visible row, `Enter`/`Space`\n * select.\n *\n * The chevron is decoration (`aria-hidden`), not a button: the row itself carries\n * `aria-expanded`, so a second focusable control there would only add noise for a\n * screen reader while duplicating an action the keyboard map already has. It still\n * accepts a click, with the event stopped so it toggles without also selecting.\n *\n * @example\n * ```tsx\n * const nodes: TreeNode[] = [\n * {\n * id: \"vendas\",\n * label: \"Vendas\",\n * children: [\n * { id: \"vendas.ler\", label: \"Ler\" },\n * { id: \"vendas.editar\", label: \"Editar\" },\n * ],\n * },\n * { id: \"config\", label: \"Configurações\", children: [] },\n * ];\n *\n * <TreeView nodes={nodes} defaultExpandedIds={[\"vendas\"]} onSelect={(node) => console.log(node.id)} />\n * ```\n */\nexport function TreeView({\n nodes,\n expandedIds,\n defaultExpandedIds = [],\n onExpandedChange,\n selectedId,\n defaultSelectedId = null,\n onSelect,\n toggleOnSelect = true,\n label,\n className,\n}: TreeViewProps) {\n const expandedControlled = expandedIds !== undefined;\n const [internalExpanded, setInternalExpanded] = useState<string[]>(defaultExpandedIds);\n const expanded = expandedControlled ? expandedIds : internalExpanded;\n\n const selectionControlled = selectedId !== undefined;\n const [internalSelected, setInternalSelected] = useState<string | null>(defaultSelectedId);\n const selected = selectionControlled ? selectedId : internalSelected;\n\n const expandedSet = useMemo(() => new Set(expanded), [expanded]);\n const rows = useMemo(() => flatten(nodes, expandedSet), [nodes, expandedSet]);\n\n const [focusedId, setFocusedId] = useState<string | null>(null);\n const containerRef = useRef<HTMLUListElement>(null);\n\n const activeId =\n focusedId ?? selected ?? rows.find((row) => !row.node.disabled)?.node.id ?? null;\n\n const setExpanded = useCallback(\n (next: string[]): void => {\n if (!expandedControlled) setInternalExpanded(next);\n onExpandedChange?.(next);\n },\n [expandedControlled, onExpandedChange],\n );\n\n const toggle = useCallback(\n (id: string): void => {\n setExpanded(\n expanded.includes(id) ? expanded.filter((x) => x !== id) : [...expanded, id],\n );\n },\n [expanded, setExpanded],\n );\n\n const select = useCallback(\n (row: FlatNode): void => {\n if (row.node.disabled) return;\n if (!selectionControlled) setInternalSelected(row.node.id);\n setFocusedId(row.node.id);\n onSelect?.(row.node);\n if (row.hasChildren && toggleOnSelect) toggle(row.node.id);\n },\n [onSelect, selectionControlled, toggle, toggleOnSelect],\n );\n\n /** Move DOM focus to a row by id, so the roving tabindex actually roves. */\n const focusRow = useCallback((id: string): void => {\n setFocusedId(id);\n const element = containerRef.current?.querySelector<HTMLElement>(`[data-tree-id=\"${id}\"]`);\n element?.focus();\n }, []);\n\n const moveFocus = useCallback(\n (from: number, delta: number): void => {\n for (let index = from + delta; index >= 0 && index < rows.length; index += delta) {\n if (!rows[index].node.disabled) {\n focusRow(rows[index].node.id);\n return;\n }\n }\n },\n [rows, focusRow],\n );\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>, row: FlatNode, index: number): void => {\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n moveFocus(index, 1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n moveFocus(index, -1);\n break;\n case \"ArrowRight\":\n event.preventDefault();\n if (row.hasChildren && !row.expanded) toggle(row.node.id);\n else if (row.expanded) moveFocus(index, 1);\n break;\n case \"ArrowLeft\":\n event.preventDefault();\n if (row.expanded) {\n toggle(row.node.id);\n } else if (row.parentId) {\n focusRow(row.parentId);\n }\n break;\n case \"Home\":\n event.preventDefault();\n moveFocus(-1, 1);\n break;\n case \"End\":\n event.preventDefault();\n moveFocus(rows.length, -1);\n break;\n case \"Enter\":\n case \" \":\n event.preventDefault();\n select(row);\n break;\n default:\n break;\n }\n },\n [moveFocus, rows.length, select, toggle, focusRow],\n );\n\n return (\n <ul\n ref={containerRef}\n role=\"tree\"\n aria-label={label}\n className={cn(styles.tree, className)}\n >\n {rows.map((row, index) => {\n const isSelected = selected === row.node.id;\n return (\n <li\n key={row.node.id}\n role=\"treeitem\"\n aria-expanded={row.hasChildren ? row.expanded : undefined}\n aria-selected={isSelected}\n aria-level={row.depth + 1}\n aria-disabled={row.node.disabled || undefined}\n className={styles.item}\n >\n <div\n data-tree-id={row.node.id}\n className={cn(\n styles.row,\n isSelected && styles.selected,\n row.node.disabled && styles.disabled,\n )}\n style={{\n paddingInlineStart: `calc(${row.depth} * var(--tempest-space-5))`,\n }}\n tabIndex={activeId === row.node.id && !row.node.disabled ? 0 : -1}\n onClick={() => select(row)}\n onFocus={() => setFocusedId(row.node.id)}\n onKeyDown={(event) => handleKeyDown(event, row, index)}\n >\n {row.hasChildren ? (\n <span\n className={cn(\n styles.chevron,\n row.expanded && styles.chevronOpen,\n )}\n aria-hidden=\"true\"\n onClick={(event) => {\n event.stopPropagation();\n if (!row.node.disabled) toggle(row.node.id);\n }}\n >\n <ChevronRight size={14} />\n </span>\n ) : (\n <span className={styles.chevronPlaceholder} aria-hidden=\"true\" />\n )}\n {row.node.icon ? (\n <span className={styles.icon} aria-hidden=\"true\">\n {row.node.icon}\n </span>\n ) : null}\n <span className={styles.label}>{row.node.label}</span>\n </div>\n </li>\n );\n })}\n </ul>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VirtualList.cjs","names":[],"sources":["../../../src/components/VirtualList/VirtualList.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"VirtualList.cjs","names":[],"sources":["../../../src/components/VirtualList/VirtualList.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — items, itemHeight, height and overscan are the\n * window maths; renderItem and getKey the row contract; label the accessible name of\n * a scroll region. Nine props is what windowing costs — the alternative is the app\n * doing the maths.\n */\nimport { useEffect, useRef, useState } from \"react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./VirtualList.module.css\";\n\nexport interface VirtualListProps<T> {\n items: T[];\n /** Fixed pixel height for each row. */\n itemHeight: number;\n /** Render a single row. */\n renderItem: (item: T, index: number) => ReactNode;\n /** Container height (px) or any CSS length. */\n height: number | string;\n /** Number of items rendered above/below the viewport. Default: 4. */\n overscan?: number;\n /** Stable key derivation; defaults to the index. */\n getKey?: (item: T, index: number) => string | number;\n className?: string;\n style?: CSSProperties;\n /**\n * Accessible name for the list. Worth setting when the list scrolls: it\n * then carries a tab stop of its own, and a focus stop that announces\n * nothing is worse than no focus stop.\n */\n label?: string;\n}\n\n/**\n * Fixed-height virtual list. Renders only the visible window plus a small\n * overscan buffer. Suitable for lists of thousands of identical rows.\n *\n * For variable heights, use `react-window`/`@tanstack/react-virtual` — those\n * solve a more general problem at the cost of extra setup.\n */\nexport function VirtualList<T>({\n items,\n itemHeight,\n renderItem,\n height,\n overscan = 4,\n getKey,\n className,\n style,\n label,\n}: VirtualListProps<T>) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState<number>(0);\n const [viewport, setViewport] = useState<number>(0);\n\n useEffect(() => {\n const element = containerRef.current;\n if (!element) return;\n setViewport(element.clientHeight);\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => setViewport(element.clientHeight));\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n const totalHeight = items.length * itemHeight;\n /*\n * Rows are absolutely positioned inside the spacer, so nothing in a\n * virtual list is focusable by default: a keyboard user can see the\n * scrollbar and has no way to move it. The tab stop only appears while\n * there is something past the fold — measured from the numbers already at\n * hand rather than from the DOM.\n */\n const scrollable = viewport > 0 && totalHeight - viewport > 1;\n const start = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);\n const visibleCount = Math.ceil(viewport / itemHeight) + overscan * 2;\n const end = Math.min(items.length, start + visibleCount);\n\n return (\n <div\n ref={containerRef}\n className={cn(styles.scroll, className)}\n style={{ height, ...style }}\n onScroll={(event) => setScrollTop((event.target as HTMLDivElement).scrollTop)}\n role=\"list\"\n aria-label={label}\n tabIndex={scrollable ? 0 : undefined}\n >\n <div className={styles.spacer} style={{ height: totalHeight }}>\n {items.slice(start, end).map((item, offset) => {\n const index = start + offset;\n const key = getKey ? getKey(item, index) : index;\n return (\n <div\n key={key}\n role=\"listitem\"\n className={styles.row}\n style={{ top: index * itemHeight, height: itemHeight }}\n >\n {renderItem(item, index)}\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n"],"mappings":"kIAwCA,SAAgB,EAAe,CAC3B,QACA,aACA,aACA,SACA,WAAW,EACX,SACA,YACA,QACA,SACoB,CACpB,IAAM,GAAA,EAAA,EAAA,OAAA,CAAsC,IAAI,EAC1C,CAAC,EAAW,IAAA,EAAA,EAAA,SAAA,CAAiC,CAAC,EAC9C,CAAC,EAAU,IAAA,EAAA,EAAA,SAAA,CAAgC,CAAC,GAElD,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAM,EAAU,EAAa,QAG7B,GAFI,CAAC,IACL,EAAY,EAAQ,YAAY,EAC5B,OAAO,eAAmB,KAAa,OAC3C,IAAM,EAAW,IAAI,mBAAqB,EAAY,EAAQ,YAAY,CAAC,EAE3E,OADA,EAAS,QAAQ,CAAO,MACX,EAAS,WAAW,CACrC,EAAG,CAAC,CAAC,EAEL,IAAM,EAAc,EAAM,OAAS,EAQ7B,EAAa,EAAW,GAAK,EAAc,EAAW,EACtD,EAAQ,KAAK,IAAI,EAAG,KAAK,MAAM,EAAY,CAAU,EAAI,CAAQ,EACjE,EAAe,KAAK,KAAK,EAAW,CAAU,EAAI,EAAW,EAC7D,EAAM,KAAK,IAAI,EAAM,OAAQ,EAAQ,CAAY,EAEvD,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,IAAK,EACL,UAAW,EAAA,GAAG,EAAA,QAAO,OAAQ,CAAS,EACtC,MAAO,CAAE,SAAQ,GAAG,CAAM,EAC1B,SAAW,GAAU,EAAc,EAAM,OAA0B,SAAS,EAC5E,KAAK,OACL,aAAY,EACZ,SAAU,EAAa,EAAI,IAAA,aAE3B,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,OAAQ,MAAO,CAAE,OAAQ,CAAY,WACvD,EAAM,MAAM,EAAO,CAAG,CAAC,CAAC,KAAK,EAAM,IAAW,CAC3C,IAAM,EAAQ,EAAQ,EAChB,EAAM,EAAS,EAAO,EAAM,CAAK,EAAI,EAC3C,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAEI,KAAK,WACL,UAAW,EAAA,QAAO,IAClB,MAAO,CAAE,IAAK,EAAQ,EAAY,OAAQ,CAAW,WAEpD,EAAW,EAAM,CAAK,CACtB,EANI,CAMJ,CAEb,CAAC,CACA,CAAA,CACJ,CAAA,CAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VirtualList.js","names":[],"sources":["../../../src/components/VirtualList/VirtualList.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"VirtualList.js","names":[],"sources":["../../../src/components/VirtualList/VirtualList.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — items, itemHeight, height and overscan are the\n * window maths; renderItem and getKey the row contract; label the accessible name of\n * a scroll region. Nine props is what windowing costs — the alternative is the app\n * doing the maths.\n */\nimport { useEffect, useRef, useState } from \"react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./VirtualList.module.css\";\n\nexport interface VirtualListProps<T> {\n items: T[];\n /** Fixed pixel height for each row. */\n itemHeight: number;\n /** Render a single row. */\n renderItem: (item: T, index: number) => ReactNode;\n /** Container height (px) or any CSS length. */\n height: number | string;\n /** Number of items rendered above/below the viewport. Default: 4. */\n overscan?: number;\n /** Stable key derivation; defaults to the index. */\n getKey?: (item: T, index: number) => string | number;\n className?: string;\n style?: CSSProperties;\n /**\n * Accessible name for the list. Worth setting when the list scrolls: it\n * then carries a tab stop of its own, and a focus stop that announces\n * nothing is worse than no focus stop.\n */\n label?: string;\n}\n\n/**\n * Fixed-height virtual list. Renders only the visible window plus a small\n * overscan buffer. Suitable for lists of thousands of identical rows.\n *\n * For variable heights, use `react-window`/`@tanstack/react-virtual` — those\n * solve a more general problem at the cost of extra setup.\n */\nexport function VirtualList<T>({\n items,\n itemHeight,\n renderItem,\n height,\n overscan = 4,\n getKey,\n className,\n style,\n label,\n}: VirtualListProps<T>) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState<number>(0);\n const [viewport, setViewport] = useState<number>(0);\n\n useEffect(() => {\n const element = containerRef.current;\n if (!element) return;\n setViewport(element.clientHeight);\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => setViewport(element.clientHeight));\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n const totalHeight = items.length * itemHeight;\n /*\n * Rows are absolutely positioned inside the spacer, so nothing in a\n * virtual list is focusable by default: a keyboard user can see the\n * scrollbar and has no way to move it. The tab stop only appears while\n * there is something past the fold — measured from the numbers already at\n * hand rather than from the DOM.\n */\n const scrollable = viewport > 0 && totalHeight - viewport > 1;\n const start = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);\n const visibleCount = Math.ceil(viewport / itemHeight) + overscan * 2;\n const end = Math.min(items.length, start + visibleCount);\n\n return (\n <div\n ref={containerRef}\n className={cn(styles.scroll, className)}\n style={{ height, ...style }}\n onScroll={(event) => setScrollTop((event.target as HTMLDivElement).scrollTop)}\n role=\"list\"\n aria-label={label}\n tabIndex={scrollable ? 0 : undefined}\n >\n <div className={styles.spacer} style={{ height: totalHeight }}>\n {items.slice(start, end).map((item, offset) => {\n const index = start + offset;\n const key = getKey ? getKey(item, index) : index;\n return (\n <div\n key={key}\n role=\"listitem\"\n className={styles.row}\n style={{ top: index * itemHeight, height: itemHeight }}\n >\n {renderItem(item, index)}\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;AAwCA,SAAgB,EAAe,EAC3B,UACA,eACA,eACA,WACA,cAAW,GACX,WACA,cACA,UACA,YACoB;CACpB,IAAM,IAAe,EAAuB,IAAI,GAC1C,CAAC,GAAW,KAAgB,EAAiB,CAAC,GAC9C,CAAC,GAAU,KAAe,EAAiB,CAAC;CAElD,QAAgB;EACZ,IAAM,IAAU,EAAa;EAG7B,IAFI,CAAC,MACL,EAAY,EAAQ,YAAY,GAC5B,OAAO,iBAAmB,MAAa;EAC3C,IAAM,IAAW,IAAI,qBAAqB,EAAY,EAAQ,YAAY,CAAC;EAE3E,OADA,EAAS,QAAQ,CAAO,SACX,EAAS,WAAW;CACrC,GAAG,CAAC,CAAC;CAEL,IAAM,IAAc,EAAM,SAAS,GAQ7B,IAAa,IAAW,KAAK,IAAc,IAAW,GACtD,IAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,IAAY,CAAU,IAAI,CAAQ,GACjE,IAAe,KAAK,KAAK,IAAW,CAAU,IAAI,IAAW,GAC7D,IAAM,KAAK,IAAI,EAAM,QAAQ,IAAQ,CAAY;CAEvD,OACI,kBAAC,OAAD;EACI,KAAK;EACL,WAAW,EAAG,EAAO,QAAQ,CAAS;EACtC,OAAO;GAAE;GAAQ,GAAG;EAAM;EAC1B,WAAW,MAAU,EAAc,EAAM,OAA0B,SAAS;EAC5E,MAAK;EACL,cAAY;EACZ,UAAU,IAAa,IAAI,KAAA;YAE3B,kBAAC,OAAD;GAAK,WAAW,EAAO;GAAQ,OAAO,EAAE,QAAQ,EAAY;aACvD,EAAM,MAAM,GAAO,CAAG,CAAC,CAAC,KAAK,GAAM,MAAW;IAC3C,IAAM,IAAQ,IAAQ,GAChB,IAAM,IAAS,EAAO,GAAM,CAAK,IAAI;IAC3C,OACI,kBAAC,OAAD;KAEI,MAAK;KACL,WAAW,EAAO;KAClB,OAAO;MAAE,KAAK,IAAQ;MAAY,QAAQ;KAAW;eAEpD,EAAW,GAAM,CAAK;IACtB,GANI,CAMJ;GAEb,CAAC;EACA,CAAA;CACJ,CAAA;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VirtualTable.cjs","names":[],"sources":["../../../src/components/VirtualTable/VirtualTable.tsx"],"sourcesContent":["import {\n type HTMLAttributes,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\n\nimport type { TableAlign } from \"../Table\";\nimport styles from \"./VirtualTable.module.css\";\n\nexport type VirtualTableSortDirection = \"asc\" | \"desc\";\n\nexport interface VirtualTableSort<T> {\n key: keyof T;\n direction: VirtualTableSortDirection;\n}\n\nexport interface VirtualTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `row[key]`. */\n render?: (row: T, index: number) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment. */\n align?: TableAlign;\n /**\n * Column width. Recommended for every column: virtualized rows enter and\n * leave the DOM as you scroll, so letting the browser auto-size columns from\n * whatever is currently rendered makes them jump mid-scroll.\n */\n width?: string | number;\n}\n\nexport interface VirtualTableProps<T> extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Full dataset. Every row is accounted for; only the visible ones render. */\n data: T[];\n /** Column definitions. */\n columns: VirtualTableColumn<T>[];\n /**\n * Row height in px. Must be uniform and must match what the CSS produces,\n * because it is what maps scroll offset to row index.\n */\n rowHeight: number;\n /** Height of the scroll viewport — a number of px or any CSS length. */\n height: number | string;\n /** Rows rendered above and below the viewport. Default `4`. */\n overscan?: number;\n /** Stable key extractor. Defaults to the row's index in `data`. */\n rowKey?: (row: T, index: number) => string | number;\n /** Sort applied before any header interaction. */\n initialSort?: VirtualTableSort<T>;\n /** Called when a row is activated by click, `Enter` or `Space`. */\n onRowClick?: (row: T, index: number) => void;\n /** Content shown when `data` is empty. */\n emptyMessage?: ReactNode;\n /** Accessible name for the table. Rendered as a visually hidden `<caption>`. */\n caption?: ReactNode;\n /** Scroll this row into view whenever it changes. */\n scrollToIndex?: number;\n}\n\n/** Resolve a column's alignment to its CSS module class. */\nfunction alignClass(align: TableAlign | undefined): string | undefined {\n if (align === \"right\") return styles.alignRight;\n if (align === \"center\") return styles.alignCenter;\n return undefined;\n}\n\n/**\n * A table that stays responsive at 10k+ rows by rendering only the visible window.\n *\n * `Table` renders every row it is given and `DataTable` paginates to keep that\n * count small; neither has an answer for \"show me all 40 000 rows in one\n * scrollable grid\". This does, at the cost of one constraint: **`rowHeight` must\n * be uniform**, since mapping a scroll offset to a row index is what makes the\n * window computable without measuring anything.\n *\n * It stays a real `<table>`. The window is produced by two spacer rows — one\n * above the visible slice, one below — instead of absolutely positioning rows.\n * That is deliberate: `position: absolute` on `<tr>` collapses table layout, so\n * every column width would have to be computed by hand, and the element would\n * stop being a table for assistive technology. With spacer rows the browser keeps\n * doing column layout and screen readers keep announcing a grid.\n *\n * Because only a slice is in the DOM, `aria-rowcount` on the table and\n * `aria-rowindex` on each row carry the real numbers — without them a screen\n * reader announces \"row 3 of 20\" while the user is on row 5003 of 40 000.\n *\n * @example\n * <VirtualTable\n * data={rows}\n * columns={[\n * { key: \"id\", header: \"#\", width: 80, sortable: true },\n * { key: \"name\", header: \"Nome\", width: 240, sortable: true },\n * { key: \"total\", header: \"Total\", align: \"right\", width: 120, sortable: true },\n * ]}\n * rowHeight={40}\n * height={480}\n * rowKey={(row) => row.id}\n * />\n */\nexport function VirtualTable<T>({\n data,\n columns,\n rowHeight,\n height,\n overscan = 4,\n rowKey,\n initialSort,\n onRowClick,\n emptyMessage = \"Nenhum registro encontrado.\",\n caption,\n scrollToIndex,\n className,\n ...rest\n}: VirtualTableProps<T>) {\n const scrollRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState<number>(0);\n const [viewport, setViewport] = useState<number>(0);\n const [sort, setSort] = useState<VirtualTableSort<T> | null>(initialSort ?? null);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element) return;\n setViewport(element.clientHeight);\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => setViewport(element.clientHeight));\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element || scrollToIndex == null) return;\n element.scrollTop = Math.max(0, Math.min(scrollToIndex, data.length - 1)) * rowHeight;\n }, [scrollToIndex, rowHeight, data.length]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort) return data;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...data].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [data, sort]);\n\n const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);\n const windowSize = Math.ceil(viewport / rowHeight) + overscan * 2;\n const end = Math.min(sorted.length, start + windowSize);\n const slice = sorted.slice(start, end);\n\n /**\n * Advance a column through asc → desc → unsorted, and jump back to the top.\n *\n * The scroll reset happens here rather than in an effect keyed on the sort\n * state for two reasons: this is the actual event that invalidates the offset\n * (keeping it would leave the user at the same pixel of a different dataset,\n * which reads as the table having jumped somewhere random), and an effect would\n * also fire on mount — clobbering an initial `scrollToIndex`.\n */\n const toggleSort = useCallback((key: keyof T): void => {\n setSort((current) => {\n if (!current || current.key !== key) return { key, direction: \"asc\" };\n if (current.direction === \"asc\") return { key, direction: \"desc\" };\n return null;\n });\n const element = scrollRef.current;\n if (element) element.scrollTop = 0;\n setScrollTop(0);\n }, []);\n\n return (\n <div\n ref={scrollRef}\n className={cn(styles.scroll, className)}\n style={{ height }}\n onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}\n {...rest}\n >\n <table className={styles.table} aria-rowcount={sorted.length}>\n {caption ? <caption className={styles.caption}>{caption}</caption> : null}\n <thead className={styles.head}>\n <tr>\n {columns.map((column) => {\n const isSorted = sort?.key === column.key;\n return (\n <th\n key={String(column.key)}\n scope=\"col\"\n className={cn(styles.th, alignClass(column.align))}\n style={{ width: column.width }}\n aria-sort={\n isSorted\n ? sort?.direction === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : column.sortable\n ? \"none\"\n : undefined\n }\n >\n {column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {isSorted\n ? sort?.direction === \"asc\"\n ? \"▲\"\n : \"▼\"\n : \"↕\"}\n </span>\n </button>\n ) : (\n column.header\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sorted.length === 0 ? (\n <tr>\n <td className={styles.emptyRow} colSpan={columns.length}>\n {emptyMessage}\n </td>\n </tr>\n ) : (\n <>\n {start > 0 && <tr aria-hidden style={{ height: start * rowHeight }} />}\n {slice.map((row, offset) => {\n const index = start + offset;\n return (\n <tr\n key={rowKey ? rowKey(row, index) : index}\n aria-rowindex={index + 1}\n className={cn(styles.tr, onRowClick && styles.clickable)}\n style={{ height: rowHeight }}\n tabIndex={onRowClick ? 0 : undefined}\n onClick={\n onRowClick ? () => onRowClick(row, index) : undefined\n }\n onKeyDown={\n onRowClick\n ? (event) => {\n if (\n event.key === \"Enter\" ||\n event.key === \" \"\n ) {\n event.preventDefault();\n onRowClick(row, index);\n }\n }\n : undefined\n }\n >\n {columns.map((column) => (\n <td\n key={String(column.key)}\n className={cn(styles.td, alignClass(column.align))}\n style={{ width: column.width }}\n >\n {column.render\n ? column.render(row, index)\n : ((row[column.key] as ReactNode) ?? null)}\n </td>\n ))}\n </tr>\n );\n })}\n {end < sorted.length && (\n <tr\n aria-hidden\n style={{ height: (sorted.length - end) * rowHeight }}\n />\n )}\n </>\n )}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VirtualTable.cjs","names":[],"sources":["../../../src/components/VirtualTable/VirtualTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count — windowing needs its own measurements\n * (rowHeight, height, overscan, scrollToIndex) on top of everything a table already\n * takes (data, columns, rowKey, initialSort, onRowClick, emptyMessage, caption). The\n * caption is not optional chrome — a virtualised grid without one is unreadable to a\n * screen reader.\n */\nimport {\n type HTMLAttributes,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\n\nimport type { TableAlign } from \"../Table\";\nimport styles from \"./VirtualTable.module.css\";\n\nexport type VirtualTableSortDirection = \"asc\" | \"desc\";\n\nexport interface VirtualTableSort<T> {\n key: keyof T;\n direction: VirtualTableSortDirection;\n}\n\nexport interface VirtualTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `row[key]`. */\n render?: (row: T, index: number) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment. */\n align?: TableAlign;\n /**\n * Column width. Recommended for every column: virtualized rows enter and\n * leave the DOM as you scroll, so letting the browser auto-size columns from\n * whatever is currently rendered makes them jump mid-scroll.\n */\n width?: string | number;\n}\n\nexport interface VirtualTableProps<T> extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Full dataset. Every row is accounted for; only the visible ones render. */\n data: T[];\n /** Column definitions. */\n columns: VirtualTableColumn<T>[];\n /**\n * Row height in px. Must be uniform and must match what the CSS produces,\n * because it is what maps scroll offset to row index.\n */\n rowHeight: number;\n /** Height of the scroll viewport — a number of px or any CSS length. */\n height: number | string;\n /** Rows rendered above and below the viewport. Default `4`. */\n overscan?: number;\n /** Stable key extractor. Defaults to the row's index in `data`. */\n rowKey?: (row: T, index: number) => string | number;\n /** Sort applied before any header interaction. */\n initialSort?: VirtualTableSort<T>;\n /** Called when a row is activated by click, `Enter` or `Space`. */\n onRowClick?: (row: T, index: number) => void;\n /** Content shown when `data` is empty. */\n emptyMessage?: ReactNode;\n /** Accessible name for the table. Rendered as a visually hidden `<caption>`. */\n caption?: ReactNode;\n /** Scroll this row into view whenever it changes. */\n scrollToIndex?: number;\n}\n\n/** Resolve a column's alignment to its CSS module class. */\nfunction alignClass(align: TableAlign | undefined): string | undefined {\n if (align === \"right\") return styles.alignRight;\n if (align === \"center\") return styles.alignCenter;\n return undefined;\n}\n\n/**\n * A table that stays responsive at 10k+ rows by rendering only the visible window.\n *\n * `Table` renders every row it is given and `DataTable` paginates to keep that\n * count small; neither has an answer for \"show me all 40 000 rows in one\n * scrollable grid\". This does, at the cost of one constraint: **`rowHeight` must\n * be uniform**, since mapping a scroll offset to a row index is what makes the\n * window computable without measuring anything.\n *\n * It stays a real `<table>`. The window is produced by two spacer rows — one\n * above the visible slice, one below — instead of absolutely positioning rows.\n * That is deliberate: `position: absolute` on `<tr>` collapses table layout, so\n * every column width would have to be computed by hand, and the element would\n * stop being a table for assistive technology. With spacer rows the browser keeps\n * doing column layout and screen readers keep announcing a grid.\n *\n * Because only a slice is in the DOM, `aria-rowcount` on the table and\n * `aria-rowindex` on each row carry the real numbers — without them a screen\n * reader announces \"row 3 of 20\" while the user is on row 5003 of 40 000.\n *\n * @example\n * <VirtualTable\n * data={rows}\n * columns={[\n * { key: \"id\", header: \"#\", width: 80, sortable: true },\n * { key: \"name\", header: \"Nome\", width: 240, sortable: true },\n * { key: \"total\", header: \"Total\", align: \"right\", width: 120, sortable: true },\n * ]}\n * rowHeight={40}\n * height={480}\n * rowKey={(row) => row.id}\n * />\n */\nexport function VirtualTable<T>({\n data,\n columns,\n rowHeight,\n height,\n overscan = 4,\n rowKey,\n initialSort,\n onRowClick,\n emptyMessage = \"Nenhum registro encontrado.\",\n caption,\n scrollToIndex,\n className,\n ...rest\n}: VirtualTableProps<T>) {\n const scrollRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState<number>(0);\n const [viewport, setViewport] = useState<number>(0);\n const [sort, setSort] = useState<VirtualTableSort<T> | null>(initialSort ?? null);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element) return;\n setViewport(element.clientHeight);\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => setViewport(element.clientHeight));\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element || scrollToIndex == null) return;\n element.scrollTop = Math.max(0, Math.min(scrollToIndex, data.length - 1)) * rowHeight;\n }, [scrollToIndex, rowHeight, data.length]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort) return data;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...data].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [data, sort]);\n\n const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);\n const windowSize = Math.ceil(viewport / rowHeight) + overscan * 2;\n const end = Math.min(sorted.length, start + windowSize);\n const slice = sorted.slice(start, end);\n\n /**\n * Advance a column through asc → desc → unsorted, and jump back to the top.\n *\n * The scroll reset happens here rather than in an effect keyed on the sort\n * state for two reasons: this is the actual event that invalidates the offset\n * (keeping it would leave the user at the same pixel of a different dataset,\n * which reads as the table having jumped somewhere random), and an effect would\n * also fire on mount — clobbering an initial `scrollToIndex`.\n */\n const toggleSort = useCallback((key: keyof T): void => {\n setSort((current) => {\n if (!current || current.key !== key) return { key, direction: \"asc\" };\n if (current.direction === \"asc\") return { key, direction: \"desc\" };\n return null;\n });\n const element = scrollRef.current;\n if (element) element.scrollTop = 0;\n setScrollTop(0);\n }, []);\n\n return (\n <div\n ref={scrollRef}\n className={cn(styles.scroll, className)}\n style={{ height }}\n onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}\n {...rest}\n >\n <table className={styles.table} aria-rowcount={sorted.length}>\n {caption ? <caption className={styles.caption}>{caption}</caption> : null}\n <thead className={styles.head}>\n <tr>\n {columns.map((column) => {\n const isSorted = sort?.key === column.key;\n return (\n <th\n key={String(column.key)}\n scope=\"col\"\n className={cn(styles.th, alignClass(column.align))}\n style={{ width: column.width }}\n aria-sort={\n isSorted\n ? sort?.direction === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : column.sortable\n ? \"none\"\n : undefined\n }\n >\n {column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {isSorted\n ? sort?.direction === \"asc\"\n ? \"▲\"\n : \"▼\"\n : \"↕\"}\n </span>\n </button>\n ) : (\n column.header\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sorted.length === 0 ? (\n <tr>\n <td className={styles.emptyRow} colSpan={columns.length}>\n {emptyMessage}\n </td>\n </tr>\n ) : (\n <>\n {start > 0 && <tr aria-hidden style={{ height: start * rowHeight }} />}\n {slice.map((row, offset) => {\n const index = start + offset;\n return (\n <tr\n key={rowKey ? rowKey(row, index) : index}\n aria-rowindex={index + 1}\n className={cn(styles.tr, onRowClick && styles.clickable)}\n style={{ height: rowHeight }}\n tabIndex={onRowClick ? 0 : undefined}\n onClick={\n onRowClick ? () => onRowClick(row, index) : undefined\n }\n onKeyDown={\n onRowClick\n ? (event) => {\n if (\n event.key === \"Enter\" ||\n event.key === \" \"\n ) {\n event.preventDefault();\n onRowClick(row, index);\n }\n }\n : undefined\n }\n >\n {columns.map((column) => (\n <td\n key={String(column.key)}\n className={cn(styles.td, alignClass(column.align))}\n style={{ width: column.width }}\n >\n {column.render\n ? column.render(row, index)\n : ((row[column.key] as ReactNode) ?? null)}\n </td>\n ))}\n </tr>\n );\n })}\n {end < sorted.length && (\n <tr\n aria-hidden\n style={{ height: (sorted.length - end) * rowHeight }}\n />\n )}\n </>\n )}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VirtualTable.js","names":[],"sources":["../../../src/components/VirtualTable/VirtualTable.tsx"],"sourcesContent":["import {\n type HTMLAttributes,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\n\nimport type { TableAlign } from \"../Table\";\nimport styles from \"./VirtualTable.module.css\";\n\nexport type VirtualTableSortDirection = \"asc\" | \"desc\";\n\nexport interface VirtualTableSort<T> {\n key: keyof T;\n direction: VirtualTableSortDirection;\n}\n\nexport interface VirtualTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `row[key]`. */\n render?: (row: T, index: number) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment. */\n align?: TableAlign;\n /**\n * Column width. Recommended for every column: virtualized rows enter and\n * leave the DOM as you scroll, so letting the browser auto-size columns from\n * whatever is currently rendered makes them jump mid-scroll.\n */\n width?: string | number;\n}\n\nexport interface VirtualTableProps<T> extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Full dataset. Every row is accounted for; only the visible ones render. */\n data: T[];\n /** Column definitions. */\n columns: VirtualTableColumn<T>[];\n /**\n * Row height in px. Must be uniform and must match what the CSS produces,\n * because it is what maps scroll offset to row index.\n */\n rowHeight: number;\n /** Height of the scroll viewport — a number of px or any CSS length. */\n height: number | string;\n /** Rows rendered above and below the viewport. Default `4`. */\n overscan?: number;\n /** Stable key extractor. Defaults to the row's index in `data`. */\n rowKey?: (row: T, index: number) => string | number;\n /** Sort applied before any header interaction. */\n initialSort?: VirtualTableSort<T>;\n /** Called when a row is activated by click, `Enter` or `Space`. */\n onRowClick?: (row: T, index: number) => void;\n /** Content shown when `data` is empty. */\n emptyMessage?: ReactNode;\n /** Accessible name for the table. Rendered as a visually hidden `<caption>`. */\n caption?: ReactNode;\n /** Scroll this row into view whenever it changes. */\n scrollToIndex?: number;\n}\n\n/** Resolve a column's alignment to its CSS module class. */\nfunction alignClass(align: TableAlign | undefined): string | undefined {\n if (align === \"right\") return styles.alignRight;\n if (align === \"center\") return styles.alignCenter;\n return undefined;\n}\n\n/**\n * A table that stays responsive at 10k+ rows by rendering only the visible window.\n *\n * `Table` renders every row it is given and `DataTable` paginates to keep that\n * count small; neither has an answer for \"show me all 40 000 rows in one\n * scrollable grid\". This does, at the cost of one constraint: **`rowHeight` must\n * be uniform**, since mapping a scroll offset to a row index is what makes the\n * window computable without measuring anything.\n *\n * It stays a real `<table>`. The window is produced by two spacer rows — one\n * above the visible slice, one below — instead of absolutely positioning rows.\n * That is deliberate: `position: absolute` on `<tr>` collapses table layout, so\n * every column width would have to be computed by hand, and the element would\n * stop being a table for assistive technology. With spacer rows the browser keeps\n * doing column layout and screen readers keep announcing a grid.\n *\n * Because only a slice is in the DOM, `aria-rowcount` on the table and\n * `aria-rowindex` on each row carry the real numbers — without them a screen\n * reader announces \"row 3 of 20\" while the user is on row 5003 of 40 000.\n *\n * @example\n * <VirtualTable\n * data={rows}\n * columns={[\n * { key: \"id\", header: \"#\", width: 80, sortable: true },\n * { key: \"name\", header: \"Nome\", width: 240, sortable: true },\n * { key: \"total\", header: \"Total\", align: \"right\", width: 120, sortable: true },\n * ]}\n * rowHeight={40}\n * height={480}\n * rowKey={(row) => row.id}\n * />\n */\nexport function VirtualTable<T>({\n data,\n columns,\n rowHeight,\n height,\n overscan = 4,\n rowKey,\n initialSort,\n onRowClick,\n emptyMessage = \"Nenhum registro encontrado.\",\n caption,\n scrollToIndex,\n className,\n ...rest\n}: VirtualTableProps<T>) {\n const scrollRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState<number>(0);\n const [viewport, setViewport] = useState<number>(0);\n const [sort, setSort] = useState<VirtualTableSort<T> | null>(initialSort ?? null);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element) return;\n setViewport(element.clientHeight);\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => setViewport(element.clientHeight));\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element || scrollToIndex == null) return;\n element.scrollTop = Math.max(0, Math.min(scrollToIndex, data.length - 1)) * rowHeight;\n }, [scrollToIndex, rowHeight, data.length]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort) return data;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...data].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [data, sort]);\n\n const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);\n const windowSize = Math.ceil(viewport / rowHeight) + overscan * 2;\n const end = Math.min(sorted.length, start + windowSize);\n const slice = sorted.slice(start, end);\n\n /**\n * Advance a column through asc → desc → unsorted, and jump back to the top.\n *\n * The scroll reset happens here rather than in an effect keyed on the sort\n * state for two reasons: this is the actual event that invalidates the offset\n * (keeping it would leave the user at the same pixel of a different dataset,\n * which reads as the table having jumped somewhere random), and an effect would\n * also fire on mount — clobbering an initial `scrollToIndex`.\n */\n const toggleSort = useCallback((key: keyof T): void => {\n setSort((current) => {\n if (!current || current.key !== key) return { key, direction: \"asc\" };\n if (current.direction === \"asc\") return { key, direction: \"desc\" };\n return null;\n });\n const element = scrollRef.current;\n if (element) element.scrollTop = 0;\n setScrollTop(0);\n }, []);\n\n return (\n <div\n ref={scrollRef}\n className={cn(styles.scroll, className)}\n style={{ height }}\n onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}\n {...rest}\n >\n <table className={styles.table} aria-rowcount={sorted.length}>\n {caption ? <caption className={styles.caption}>{caption}</caption> : null}\n <thead className={styles.head}>\n <tr>\n {columns.map((column) => {\n const isSorted = sort?.key === column.key;\n return (\n <th\n key={String(column.key)}\n scope=\"col\"\n className={cn(styles.th, alignClass(column.align))}\n style={{ width: column.width }}\n aria-sort={\n isSorted\n ? sort?.direction === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : column.sortable\n ? \"none\"\n : undefined\n }\n >\n {column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {isSorted\n ? sort?.direction === \"asc\"\n ? \"▲\"\n : \"▼\"\n : \"↕\"}\n </span>\n </button>\n ) : (\n column.header\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sorted.length === 0 ? (\n <tr>\n <td className={styles.emptyRow} colSpan={columns.length}>\n {emptyMessage}\n </td>\n </tr>\n ) : (\n <>\n {start > 0 && <tr aria-hidden style={{ height: start * rowHeight }} />}\n {slice.map((row, offset) => {\n const index = start + offset;\n return (\n <tr\n key={rowKey ? rowKey(row, index) : index}\n aria-rowindex={index + 1}\n className={cn(styles.tr, onRowClick && styles.clickable)}\n style={{ height: rowHeight }}\n tabIndex={onRowClick ? 0 : undefined}\n onClick={\n onRowClick ? () => onRowClick(row, index) : undefined\n }\n onKeyDown={\n onRowClick\n ? (event) => {\n if (\n event.key === \"Enter\" ||\n event.key === \" \"\n ) {\n event.preventDefault();\n onRowClick(row, index);\n }\n }\n : undefined\n }\n >\n {columns.map((column) => (\n <td\n key={String(column.key)}\n className={cn(styles.td, alignClass(column.align))}\n style={{ width: column.width }}\n >\n {column.render\n ? column.render(row, index)\n : ((row[column.key] as ReactNode) ?? null)}\n </td>\n ))}\n </tr>\n );\n })}\n {end < sorted.length && (\n <tr\n aria-hidden\n style={{ height: (sorted.length - end) * rowHeight }}\n />\n )}\n </>\n )}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VirtualTable.js","names":[],"sources":["../../../src/components/VirtualTable/VirtualTable.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count — windowing needs its own measurements\n * (rowHeight, height, overscan, scrollToIndex) on top of everything a table already\n * takes (data, columns, rowKey, initialSort, onRowClick, emptyMessage, caption). The\n * caption is not optional chrome — a virtualised grid without one is unreadable to a\n * screen reader.\n */\nimport {\n type HTMLAttributes,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { compareValues } from \"@/utils/compare-values\";\n\nimport type { TableAlign } from \"../Table\";\nimport styles from \"./VirtualTable.module.css\";\n\nexport type VirtualTableSortDirection = \"asc\" | \"desc\";\n\nexport interface VirtualTableSort<T> {\n key: keyof T;\n direction: VirtualTableSortDirection;\n}\n\nexport interface VirtualTableColumn<T> {\n /** Property of the row this column reads from. Doubles as the cell key. */\n key: keyof T;\n /** Column heading. */\n header: ReactNode;\n /** Custom cell renderer. Defaults to `row[key]`. */\n render?: (row: T, index: number) => ReactNode;\n /** Enable click-to-sort on this column's header. */\n sortable?: boolean;\n /** Text alignment. */\n align?: TableAlign;\n /**\n * Column width. Recommended for every column: virtualized rows enter and\n * leave the DOM as you scroll, so letting the browser auto-size columns from\n * whatever is currently rendered makes them jump mid-scroll.\n */\n width?: string | number;\n}\n\nexport interface VirtualTableProps<T> extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Full dataset. Every row is accounted for; only the visible ones render. */\n data: T[];\n /** Column definitions. */\n columns: VirtualTableColumn<T>[];\n /**\n * Row height in px. Must be uniform and must match what the CSS produces,\n * because it is what maps scroll offset to row index.\n */\n rowHeight: number;\n /** Height of the scroll viewport — a number of px or any CSS length. */\n height: number | string;\n /** Rows rendered above and below the viewport. Default `4`. */\n overscan?: number;\n /** Stable key extractor. Defaults to the row's index in `data`. */\n rowKey?: (row: T, index: number) => string | number;\n /** Sort applied before any header interaction. */\n initialSort?: VirtualTableSort<T>;\n /** Called when a row is activated by click, `Enter` or `Space`. */\n onRowClick?: (row: T, index: number) => void;\n /** Content shown when `data` is empty. */\n emptyMessage?: ReactNode;\n /** Accessible name for the table. Rendered as a visually hidden `<caption>`. */\n caption?: ReactNode;\n /** Scroll this row into view whenever it changes. */\n scrollToIndex?: number;\n}\n\n/** Resolve a column's alignment to its CSS module class. */\nfunction alignClass(align: TableAlign | undefined): string | undefined {\n if (align === \"right\") return styles.alignRight;\n if (align === \"center\") return styles.alignCenter;\n return undefined;\n}\n\n/**\n * A table that stays responsive at 10k+ rows by rendering only the visible window.\n *\n * `Table` renders every row it is given and `DataTable` paginates to keep that\n * count small; neither has an answer for \"show me all 40 000 rows in one\n * scrollable grid\". This does, at the cost of one constraint: **`rowHeight` must\n * be uniform**, since mapping a scroll offset to a row index is what makes the\n * window computable without measuring anything.\n *\n * It stays a real `<table>`. The window is produced by two spacer rows — one\n * above the visible slice, one below — instead of absolutely positioning rows.\n * That is deliberate: `position: absolute` on `<tr>` collapses table layout, so\n * every column width would have to be computed by hand, and the element would\n * stop being a table for assistive technology. With spacer rows the browser keeps\n * doing column layout and screen readers keep announcing a grid.\n *\n * Because only a slice is in the DOM, `aria-rowcount` on the table and\n * `aria-rowindex` on each row carry the real numbers — without them a screen\n * reader announces \"row 3 of 20\" while the user is on row 5003 of 40 000.\n *\n * @example\n * <VirtualTable\n * data={rows}\n * columns={[\n * { key: \"id\", header: \"#\", width: 80, sortable: true },\n * { key: \"name\", header: \"Nome\", width: 240, sortable: true },\n * { key: \"total\", header: \"Total\", align: \"right\", width: 120, sortable: true },\n * ]}\n * rowHeight={40}\n * height={480}\n * rowKey={(row) => row.id}\n * />\n */\nexport function VirtualTable<T>({\n data,\n columns,\n rowHeight,\n height,\n overscan = 4,\n rowKey,\n initialSort,\n onRowClick,\n emptyMessage = \"Nenhum registro encontrado.\",\n caption,\n scrollToIndex,\n className,\n ...rest\n}: VirtualTableProps<T>) {\n const scrollRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState<number>(0);\n const [viewport, setViewport] = useState<number>(0);\n const [sort, setSort] = useState<VirtualTableSort<T> | null>(initialSort ?? null);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element) return;\n setViewport(element.clientHeight);\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => setViewport(element.clientHeight));\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element || scrollToIndex == null) return;\n element.scrollTop = Math.max(0, Math.min(scrollToIndex, data.length - 1)) * rowHeight;\n }, [scrollToIndex, rowHeight, data.length]);\n\n const sorted = useMemo<T[]>(() => {\n if (!sort) return data;\n const factor = sort.direction === \"asc\" ? 1 : -1;\n return [...data].sort((a, b) => compareValues(a[sort.key], b[sort.key]) * factor);\n }, [data, sort]);\n\n const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);\n const windowSize = Math.ceil(viewport / rowHeight) + overscan * 2;\n const end = Math.min(sorted.length, start + windowSize);\n const slice = sorted.slice(start, end);\n\n /**\n * Advance a column through asc → desc → unsorted, and jump back to the top.\n *\n * The scroll reset happens here rather than in an effect keyed on the sort\n * state for two reasons: this is the actual event that invalidates the offset\n * (keeping it would leave the user at the same pixel of a different dataset,\n * which reads as the table having jumped somewhere random), and an effect would\n * also fire on mount — clobbering an initial `scrollToIndex`.\n */\n const toggleSort = useCallback((key: keyof T): void => {\n setSort((current) => {\n if (!current || current.key !== key) return { key, direction: \"asc\" };\n if (current.direction === \"asc\") return { key, direction: \"desc\" };\n return null;\n });\n const element = scrollRef.current;\n if (element) element.scrollTop = 0;\n setScrollTop(0);\n }, []);\n\n return (\n <div\n ref={scrollRef}\n className={cn(styles.scroll, className)}\n style={{ height }}\n onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}\n {...rest}\n >\n <table className={styles.table} aria-rowcount={sorted.length}>\n {caption ? <caption className={styles.caption}>{caption}</caption> : null}\n <thead className={styles.head}>\n <tr>\n {columns.map((column) => {\n const isSorted = sort?.key === column.key;\n return (\n <th\n key={String(column.key)}\n scope=\"col\"\n className={cn(styles.th, alignClass(column.align))}\n style={{ width: column.width }}\n aria-sort={\n isSorted\n ? sort?.direction === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : column.sortable\n ? \"none\"\n : undefined\n }\n >\n {column.sortable ? (\n <button\n type=\"button\"\n className={styles.sortButton}\n onClick={() => toggleSort(column.key)}\n >\n {column.header}\n <span className={styles.sortIndicator} aria-hidden>\n {isSorted\n ? sort?.direction === \"asc\"\n ? \"▲\"\n : \"▼\"\n : \"↕\"}\n </span>\n </button>\n ) : (\n column.header\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sorted.length === 0 ? (\n <tr>\n <td className={styles.emptyRow} colSpan={columns.length}>\n {emptyMessage}\n </td>\n </tr>\n ) : (\n <>\n {start > 0 && <tr aria-hidden style={{ height: start * rowHeight }} />}\n {slice.map((row, offset) => {\n const index = start + offset;\n return (\n <tr\n key={rowKey ? rowKey(row, index) : index}\n aria-rowindex={index + 1}\n className={cn(styles.tr, onRowClick && styles.clickable)}\n style={{ height: rowHeight }}\n tabIndex={onRowClick ? 0 : undefined}\n onClick={\n onRowClick ? () => onRowClick(row, index) : undefined\n }\n onKeyDown={\n onRowClick\n ? (event) => {\n if (\n event.key === \"Enter\" ||\n event.key === \" \"\n ) {\n event.preventDefault();\n onRowClick(row, index);\n }\n }\n : undefined\n }\n >\n {columns.map((column) => (\n <td\n key={String(column.key)}\n className={cn(styles.td, alignClass(column.align))}\n style={{ width: column.width }}\n >\n {column.render\n ? column.render(row, index)\n : ((row[column.key] as ReactNode) ?? null)}\n </td>\n ))}\n </tr>\n );\n })}\n {end < sorted.length && (\n <tr\n aria-hidden\n style={{ height: (sorted.length - end) * rowHeight }}\n />\n )}\n </>\n )}\n </tbody>\n </table>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Wizard.cjs","names":[],"sources":["../../../src/components/Wizard/Wizard.tsx"],"sourcesContent":["import { useCallback, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Button } from \"../Button\";\nimport { Stepper } from \"../Stepper\";\nimport styles from \"./Wizard.module.css\";\n\n/** One step of the flow. */\nexport interface WizardStep {\n /** Stable identifier. */\n id: string;\n /** Step label shown in the indicator. */\n label: string;\n /** Optional description under the label. */\n description?: string;\n /** Step body. A function receives the flow controls, for a \"skip\" link inside the form. */\n content: ReactNode | ((controls: WizardControls) => ReactNode);\n /**\n * Gate for leaving this step forward. Return `false` (or a rejected/`false`\n * promise) to keep the user here — typically `() => form.trigger()`.\n * Async is supported: the Next button shows a pending state while it runs.\n */\n validate?: () => boolean | Promise<boolean>;\n /** Marks the step as optional, so `onComplete` can ignore it. */\n optional?: boolean;\n}\n\n/** Flow controls handed to a step body and to `renderActions`. */\nexport interface WizardControls {\n /** Zero-based index of the current step. */\n activeIndex: number;\n /** The current step. */\n step: WizardStep;\n /** `true` while a `validate` promise is pending. */\n validating: boolean;\n isFirst: boolean;\n isLast: boolean;\n /** Run the current step's `validate` and advance when it passes. */\n next: () => Promise<void>;\n /** Go back one step. No validation — going back never blocks. */\n back: () => void;\n /** Jump to an index. Forward jumps validate every step in between. */\n goTo: (index: number) => Promise<void>;\n}\n\nexport interface WizardProps {\n steps: WizardStep[];\n /** Controlled active index. */\n activeIndex?: number;\n /** Uncontrolled initial index. Default `0`. */\n defaultActiveIndex?: number;\n onStepChange?: (index: number, step: WizardStep) => void;\n /** Called when the last step passes validation. */\n onComplete?: () => void | Promise<void>;\n /** Label of the advance button. Default `\"Next\"`. */\n nextLabel?: string;\n /** Label of the back button. Default `\"Back\"`. */\n backLabel?: string;\n /** Label of the button on the last step. Default `\"Finish\"`. */\n finishLabel?: string;\n /**\n * Suffix appended to an optional step's description in the indicator.\n * Default `\"(optional)\"` — override it to localize, since the SDK ships no\n * translation for component-internal copy.\n */\n optionalLabel?: string;\n /**\n * Allow clicking the indicator to jump. Default `false` — a wizard exists\n * because order matters, and a free jump skips the gates.\n */\n clickableSteps?: boolean;\n /** Replace the default button row. */\n renderActions?: (controls: WizardControls) => ReactNode;\n className?: string;\n}\n\n/**\n * Multi-step flow: step indicator, one body at a time, and navigation that\n * respects per-step validation.\n *\n * `Stepper` draws the indicator; this owns the part every app was rewriting — the\n * active index, the async gate before advancing, the disabled/pending buttons and\n * the completion call.\n *\n * Only the active step's body is mounted. Uncommitted input in a step you leave is\n * therefore lost unless the state lives outside (react-hook-form's `FormProvider`,\n * a store, a parent `useState`) — which is the right place for it anyway, since\n * the last step usually needs to submit everything at once.\n *\n * @example\n * ```tsx\n * const form = useZodForm(schema);\n *\n * <FormProvider {...form}>\n * <Wizard\n * steps={[\n * {\n * id: \"dados\",\n * label: \"Dados\",\n * validate: () => form.trigger([\"nome\", \"email\"]),\n * content: (\n * <>\n * <FormField name=\"nome\" label=\"Nome\"><Input /></FormField>\n * <FormField name=\"email\" label=\"E-mail\"><Input type=\"email\" /></FormField>\n * </>\n * ),\n * },\n * { id: \"revisao\", label: \"Revisão\", content: <Review /> },\n * ]}\n * onComplete={form.handleSubmit(onSubmit)}\n * />\n * </FormProvider>\n * ```\n */\nexport function Wizard({\n steps,\n activeIndex,\n defaultActiveIndex = 0,\n onStepChange,\n onComplete,\n nextLabel = \"Next\",\n backLabel = \"Back\",\n finishLabel = \"Finish\",\n optionalLabel = \"(optional)\",\n clickableSteps = false,\n renderActions,\n className,\n}: WizardProps) {\n const isControlled = activeIndex !== undefined;\n const [internalIndex, setInternalIndex] = useState(defaultActiveIndex);\n const [validating, setValidating] = useState(false);\n\n const current = Math.min(isControlled ? activeIndex : internalIndex, steps.length - 1);\n const step = steps[current];\n\n const moveTo = useCallback(\n (index: number): void => {\n if (!isControlled) setInternalIndex(index);\n onStepChange?.(index, steps[index]);\n },\n [isControlled, onStepChange, steps],\n );\n\n /**\n * Run a step's gate. A gate that throws counts as \"not allowed\": a `validate`\n * wired to a network check should not strand the user on a half-advanced flow\n * when the request fails.\n */\n const runValidate = useCallback(async (candidate: WizardStep): Promise<boolean> => {\n if (!candidate.validate) return true;\n setValidating(true);\n try {\n return await candidate.validate();\n } catch {\n return false;\n } finally {\n setValidating(false);\n }\n }, []);\n\n const next = useCallback(async (): Promise<void> => {\n if (!(await runValidate(step))) return;\n if (current === steps.length - 1) {\n await onComplete?.();\n return;\n }\n moveTo(current + 1);\n }, [current, moveTo, onComplete, runValidate, step, steps.length]);\n\n const back = useCallback((): void => {\n if (current > 0) moveTo(current - 1);\n }, [current, moveTo]);\n\n const goTo = useCallback(\n async (index: number): Promise<void> => {\n const target = Math.max(0, Math.min(index, steps.length - 1));\n if (target === current) return;\n if (target < current) {\n moveTo(target);\n return;\n }\n for (let i = current; i < target; i += 1) {\n if (!(await runValidate(steps[i]))) return;\n }\n moveTo(target);\n },\n [current, moveTo, runValidate, steps],\n );\n\n const controls = useMemo<WizardControls>(\n () => ({\n activeIndex: current,\n step,\n validating,\n isFirst: current === 0,\n isLast: current === steps.length - 1,\n next,\n back,\n goTo,\n }),\n [back, current, goTo, next, step, steps.length, validating],\n );\n\n const indicatorSteps = useMemo(\n () =>\n steps.map((item) => ({\n label: item.label,\n description: item.optional\n ? `${item.description ?? \"\"} ${optionalLabel}`.trim()\n : item.description,\n })),\n [optionalLabel, steps],\n );\n\n return (\n <div className={cn(styles.wizard, className)}>\n <Stepper\n steps={indicatorSteps}\n current={current}\n onStepClick={clickableSteps ? (index) => void goTo(index) : undefined}\n />\n\n <div className={styles.body} role=\"group\" aria-label={step.label}>\n {typeof step.content === \"function\" ? step.content(controls) : step.content}\n </div>\n\n {renderActions ? (\n renderActions(controls)\n ) : (\n <div className={styles.actions}>\n <Button\n variant=\"secondary\"\n onClick={back}\n disabled={controls.isFirst || validating}\n >\n {backLabel}\n </Button>\n <Button onClick={() => void next()} loading={validating}>\n {controls.isLast ? finishLabel : nextLabel}\n </Button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Wizard.cjs","names":[],"sources":["../../../src/components/Wizard/Wizard.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — steps plus the\n * controlled-or-not pair (activeIndex, defaultActiveIndex, onStepChange,\n * onComplete), the four strings it renders (nextLabel, backLabel, finishLabel,\n * optionalLabel) and the two behaviours (clickableSteps, renderActions). The labels\n * are props because a wizard's buttons are the most translated text in an app.\n */\nimport { useCallback, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Button } from \"../Button\";\nimport { Stepper } from \"../Stepper\";\nimport styles from \"./Wizard.module.css\";\n\n/** One step of the flow. */\nexport interface WizardStep {\n /** Stable identifier. */\n id: string;\n /** Step label shown in the indicator. */\n label: string;\n /** Optional description under the label. */\n description?: string;\n /** Step body. A function receives the flow controls, for a \"skip\" link inside the form. */\n content: ReactNode | ((controls: WizardControls) => ReactNode);\n /**\n * Gate for leaving this step forward. Return `false` (or a rejected/`false`\n * promise) to keep the user here — typically `() => form.trigger()`.\n * Async is supported: the Next button shows a pending state while it runs.\n */\n validate?: () => boolean | Promise<boolean>;\n /** Marks the step as optional, so `onComplete` can ignore it. */\n optional?: boolean;\n}\n\n/** Flow controls handed to a step body and to `renderActions`. */\nexport interface WizardControls {\n /** Zero-based index of the current step. */\n activeIndex: number;\n /** The current step. */\n step: WizardStep;\n /** `true` while a `validate` promise is pending. */\n validating: boolean;\n isFirst: boolean;\n isLast: boolean;\n /** Run the current step's `validate` and advance when it passes. */\n next: () => Promise<void>;\n /** Go back one step. No validation — going back never blocks. */\n back: () => void;\n /** Jump to an index. Forward jumps validate every step in between. */\n goTo: (index: number) => Promise<void>;\n}\n\nexport interface WizardProps {\n steps: WizardStep[];\n /** Controlled active index. */\n activeIndex?: number;\n /** Uncontrolled initial index. Default `0`. */\n defaultActiveIndex?: number;\n onStepChange?: (index: number, step: WizardStep) => void;\n /** Called when the last step passes validation. */\n onComplete?: () => void | Promise<void>;\n /** Label of the advance button. Default `\"Next\"`. */\n nextLabel?: string;\n /** Label of the back button. Default `\"Back\"`. */\n backLabel?: string;\n /** Label of the button on the last step. Default `\"Finish\"`. */\n finishLabel?: string;\n /**\n * Suffix appended to an optional step's description in the indicator.\n * Default `\"(optional)\"` — override it to localize, since the SDK ships no\n * translation for component-internal copy.\n */\n optionalLabel?: string;\n /**\n * Allow clicking the indicator to jump. Default `false` — a wizard exists\n * because order matters, and a free jump skips the gates.\n */\n clickableSteps?: boolean;\n /** Replace the default button row. */\n renderActions?: (controls: WizardControls) => ReactNode;\n className?: string;\n}\n\n/**\n * Multi-step flow: step indicator, one body at a time, and navigation that\n * respects per-step validation.\n *\n * `Stepper` draws the indicator; this owns the part every app was rewriting — the\n * active index, the async gate before advancing, the disabled/pending buttons and\n * the completion call.\n *\n * Only the active step's body is mounted. Uncommitted input in a step you leave is\n * therefore lost unless the state lives outside (react-hook-form's `FormProvider`,\n * a store, a parent `useState`) — which is the right place for it anyway, since\n * the last step usually needs to submit everything at once.\n *\n * @example\n * ```tsx\n * const form = useZodForm(schema);\n *\n * <FormProvider {...form}>\n * <Wizard\n * steps={[\n * {\n * id: \"dados\",\n * label: \"Dados\",\n * validate: () => form.trigger([\"nome\", \"email\"]),\n * content: (\n * <>\n * <FormField name=\"nome\" label=\"Nome\"><Input /></FormField>\n * <FormField name=\"email\" label=\"E-mail\"><Input type=\"email\" /></FormField>\n * </>\n * ),\n * },\n * { id: \"revisao\", label: \"Revisão\", content: <Review /> },\n * ]}\n * onComplete={form.handleSubmit(onSubmit)}\n * />\n * </FormProvider>\n * ```\n */\nexport function Wizard({\n steps,\n activeIndex,\n defaultActiveIndex = 0,\n onStepChange,\n onComplete,\n nextLabel = \"Next\",\n backLabel = \"Back\",\n finishLabel = \"Finish\",\n optionalLabel = \"(optional)\",\n clickableSteps = false,\n renderActions,\n className,\n}: WizardProps) {\n const isControlled = activeIndex !== undefined;\n const [internalIndex, setInternalIndex] = useState(defaultActiveIndex);\n const [validating, setValidating] = useState(false);\n\n const current = Math.min(isControlled ? activeIndex : internalIndex, steps.length - 1);\n const step = steps[current];\n\n const moveTo = useCallback(\n (index: number): void => {\n if (!isControlled) setInternalIndex(index);\n onStepChange?.(index, steps[index]);\n },\n [isControlled, onStepChange, steps],\n );\n\n /**\n * Run a step's gate. A gate that throws counts as \"not allowed\": a `validate`\n * wired to a network check should not strand the user on a half-advanced flow\n * when the request fails.\n */\n const runValidate = useCallback(async (candidate: WizardStep): Promise<boolean> => {\n if (!candidate.validate) return true;\n setValidating(true);\n try {\n return await candidate.validate();\n } catch {\n return false;\n } finally {\n setValidating(false);\n }\n }, []);\n\n const next = useCallback(async (): Promise<void> => {\n if (!(await runValidate(step))) return;\n if (current === steps.length - 1) {\n await onComplete?.();\n return;\n }\n moveTo(current + 1);\n }, [current, moveTo, onComplete, runValidate, step, steps.length]);\n\n const back = useCallback((): void => {\n if (current > 0) moveTo(current - 1);\n }, [current, moveTo]);\n\n const goTo = useCallback(\n async (index: number): Promise<void> => {\n const target = Math.max(0, Math.min(index, steps.length - 1));\n if (target === current) return;\n if (target < current) {\n moveTo(target);\n return;\n }\n for (let i = current; i < target; i += 1) {\n if (!(await runValidate(steps[i]))) return;\n }\n moveTo(target);\n },\n [current, moveTo, runValidate, steps],\n );\n\n const controls = useMemo<WizardControls>(\n () => ({\n activeIndex: current,\n step,\n validating,\n isFirst: current === 0,\n isLast: current === steps.length - 1,\n next,\n back,\n goTo,\n }),\n [back, current, goTo, next, step, steps.length, validating],\n );\n\n const indicatorSteps = useMemo(\n () =>\n steps.map((item) => ({\n label: item.label,\n description: item.optional\n ? `${item.description ?? \"\"} ${optionalLabel}`.trim()\n : item.description,\n })),\n [optionalLabel, steps],\n );\n\n return (\n <div className={cn(styles.wizard, className)}>\n <Stepper\n steps={indicatorSteps}\n current={current}\n onStepClick={clickableSteps ? (index) => void goTo(index) : undefined}\n />\n\n <div className={styles.body} role=\"group\" aria-label={step.label}>\n {typeof step.content === \"function\" ? step.content(controls) : step.content}\n </div>\n\n {renderActions ? (\n renderActions(controls)\n ) : (\n <div className={styles.actions}>\n <Button\n variant=\"secondary\"\n onClick={back}\n disabled={controls.isFirst || validating}\n >\n {backLabel}\n </Button>\n <Button onClick={() => void next()} loading={validating}>\n {controls.isLast ? finishLabel : nextLabel}\n </Button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Wizard.js","names":[],"sources":["../../../src/components/Wizard/Wizard.tsx"],"sourcesContent":["import { useCallback, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Button } from \"../Button\";\nimport { Stepper } from \"../Stepper\";\nimport styles from \"./Wizard.module.css\";\n\n/** One step of the flow. */\nexport interface WizardStep {\n /** Stable identifier. */\n id: string;\n /** Step label shown in the indicator. */\n label: string;\n /** Optional description under the label. */\n description?: string;\n /** Step body. A function receives the flow controls, for a \"skip\" link inside the form. */\n content: ReactNode | ((controls: WizardControls) => ReactNode);\n /**\n * Gate for leaving this step forward. Return `false` (or a rejected/`false`\n * promise) to keep the user here — typically `() => form.trigger()`.\n * Async is supported: the Next button shows a pending state while it runs.\n */\n validate?: () => boolean | Promise<boolean>;\n /** Marks the step as optional, so `onComplete` can ignore it. */\n optional?: boolean;\n}\n\n/** Flow controls handed to a step body and to `renderActions`. */\nexport interface WizardControls {\n /** Zero-based index of the current step. */\n activeIndex: number;\n /** The current step. */\n step: WizardStep;\n /** `true` while a `validate` promise is pending. */\n validating: boolean;\n isFirst: boolean;\n isLast: boolean;\n /** Run the current step's `validate` and advance when it passes. */\n next: () => Promise<void>;\n /** Go back one step. No validation — going back never blocks. */\n back: () => void;\n /** Jump to an index. Forward jumps validate every step in between. */\n goTo: (index: number) => Promise<void>;\n}\n\nexport interface WizardProps {\n steps: WizardStep[];\n /** Controlled active index. */\n activeIndex?: number;\n /** Uncontrolled initial index. Default `0`. */\n defaultActiveIndex?: number;\n onStepChange?: (index: number, step: WizardStep) => void;\n /** Called when the last step passes validation. */\n onComplete?: () => void | Promise<void>;\n /** Label of the advance button. Default `\"Next\"`. */\n nextLabel?: string;\n /** Label of the back button. Default `\"Back\"`. */\n backLabel?: string;\n /** Label of the button on the last step. Default `\"Finish\"`. */\n finishLabel?: string;\n /**\n * Suffix appended to an optional step's description in the indicator.\n * Default `\"(optional)\"` — override it to localize, since the SDK ships no\n * translation for component-internal copy.\n */\n optionalLabel?: string;\n /**\n * Allow clicking the indicator to jump. Default `false` — a wizard exists\n * because order matters, and a free jump skips the gates.\n */\n clickableSteps?: boolean;\n /** Replace the default button row. */\n renderActions?: (controls: WizardControls) => ReactNode;\n className?: string;\n}\n\n/**\n * Multi-step flow: step indicator, one body at a time, and navigation that\n * respects per-step validation.\n *\n * `Stepper` draws the indicator; this owns the part every app was rewriting — the\n * active index, the async gate before advancing, the disabled/pending buttons and\n * the completion call.\n *\n * Only the active step's body is mounted. Uncommitted input in a step you leave is\n * therefore lost unless the state lives outside (react-hook-form's `FormProvider`,\n * a store, a parent `useState`) — which is the right place for it anyway, since\n * the last step usually needs to submit everything at once.\n *\n * @example\n * ```tsx\n * const form = useZodForm(schema);\n *\n * <FormProvider {...form}>\n * <Wizard\n * steps={[\n * {\n * id: \"dados\",\n * label: \"Dados\",\n * validate: () => form.trigger([\"nome\", \"email\"]),\n * content: (\n * <>\n * <FormField name=\"nome\" label=\"Nome\"><Input /></FormField>\n * <FormField name=\"email\" label=\"E-mail\"><Input type=\"email\" /></FormField>\n * </>\n * ),\n * },\n * { id: \"revisao\", label: \"Revisão\", content: <Review /> },\n * ]}\n * onComplete={form.handleSubmit(onSubmit)}\n * />\n * </FormProvider>\n * ```\n */\nexport function Wizard({\n steps,\n activeIndex,\n defaultActiveIndex = 0,\n onStepChange,\n onComplete,\n nextLabel = \"Next\",\n backLabel = \"Back\",\n finishLabel = \"Finish\",\n optionalLabel = \"(optional)\",\n clickableSteps = false,\n renderActions,\n className,\n}: WizardProps) {\n const isControlled = activeIndex !== undefined;\n const [internalIndex, setInternalIndex] = useState(defaultActiveIndex);\n const [validating, setValidating] = useState(false);\n\n const current = Math.min(isControlled ? activeIndex : internalIndex, steps.length - 1);\n const step = steps[current];\n\n const moveTo = useCallback(\n (index: number): void => {\n if (!isControlled) setInternalIndex(index);\n onStepChange?.(index, steps[index]);\n },\n [isControlled, onStepChange, steps],\n );\n\n /**\n * Run a step's gate. A gate that throws counts as \"not allowed\": a `validate`\n * wired to a network check should not strand the user on a half-advanced flow\n * when the request fails.\n */\n const runValidate = useCallback(async (candidate: WizardStep): Promise<boolean> => {\n if (!candidate.validate) return true;\n setValidating(true);\n try {\n return await candidate.validate();\n } catch {\n return false;\n } finally {\n setValidating(false);\n }\n }, []);\n\n const next = useCallback(async (): Promise<void> => {\n if (!(await runValidate(step))) return;\n if (current === steps.length - 1) {\n await onComplete?.();\n return;\n }\n moveTo(current + 1);\n }, [current, moveTo, onComplete, runValidate, step, steps.length]);\n\n const back = useCallback((): void => {\n if (current > 0) moveTo(current - 1);\n }, [current, moveTo]);\n\n const goTo = useCallback(\n async (index: number): Promise<void> => {\n const target = Math.max(0, Math.min(index, steps.length - 1));\n if (target === current) return;\n if (target < current) {\n moveTo(target);\n return;\n }\n for (let i = current; i < target; i += 1) {\n if (!(await runValidate(steps[i]))) return;\n }\n moveTo(target);\n },\n [current, moveTo, runValidate, steps],\n );\n\n const controls = useMemo<WizardControls>(\n () => ({\n activeIndex: current,\n step,\n validating,\n isFirst: current === 0,\n isLast: current === steps.length - 1,\n next,\n back,\n goTo,\n }),\n [back, current, goTo, next, step, steps.length, validating],\n );\n\n const indicatorSteps = useMemo(\n () =>\n steps.map((item) => ({\n label: item.label,\n description: item.optional\n ? `${item.description ?? \"\"} ${optionalLabel}`.trim()\n : item.description,\n })),\n [optionalLabel, steps],\n );\n\n return (\n <div className={cn(styles.wizard, className)}>\n <Stepper\n steps={indicatorSteps}\n current={current}\n onStepClick={clickableSteps ? (index) => void goTo(index) : undefined}\n />\n\n <div className={styles.body} role=\"group\" aria-label={step.label}>\n {typeof step.content === \"function\" ? step.content(controls) : step.content}\n </div>\n\n {renderActions ? (\n renderActions(controls)\n ) : (\n <div className={styles.actions}>\n <Button\n variant=\"secondary\"\n onClick={back}\n disabled={controls.isFirst || validating}\n >\n {backLabel}\n </Button>\n <Button onClick={() => void next()} loading={validating}>\n {controls.isLast ? finishLabel : nextLabel}\n </Button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAkHA,SAAgB,EAAO,EACnB,UACA,gBACA,wBAAqB,GACrB,iBACA,eACA,eAAY,QACZ,eAAY,QACZ,iBAAc,UACd,mBAAgB,cAChB,oBAAiB,IACjB,kBACA,gBACY;CACZ,IAAM,IAAe,MAAgB,KAAA,GAC/B,CAAC,GAAe,KAAoB,EAAS,CAAkB,GAC/D,CAAC,GAAY,KAAiB,EAAS,EAAK,GAE5C,IAAU,KAAK,IAAI,IAAe,IAAc,GAAe,EAAM,SAAS,CAAC,GAC/E,IAAO,EAAM,IAEb,IAAS,GACV,MAAwB;EAErB,AADK,KAAc,EAAiB,CAAK,GACzC,IAAe,GAAO,EAAM,EAAM;CACtC,GACA;EAAC;EAAc;EAAc;CAAK,CACtC,GAOM,IAAc,EAAY,OAAO,MAA4C;EAC/E,IAAI,CAAC,EAAU,UAAU,OAAO;EAChC,EAAc,EAAI;EAClB,IAAI;GACA,OAAO,MAAM,EAAU,SAAS;EACpC,QAAQ;GACJ,OAAO;EACX,UAAU;GACN,EAAc,EAAK;EACvB;CACJ,GAAG,CAAC,CAAC,GAEC,IAAO,EAAY,YAA2B;EAC1C,UAAM,EAAY,CAAI,GAC5B;OAAI,MAAY,EAAM,SAAS,GAAG;IAC9B,MAAM,IAAa;IACnB;GACJ;GACA,EAAO,IAAU,CAAC;EADlB;CAEJ,GAAG;EAAC;EAAS;EAAQ;EAAY;EAAa;EAAM,EAAM;CAAM,CAAC,GAE3D,IAAO,QAAwB;EACjC,AAAI,IAAU,KAAG,EAAO,IAAU,CAAC;CACvC,GAAG,CAAC,GAAS,CAAM,CAAC,GAEd,IAAO,EACT,OAAO,MAAiC;EACpC,IAAM,IAAS,KAAK,IAAI,GAAG,KAAK,IAAI,GAAO,EAAM,SAAS,CAAC,CAAC;EACxD,UAAW,GACf;OAAI,IAAS,GAAS;IAClB,EAAO,CAAM;IACb;GACJ;GACA,KAAK,IAAI,IAAI,GAAS,IAAI,GAAQ,KAAK,GACnC,IAAI,CAAE,MAAM,EAAY,EAAM,EAAE,GAAI;GAExC,EAAO,CAAM;EAJb;CAKJ,GACA;EAAC;EAAS;EAAQ;EAAa;CAAK,CACxC,GAEM,IAAW,SACN;EACH,aAAa;EACb;EACA;EACA,SAAS,MAAY;EACrB,QAAQ,MAAY,EAAM,SAAS;EACnC;EACA;EACA;CACJ,IACA;EAAC;EAAM;EAAS;EAAM;EAAM;EAAM,EAAM;EAAQ;CAAU,CAC9D,GAEM,IAAiB,QAEf,EAAM,KAAK,OAAU;EACjB,OAAO,EAAK;EACZ,aAAa,EAAK,WACZ,GAAG,EAAK,eAAe,GAAG,GAAG,IAAgB,KAAK,IAClD,EAAK;CACf,EAAE,GACN,CAAC,GAAe,CAAK,CACzB;CAEA,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,QAAQ,CAAS;YAA3C;GACI,kBAAC,GAAD;IACI,OAAO;IACE;IACT,aAAa,KAAkB,MAAU,KAAK,EAAK,CAAK,IAAI,KAAA;GAC/D,CAAA;GAED,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAM,MAAK;IAAQ,cAAY,EAAK;cACtD,OAAO,EAAK,WAAY,aAAa,EAAK,QAAQ,CAAQ,IAAI,EAAK;GACnE,CAAA;GAEJ,IACG,EAAc,CAAQ,IAEtB,kBAAC,OAAD;IAAK,WAAW,EAAO;cAAvB,CACI,kBAAC,GAAD;KACI,SAAQ;KACR,SAAS;KACT,UAAU,EAAS,WAAW;eAE7B;IACG,CAAA,GACR,kBAAC,GAAD;KAAQ,eAAe,KAAK,EAAK;KAAG,SAAS;eACxC,EAAS,SAAS,IAAc;IAC7B,CAAA,CACP;;EAER;;AAEb"}
|
|
1
|
+
{"version":3,"file":"Wizard.js","names":[],"sources":["../../../src/components/Wizard/Wizard.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — steps plus the\n * controlled-or-not pair (activeIndex, defaultActiveIndex, onStepChange,\n * onComplete), the four strings it renders (nextLabel, backLabel, finishLabel,\n * optionalLabel) and the two behaviours (clickableSteps, renderActions). The labels\n * are props because a wizard's buttons are the most translated text in an app.\n */\nimport { useCallback, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Button } from \"../Button\";\nimport { Stepper } from \"../Stepper\";\nimport styles from \"./Wizard.module.css\";\n\n/** One step of the flow. */\nexport interface WizardStep {\n /** Stable identifier. */\n id: string;\n /** Step label shown in the indicator. */\n label: string;\n /** Optional description under the label. */\n description?: string;\n /** Step body. A function receives the flow controls, for a \"skip\" link inside the form. */\n content: ReactNode | ((controls: WizardControls) => ReactNode);\n /**\n * Gate for leaving this step forward. Return `false` (or a rejected/`false`\n * promise) to keep the user here — typically `() => form.trigger()`.\n * Async is supported: the Next button shows a pending state while it runs.\n */\n validate?: () => boolean | Promise<boolean>;\n /** Marks the step as optional, so `onComplete` can ignore it. */\n optional?: boolean;\n}\n\n/** Flow controls handed to a step body and to `renderActions`. */\nexport interface WizardControls {\n /** Zero-based index of the current step. */\n activeIndex: number;\n /** The current step. */\n step: WizardStep;\n /** `true` while a `validate` promise is pending. */\n validating: boolean;\n isFirst: boolean;\n isLast: boolean;\n /** Run the current step's `validate` and advance when it passes. */\n next: () => Promise<void>;\n /** Go back one step. No validation — going back never blocks. */\n back: () => void;\n /** Jump to an index. Forward jumps validate every step in between. */\n goTo: (index: number) => Promise<void>;\n}\n\nexport interface WizardProps {\n steps: WizardStep[];\n /** Controlled active index. */\n activeIndex?: number;\n /** Uncontrolled initial index. Default `0`. */\n defaultActiveIndex?: number;\n onStepChange?: (index: number, step: WizardStep) => void;\n /** Called when the last step passes validation. */\n onComplete?: () => void | Promise<void>;\n /** Label of the advance button. Default `\"Next\"`. */\n nextLabel?: string;\n /** Label of the back button. Default `\"Back\"`. */\n backLabel?: string;\n /** Label of the button on the last step. Default `\"Finish\"`. */\n finishLabel?: string;\n /**\n * Suffix appended to an optional step's description in the indicator.\n * Default `\"(optional)\"` — override it to localize, since the SDK ships no\n * translation for component-internal copy.\n */\n optionalLabel?: string;\n /**\n * Allow clicking the indicator to jump. Default `false` — a wizard exists\n * because order matters, and a free jump skips the gates.\n */\n clickableSteps?: boolean;\n /** Replace the default button row. */\n renderActions?: (controls: WizardControls) => ReactNode;\n className?: string;\n}\n\n/**\n * Multi-step flow: step indicator, one body at a time, and navigation that\n * respects per-step validation.\n *\n * `Stepper` draws the indicator; this owns the part every app was rewriting — the\n * active index, the async gate before advancing, the disabled/pending buttons and\n * the completion call.\n *\n * Only the active step's body is mounted. Uncommitted input in a step you leave is\n * therefore lost unless the state lives outside (react-hook-form's `FormProvider`,\n * a store, a parent `useState`) — which is the right place for it anyway, since\n * the last step usually needs to submit everything at once.\n *\n * @example\n * ```tsx\n * const form = useZodForm(schema);\n *\n * <FormProvider {...form}>\n * <Wizard\n * steps={[\n * {\n * id: \"dados\",\n * label: \"Dados\",\n * validate: () => form.trigger([\"nome\", \"email\"]),\n * content: (\n * <>\n * <FormField name=\"nome\" label=\"Nome\"><Input /></FormField>\n * <FormField name=\"email\" label=\"E-mail\"><Input type=\"email\" /></FormField>\n * </>\n * ),\n * },\n * { id: \"revisao\", label: \"Revisão\", content: <Review /> },\n * ]}\n * onComplete={form.handleSubmit(onSubmit)}\n * />\n * </FormProvider>\n * ```\n */\nexport function Wizard({\n steps,\n activeIndex,\n defaultActiveIndex = 0,\n onStepChange,\n onComplete,\n nextLabel = \"Next\",\n backLabel = \"Back\",\n finishLabel = \"Finish\",\n optionalLabel = \"(optional)\",\n clickableSteps = false,\n renderActions,\n className,\n}: WizardProps) {\n const isControlled = activeIndex !== undefined;\n const [internalIndex, setInternalIndex] = useState(defaultActiveIndex);\n const [validating, setValidating] = useState(false);\n\n const current = Math.min(isControlled ? activeIndex : internalIndex, steps.length - 1);\n const step = steps[current];\n\n const moveTo = useCallback(\n (index: number): void => {\n if (!isControlled) setInternalIndex(index);\n onStepChange?.(index, steps[index]);\n },\n [isControlled, onStepChange, steps],\n );\n\n /**\n * Run a step's gate. A gate that throws counts as \"not allowed\": a `validate`\n * wired to a network check should not strand the user on a half-advanced flow\n * when the request fails.\n */\n const runValidate = useCallback(async (candidate: WizardStep): Promise<boolean> => {\n if (!candidate.validate) return true;\n setValidating(true);\n try {\n return await candidate.validate();\n } catch {\n return false;\n } finally {\n setValidating(false);\n }\n }, []);\n\n const next = useCallback(async (): Promise<void> => {\n if (!(await runValidate(step))) return;\n if (current === steps.length - 1) {\n await onComplete?.();\n return;\n }\n moveTo(current + 1);\n }, [current, moveTo, onComplete, runValidate, step, steps.length]);\n\n const back = useCallback((): void => {\n if (current > 0) moveTo(current - 1);\n }, [current, moveTo]);\n\n const goTo = useCallback(\n async (index: number): Promise<void> => {\n const target = Math.max(0, Math.min(index, steps.length - 1));\n if (target === current) return;\n if (target < current) {\n moveTo(target);\n return;\n }\n for (let i = current; i < target; i += 1) {\n if (!(await runValidate(steps[i]))) return;\n }\n moveTo(target);\n },\n [current, moveTo, runValidate, steps],\n );\n\n const controls = useMemo<WizardControls>(\n () => ({\n activeIndex: current,\n step,\n validating,\n isFirst: current === 0,\n isLast: current === steps.length - 1,\n next,\n back,\n goTo,\n }),\n [back, current, goTo, next, step, steps.length, validating],\n );\n\n const indicatorSteps = useMemo(\n () =>\n steps.map((item) => ({\n label: item.label,\n description: item.optional\n ? `${item.description ?? \"\"} ${optionalLabel}`.trim()\n : item.description,\n })),\n [optionalLabel, steps],\n );\n\n return (\n <div className={cn(styles.wizard, className)}>\n <Stepper\n steps={indicatorSteps}\n current={current}\n onStepClick={clickableSteps ? (index) => void goTo(index) : undefined}\n />\n\n <div className={styles.body} role=\"group\" aria-label={step.label}>\n {typeof step.content === \"function\" ? step.content(controls) : step.content}\n </div>\n\n {renderActions ? (\n renderActions(controls)\n ) : (\n <div className={styles.actions}>\n <Button\n variant=\"secondary\"\n onClick={back}\n disabled={controls.isFirst || validating}\n >\n {backLabel}\n </Button>\n <Button onClick={() => void next()} loading={validating}>\n {controls.isLast ? finishLabel : nextLabel}\n </Button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAyHA,SAAgB,EAAO,EACnB,UACA,gBACA,wBAAqB,GACrB,iBACA,eACA,eAAY,QACZ,eAAY,QACZ,iBAAc,UACd,mBAAgB,cAChB,oBAAiB,IACjB,kBACA,gBACY;CACZ,IAAM,IAAe,MAAgB,KAAA,GAC/B,CAAC,GAAe,KAAoB,EAAS,CAAkB,GAC/D,CAAC,GAAY,KAAiB,EAAS,EAAK,GAE5C,IAAU,KAAK,IAAI,IAAe,IAAc,GAAe,EAAM,SAAS,CAAC,GAC/E,IAAO,EAAM,IAEb,IAAS,GACV,MAAwB;EAErB,AADK,KAAc,EAAiB,CAAK,GACzC,IAAe,GAAO,EAAM,EAAM;CACtC,GACA;EAAC;EAAc;EAAc;CAAK,CACtC,GAOM,IAAc,EAAY,OAAO,MAA4C;EAC/E,IAAI,CAAC,EAAU,UAAU,OAAO;EAChC,EAAc,EAAI;EAClB,IAAI;GACA,OAAO,MAAM,EAAU,SAAS;EACpC,QAAQ;GACJ,OAAO;EACX,UAAU;GACN,EAAc,EAAK;EACvB;CACJ,GAAG,CAAC,CAAC,GAEC,IAAO,EAAY,YAA2B;EAC1C,UAAM,EAAY,CAAI,GAC5B;OAAI,MAAY,EAAM,SAAS,GAAG;IAC9B,MAAM,IAAa;IACnB;GACJ;GACA,EAAO,IAAU,CAAC;EADlB;CAEJ,GAAG;EAAC;EAAS;EAAQ;EAAY;EAAa;EAAM,EAAM;CAAM,CAAC,GAE3D,IAAO,QAAwB;EACjC,AAAI,IAAU,KAAG,EAAO,IAAU,CAAC;CACvC,GAAG,CAAC,GAAS,CAAM,CAAC,GAEd,IAAO,EACT,OAAO,MAAiC;EACpC,IAAM,IAAS,KAAK,IAAI,GAAG,KAAK,IAAI,GAAO,EAAM,SAAS,CAAC,CAAC;EACxD,UAAW,GACf;OAAI,IAAS,GAAS;IAClB,EAAO,CAAM;IACb;GACJ;GACA,KAAK,IAAI,IAAI,GAAS,IAAI,GAAQ,KAAK,GACnC,IAAI,CAAE,MAAM,EAAY,EAAM,EAAE,GAAI;GAExC,EAAO,CAAM;EAJb;CAKJ,GACA;EAAC;EAAS;EAAQ;EAAa;CAAK,CACxC,GAEM,IAAW,SACN;EACH,aAAa;EACb;EACA;EACA,SAAS,MAAY;EACrB,QAAQ,MAAY,EAAM,SAAS;EACnC;EACA;EACA;CACJ,IACA;EAAC;EAAM;EAAS;EAAM;EAAM;EAAM,EAAM;EAAQ;CAAU,CAC9D,GAEM,IAAiB,QAEf,EAAM,KAAK,OAAU;EACjB,OAAO,EAAK;EACZ,aAAa,EAAK,WACZ,GAAG,EAAK,eAAe,GAAG,GAAG,IAAgB,KAAK,IAClD,EAAK;CACf,EAAE,GACN,CAAC,GAAe,CAAK,CACzB;CAEA,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,QAAQ,CAAS;YAA3C;GACI,kBAAC,GAAD;IACI,OAAO;IACE;IACT,aAAa,KAAkB,MAAU,KAAK,EAAK,CAAK,IAAI,KAAA;GAC/D,CAAA;GAED,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAM,MAAK;IAAQ,cAAY,EAAK;cACtD,OAAO,EAAK,WAAY,aAAa,EAAK,QAAQ,CAAQ,IAAI,EAAK;GACnE,CAAA;GAEJ,IACG,EAAc,CAAQ,IAEtB,kBAAC,OAAD;IAAK,WAAW,EAAO;cAAvB,CACI,kBAAC,GAAD;KACI,SAAQ;KACR,SAAS;KACT,UAAU,EAAS,WAAW;eAE7B;IACG,CAAA,GACR,kBAAC,GAAD;KAAQ,eAAe,KAAK,EAAK;KAAG,SAAS;eACxC,EAAS,SAAS,IAAc;IAC7B,CAAA,CACP;;EAER;;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RichTextEditor.cjs","names":[],"sources":["../../src/editor/RichTextEditor.tsx"],"sourcesContent":["import { useEffect } from \"react\";\nimport { EditorContent, useEditor, useEditorState } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport {\n Bold,\n Code,\n Heading1,\n Heading2,\n Italic,\n List,\n ListOrdered,\n Quote,\n Redo2,\n Strikethrough,\n Undo2,\n} from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./RichTextEditor.module.css\";\n\nexport interface RichTextEditorProps {\n /** Current editor content as an HTML string (controlled). */\n value: string;\n /** Called with the updated HTML whenever the document changes. */\n onChange: (html: string) => void;\n /** Placeholder text shown when the editor is empty. */\n placeholder?: string;\n /** Whether the content is editable. Defaults to `true`. */\n editable?: boolean;\n /** Whether to render the formatting toolbar. Defaults to `true`. */\n toolbar?: boolean;\n /** Extra class names applied to the wrapper element. */\n className?: string;\n}\n\n/**\n * RichTextEditor — a controlled WYSIWYG editor built on tiptap v3.\n *\n * Renders an optional formatting toolbar (bold, italic, strike, code, headings,\n * lists, blockquote, undo/redo) above a ProseMirror-backed editable area. The\n * document is controlled via the `value` (HTML) / `onChange` pair: external\n * changes to `value` are synced into the editor without re-emitting updates.\n *\n * The toolbar's pressed states and the undo/redo `disabled` flags come from\n * `useEditorState`, which subscribes to exactly those values. `useEditor` does\n * not re-render on transactions (the tiptap v3 default), so reading `isActive`\n * and `can()` directly in the render body left every button stale — toggling\n * bold with a collapsed cursor arms the mark without changing the document, so no\n * update event ever brought React back to recompute the state. Subscribing\n * re-renders only when one of the tracked values actually flips, which is also\n * cheaper than re-rendering on every keystroke.\n *\n * Two effects keep the instance aligned with the props: one pushes an external\n * `value` change into the document with `emitUpdate: false` (so syncing never\n * re-emits `onChange`), the other mirrors the `editable` flag.\n */\nexport function RichTextEditor({\n value,\n onChange,\n placeholder,\n editable = true,\n toolbar = true,\n className,\n}: RichTextEditorProps) {\n const editor = useEditor({\n extensions: [StarterKit],\n content: value,\n editable,\n onUpdate: ({ editor: instance }) => onChange(instance.getHTML()),\n });\n\n useEffect(() => {\n if (!editor) return;\n if (value !== editor.getHTML()) {\n editor.commands.setContent(value, { emitUpdate: false });\n }\n }, [editor, value]);\n\n useEffect(() => {\n if (!editor) return;\n editor.setEditable(editable);\n }, [editor, editable]);\n\n const toolbarState = useEditorState({\n editor,\n selector: ({ editor: instance }) => ({\n bold: instance.isActive(\"bold\"),\n italic: instance.isActive(\"italic\"),\n strike: instance.isActive(\"strike\"),\n code: instance.isActive(\"code\"),\n heading1: instance.isActive(\"heading\", { level: 1 }),\n heading2: instance.isActive(\"heading\", { level: 2 }),\n bulletList: instance.isActive(\"bulletList\"),\n orderedList: instance.isActive(\"orderedList\"),\n blockquote: instance.isActive(\"blockquote\"),\n canUndo: instance.can().chain().focus().undo().run(),\n canRedo: instance.can().chain().focus().redo().run(),\n }),\n });\n\n if (!editor || !toolbarState) return null;\n\n const canUndo = toolbarState.canUndo;\n const canRedo = toolbarState.canRedo;\n\n return (\n <div className={cn(styles.wrapper, className)}>\n {toolbar && (\n <div className={styles.toolbar} role=\"toolbar\" aria-label=\"Formatação de texto\">\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.bold && styles.active)}\n aria-label=\"Negrito\"\n aria-pressed={toolbarState.bold}\n onClick={() => editor.chain().focus().toggleBold().run()}\n >\n <Bold size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.italic && styles.active)}\n aria-label=\"Itálico\"\n aria-pressed={toolbarState.italic}\n onClick={() => editor.chain().focus().toggleItalic().run()}\n >\n <Italic size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.strike && styles.active)}\n aria-label=\"Tachado\"\n aria-pressed={toolbarState.strike}\n onClick={() => editor.chain().focus().toggleStrike().run()}\n >\n <Strikethrough size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.code && styles.active)}\n aria-label=\"Código\"\n aria-pressed={toolbarState.code}\n onClick={() => editor.chain().focus().toggleCode().run()}\n >\n <Code size={16} aria-hidden />\n </button>\n <span className={styles.separator} aria-hidden />\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.heading1 && styles.active)}\n aria-label=\"Título 1\"\n aria-pressed={toolbarState.heading1}\n onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}\n >\n <Heading1 size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.heading2 && styles.active)}\n aria-label=\"Título 2\"\n aria-pressed={toolbarState.heading2}\n onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}\n >\n <Heading2 size={16} aria-hidden />\n </button>\n <span className={styles.separator} aria-hidden />\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.bulletList && styles.active)}\n aria-label=\"Lista com marcadores\"\n aria-pressed={toolbarState.bulletList}\n onClick={() => editor.chain().focus().toggleBulletList().run()}\n >\n <List size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.orderedList && styles.active)}\n aria-label=\"Lista numerada\"\n aria-pressed={toolbarState.orderedList}\n onClick={() => editor.chain().focus().toggleOrderedList().run()}\n >\n <ListOrdered size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.blockquote && styles.active)}\n aria-label=\"Citação\"\n aria-pressed={toolbarState.blockquote}\n onClick={() => editor.chain().focus().toggleBlockquote().run()}\n >\n <Quote size={16} aria-hidden />\n </button>\n <span className={styles.separator} aria-hidden />\n <button\n type=\"button\"\n className={styles.button}\n aria-label=\"Desfazer\"\n disabled={!canUndo}\n onClick={() => editor.chain().focus().undo().run()}\n >\n <Undo2 size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={styles.button}\n aria-label=\"Refazer\"\n disabled={!canRedo}\n onClick={() => editor.chain().focus().redo().run()}\n >\n <Redo2 size={16} aria-hidden />\n </button>\n </div>\n )}\n <EditorContent\n editor={editor}\n className={styles.content}\n data-placeholder={placeholder}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RichTextEditor.cjs","names":[],"sources":["../../src/editor/RichTextEditor.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — the body builds the tiptap editor,\n * its toolbar state and the change plumbing: every toolbar button reads\n * `editor.isActive`, so the toolbar cannot be lifted out without passing the editor\n * instance back down.\n */\nimport { useEffect } from \"react\";\nimport { EditorContent, useEditor, useEditorState } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport {\n Bold,\n Code,\n Heading1,\n Heading2,\n Italic,\n List,\n ListOrdered,\n Quote,\n Redo2,\n Strikethrough,\n Undo2,\n} from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./RichTextEditor.module.css\";\n\nexport interface RichTextEditorProps {\n /** Current editor content as an HTML string (controlled). */\n value: string;\n /** Called with the updated HTML whenever the document changes. */\n onChange: (html: string) => void;\n /** Placeholder text shown when the editor is empty. */\n placeholder?: string;\n /** Whether the content is editable. Defaults to `true`. */\n editable?: boolean;\n /** Whether to render the formatting toolbar. Defaults to `true`. */\n toolbar?: boolean;\n /** Extra class names applied to the wrapper element. */\n className?: string;\n}\n\n/**\n * RichTextEditor — a controlled WYSIWYG editor built on tiptap v3.\n *\n * Renders an optional formatting toolbar (bold, italic, strike, code, headings,\n * lists, blockquote, undo/redo) above a ProseMirror-backed editable area. The\n * document is controlled via the `value` (HTML) / `onChange` pair: external\n * changes to `value` are synced into the editor without re-emitting updates.\n *\n * The toolbar's pressed states and the undo/redo `disabled` flags come from\n * `useEditorState`, which subscribes to exactly those values. `useEditor` does\n * not re-render on transactions (the tiptap v3 default), so reading `isActive`\n * and `can()` directly in the render body left every button stale — toggling\n * bold with a collapsed cursor arms the mark without changing the document, so no\n * update event ever brought React back to recompute the state. Subscribing\n * re-renders only when one of the tracked values actually flips, which is also\n * cheaper than re-rendering on every keystroke.\n *\n * Two effects keep the instance aligned with the props: one pushes an external\n * `value` change into the document with `emitUpdate: false` (so syncing never\n * re-emits `onChange`), the other mirrors the `editable` flag.\n */\nexport function RichTextEditor({\n value,\n onChange,\n placeholder,\n editable = true,\n toolbar = true,\n className,\n}: RichTextEditorProps) {\n const editor = useEditor({\n extensions: [StarterKit],\n content: value,\n editable,\n onUpdate: ({ editor: instance }) => onChange(instance.getHTML()),\n });\n\n useEffect(() => {\n if (!editor) return;\n if (value !== editor.getHTML()) {\n editor.commands.setContent(value, { emitUpdate: false });\n }\n }, [editor, value]);\n\n useEffect(() => {\n if (!editor) return;\n editor.setEditable(editable);\n }, [editor, editable]);\n\n const toolbarState = useEditorState({\n editor,\n selector: ({ editor: instance }) => ({\n bold: instance.isActive(\"bold\"),\n italic: instance.isActive(\"italic\"),\n strike: instance.isActive(\"strike\"),\n code: instance.isActive(\"code\"),\n heading1: instance.isActive(\"heading\", { level: 1 }),\n heading2: instance.isActive(\"heading\", { level: 2 }),\n bulletList: instance.isActive(\"bulletList\"),\n orderedList: instance.isActive(\"orderedList\"),\n blockquote: instance.isActive(\"blockquote\"),\n canUndo: instance.can().chain().focus().undo().run(),\n canRedo: instance.can().chain().focus().redo().run(),\n }),\n });\n\n if (!editor || !toolbarState) return null;\n\n const canUndo = toolbarState.canUndo;\n const canRedo = toolbarState.canRedo;\n\n return (\n <div className={cn(styles.wrapper, className)}>\n {toolbar && (\n <div className={styles.toolbar} role=\"toolbar\" aria-label=\"Formatação de texto\">\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.bold && styles.active)}\n aria-label=\"Negrito\"\n aria-pressed={toolbarState.bold}\n onClick={() => editor.chain().focus().toggleBold().run()}\n >\n <Bold size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.italic && styles.active)}\n aria-label=\"Itálico\"\n aria-pressed={toolbarState.italic}\n onClick={() => editor.chain().focus().toggleItalic().run()}\n >\n <Italic size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.strike && styles.active)}\n aria-label=\"Tachado\"\n aria-pressed={toolbarState.strike}\n onClick={() => editor.chain().focus().toggleStrike().run()}\n >\n <Strikethrough size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.code && styles.active)}\n aria-label=\"Código\"\n aria-pressed={toolbarState.code}\n onClick={() => editor.chain().focus().toggleCode().run()}\n >\n <Code size={16} aria-hidden />\n </button>\n <span className={styles.separator} aria-hidden />\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.heading1 && styles.active)}\n aria-label=\"Título 1\"\n aria-pressed={toolbarState.heading1}\n onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}\n >\n <Heading1 size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.heading2 && styles.active)}\n aria-label=\"Título 2\"\n aria-pressed={toolbarState.heading2}\n onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}\n >\n <Heading2 size={16} aria-hidden />\n </button>\n <span className={styles.separator} aria-hidden />\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.bulletList && styles.active)}\n aria-label=\"Lista com marcadores\"\n aria-pressed={toolbarState.bulletList}\n onClick={() => editor.chain().focus().toggleBulletList().run()}\n >\n <List size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.orderedList && styles.active)}\n aria-label=\"Lista numerada\"\n aria-pressed={toolbarState.orderedList}\n onClick={() => editor.chain().focus().toggleOrderedList().run()}\n >\n <ListOrdered size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.blockquote && styles.active)}\n aria-label=\"Citação\"\n aria-pressed={toolbarState.blockquote}\n onClick={() => editor.chain().focus().toggleBlockquote().run()}\n >\n <Quote size={16} aria-hidden />\n </button>\n <span className={styles.separator} aria-hidden />\n <button\n type=\"button\"\n className={styles.button}\n aria-label=\"Desfazer\"\n disabled={!canUndo}\n onClick={() => editor.chain().focus().undo().run()}\n >\n <Undo2 size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={styles.button}\n aria-label=\"Refazer\"\n disabled={!canRedo}\n onClick={() => editor.chain().focus().redo().run()}\n >\n <Redo2 size={16} aria-hidden />\n </button>\n </div>\n )}\n <EditorContent\n editor={editor}\n className={styles.content}\n data-placeholder={placeholder}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|