From 858112fca7be61f8e02a126986bcf822d137452b Mon Sep 17 00:00:00 2001 From: Stephen Haberman Date: Mon, 20 Nov 2023 11:35:24 -0600 Subject: [PATCH 01/25] chore: Fix useTestIds docs, add a few tests. (#973) --- src/utils/useTestIds.test.tsx | 15 +++++++++++++++ src/utils/useTestIds.tsx | 18 ++++++++++-------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/utils/useTestIds.test.tsx b/src/utils/useTestIds.test.tsx index dc86b5a90..628b996f3 100644 --- a/src/utils/useTestIds.test.tsx +++ b/src/utils/useTestIds.test.tsx @@ -15,4 +15,19 @@ describe("useTestIds", () => { const tid = useTestIds({ "data-testid": "firstName" }); expect({ ...tid }).toEqual({ "data-testid": "firstName" }); }); + + it("can use a label as a default prefix", () => { + const tid = useTestIds({}, "First Name"); + expect(tid.input).toEqual({ "data-testid": "firstName_input" }); + }); + + it("can use an optional enum as a default prefix", () => { + enum Color { + Blue = "Blue", + Green = "Green", + } + const colorProp = Color.Blue as Color | undefined; + const tid = useTestIds({}, colorProp); + expect(tid.input).toEqual({ "data-testid": "blue_input" }); + }); }); diff --git a/src/utils/useTestIds.tsx b/src/utils/useTestIds.tsx index e3a59ab82..b03f93ed3 100644 --- a/src/utils/useTestIds.tsx +++ b/src/utils/useTestIds.tsx @@ -1,7 +1,11 @@ +import { defaultTestId } from "src/utils/defaultTestId"; + +export type TestIds = Record; + /** * Provides a way to easily generate `data-testid`s. * - * The test ids are made of a `prefix` + `_` + `key`, where: + * The test ids are made of a `${prefix}_${key}`, where: * * - The prefix is the component name, like "profile", and * - The key is the specific DOM element that's being tagged, like "firstName" @@ -12,9 +16,9 @@ * * ```tsx * const { a, b } = props; - * const testIds = useTestIds(props); + * const tid = useTestIds(props); * - * return ; + * return ; * ``` * * This allows components that embed the component to customize the prefix, i.e. @@ -25,12 +29,10 @@ * - `firstName_errors` * - `lastName_input` * - `lastName_errors` - * - etc + * + * @param props the component's `props` object, which we'll scan for `data-testid` to use as the prefix + * @param defaultPrefix the default prefix to use if no `data-testid` is found on `props` */ -import { defaultTestId } from "src/utils/defaultTestId"; - -export type TestIds = Record; - export function useTestIds(props: object, defaultPrefix?: string): Record { const prefix: string | undefined = (props as any)["data-testid"] || From 4648a3f34ea3b6a1758b27f770cf72e469aeba32 Mon Sep 17 00:00:00 2001 From: Edwar Plata Date: Tue, 28 Nov 2023 11:41:41 -0500 Subject: [PATCH 02/25] feat: add change type icons (#975) ![imagen](https://github.com/homebound-team/beam/assets/35903168/e63e59d4-91b7-4eef-864a-5ddde98c1822) --- src/components/Icon.stories.tsx | 10 ++++++ src/components/Icon.tsx | 58 +++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/components/Icon.stories.tsx b/src/components/Icon.stories.tsx index 92758a847..54a1455e5 100644 --- a/src/components/Icon.stories.tsx +++ b/src/components/Icon.stories.tsx @@ -53,6 +53,8 @@ export const Icon = (props: IconProps) => { "undoCircle", "drag", "move", + "add", + "remove", ]; const alertIcons: IconProps["icon"][] = [ "errorCircle", @@ -142,6 +144,14 @@ export const Icon = (props: IconProps) => { "car", "basement", "cube", + "cart", + "programChange", + "architectural", + "structural", + "mep", + "designPackage", + "updateDesignPackage", + "exteriorStyle", ]; const navigationIcons: IconProps["icon"][] = [ "projects", diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 0b235c7eb..c2c0b1fdc 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -249,6 +249,18 @@ export const Icons = { d="M6 2H4V20H3V22H7V20H6V15H19C19.5523 15 20 14.5523 20 14V5C20 4.44772 19.5523 4 19 4H6V2ZM6 6V13H18V6H6Z" /> ), + add: ( + <> + + + + ), + remove: ( + <> + + + + ), // Arrows chevronsDown: ( <> @@ -740,6 +752,52 @@ export const Icons = { checkCircleFilled: ( ), + cart: ( + <> + + + + + ), + programChange: ( + + ), + architectural: ( + + ), + structural: ( + + ), + mep: ( + + ), + designPackage: ( + + ), + updateDesignPackage: ( + + ), + exteriorStyle: ( + + ), }; export type IconKey = keyof typeof Icons; From d3424d20b383a44b629a61efa9686d57b3e96fe8 Mon Sep 17 00:00:00 2001 From: Homebound Bot Date: Tue, 28 Nov 2023 17:05:04 +0000 Subject: [PATCH 03/25] chore(release): 2.325.0 [skip ci] ## [2.325.0](https://github.com/homebound-team/beam/compare/v2.324.3...v2.325.0) (2023-11-28) ### Features * add change type icons ([#975](https://github.com/homebound-team/beam/issues/975)) ([4648a3f](https://github.com/homebound-team/beam/commit/4648a3f34ea3b6a1758b27f770cf72e469aeba32)) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c5072f45a..ef6040973 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@homebound/beam", - "version": "2.324.3", + "version": "2.325.0", "author": "Homebound", "license": "MIT", "main": "dist/index.js", From 69ca386af0bd17004c3b0a4a606bc1c9fb6c864b Mon Sep 17 00:00:00 2001 From: Edwar Plata Date: Thu, 30 Nov 2023 12:49:46 -0500 Subject: [PATCH 04/25] fix: IconCardGroup generic typing (#976) This is a fix to be able to use the IconCardGroup with enum values --- src/forms/BoundIconCardGroupField.test.tsx | 35 ++++++---- src/forms/BoundIconCardGroupField.tsx | 12 ++-- src/inputs/IconCardGroup.stories.tsx | 31 +++++---- src/inputs/IconCardGroup.tsx | 76 ++++++++++++++++------ 4 files changed, 108 insertions(+), 46 deletions(-) diff --git a/src/forms/BoundIconCardGroupField.test.tsx b/src/forms/BoundIconCardGroupField.test.tsx index 48503dbe1..6498e9dd2 100644 --- a/src/forms/BoundIconCardGroupField.test.tsx +++ b/src/forms/BoundIconCardGroupField.test.tsx @@ -4,25 +4,36 @@ import { ObjectConfig, ObjectState, createObjectState, required } from "@homebou import { BoundIconCardGroupField } from "./BoundIconCardGroupField"; import { click, render } from "src/utils/rtl"; -const categories: IconCardGroupItemOption[] = [ - { icon: "abacus", label: "Math", value: "math" }, - { icon: "archive", label: "History", value: "history" }, - { icon: "dollar", label: "Finance", value: "finance" }, - { icon: "hardHat", label: "Engineering", value: "engineering" }, - { icon: "kanban", label: "Management", value: "management" }, - { icon: "camera", label: "Media", value: "media" }, +enum Category { + Math, + History, + Finance, + Engineering, + Management, + Media, +} + +const categories: IconCardGroupItemOption[] = [ + { icon: "abacus", label: "Math", value: Category.Math }, + { icon: "archive", label: "History", value: Category.History }, + { icon: "dollar", label: "Finance", value: Category.Finance }, + { icon: "hardHat", label: "Engineering", value: Category.Engineering }, + { icon: "kanban", label: "Management", value: Category.Management }, + { icon: "camera", label: "Media", value: Category.Media }, ]; +type NewAuthor = Omit & { favoriteGenres?: Category[] | null }; + describe("BoundIconCardGroupField", () => { it("shows the label", async () => { - const author = createObjectState(formConfig, { favoriteGenres: ["math"] }); + const author = createObjectState(formConfig, { favoriteGenres: [Category.Math] }); const r = await render(); expect(r.favoriteGenres_label).toHaveTextContent("Favorite Genres"); }); it("triggers 'maybeAutoSave' on change", async () => { const autoSave = jest.fn(); // Given a BoundIconCardGroupField with auto save - const author: ObjectState = createObjectState( + const author: ObjectState = createObjectState( formConfig, {}, { maybeAutoSave: () => autoSave(author.favoriteGenres.value) }, @@ -30,12 +41,12 @@ describe("BoundIconCardGroupField", () => { const r = await render(); // When toggling the checkbox off - click(r.favoriteGenres_math); + click(r.favoriteGenres_Math); // Then the callback should be triggered with the current value - expect(autoSave).toBeCalledWith(["math"]); + expect(autoSave).toBeCalledWith([Category.Math]); }); }); -const formConfig: ObjectConfig = { +const formConfig: ObjectConfig = { favoriteGenres: { type: "value", rules: [required], strictOrder: false }, }; diff --git a/src/forms/BoundIconCardGroupField.tsx b/src/forms/BoundIconCardGroupField.tsx index 928462abe..151bfd410 100644 --- a/src/forms/BoundIconCardGroupField.tsx +++ b/src/forms/BoundIconCardGroupField.tsx @@ -3,16 +3,20 @@ import { Observer } from "mobx-react"; import { IconCardGroup, IconCardGroupProps } from "src/inputs/IconCardGroup"; import { useTestIds } from "src/utils"; import { defaultLabel } from "src/utils/defaultLabel"; +import { Value } from "src/inputs"; -export type BoundIconCardGroupFieldProps = Omit & { - field: FieldState; +export type BoundIconCardGroupFieldProps = Omit< + IconCardGroupProps, + "label" | "values" | "onChange" +> & { + field: FieldState; /** Make optional so that callers can override if they want to. */ - onChange?: (values: string[]) => void; + onChange?: (values: V[]) => void; label?: string; }; /** Wraps `IconCardGroup` and binds it to a form field. */ -export function BoundIconCardGroupField(props: BoundIconCardGroupFieldProps) { +export function BoundIconCardGroupField(props: BoundIconCardGroupFieldProps) { const { field, onChange = (value) => field.set(value), label = defaultLabel(field.key), ...others } = props; const testId = useTestIds(props, field.key); return ( diff --git a/src/inputs/IconCardGroup.stories.tsx b/src/inputs/IconCardGroup.stories.tsx index 227ee5c9f..4d13949d6 100644 --- a/src/inputs/IconCardGroup.stories.tsx +++ b/src/inputs/IconCardGroup.stories.tsx @@ -1,28 +1,37 @@ import { Meta } from "@storybook/react"; import { IconCardGroup, IconCardGroupItemOption, IconCardGroupProps } from "./IconCardGroup"; -import { Chips } from "src/components"; import { useState } from "react"; import { Css } from "src/Css"; export default { component: IconCardGroup, -} as Meta; +} as Meta>; -const categories: IconCardGroupItemOption[] = [ - { icon: "abacus", label: "Math", value: "math" }, - { icon: "archive", label: "History", value: "history" }, - { icon: "dollar", label: "Finance", value: "finance" }, - { icon: "hardHat", label: "Engineering", value: "engineering" }, - { icon: "kanban", label: "Management", value: "management" }, - { icon: "camera", label: "Media", value: "media" }, +enum Category { + Math, + History, + Finance, + Engineering, + Management, + Media, + Na, +} + +const categories: IconCardGroupItemOption[] = [ + { icon: "abacus", label: "Math", value: Category.Math }, + { icon: "archive", label: "History", value: Category.History }, + { icon: "dollar", label: "Finance", value: Category.Finance }, + { icon: "hardHat", label: "Engineering", value: Category.Engineering }, + { icon: "kanban", label: "Management", value: Category.Management }, + { icon: "camera", label: "Media", value: Category.Media }, + { icon: "remove", label: "N/A", value: Category.Na, exclusive: true }, ]; export function Default() { - const [values, setValues] = useState(["math"]); + const [values, setValues] = useState([Category.Math]); return (
-
{ icon: IconProps["icon"]; label: string; disabled?: boolean; /** The value of the IconCardGroup item, stored in value array in state. */ - value: string; + value: V; + /** Exclusive: if true, this option will override all other options when selected. */ + exclusive?: boolean; } -export interface IconCardGroupProps extends Pick { +export interface IconCardGroupProps extends Pick { label: string; /** Called when a card is selected */ - onChange: (values: string[]) => void; + onChange: (values: V[]) => void; /** Options for the cards contained within the IconCardGroup. */ - options: IconCardGroupItemOption[]; + options: IconCardGroupItemOption[]; /** The values currently selected. */ - values: string[]; + values: V[]; errorMsg?: string; helperText?: string | ReactNode; disabled?: boolean; } -export function IconCardGroup(props: IconCardGroupProps) { +export function IconCardGroup(props: IconCardGroupProps) { const { fieldProps } = usePresentationContext(); const { options, @@ -41,14 +43,51 @@ export function IconCardGroup(props: IconCardGroupProps) { errorMsg, helperText, disabled: isDisabled = false, + onChange, } = props; - const state = useCheckboxGroupState({ ...props, isDisabled, value: values }); - const { groupProps, labelProps } = useCheckboxGroup(props, state); + const [selected, setSelected] = useState(values); + + const exclusiveOptions = useMemo(() => options.filter((o) => o.exclusive), [options]); + + const toggleValue = useCallback( + (value: V) => { + if (isDisabled) return; + + const option = options.find((o) => o.value === value); + if (!option) return; + + let newSelected: V[] = []; + if (selected.includes(value)) { + newSelected = selected.filter((v) => v !== value); + } else { + if (option.exclusive) { + newSelected = [value]; + } else { + newSelected = [...selected, value]; + + // Filter out any exclusive options as a non-exclusive option was selected. + newSelected = newSelected.filter((v) => !exclusiveOptions.some((o) => o.value === v)); + } + } + setSelected(newSelected); + onChange(newSelected); + }, + [exclusiveOptions, isDisabled, onChange, options, selected], + ); + const tid = useTestIds(props); + const { labelProps, fieldProps: fieldPropsAria } = useField(props); + + const groupProps = mergeProps(tid, { + role: "group", + "aria-disabled": isDisabled || undefined, + ...fieldPropsAria, + }); + return ( -
+
{labelStyle !== "hidden" && (
+ + + ); } diff --git a/src/components/Modal/ModalContext.tsx b/src/components/Modal/ModalContext.tsx new file mode 100644 index 000000000..a6ae167e1 --- /dev/null +++ b/src/components/Modal/ModalContext.tsx @@ -0,0 +1,20 @@ +import { ReactNode, createContext, useContext, useMemo } from "react"; + +interface ModalContextState { + inModal: boolean; +} + +export const ModalContext = createContext({ inModal: false }); + +interface ModalProviderProps { + children: ReactNode; +} + +export function ModalProvider({ children }: ModalProviderProps) { + const value = useMemo(() => ({ inModal: true }), []); + return {children}; +} + +export function useModalContext(): ModalContextState { + return useContext(ModalContext); +} diff --git a/src/components/Modal/useModal.test.tsx b/src/components/Modal/useModal.test.tsx index 8ad60c07b..53e382dad 100644 --- a/src/components/Modal/useModal.test.tsx +++ b/src/components/Modal/useModal.test.tsx @@ -95,4 +95,27 @@ describe("useModal", () => { // And the BeamContext has been cleared expect(beamContext!.modalCanCloseChecks.current).toEqual([]); }); + + it("can identify when component is In Modal", async () => { + // Given a test app that opens a modal with content that checks if it is in a modal + function TestApp(props: ModalProps) { + const { openModal, inModal } = useModal(); + useEffect(() => openModal(props), [openModal, props]); + return
Behind Modal: InModal? {String(inModal)}
; + } + + // And a modal content that checks if it is in a modal also + function TestModalContent() { + const { inModal } = useModal(); + return
Modal Content: InModal? {String(inModal)}
; + } + + // When rendering the test app + const r = await render(} />); + + // Then the test app should not be in a modal + expect(r.testApp).toHaveTextContent("Behind Modal: InModal? false"); + // And the modal content should be in a modal + expect(r.modalContent).toHaveTextContent("Modal Content: InModal? true"); + }); }); diff --git a/src/components/Modal/useModal.tsx b/src/components/Modal/useModal.tsx index 2a7a43eba..a43161417 100644 --- a/src/components/Modal/useModal.tsx +++ b/src/components/Modal/useModal.tsx @@ -3,16 +3,19 @@ import { useBeamContext } from "src/components/BeamContext"; import { CheckFn } from "src/types"; import { maybeCall } from "src/utils"; import { ModalApi, ModalProps } from "./Modal"; +import { useModalContext } from "./ModalContext"; export interface UseModalHook { openModal: (props: ModalProps) => void; closeModal: VoidFunction; addCanClose: (canClose: CheckFn) => void; setSize: (size: ModalProps["size"]) => void; + inModal: boolean; } export function useModal(): UseModalHook { const { modalState, modalCanCloseChecks } = useBeamContext(); + const { inModal } = useModalContext(); const lastCanClose = useRef(); const api = useRef(); useEffect(() => { @@ -51,7 +54,8 @@ export function useModal(): UseModalHook { modalState.current.api.current.setSize(size); } }, + inModal, }), - [modalState, modalCanCloseChecks], + [inModal, modalState, modalCanCloseChecks], ); } From 6aa3ae7d45978c011bc3f4bc259cd2b500d36ec4 Mon Sep 17 00:00:00 2001 From: Homebound Bot Date: Fri, 1 Dec 2023 17:58:56 +0000 Subject: [PATCH 09/25] chore(release): 2.326.0 [skip ci] ## [2.326.0](https://github.com/homebound-team/beam/compare/v2.325.2...v2.326.0) (2023-12-01) ### Features * add inModal state on useModal ([#974](https://github.com/homebound-team/beam/issues/974)) ([8f8ca2e](https://github.com/homebound-team/beam/commit/8f8ca2e0f2bbacff023774b1faa3c4c03295e78a)) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6e522f9d1..a06711721 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@homebound/beam", - "version": "2.325.2", + "version": "2.326.0", "author": "Homebound", "license": "MIT", "main": "dist/index.js", From 91c79d528edd1c0b4bb7429a11c7adeaa4881f4a Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 5 Dec 2023 14:04:56 -0500 Subject: [PATCH 10/25] fix: ChipTextField cursor resetting position in Safari (#978) --- src/inputs/ChipTextField.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/inputs/ChipTextField.tsx b/src/inputs/ChipTextField.tsx index 18bf02127..c866642d2 100644 --- a/src/inputs/ChipTextField.tsx +++ b/src/inputs/ChipTextField.tsx @@ -80,9 +80,11 @@ export function ChipTextField(props: ChipTextFieldProps) { } }} onInput={(e: KeyboardEvent) => { - // Prevent user from pasting content that has new line characters and replace with empty space. const target = e.target as HTMLElement; - target.textContent = target.textContent?.replace(/[\n\r]/g, " ") ?? ""; + if ("inputType" in e.nativeEvent && e.nativeEvent.inputType === "insertFromPaste") { + // Clean up any formatting from pasted text + target.innerHTML = target.textContent?.replace(/[A\n\r]/g, " ") ?? ""; + } onChange(target.textContent ?? ""); }} {...focusProps} From bb3b5faa58529db440e1e03ad34194c5675b92ff Mon Sep 17 00:00:00 2001 From: Homebound Bot Date: Tue, 5 Dec 2023 19:07:45 +0000 Subject: [PATCH 11/25] chore(release): 2.326.1 [skip ci] ## [2.326.1](https://github.com/homebound-team/beam/compare/v2.326.0...v2.326.1) (2023-12-05) ### Bug Fixes * ChipTextField cursor resetting position in Safari ([#978](https://github.com/homebound-team/beam/issues/978)) ([91c79d5](https://github.com/homebound-team/beam/commit/91c79d528edd1c0b4bb7429a11c7adeaa4881f4a)) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a06711721..9f2175339 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@homebound/beam", - "version": "2.326.0", + "version": "2.326.1", "author": "Homebound", "license": "MIT", "main": "dist/index.js", From 6bf26ac11dadd2f38f67d3c7c49555e96deb0e18 Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 6 Dec 2023 21:43:17 -0500 Subject: [PATCH 12/25] feat: Support 'fullWidth' prop on all text fields (#979) Adds support for a `PresentationFieldProps.fullWidth` property. The following components will respect the property: - FormLines - TextField - TextAreaField - RichTextField - SelectField - MultiSelectField - DateField - NumberField - TreeSelectField Updates the `FormLines` component to default to the `"lg"` size instead of `"full"`. This will hopefully minimize the impact on existing layouts as the previous default width for each field within `FormLines` was 550px. By using the `lg` size, we'll continue to default the fields within `FormLines` to 550px. --- package.json | 2 +- src/Css.ts | 16 ++ src/components/Pagination.stories.tsx | 2 +- src/components/PresentationContext.tsx | 2 + src/forms/FormLines.stories.tsx | 152 ++++++++++++++++-- src/forms/FormLines.tsx | 3 +- src/inputs/Autocomplete.stories.tsx | 14 +- src/inputs/Autocomplete.tsx | 4 +- src/inputs/DateFields/DateField.stories.tsx | 3 +- src/inputs/DateFields/DateFieldBase.tsx | 5 +- .../DateFields/DateRangeField.stories.tsx | 1 + src/inputs/MultiSelectField.stories.tsx | 5 + src/inputs/NumberField.stories.tsx | 5 + src/inputs/NumberField.tsx | 2 +- src/inputs/RichTextField.tsx | 17 +- src/inputs/SelectField.stories.tsx | 2 + src/inputs/TextAreaField.stories.tsx | 3 + src/inputs/TextField.stories.tsx | 5 + src/inputs/TextFieldBase.tsx | 9 +- .../TreeSelectField.stories.tsx | 11 ++ .../TreeSelectField/TreeSelectField.tsx | 12 +- src/inputs/internal/ComboBoxBase.tsx | 9 +- src/inputs/utils.ts | 3 + yarn.lock | 10 +- 24 files changed, 258 insertions(+), 39 deletions(-) create mode 100644 src/inputs/utils.ts diff --git a/package.json b/package.json index 9f2175339..064f0c8e0 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "@homebound/eslint-config": "^1.8.0", "@homebound/rtl-react-router-utils": "1.0.3", "@homebound/rtl-utils": "^2.65.0", - "@homebound/truss": "^1.131.0", + "@homebound/truss": "^1.132.0", "@homebound/tsconfig": "^1.0.3", "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", diff --git a/src/Css.ts b/src/Css.ts index 4faf58a80..279b06df5 100644 --- a/src/Css.ts +++ b/src/Css.ts @@ -1133,6 +1133,22 @@ class CssBuilder { fd(value: Properties["flexDirection"]) { return this.add("flexDirection", value); } + /** Sets `flexWrap: "wrap"`. */ + get fww() { + return this.add("flexWrap", "wrap"); + } + /** Sets `flexWrap: "wrap-reverse"`. */ + get fwr() { + return this.add("flexWrap", "wrap-reverse"); + } + /** Sets `flexWrap: "nowrap"`. */ + get fwnw() { + return this.add("flexWrap", "nowrap"); + } + /** Sets `flexWrap: value`. */ + flexWrap(value: Properties["flexWrap"]) { + return this.add("flexWrap", value); + } // float /** Sets `float: "left"`. */ diff --git a/src/components/Pagination.stories.tsx b/src/components/Pagination.stories.tsx index e7ef412ff..adc89aaea 100644 --- a/src/components/Pagination.stories.tsx +++ b/src/components/Pagination.stories.tsx @@ -31,7 +31,7 @@ function RenderPagination({ totalRows }: { totalRows: number }) { const page = useState({ pageNumber: 1, pageSize: 100 }); return ( <> - + diff --git a/src/components/PresentationContext.tsx b/src/components/PresentationContext.tsx index 06903b3d3..10c1c389c 100644 --- a/src/components/PresentationContext.tsx +++ b/src/components/PresentationContext.tsx @@ -17,6 +17,8 @@ export interface PresentationFieldProps { visuallyDisabled?: false; // If set error messages will be rendered as tooltips rather than below the field errorInTooltip?: true; + /** Allow the fields to grow to the width of its container. By default, fields will extend up to 550px */ + fullWidth?: boolean; } export type PresentationContextProps = { diff --git a/src/forms/FormLines.stories.tsx b/src/forms/FormLines.stories.tsx index 3f22d90c7..af412d5f2 100644 --- a/src/forms/FormLines.stories.tsx +++ b/src/forms/FormLines.stories.tsx @@ -1,6 +1,6 @@ import { Meta } from "@storybook/react"; import { FieldGroup, FormDivider, FormLines } from "src/forms/FormLines"; -import { SelectField } from "src/inputs"; +import { DateField, MultiSelectField, RichTextField, SelectField, TextAreaField, TreeSelectField } from "src/inputs"; import { NumberField } from "src/inputs/NumberField"; import { Switch } from "src/inputs/Switch"; import { TextField } from "src/inputs/TextField"; @@ -13,8 +13,38 @@ export default { export function FlatList() { return ( - {}} /> - {}} /> + + + + + label="Single Select Field" + value={1} + options={[ + { id: 1, name: "Green" }, + { id: 2, name: "Red" }, + ]} + onSelect={noop} + /> + + label="Multiselect Field" + values={[1]} + options={[ + { id: 1, name: "Soccer" }, + { id: 2, name: "Basketball" }, + { id: 3, name: "Football" }, + ]} + onSelect={noop} + /> + o.id} + getOptionLabel={(o) => o.name} + /> + + ); } @@ -22,8 +52,38 @@ export function FlatList() { export function SmallFlatList() { return ( - {}} /> - {}} /> + + + + + label="Single Select Field" + value={1} + options={[ + { id: 1, name: "Green" }, + { id: 2, name: "Red" }, + ]} + onSelect={noop} + /> + + label="Multiselect Field" + values={[1]} + options={[ + { id: 1, name: "Soccer" }, + { id: 2, name: "Basketball" }, + { id: 3, name: "Football" }, + ]} + onSelect={noop} + /> + o.id} + getOptionLabel={(o) => o.name} + /> + + ); } @@ -31,15 +91,45 @@ export function SmallFlatList() { export function FullFlatList() { return ( - {}} /> - {}} /> + + + + + label="Single Select Field" + value={1} + options={[ + { id: 1, name: "Green" }, + { id: 2, name: "Red" }, + ]} + onSelect={noop} + /> + + label="Multiselect Field" + values={[1]} + options={[ + { id: 1, name: "Soccer" }, + { id: 2, name: "Basketball" }, + { id: 3, name: "Football" }, + ]} + onSelect={noop} + /> + o.id} + getOptionLabel={(o) => o.name} + /> + + ); } -export function SideBySideMedium() { +export function SideBySideFull() { return ( - + {}} /> {}} /> @@ -80,7 +170,7 @@ export function SideBySideMedium() { } export function SideBySideLarge() { return ( - + {}} /> {}} /> @@ -129,6 +219,48 @@ export function SideBySideLarge() { ); } + +export function SideBySideMedium() { + return ( + + + {}} /> + {}} /> + + {}} /> + + {}} /> + {}} /> + + + {}} /> + + + + {}} /> + {}} /> + {}} /> + + + {}} /> + {}} /> + {}} /> + + + {}} /> + + label="Unit of Measure" + value={1} + options={[ + { id: 1, name: "Each" }, + { id: 2, name: "Square Feet" }, + ]} + onSelect={noop} + /> + + + ); +} export function SideBySideSmall() { return ( diff --git a/src/forms/FormLines.tsx b/src/forms/FormLines.tsx index 1f5fbe004..c525390b7 100644 --- a/src/forms/FormLines.tsx +++ b/src/forms/FormLines.tsx @@ -28,7 +28,7 @@ export interface FormLinesProps { * (see the `FieldGroup` component), where they will be laid out side-by-side. */ export function FormLines(props: FormLinesProps) { - const { children, width = "full", labelSuffix, labelStyle, compact } = props; + const { children, width = "lg", labelSuffix, labelStyle, compact } = props; let firstFormHeading = true; // Only overwrite `fieldProps` if new values are explicitly set. Ensures we only set to `undefined` if explicitly set. @@ -36,6 +36,7 @@ export function FormLines(props: FormLinesProps) { ...("labelSuffix" in props ? { labelSuffix } : {}), ...("labelStyle" in props ? { labelStyle } : {}), ...("compact" in props ? { compact } : {}), + ...(width === "full" ? { fullWidth: true } : {}), }; return ( diff --git a/src/inputs/Autocomplete.stories.tsx b/src/inputs/Autocomplete.stories.tsx index ba05091c3..02bde5908 100644 --- a/src/inputs/Autocomplete.stories.tsx +++ b/src/inputs/Autocomplete.stories.tsx @@ -4,22 +4,25 @@ import { within } from "@storybook/testing-library"; import { useState } from "react"; import { useFilter } from "react-aria"; import { Css } from "src/Css"; -import { Autocomplete } from "src/inputs/Autocomplete"; +import { Autocomplete, AutocompleteProps } from "src/inputs/Autocomplete"; export default { component: Autocomplete, } as Meta; -export const Example: StoryFn = Template.bind({}); +export const Example: StoryFn> = Template.bind({}); Example.parameters = { chromatic: { disableSnapshot: true } }; -export const MenuOpen: StoryFn = Template.bind({}); +export const FullWidth: StoryFn> = Template.bind({}); +FullWidth.args = { fullWidth: true }; + +export const MenuOpen: StoryFn> = Template.bind({}); MenuOpen.play = async ({ canvasElement }: { canvasElement: HTMLElement }) => { const canvas = within(canvasElement); canvas.getByTestId("heroes").focus(); }; -function Template() { +function Template(props: AutocompleteProps) { const { contains } = useFilter({ sensitivity: "base" }); const [value, setValue] = useState(); const allOptions = [ @@ -30,9 +33,10 @@ function Template() { { label: "Black Widow", imgSrc: "/black-widow.jpg" }, ]; const [options, setOptions] = useState(allOptions); - + console.log("props", props); return ( + {...props} label="Heroes" labelStyle="hidden" getOptionValue={(o) => o.label} diff --git a/src/inputs/Autocomplete.tsx b/src/inputs/Autocomplete.tsx index 95a055f6a..29ebeefe6 100644 --- a/src/inputs/Autocomplete.tsx +++ b/src/inputs/Autocomplete.tsx @@ -9,9 +9,9 @@ import { ListBox } from "src/inputs/internal/ListBox"; import { TextFieldBase, TextFieldBaseProps } from "src/inputs/TextFieldBase"; import { Value, valueToKey } from "src/inputs/Value"; -interface AutocompleteProps +export interface AutocompleteProps extends Pick, - Pick, "label" | "clearable" | "startAdornment"> { + Pick, "label" | "clearable" | "startAdornment" | "fullWidth"> { onSelect: (item: T) => void; /** A function that returns how to render the an option in the menu. If not set, `getOptionLabel` will be used */ getOptionMenuLabel?: (o: T) => ReactNode; diff --git a/src/inputs/DateFields/DateField.stories.tsx b/src/inputs/DateFields/DateField.stories.tsx index 5438e631a..7e937045c 100644 --- a/src/inputs/DateFields/DateField.stories.tsx +++ b/src/inputs/DateFields/DateField.stories.tsx @@ -10,7 +10,6 @@ import { samples, withDimensions } from "src/utils/sb"; export default { component: Button, - decorators: [withDimensions()], parameters: { design: { type: "figma", @@ -61,6 +60,7 @@ export function DateFields() { disabledDays={[{ before: jan1 }, { after: jan10 }]} />, ], + ["Full Width", ], ); } @@ -79,6 +79,7 @@ export function DatePickerOpen() { ); } +DatePickerOpen.decorators = [withDimensions()]; function TestDateField(props: Omit) { const [value, onChange] = useState(jan1); diff --git a/src/inputs/DateFields/DateFieldBase.tsx b/src/inputs/DateFields/DateFieldBase.tsx index 93e61ff2c..30bfac1bc 100644 --- a/src/inputs/DateFields/DateFieldBase.tsx +++ b/src/inputs/DateFields/DateFieldBase.tsx @@ -22,7 +22,10 @@ import { maybeCall, useTestIds } from "src/utils"; import { defaultTestId } from "src/utils/defaultTestId"; export interface DateFieldBaseProps - extends Pick, "borderless" | "visuallyDisabled" | "labelStyle" | "compact"> { + extends Pick< + TextFieldBaseProps, + "borderless" | "visuallyDisabled" | "labelStyle" | "compact" | "fullWidth" + > { label: string; /** Called when the component loses focus */ onBlur?: () => void; diff --git a/src/inputs/DateFields/DateRangeField.stories.tsx b/src/inputs/DateFields/DateRangeField.stories.tsx index 00a71ed3e..fd4b70103 100644 --- a/src/inputs/DateFields/DateRangeField.stories.tsx +++ b/src/inputs/DateFields/DateRangeField.stories.tsx @@ -40,6 +40,7 @@ export function Example() { label="Error message" errorMsg={rangeInitUndefined ? undefined : "Required"} /> +
); } diff --git a/src/inputs/MultiSelectField.stories.tsx b/src/inputs/MultiSelectField.stories.tsx index 4b7844e50..950064bc7 100644 --- a/src/inputs/MultiSelectField.stories.tsx +++ b/src/inputs/MultiSelectField.stories.tsx @@ -173,6 +173,11 @@ export function MultiSelectFields() { )} />
+ +
+

Full Width

+ +
); } diff --git a/src/inputs/NumberField.stories.tsx b/src/inputs/NumberField.stories.tsx index dd6eb09ab..07237f211 100644 --- a/src/inputs/NumberField.stories.tsx +++ b/src/inputs/NumberField.stories.tsx @@ -63,6 +63,11 @@ export function NumberFieldStyles() {

Without grouping

+ +
+

Full Width

+ +
); } diff --git a/src/inputs/NumberField.tsx b/src/inputs/NumberField.tsx index a743c8332..28dee2a10 100644 --- a/src/inputs/NumberField.tsx +++ b/src/inputs/NumberField.tsx @@ -11,7 +11,7 @@ import { TextFieldBase } from "./TextFieldBase"; export type NumberFieldType = "cents" | "dollars" | "percent" | "basisPoints" | "days"; // exported for testing purposes -export interface NumberFieldProps extends Pick { +export interface NumberFieldProps extends Pick { label: string; /** If set, the label will be defined as 'aria-label` on the input element */ type?: NumberFieldType; diff --git a/src/inputs/RichTextField.tsx b/src/inputs/RichTextField.tsx index 1f63990d3..d89e00f98 100644 --- a/src/inputs/RichTextField.tsx +++ b/src/inputs/RichTextField.tsx @@ -8,8 +8,9 @@ import Tribute from "tributejs"; import "tributejs/dist/tribute.css"; import "trix/dist/trix"; import "trix/dist/trix.css"; +import { PresentationFieldProps, usePresentationContext } from "src/components/PresentationContext"; -export interface RichTextFieldProps { +export interface RichTextFieldProps extends Pick { /** The initial html value to show in the trix editor. */ value: string | undefined; onChange: (html: string | undefined, text: string | undefined, mergeTags: string[]) => void; @@ -28,8 +29,6 @@ export interface RichTextFieldProps { onFocus?: () => void; /** For rendering formatted text */ readOnly?: boolean; - /** Will set width to: 100% */ - fullWidth?: boolean; } /** @@ -40,7 +39,17 @@ export interface RichTextFieldProps { * We also integrate [tributejs]{@link https://github.com/zurb/tribute} for @ mentions. */ export function RichTextField(props: RichTextFieldProps) { - const { mergeTags, label, value = "", onChange, onBlur = noop, onFocus = noop, readOnly, fullWidth } = props; + const { fieldProps } = usePresentationContext(); + const { + mergeTags, + label, + value = "", + onChange, + onBlur = noop, + onFocus = noop, + readOnly, + fullWidth = fieldProps?.fullWidth ?? false, + } = props; // We get a reference to the Editor instance after trix-init fires const [editor, setEditor] = useState(); diff --git a/src/inputs/SelectField.stories.tsx b/src/inputs/SelectField.stories.tsx index 3591acd00..4932ff207 100644 --- a/src/inputs/SelectField.stories.tsx +++ b/src/inputs/SelectField.stories.tsx @@ -201,6 +201,8 @@ function Template(args: SelectFieldProps) { disabledOptions={[options[0].id, { value: options[3].id, reason: "Example disabled tooltip" }]} helperText="Disabled options can optionally have tooltip text" /> + +
diff --git a/src/inputs/TextAreaField.stories.tsx b/src/inputs/TextAreaField.stories.tsx index 6e017397e..583a69c8b 100644 --- a/src/inputs/TextAreaField.stories.tsx +++ b/src/inputs/TextAreaField.stories.tsx @@ -40,6 +40,9 @@ export function TextAreaStyles() { {}} /> +

Full Width

+ +

Modified for Blueprint To Do Title

} />
+ +
+

Full Width

+ +
); } diff --git a/src/inputs/TextFieldBase.tsx b/src/inputs/TextFieldBase.tsx index 402568a27..c45a4d96a 100644 --- a/src/inputs/TextFieldBase.tsx +++ b/src/inputs/TextFieldBase.tsx @@ -14,13 +14,14 @@ import { Icon, IconButton, maybeTooltip } from "src/components"; import { HelperText } from "src/components/HelperText"; import { InlineLabel, Label } from "src/components/Label"; import { usePresentationContext } from "src/components/PresentationContext"; -import { Css, Only, Palette, px } from "src/Css"; +import { Css, Only, Palette } from "src/Css"; import { getLabelSuffix } from "src/forms/labelUtils"; import { useGetRef } from "src/hooks/useGetRef"; import { ErrorMessage } from "src/inputs/ErrorMessage"; import { BeamTextFieldProps, TextFieldInternalProps, TextFieldXss } from "src/interfaces"; import { defaultTestId } from "src/utils/defaultTestId"; import { useTestIds } from "src/utils/useTestIds"; +import { getFieldWidth } from "src/inputs/utils"; export interface TextFieldBaseProps extends Pick< @@ -37,6 +38,7 @@ export interface TextFieldBaseProps | "compact" | "borderless" | "visuallyDisabled" + | "fullWidth" | "xss" >, Partial, "onChange">> { @@ -89,6 +91,7 @@ export function TextFieldBase>(props: TextFieldB errorInTooltip = fieldProps?.errorInTooltip ?? false, hideErrorMessage = false, alwaysShowHelperText = false, + fullWidth = fieldProps?.fullWidth ?? false, } = props; const typeScale = fieldProps?.typeScale ?? (inputProps.readOnly && labelStyle !== "hidden" ? "smMd" : "sm"); @@ -113,8 +116,10 @@ export function TextFieldBase>(props: TextFieldB ? [Palette.Gray100, Palette.Gray200, Palette.Gray200] : [Palette.White, Palette.Gray100, Palette.Gray100]; + const fieldMaxWidth = getFieldWidth(fullWidth); + const fieldStyles = { - container: Css.df.fdc.w100.maxw(px(550)).relative.if(labelStyle === "left").maxw100.fdr.gap2.jcsb.aic.$, + container: Css.df.fdc.w100.maxw(fieldMaxWidth).relative.if(labelStyle === "left").maxw100.fdr.gap2.jcsb.aic.$, inputWrapper: { ...Css[typeScale].df.aic.br4.px1.w100 .hPx(fieldHeight - maybeSmaller) diff --git a/src/inputs/TreeSelectField/TreeSelectField.stories.tsx b/src/inputs/TreeSelectField/TreeSelectField.stories.tsx index 9d6efbbf5..84d00691b 100644 --- a/src/inputs/TreeSelectField/TreeSelectField.stories.tsx +++ b/src/inputs/TreeSelectField/TreeSelectField.stories.tsx @@ -119,6 +119,17 @@ function Template(args: TreeSelectFieldProps) { placeholder="Hidden label" /> + +
+ +
diff --git a/src/inputs/TreeSelectField/TreeSelectField.tsx b/src/inputs/TreeSelectField/TreeSelectField.tsx index 0139a7cbd..03df3e6ed 100644 --- a/src/inputs/TreeSelectField/TreeSelectField.tsx +++ b/src/inputs/TreeSelectField/TreeSelectField.tsx @@ -14,8 +14,8 @@ import { useButton, useComboBox, useFilter, useOverlayPosition } from "react-ari import { Item, useComboBoxState, useMultipleSelectionState } from "react-stately"; import { resolveTooltip } from "src/components"; import { Popover } from "src/components/internal"; -import { PresentationFieldProps } from "src/components/PresentationContext"; -import { Css, px } from "src/Css"; +import { PresentationFieldProps, usePresentationContext } from "src/components/PresentationContext"; +import { Css } from "src/Css"; import { Value } from "src/inputs/index"; import { ComboBoxInput } from "src/inputs/internal/ComboBoxInput"; import { ListBox } from "src/inputs/internal/ListBox"; @@ -31,6 +31,7 @@ import { import { keyToValue, valueToKey } from "src/inputs/Value"; import { BeamFocusableProps } from "src/interfaces"; import { HasIdAndName, Optional } from "src/types"; +import { getFieldWidth } from "src/inputs/utils"; export interface TreeSelectFieldProps extends BeamFocusableProps, PresentationFieldProps { /** Renders `opt` in the dropdown menu, defaults to the `getOptionLabel` prop. `isUnsetOpt` is only defined for single SelectField */ @@ -127,6 +128,7 @@ export const CollapsedContext = React.createContext(props: TreeSelectFieldProps) { + const { fieldProps } = usePresentationContext(); const { values, options, @@ -142,6 +144,7 @@ function TreeSelectFieldBase(props: TreeSelectFieldProps(props: TreeSelectFieldProps +
extends BeamFocusableProps, PresentationFieldProps { @@ -88,6 +89,7 @@ export function ComboBoxBase(props: ComboBoxBaseProps) getOptionLabel: propOptionLabel, getOptionValue: propOptionValue, getOptionMenuLabel: propOptionMenuLabel, + fullWidth = fieldProps?.fullWidth ?? false, ...otherProps } = props; const labelStyle = otherProps.labelStyle ?? fieldProps?.labelStyle ?? "above"; @@ -320,10 +322,13 @@ export function ComboBoxBase(props: ComboBoxBaseProps) minWidth: 200, }; + const fieldMaxWidth = getFieldWidth(fullWidth); + return ( -
+
Date: Thu, 7 Dec 2023 02:46:14 +0000 Subject: [PATCH 13/25] chore(release): 2.327.0 [skip ci] ## [2.327.0](https://github.com/homebound-team/beam/compare/v2.326.1...v2.327.0) (2023-12-07) ### Features * Support 'fullWidth' prop on all text fields ([#979](https://github.com/homebound-team/beam/issues/979)) ([6bf26ac](https://github.com/homebound-team/beam/commit/6bf26ac11dadd2f38f67d3c7c49555e96deb0e18)) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 064f0c8e0..9969539d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@homebound/beam", - "version": "2.326.1", + "version": "2.327.0", "author": "Homebound", "license": "MIT", "main": "dist/index.js", From 671e918c43d15625485232ccb301736aa4a02979 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 7 Dec 2023 09:20:44 -0500 Subject: [PATCH 14/25] fix: FormLines to default to full width inside Modals (#980) Default to 'full' width for FormLines when inside of a modal. This prevents the FormLines from forcing a horizontal scroll bar when at the 'lg' size (width: 550px'). --- src/forms/FormLines.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/forms/FormLines.tsx b/src/forms/FormLines.tsx index c525390b7..9ad4fe097 100644 --- a/src/forms/FormLines.tsx +++ b/src/forms/FormLines.tsx @@ -1,6 +1,7 @@ import { Children, cloneElement, ReactNode } from "react"; import { LabelSuffixStyle, PresentationProvider } from "src/components/PresentationContext"; import { Css } from "src/Css"; +import { useModal } from "src/components"; export type FormWidth = /** 320px. */ @@ -28,7 +29,8 @@ export interface FormLinesProps { * (see the `FieldGroup` component), where they will be laid out side-by-side. */ export function FormLines(props: FormLinesProps) { - const { children, width = "lg", labelSuffix, labelStyle, compact } = props; + const { inModal } = useModal(); + const { children, width = inModal ? "full" : "lg", labelSuffix, labelStyle, compact } = props; let firstFormHeading = true; // Only overwrite `fieldProps` if new values are explicitly set. Ensures we only set to `undefined` if explicitly set. From 79a9a434c1fb5c3ecd3b5e7ba120493734dea8e8 Mon Sep 17 00:00:00 2001 From: Homebound Bot Date: Thu, 7 Dec 2023 14:23:37 +0000 Subject: [PATCH 15/25] chore(release): 2.327.1 [skip ci] ## [2.327.1](https://github.com/homebound-team/beam/compare/v2.327.0...v2.327.1) (2023-12-07) ### Bug Fixes * FormLines to default to full width inside Modals ([#980](https://github.com/homebound-team/beam/issues/980)) ([671e918](https://github.com/homebound-team/beam/commit/671e918c43d15625485232ccb301736aa4a02979)) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9969539d6..3c9334e4e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@homebound/beam", - "version": "2.327.0", + "version": "2.327.1", "author": "Homebound", "license": "MIT", "main": "dist/index.js", From 41dff2a36f02a393d09117dd75f519ceb7fcdfcd Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 14 Dec 2023 09:14:55 -0500 Subject: [PATCH 16/25] feat: Allow Tag to support custom bg and text color via xss (#981) --- src/components/Tag.stories.tsx | 1 + src/components/Tag.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Tag.stories.tsx b/src/components/Tag.stories.tsx index 23c62fe65..21f292a3e 100644 --- a/src/components/Tag.stories.tsx +++ b/src/components/Tag.stories.tsx @@ -23,6 +23,7 @@ export function NoIcon() {
+
); } diff --git a/src/components/Tag.tsx b/src/components/Tag.tsx index 30298c295..92ab0bce3 100644 --- a/src/components/Tag.tsx +++ b/src/components/Tag.tsx @@ -2,7 +2,7 @@ import { Icon, IconKey } from "src/components"; import { Css, Margin, Only, Xss } from "src/Css"; import { useTestIds } from "src/utils"; -type TagXss = Margin; +type TagXss = Margin | "backgroundColor" | "color"; export type TagType = "info" | "caution" | "warning" | "success" | "neutral"; interface TagProps { text: string; From 2e6738f55debfc2d0d8d8a1257c5e86922a1dec9 Mon Sep 17 00:00:00 2001 From: Homebound Bot Date: Thu, 14 Dec 2023 14:17:45 +0000 Subject: [PATCH 17/25] chore(release): 2.328.0 [skip ci] ## [2.328.0](https://github.com/homebound-team/beam/compare/v2.327.1...v2.328.0) (2023-12-14) ### Features * Allow Tag to support custom bg and text color via xss ([#981](https://github.com/homebound-team/beam/issues/981)) ([41dff2a](https://github.com/homebound-team/beam/commit/41dff2a36f02a393d09117dd75f519ceb7fcdfcd)) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3c9334e4e..2c1b2748a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@homebound/beam", - "version": "2.327.1", + "version": "2.328.0", "author": "Homebound", "license": "MIT", "main": "dist/index.js", From 8bc5dc0719c3d827808e8ca4bcaabbdd18ca870f Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 14 Dec 2023 11:15:43 -0500 Subject: [PATCH 18/25] feat: Update Switch component styling (#982) Adds new 'labelStyle: "centered"'. This will stack the label on top of the switch and center both elements in the column. Updates how tooltips are rendered for Switches. Now includes an icon to denote there is a tooltip, rather than requiring the user to discover it by hovering over it. Adds this tooltip functionality to the Label component, though it is only currently used by Switch. At some point it would be good to move all Tooltips to this sort of UX. --- src/components/Label.tsx | 35 ++++++++--- src/inputs/Switch.stories.tsx | 8 +++ src/inputs/Switch.tsx | 115 ++++++++++++++++------------------ 3 files changed, 90 insertions(+), 68 deletions(-) diff --git a/src/components/Label.tsx b/src/components/Label.tsx index 84e817edc..1f96b1ea2 100644 --- a/src/components/Label.tsx +++ b/src/components/Label.tsx @@ -1,8 +1,11 @@ -import React, { LabelHTMLAttributes } from "react"; +import React, { LabelHTMLAttributes, ReactNode } from "react"; import { VisuallyHidden } from "react-aria"; -import { Css } from "src/Css"; +import { Css, Font, Only, Palette, Xss } from "src/Css"; +import { Icon } from "src"; -interface LabelProps { +type LabelXss = Font | "color"; + +interface LabelProps { // We don't usually have `fooProps`-style props, but this is for/from react-aria labelProps?: LabelHTMLAttributes; label: string; @@ -11,22 +14,38 @@ interface LabelProps { hidden?: boolean; contrast?: boolean; multiline?: boolean; + tooltip?: ReactNode; + // Removes margin bottom if true - This is different from InlineLabel. InlineLabel expects to be rendered visually within the field element. Rather just on the same line. + inline?: boolean; + xss?: X; } /** An internal helper component for rendering form labels. */ -export const Label = React.memo((props: LabelProps) => { - const { labelProps, label, hidden, suffix, contrast = false, ...others } = props; +function LabelComponent, X>>(props: LabelProps) { + const { labelProps, label, hidden, suffix, contrast = false, tooltip, inline, xss, ...others } = props; const labelEl = ( -
); }; diff --git a/src/inputs/Switch.tsx b/src/inputs/Switch.tsx index 17a78e661..3450392a9 100644 --- a/src/inputs/Switch.tsx +++ b/src/inputs/Switch.tsx @@ -1,6 +1,6 @@ import { ReactNode, useRef } from "react"; import { useFocusRing, useHover, useSwitch, VisuallyHidden } from "react-aria"; -import { maybeTooltip, resolveTooltip } from "src/components"; +import { resolveTooltip } from "src/components"; import { Label } from "src/components/Label"; import { Css, Palette } from "src/Css"; import { Icon } from "../components/Icon"; @@ -16,7 +16,7 @@ export interface SwitchProps { /** Input label */ label: string; /** Where to put the label. */ - labelStyle?: "form" | "inline" | "filter" | "hidden" | "left"; // TODO: Update `labelStyle` to make consistent with other `labelStyle` properties in the library + labelStyle?: "form" | "inline" | "filter" | "hidden" | "left" | "centered"; // TODO: Update `labelStyle` to make consistent with other `labelStyle` properties in the library /** Whether or not to hide the label */ hideLabel?: boolean; /** Handler when the interactive element state changes. */ @@ -25,6 +25,8 @@ export interface SwitchProps { selected: boolean; /** Whether to include icons like the check mark */ withIcon?: boolean; + /** Adds tooltip for the switch */ + tooltip?: ReactNode; } export function Switch(props: SwitchProps) { @@ -46,74 +48,67 @@ export function Switch(props: SwitchProps) { const { inputProps } = useSwitch({ ...ariaProps, "aria-label": label }, state, ref); const { isFocusVisible: isKeyboardFocus, focusProps } = useFocusRing(otherProps); const { hoverProps, isHovered } = useHover(ariaProps); - const tooltip = resolveTooltip(disabled); + const tooltip = resolveTooltip(disabled, props.tooltip); - return maybeTooltip({ - title: tooltip, - placement: "top", - children: ( - - ), - }); + {labelStyle === "inline" && ( +
+ ); } /** Styles */ From 3da849d9e6bd063c87fe221140721e015d2340ef Mon Sep 17 00:00:00 2001 From: Homebound Bot Date: Thu, 14 Dec 2023 16:18:37 +0000 Subject: [PATCH 19/25] chore(release): 2.329.0 [skip ci] ## [2.329.0](https://github.com/homebound-team/beam/compare/v2.328.0...v2.329.0) (2023-12-14) ### Features * Update Switch component styling ([#982](https://github.com/homebound-team/beam/issues/982)) ([8bc5dc0](https://github.com/homebound-team/beam/commit/8bc5dc0719c3d827808e8ca4bcaabbdd18ca870f)) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2c1b2748a..2b5138009 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@homebound/beam", - "version": "2.328.0", + "version": "2.329.0", "author": "Homebound", "license": "MIT", "main": "dist/index.js", From ee42c576b4c74b1213adc853f25c538ddc3ecc0e Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 14 Dec 2023 11:50:43 -0500 Subject: [PATCH 20/25] feat: Update GridTable's cardStyle (#983) Adds 'GridStyle.nonHeaderRowCss' to directly apply styles to non-header row elements. Adds 'GridStyle.rowHoverCss' to apply hover styles to non-header row elements Updates cardStyle to take advantage of the new styles to provide a box-shadow and border color change when hovering over a row. Updates cardStyle to apply more styles directly on the row element rather than on the individual cells --- src/components/Table/GridTable.tsx | 6 +++--- src/components/Table/TableStyles.tsx | 20 +++++++++++--------- src/components/Table/components/Row.tsx | 8 +++++++- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/components/Table/GridTable.tsx b/src/components/Table/GridTable.tsx index e8eac1f14..4d6cb300b 100644 --- a/src/components/Table/GridTable.tsx +++ b/src/components/Table/GridTable.tsx @@ -437,7 +437,7 @@ function renderDiv(
div > *`, style.betweenRowsCss).$ : {}), - ...(style.firstNonHeaderRowCss ? Css.addIn(`& > div:first-of-type > *`, style.firstNonHeaderRowCss).$ : {}), + ...(style.firstNonHeaderRowCss ? Css.addIn(`& > div:first-of-type`, style.firstNonHeaderRowCss).$ : {}), ...(style.lastRowCss && Css.addIn("& > div:last-of-type", style.lastRowCss).$), }} > @@ -475,7 +475,7 @@ function renderTable( ...Css.w100.add("borderCollapse", "separate").add("borderSpacing", "0").$, ...Css.addIn("& > tbody > tr > * ", style.betweenRowsCss || {}) // removes border between header and second row - .addIn("& > tbody > tr:first-of-type > *", style.firstNonHeaderRowCss || {}).$, + .addIn("& > tbody > tr:first-of-type", style.firstNonHeaderRowCss || {}).$, ...Css.addIn("& > tbody > tr:last-of-type", style.lastRowCss).$, ...Css.addIn("& > thead > tr:first-of-type", style.firstRowCss).$, ...style.rootCss, @@ -669,7 +669,7 @@ const VirtualRoot = memoizeOne<(gs: GridStyle, columns: GridColumn[], id: s ...(isHeader ? Css.addIn("& > div:first-of-type > *", gs.firstRowCss).$ : { - ...Css.addIn("& > div:first-of-type > *", gs.firstNonHeaderRowCss).$, + ...Css.addIn("& > div:first-of-type", gs.firstNonHeaderRowCss).$, ...Css.addIn("& > div:last-of-type > *", gs.lastRowCss).$, }), ...gs.rootCss, diff --git a/src/components/Table/TableStyles.tsx b/src/components/Table/TableStyles.tsx index 089ca39fb..97bfcd283 100644 --- a/src/components/Table/TableStyles.tsx +++ b/src/components/Table/TableStyles.tsx @@ -15,6 +15,8 @@ export interface GridStyle { lastRowCss?: Properties; /** Applied on the first row of the table (could be the Header or Totals row). */ firstRowCss?: Properties; + /** Applied to every non-header row of the table */ + nonHeaderRowCss?: Properties; /** Applied to the first non-header row, i.e. if you want to cancel out `betweenRowsCss`. */ firstNonHeaderRowCss?: Properties; /** Applied to all cell divs (via a selector off the base div). */ @@ -39,7 +41,9 @@ export interface GridStyle { /** Applied if there is a fallback/overflow message showing. */ firstRowMessageCss?: Properties; /** Applied on hover if a row has a rowLink/onClick set. */ - rowHoverColor?: Palette; + rowHoverColor?: Palette | "none"; + /** Applied on hover of a row */ + nonHeaderRowHoverCss?: Properties; /** Default content to put into an empty cell */ emptyCell?: ReactNode; presentationSettings?: Pick & @@ -209,18 +213,16 @@ export const condensedStyle: GridStyle = { export const cardStyle: GridStyle = { ...defaultStyle, betweenRowsCss: {}, - firstNonHeaderRowCss: Css.mt2.$, - cellCss: Css.p2.my1.bt.bb.bGray400.$, - firstCellCss: Css.bl.add({ borderTopLeftRadius: "4px", borderBottomLeftRadius: "4px" }).$, - lastCellCss: Css.br.add({ borderTopRightRadius: "4px", borderBottomRightRadius: "4px" }).$, + nonHeaderRowCss: Css.br4.overflowHidden.ba.bGray400.mt2.add("transition", "all 240ms").$, + firstRowCss: Css.bl.br.bGray200.borderRadius("8px 8px 0 0").overflowHidden.$, + cellCss: Css.p2.$, // Undo the card look & feel for the header headerCellCss: { ...defaultStyle.headerCellCss, - ...Css.add({ - border: "none", - borderRadius: "unset", - }).p1.m0.xsMd.gray700.$, + ...Css.p1.m0.xsMd.gray700.$, }, + rowHoverColor: "none", + nonHeaderRowHoverCss: Css.bshHover.bGray700.$, }; export function resolveStyles(style: GridStyle | GridStyleDef): GridStyle { diff --git a/src/components/Table/components/Row.tsx b/src/components/Table/components/Row.tsx index 96d74e3a3..95da77496 100644 --- a/src/components/Table/components/Row.tsx +++ b/src/components/Table/components/Row.tsx @@ -75,15 +75,21 @@ function RowImpl(props: RowProps): ReactElement { const revealOnRowHoverClass = "revealOnRowHover"; - const showRowHoverColor = !reservedRowKinds.includes(row.kind) && !omitRowHover; + const showRowHoverColor = !reservedRowKinds.includes(row.kind) && !omitRowHover && style.rowHoverColor !== "none"; const rowStyleCellCss = maybeApplyFunction(row as any, rowStyle?.cellCss); const rowCss = { + ...(!reservedRowKinds.includes(row.kind) && style.nonHeaderRowCss), // Optionally include the row hover styles, by default they should be turned on. ...(showRowHoverColor && { // Even though backgroundColor is set on the cellCss, the hover target is the row. "&:hover > *": Css.bgColor(style.rowHoverColor ?? Palette.Blue100).$, }), + ...(!reservedRowKinds.includes(row.kind) && + style.nonHeaderRowHoverCss && { + // Need to spread this to make TS happy. + ":hover": { ...style.nonHeaderRowHoverCss }, + }), // For virtual tables use `display: flex` to keep all cells on the same row. For each cell in the row use `flexNone` to ensure they stay their defined widths ...(as === "table" ? {} : Css.relative.df.fg1.fs1.addIn("&>*", Css.flexNone.$).$), // Apply `cursorPointer` to the row if it has a link or `onClick` value. From 70105e78faeb9220ba5c11316d728996a3a90099 Mon Sep 17 00:00:00 2001 From: Homebound Bot Date: Thu, 14 Dec 2023 16:53:25 +0000 Subject: [PATCH 21/25] chore(release): 2.330.0 [skip ci] ## [2.330.0](https://github.com/homebound-team/beam/compare/v2.329.0...v2.330.0) (2023-12-14) ### Features * Update GridTable's cardStyle ([#983](https://github.com/homebound-team/beam/issues/983)) ([ee42c57](https://github.com/homebound-team/beam/commit/ee42c576b4c74b1213adc853f25c538ddc3ecc0e)) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2b5138009..fe4119133 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@homebound/beam", - "version": "2.329.0", + "version": "2.330.0", "author": "Homebound", "license": "MIT", "main": "dist/index.js", From 05b34f6033d93a6023f422edefded84433091797 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 14 Dec 2023 13:10:26 -0500 Subject: [PATCH 22/25] feat: Support indentation of rows (#984) --- src/components/Table/GridTable.stories.tsx | 27 ++++++++++++++++++++++ src/components/Table/TableStyles.tsx | 14 ++++++++++- src/components/Table/components/Row.tsx | 8 ++++++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/components/Table/GridTable.stories.tsx b/src/components/Table/GridTable.stories.tsx index dcf020e31..e4a561509 100644 --- a/src/components/Table/GridTable.stories.tsx +++ b/src/components/Table/GridTable.stories.tsx @@ -517,6 +517,33 @@ export const StyleCard = newStory(() => { ); }, {}); +export const LeveledStyleCard = newStory(() => { + const nameColumn: GridColumn = { + header: () => "Name", + parent: (row) => row.name, + child: (row) => row.name, + grandChild: (row) => row.name, + add: () => "Add", + }; + const valueColumn: GridColumn = { + header: () => "Value", + parent: (row) => row.name, + child: (row) => row.name, + grandChild: (row) => row.name, + add: () => "Add", + w: "200px", + }; + return ( +
+ (), selectColumn(), nameColumn, valueColumn]} + rows={rowsWithHeader} + /> +
+ ); +}, {}); + export const StyleCardWithOneColumn = newStory(() => { const nameColumn: GridColumn = { header: "Name", data: ({ name }) => name }; return ( diff --git a/src/components/Table/TableStyles.tsx b/src/components/Table/TableStyles.tsx index 97bfcd283..cf973f236 100644 --- a/src/components/Table/TableStyles.tsx +++ b/src/components/Table/TableStyles.tsx @@ -51,7 +51,15 @@ export interface GridStyle { /** Minimum table width in pixels. Used when calculating columns sizes */ minWidthPx?: number; /** Css to apply at each level of a parent/child nested table. */ - levels?: Record; + levels?: Record< + number, + { + /** Number of pixels to indent the row. This value will be subtracted from the "first content column" width. First content column is the first column that is not an 'action' column (i.e. non-checkbox or non-collapse button column) */ + rowIndent?: number; + cellCss?: Properties; + firstContentColumn?: Properties; + } + >; /** Allows for customization of the background color used to denote an "active" row */ activeBgColor?: Palette; /** Defines styles for the group row which holds the selected rows that have been filtered out */ @@ -223,6 +231,10 @@ export const cardStyle: GridStyle = { }, rowHoverColor: "none", nonHeaderRowHoverCss: Css.bshHover.bGray700.$, + levels: { + 1: { rowIndent: 24 }, + 2: { rowIndent: 48 }, + }, }; export function resolveStyles(style: GridStyle | GridStyleDef): GridStyle { diff --git a/src/components/Table/components/Row.tsx b/src/components/Table/components/Row.tsx index 95da77496..771e7c256 100644 --- a/src/components/Table/components/Row.tsx +++ b/src/components/Table/components/Row.tsx @@ -78,6 +78,8 @@ function RowImpl(props: RowProps): ReactElement { const showRowHoverColor = !reservedRowKinds.includes(row.kind) && !omitRowHover && style.rowHoverColor !== "none"; const rowStyleCellCss = maybeApplyFunction(row as any, rowStyle?.cellCss); + const levelIndent = style.levels && style.levels[level]?.rowIndent; + const rowCss = { ...(!reservedRowKinds.includes(row.kind) && style.nonHeaderRowCss), // Optionally include the row hover styles, by default they should be turned on. @@ -90,6 +92,7 @@ function RowImpl(props: RowProps): ReactElement { // Need to spread this to make TS happy. ":hover": { ...style.nonHeaderRowHoverCss }, }), + ...(levelIndent && Css.mlPx(levelIndent).$), // For virtual tables use `display: flex` to keep all cells on the same row. For each cell in the row use `flexNone` to ensure they stay their defined widths ...(as === "table" ? {} : Css.relative.df.fg1.fs1.addIn("&>*", Css.flexNone.$).$), // Apply `cursorPointer` to the row if it has a link or `onClick` value. @@ -289,7 +292,10 @@ function RowImpl(props: RowProps): ReactElement { // Apply cell highlight styles to active cell and hover ...Css.if(applyCellHighlight && isCellActive).br4.boxShadow(`inset 0 0 0 1px ${Palette.Blue700}`).$, // Define the width of the column on each cell. Supports col spans. - width: `calc(${columnSizes.slice(columnIndex, columnIndex + currentColspan).join(" + ")})`, + // If we have a 'levelIndent' defined, then subtract that amount from the first content column's width to ensure all columns will still line up properly + width: `calc(${columnSizes.slice(columnIndex, columnIndex + currentColspan).join(" + ")}${ + applyFirstContentColumnStyles && levelIndent ? ` - ${levelIndent}px` : "" + })`, ...(typeof column.mw === "string" ? Css.mw(column.mw).$ : {}), }; From c2e1bcd115b9f037d8318dba8d2b4333c59e8244 Mon Sep 17 00:00:00 2001 From: Homebound Bot Date: Thu, 14 Dec 2023 18:13:20 +0000 Subject: [PATCH 23/25] chore(release): 2.331.0 [skip ci] ## [2.331.0](https://github.com/homebound-team/beam/compare/v2.330.0...v2.331.0) (2023-12-14) ### Features * Support indentation of rows ([#984](https://github.com/homebound-team/beam/issues/984)) ([05b34f6](https://github.com/homebound-team/beam/commit/05b34f6033d93a6023f422edefded84433091797)) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fe4119133..9b64afdfa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@homebound/beam", - "version": "2.330.0", + "version": "2.331.0", "author": "Homebound", "license": "MIT", "main": "dist/index.js", From e2a06bd6022663fc4b4c858b390de551ce483093 Mon Sep 17 00:00:00 2001 From: Stephen Haberman Date: Fri, 15 Dec 2023 10:03:01 -0600 Subject: [PATCH 24/25] fix: Fix Switch not toggling. (#985) --- src/inputs/Switch.stories.tsx | 1 - src/inputs/Switch.test.tsx | 41 +++++++++++++++++++++++++++++++++++ src/inputs/Switch.tsx | 36 ++++++++++++++++-------------- 3 files changed, 61 insertions(+), 17 deletions(-) create mode 100644 src/inputs/Switch.test.tsx diff --git a/src/inputs/Switch.stories.tsx b/src/inputs/Switch.stories.tsx index 1567372a8..c4cb5e3b7 100644 --- a/src/inputs/Switch.stories.tsx +++ b/src/inputs/Switch.stories.tsx @@ -123,7 +123,6 @@ type SwitchWrapperProps = Omit & function SwitchWrapper({ isHovered, isFocused, ...props }: SwitchWrapperProps) { const [selected, setSelected] = useState(props.selected || false); - return (
{ + it("can change", async () => { + const onChange = jest.fn(); + // Given a switch + const r = await render(); + // Then it defaults no checked + expect(r.age).not.toBeChecked(); + // And when we click it, it flips to checked + click(r.age); + expect(r.age).toBeChecked(); + expect(onChange).toHaveBeenCalledTimes(1); + // And if we click it again, it flips back to unchecked + click(r.age); + expect(r.age).not.toBeChecked(); + expect(onChange).toHaveBeenCalledTimes(2); + }); +}); + +type SwitchTestProps = Omit & { + onChange?: (value: boolean) => void; + selected?: boolean; +}; + +function SwitchTest({ selected: initSelected, onChange: _onChange, ...props }: SwitchTestProps) { + const [selected, setSelected] = useState(initSelected || false); + return ( + { + _onChange?.(value); + setSelected(value); + }} + {...props} + /> + ); +} diff --git a/src/inputs/Switch.tsx b/src/inputs/Switch.tsx index 3450392a9..96e482ca9 100644 --- a/src/inputs/Switch.tsx +++ b/src/inputs/Switch.tsx @@ -4,7 +4,7 @@ import { resolveTooltip } from "src/components"; import { Label } from "src/components/Label"; import { Css, Palette } from "src/Css"; import { Icon } from "../components/Icon"; -import { toToggleState } from "../utils"; +import { toToggleState, useTestIds } from "../utils"; export interface SwitchProps { /** Whether the element should receive focus on render. */ @@ -17,7 +17,7 @@ export interface SwitchProps { label: string; /** Where to put the label. */ labelStyle?: "form" | "inline" | "filter" | "hidden" | "left" | "centered"; // TODO: Update `labelStyle` to make consistent with other `labelStyle` properties in the library - /** Whether or not to hide the label */ + /** Whether to hide the label */ hideLabel?: boolean; /** Handler when the interactive element state changes. */ onChange: (value: boolean) => void; @@ -49,12 +49,13 @@ export function Switch(props: SwitchProps) { const { isFocusVisible: isKeyboardFocus, focusProps } = useFocusRing(otherProps); const { hoverProps, isHovered } = useHover(ariaProps); const tooltip = resolveTooltip(disabled, props.tooltip); + const tid = useTestIds(otherProps, label); return ( -
)} - + -
+ ); } @@ -122,22 +123,25 @@ export const switchFocusStyles = Css.bshFocus.$; export const switchSelectedHoverStyles = Css.bgBlue900.$; // Circle inside Switcher/Toggle element styles -const switchCircleDefaultStyles = (isCompact: boolean) => ({ - ...Css.wPx(circleDiameter(isCompact)) - .hPx(circleDiameter(isCompact)) - .br100.bgWhite.bshBasic.absolute.leftPx(2) - .topPx(2).transition.df.aic.jcc.$, - svg: Css.hPx(toggleHeight(isCompact) / 2).wPx(toggleHeight(isCompact) / 2).$, -}); +function switchCircleDefaultStyles(isCompact: boolean) { + return { + ...Css.wPx(circleDiameter(isCompact)) + .hPx(circleDiameter(isCompact)) + .br100.bgWhite.bshBasic.absolute.leftPx(2) + .topPx(2).transition.df.aic.jcc.$, + svg: Css.hPx(toggleHeight(isCompact) / 2).wPx(toggleHeight(isCompact) / 2).$, + }; +} /** * Affecting the `left` property due to transitions only working when there is * a previous value to work from. * - * Calculation is as follow: + * Calculation is as follows: * - `100%` is the toggle width * - `${circleDiameter(isCompact)}px` is the circle diameter * - `2px` is to keep 2px edge spacing. */ -const switchCircleSelectedStyles = (isCompact: boolean) => - Css.left(`calc(100% - ${circleDiameter(isCompact)}px - 2px);`).$; +function switchCircleSelectedStyles(isCompact: boolean) { + return Css.left(`calc(100% - ${circleDiameter(isCompact)}px - 2px);`).$; +} From 9106443c00a69ff69dae1c5ccb40dd1458a758ba Mon Sep 17 00:00:00 2001 From: Homebound Bot Date: Fri, 15 Dec 2023 16:05:56 +0000 Subject: [PATCH 25/25] chore(release): 2.331.1 [skip ci] ## [2.331.1](https://github.com/homebound-team/beam/compare/v2.331.0...v2.331.1) (2023-12-15) ### Bug Fixes * Fix Switch not toggling. ([#985](https://github.com/homebound-team/beam/issues/985)) ([e2a06bd](https://github.com/homebound-team/beam/commit/e2a06bd6022663fc4b4c858b390de551ce483093)) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b64afdfa..15c0d4799 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@homebound/beam", - "version": "2.331.0", + "version": "2.331.1", "author": "Homebound", "license": "MIT", "main": "dist/index.js",