feat(ui): simplify form components
Better pass through to the underlying Kobalte API without re-defining types.
This commit is contained in:
@@ -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;
|
||||
|
||||
50
pkgs/clan-app/ui/src/components/v2/Form/Checkbox.css
Normal file
50
pkgs/clan-app/ui/src/components/v2/Form/Checkbox.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
44
pkgs/clan-app/ui/src/components/v2/Form/Checkbox.tsx
Normal file
44
pkgs/clan-app/ui/src/components/v2/Form/Checkbox.tsx
Normal 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>
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
|
||||
24
pkgs/clan-app/ui/src/components/v2/Form/Label.css
Normal file
24
pkgs/clan-app/ui/src/components/v2/Form/Label.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
85
pkgs/clan-app/ui/src/components/v2/Form/Label.tsx
Normal file
85
pkgs/clan-app/ui/src/components/v2/Form/Label.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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. ",
|
||||
},
|
||||
};
|
||||
|
||||
34
pkgs/clan-app/ui/src/components/v2/Form/TextArea.tsx
Normal file
34
pkgs/clan-app/ui/src/components/v2/Form/TextArea.tsx
Normal 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>
|
||||
);
|
||||
136
pkgs/clan-app/ui/src/components/v2/Form/TextInput.css
Normal file
136
pkgs/clan-app/ui/src/components/v2/Form/TextInput.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
47
pkgs/clan-app/ui/src/components/v2/Form/TextInput.tsx
Normal file
47
pkgs/clan-app/ui/src/components/v2/Form/TextInput.tsx
Normal 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>
|
||||
);
|
||||
1
pkgs/clan-app/ui/src/components/v2/shared.ts
Normal file
1
pkgs/clan-app/ui/src/components/v2/shared.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type Orientation = "horizontal" | "vertical";
|
||||
Reference in New Issue
Block a user