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 "./Divider.css";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
import { Orientation } from "@/src/components/v2/shared";
|
||||||
export type Orientation = "horizontal" | "vertical";
|
|
||||||
|
|
||||||
export interface DividerProps {
|
export interface DividerProps {
|
||||||
inverted?: boolean;
|
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 = {
|
const Examples = (props: CheckboxProps) => (
|
||||||
...meta,
|
<div class="flex flex-col gap-8">
|
||||||
title: "Components/Form/Fields/Checkbox",
|
<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 = {
|
export const Bare: Story = {
|
||||||
args: {
|
args: {},
|
||||||
type: "checkbox",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Label: Story = {
|
export const Label: Story = {
|
||||||
@@ -45,7 +83,7 @@ export const Tooltip: Story = {
|
|||||||
export const Invalid: Story = {
|
export const Invalid: Story = {
|
||||||
args: {
|
args: {
|
||||||
...Tooltip.args,
|
...Tooltip.args,
|
||||||
invalid: true,
|
validationState: "invalid",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,9 +98,6 @@ export const ReadOnly: Story = {
|
|||||||
args: {
|
args: {
|
||||||
...Tooltip.args,
|
...Tooltip.args,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
checkbox: {
|
defaultChecked: true,
|
||||||
...Tooltip.args.checkbox,
|
|
||||||
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 {
|
div.form-field {
|
||||||
@apply flex items-center w-full;
|
@apply flex flex-col gap-2 items-center w-full;
|
||||||
|
|
||||||
& > div.meta {
|
&.horizontal {
|
||||||
@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 {
|
|
||||||
@apply flex-row gap-2 justify-between;
|
@apply flex-row gap-2 justify-between;
|
||||||
|
|
||||||
& > div.meta {
|
& > div.form-label {
|
||||||
@apply w-1/2 shrink;
|
@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 {
|
import { Size } from "@/src/components/v2/Form/Label";
|
||||||
TextField as KTextField,
|
import { Orientation } from "@/src/components/v2/shared";
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FieldProps {
|
export interface FieldProps {
|
||||||
class?: string;
|
class?: string;
|
||||||
name?: string;
|
|
||||||
label?: string;
|
label?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
icon?: IconVariant;
|
|
||||||
ghost?: boolean;
|
ghost?: boolean;
|
||||||
|
|
||||||
size?: Size;
|
size?: Size;
|
||||||
orientation?: Orientation;
|
orientation?: Orientation;
|
||||||
inverted?: boolean;
|
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 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 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) => (
|
const FieldsetExamples = (props: FieldsetProps) => (
|
||||||
<div class="flex flex-col gap-8">
|
<div class="flex flex-col gap-8">
|
||||||
@@ -36,30 +43,28 @@ export type Story = StoryObj<typeof meta>;
|
|||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {
|
args: {
|
||||||
legend: "Signup",
|
legend: "Signup",
|
||||||
fields: [
|
fields: (props: FieldProps) => (
|
||||||
{
|
<>
|
||||||
type: "text",
|
<TextInput
|
||||||
label: "First Name",
|
{...props}
|
||||||
required: true,
|
label="First Name"
|
||||||
control: { placeholder: "Ron" },
|
required={true}
|
||||||
},
|
input={{ placeholder: "Ron" }}
|
||||||
{
|
/>
|
||||||
type: "text",
|
<TextInput
|
||||||
label: "Last Name",
|
{...props}
|
||||||
required: true,
|
label="Last Name"
|
||||||
control: { placeholder: "Burgundy" },
|
required={true}
|
||||||
},
|
input={{ placeholder: "Burgundy" }}
|
||||||
{
|
/>
|
||||||
type: "textarea",
|
<TextArea
|
||||||
label: "Bio",
|
{...props}
|
||||||
control: { placeholder: "Tell us a bit about yourself", rows: 8 },
|
label="Bio"
|
||||||
},
|
input={{ placeholder: "Tell us a bit about yourself", rows: 8 }}
|
||||||
{
|
/>
|
||||||
type: "checkbox",
|
<Checkbox {...props} label="Accept Terms" required={true} />
|
||||||
label: "Accept Terms & Conditions",
|
</>
|
||||||
required: true,
|
),
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,32 +93,34 @@ export const Error: Story = {
|
|||||||
args: {
|
args: {
|
||||||
legend: "Signup",
|
legend: "Signup",
|
||||||
error: "You must enter a First Name",
|
error: "You must enter a First Name",
|
||||||
fields: [
|
fields: (props: FieldProps) => (
|
||||||
{
|
<>
|
||||||
type: "text",
|
<TextInput
|
||||||
label: "First Name",
|
{...props}
|
||||||
required: true,
|
label="First Name"
|
||||||
invalid: true,
|
required={true}
|
||||||
control: { placeholder: "Ron" },
|
validationState="invalid"
|
||||||
},
|
input={{ placeholder: "Ron" }}
|
||||||
{
|
/>
|
||||||
type: "text",
|
<TextInput
|
||||||
label: "Last Name",
|
{...props}
|
||||||
required: true,
|
label="Last Name"
|
||||||
invalid: true,
|
required={true}
|
||||||
control: { placeholder: "Burgundy" },
|
validationState="invalid"
|
||||||
},
|
input={{ placeholder: "Burgundy" }}
|
||||||
{
|
/>
|
||||||
type: "textarea",
|
<TextArea
|
||||||
label: "Bio",
|
{...props}
|
||||||
control: { placeholder: "Tell us a bit about yourself", rows: 8 },
|
label="Bio"
|
||||||
},
|
input={{ placeholder: "Tell us a bit about yourself", rows: 8 }}
|
||||||
{
|
/>
|
||||||
type: "checkbox",
|
<Checkbox
|
||||||
label: "Accept Terms & Conditions",
|
{...props}
|
||||||
invalid: true,
|
label="Accept Terms"
|
||||||
required: true,
|
validationState="invalid"
|
||||||
},
|
required={true}
|
||||||
],
|
/>
|
||||||
|
</>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import "./Fieldset.css";
|
import "./Fieldset.css";
|
||||||
import { Field, FieldProps, Orientation } from "./Field";
|
import { JSX } from "solid-js";
|
||||||
import { For } from "solid-js";
|
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { Typography } from "@/src/components/v2/Typography/Typography";
|
import { Typography } from "@/src/components/v2/Typography/Typography";
|
||||||
|
import { FieldProps } from "./Field";
|
||||||
|
|
||||||
export interface FieldsetProps {
|
export interface FieldsetProps extends FieldProps {
|
||||||
legend: string;
|
legend: string;
|
||||||
fields: FieldProps[];
|
disabled: boolean;
|
||||||
inverted?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
orientation?: Orientation;
|
|
||||||
error?: string;
|
error?: string;
|
||||||
|
fields: (props: FieldProps) => JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Fieldset = (props: FieldsetProps) => {
|
export const Fieldset = (props: FieldsetProps) => {
|
||||||
@@ -36,18 +34,7 @@ export const Fieldset = (props: FieldsetProps) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</legend>
|
</legend>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<For each={props.fields}>
|
{props.fields({ ...props, orientation: orientation() })}
|
||||||
{(fieldProps) => {
|
|
||||||
return (
|
|
||||||
<Field
|
|
||||||
{...fieldProps}
|
|
||||||
orientation={orientation()}
|
|
||||||
disabled={props.disabled}
|
|
||||||
inverted={props.inverted}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</div>
|
</div>
|
||||||
{props.error && (
|
{props.error && (
|
||||||
<div class="error" role="alert">
|
<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 = {
|
const Examples = (props: TextAreaProps) => (
|
||||||
...meta,
|
<div class="flex flex-col gap-8">
|
||||||
title: "Components/Form/Fields/TextArea",
|
<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 = {
|
export const Bare: Story = {
|
||||||
args: {
|
args: {
|
||||||
type: "textarea",
|
input: {
|
||||||
textarea: {
|
rows: 10,
|
||||||
input: {
|
placeholder: "I like craft beer and long walks on the beach",
|
||||||
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 = {
|
export const Invalid: Story = {
|
||||||
args: {
|
args: {
|
||||||
...Tooltip.args,
|
...Tooltip.args,
|
||||||
invalid: true,
|
validationState: "invalid",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,10 +110,7 @@ export const ReadOnly: Story = {
|
|||||||
args: {
|
args: {
|
||||||
...Tooltip.args,
|
...Tooltip.args,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
textarea: {
|
defaultValue:
|
||||||
...Tooltip.args.textarea,
|
"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 type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid";
|
||||||
import { Field, FieldProps } from "./Field";
|
|
||||||
import cx from "classnames";
|
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">
|
||||||
<div class="flex flex-col gap-8 p-8">
|
<div class="flex flex-col gap-8 p-8">
|
||||||
<Field {...props} />
|
<TextInput {...props} />
|
||||||
<Field {...props} size="s" />
|
<TextInput {...props} size="s" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-8 p-8 bg-inv-acc-3">
|
<div class="flex flex-col gap-8 p-8 bg-inv-acc-3">
|
||||||
<Field {...props} inverted={true} />
|
<TextInput {...props} inverted={true} />
|
||||||
<Field {...props} inverted={true} size="s" />
|
<TextInput {...props} inverted={true} size="s" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-8 p-8">
|
<div class="flex flex-col gap-8 p-8">
|
||||||
<Field {...props} orientation="horizontal" />
|
<TextInput {...props} orientation="horizontal" />
|
||||||
<Field {...props} orientation="horizontal" size="s" />
|
<TextInput {...props} orientation="horizontal" size="s" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-8 p-8 bg-inv-acc-3">
|
<div class="flex flex-col gap-8 p-8 bg-inv-acc-3">
|
||||||
<Field {...props} inverted={true} orientation="horizontal" />
|
<TextInput {...props} inverted={true} orientation="horizontal" />
|
||||||
<Field {...props} inverted={true} orientation="horizontal" size="s" />
|
<TextInput {...props} inverted={true} orientation="horizontal" size="s" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: "Components/Form/Fields/TextField",
|
title: "Components/Form/TextInput",
|
||||||
component: FieldExamples,
|
component: Examples,
|
||||||
decorators: [
|
decorators: [
|
||||||
(Story: StoryObj, context: StoryContext<FieldProps>) => {
|
(Story: StoryObj, context: StoryContext<TextInputProps>) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={cx({
|
class={cx({
|
||||||
@@ -41,7 +41,7 @@ const meta = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} satisfies Meta<FieldProps>;
|
} satisfies Meta<TextInputProps>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
||||||
@@ -49,11 +49,8 @@ export type Story = StoryObj<typeof meta>;
|
|||||||
|
|
||||||
export const Bare: Story = {
|
export const Bare: Story = {
|
||||||
args: {
|
args: {
|
||||||
type: "text",
|
input: {
|
||||||
text: {
|
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 = {
|
export const Invalid: Story = {
|
||||||
args: {
|
args: {
|
||||||
...Tooltip.args,
|
...Tooltip.args,
|
||||||
invalid: true,
|
validationState: "invalid",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,8 +115,6 @@ export const ReadOnly: Story = {
|
|||||||
args: {
|
args: {
|
||||||
...Icon.args,
|
...Icon.args,
|
||||||
readOnly: true,
|
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