From 32807a0d80df7612c703f345cbe8b591297eb444 Mon Sep 17 00:00:00 2001 From: Konstantin Schaper Date: Thu, 1 Jun 2023 11:43:41 +0200 Subject: [PATCH] Implement chip input for multiple text entries Co-authored-by: Eduard Heimbuch --- gradle/changelog/chip_input_component.yaml | 2 + scm-ui/ui-forms/package.json | 6 +- scm-ui/ui-forms/src/Form.stories.tsx | 7 + scm-ui/ui-forms/src/base/help/Help.tsx | 2 +- .../src/chip-input/ChipInputField.stories.tsx | 40 +++++ .../src/chip-input/ChipInputField.tsx | 141 ++++++++++++++++ .../chip-input/ControlledChipInputField.tsx | 98 +++++++++++ .../src/headless-chip-input/ChipInput.tsx | 154 ++++++++++++++++++ scm-ui/ui-forms/src/index.ts | 3 + .../ui-webapp/public/locales/de/commons.json | 6 + .../ui-webapp/public/locales/en/commons.json | 6 + yarn.lock | 31 ++++ 12 files changed, 493 insertions(+), 3 deletions(-) create mode 100644 gradle/changelog/chip_input_component.yaml create mode 100644 scm-ui/ui-forms/src/chip-input/ChipInputField.stories.tsx create mode 100644 scm-ui/ui-forms/src/chip-input/ChipInputField.tsx create mode 100644 scm-ui/ui-forms/src/chip-input/ControlledChipInputField.tsx create mode 100644 scm-ui/ui-forms/src/headless-chip-input/ChipInput.tsx diff --git a/gradle/changelog/chip_input_component.yaml b/gradle/changelog/chip_input_component.yaml new file mode 100644 index 0000000000..36625a8de6 --- /dev/null +++ b/gradle/changelog/chip_input_component.yaml @@ -0,0 +1,2 @@ +- type: added + description: New chip input component diff --git a/scm-ui/ui-forms/package.json b/scm-ui/ui-forms/package.json index 6628fc2e11..300ab148bb 100644 --- a/scm-ui/ui-forms/package.json +++ b/scm-ui/ui-forms/package.json @@ -43,7 +43,9 @@ "dependencies": { "@scm-manager/ui-buttons": "2.43.2-SNAPSHOT", "@scm-manager/ui-overlays": "2.43.2-SNAPSHOT", - "@scm-manager/ui-api": "2.43.2-SNAPSHOT" + "@scm-manager/ui-api": "2.43.2-SNAPSHOT", + "@radix-ui/react-slot": "^1.0.1", + "@radix-ui/react-visually-hidden": "^1.0.3" }, "prettier": "@scm-manager/prettier-config", "eslintConfig": { @@ -52,4 +54,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/scm-ui/ui-forms/src/Form.stories.tsx b/scm-ui/ui-forms/src/Form.stories.tsx index ac98fe2d19..8efeda280c 100644 --- a/scm-ui/ui-forms/src/Form.stories.tsx +++ b/scm-ui/ui-forms/src/Form.stories.tsx @@ -36,6 +36,7 @@ import ControlledTable from "./table/ControlledTable"; import AddListEntryForm from "./AddListEntryForm"; import { ScmFormListContextProvider } from "./ScmFormListContext"; import { HalRepresentation } from "@scm-manager/ui-types"; +import ControlledChipInputField from "./chip-input/ControlledChipInputField"; export type SimpleWebHookConfiguration = { urlPattern: string; @@ -239,6 +240,7 @@ storiesOf("Forms", module) disableA: false, disableB: false, disableC: true, + labels: ["test"], }} > @@ -247,6 +249,7 @@ storiesOf("Forms", module) + )) .add("ReadOnly", () => ( @@ -257,6 +260,7 @@ storiesOf("Forms", module) name: "trillian", password: "secret", active: true, + labels: ["test", "hero"], }} readOnly > @@ -269,6 +273,9 @@ storiesOf("Forms", module) + + + )) .add("Nested", () => ( diff --git a/scm-ui/ui-forms/src/base/help/Help.tsx b/scm-ui/ui-forms/src/base/help/Help.tsx index cd25fbde36..80b602cfdf 100644 --- a/scm-ui/ui-forms/src/base/help/Help.tsx +++ b/scm-ui/ui-forms/src/base/help/Help.tsx @@ -28,7 +28,7 @@ import { Tooltip } from "@scm-manager/ui-overlays"; type Props = { text?: string; className?: string }; const Help = ({ text, className }: Props) => ( - + ); export default Help; diff --git a/scm-ui/ui-forms/src/chip-input/ChipInputField.stories.tsx b/scm-ui/ui-forms/src/chip-input/ChipInputField.stories.tsx new file mode 100644 index 0000000000..08f92e1054 --- /dev/null +++ b/scm-ui/ui-forms/src/chip-input/ChipInputField.stories.tsx @@ -0,0 +1,40 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { storiesOf } from "@storybook/react"; +import React, { useState } from "react"; +import ChipInputField from "./ChipInputField"; + +storiesOf("Chip Input Field", module).add("Default", () => { + const [value, setValue] = useState(["test"]); + return ( + + ); +}); diff --git a/scm-ui/ui-forms/src/chip-input/ChipInputField.tsx b/scm-ui/ui-forms/src/chip-input/ChipInputField.tsx new file mode 100644 index 0000000000..c161016741 --- /dev/null +++ b/scm-ui/ui-forms/src/chip-input/ChipInputField.tsx @@ -0,0 +1,141 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React, { ComponentProps, useCallback } from "react"; +import { createAttributesForTesting, useGeneratedId } from "@scm-manager/ui-components"; +import Field from "../base/Field"; +import Label from "../base/label/Label"; +import Help from "../base/help/Help"; +import FieldMessage from "../base/field-message/FieldMessage"; +import styled from "styled-components"; +import classNames from "classnames"; +import { createVariantClass } from "../variants"; +import ChipInput, { NewChipInput } from "../headless-chip-input/ChipInput"; +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; +import { useTranslation } from "react-i18next"; + +const StyledChipInput = styled(ChipInput)` + min-height: 40px; + height: min-content; + gap: 0.5rem; + &:focus-within { + border: 1px solid var(--scm-info-color); + box-shadow: rgba(51, 178, 232, 0.25) 0px 0px 0px 0.125em; + } +`; + +const StyledInput = styled(NewChipInput)` + color: var(--scm-secondary-more-color); + font-size: 1rem; + &:focus { + outline: none; + } +`; + +type InputFieldProps = { + label: string; + createDeleteText?: (value: string) => string; + helpText?: string; + error?: string; + testId?: string; + id?: string; +} & Pick, "placeholder"> & + Pick, "onChange" | "value" | "readOnly" | "disabled"> & + Pick, "className">; + +/** + * @beta + * @since 2.44.0 + */ +const ChipInputField = React.forwardRef( + ( + { + label, + helpText, + readOnly, + disabled, + error, + createDeleteText, + onChange, + placeholder, + value, + className, + testId, + id, + ...props + }, + ref + ) => { + const [t] = useTranslation("commons", { keyPrefix: "form.chipList" }); + const deleteTextCallback = useCallback( + (item) => (createDeleteText ? createDeleteText(item) : t("delete", { item })), + [createDeleteText, t] + ); + const inputId = useGeneratedId(id ?? testId); + const labelId = useGeneratedId(); + const inputDescriptionId = useGeneratedId(); + const variant = error ? "danger" : undefined; + return ( + + + + {value?.map((val, index) => ( + + {val} + + + ))} + + + + {t("input.description")} + + {error ? {error} : null} + + ); + } +); +export default ChipInputField; diff --git a/scm-ui/ui-forms/src/chip-input/ControlledChipInputField.tsx b/scm-ui/ui-forms/src/chip-input/ControlledChipInputField.tsx new file mode 100644 index 0000000000..bde24cc4b5 --- /dev/null +++ b/scm-ui/ui-forms/src/chip-input/ControlledChipInputField.tsx @@ -0,0 +1,98 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React, { ComponentProps } from "react"; +import { Controller, ControllerRenderProps, Path } from "react-hook-form"; +import { useScmFormContext } from "../ScmFormContext"; +import { useScmFormPathContext } from "../FormPathContext"; +import { prefixWithoutIndices } from "../helpers"; +import classNames from "classnames"; +import ChipInputField from "./ChipInputField"; + +type Props> = Omit< + ComponentProps, + "error" | "createDeleteText" | "label" | "defaultChecked" | "required" | keyof ControllerRenderProps +> & { + rules?: ComponentProps["rules"]; + name: Path; + label?: string; + defaultValue?: string[]; + createDeleteText?: (value: string) => string; +}; + +/** + * @beta + * @since 2.44.0 + */ +function ControlledChipInputField>({ + name, + label, + helpText, + rules, + testId, + defaultValue, + readOnly, + placeholder, + className, + createDeleteText, + ...props +}: Props) { + const { control, t, readOnly: formReadonly } = useScmFormContext(); + const formPathPrefix = useScmFormPathContext(); + + const nameWithPrefix = formPathPrefix ? `${formPathPrefix}.${name}` : name; + const prefixedNameWithoutIndices = prefixWithoutIndices(nameWithPrefix); + const labelTranslation = label || t(`${prefixedNameWithoutIndices}.label`) || ""; + const placeholderTranslation = placeholder || t(`${prefixedNameWithoutIndices}.placeholder`) || ""; + const ariaLabelTranslation = t(`${prefixedNameWithoutIndices}.ariaLabel`); + const helpTextTranslation = helpText || t(`${prefixedNameWithoutIndices}.helpText`); + return ( + ( + + )} + /> + ); +} + +export default ControlledChipInputField; diff --git a/scm-ui/ui-forms/src/headless-chip-input/ChipInput.tsx b/scm-ui/ui-forms/src/headless-chip-input/ChipInput.tsx new file mode 100644 index 0000000000..acf72dfd63 --- /dev/null +++ b/scm-ui/ui-forms/src/headless-chip-input/ChipInput.tsx @@ -0,0 +1,154 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React, { + ButtonHTMLAttributes, + createContext, + HTMLAttributes, + InputHTMLAttributes, + KeyboardEventHandler, + LiHTMLAttributes, + useCallback, + useContext, + useMemo, +} from "react"; +import { Slot } from "@radix-ui/react-slot"; + +type ChipInputContextType = { + add(newValue: string): void; + remove(index: number): void; + disabled?: boolean; + readOnly?: boolean; + value: string[]; +}; +const ChipInputContext = createContext(null as unknown as ChipInputContextType); + +type ChipDeleteProps = { + asChild?: boolean; + index: number; +} & Omit, "type" | "onClick">; + +/** + * @beta + * @since 2.44.0 + */ +export const ChipDelete = React.forwardRef(({ asChild, index, ...props }, ref) => { + const { remove, disabled } = useContext(ChipInputContext); + const Comp = asChild ? Slot : "button"; + + if (disabled) { + return null; + } + + return remove(index)} />; +}); + +type NewChipInputProps = { + asChild?: boolean; +} & Omit, "onKeyDown" | "disabled" | "readOnly">; + +/** + * @beta + * @since 2.44.0 + */ +export const NewChipInput = React.forwardRef(({ asChild, ...props }, ref) => { + const { add, value, disabled, readOnly } = useContext(ChipInputContext); + const handleKeyDown = useCallback>( + (e) => { + if (e.key === "Enter") { + e.preventDefault(); + const newValue = e.currentTarget.value.trim(); + if (newValue && !value?.includes(newValue)) { + add(newValue); + e.currentTarget.value = ""; + } + return false; + } + }, + [add, value] + ); + const Comp = asChild ? Slot : "input"; + return ; +}); + +type ChipProps = { asChild?: boolean } & LiHTMLAttributes; + +/** + * @beta + * @since 2.44.0 + */ +export const Chip = React.forwardRef(({ asChild, ...props }, ref) => { + const Comp = asChild ? Slot : "li"; + + return ; +}); + +type Props = { + value?: string[]; + onChange?: (newValue: string[]) => void; + readOnly?: boolean; + disabled?: boolean; +} & Omit, "onChange">; + +/** + * @beta + * @since 2.44.0 + */ +const ChipInput = React.forwardRef( + ({ children, value = [], disabled, readOnly, onChange, ...props }, ref) => { + const isInactive = useMemo(() => disabled || readOnly, [disabled, readOnly]); + const add = useCallback( + (newValue: string) => !isInactive && onChange && onChange([...value, newValue]), + [isInactive, onChange, value] + ); + const remove = useCallback( + (index: number) => !isInactive && onChange && onChange(value?.filter((_, itdx) => itdx !== index)), + [isInactive, onChange, value] + ); + return ( + ({ + value, + disabled, + readOnly, + add, + remove, + }), + [add, disabled, readOnly, remove, value] + )} + > +
    + {children} +
+
+ ); + } +); + +export default Object.assign(ChipInput, { + Chip: Object.assign(Chip, { + Delete: ChipDelete, + }), +}); diff --git a/scm-ui/ui-forms/src/index.ts b/scm-ui/ui-forms/src/index.ts index b0eaa166e3..47b2f53d23 100644 --- a/scm-ui/ui-forms/src/index.ts +++ b/scm-ui/ui-forms/src/index.ts @@ -28,6 +28,7 @@ import ControlledInputField from "./input/ControlledInputField"; import ControlledCheckboxField from "./checkbox/ControlledCheckboxField"; import ControlledSecretConfirmationField from "./input/ControlledSecretConfirmationField"; import ControlledSelectField from "./select/ControlledSelectField"; +import ControlledChipInputField from "./chip-input/ControlledChipInputField"; import { ScmFormListContextProvider } from "./ScmFormListContext"; import ControlledList from "./list/ControlledList"; import ControlledTable from "./table/ControlledTable"; @@ -37,6 +38,7 @@ import { ScmNestedFormPathContextProvider } from "./FormPathContext"; export { default as ConfigurationForm } from "./ConfigurationForm"; export { default as SelectField } from "./select/SelectField"; +export { default as ChipInputField } from "./chip-input/ChipInputField"; export { default as Select } from "./select/Select"; export * from "./resourceHooks"; @@ -53,4 +55,5 @@ export const Form = Object.assign(FormCmp, { Table: Object.assign(ControlledTable, { Column: ControlledColumn, }), + ChipInput: ControlledChipInputField, }); diff --git a/scm-ui/ui-webapp/public/locales/de/commons.json b/scm-ui/ui-webapp/public/locales/de/commons.json index 1e6bb3e825..2b97696bd2 100644 --- a/scm-ui/ui-webapp/public/locales/de/commons.json +++ b/scm-ui/ui-webapp/public/locales/de/commons.json @@ -7,6 +7,12 @@ "reset": "Leeren", "discardChanges": "Änderungen verwerfen", "submit-success-notification": "Einstellungen wurden erfolgreich geändert!", + "chipList": { + "delete": "'{{item}}' löschen", + "input": { + "description": "Drücke 'Eingabetaste' um den Wert der Liste hinzuzufügen." + } + }, "table": { "headers": { "action": { diff --git a/scm-ui/ui-webapp/public/locales/en/commons.json b/scm-ui/ui-webapp/public/locales/en/commons.json index 1b0cbeb8c5..1e514639e2 100644 --- a/scm-ui/ui-webapp/public/locales/en/commons.json +++ b/scm-ui/ui-webapp/public/locales/en/commons.json @@ -7,6 +7,12 @@ "reset": "Clear", "discardChanges": "Discard changes", "submit-success-notification": "Configuration changed successfully!", + "chipList": { + "delete": "Delete '{{item}}'", + "input": { + "description": "Press enter to add value to list." + } + }, "table": { "headers": { "action": { diff --git a/yarn.lock b/yarn.lock index c588b80a4c..10fb4b4b5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2411,6 +2411,13 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-compose-refs@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989" + integrity sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-context@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0" @@ -2587,6 +2594,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-slot" "1.0.1" +"@radix-ui/react-primitive@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0" + integrity sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-roving-focus@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.3.tgz#0b4f4f9bd509f4510079e9e0734a734fd17cdce3" @@ -2611,6 +2626,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.0" +"@radix-ui/react-slot@1.0.2", "@radix-ui/react-slot@^1.0.1": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab" + integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-tooltip@1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.2.tgz#8e10b075767f785bf013146fdc954ac6885efda3" @@ -2684,6 +2707,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.1" +"@radix-ui/react-visually-hidden@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz#51aed9dd0fe5abcad7dee2a234ad36106a6984ac" + integrity sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/rect@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.0.tgz#0dc8e6a829ea2828d53cbc94b81793ba6383bf3c"