feat(ui): simplify form components

Better pass through to the underlying Kobalte API without re-defining types.
This commit is contained in:
Brian McGee
2025-07-01 10:32:05 +01:00
parent 9d257c1538
commit 3b4fa41840
16 changed files with 611 additions and 543 deletions

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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) => (
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-8 p-8">
<Checkbox {...props} />
<Checkbox {...props} size="s" />
</div>
<div class="flex flex-col gap-8 p-8 bg-inv-acc-3">
<Checkbox {...props} inverted={true} />
<Checkbox {...props} inverted={true} size="s" />
</div>
<div class="flex flex-col gap-8 p-8">
<Checkbox {...props} orientation="horizontal" />
<Checkbox {...props} orientation="horizontal" size="s" />
</div>
<div class="flex flex-col gap-8 p-8 bg-inv-acc-3">
<Checkbox {...props} inverted={true} orientation="horizontal" />
<Checkbox {...props} inverted={true} orientation="horizontal" size="s" />
</div>
</div>
);
export default checkboxMeta;
const meta = {
title: "Components/Form/Checkbox",
component: Examples,
decorators: [
(Story: StoryObj, context: StoryContext<CheckboxProps>) => {
return (
<div
class={cx({
"w-[600px]": (context.args.orientation || "vertical") == "vertical",
"w-[1024px]": context.args.orientation == "horizontal",
"bg-inv-acc-3": context.args.inverted,
})}
>
<Story />
</div>
);
},
],
} satisfies Meta<CheckboxProps>;
export default meta;
export type Story = StoryObj<typeof meta>;
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,
},
};

View File

@@ -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) => (
<KCheckbox
class={cx("form-field", "checkbox", props.size, props.orientation, {
inverted: props.inverted,
ghost: props.ghost,
})}
{...props}
>
<Label
labelComponent={KCheckbox.Label}
descriptionComponent={KCheckbox.Description}
{...props}
/>
<KCheckbox.Input {...props.input} />
<KCheckbox.Control class="checkbox-control">
<KCheckbox.Indicator>
<Icon
icon="Checkmark"
inverted={props.inverted}
color="secondary"
size="100%"
/>
</KCheckbox.Indicator>
</KCheckbox.Control>
</KCheckbox>
);

View File

@@ -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;
}
}
}
}

View File

@@ -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 (
<Dynamic
component={container}
class={cx("form-field", fieldClass(), labelSize(), orientation(), {
inverted: props.inverted,
ghost: props.ghost,
})}
validationState={validationState()}
{...commonProps}
{...textProps}
{...checkboxProps}
>
{props.label && (
<div class="meta">
{props.label && (
<Dynamic component={label}>
<Typography
hierarchy="label"
size={props.size || "default"}
color={props.invalid ? "error" : "primary"}
weight="bold"
inverted={props.inverted}
>
{props.label}
</Typography>
{props.tooltip && (
<KTooltip>
<KTooltip.Trigger class="tooltip-trigger">
<Icon
icon="Info"
color="tertiary"
inverted={props.inverted}
size={props.size == "default" ? "0.85em" : "0.75rem"}
/>
<KTooltip.Portal>
<KTooltip.Content>
<Typography hierarchy="body" size="xs">
{props.tooltip}
</Typography>
</KTooltip.Content>
</KTooltip.Portal>
</KTooltip.Trigger>
</KTooltip>
)}
</Dynamic>
)}
{props.description && (
<Dynamic component={description}>
<Typography
hierarchy="body"
size={descriptionSize()}
color="secondary"
weight="normal"
inverted={props.inverted}
>
{props.description}
</Typography>
</Dynamic>
)}
</div>
)}
<Switch>
<Match when={props.type == "text"}>
<div class="input-container">
{props.icon && (
<Icon
icon={props.icon}
inverted={props.inverted}
color={props.disabled ? "quaternary" : "tertiary"}
/>
)}
<KTextField.Input
{...props.text?.input}
classList={{ "has-icon": props.icon }}
/>
</div>
</Match>
<Match when={props.type == "textarea"}>
<KTextField.TextArea {...props.textarea?.input} />
</Match>
<Match when={props.type == "checkbox"}>
<KCheckbox.Input {...props.checkbox?.input} />
<KCheckbox.Control class="checkbox-control">
<KCheckbox.Indicator>
<Icon
icon="Checkmark"
inverted={props.inverted}
color="secondary"
/>
</KCheckbox.Indicator>
</KCheckbox.Control>
</Match>
</Switch>
</Dynamic>
);
};

