From 3b4fa41840863188cd4b37029bf1a99111480680 Mon Sep 17 00:00:00 2001 From: Brian McGee Date: Tue, 1 Jul 2025 10:32:05 +0100 Subject: [PATCH] feat(ui): simplify form components Better pass through to the underlying Kobalte API without re-defining types. --- .../ui/src/components/v2/Divider/Divider.tsx | 3 +- .../ui/src/components/v2/Form/Checkbox.css | 50 ++++ .../components/v2/Form/Checkbox.stories.tsx | 63 +++-- .../ui/src/components/v2/Form/Checkbox.tsx | 44 ++++ .../ui/src/components/v2/Form/Field.css | 216 +----------------- .../ui/src/components/v2/Form/Field.tsx | 204 +---------------- .../components/v2/Form/Fieldset.stories.tsx | 111 ++++----- .../ui/src/components/v2/Form/Fieldset.tsx | 25 +- .../ui/src/components/v2/Form/Label.css | 24 ++ .../ui/src/components/v2/Form/Label.tsx | 85 +++++++ .../components/v2/Form/TextArea.stories.tsx | 70 ++++-- .../ui/src/components/v2/Form/TextArea.tsx | 34 +++ .../ui/src/components/v2/Form/TextInput.css | 136 +++++++++++ ...ield.stories.tsx => TextInput.stories.tsx} | 41 ++-- .../ui/src/components/v2/Form/TextInput.tsx | 47 ++++ pkgs/clan-app/ui/src/components/v2/shared.ts | 1 + 16 files changed, 611 insertions(+), 543 deletions(-) create mode 100644 pkgs/clan-app/ui/src/components/v2/Form/Checkbox.css create mode 100644 pkgs/clan-app/ui/src/components/v2/Form/Checkbox.tsx create mode 100644 pkgs/clan-app/ui/src/components/v2/Form/Label.css create mode 100644 pkgs/clan-app/ui/src/components/v2/Form/Label.tsx create mode 100644 pkgs/clan-app/ui/src/components/v2/Form/TextArea.tsx create mode 100644 pkgs/clan-app/ui/src/components/v2/Form/TextInput.css rename pkgs/clan-app/ui/src/components/v2/Form/{TextField.stories.tsx => TextInput.stories.tsx} (67%) create mode 100644 pkgs/clan-app/ui/src/components/v2/Form/TextInput.tsx create mode 100644 pkgs/clan-app/ui/src/components/v2/shared.ts diff --git a/pkgs/clan-app/ui/src/components/v2/Divider/Divider.tsx b/pkgs/clan-app/ui/src/components/v2/Divider/Divider.tsx index c25fa7c6d..d1cc9882b 100644 --- a/pkgs/clan-app/ui/src/components/v2/Divider/Divider.tsx +++ b/pkgs/clan-app/ui/src/components/v2/Divider/Divider.tsx @@ -1,7 +1,6 @@ import "./Divider.css"; import cx from "classnames"; - -export type Orientation = "horizontal" | "vertical"; +import { Orientation } from "@/src/components/v2/shared"; export interface DividerProps { inverted?: boolean; diff --git a/pkgs/clan-app/ui/src/components/v2/Form/Checkbox.css b/pkgs/clan-app/ui/src/components/v2/Form/Checkbox.css new file mode 100644 index 000000000..924068121 --- /dev/null +++ b/pkgs/clan-app/ui/src/components/v2/Form/Checkbox.css @@ -0,0 +1,50 @@ +@import "./Field.css"; + +div.form-field { + &.checkbox { + @apply items-start; + + & > div.checkbox-control { + @apply w-5 h-5 rounded-sm bg-def-1 border border-inv-1 p-[0.0625rem]; + + &:hover { + @apply bg-def-acc-2; + } + + &[data-disabled] { + @apply border-def-2; + } + + &[data-invalid] { + @apply border-semantic-error-4; + } + } + } + + &.horizontal.checkbox { + @apply items-center; + } + + &.inverted { + &.checkbox { + & > div.checkbox-control { + @apply bg-inv-1; + + &:hover, + &[data-checked] { + @apply bg-inv-acc-4; + } + + &[data-disabled] { + @apply bg-def-4 border-none; + } + } + } + } + + &.s { + & > div.checkbox-control { + @apply w-4 h-4; + } + } +} diff --git a/pkgs/clan-app/ui/src/components/v2/Form/Checkbox.stories.tsx b/pkgs/clan-app/ui/src/components/v2/Form/Checkbox.stories.tsx index 191c44e0c..9ce48ab7a 100644 --- a/pkgs/clan-app/ui/src/components/v2/Form/Checkbox.stories.tsx +++ b/pkgs/clan-app/ui/src/components/v2/Form/Checkbox.stories.tsx @@ -1,16 +1,54 @@ -import meta, { Story } from "./TextField.stories"; +import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid"; +import cx from "classnames"; +import { Checkbox, CheckboxProps } from "@/src/components/v2/Form/Checkbox"; -const checkboxMeta = { - ...meta, - title: "Components/Form/Fields/Checkbox", -}; +const Examples = (props: CheckboxProps) => ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+); -export default checkboxMeta; +const meta = { + title: "Components/Form/Checkbox", + component: Examples, + decorators: [ + (Story: StoryObj, context: StoryContext) => { + return ( +
+ +
+ ); + }, + ], +} satisfies Meta; + +export default meta; + +export type Story = StoryObj; export const Bare: Story = { - args: { - type: "checkbox", - }, + args: {}, }; export const Label: Story = { @@ -45,7 +83,7 @@ export const Tooltip: Story = { export const Invalid: Story = { args: { ...Tooltip.args, - invalid: true, + validationState: "invalid", }, }; @@ -60,9 +98,6 @@ export const ReadOnly: Story = { args: { ...Tooltip.args, readOnly: true, - checkbox: { - ...Tooltip.args.checkbox, - defaultChecked: true, - }, + defaultChecked: true, }, }; diff --git a/pkgs/clan-app/ui/src/components/v2/Form/Checkbox.tsx b/pkgs/clan-app/ui/src/components/v2/Form/Checkbox.tsx new file mode 100644 index 000000000..b7fa4ca8d --- /dev/null +++ b/pkgs/clan-app/ui/src/components/v2/Form/Checkbox.tsx @@ -0,0 +1,44 @@ +import { + Checkbox as KCheckbox, + CheckboxInputProps as KCheckboxInputProps, + CheckboxRootProps as KCheckboxRootProps, +} from "@kobalte/core/checkbox"; +import Icon from "@/src/components/v2/Icon/Icon"; + +import cx from "classnames"; +import { Label } from "./Label"; +import { PolymorphicProps } from "@kobalte/core/polymorphic"; +import "./Checkbox.css"; +import { FieldProps } from "./Field"; + +export type CheckboxProps = FieldProps & + KCheckboxRootProps & { + input?: PolymorphicProps<"input", KCheckboxInputProps<"input">>; + }; + +export const Checkbox = (props: CheckboxProps) => ( + + +); diff --git a/pkgs/clan-app/ui/src/components/v2/Form/Field.css b/pkgs/clan-app/ui/src/components/v2/Form/Field.css index ceb3badf2..06ff4b703 100644 --- a/pkgs/clan-app/ui/src/components/v2/Form/Field.css +++ b/pkgs/clan-app/ui/src/components/v2/Form/Field.css @@ -1,221 +1,11 @@ div.form-field { - @apply flex items-center w-full; + @apply flex flex-col gap-2 items-center w-full; - & > div.meta { - @apply flex flex-col gap-1 w-full; - - & > label, - & > div { - @apply w-full; - /* remove line height which messes with sizing */ - @apply leading-none; - } - - & > label { - @apply flex items-center gap-1; - } - - & > label[data-required] { - span.typography::after { - @apply fg-def-4 ml-1; - - content: "*"; - font-family: "Commit Mono", monospace; - font-size: 0.6875rem; - } - } - - .tooltip-trigger { - } - } - - & input, - & textarea { - @apply w-full px-2 py-1.5 rounded-sm; - @apply outline outline-1 outline-def-acc-1 bg-def-1 fg-def-1; - - font-weight: 500; - font-family: "Archivo", sans-serif; - line-height: 132%; - - &::placeholder { - @apply fg-def-4; - } - - &:hover { - @apply bg-def-acc-1 outline-def-acc-2; - } - - &:focus-visible { - @apply bg-def-1 outline-def-acc-3; - - box-shadow: - 0 0 0 0.125rem theme(colors.bg.def.1), - 0 0 0 0.1875rem theme(colors.border.semantic.info.1); - } - - &[data-invalid] { - @apply outline-semantic-error-4; - } - - &[data-disabled] { - @apply outline-def-2 fg-def-4 cursor-not-allowed; - } - - &[data-readonly] { - @apply outline-def-2 cursor-not-allowed; - } - } - - &.orientation-vertical { - @apply flex-col gap-2; - } - - &.orientation-horizontal { + &.horizontal { @apply flex-row gap-2 justify-between; - & > div.meta { + & > div.form-label { @apply w-1/2 shrink; } - - & div.input-container, - & textarea { - @apply w-1/2 grow; - } - - &:has(> textarea) { - @apply items-start; - } - } - - div.input-container { - @apply inline-block relative w-full; - - /* I'm unsure why I have to do this */ - @apply leading-none; - - & > input { - @apply w-full; - - &.has-icon { - @apply pl-7; - } - } - - & > .icon { - @apply absolute left-2 top-1/2 transform -translate-y-1/2; - @apply w-[0.875rem] h-[0.875rem] pointer-events-none; - } - } - - &.form-field-checkbox { - @apply items-start; - - & > div.checkbox-control { - @apply w-5 h-5 rounded-sm bg-def-1 border border-inv-1 p-[0.0625rem]; - - &:hover { - @apply bg-def-acc-2; - } - - &[data-disabled] { - @apply border-def-2; - } - - &[data-invalid] { - @apply border-semantic-error-4; - } - } - } - - &.size-default { - & input, - & textarea { - font-size: 0.875rem; - } - - div.input-container { - @apply h-[1.875rem]; - - input { - @apply h-[1.875rem]; - } - } - } - - &.size-s { - & input, - & textarea { - @apply px-1.5 py-1; - font-size: 0.75rem; - } - - div.input-container { - @apply h-[1.25rem]; - - input { - @apply h-[1.25rem]; - } - - input.has-icon { - @apply pl-6; - } - - & > .icon { - @apply w-[0.6875rem] h-[0.6875rem] transform -translate-y-1/2; - } - } - } - - &.inverted { - & input, - & textarea { - @apply bg-inv-1 fg-inv-1 outline-inv-acc-1; - - &::placeholder { - @apply fg-inv-4; - } - - &:hover { - @apply bg-inv-acc-2 outline-inv-acc-2; - } - - &:focus-visible { - @apply bg-inv-acc-4; - box-shadow: - 0 0 0 0.125rem theme(colors.bg.inv.1), - 0 0 0 0.1875rem theme(colors.border.semantic.info.1); - } - - &[data-invalid] { - @apply outline-semantic-error-4; - } - } - - &.form-field-checkbox { - & > div.checkbox-control { - @apply bg-inv-1; - - &:hover, - &[data-checked] { - @apply bg-inv-acc-4; - } - - &[data-disabled] { - @apply bg-def-4 border-none; - } - } - } - } - - &.ghost { - & input, - & textarea { - @apply outline-none; - - &:hover { - @apply outline-none; - } - } } } diff --git a/pkgs/clan-app/ui/src/components/v2/Form/Field.tsx b/pkgs/clan-app/ui/src/components/v2/Form/Field.tsx index cebee474c..7213752cc 100644 --- a/pkgs/clan-app/ui/src/components/v2/Form/Field.tsx +++ b/pkgs/clan-app/ui/src/components/v2/Form/Field.tsx @@ -1,214 +1,14 @@ -import { - TextField as KTextField, - TextFieldInputProps as KTextFieldInputProps, - TextFieldTextAreaProps as KTextFieldTextAreaProps, -} from "@kobalte/core/text-field"; -import { - Checkbox as KCheckbox, - CheckboxInputProps as KCheckboxInputProps, -} from "@kobalte/core/checkbox"; -import { Typography } from "@/src/components/v2/Typography/Typography"; -import Icon, { IconVariant } from "@/src/components/v2/Icon/Icon"; - -import cx from "classnames"; -import { Match, splitProps, Switch } from "solid-js"; -import { Dynamic } from "solid-js/web"; -import { Tooltip as KTooltip } from "@kobalte/core/tooltip"; -import "./Field.css"; - -type Size = "default" | "s"; -export type Orientation = "horizontal" | "vertical"; -type FieldType = "text" | "textarea" | "checkbox"; - -export interface TextFieldProps { - input?: KTextFieldInputProps; - value?: string; - onChange?: (value: string) => void; - defaultValue?: string; -} - -export interface TextAreaProps { - input?: KTextFieldTextAreaProps; - value?: string; - onChange?: (value: string) => void; - defaultValue?: string; -} - -export interface CheckboxProps { - input?: KCheckboxInputProps; - checked?: boolean; - defaultChecked?: boolean; - indeterminate?: boolean; - onChange?: (value: boolean) => void; -} +import { Size } from "@/src/components/v2/Form/Label"; +import { Orientation } from "@/src/components/v2/shared"; export interface FieldProps { class?: string; - name?: string; label?: string; description?: string; tooltip?: string; - icon?: IconVariant; ghost?: boolean; size?: Size; orientation?: Orientation; inverted?: boolean; - - required?: boolean; - disabled?: boolean; - readOnly?: boolean; - invalid?: boolean; - - type: FieldType; - text?: TextFieldProps; - textarea?: TextAreaProps; - checkbox?: CheckboxProps; } - -const componentsForType = { - text: { - container: KTextField, - label: KTextField.Label, - description: KTextField.Description, - }, - textarea: { - container: KTextField, - label: KTextField.Label, - description: KTextField.Description, - }, - checkbox: { - container: KCheckbox, - label: KCheckbox.Label, - description: KCheckbox.Description, - }, -}; - -export const Field = (props: FieldProps) => { - const [commonProps] = splitProps(props, [ - "name", - "class", - "required", - "disabled", - "readOnly", - ]); - - const [textProps] = splitProps(props.text || props.textarea || {}, [ - "value", - "onChange", - "defaultValue", - ]); - - const [checkboxProps] = splitProps(props.checkbox || {}, [ - "checked", - "defaultChecked", - "indeterminate", - ]); - - const validationState = () => (props.invalid ? "invalid" : "valid"); - - const labelSize = () => `size-${props.size || "default"}`; - const orientation = () => `orientation-${props.orientation || "vertical"}`; - const descriptionSize = () => (labelSize() == "size-default" ? "xs" : "xxs"); - const fieldClass = () => (props.type ? `form-field-${props.type}` : ""); - - const { container, label, description } = componentsForType[props.type]; - - return ( - - {props.label && ( -
- {props.label && ( - - - {props.label} - - - {props.tooltip && ( - - - - - - - {props.tooltip} - - - - - - )} - - )} - {props.description && ( - - - {props.description} - - - )} -
- )} - - - -
- {props.icon && ( - - )} - -
-
- - - - - - - - - - - -
-
- ); -}; diff --git a/pkgs/clan-app/ui/src/components/v2/Form/Fieldset.stories.tsx b/pkgs/clan-app/ui/src/components/v2/Form/Fieldset.stories.tsx index 632361e1d..2c5b5432b 100644 --- a/pkgs/clan-app/ui/src/components/v2/Form/Fieldset.stories.tsx +++ b/pkgs/clan-app/ui/src/components/v2/Form/Fieldset.stories.tsx @@ -1,6 +1,13 @@ import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid"; -import { Fieldset, FieldsetProps } from "@/src/components/v2/Form/Fieldset"; +import { + Fieldset, + FieldsetProps, +} from "@/src/components/v2/Form/Fieldset"; import cx from "classnames"; +import { TextInput } from "@/src/components/v2/Form/TextInput"; +import { TextArea } from "@/src/components/v2/Form/TextArea"; +import { Checkbox } from "@/src/components/v2/Form/Checkbox"; +import { FieldProps } from "./Field"; const FieldsetExamples = (props: FieldsetProps) => (
@@ -36,30 +43,28 @@ export type Story = StoryObj; export const Default: Story = { args: { legend: "Signup", - fields: [ - { - type: "text", - label: "First Name", - required: true, - control: { placeholder: "Ron" }, - }, - { - type: "text", - label: "Last Name", - required: true, - control: { placeholder: "Burgundy" }, - }, - { - type: "textarea", - label: "Bio", - control: { placeholder: "Tell us a bit about yourself", rows: 8 }, - }, - { - type: "checkbox", - label: "Accept Terms & Conditions", - required: true, - }, - ], + fields: (props: FieldProps) => ( + <> + + +