From a8049c4fe8875702f5e2d9eb7ce02b009a4a6351 Mon Sep 17 00:00:00 2001 From: Jeremy <168515712+jeremy-babylonlabs@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:48:48 +0800 Subject: [PATCH] add form control (#59) --- .changeset/famous-dancers-raise.md | 5 ++ src/components/Form/Input.css | 16 ---- src/components/Form/Input.stories.tsx | 4 +- src/components/Form/Input.tsx | 16 ++-- src/components/Form/Select.css | 12 +++ src/components/Form/Select.stories.tsx | 13 +++ src/components/Form/Select.tsx | 81 +++++++++++-------- .../Form/components/FormControl.css | 19 +++++ .../Form/components/FormControl.tsx | 30 +++++++ src/components/Table/Table.stories.tsx | 62 ++++++++++++++ src/components/Table/Table.tsx | 5 +- 11 files changed, 201 insertions(+), 62 deletions(-) create mode 100644 .changeset/famous-dancers-raise.md create mode 100644 src/components/Form/components/FormControl.css create mode 100644 src/components/Form/components/FormControl.tsx diff --git a/.changeset/famous-dancers-raise.md b/.changeset/famous-dancers-raise.md new file mode 100644 index 0000000..bc25439 --- /dev/null +++ b/.changeset/famous-dancers-raise.md @@ -0,0 +1,5 @@ +--- +"@babylonlabs-io/bbn-core-ui": minor +--- + +add form control component \ No newline at end of file diff --git a/src/components/Form/Input.css b/src/components/Form/Input.css index 0b540dd..dcd2f13 100644 --- a/src/components/Form/Input.css +++ b/src/components/Form/Input.css @@ -36,20 +36,4 @@ &-prefix { @apply mr-2 flex items-center text-primary-light/50; } - - &-state-text { - @apply mt-1 text-sm; - } - - &-state-text-error { - @apply text-error-main; - } - - &-state-text-warning { - @apply text-warning-main; - } - - &-state-text-success { - @apply text-success-main; - } } \ No newline at end of file diff --git a/src/components/Form/Input.stories.tsx b/src/components/Form/Input.stories.tsx index 98f26bd..946a4c5 100644 --- a/src/components/Form/Input.stories.tsx +++ b/src/components/Form/Input.stories.tsx @@ -28,11 +28,11 @@ export const Disabled: Story = { }, }; -export const Error: Story = { +export const WithError: Story = { args: { placeholder: "Input with error", state: "error", - stateText: "This field is required", + hint: "This field is required", }, }; diff --git a/src/components/Form/Input.tsx b/src/components/Form/Input.tsx index a1deabb..1391358 100644 --- a/src/components/Form/Input.tsx +++ b/src/components/Form/Input.tsx @@ -1,6 +1,7 @@ import { forwardRef, type DetailedHTMLProps, type InputHTMLAttributes, type ReactNode } from "react"; import { twJoin } from "tailwind-merge"; import "./Input.css"; +import { FormControl } from "./components/FormControl"; export interface InputProps extends Omit, HTMLInputElement>, "prefix" | "suffix"> { @@ -10,22 +11,23 @@ export interface InputProps suffix?: ReactNode; disabled?: boolean; state?: "default" | "error" | "warning"; - stateText?: string; + hint?: string; + label?: string; } export const Input = forwardRef( - ({ className, wrapperClassName, prefix, suffix, disabled = false, state = "default", stateText, ...props }, ref) => { + ( + { className, wrapperClassName, prefix, suffix, disabled = false, state = "default", hint, label, ...props }, + ref, + ) => { return ( -
+
{prefix &&
{prefix}
} {suffix &&
{suffix}
}
- {stateText && ( - {stateText} - )} -
+ ); }, ); diff --git a/src/components/Form/Select.css b/src/components/Form/Select.css index 8e840f0..a04b96c 100644 --- a/src/components/Form/Select.css +++ b/src/components/Form/Select.css @@ -33,4 +33,16 @@ &-disabled { @apply pointer-events-none cursor-not-allowed opacity-50; } + + &-error { + @apply border-error-main; + } + + &-warning { + @apply border-warning-main; + } + + &-success { + @apply border-success-main; + } } diff --git a/src/components/Form/Select.stories.tsx b/src/components/Form/Select.stories.tsx index ef60b6a..56830e0 100644 --- a/src/components/Form/Select.stories.tsx +++ b/src/components/Form/Select.stories.tsx @@ -85,3 +85,16 @@ export const CustomSelectedDisplay: Story = { renderSelectedOption: (option) => `Showing ${option.value}`, }, }; + +export const WithError: Story = { + args: { + options: [ + { value: "active", label: "Active" }, + { value: "inactive", label: "Inactive" }, + { value: "pending", label: "Pending" }, + ], + placeholder: "Select status", + state: "error", + hint: "", + }, +}; diff --git a/src/components/Form/Select.tsx b/src/components/Form/Select.tsx index f79d356..515073c 100644 --- a/src/components/Form/Select.tsx +++ b/src/components/Form/Select.tsx @@ -14,6 +14,7 @@ import { Popover } from "@/components/Popover"; import { useControlledState } from "@/hooks/useControlledState"; import "./Select.css"; import { useResizeObserver } from "@/hooks/useResizeObserver"; +import { FormControl } from "./components/FormControl"; export type Value = string | number; @@ -36,12 +37,16 @@ export interface SelectProps { className?: string; optionClassName?: string; popoverClassName?: string; + wrapperClassName?: string; + state?: "default" | "error" | "warning"; + hint?: string; onSelect?: (value: Value) => void; onOpen?: () => void; onClose?: () => void; onFocus?: () => void; onBlur?: () => void; renderSelectedOption?: (option: Option) => ReactNode; + label?: string; } const defaultOptionRenderer = (option: Option) => option.label; @@ -51,6 +56,7 @@ export const Select = forwardRef( { disabled, className, + wrapperClassName, value, defaultValue, placeholder = "Select option", @@ -63,6 +69,9 @@ export const Select = forwardRef( onSelect, onClose, renderSelectedOption = defaultOptionRenderer, + state = "default", + hint, + label, ...props }, ref, @@ -107,42 +116,44 @@ export const Select = forwardRef( }, [isOpen, disabled, setIsOpen]); return ( - <> -
- {selectedOption ? renderSelectedOption(selectedOption) : placeholder} - + +
+
+ {selectedOption ? renderSelectedOption(selectedOption) : placeholder} + +
+ + + {options.map((option) => ( +
handleSelect(option)} + > + {option.label} +
+ ))} +
- - - {options.map((option) => ( -
handleSelect(option)} - > - {option.label} -
- ))} -
- +
); }, ); diff --git a/src/components/Form/components/FormControl.css b/src/components/Form/components/FormControl.css new file mode 100644 index 0000000..f0a095f --- /dev/null +++ b/src/components/Form/components/FormControl.css @@ -0,0 +1,19 @@ +.bbn-form-control { + @apply flex flex-col; + + &-hint { + @apply mt-1 text-sm; + + &.bbn-form-control-hint-error { + @apply text-error-main; + } + + &.bbn-form-control-hint-warning { + @apply text-warning-main; + } + + &.bbn-form-control-hint-success { + @apply text-success-main; + } + } +} \ No newline at end of file diff --git a/src/components/Form/components/FormControl.tsx b/src/components/Form/components/FormControl.tsx new file mode 100644 index 0000000..6736700 --- /dev/null +++ b/src/components/Form/components/FormControl.tsx @@ -0,0 +1,30 @@ +import { type PropsWithChildren } from "react"; +import { twJoin } from "tailwind-merge"; +import "./FormControl.css"; + +export interface FormControlProps extends PropsWithChildren { + label?: string; + hint?: string; + state?: "default" | "error" | "warning" | "success"; + className?: string; + wrapperClassName?: string; +} + +export function FormControl({ + children, + label, + hint, + state = "default", + className, + wrapperClassName, +}: FormControlProps) { + return ( +
+ {label && } + +
{children}
+ + {hint && {hint}} +
+ ); +} diff --git a/src/components/Table/Table.stories.tsx b/src/components/Table/Table.stories.tsx index d9355d1..2e093bf 100644 --- a/src/components/Table/Table.stories.tsx +++ b/src/components/Table/Table.stories.tsx @@ -119,3 +119,65 @@ export const Default: Story = { ); }, }; + +export const WithoutRowSelect: Story = { + render: () => { + const [tableData, setTableData] = useState(data.slice(0, 3)); + const [loading, setLoading] = useState(false); + const [hasMore, setHasMore] = useState(true); + + const handleLoadMore = async () => { + setLoading(true); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const nextItems = data.slice(tableData.length, tableData.length + 3); + setTableData((prev) => [...prev, ...nextItems]); + setHasMore(tableData.length + nextItems.length < data.length); + setLoading(false); + }; + + return ( +
+ ( +
+ + {row.name} +
+ ), + sorter: (a, b) => a.name.localeCompare(b.name), + }, + { + key: "status", + header: "Status", + }, + { + key: "btcPk", + header: "BTC PK", + }, + { + key: "totalDelegation", + header: "Total Delegation", + render: (value) => `${value} sBTC`, + sorter: (a, b) => a.totalDelegation - b.totalDelegation, + }, + { + key: "commission", + header: "Commission", + render: (value) => `${value}%`, + sorter: (a, b) => a.commission - b.commission, + }, + ]} + /> + + ); + }, +}; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 2e9882c..456a37c 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -36,9 +36,10 @@ export function Table({ }; const handleRowSelect = (row: T) => { + if (!onRowSelect) return; if (selectedRow === row.id) return; setSelectedRow(row.id); - onRowSelect?.(row); + onRowSelect(row); }; const handleColumnSort = (columnKey: string, sorter?: (a: T, b: T) => number) => { @@ -127,7 +128,7 @@ export function Table({ {sortedData.map((row) => ( handleRowSelect(row)} > {columns.map((column) => (