View File

@@ -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) => (
<div class="flex flex-col gap-8">
@@ -36,30 +43,28 @@ export type Story = StoryObj<typeof meta>;
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) => (
<>
<TextInput
{...props}
label="First Name"
required={true}
input={{ placeholder: "Ron" }}
/>
<TextInput
{...props}
label="Last Name"
required={true}
input={{ placeholder: "Burgundy" }}
/>
<TextArea
{...props}
label="Bio"
input={{ placeholder: "Tell us a bit about yourself", rows: 8 }}
/>
<Checkbox {...props} label="Accept Terms" required={true} />
</>
),
},
};
@@ -88,32 +93,34 @@ export const Error: Story = {
args: {
legend: "Signup",
error: "You must enter a First Name",
fields: [
{
type: "text",
label: "First Name",
required: true,
invalid: true,
control: { placeholder: "Ron" },
},
{
type: "text",
label: "Last Name",
required: true,
invalid: true,
control: { placeholder: "Burgundy" },
},
{
type: "textarea",
label: "Bio",
control: { placeholder: "Tell us a bit about yourself", rows: 8 },
},
{
type: "checkbox",
label: "Accept Terms & Conditions",
invalid: true,
required: true,
},
],
fields: (props: FieldProps) => (
<>
<TextInput
{...props}
label="First Name"
required={true}
validationState="invalid"
input={{ placeholder: "Ron" }}
/>
<TextInput
{...props}
label="Last Name"
required={true}
validationState="invalid"
input={{ placeholder: "Burgundy" }}
/>
<TextArea
{...props}
label="Bio"
input={{ placeholder: "Tell us a bit about yourself", rows: 8 }}
/>
<Checkbox
{...props}
label="Accept Terms"
validationState="invalid"
required={true}
/>
</>
),
},
};

View File

