@umodeapporg/ui 0.27.0 → 0.29.0
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/dist/assets/umode-ui.css +0 -5
- package/dist/components/DateTime/DatePicker.d.ts +2 -0
- package/dist/components/DateTime/DatePicker.js +5 -2
- package/dist/components/DateTime/DatePicker.js.map +1 -1
- package/dist/components/DateTime/DateRangePicker.d.ts +2 -0
- package/dist/components/DateTime/DateRangePicker.js +3 -0
- package/dist/components/DateTime/DateRangePicker.js.map +1 -1
- package/dist/components/Form/ControlledButton.d.ts +1 -1
- package/dist/components/Form/ControlledButton.js +2 -2
- package/dist/components/Form/ControlledButton.js.map +1 -1
- package/dist/components/Form/ControlledCheckbox.d.ts +5 -1
- package/dist/components/Form/ControlledCheckbox.js +53 -39
- package/dist/components/Form/ControlledCheckbox.js.map +1 -1
- package/dist/components/Form/ControlledDialogPrimitives.js +17 -5
- package/dist/components/Form/ControlledDialogPrimitives.js.map +1 -1
- package/dist/components/Form/ControlledDrawer.js +1 -0
- package/dist/components/Form/ControlledDrawer.js.map +1 -1
- package/dist/components/Form/ControlledImage/index.js +2 -0
- package/dist/components/Form/ControlledImage/index.js.map +1 -1
- package/dist/components/Form/ControlledInput.d.ts +1 -1
- package/dist/components/Form/ControlledInput.js +4 -4
- package/dist/components/Form/ControlledInput.js.map +1 -1
- package/dist/components/Form/ControlledSelect.js +1 -1
- package/dist/components/Form/ControlledSelect.js.map +1 -1
- package/dist/components/Form/ControlledTabs.js +17 -3
- package/dist/components/Form/ControlledTabs.js.map +1 -1
- package/dist/components/Form/ControlledTabsPrimitives.js +6 -6
- package/dist/components/Form/ControlledTabsPrimitives.js.map +1 -1
- package/dist/components/Form/ControlledTextarea.js +2 -2
- package/dist/components/Form/ControlledTextarea.js.map +1 -1
- package/dist/components/Form/ControlledTimeline.js +11 -8
- package/dist/components/Form/ControlledTimeline.js.map +1 -1
- package/dist/components/Form/ControlledVoiceTextarea.d.ts +2 -0
- package/dist/components/Form/ControlledVoiceTextarea.js +3 -0
- package/dist/components/Form/ControlledVoiceTextarea.js.map +1 -1
- package/dist/components/Form/FieldLabel.d.ts +16 -0
- package/dist/components/Form/FieldLabel.js +7 -0
- package/dist/components/Form/FieldLabel.js.map +1 -0
- package/dist/components/Form/RichTextEditor.js +15 -1
- package/dist/components/Form/RichTextEditor.js.map +1 -1
- package/dist/components/Form/attachments/AttachmentPreviewList.js +3 -0
- package/dist/components/Form/attachments/AttachmentPreviewList.js.map +1 -1
- package/dist/components/Form/attachments/FileDropzone.js +3 -3
- package/dist/components/Form/attachments/FileDropzone.js.map +1 -1
- package/dist/components/Navigation/PageTitle.d.ts +6 -0
- package/dist/components/Navigation/PageTitle.js +6 -4
- package/dist/components/Navigation/PageTitle.js.map +1 -1
- package/dist/components/Search/filters/FiltersPanel.js +1 -0
- package/dist/components/Search/filters/FiltersPanel.js.map +1 -1
- package/dist/components/Search/filters/FiltersSummaryTooltip.js +2 -2
- package/dist/components/Search/filters/FiltersSummaryTooltip.js.map +1 -1
- package/dist/components/Shell/LanguageToggle.js +1 -1
- package/dist/components/Shell/LanguageToggle.js.map +1 -1
- package/dist/components/Shell/SidebarMenu.js +9 -1
- package/dist/components/Shell/SidebarMenu.js.map +1 -1
- package/dist/components/Shell/ThemeToggle.js +2 -1
- package/dist/components/Shell/ThemeToggle.js.map +1 -1
- package/dist/components/UI/BaseDataCard.d.ts +2 -0
- package/dist/components/UI/BaseDataCard.js +6 -0
- package/dist/components/UI/BaseDataCard.js.map +1 -1
- package/dist/components/UI/BulkActionBar.js +2 -0
- package/dist/components/UI/BulkActionBar.js.map +1 -1
- package/dist/components/UI/Chips/OutlinedChip.js +13 -6
- package/dist/components/UI/Chips/OutlinedChip.js.map +1 -1
- package/dist/components/UI/ColorSwatch.js +2 -2
- package/dist/components/UI/ColorSwatch.js.map +1 -1
- package/dist/components/UI/ControlledAccordion.js +10 -4
- package/dist/components/UI/ControlledAccordion.js.map +1 -1
- package/dist/components/UI/ControlledCollapse.js +2 -2
- package/dist/components/UI/ControlledCollapse.js.map +1 -1
- package/dist/components/UI/ControlledMenu.js +3 -3
- package/dist/components/UI/ControlledMenu.js.map +1 -1
- package/dist/components/UI/ControlledPopover.d.ts +12 -0
- package/dist/components/UI/ControlledPopover.js +35 -5
- package/dist/components/UI/ControlledPopover.js.map +1 -1
- package/dist/components/UI/ControlledProgress.js +3 -3
- package/dist/components/UI/ControlledProgress.js.map +1 -1
- package/dist/components/UI/ControlledSlider.d.ts +2 -0
- package/dist/components/UI/ControlledSlider.js +8 -3
- package/dist/components/UI/ControlledSlider.js.map +1 -1
- package/dist/components/UI/ControlledTooltip.js +7 -3
- package/dist/components/UI/ControlledTooltip.js.map +1 -1
- package/dist/components/UI/DataTable/DataTable.js +1 -1
- package/dist/components/UI/DataTable/DataTableBody.js +4 -0
- package/dist/components/UI/DataTable/DataTableBody.js.map +1 -1
- package/dist/components/UI/DataTable/DataTableBulkEditDialog.js +4 -2
- package/dist/components/UI/DataTable/DataTableBulkEditDialog.js.map +1 -1
- package/dist/components/UI/DataTable/DataTableBulkEditRowsPicker.js +1 -1
- package/dist/components/UI/DataTable/DataTableBulkEditValueEditor.d.ts +5 -1
- package/dist/components/UI/DataTable/DataTableBulkEditValueEditor.js +19 -4
- package/dist/components/UI/DataTable/DataTableBulkEditValueEditor.js.map +1 -1
- package/dist/components/UI/DataTable/DataTableColumnToggle.js +3 -1
- package/dist/components/UI/DataTable/DataTableColumnToggle.js.map +1 -1
- package/dist/components/UI/DataTable/DataTableFilters.js +1 -1
- package/dist/components/UI/DataTable/DataTablePagination.js +4 -0
- package/dist/components/UI/DataTable/DataTablePagination.js.map +1 -1
- package/dist/components/UI/DataTable/DataTableStates.js +2 -2
- package/dist/components/UI/DataTable/DataTableStates.js.map +1 -1
- package/dist/components/UI/DataTable/DataTableToolbar.js +1 -1
- package/dist/components/UI/DataTable/applyDataTableFilters.js +1 -1
- package/dist/components/UI/DataTable/cells/DataTableEditableCell.js +2 -1
- package/dist/components/UI/DataTable/cells/DataTableEditableCell.js.map +1 -1
- package/dist/components/UI/DataTable/types.d.ts +4 -0
- package/dist/components/UI/DataTable/types.js.map +1 -1
- package/dist/components/UI/Flow/FlowBlockNode.js +1 -0
- package/dist/components/UI/Flow/FlowBlockNode.js.map +1 -1
- package/dist/components/UI/Flow/FlowCanvas.js +2 -2
- package/dist/components/UI/Flow/FlowCanvas.js.map +1 -1
- package/dist/components/UI/Flow/FlowConnectionEdge.js +1 -0
- package/dist/components/UI/Flow/FlowConnectionEdge.js.map +1 -1
- package/dist/components/UI/Flow/FlowInspector.js +3 -2
- package/dist/components/UI/Flow/FlowInspector.js.map +1 -1
- package/dist/components/UI/Flow/FlowIssueList.js +2 -2
- package/dist/components/UI/Flow/FlowIssueList.js.map +1 -1
- package/dist/components/UI/Flow/FlowLaneMap.js +2 -2
- package/dist/components/UI/Flow/FlowLaneMap.js.map +1 -1
- package/dist/components/UI/Flow/FlowResizablePanel.js +3 -2
- package/dist/components/UI/Flow/FlowResizablePanel.js.map +1 -1
- package/dist/components/UI/Flow/FlowZoomControls.js +1 -0
- package/dist/components/UI/Flow/FlowZoomControls.js.map +1 -1
- package/dist/components/UI/HorizontalScrollArea.d.ts +1 -1
- package/dist/components/UI/HorizontalScrollArea.js +1 -0
- package/dist/components/UI/HorizontalScrollArea.js.map +1 -1
- package/dist/components/UI/ImageAnnotator/components/AnnotationColorPopover.js +1 -0
- package/dist/components/UI/ImageAnnotator/components/AnnotationColorPopover.js.map +1 -1
- package/dist/components/UI/ImagePlaceholder.js +5 -2
- package/dist/components/UI/ImagePlaceholder.js.map +1 -1
- package/dist/components/UI/ImageZoomOverlay.js +1 -0
- package/dist/components/UI/ImageZoomOverlay.js.map +1 -1
- package/dist/components/UI/MaterialSymbol/index.js +5 -0
- package/dist/components/UI/MaterialSymbol/index.js.map +1 -1
- package/dist/components/UI/NoDataFound/index.js +3 -3
- package/dist/components/UI/NoDataFound/index.js.map +1 -1
- package/dist/components/UI/RichTextContent.js +3 -3
- package/dist/components/UI/RichTextContent.js.map +1 -1
- package/dist/components/UI/ThreeDots.js +15 -11
- package/dist/components/UI/ThreeDots.js.map +1 -1
- package/dist/helpers/mergeClasses.d.ts +7 -3
- package/dist/helpers/mergeClasses.js +23 -1
- package/dist/helpers/mergeClasses.js.map +1 -1
- package/dist/hooks/useVoiceTranscription.js +5 -2
- package/dist/hooks/useVoiceTranscription.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +58 -54
- package/dist/index.js.map +1 -1
- package/dist/locales/en/ui.json.d.ts +8 -3
- package/dist/locales/en/ui.json.js +3 -3
- package/dist/locales/index.d.ts +10 -0
- package/dist/locales/pt/ui.json.d.ts +8 -3
- package/dist/locales/pt/ui.json.js +3 -3
- package/package.json +1 -1
- package/src/assets/styles/base.css +7 -1
- package/tailwind-plugins/glass.js +23 -28
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OutlinedChip.js","sources":["../../../../src/components/UI/Chips/OutlinedChip.tsx"],"sourcesContent":["import React, { useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport {\n ControlledMenuHandler,\n ControlledMenuItem,\n ControlledMenuList,\n ControlledMenuRoot,\n} from '@/components/UI/ControlledMenu';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport { capitalizeAllLetters } from '@/helpers/stringHelpers';\n\nimport { IFilterOption } from '../../../interfaces/IFilterOption';\nimport { MaterialSymbol, SymbolCodepoints } from '../../UI/MaterialSymbol';\n\nimport { resolveChipDotClass, resolveChipToneClass, TChipSize, TChipTone, TChipVariant } from './chipTones';\n\nconst CHIP_PADDING_CLASS: Record<TChipSize, string> = {\n xs: 'px-2.5 py-0.5',\n sm: 'px-2.5 py-0.5',\n md: 'px-3 py-1',\n};\n\nconst CHIP_LABEL_SIZE_CLASS: Record<TChipSize, string> = {\n xs: 'text-xxs',\n sm: 'text-xs',\n md: 'text-sm',\n};\n\nexport interface IOutlinedChipDropdownOption extends IFilterOption {\n /** Cabeçalho de grupo não selecionável, renderizado acima dos filhos recuados. */\n isGroupHeading?: boolean;\n /** Recuo de filho sob o cabeçalho do grupo. */\n isGroupChild?: boolean;\n}\n\ninterface IOutlinedChipProps {\n label?: string;\n count?: number;\n tone?: TChipTone;\n variant?: TChipVariant;\n size?: TChipSize;\n dot?: boolean;\n dotColor?: string;\n className?: string;\n wrapperClassName?: string;\n labelClassName?: string;\n // Por padrão a label passa por capitalizeAllLetters. Use true quando o texto for um\n // identificador literal (nome de tabela, coluna, chave) cujo caixa não pode ser alterado.\n preserveLabelCase?: boolean;\n truncate?: boolean;\n backgroundColor?: string;\n chipIcon?: SymbolCodepoints;\n chipIconSize?: number;\n trailingIcon?: SymbolCodepoints;\n loading?: boolean;\n tooltip?: string;\n dotClassName?: string;\n onClick?: (event: React.MouseEvent) => void;\n disabled?: boolean;\n useAsDropdown?: boolean;\n dropdownOptions?: Array<IOutlinedChipDropdownOption>;\n dropdownCallback?: (option: IFilterOption) => void;\n dropdownSelectedValue?: string;\n dropdownMenuClassName?: string;\n canClose?: boolean;\n onClose?: (event: React.MouseEvent) => void;\n}\n\nconst OutlinedChip: React.FC<IOutlinedChipProps> = ({\n label,\n count,\n tone,\n variant = 'soft',\n size = 'xs',\n dot = false,\n dotColor,\n className,\n wrapperClassName,\n labelClassName,\n preserveLabelCase = false,\n truncate = false,\n backgroundColor,\n chipIcon,\n chipIconSize = 16,\n trailingIcon,\n loading = false,\n tooltip,\n dotClassName,\n onClick,\n disabled = false,\n useAsDropdown = false,\n dropdownOptions = [],\n dropdownCallback,\n dropdownSelectedValue,\n dropdownMenuClassName,\n canClose = false,\n onClose,\n}) => {\n const { t } = useTranslation('ui');\n const [open, setOpen] = useState(false);\n\n const hasCount = count !== undefined && count !== null;\n if (!label && !hasCount) return null;\n\n const isCountOnly = hasCount && !label;\n const displayLabel = isCountOnly ? String(count) : preserveLabelCase ? label : capitalizeAllLetters(label ?? '');\n\n const toneClass = tone ? resolveChipToneClass(tone, variant) : undefined;\n const resolvedDotClass = dotClassName ?? (dot && tone ? resolveChipDotClass(tone) : undefined);\n const inlineStyle = backgroundColor ? { backgroundColor } : undefined;\n\n const leadingDot =\n resolvedDotClass || dotColor ? (\n <span\n className={classNames('size-1.5 shrink-0 rounded-full', resolvedDotClass)}\n style={dotColor ? { backgroundColor: dotColor } : undefined}\n />\n ) : null;\n\n const leadingIcon = loading ? (\n <LoadingSpinner size={4} className=\"shrink-0\" />\n ) : chipIcon ? (\n <MaterialSymbol icon={chipIcon} size={chipIconSize} />\n ) : null;\n\n if (useAsDropdown && !disabled) {\n const handler = (\n <ControlledMenuHandler>\n <div\n onClick={(event: React.MouseEvent) => event.stopPropagation()}\n className={twMerge(\n classNames(\n 'flex cursor-pointer items-center justify-center gap-x-1 truncate rounded-full font-semibold transition-opacity hover:opacity-80',\n CHIP_PADDING_CLASS[size],\n CHIP_LABEL_SIZE_CLASS[size],\n toneClass,\n ),\n className,\n )}\n style={inlineStyle}\n >\n {leadingDot}\n {leadingIcon}\n {displayLabel}\n <MaterialSymbol icon=\"arrow_drop_down\" size={16} />\n </div>\n </ControlledMenuHandler>\n );\n\n return (\n <ControlledMenuRoot open={open} handler={setOpen} placement=\"bottom-start\">\n {tooltip ? <ControlledTooltip content={tooltip}>{handler}</ControlledTooltip> : handler}\n\n <ControlledMenuList\n className={twMerge('zIndexMax max-h-72 min-w-[12rem] overflow-y-auto', dropdownMenuClassName)}\n onClick={(event: React.MouseEvent<HTMLDivElement>) => {\n event.stopPropagation();\n }}\n >\n {(dropdownOptions ?? []).map((option) => {\n if (option.isGroupHeading) {\n return (\n <div\n key={option.value}\n className=\"select-none px-3 pb-1 pt-2 text-xs font-semibold text-foreground-muted\"\n >\n {preserveLabelCase ? option.label : capitalizeAllLetters(option.label)}\n </div>\n );\n }\n\n const menuItem = (\n <ControlledMenuItem\n disabled={option.disabled}\n onClick={() => {\n dropdownCallback?.(option);\n setOpen(false);\n }}\n className={classNames('text-sm text-foreground', {\n 'bg-muted-active font-semibold hover:bg-muted-active focus:bg-muted-active':\n dropdownSelectedValue === option.value,\n 'pl-7': option.isGroupChild,\n })}\n >\n {preserveLabelCase ? option.label : capitalizeAllLetters(option.label)}\n </ControlledMenuItem>\n );\n\n // A tooltip explains WHY an option is disabled; the wrapper div exists because the\n // menu item does not forward its ref (the tooltip clones its child).\n return option.tooltip ? (\n <ControlledTooltip key={option.value} content={option.tooltip} className=\"z-[9999] max-w-64\">\n <div>{menuItem}</div>\n </ControlledTooltip>\n ) : (\n <React.Fragment key={option.value}>{menuItem}</React.Fragment>\n );\n })}\n </ControlledMenuList>\n </ControlledMenuRoot>\n );\n }\n\n const containerClass = twMerge(\n classNames(\n 'flex items-center gap-x-1 rounded-full text-center',\n CHIP_PADDING_CLASS[size],\n isCountOnly ? 'min-w-5 justify-center' : 'min-w-14',\n toneClass,\n {\n 'justify-between': canClose && onClose,\n 'justify-center': !canClose && !onClose && !isCountOnly,\n 'cursor-default opacity-70': disabled,\n 'transition-opacity hover:opacity-80': onClick && !disabled,\n },\n ),\n className,\n );\n\n const chipInner = (\n <>\n {leadingDot}\n {leadingIcon}\n <span className={twMerge(classNames('font-semibold', CHIP_LABEL_SIZE_CLASS[size], { truncate }), labelClassName)}>\n {displayLabel}\n </span>\n {trailingIcon && <MaterialSymbol icon={trailingIcon} size={chipIconSize} />}\n {canClose && onClose && (\n <ControlledTooltip content={t('actions.remove')}>\n <MaterialSymbol\n icon=\"close\"\n size={16}\n onClick={(event: React.MouseEvent) => {\n event.stopPropagation();\n onClose(event);\n }}\n />\n </ControlledTooltip>\n )}\n </>\n );\n\n const chip = onClick ? (\n <ControlledButton\n defaultHtmlButton\n className={containerClass}\n style={inlineStyle}\n onClick={onClick}\n disabled={disabled}\n >\n {chipInner}\n </ControlledButton>\n ) : (\n <div className={containerClass} style={inlineStyle}>\n {chipInner}\n </div>\n );\n\n if (!tooltip) return chip;\n\n return (\n <ControlledTooltip content={tooltip}>\n <div className={twMerge('inline-flex', wrapperClassName)}>{chip}</div>\n </ControlledTooltip>\n );\n};\n\nexport default OutlinedChip;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OutlinedChip.js","sources":["../../../../src/components/UI/Chips/OutlinedChip.tsx"],"sourcesContent":["import React, { useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport {\n ControlledMenuHandler,\n ControlledMenuItem,\n ControlledMenuList,\n ControlledMenuRoot,\n} from '@/components/UI/ControlledMenu';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport { mergeClasses } from '@/helpers/mergeClasses';\nimport { capitalizeAllLetters } from '@/helpers/stringHelpers';\n\nimport { IFilterOption } from '../../../interfaces/IFilterOption';\nimport { MaterialSymbol, SymbolCodepoints } from '../../UI/MaterialSymbol';\n\nimport { resolveChipDotClass, resolveChipToneClass, TChipSize, TChipTone, TChipVariant } from './chipTones';\n\nconst CHIP_PADDING_CLASS: Record<TChipSize, string> = {\n xs: 'px-2.5 py-0.5',\n sm: 'px-2.5 py-0.5',\n md: 'px-3 py-1',\n};\n\nconst CHIP_LABEL_SIZE_CLASS: Record<TChipSize, string> = {\n xs: 'text-xxs',\n sm: 'text-xs',\n md: 'text-sm',\n};\n\nexport interface IOutlinedChipDropdownOption extends IFilterOption {\n /** Cabeçalho de grupo não selecionável, renderizado acima dos filhos recuados. */\n isGroupHeading?: boolean;\n /** Recuo de filho sob o cabeçalho do grupo. */\n isGroupChild?: boolean;\n}\n\ninterface IOutlinedChipProps {\n label?: string;\n count?: number;\n tone?: TChipTone;\n variant?: TChipVariant;\n size?: TChipSize;\n dot?: boolean;\n dotColor?: string;\n className?: string;\n wrapperClassName?: string;\n labelClassName?: string;\n // Por padrão a label passa por capitalizeAllLetters. Use true quando o texto for um\n // identificador literal (nome de tabela, coluna, chave) cujo caixa não pode ser alterado.\n preserveLabelCase?: boolean;\n truncate?: boolean;\n backgroundColor?: string;\n chipIcon?: SymbolCodepoints;\n chipIconSize?: number;\n trailingIcon?: SymbolCodepoints;\n loading?: boolean;\n tooltip?: string;\n dotClassName?: string;\n onClick?: (event: React.MouseEvent) => void;\n disabled?: boolean;\n useAsDropdown?: boolean;\n dropdownOptions?: Array<IOutlinedChipDropdownOption>;\n dropdownCallback?: (option: IFilterOption) => void;\n dropdownSelectedValue?: string;\n dropdownMenuClassName?: string;\n canClose?: boolean;\n onClose?: (event: React.MouseEvent) => void;\n}\n\nconst OutlinedChip: React.FC<IOutlinedChipProps> = ({\n label,\n count,\n tone,\n variant = 'soft',\n size = 'xs',\n dot = false,\n dotColor,\n className,\n wrapperClassName,\n labelClassName,\n preserveLabelCase = false,\n truncate = false,\n backgroundColor,\n chipIcon,\n chipIconSize = 16,\n trailingIcon,\n loading = false,\n tooltip,\n dotClassName,\n onClick,\n disabled = false,\n useAsDropdown = false,\n dropdownOptions = [],\n dropdownCallback,\n dropdownSelectedValue,\n dropdownMenuClassName,\n canClose = false,\n onClose,\n}) => {\n const { t } = useTranslation('ui');\n const [open, setOpen] = useState(false);\n\n const hasCount = count !== undefined && count !== null;\n if (!label && !hasCount) return null;\n\n const isCountOnly = hasCount && !label;\n const displayLabel = isCountOnly ? String(count) : preserveLabelCase ? label : capitalizeAllLetters(label ?? '');\n\n const toneClass = tone ? resolveChipToneClass(tone, variant) : undefined;\n const resolvedDotClass = dotClassName ?? (dot && tone ? resolveChipDotClass(tone) : undefined);\n const inlineStyle = backgroundColor ? { backgroundColor } : undefined;\n\n const leadingDot =\n resolvedDotClass || dotColor ? (\n <span\n className={classNames('size-1.5 shrink-0 rounded-full', resolvedDotClass)}\n style={dotColor ? { backgroundColor: dotColor } : undefined}\n />\n ) : null;\n\n const leadingIcon = loading ? (\n <LoadingSpinner size={4} className=\"shrink-0\" />\n ) : chipIcon ? (\n <MaterialSymbol icon={chipIcon} size={chipIconSize} />\n ) : null;\n\n if (useAsDropdown && !disabled) {\n const handler = (\n <ControlledMenuHandler>\n <div\n onClick={(event: React.MouseEvent) => event.stopPropagation()}\n className={mergeClasses(\n classNames(\n 'flex cursor-pointer items-center justify-center gap-x-1 truncate rounded-full font-semibold transition-opacity hover:opacity-80',\n CHIP_PADDING_CLASS[size],\n CHIP_LABEL_SIZE_CLASS[size],\n toneClass,\n ),\n className,\n )}\n style={inlineStyle}\n >\n {leadingDot}\n {leadingIcon}\n {displayLabel}\n <MaterialSymbol icon=\"arrow_drop_down\" size={16} />\n </div>\n </ControlledMenuHandler>\n );\n\n return (\n <ControlledMenuRoot open={open} handler={setOpen} placement=\"bottom-start\">\n {tooltip ? <ControlledTooltip content={tooltip}>{handler}</ControlledTooltip> : handler}\n\n <ControlledMenuList\n className={mergeClasses('zIndexMax max-h-72 min-w-[12rem] overflow-y-auto', dropdownMenuClassName)}\n onClick={(event: React.MouseEvent<HTMLDivElement>) => {\n event.stopPropagation();\n }}\n >\n {(dropdownOptions ?? []).map((option) => {\n if (option.isGroupHeading) {\n return (\n <div\n key={option.value}\n className=\"select-none px-3 pb-1 pt-2 text-xs font-semibold text-foreground-muted\"\n >\n {preserveLabelCase ? option.label : capitalizeAllLetters(option.label)}\n </div>\n );\n }\n\n const menuItem = (\n <ControlledMenuItem\n disabled={option.disabled}\n onClick={() => {\n dropdownCallback?.(option);\n setOpen(false);\n }}\n className={classNames('text-sm text-foreground', {\n 'bg-muted-active font-semibold hover:bg-muted-active focus:bg-muted-active':\n dropdownSelectedValue === option.value,\n 'pl-7': option.isGroupChild,\n })}\n >\n {preserveLabelCase ? option.label : capitalizeAllLetters(option.label)}\n </ControlledMenuItem>\n );\n\n // A tooltip explains WHY an option is disabled; the wrapper div exists because the\n // menu item does not forward its ref (the tooltip clones its child).\n return option.tooltip ? (\n <ControlledTooltip key={option.value} content={option.tooltip} className=\"z-[9999] max-w-64\">\n <div>{menuItem}</div>\n </ControlledTooltip>\n ) : (\n <React.Fragment key={option.value}>{menuItem}</React.Fragment>\n );\n })}\n </ControlledMenuList>\n </ControlledMenuRoot>\n );\n }\n\n const containerClass = mergeClasses(\n classNames(\n 'flex items-center gap-x-1 rounded-full text-center',\n CHIP_PADDING_CLASS[size],\n isCountOnly ? 'min-w-5 justify-center' : 'min-w-14',\n toneClass,\n {\n 'justify-between': canClose && onClose,\n 'justify-center': !canClose && !onClose && !isCountOnly,\n 'cursor-default opacity-70': disabled,\n 'transition-opacity hover:opacity-80': onClick && !disabled,\n },\n ),\n className,\n );\n\n const chipInner = (\n <>\n {leadingDot}\n {leadingIcon}\n <span\n className={mergeClasses(classNames('font-semibold', CHIP_LABEL_SIZE_CLASS[size], { truncate }), labelClassName)}\n >\n {displayLabel}\n </span>\n {trailingIcon && <MaterialSymbol icon={trailingIcon} size={chipIconSize} />}\n {canClose && onClose && (\n <ControlledTooltip content={t('actions.remove')}>\n <MaterialSymbol\n icon=\"close\"\n size={16}\n aria-label={t('actions.remove')}\n onClick={(event: React.MouseEvent) => {\n event.stopPropagation();\n onClose(event);\n }}\n />\n </ControlledTooltip>\n )}\n </>\n );\n\n const chip = onClick ? (\n <ControlledButton\n defaultHtmlButton\n className={containerClass}\n style={inlineStyle}\n onClick={onClick}\n disabled={disabled}\n >\n {chipInner}\n </ControlledButton>\n ) : (\n <div className={containerClass} style={inlineStyle}>\n {chipInner}\n </div>\n );\n\n if (!tooltip) return chip;\n\n return (\n <ControlledTooltip content={tooltip}>\n <div className={mergeClasses('inline-flex', wrapperClassName)}>{chip}</div>\n </ControlledTooltip>\n );\n};\n\nexport default OutlinedChip;\n"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import classNames from "classnames";
|
|
3
3
|
import { useTranslation } from "react-i18next";
|
|
4
|
-
import { twMerge } from "tailwind-merge";
|
|
5
4
|
import ControlledTooltip from "./ControlledTooltip.js";
|
|
5
|
+
import { mergeClasses } from "../../helpers/mergeClasses.js";
|
|
6
6
|
const SIZE_CLASS = {
|
|
7
7
|
xs: "size-2",
|
|
8
8
|
sm: "size-3",
|
|
@@ -26,7 +26,7 @@ const ColorSwatch = ({
|
|
|
26
26
|
const dot = /* @__PURE__ */ jsx(
|
|
27
27
|
"span",
|
|
28
28
|
{
|
|
29
|
-
className:
|
|
29
|
+
className: mergeClasses(
|
|
30
30
|
classNames(
|
|
31
31
|
"inline-block shrink-0 rounded-full border border-border",
|
|
32
32
|
sizePx === void 0 && SIZE_CLASS[size],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ColorSwatch.js","sources":["../../../src/components/UI/ColorSwatch.tsx"],"sourcesContent":["import classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\nimport
|
|
1
|
+
{"version":3,"file":"ColorSwatch.js","sources":["../../../src/components/UI/ColorSwatch.tsx"],"sourcesContent":["import classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { mergeClasses } from '@/helpers/mergeClasses';\n\nexport type TColorSwatchSize = 'xs' | 'sm' | 'md' | 'lg';\n\nconst SIZE_CLASS: Record<TColorSwatchSize, string> = {\n xs: 'size-2',\n sm: 'size-3',\n md: 'size-4',\n lg: 'size-5',\n};\n\ninterface IColorSwatchProps {\n /** Cor CSS já resolvida. Ausente é estado válido: a biblioteca aceita cor sem hex e sem RGB. */\n cssColor?: string | null;\n name?: string | null;\n size?: TColorSwatchSize;\n /** Substitui o tamanho do preset quando a medida é calculada (zoom do canvas, por exemplo). */\n sizePx?: number;\n /** Mostra o nome ao lado da bolinha, além do tooltip. */\n showName?: boolean;\n /** Área de hover invisível sobre uma bolinha já desenhada em outra camada. */\n hitAreaOnly?: boolean;\n /**\n * Cada tooltip monta um floating-ui próprio. Numa lista longa (satélites do board, fileiras de\n * cores), desligue e deixe UM tooltip no container: o custo cai de N instâncias para uma.\n * Já vem desligado com `showName`, onde o nome está visível e o tooltip só repetiria.\n */\n tooltip?: boolean;\n className?: string;\n nameClassName?: string;\n}\n\n/**\n * A ÚNICA bolinha de cor da plataforma.\n *\n * Cor sem código cadastrado não some nem vira um buraco na interface: vira cinza opaco e o nome\n * carrega a identidade. Foi a divergência entre \"esconde\" e \"pinta de cinza\" que fez uma cor sem hex\n * parecer bug de renderização.\n */\nconst ColorSwatch: React.FC<IColorSwatchProps> = ({\n cssColor,\n name,\n size = 'sm',\n sizePx,\n showName = false,\n hitAreaOnly = false,\n tooltip,\n className,\n nameClassName,\n}) => {\n const { t } = useTranslation('ui');\n\n const resolvedName = name?.trim();\n const tooltipContent = [resolvedName, cssColor ? undefined : t('colorSwatch.noCode')].filter(Boolean).join(' · ');\n\n const dot = (\n <span\n className={mergeClasses(\n classNames(\n 'inline-block shrink-0 rounded-full border border-border',\n sizePx === undefined && SIZE_CLASS[size],\n hitAreaOnly && 'cursor-help border-transparent bg-transparent',\n ),\n className,\n )}\n style={{\n ...(sizePx === undefined ? undefined : { width: sizePx, height: sizePx }),\n ...(hitAreaOnly ? undefined : { backgroundColor: cssColor ?? 'rgb(var(--color-muted-active))' }),\n }}\n />\n );\n\n const content = showName ? (\n <span className=\"flex min-w-0 items-center gap-1\">\n {dot}\n <span className={classNames('truncate', nameClassName)}>{resolvedName}</span>\n </span>\n ) : (\n dot\n );\n\n if (!tooltipContent || !(tooltip ?? !showName)) return content;\n\n return <ControlledTooltip content={tooltipContent}>{content}</ControlledTooltip>;\n};\n\nexport default ColorSwatch;\n"],"names":[],"mappings":";;;;;AAQA,MAAM,aAA+C;AAAA,EACnD,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AA8BA,MAAM,cAA2C,CAAC;AAAA,EAChD;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,WAAW;AAAA,EACX,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AAEjC,QAAM,eAAe,MAAM,KAAA;AAC3B,QAAM,iBAAiB,CAAC,cAAc,WAAW,SAAY,EAAE,oBAAoB,CAAC,EAAE,OAAO,OAAO,EAAE,KAAK,KAAK;AAEhH,QAAM,MACJ;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,UACE;AAAA,UACA,WAAW,UAAa,WAAW,IAAI;AAAA,UACvC,eAAe;AAAA,QAAA;AAAA,QAEjB;AAAA,MAAA;AAAA,MAEF,OAAO;AAAA,QACL,GAAI,WAAW,SAAY,SAAY,EAAE,OAAO,QAAQ,QAAQ,OAAA;AAAA,QAChE,GAAI,cAAc,SAAY,EAAE,iBAAiB,YAAY,iCAAA;AAAA,MAAiC;AAAA,IAChG;AAAA,EAAA;AAIJ,QAAM,UAAU,WACd,qBAAC,QAAA,EAAK,WAAU,mCACb,UAAA;AAAA,IAAA;AAAA,wBACA,QAAA,EAAK,WAAW,WAAW,YAAY,aAAa,GAAI,UAAA,aAAA,CAAa;AAAA,EAAA,EAAA,CACxE,IAEA;AAGF,MAAI,CAAC,kBAAkB,EAAE,WAAW,CAAC,UAAW,QAAO;AAEvD,SAAO,oBAAC,mBAAA,EAAkB,SAAS,gBAAiB,UAAA,SAAQ;AAC9D;"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useContext } from "react";
|
|
3
3
|
import classNames from "classnames";
|
|
4
|
-
import {
|
|
4
|
+
import { mergeClasses } from "../../helpers/mergeClasses.js";
|
|
5
5
|
import ControlledCollapse from "./ControlledCollapse.js";
|
|
6
6
|
import { MaterialSymbol } from "./MaterialSymbol/index.js";
|
|
7
7
|
const AccordionContext = createContext({ open: false });
|
|
@@ -14,7 +14,7 @@ const ControlledAccordionRoot = ({
|
|
|
14
14
|
}) => /* @__PURE__ */ jsx(AccordionContext.Provider, { value: { open, icon, disabled }, children: /* @__PURE__ */ jsx(
|
|
15
15
|
"div",
|
|
16
16
|
{
|
|
17
|
-
className:
|
|
17
|
+
className: mergeClasses(
|
|
18
18
|
classNames("relative block w-full", { "pointer-events-none opacity-50": disabled }),
|
|
19
19
|
className
|
|
20
20
|
),
|
|
@@ -43,7 +43,7 @@ const ControlledAccordionHeader = ({
|
|
|
43
43
|
"aria-expanded": isInteractive ? open : void 0,
|
|
44
44
|
onClick,
|
|
45
45
|
onKeyDown: handleKeyDown,
|
|
46
|
-
className:
|
|
46
|
+
className: mergeClasses(
|
|
47
47
|
"flex w-full select-none items-center justify-between border-b border-border-subtle py-4 text-left",
|
|
48
48
|
"text-xl font-semibold leading-snug text-foreground-muted transition-colors hover:text-foreground",
|
|
49
49
|
classNames({ "text-foreground": open }),
|
|
@@ -65,7 +65,13 @@ const ControlledAccordionHeader = ({
|
|
|
65
65
|
};
|
|
66
66
|
const ControlledAccordionBody = ({ children, className }) => {
|
|
67
67
|
const { open } = useContext(AccordionContext);
|
|
68
|
-
return /* @__PURE__ */ jsx(ControlledCollapse, { open, children: /* @__PURE__ */ jsx(
|
|
68
|
+
return /* @__PURE__ */ jsx(ControlledCollapse, { open, children: /* @__PURE__ */ jsx(
|
|
69
|
+
"div",
|
|
70
|
+
{
|
|
71
|
+
className: mergeClasses("block w-full py-4 text-sm font-light leading-normal text-foreground-muted", className),
|
|
72
|
+
children
|
|
73
|
+
}
|
|
74
|
+
) });
|
|
69
75
|
};
|
|
70
76
|
const ControlledAccordion = ({
|
|
71
77
|
open,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControlledAccordion.js","sources":["../../../src/components/UI/ControlledAccordion.tsx"],"sourcesContent":["import React, { createContext, useContext } from 'react';\nimport classNames from 'classnames';\nimport {
|
|
1
|
+
{"version":3,"file":"ControlledAccordion.js","sources":["../../../src/components/UI/ControlledAccordion.tsx"],"sourcesContent":["import React, { createContext, useContext } from 'react';\nimport classNames from 'classnames';\n\nimport { mergeClasses } from '@/helpers/mergeClasses';\n\nimport ControlledCollapse from './ControlledCollapse';\nimport { MaterialSymbol } from './MaterialSymbol';\n\ninterface IAccordionContext {\n open: boolean;\n icon?: React.ReactNode;\n disabled?: boolean;\n}\n\nconst AccordionContext = createContext<IAccordionContext>({ open: false });\n\ninterface IControlledAccordionRootProps {\n open: boolean;\n children: React.ReactNode;\n icon?: React.ReactNode;\n disabled?: boolean;\n className?: string;\n}\n\nexport const ControlledAccordionRoot: React.FC<IControlledAccordionRootProps> = ({\n open,\n children,\n icon,\n disabled = false,\n className,\n}) => (\n <AccordionContext.Provider value={{ open, icon, disabled }}>\n <div\n className={mergeClasses(\n classNames('relative block w-full', { 'pointer-events-none opacity-50': disabled }),\n className,\n )}\n >\n {children}\n </div>\n </AccordionContext.Provider>\n);\n\ninterface IControlledAccordionHeaderProps {\n onClick?: () => void;\n children: React.ReactNode;\n className?: string;\n tabIndex?: number;\n}\n\n/**\n * Rendered as a div with role=\"button\", not a native button: several headers in this repo\n * carry their own buttons (rename, visibility, drag handle), and a button inside a button\n * is invalid HTML that silently breaks the inner controls.\n */\nexport const ControlledAccordionHeader: React.FC<IControlledAccordionHeaderProps> = ({\n onClick,\n children,\n className,\n tabIndex,\n}) => {\n const { open, icon } = useContext(AccordionContext);\n const isInteractive = Boolean(onClick);\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key !== 'Enter' && event.key !== ' ') return;\n if (event.target !== event.currentTarget) return;\n event.preventDefault();\n onClick?.();\n };\n\n return (\n <div\n role={isInteractive ? 'button' : undefined}\n tabIndex={tabIndex ?? (isInteractive ? 0 : undefined)}\n aria-expanded={isInteractive ? open : undefined}\n onClick={onClick}\n onKeyDown={handleKeyDown}\n className={mergeClasses(\n 'flex w-full select-none items-center justify-between border-b border-border-subtle py-4 text-left',\n 'text-xl font-semibold leading-snug text-foreground-muted transition-colors hover:text-foreground',\n classNames({ 'text-foreground': open }),\n className,\n )}\n >\n {children}\n\n {icon ?? (\n <MaterialSymbol\n icon=\"expand_more\"\n size={20}\n className={classNames('ml-4 shrink-0 transition-transform duration-base', { 'rotate-180': open })}\n />\n )}\n </div>\n );\n};\n\ninterface IControlledAccordionBodyProps {\n children: React.ReactNode;\n className?: string;\n}\n\nexport const ControlledAccordionBody: React.FC<IControlledAccordionBodyProps> = ({ children, className }) => {\n const { open } = useContext(AccordionContext);\n\n return (\n <ControlledCollapse open={open}>\n <div\n className={mergeClasses('block w-full py-4 text-sm font-light leading-normal text-foreground-muted', className)}\n >\n {children}\n </div>\n </ControlledCollapse>\n );\n};\n\ninterface IControlledAccordionProps {\n open: boolean;\n onToggle: () => void;\n header: React.ReactNode;\n children: React.ReactNode;\n icon?: React.ReactNode;\n className?: string;\n headerClassName?: string;\n bodyClassName?: string;\n}\n\nconst ControlledAccordion: React.FC<IControlledAccordionProps> = ({\n open,\n onToggle,\n header,\n children,\n icon,\n className,\n headerClassName,\n bodyClassName,\n}) => (\n <ControlledAccordionRoot open={open} icon={icon} className={className}>\n <ControlledAccordionHeader onClick={onToggle} className={headerClassName}>\n {header}\n </ControlledAccordionHeader>\n <ControlledAccordionBody className={bodyClassName}>{children}</ControlledAccordionBody>\n </ControlledAccordionRoot>\n);\n\nexport default ControlledAccordion;\n"],"names":[],"mappings":";;;;;;AAcA,MAAM,mBAAmB,cAAiC,EAAE,MAAM,OAAO;AAUlE,MAAM,0BAAmE,CAAC;AAAA,EAC/E;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AACF,MACE,oBAAC,iBAAiB,UAAjB,EAA0B,OAAO,EAAE,MAAM,MAAM,YAC9C,UAAA;AAAA,EAAC;AAAA,EAAA;AAAA,IACC,WAAW;AAAA,MACT,WAAW,yBAAyB,EAAE,kCAAkC,UAAU;AAAA,MAClF;AAAA,IAAA;AAAA,IAGD;AAAA,EAAA;AACH,EAAA,CACF;AAeK,MAAM,4BAAuE,CAAC;AAAA,EACnF;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,MAAM,SAAS,WAAW,gBAAgB;AAClD,QAAM,gBAAgB,QAAQ,OAAO;AAErC,QAAM,gBAAgB,CAAC,UAA+C;AACpE,QAAI,MAAM,QAAQ,WAAW,MAAM,QAAQ,IAAK;AAChD,QAAI,MAAM,WAAW,MAAM,cAAe;AAC1C,UAAM,eAAA;AACN,cAAA;AAAA,EACF;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAM,gBAAgB,WAAW;AAAA,MACjC,UAAU,aAAa,gBAAgB,IAAI;AAAA,MAC3C,iBAAe,gBAAgB,OAAO;AAAA,MACtC;AAAA,MACA,WAAW;AAAA,MACX,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,WAAW,EAAE,mBAAmB,MAAM;AAAA,QACtC;AAAA,MAAA;AAAA,MAGD,UAAA;AAAA,QAAA;AAAA,QAEA,QACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,MAAM;AAAA,YACN,WAAW,WAAW,oDAAoD,EAAE,cAAc,MAAM;AAAA,UAAA;AAAA,QAAA;AAAA,MAClG;AAAA,IAAA;AAAA,EAAA;AAIR;AAOO,MAAM,0BAAmE,CAAC,EAAE,UAAU,gBAAgB;AAC3G,QAAM,EAAE,KAAA,IAAS,WAAW,gBAAgB;AAE5C,SACE,oBAAC,sBAAmB,MAClB,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAW,aAAa,6EAA6E,SAAS;AAAA,MAE7G;AAAA,IAAA;AAAA,EAAA,GAEL;AAEJ;AAaA,MAAM,sBAA2D,CAAC;AAAA,EAChE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MACE,qBAAC,yBAAA,EAAwB,MAAY,MAAY,WAC/C,UAAA;AAAA,EAAA,oBAAC,2BAAA,EAA0B,SAAS,UAAU,WAAW,iBACtD,UAAA,QACH;AAAA,EACA,oBAAC,yBAAA,EAAwB,WAAW,eAAgB,SAAA,CAAS;AAAA,EAAA,CAC/D;"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useRef, useState, useEffect } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { mergeClasses } from "../../helpers/mergeClasses.js";
|
|
4
4
|
const ControlledCollapse = ({ open, children, className }) => {
|
|
5
5
|
const wrapperRef = useRef(null);
|
|
6
6
|
const contentRef = useRef(null);
|
|
@@ -29,7 +29,7 @@ const ControlledCollapse = ({ open, children, className }) => {
|
|
|
29
29
|
ref: wrapperRef,
|
|
30
30
|
style: { height },
|
|
31
31
|
"aria-hidden": !open,
|
|
32
|
-
className:
|
|
32
|
+
className: mergeClasses("overflow-hidden transition-[height] duration-slow ease-enter", className),
|
|
33
33
|
children: /* @__PURE__ */ jsx("div", { ref: contentRef, children })
|
|
34
34
|
}
|
|
35
35
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControlledCollapse.js","sources":["../../../src/components/UI/ControlledCollapse.tsx"],"sourcesContent":["import React, { useEffect, useRef, useState } from 'react';\nimport {
|
|
1
|
+
{"version":3,"file":"ControlledCollapse.js","sources":["../../../src/components/UI/ControlledCollapse.tsx"],"sourcesContent":["import React, { useEffect, useRef, useState } from 'react';\n\nimport { mergeClasses } from '@/helpers/mergeClasses';\n\ninterface IControlledCollapseProps {\n open: boolean;\n children: React.ReactNode;\n className?: string;\n}\n\n/**\n * Height is measured from the content node and written to the wrapper, so the transition\n * always has a numeric start and end. The observer watches the CONTENT, whose height is\n * unconstrained, so updating the wrapper cannot feed back into it.\n *\n * A closed collapse is `inert`: `height: 0` with `overflow: hidden` still leaves whatever is\n * inside reachable by Tab, so without it a keyboard lands on controls nobody can see, and\n * `aria-hidden` around focusable content is itself an ARIA violation. Set through the DOM\n * because React 18 does not type the attribute.\n */\nconst ControlledCollapse: React.FC<IControlledCollapseProps> = ({ open, children, className }) => {\n const wrapperRef = useRef<HTMLDivElement>(null);\n const contentRef = useRef<HTMLDivElement>(null);\n const [height, setHeight] = useState(0);\n\n useEffect(() => {\n const wrapper = wrapperRef.current;\n if (!wrapper) return;\n if (open) wrapper.removeAttribute('inert');\n else wrapper.setAttribute('inert', '');\n }, [open]);\n\n useEffect(() => {\n const content = contentRef.current;\n if (!content) return undefined;\n\n if (!open) {\n setHeight(0);\n return undefined;\n }\n\n setHeight(content.scrollHeight);\n\n const observer = new ResizeObserver(() => setHeight(content.scrollHeight));\n observer.observe(content);\n\n return () => observer.disconnect();\n }, [open]);\n\n return (\n <div\n ref={wrapperRef}\n style={{ height }}\n aria-hidden={!open}\n className={mergeClasses('overflow-hidden transition-[height] duration-slow ease-enter', className)}\n >\n <div ref={contentRef}>{children}</div>\n </div>\n );\n};\n\nexport default ControlledCollapse;\n"],"names":[],"mappings":";;;AAoBA,MAAM,qBAAyD,CAAC,EAAE,MAAM,UAAU,gBAAgB;AAChG,QAAM,aAAa,OAAuB,IAAI;AAC9C,QAAM,aAAa,OAAuB,IAAI;AAC9C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,CAAC;AAEtC,YAAU,MAAM;AACd,UAAM,UAAU,WAAW;AAC3B,QAAI,CAAC,QAAS;AACd,QAAI,KAAM,SAAQ,gBAAgB,OAAO;AAAA,QACpC,SAAQ,aAAa,SAAS,EAAE;AAAA,EACvC,GAAG,CAAC,IAAI,CAAC;AAET,YAAU,MAAM;AACd,UAAM,UAAU,WAAW;AAC3B,QAAI,CAAC,QAAS,QAAO;AAErB,QAAI,CAAC,MAAM;AACT,gBAAU,CAAC;AACX,aAAO;AAAA,IACT;AAEA,cAAU,QAAQ,YAAY;AAE9B,UAAM,WAAW,IAAI,eAAe,MAAM,UAAU,QAAQ,YAAY,CAAC;AACzE,aAAS,QAAQ,OAAO;AAExB,WAAO,MAAM,SAAS,WAAA;AAAA,EACxB,GAAG,CAAC,IAAI,CAAC;AAET,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAK;AAAA,MACL,OAAO,EAAE,OAAA;AAAA,MACT,eAAa,CAAC;AAAA,MACd,WAAW,aAAa,gEAAgE,SAAS;AAAA,MAEjG,UAAA,oBAAC,OAAA,EAAI,KAAK,YAAa,SAAA,CAAS;AAAA,IAAA;AAAA,EAAA;AAGtC;"}
|
|
@@ -3,11 +3,11 @@ import React, { createContext, isValidElement, cloneElement, useEffect, useConte
|
|
|
3
3
|
import classNames from "classnames";
|
|
4
4
|
import { AnimatePresence, motion } from "motion/react";
|
|
5
5
|
import { createPortal } from "react-dom";
|
|
6
|
-
import { twMerge } from "tailwind-merge";
|
|
7
6
|
import ControlledTooltip from "./ControlledTooltip.js";
|
|
8
7
|
import { MaterialSymbol } from "./MaterialSymbol/index.js";
|
|
9
8
|
import { composeRefs } from "../../helpers/composeRefs.js";
|
|
10
9
|
import { LAYERED_SURFACE_ATTRIBUTE, isPressOnLayeredSurface } from "../../helpers/layeredSurfaces.js";
|
|
10
|
+
import { mergeClasses } from "../../helpers/mergeClasses.js";
|
|
11
11
|
import { getPortalHost } from "../../helpers/portalHost.js";
|
|
12
12
|
import { useAnchoredFloating } from "../../hooks/useAnchoredFloating.js";
|
|
13
13
|
import { floatingVariants } from "../../motion/motionPresets.js";
|
|
@@ -131,7 +131,7 @@ const ControlledMenuList = React.forwardRef(
|
|
|
131
131
|
if (target?.closest('[role="menuitem"]')) setOpen(false);
|
|
132
132
|
},
|
|
133
133
|
...rest,
|
|
134
|
-
className:
|
|
134
|
+
className: mergeClasses(
|
|
135
135
|
"fixed left-0 top-0 z-[9999] min-w-44 overflow-auto rounded-lg border border-border p-1 text-sm text-foreground glass",
|
|
136
136
|
className
|
|
137
137
|
),
|
|
@@ -164,7 +164,7 @@ const ControlledMenuItem = ({
|
|
|
164
164
|
event.currentTarget.click();
|
|
165
165
|
},
|
|
166
166
|
...rest,
|
|
167
|
-
className:
|
|
167
|
+
className: mergeClasses(
|
|
168
168
|
classNames(
|
|
169
169
|
"flex w-full cursor-pointer select-none items-center rounded-md px-3 py-2 text-sm outline-none",
|
|
170
170
|
"transition-colors hover:bg-muted-hover focus:bg-muted-hover",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControlledMenu.js","sources":["../../../src/components/UI/ControlledMenu.tsx"],"sourcesContent":["import React, {\n cloneElement,\n createContext,\n isValidElement,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useState,\n} from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol, SymbolCodepoints } from '@/components/UI/MaterialSymbol';\nimport { composeRefs } from '@/helpers/composeRefs';\nimport { isPressOnLayeredSurface, LAYERED_SURFACE_ATTRIBUTE } from '@/helpers/layeredSurfaces';\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { TFloatingPlacement, useAnchoredFloating } from '@/hooks/useAnchoredFloating';\nimport { floatingVariants } from '@/motion';\n\nexport type TMenuPlacement = TFloatingPlacement;\nexport type TMenuItemTone = 'default' | 'danger';\n\nexport interface IMenuDismissConfig {\n /** Closes the menu when an item is pressed. Multi-select surfaces pass false. */\n itemPress?: boolean;\n escapeKey?: boolean;\n outsidePress?: boolean;\n}\n\nexport interface IControlledMenuItem {\n key: string;\n label: string;\n icon?: SymbolCodepoints;\n hint?: string;\n onClick?: () => void;\n disabled?: boolean;\n tone?: TMenuItemTone;\n}\n\ninterface IMenuContext {\n open: boolean;\n setOpen: (open: boolean) => void;\n dismiss?: IMenuDismissConfig;\n listId: string;\n setReference: (node: HTMLElement | null) => void;\n setFloating: (node: HTMLElement | null) => void;\n floatingRef: React.MutableRefObject<HTMLElement | null>;\n referenceRef: React.MutableRefObject<HTMLElement | null>;\n}\n\nconst MenuContext = createContext<IMenuContext | null>(null);\n\nconst useMenuContext = (): IMenuContext => {\n const context = useContext(MenuContext);\n if (!context) throw new Error('Menu parts must be rendered inside ControlledMenuRoot');\n return context;\n};\n\ninterface IControlledMenuRootProps {\n children: React.ReactNode;\n open?: boolean;\n handler?: (open: boolean) => void;\n placement?: TMenuPlacement;\n offsetPx?: number;\n dismiss?: IMenuDismissConfig;\n matchTriggerWidth?: boolean;\n}\n\nexport const ControlledMenuRoot: React.FC<IControlledMenuRootProps> = ({\n children,\n open,\n handler,\n placement = 'bottom-start',\n offsetPx = 6,\n dismiss,\n matchTriggerWidth = false,\n}) => {\n const listId = useId();\n const [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\n const isControlled = open !== undefined;\n const isOpen = isControlled ? Boolean(open) : uncontrolledOpen;\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n handler?.(next);\n },\n [isControlled, handler],\n );\n\n const { setReference, setFloating, floatingRef, referenceRef } = useAnchoredFloating({\n open: isOpen,\n placement,\n offsetPx,\n matchReferenceWidth: matchTriggerWidth,\n });\n\n const value = useMemo(\n () => ({ open: isOpen, setOpen, dismiss, listId, setReference, setFloating, floatingRef, referenceRef }),\n [isOpen, setOpen, dismiss, listId, setReference, setFloating, floatingRef, referenceRef],\n );\n\n return <MenuContext.Provider value={value}>{children}</MenuContext.Provider>;\n};\n\ninterface IHandlerChildProps {\n onClick?: (event: React.MouseEvent<HTMLElement>) => void;\n ref?: React.Ref<HTMLElement>;\n}\n\n/**\n * Repassa ao filho o que ele recebe de fora, além do que injeta.\n *\n * O gatilho é o alvo natural de um `ControlledTooltip`, e o tooltip se prende clonando o filho e\n * injetando `onPointerEnter`. Aceitando só `children`, este componente engolia essa injeção e o\n * tooltip simplesmente nunca abria, mesmo com a ordem que a documentação manda usar\n * (tooltip POR FORA do handler). Os `onClick` são encadeados: o de fora roda antes de abrir.\n */\nexport const ControlledMenuHandler = React.forwardRef<\n HTMLElement,\n {\n children: React.ReactNode;\n /**\n * Leaves the popup ARIA to the child: set it when the real trigger is an element INSIDE the\n * child that owns its own role (the select's combobox input), so the wrapper does not\n * claim `aria-haspopup=\"menu\"` for a listbox.\n */\n skipTriggerAria?: boolean;\n } & React.HTMLAttributes<HTMLElement>\n>(({ children, onClick: onOuterClick, skipTriggerAria = false, ...rest }, forwardedRef) => {\n const { open, setOpen, setReference, listId } = useMenuContext();\n\n if (!isValidElement(children)) return <>{children}</>;\n\n const child = children as React.ReactElement<IHandlerChildProps>;\n const triggerAria = skipTriggerAria\n ? {}\n : { 'aria-haspopup': 'menu', 'aria-expanded': open, 'aria-controls': open ? listId : undefined };\n\n return cloneElement(child, {\n ...rest,\n ref: composeRefs<HTMLElement>(setReference, forwardedRef, (child as { ref?: React.Ref<HTMLElement> }).ref),\n ...triggerAria,\n onClick: (event: React.MouseEvent<HTMLElement>) => {\n onOuterClick?.(event);\n child.props.onClick?.(event);\n setOpen(!open);\n },\n } as IHandlerChildProps);\n});\n\nControlledMenuHandler.displayName = 'ControlledMenuHandler';\n\ninterface IControlledMenuListProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'className' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'\n> {\n children: React.ReactNode;\n className?: string;\n}\n\nconst FOCUSABLE_ITEM_SELECTOR = '[role=\"menuitem\"]:not([aria-disabled=\"true\"])';\n\nexport const ControlledMenuList = React.forwardRef<HTMLDivElement, IControlledMenuListProps>(\n ({ children, className, onClick, onKeyDown, ...rest }, forwardedRef) => {\n const { open, setOpen, dismiss, listId, setFloating, floatingRef, referenceRef } = useMenuContext();\n\n useEffect(() => {\n if (!open) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape' || dismiss?.escapeKey === false) return;\n setOpen(false);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n if (dismiss?.outsidePress === false) return;\n const target = event.target as Node | null;\n if (!target) return;\n if (floatingRef.current?.contains(target) || referenceRef.current?.contains(target)) return;\n if (isPressOnLayeredSurface(target, floatingRef.current)) return;\n setOpen(false);\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [open, setOpen, dismiss, floatingRef, referenceRef]);\n\n const handleListKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(event);\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;\n\n const list = floatingRef.current;\n if (!list) return;\n\n const items = Array.from(list.querySelectorAll<HTMLElement>(FOCUSABLE_ITEM_SELECTOR));\n if (items.length === 0) return;\n\n event.preventDefault();\n const currentIndex = items.indexOf(document.activeElement as HTMLElement);\n const step = event.key === 'ArrowDown' ? 1 : -1;\n const nextIndex = currentIndex < 0 ? 0 : (currentIndex + step + items.length) % items.length;\n items[nextIndex]?.focus();\n };\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <motion.div\n key=\"menu-list\"\n id={listId}\n variants={floatingVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"menu\"\n {...LAYERED_SURFACE_ATTRIBUTE}\n ref={(node) => {\n setFloating(node);\n if (typeof forwardedRef === 'function') forwardedRef(node);\n else if (forwardedRef) forwardedRef.current = node;\n }}\n onKeyDown={handleListKeyDown}\n onClick={(event) => {\n onClick?.(event);\n if (dismiss?.itemPress === false) return;\n const target = event.target as HTMLElement | null;\n if (target?.closest('[role=\"menuitem\"]')) setOpen(false);\n }}\n {...rest}\n className={twMerge(\n 'fixed left-0 top-0 z-[9999] min-w-44 overflow-auto rounded-lg border border-border p-1 text-sm text-foreground glass',\n className,\n )}\n >\n {children}\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n },\n);\n\nControlledMenuList.displayName = 'ControlledMenuList';\n\ninterface IControlledMenuItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'className'> {\n children: React.ReactNode;\n className?: string;\n disabled?: boolean;\n}\n\nexport const ControlledMenuItem: React.FC<IControlledMenuItemProps> = ({\n children,\n className,\n disabled = false,\n onClick,\n ...rest\n}) => (\n <div\n role=\"menuitem\"\n tabIndex={disabled ? -1 : 0}\n aria-disabled={disabled || undefined}\n onClick={disabled ? undefined : onClick}\n onKeyDown={(event) => {\n if (disabled) return;\n if (event.key !== 'Enter' && event.key !== ' ') return;\n event.preventDefault();\n (event.currentTarget as HTMLElement).click();\n }}\n {...rest}\n className={twMerge(\n classNames(\n 'flex w-full cursor-pointer select-none items-center rounded-md px-3 py-2 text-sm outline-none',\n 'transition-colors hover:bg-muted-hover focus:bg-muted-hover',\n { 'pointer-events-none opacity-50': disabled },\n ),\n className,\n )}\n >\n {children}\n </div>\n);\n\ninterface IControlledMenuProps {\n trigger: React.ReactNode;\n items?: Array<IControlledMenuItem>;\n children?: React.ReactNode;\n triggerTooltip?: string;\n placement?: TMenuPlacement;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n disabled?: boolean;\n dismissOnItemClick?: boolean;\n listClassName?: string;\n tooltipClassName?: string;\n listRef?: React.Ref<HTMLDivElement>;\n}\n\nconst ITEM_TONE_CLASS: Record<TMenuItemTone, string> = {\n default: 'text-foreground-muted',\n danger: 'text-danger-emphasis',\n};\n\nconst ICON_TONE_CLASS: Record<TMenuItemTone, string> = {\n default: 'text-foreground-subtle',\n danger: 'text-danger-emphasis',\n};\n\n/**\n * A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to\n * inject the click handler and the reference ref, and a tooltip in that position swallows\n * both, so the menu stops opening.\n */\nconst ControlledMenu: React.FC<IControlledMenuProps> = ({\n trigger,\n items,\n children,\n triggerTooltip,\n placement = 'bottom-start',\n open,\n onOpenChange,\n disabled = false,\n dismissOnItemClick = true,\n listClassName,\n tooltipClassName,\n listRef,\n}) => {\n const menuItems = useMemo(() => (Array.isArray(items) ? items : []), [items]);\n const dismiss = useMemo(() => ({ itemPress: dismissOnItemClick }), [dismissOnItemClick]);\n const handle = <ControlledMenuHandler>{trigger}</ControlledMenuHandler>;\n\n if (disabled) return <>{trigger}</>;\n\n return (\n <ControlledMenuRoot open={open} handler={onOpenChange} placement={placement} dismiss={dismiss}>\n {triggerTooltip ? (\n <ControlledTooltip content={triggerTooltip} className={tooltipClassName}>\n {handle}\n </ControlledTooltip>\n ) : (\n handle\n )}\n\n <ControlledMenuList ref={listRef} className={listClassName}>\n {children ??\n menuItems.map((item) => {\n const tone = item.tone ?? 'default';\n\n return (\n <ControlledMenuItem\n key={item.key}\n disabled={item.disabled}\n onClick={item.onClick}\n className={classNames('items-start gap-x-2', ITEM_TONE_CLASS[tone])}\n >\n {item.icon && <MaterialSymbol icon={item.icon} size={16} className={ICON_TONE_CLASS[tone]} />}\n\n <span className=\"flex flex-col items-start gap-y-0.5\">\n <span>{item.label}</span>\n {item.hint && <span className=\"text-xs text-foreground-subtle\">{item.hint}</span>}\n </span>\n </ControlledMenuItem>\n );\n })}\n </ControlledMenuList>\n </ControlledMenuRoot>\n );\n};\n\nexport default ControlledMenu;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ControlledMenu.js","sources":["../../../src/components/UI/ControlledMenu.tsx"],"sourcesContent":["import React, {\n cloneElement,\n createContext,\n isValidElement,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useState,\n} from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol, SymbolCodepoints } from '@/components/UI/MaterialSymbol';\nimport { composeRefs } from '@/helpers/composeRefs';\nimport { isPressOnLayeredSurface, LAYERED_SURFACE_ATTRIBUTE } from '@/helpers/layeredSurfaces';\nimport { mergeClasses } from '@/helpers/mergeClasses';\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { TFloatingPlacement, useAnchoredFloating } from '@/hooks/useAnchoredFloating';\nimport { floatingVariants } from '@/motion';\n\nexport type TMenuPlacement = TFloatingPlacement;\nexport type TMenuItemTone = 'default' | 'danger';\n\nexport interface IMenuDismissConfig {\n /** Closes the menu when an item is pressed. Multi-select surfaces pass false. */\n itemPress?: boolean;\n escapeKey?: boolean;\n outsidePress?: boolean;\n}\n\nexport interface IControlledMenuItem {\n key: string;\n label: string;\n icon?: SymbolCodepoints;\n hint?: string;\n onClick?: () => void;\n disabled?: boolean;\n tone?: TMenuItemTone;\n}\n\ninterface IMenuContext {\n open: boolean;\n setOpen: (open: boolean) => void;\n dismiss?: IMenuDismissConfig;\n listId: string;\n setReference: (node: HTMLElement | null) => void;\n setFloating: (node: HTMLElement | null) => void;\n floatingRef: React.MutableRefObject<HTMLElement | null>;\n referenceRef: React.MutableRefObject<HTMLElement | null>;\n}\n\nconst MenuContext = createContext<IMenuContext | null>(null);\n\nconst useMenuContext = (): IMenuContext => {\n const context = useContext(MenuContext);\n if (!context) throw new Error('Menu parts must be rendered inside ControlledMenuRoot');\n return context;\n};\n\ninterface IControlledMenuRootProps {\n children: React.ReactNode;\n open?: boolean;\n handler?: (open: boolean) => void;\n placement?: TMenuPlacement;\n offsetPx?: number;\n dismiss?: IMenuDismissConfig;\n matchTriggerWidth?: boolean;\n}\n\nexport const ControlledMenuRoot: React.FC<IControlledMenuRootProps> = ({\n children,\n open,\n handler,\n placement = 'bottom-start',\n offsetPx = 6,\n dismiss,\n matchTriggerWidth = false,\n}) => {\n const listId = useId();\n const [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\n const isControlled = open !== undefined;\n const isOpen = isControlled ? Boolean(open) : uncontrolledOpen;\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n handler?.(next);\n },\n [isControlled, handler],\n );\n\n const { setReference, setFloating, floatingRef, referenceRef } = useAnchoredFloating({\n open: isOpen,\n placement,\n offsetPx,\n matchReferenceWidth: matchTriggerWidth,\n });\n\n const value = useMemo(\n () => ({ open: isOpen, setOpen, dismiss, listId, setReference, setFloating, floatingRef, referenceRef }),\n [isOpen, setOpen, dismiss, listId, setReference, setFloating, floatingRef, referenceRef],\n );\n\n return <MenuContext.Provider value={value}>{children}</MenuContext.Provider>;\n};\n\ninterface IHandlerChildProps {\n onClick?: (event: React.MouseEvent<HTMLElement>) => void;\n ref?: React.Ref<HTMLElement>;\n}\n\n/**\n * Repassa ao filho o que ele recebe de fora, além do que injeta.\n *\n * O gatilho é o alvo natural de um `ControlledTooltip`, e o tooltip se prende clonando o filho e\n * injetando `onPointerEnter`. Aceitando só `children`, este componente engolia essa injeção e o\n * tooltip simplesmente nunca abria, mesmo com a ordem que a documentação manda usar\n * (tooltip POR FORA do handler). Os `onClick` são encadeados: o de fora roda antes de abrir.\n */\nexport const ControlledMenuHandler = React.forwardRef<\n HTMLElement,\n {\n children: React.ReactNode;\n /**\n * Leaves the popup ARIA to the child: set it when the real trigger is an element INSIDE the\n * child that owns its own role (the select's combobox input), so the wrapper does not\n * claim `aria-haspopup=\"menu\"` for a listbox.\n */\n skipTriggerAria?: boolean;\n } & React.HTMLAttributes<HTMLElement>\n>(({ children, onClick: onOuterClick, skipTriggerAria = false, ...rest }, forwardedRef) => {\n const { open, setOpen, setReference, listId } = useMenuContext();\n\n if (!isValidElement(children)) return <>{children}</>;\n\n const child = children as React.ReactElement<IHandlerChildProps>;\n const triggerAria = skipTriggerAria\n ? {}\n : { 'aria-haspopup': 'menu', 'aria-expanded': open, 'aria-controls': open ? listId : undefined };\n\n return cloneElement(child, {\n ...rest,\n ref: composeRefs<HTMLElement>(setReference, forwardedRef, (child as { ref?: React.Ref<HTMLElement> }).ref),\n ...triggerAria,\n onClick: (event: React.MouseEvent<HTMLElement>) => {\n onOuterClick?.(event);\n child.props.onClick?.(event);\n setOpen(!open);\n },\n } as IHandlerChildProps);\n});\n\nControlledMenuHandler.displayName = 'ControlledMenuHandler';\n\ninterface IControlledMenuListProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'className' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'\n> {\n children: React.ReactNode;\n className?: string;\n}\n\nconst FOCUSABLE_ITEM_SELECTOR = '[role=\"menuitem\"]:not([aria-disabled=\"true\"])';\n\nexport const ControlledMenuList = React.forwardRef<HTMLDivElement, IControlledMenuListProps>(\n ({ children, className, onClick, onKeyDown, ...rest }, forwardedRef) => {\n const { open, setOpen, dismiss, listId, setFloating, floatingRef, referenceRef } = useMenuContext();\n\n useEffect(() => {\n if (!open) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape' || dismiss?.escapeKey === false) return;\n setOpen(false);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n if (dismiss?.outsidePress === false) return;\n const target = event.target as Node | null;\n if (!target) return;\n if (floatingRef.current?.contains(target) || referenceRef.current?.contains(target)) return;\n if (isPressOnLayeredSurface(target, floatingRef.current)) return;\n setOpen(false);\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [open, setOpen, dismiss, floatingRef, referenceRef]);\n\n const handleListKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(event);\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;\n\n const list = floatingRef.current;\n if (!list) return;\n\n const items = Array.from(list.querySelectorAll<HTMLElement>(FOCUSABLE_ITEM_SELECTOR));\n if (items.length === 0) return;\n\n event.preventDefault();\n const currentIndex = items.indexOf(document.activeElement as HTMLElement);\n const step = event.key === 'ArrowDown' ? 1 : -1;\n const nextIndex = currentIndex < 0 ? 0 : (currentIndex + step + items.length) % items.length;\n items[nextIndex]?.focus();\n };\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <motion.div\n key=\"menu-list\"\n id={listId}\n variants={floatingVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"menu\"\n {...LAYERED_SURFACE_ATTRIBUTE}\n ref={(node) => {\n setFloating(node);\n if (typeof forwardedRef === 'function') forwardedRef(node);\n else if (forwardedRef) forwardedRef.current = node;\n }}\n onKeyDown={handleListKeyDown}\n onClick={(event) => {\n onClick?.(event);\n if (dismiss?.itemPress === false) return;\n const target = event.target as HTMLElement | null;\n if (target?.closest('[role=\"menuitem\"]')) setOpen(false);\n }}\n {...rest}\n className={mergeClasses(\n 'fixed left-0 top-0 z-[9999] min-w-44 overflow-auto rounded-lg border border-border p-1 text-sm text-foreground glass',\n className,\n )}\n >\n {children}\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n },\n);\n\nControlledMenuList.displayName = 'ControlledMenuList';\n\ninterface IControlledMenuItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'className'> {\n children: React.ReactNode;\n className?: string;\n disabled?: boolean;\n}\n\nexport const ControlledMenuItem: React.FC<IControlledMenuItemProps> = ({\n children,\n className,\n disabled = false,\n onClick,\n ...rest\n}) => (\n <div\n role=\"menuitem\"\n tabIndex={disabled ? -1 : 0}\n aria-disabled={disabled || undefined}\n onClick={disabled ? undefined : onClick}\n onKeyDown={(event) => {\n if (disabled) return;\n if (event.key !== 'Enter' && event.key !== ' ') return;\n event.preventDefault();\n (event.currentTarget as HTMLElement).click();\n }}\n {...rest}\n className={mergeClasses(\n classNames(\n 'flex w-full cursor-pointer select-none items-center rounded-md px-3 py-2 text-sm outline-none',\n 'transition-colors hover:bg-muted-hover focus:bg-muted-hover',\n { 'pointer-events-none opacity-50': disabled },\n ),\n className,\n )}\n >\n {children}\n </div>\n);\n\ninterface IControlledMenuProps {\n trigger: React.ReactNode;\n items?: Array<IControlledMenuItem>;\n children?: React.ReactNode;\n triggerTooltip?: string;\n placement?: TMenuPlacement;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n disabled?: boolean;\n dismissOnItemClick?: boolean;\n listClassName?: string;\n tooltipClassName?: string;\n listRef?: React.Ref<HTMLDivElement>;\n}\n\nconst ITEM_TONE_CLASS: Record<TMenuItemTone, string> = {\n default: 'text-foreground-muted',\n danger: 'text-danger-emphasis',\n};\n\nconst ICON_TONE_CLASS: Record<TMenuItemTone, string> = {\n default: 'text-foreground-subtle',\n danger: 'text-danger-emphasis',\n};\n\n/**\n * A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to\n * inject the click handler and the reference ref, and a tooltip in that position swallows\n * both, so the menu stops opening.\n */\nconst ControlledMenu: React.FC<IControlledMenuProps> = ({\n trigger,\n items,\n children,\n triggerTooltip,\n placement = 'bottom-start',\n open,\n onOpenChange,\n disabled = false,\n dismissOnItemClick = true,\n listClassName,\n tooltipClassName,\n listRef,\n}) => {\n const menuItems = useMemo(() => (Array.isArray(items) ? items : []), [items]);\n const dismiss = useMemo(() => ({ itemPress: dismissOnItemClick }), [dismissOnItemClick]);\n const handle = <ControlledMenuHandler>{trigger}</ControlledMenuHandler>;\n\n if (disabled) return <>{trigger}</>;\n\n return (\n <ControlledMenuRoot open={open} handler={onOpenChange} placement={placement} dismiss={dismiss}>\n {triggerTooltip ? (\n <ControlledTooltip content={triggerTooltip} className={tooltipClassName}>\n {handle}\n </ControlledTooltip>\n ) : (\n handle\n )}\n\n <ControlledMenuList ref={listRef} className={listClassName}>\n {children ??\n menuItems.map((item) => {\n const tone = item.tone ?? 'default';\n\n return (\n <ControlledMenuItem\n key={item.key}\n disabled={item.disabled}\n onClick={item.onClick}\n className={classNames('items-start gap-x-2', ITEM_TONE_CLASS[tone])}\n >\n {item.icon && <MaterialSymbol icon={item.icon} size={16} className={ICON_TONE_CLASS[tone]} />}\n\n <span className=\"flex flex-col items-start gap-y-0.5\">\n <span>{item.label}</span>\n {item.hint && <span className=\"text-xs text-foreground-subtle\">{item.hint}</span>}\n </span>\n </ControlledMenuItem>\n );\n })}\n </ControlledMenuList>\n </ControlledMenuRoot>\n );\n};\n\nexport default ControlledMenu;\n"],"names":[],"mappings":"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"}
|
|
@@ -25,6 +25,16 @@ export declare const ControlledPopoverHandler: React.ForwardRefExoticComponent<{
|
|
|
25
25
|
interface IControlledPopoverContentProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'className' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'> {
|
|
26
26
|
children: React.ReactNode;
|
|
27
27
|
className?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Keyboard support for a panel of controls, which portals to the end of the page and is otherwise
|
|
30
|
+
* only reached after everything else: opening moves the focus inside, closing from inside gives it
|
|
31
|
+
* back to the trigger, and focus going anywhere else closes the panel. Off by default because a
|
|
32
|
+
* popover that opens while someone works in a field (a date input, the colour of a text selection)
|
|
33
|
+
* must not take the focus away from it.
|
|
34
|
+
*/
|
|
35
|
+
manageFocus?: boolean;
|
|
36
|
+
/** Where the keyboard lands with `manageFocus`; the first control by default. */
|
|
37
|
+
initialFocus?: (panel: HTMLElement) => HTMLElement | null;
|
|
28
38
|
}
|
|
29
39
|
export declare const ControlledPopoverContent: React.FC<IControlledPopoverContentProps>;
|
|
30
40
|
interface IControlledPopoverProps {
|
|
@@ -37,6 +47,8 @@ interface IControlledPopoverProps {
|
|
|
37
47
|
contentClassName?: string;
|
|
38
48
|
tooltipClassName?: string;
|
|
39
49
|
matchTriggerWidth?: boolean;
|
|
50
|
+
manageFocus?: boolean;
|
|
51
|
+
initialFocus?: (panel: HTMLElement) => HTMLElement | null;
|
|
40
52
|
}
|
|
41
53
|
/**
|
|
42
54
|
* A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsx, Fragment, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { createContext, isValidElement, cloneElement, useContext, useId, useState, useEffect } from "react";
|
|
2
|
+
import React, { createContext, isValidElement, cloneElement, useContext, useId, useState, useRef, useEffect } from "react";
|
|
3
3
|
import classNames from "classnames";
|
|
4
4
|
import { AnimatePresence, motion } from "motion/react";
|
|
5
5
|
import { createPortal } from "react-dom";
|
|
6
|
-
import {
|
|
6
|
+
import { getTabbableElements } from "../Form/ControlledDialogPrimitives.js";
|
|
7
7
|
import ControlledTooltip from "./ControlledTooltip.js";
|
|
8
8
|
import { composeRefs } from "../../helpers/composeRefs.js";
|
|
9
9
|
import { isPressOnLayeredSurface } from "../../helpers/layeredSurfaces.js";
|
|
10
|
+
import { mergeClasses } from "../../helpers/mergeClasses.js";
|
|
10
11
|
import { getPortalHost } from "../../helpers/portalHost.js";
|
|
11
12
|
import { useAnchoredFloating } from "../../hooks/useAnchoredFloating.js";
|
|
12
13
|
import { floatingVariants } from "../../motion/motionPresets.js";
|
|
@@ -77,9 +78,36 @@ ControlledPopoverHandler.displayName = "ControlledPopoverHandler";
|
|
|
77
78
|
const ControlledPopoverContent = ({
|
|
78
79
|
children,
|
|
79
80
|
className,
|
|
81
|
+
manageFocus = false,
|
|
82
|
+
initialFocus,
|
|
80
83
|
...rest
|
|
81
84
|
}) => {
|
|
82
85
|
const { open, setOpen, contentId, setFloating, floatingRef, referenceRef } = usePopoverContext();
|
|
86
|
+
const setOpenRef = useRef(setOpen);
|
|
87
|
+
setOpenRef.current = setOpen;
|
|
88
|
+
const initialFocusRef = useRef(initialFocus);
|
|
89
|
+
initialFocusRef.current = initialFocus;
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (!open || !manageFocus) return void 0;
|
|
92
|
+
const frame = requestAnimationFrame(() => {
|
|
93
|
+
const panel = floatingRef.current;
|
|
94
|
+
if (!panel || panel.contains(document.activeElement)) return;
|
|
95
|
+
(initialFocusRef.current?.(panel) ?? getTabbableElements(panel)[0])?.focus();
|
|
96
|
+
});
|
|
97
|
+
const closeWhenFocusLeaves = (event) => {
|
|
98
|
+
const { target } = event;
|
|
99
|
+
if (!(target instanceof Node)) return;
|
|
100
|
+
if (floatingRef.current?.contains(target) || referenceRef.current?.contains(target)) return;
|
|
101
|
+
if (isPressOnLayeredSurface(target, floatingRef.current)) return;
|
|
102
|
+
setOpenRef.current(false);
|
|
103
|
+
};
|
|
104
|
+
document.addEventListener("focusin", closeWhenFocusLeaves);
|
|
105
|
+
return () => {
|
|
106
|
+
cancelAnimationFrame(frame);
|
|
107
|
+
document.removeEventListener("focusin", closeWhenFocusLeaves);
|
|
108
|
+
if (floatingRef.current?.contains(document.activeElement)) referenceRef.current?.focus();
|
|
109
|
+
};
|
|
110
|
+
}, [open, manageFocus, floatingRef, referenceRef]);
|
|
83
111
|
useEffect(() => {
|
|
84
112
|
if (!open) return void 0;
|
|
85
113
|
const handleKeyDown = (event) => {
|
|
@@ -113,7 +141,7 @@ const ControlledPopoverContent = ({
|
|
|
113
141
|
exit: "exit",
|
|
114
142
|
role: "dialog",
|
|
115
143
|
...rest,
|
|
116
|
-
className:
|
|
144
|
+
className: mergeClasses(
|
|
117
145
|
"fixed left-0 top-0 z-[9999] overflow-auto rounded-lg border border-border p-2 text-sm text-foreground glass",
|
|
118
146
|
className
|
|
119
147
|
),
|
|
@@ -133,7 +161,9 @@ const ControlledPopover = ({
|
|
|
133
161
|
onOpenChange,
|
|
134
162
|
contentClassName,
|
|
135
163
|
tooltipClassName,
|
|
136
|
-
matchTriggerWidth
|
|
164
|
+
matchTriggerWidth,
|
|
165
|
+
manageFocus,
|
|
166
|
+
initialFocus
|
|
137
167
|
}) => {
|
|
138
168
|
const handle = /* @__PURE__ */ jsx(ControlledPopoverHandler, { children: trigger });
|
|
139
169
|
return /* @__PURE__ */ jsxs(
|
|
@@ -145,7 +175,7 @@ const ControlledPopover = ({
|
|
|
145
175
|
matchTriggerWidth,
|
|
146
176
|
children: [
|
|
147
177
|
triggerTooltip ? /* @__PURE__ */ jsx(ControlledTooltip, { content: triggerTooltip, className: classNames(tooltipClassName), children: handle }) : handle,
|
|
148
|
-
/* @__PURE__ */ jsx(ControlledPopoverContent, { className: contentClassName, children })
|
|
178
|
+
/* @__PURE__ */ jsx(ControlledPopoverContent, { className: contentClassName, manageFocus, initialFocus, children })
|
|
149
179
|
]
|
|
150
180
|
}
|
|
151
181
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControlledPopover.js","sources":["../../../src/components/UI/ControlledPopover.tsx"],"sourcesContent":["import React, { cloneElement, createContext, isValidElement, useContext, useEffect, useId, useState } from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { composeRefs } from '@/helpers/composeRefs';\nimport { isPressOnLayeredSurface } from '@/helpers/layeredSurfaces';\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { TFloatingPlacement, useAnchoredFloating } from '@/hooks/useAnchoredFloating';\nimport { floatingVariants } from '@/motion';\n\nexport type TPopoverPlacement = TFloatingPlacement;\n\ninterface IPopoverContext {\n open: boolean;\n setOpen: (open: boolean) => void;\n placement: TFloatingPlacement;\n offsetPx: number;\n contentId: string;\n setReference: (node: HTMLElement | null) => void;\n setFloating: (node: HTMLElement | null) => void;\n floatingRef: React.MutableRefObject<HTMLElement | null>;\n referenceRef: React.MutableRefObject<HTMLElement | null>;\n}\n\nconst PopoverContext = createContext<IPopoverContext | null>(null);\n\nconst usePopoverContext = (): IPopoverContext => {\n const context = useContext(PopoverContext);\n if (!context) throw new Error('Popover parts must be rendered inside ControlledPopoverRoot');\n return context;\n};\n\ninterface IControlledPopoverRootProps {\n children: React.ReactNode;\n open?: boolean;\n handler?: (open: boolean) => void;\n placement?: TPopoverPlacement;\n offsetPx?: number;\n /** Sizes the content to the trigger width. Use it when the popover acts as a select dropdown. */\n matchTriggerWidth?: boolean;\n}\n\nexport const ControlledPopoverRoot: React.FC<IControlledPopoverRootProps> = ({\n children,\n open,\n handler,\n placement = 'bottom-start',\n offsetPx = 6,\n matchTriggerWidth = false,\n}) => {\n const contentId = useId();\n const [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\n const isControlled = open !== undefined;\n const isOpen = isControlled ? Boolean(open) : uncontrolledOpen;\n\n const setOpen = (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n handler?.(next);\n };\n\n const { setReference, setFloating, floatingRef, referenceRef } = useAnchoredFloating({\n open: isOpen,\n placement,\n offsetPx,\n matchReferenceWidth: matchTriggerWidth,\n });\n\n return (\n <PopoverContext.Provider\n value={{\n open: isOpen,\n setOpen,\n placement,\n offsetPx,\n contentId,\n setReference,\n setFloating,\n floatingRef,\n referenceRef,\n }}\n >\n {children}\n </PopoverContext.Provider>\n );\n};\n\ninterface IHandlerChildProps {\n onClick?: (event: React.MouseEvent<HTMLElement>) => void;\n ref?: React.Ref<HTMLElement>;\n}\n\n/**\n * Repassa ao filho o que ele recebe de fora, além do que injeta.\n *\n * O gatilho é o alvo natural de um `ControlledTooltip`, e o tooltip se prende clonando o filho e\n * injetando `onPointerEnter`. Aceitando só `children`, este componente engolia essa injeção e o\n * tooltip simplesmente nunca abria, mesmo com a ordem que a documentação manda usar\n * (tooltip POR FORA do handler). Os `onClick` são encadeados: o de fora roda antes de abrir.\n */\nexport const ControlledPopoverHandler = React.forwardRef<\n HTMLElement,\n { children: React.ReactNode } & React.HTMLAttributes<HTMLElement>\n>(({ children, onClick: onOuterClick, ...rest }, forwardedRef) => {\n const { open, setOpen, setReference, contentId } = usePopoverContext();\n\n if (!isValidElement(children)) return <>{children}</>;\n\n const child = children as React.ReactElement<IHandlerChildProps>;\n\n return cloneElement(child, {\n ...rest,\n ref: composeRefs<HTMLElement>(setReference, forwardedRef, (child as { ref?: React.Ref<HTMLElement> }).ref),\n 'aria-expanded': open,\n 'aria-controls': open ? contentId : undefined,\n onClick: (event: React.MouseEvent<HTMLElement>) => {\n onOuterClick?.(event);\n child.props.onClick?.(event);\n setOpen(!open);\n },\n } as IHandlerChildProps);\n});\n\nControlledPopoverHandler.displayName = 'ControlledPopoverHandler';\n\ninterface IControlledPopoverContentProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'className' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'\n> {\n children: React.ReactNode;\n className?: string;\n}\n\nexport const ControlledPopoverContent: React.FC<IControlledPopoverContentProps> = ({\n children,\n className,\n ...rest\n}) => {\n const { open, setOpen, contentId, setFloating, floatingRef, referenceRef } = usePopoverContext();\n\n useEffect(() => {\n if (!open) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setOpen(false);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n const target = event.target as Node | null;\n if (!target) return;\n if (floatingRef.current?.contains(target) || referenceRef.current?.contains(target)) return;\n if (isPressOnLayeredSurface(target, floatingRef.current)) return;\n setOpen(false);\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [open, setOpen, floatingRef, referenceRef]);\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <motion.div\n key=\"popover-content\"\n id={contentId}\n ref={setFloating}\n variants={floatingVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"dialog\"\n {...rest}\n className={twMerge(\n 'fixed left-0 top-0 z-[9999] overflow-auto rounded-lg border border-border p-2 text-sm text-foreground glass',\n className,\n )}\n >\n {children}\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n};\n\ninterface IControlledPopoverProps {\n trigger: React.ReactNode;\n children: React.ReactNode;\n triggerTooltip?: string;\n placement?: TPopoverPlacement;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n contentClassName?: string;\n tooltipClassName?: string;\n matchTriggerWidth?: boolean;\n}\n\n/**\n * A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to\n * inject the click handler and the reference ref, and a tooltip in that position swallows\n * both, so the popover stops opening.\n */\nconst ControlledPopover: React.FC<IControlledPopoverProps> = ({\n trigger,\n children,\n triggerTooltip,\n placement = 'bottom-start',\n open,\n onOpenChange,\n contentClassName,\n tooltipClassName,\n matchTriggerWidth,\n}) => {\n const handle = <ControlledPopoverHandler>{trigger}</ControlledPopoverHandler>;\n\n return (\n <ControlledPopoverRoot\n open={open}\n handler={onOpenChange}\n placement={placement}\n matchTriggerWidth={matchTriggerWidth}\n >\n {triggerTooltip ? (\n <ControlledTooltip content={triggerTooltip} className={classNames(tooltipClassName)}>\n {handle}\n </ControlledTooltip>\n ) : (\n handle\n )}\n\n <ControlledPopoverContent className={contentClassName}>{children}</ControlledPopoverContent>\n </ControlledPopoverRoot>\n );\n};\n\nexport default ControlledPopover;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ControlledPopover.js","sources":["../../../src/components/UI/ControlledPopover.tsx"],"sourcesContent":["import React, {\n cloneElement,\n createContext,\n isValidElement,\n useContext,\n useEffect,\n useId,\n useRef,\n useState,\n} from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\n\nimport { getTabbableElements } from '@/components/Form/ControlledDialogPrimitives';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { composeRefs } from '@/helpers/composeRefs';\nimport { isPressOnLayeredSurface } from '@/helpers/layeredSurfaces';\nimport { mergeClasses } from '@/helpers/mergeClasses';\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { TFloatingPlacement, useAnchoredFloating } from '@/hooks/useAnchoredFloating';\nimport { floatingVariants } from '@/motion';\n\nexport type TPopoverPlacement = TFloatingPlacement;\n\ninterface IPopoverContext {\n open: boolean;\n setOpen: (open: boolean) => void;\n placement: TFloatingPlacement;\n offsetPx: number;\n contentId: string;\n setReference: (node: HTMLElement | null) => void;\n setFloating: (node: HTMLElement | null) => void;\n floatingRef: React.MutableRefObject<HTMLElement | null>;\n referenceRef: React.MutableRefObject<HTMLElement | null>;\n}\n\nconst PopoverContext = createContext<IPopoverContext | null>(null);\n\nconst usePopoverContext = (): IPopoverContext => {\n const context = useContext(PopoverContext);\n if (!context) throw new Error('Popover parts must be rendered inside ControlledPopoverRoot');\n return context;\n};\n\ninterface IControlledPopoverRootProps {\n children: React.ReactNode;\n open?: boolean;\n handler?: (open: boolean) => void;\n placement?: TPopoverPlacement;\n offsetPx?: number;\n /** Sizes the content to the trigger width. Use it when the popover acts as a select dropdown. */\n matchTriggerWidth?: boolean;\n}\n\nexport const ControlledPopoverRoot: React.FC<IControlledPopoverRootProps> = ({\n children,\n open,\n handler,\n placement = 'bottom-start',\n offsetPx = 6,\n matchTriggerWidth = false,\n}) => {\n const contentId = useId();\n const [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\n const isControlled = open !== undefined;\n const isOpen = isControlled ? Boolean(open) : uncontrolledOpen;\n\n const setOpen = (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n handler?.(next);\n };\n\n const { setReference, setFloating, floatingRef, referenceRef } = useAnchoredFloating({\n open: isOpen,\n placement,\n offsetPx,\n matchReferenceWidth: matchTriggerWidth,\n });\n\n return (\n <PopoverContext.Provider\n value={{\n open: isOpen,\n setOpen,\n placement,\n offsetPx,\n contentId,\n setReference,\n setFloating,\n floatingRef,\n referenceRef,\n }}\n >\n {children}\n </PopoverContext.Provider>\n );\n};\n\ninterface IHandlerChildProps {\n onClick?: (event: React.MouseEvent<HTMLElement>) => void;\n ref?: React.Ref<HTMLElement>;\n}\n\n/**\n * Repassa ao filho o que ele recebe de fora, além do que injeta.\n *\n * O gatilho é o alvo natural de um `ControlledTooltip`, e o tooltip se prende clonando o filho e\n * injetando `onPointerEnter`. Aceitando só `children`, este componente engolia essa injeção e o\n * tooltip simplesmente nunca abria, mesmo com a ordem que a documentação manda usar\n * (tooltip POR FORA do handler). Os `onClick` são encadeados: o de fora roda antes de abrir.\n */\nexport const ControlledPopoverHandler = React.forwardRef<\n HTMLElement,\n { children: React.ReactNode } & React.HTMLAttributes<HTMLElement>\n>(({ children, onClick: onOuterClick, ...rest }, forwardedRef) => {\n const { open, setOpen, setReference, contentId } = usePopoverContext();\n\n if (!isValidElement(children)) return <>{children}</>;\n\n const child = children as React.ReactElement<IHandlerChildProps>;\n\n return cloneElement(child, {\n ...rest,\n ref: composeRefs<HTMLElement>(setReference, forwardedRef, (child as { ref?: React.Ref<HTMLElement> }).ref),\n 'aria-expanded': open,\n 'aria-controls': open ? contentId : undefined,\n onClick: (event: React.MouseEvent<HTMLElement>) => {\n onOuterClick?.(event);\n child.props.onClick?.(event);\n setOpen(!open);\n },\n } as IHandlerChildProps);\n});\n\nControlledPopoverHandler.displayName = 'ControlledPopoverHandler';\n\ninterface IControlledPopoverContentProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'className' | 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'\n> {\n children: React.ReactNode;\n className?: string;\n /**\n * Keyboard support for a panel of controls, which portals to the end of the page and is otherwise\n * only reached after everything else: opening moves the focus inside, closing from inside gives it\n * back to the trigger, and focus going anywhere else closes the panel. Off by default because a\n * popover that opens while someone works in a field (a date input, the colour of a text selection)\n * must not take the focus away from it.\n */\n manageFocus?: boolean;\n /** Where the keyboard lands with `manageFocus`; the first control by default. */\n initialFocus?: (panel: HTMLElement) => HTMLElement | null;\n}\n\nexport const ControlledPopoverContent: React.FC<IControlledPopoverContentProps> = ({\n children,\n className,\n manageFocus = false,\n initialFocus,\n ...rest\n}) => {\n const { open, setOpen, contentId, setFloating, floatingRef, referenceRef } = usePopoverContext();\n\n // Both change on every render of the root, and leaving the effect below re-runs would hand the\n // focus back to the trigger each time.\n const setOpenRef = useRef(setOpen);\n setOpenRef.current = setOpen;\n const initialFocusRef = useRef(initialFocus);\n initialFocusRef.current = initialFocus;\n\n useEffect(() => {\n if (!open || !manageFocus) return undefined;\n\n // The panel mounts with this update: wait for the frame that paints it.\n const frame = requestAnimationFrame(() => {\n const panel = floatingRef.current;\n if (!panel || panel.contains(document.activeElement)) return;\n (initialFocusRef.current?.(panel) ?? getTabbableElements(panel)[0])?.focus();\n });\n\n const closeWhenFocusLeaves = (event: FocusEvent) => {\n const { target } = event;\n if (!(target instanceof Node)) return;\n if (floatingRef.current?.contains(target) || referenceRef.current?.contains(target)) return;\n if (isPressOnLayeredSurface(target, floatingRef.current)) return;\n setOpenRef.current(false);\n };\n\n document.addEventListener('focusin', closeWhenFocusLeaves);\n\n return () => {\n cancelAnimationFrame(frame);\n document.removeEventListener('focusin', closeWhenFocusLeaves);\n if (floatingRef.current?.contains(document.activeElement)) referenceRef.current?.focus();\n };\n }, [open, manageFocus, floatingRef, referenceRef]);\n\n useEffect(() => {\n if (!open) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') setOpen(false);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n const target = event.target as Node | null;\n if (!target) return;\n if (floatingRef.current?.contains(target) || referenceRef.current?.contains(target)) return;\n if (isPressOnLayeredSurface(target, floatingRef.current)) return;\n setOpen(false);\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [open, setOpen, floatingRef, referenceRef]);\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <motion.div\n key=\"popover-content\"\n id={contentId}\n ref={setFloating}\n variants={floatingVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"dialog\"\n {...rest}\n className={mergeClasses(\n 'fixed left-0 top-0 z-[9999] overflow-auto rounded-lg border border-border p-2 text-sm text-foreground glass',\n className,\n )}\n >\n {children}\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n};\n\ninterface IControlledPopoverProps {\n trigger: React.ReactNode;\n children: React.ReactNode;\n triggerTooltip?: string;\n placement?: TPopoverPlacement;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n contentClassName?: string;\n tooltipClassName?: string;\n matchTriggerWidth?: boolean;\n manageFocus?: boolean;\n initialFocus?: (panel: HTMLElement) => HTMLElement | null;\n}\n\n/**\n * A tooltip on the trigger must sit OUTSIDE the handler: the handler clones its child to\n * inject the click handler and the reference ref, and a tooltip in that position swallows\n * both, so the popover stops opening.\n */\nconst ControlledPopover: React.FC<IControlledPopoverProps> = ({\n trigger,\n children,\n triggerTooltip,\n placement = 'bottom-start',\n open,\n onOpenChange,\n contentClassName,\n tooltipClassName,\n matchTriggerWidth,\n manageFocus,\n initialFocus,\n}) => {\n const handle = <ControlledPopoverHandler>{trigger}</ControlledPopoverHandler>;\n\n return (\n <ControlledPopoverRoot\n open={open}\n handler={onOpenChange}\n placement={placement}\n matchTriggerWidth={matchTriggerWidth}\n >\n {triggerTooltip ? (\n <ControlledTooltip content={triggerTooltip} className={classNames(tooltipClassName)}>\n {handle}\n </ControlledTooltip>\n ) : (\n handle\n )}\n\n <ControlledPopoverContent className={contentClassName} manageFocus={manageFocus} initialFocus={initialFocus}>\n {children}\n </ControlledPopoverContent>\n </ControlledPopoverRoot>\n );\n};\n\nexport default ControlledPopover;\n"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import classNames from "classnames";
|
|
3
|
-
import {
|
|
3
|
+
import { mergeClasses } from "../../helpers/mergeClasses.js";
|
|
4
4
|
const TONE_BAR_CLASS = {
|
|
5
5
|
brand: "bg-brand",
|
|
6
6
|
success: "bg-success",
|
|
@@ -26,11 +26,11 @@ const ControlledProgress = ({
|
|
|
26
26
|
"aria-valuenow": Math.round(clamped),
|
|
27
27
|
"aria-valuemin": 0,
|
|
28
28
|
"aria-valuemax": 100,
|
|
29
|
-
className:
|
|
29
|
+
className: mergeClasses("h-1.5 w-full overflow-hidden rounded-full bg-muted", className),
|
|
30
30
|
children: /* @__PURE__ */ jsx(
|
|
31
31
|
"div",
|
|
32
32
|
{
|
|
33
|
-
className:
|
|
33
|
+
className: mergeClasses(
|
|
34
34
|
classNames("h-full rounded-full transition-interactive duration-slow", !barColor && TONE_BAR_CLASS[tone]),
|
|
35
35
|
barClassName
|
|
36
36
|
),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControlledProgress.js","sources":["../../../src/components/UI/ControlledProgress.tsx"],"sourcesContent":["import React from 'react';\nimport classNames from 'classnames';\nimport {
|
|
1
|
+
{"version":3,"file":"ControlledProgress.js","sources":["../../../src/components/UI/ControlledProgress.tsx"],"sourcesContent":["import React from 'react';\nimport classNames from 'classnames';\n\nimport { mergeClasses } from '@/helpers/mergeClasses';\n\nexport type TProgressTone = 'brand' | 'success' | 'warning' | 'danger' | 'info' | 'neutral';\n\ninterface IControlledProgressProps {\n value: number;\n tone?: TProgressTone;\n className?: string;\n barClassName?: string;\n /** A colour that is DATA (a stage colour the user picked), not a theme token. */\n barColor?: string;\n label?: string;\n}\n\nconst TONE_BAR_CLASS: Record<TProgressTone, string> = {\n brand: 'bg-brand',\n success: 'bg-success',\n warning: 'bg-warning',\n danger: 'bg-danger',\n info: 'bg-info',\n neutral: 'bg-foreground-faint',\n};\n\nconst ControlledProgress: React.FC<IControlledProgressProps> = ({\n value,\n tone = 'brand',\n className,\n barClassName,\n barColor,\n label,\n}) => {\n const clamped = Number.isFinite(value) ? Math.min(100, Math.max(0, value)) : 0;\n\n return (\n <div\n role=\"progressbar\"\n aria-label={label}\n aria-valuenow={Math.round(clamped)}\n aria-valuemin={0}\n aria-valuemax={100}\n className={mergeClasses('h-1.5 w-full overflow-hidden rounded-full bg-muted', className)}\n >\n <div\n className={mergeClasses(\n classNames('h-full rounded-full transition-interactive duration-slow', !barColor && TONE_BAR_CLASS[tone]),\n barClassName,\n )}\n style={{ width: `${clamped}%`, backgroundColor: barColor }}\n />\n </div>\n );\n};\n\nexport default ControlledProgress;\n"],"names":[],"mappings":";;;AAiBA,MAAM,iBAAgD;AAAA,EACpD,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,SAAS;AACX;AAEA,MAAM,qBAAyD,CAAC;AAAA,EAC9D;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,UAAU,OAAO,SAAS,KAAK,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,KAAK,CAAC,IAAI;AAE7E,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,iBAAe,KAAK,MAAM,OAAO;AAAA,MACjC,iBAAe;AAAA,MACf,iBAAe;AAAA,MACf,WAAW,aAAa,sDAAsD,SAAS;AAAA,MAEvF,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,WAAW;AAAA,YACT,WAAW,4DAA4D,CAAC,YAAY,eAAe,IAAI,CAAC;AAAA,YACxG;AAAA,UAAA;AAAA,UAEF,OAAO,EAAE,OAAO,GAAG,OAAO,KAAK,iBAAiB,SAAA;AAAA,QAAS;AAAA,MAAA;AAAA,IAC3D;AAAA,EAAA;AAGN;"}
|
|
@@ -8,6 +8,8 @@ interface IControlledSliderProps {
|
|
|
8
8
|
disabled?: boolean;
|
|
9
9
|
className?: string;
|
|
10
10
|
label?: string;
|
|
11
|
+
/** Reaches the field itself, so a `FieldLabel` can point at it. */
|
|
12
|
+
id?: string;
|
|
11
13
|
}
|
|
12
14
|
declare const ControlledSlider: React.FC<IControlledSliderProps>;
|
|
13
15
|
export default ControlledSlider;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { mergeClasses } from "../../helpers/mergeClasses.js";
|
|
3
3
|
const ControlledSlider = ({
|
|
4
4
|
value,
|
|
5
5
|
onChange,
|
|
@@ -8,10 +8,12 @@ const ControlledSlider = ({
|
|
|
8
8
|
step = 1,
|
|
9
9
|
disabled = false,
|
|
10
10
|
className,
|
|
11
|
-
label
|
|
11
|
+
label,
|
|
12
|
+
id
|
|
12
13
|
}) => /* @__PURE__ */ jsx(
|
|
13
14
|
"input",
|
|
14
15
|
{
|
|
16
|
+
id,
|
|
15
17
|
type: "range",
|
|
16
18
|
min,
|
|
17
19
|
max,
|
|
@@ -20,7 +22,10 @@ const ControlledSlider = ({
|
|
|
20
22
|
disabled,
|
|
21
23
|
"aria-label": label,
|
|
22
24
|
onChange: (event) => onChange(Number(event.target.value)),
|
|
23
|
-
className:
|
|
25
|
+
className: mergeClasses(
|
|
26
|
+
"h-1.5 w-full cursor-pointer appearance-none rounded-full bg-muted accent-brand",
|
|
27
|
+
className
|
|
28
|
+
)
|
|
24
29
|
}
|
|
25
30
|
);
|
|
26
31
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControlledSlider.js","sources":["../../../src/components/UI/ControlledSlider.tsx"],"sourcesContent":["import React from 'react';\nimport {
|
|
1
|
+
{"version":3,"file":"ControlledSlider.js","sources":["../../../src/components/UI/ControlledSlider.tsx"],"sourcesContent":["import React from 'react';\n\nimport { mergeClasses } from '@/helpers/mergeClasses';\n\ninterface IControlledSliderProps {\n value: number;\n onChange: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n disabled?: boolean;\n className?: string;\n label?: string;\n /** Reaches the field itself, so a `FieldLabel` can point at it. */\n id?: string;\n}\n\nconst ControlledSlider: React.FC<IControlledSliderProps> = ({\n value,\n onChange,\n min = 0,\n max = 100,\n step = 1,\n disabled = false,\n className,\n label,\n id,\n}) => (\n <input\n id={id}\n type=\"range\"\n min={min}\n max={max}\n step={step}\n value={Number.isFinite(value) ? value : 0}\n disabled={disabled}\n aria-label={label}\n onChange={(event) => onChange(Number(event.target.value))}\n className={mergeClasses(\n 'h-1.5 w-full cursor-pointer appearance-none rounded-full bg-muted accent-brand',\n className,\n )}\n />\n);\n\nexport default ControlledSlider;\n"],"names":[],"mappings":";;AAiBA,MAAM,mBAAqD,CAAC;AAAA,EAC1D;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AAAA,EACP,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AACF,MACE;AAAA,EAAC;AAAA,EAAA;AAAA,IACC;AAAA,IACA,MAAK;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AAAA,IACxC;AAAA,IACA,cAAY;AAAA,IACZ,UAAU,CAAC,UAAU,SAAS,OAAO,MAAM,OAAO,KAAK,CAAC;AAAA,IACxD,WAAW;AAAA,MACT;AAAA,MACA;AAAA,IAAA;AAAA,EACF;AACF;"}
|