@@ -1,16 +1,14 @@
import "./Fieldset.css";
import { Field, FieldProps, Orientation } from "./Field";
import { For } from "solid-js";
import { JSX } from "solid-js";
import cx from "classnames";
import { Typography } from "@/src/components/v2/Typography/Typography";
import { FieldProps } from "./Field";
export interface FieldsetProps {
export interface FieldsetProps extends FieldProps {
legend: string;
fields: FieldProps[];
inverted?: boolean;
disabled?: boolean;
orientation?: Orientation;
disabled: boolean;
error?: string;
fields: (props: FieldProps) => JSX.Element;
}
export const Fieldset = (props: FieldsetProps) => {
@@ -36,18 +34,7 @@ export const Fieldset = (props: FieldsetProps) => {
</Typography>
</legend>
<div class="fields">
<For each={props.fields}>
{(fieldProps) => {
return (
<Field
{...fieldProps}
orientation={orientation()}
disabled={props.disabled}
inverted={props.inverted}
/>
);
}}
</For>
{props.fields({ ...props, orientation: orientation() })}
</div>
{props.error && (
<div class="error" role="alert">

View File

@@ -0,0 +1,24 @@
div.form-label {
@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;
}
}
}

View File

@@ -0,0 +1,85 @@
import { Show } from "solid-js";
import { Typography } from "@/src/components/v2/Typography/Typography";
import { Tooltip as KTooltip } from "@kobalte/core/tooltip";
import Icon from "@/src/components/v2/Icon/Icon";
import { TextField } from "@kobalte/core/text-field";
import { Checkbox } from "@kobalte/core/checkbox";
import { Combobox } from "@kobalte/core/combobox";
import "./Label.css";
export type Size = "default" | "s";
export type LabelComponent =
| typeof TextField.Label
| typeof Checkbox.Label
| typeof Combobox.Label;
export type DescriptionComponent =
| typeof TextField.Description
| typeof Checkbox.Description
| typeof Combobox.Description;
export interface LabelProps {
labelComponent: LabelComponent;
descriptionComponent: DescriptionComponent;
size?: Size;
label?: string;
description?: string;
tooltip?: string;
icon?: string;
inverted?: boolean;
validationState?: "valid" | "invalid";
}
export const Label = (props: LabelProps) => {
const descriptionSize = () => (props.size == "default" ? "xs" : "xxs");
return (
<Show when={props.label}>
<div class="form-label">
<props.labelComponent>
<Typography
hierarchy="label"
size={props.size || "default"}
color={props.validationState == "invalid" ? "error" : "primary"}
weight="bold"
inverted={props.inverted}
>
{props.label}
</Typography>
{props.tooltip && (
<KTooltip>
<KTooltip.Trigger>
<Icon
icon="Info"
color="tertiary"
inverted={props.inverted}
size={props.size == "default" ? "0.85em" : "0.75rem"}
/>
<KTooltip.Portal>
<KTooltip.Content>
<Typography hierarchy="body" size="xs">
{props.tooltip}
</Typography>
</KTooltip.Content>
</KTooltip.Portal>
</KTooltip.Trigger>
</KTooltip>
)}
</props.labelComponent>
{props.description && (
<props.descriptionComponent>
<Typography
hierarchy="body"
size={descriptionSize()}
color="secondary"
weight="normal"
inverted={props.inverted}
>
{props.description}
</Typography>
</props.descriptionComponent>
)}
</div>
</Show>
);
};

View File

@@ -1,20 +1,57 @@
import meta, { Story } from "./TextField.stories";
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
import cx from "classnames";
import { TextArea, TextAreaProps } from "./TextArea";
const textAreaMeta = {
...meta,
title: "Components/Form/Fields/TextArea",
};
const Examples = (props: TextAreaProps) => (
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-8 p-8">
<TextArea {...props} />
<TextArea {...props} size="s" />
</div>
<div class="flex flex-col gap-8 p-8 bg-inv-acc-3">
<TextArea {...props} inverted={true} />
<TextArea {...props} inverted={true} size="s" />
</div>
<div class="flex flex-col gap-8 p-8">
<TextArea {...props} orientation="horizontal" />
<TextArea {...props} orientation="horizontal" size="s" />
</div>
<div class="flex flex-col gap-8 p-8 bg-inv-acc-3">
<TextArea {...props} inverted={true} orientation="horizontal" />
<TextArea {...props} inverted={true} orientation="horizontal" size="s" />
</div>
</div>
);
export default textAreaMeta;
const meta = {
title: "Components/Form/TextArea",
component: Examples,
decorators: [
(Story: StoryObj, context: StoryContext<TextAreaProps>) => {
return (
<div
class={cx({
"w-[600px]": (context.args.orientation || "vertical") == "vertical",
"w-[1024px]": context.args.orientation == "horizontal",
"bg-inv-acc-3": context.args.inverted,
})}
>
<Story />
</div>
);
},
],
} satisfies Meta<TextAreaProps>;
export default meta;
export type Story = StoryObj<typeof meta>;
export const Bare: Story = {
args: {
type: "textarea",
textarea: {
input: {
rows: 10,
placeholder: "I like craft beer and long walks on the beach",
},
input: {
rows: 10,
placeholder: "I like craft beer and long walks on the beach",
},
},
};
@@ -58,7 +95,7 @@ export const Ghost: Story = {
export const Invalid: Story = {
args: {
...Tooltip.args,
invalid: true,
validationState: "invalid",
},
};
@@ -73,10 +110,7 @@ export const ReadOnly: Story = {
args: {
...Tooltip.args,
readOnly: true,
textarea: {
...Tooltip.args.textarea,
defaultValue:
"Good evening. I'm Ron Burgundy, and this is what's happening in your world tonight. ",
},
defaultValue:
"Good evening. I'm Ron Burgundy, and this is what's happening in your world tonight. ",
},
};

View File

@@ -0,0 +1,34 @@
import {
TextField,
TextFieldRootProps,
TextFieldTextAreaProps,
} from "@kobalte/core/text-field";
import cx from "classnames";
import { Label } from "./Label";
import { PolymorphicProps } from "@kobalte/core/polymorphic";
import "./TextInput.css";
import { FieldProps } from "./Field";
export type TextAreaProps = FieldProps &
TextFieldRootProps & {
input?: PolymorphicProps<"textarea", TextFieldTextAreaProps<"input">>;
};
export const TextArea = (props: TextAreaProps) => (
<TextField
class={cx("form-field", "textarea", props.size, props.orientation, {
inverted: props.inverted,
ghost: props.ghost,
})}
{...props}
>
<Label
labelComponent={TextField.Label}
descriptionComponent={TextField.Description}
{...props}
/>
<TextField.TextArea {...props.input} />
</TextField>
);

View File

@@ -0,0 +1,136 @@
@import "./Field.css";
div.form-field {
&.text input,
&.textarea 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-size: 0.875rem;
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;
}
}
&.horizontal {
@apply flex-row gap-2 justify-between;
&.text div.input-container,
&.textarea textarea {
@apply w-1/2 grow;
}
&.textarea:has(> textarea) {
@apply items-start;
}
}
&.text div.input-container {
@apply inline-block relative w-full h-[1.875rem];
/* I'm unsure why I have to do this */
@apply leading-none;
& > input {
@apply w-full h-[1.875rem];
&.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;
}
}
&.s {
&.text input,
&.textarea textarea {
@apply px-1.5 py-1;
font-size: 0.75rem;
}
&.text 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 {
&.text input,
&.textarea 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;
}
}
}
&.ghost {
&.text input,
&.textarea textarea {
@apply outline-none;
&:hover {
@apply outline-none;
}
}
}
}

View File

@@ -1,33 +1,33 @@
import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
import { Field, FieldProps } from "./Field";
import cx from "classnames";
import { TextInput, TextInputProps } from "@/src/components/v2/Form/TextInput";
const FieldExamples = (props: FieldProps) => (
const Examples = (props: TextInputProps) => (
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-8 p-8">
<Field {...props} />
<Field {...props} size="s" />
<TextInput {...props} />
<TextInput {...props} size="s" />
</div>
<div class="flex flex-col gap-8 p-8 bg-inv-acc-3">
<Field {...props} inverted={true} />
<Field {...props} inverted={true} size="s" />
<TextInput {...props} inverted={true} />
<TextInput {...props} inverted={true} size="s" />
</div>
<div class="flex flex-col gap-8 p-8">
<Field {...props} orientation="horizontal" />
<Field {...props} orientation="horizontal" size="s" />
<TextInput {...props} orientation="horizontal" />
<TextInput {...props} orientation="horizontal" size="s" />
</div>
<div class="flex flex-col gap-8 p-8 bg-inv-acc-3">
<Field {...props} inverted={true} orientation="horizontal" />
<Field {...props} inverted={true} orientation="horizontal" size="s" />
<TextInput {...props} inverted={true} orientation="horizontal" />
<TextInput {...props} inverted={true} orientation="horizontal" size="s" />
</div>
</div>
);
const meta = {
title: "Components/Form/Fields/TextField",
component: FieldExamples,
title: "Components/Form/TextInput",
component: Examples,
decorators: [
(Story: StoryObj, context: StoryContext<FieldProps>) => {
(Story: StoryObj, context: StoryContext<TextInputProps>) => {
return (
<div
class={cx({
@@ -41,7 +41,7 @@ const meta = {
);
},
],
} satisfies Meta<FieldProps>;
} satisfies Meta<TextInputProps>;
export default meta;
@@ -49,11 +49,8 @@ export type Story = StoryObj<typeof meta>;
export const Bare: Story = {
args: {
type: "text",
text: {
input: {
placeholder: "e.g. 11/06/89",
},
input: {
placeholder: "e.g. 11/06/89",
},
},
};
@@ -103,7 +100,7 @@ export const Ghost: Story = {
export const Invalid: Story = {
args: {
...Tooltip.args,
invalid: true,
validationState: "invalid",
},
};
@@ -118,8 +115,6 @@ export const ReadOnly: Story = {
args: {
...Icon.args,
readOnly: true,
text: {
defaultValue: "14/05/02",
},
defaultValue: "14/05/02",
},
};

View File

@@ -0,0 +1,47 @@
import {
TextField,
TextFieldInputProps,
TextFieldRootProps,
} from "@kobalte/core/text-field";
import Icon, { IconVariant } from "@/src/components/v2/Icon/Icon";
import cx from "classnames";
import { Label } from "./Label";
import "./TextInput.css";
import { PolymorphicProps } from "@kobalte/core/polymorphic";
import { FieldProps } from "./Field";
export type TextInputProps = FieldProps &
TextFieldRootProps & {
icon?: IconVariant;
input?: PolymorphicProps<"input", TextFieldInputProps<"input">>;
};
export const TextInput = (props: TextInputProps) => (
<TextField
class={cx("form-field", "text", props.size, props.orientation, {
inverted: props.inverted,
ghost: props.ghost,
})}
{...props}
>
<Label
labelComponent={TextField.Label}
descriptionComponent={TextField.Description}
{...props}
/>
<div class="input-container">
{props.icon && (
<Icon
icon={props.icon}
inverted={props.inverted}
color={props.disabled ? "tertiary" : "quaternary"}
/>
)}
<TextField.Input
{...props.input}
classList={{ "has-icon": props.icon }}
/>
</div>
</TextField>
);

View File

@@ -0,0 +1 @@
export type Orientation = "horizontal" | "vertical";