From 7be9e3f333bf3d8521d2f9636b46f7af60ea485f Mon Sep 17 00:00:00 2001 From: Glen Huang Date: Mon, 22 Sep 2025 22:35:50 +0800 Subject: [PATCH] ui: use css modules for TextArea and TextInput --- .../ui/src/components/Form/Checkbox.tsx | 15 ++- .../clan-app/ui/src/components/Form/Field.tsx | 1 - .../ui/src/components/Form/HostFileInput.tsx | 20 ++-- .../ui/src/components/Form/MachineTags.tsx | 5 +- .../src/components/Form/TextArea.stories.tsx | 10 +- .../ui/src/components/Form/TextArea.tsx | 94 +++++++++--------- .../{TextInput.css => TextField.module.css} | 95 ++++++++----------- .../ui/src/components/Form/TextInput.tsx | 59 +++++------- .../ClanSettingsModal/ClanSettingsModal.tsx | 44 ++++----- .../ui/src/routes/Machine/SectionGeneral.tsx | 6 +- 10 files changed, 157 insertions(+), 192 deletions(-) rename pkgs/clan-app/ui/src/components/Form/{TextInput.css => TextField.module.css} (73%) diff --git a/pkgs/clan-app/ui/src/components/Form/Checkbox.tsx b/pkgs/clan-app/ui/src/components/Form/Checkbox.tsx index 234006d9c..894ada46a 100644 --- a/pkgs/clan-app/ui/src/components/Form/Checkbox.tsx +++ b/pkgs/clan-app/ui/src/components/Form/Checkbox.tsx @@ -21,10 +21,17 @@ export type CheckboxProps = FieldProps & }; export const Checkbox = (props: CheckboxProps) => { - const [local, other] = splitProps( - mergeProps({ size: "default", orientation: "vertical" } as const, props), - ["size", "orientation", "inverted", "ghost", "input"], + const withDefaults = mergeProps( + { size: "default", orientation: "vertical" } as const, + props, ); + const [local, other] = splitProps(withDefaults, [ + "size", + "orientation", + "inverted", + "ghost", + "input", + ]); const iconChecked = ( { in={keepTruthy( local.orientation == "horizontal" && "Orienter-horizontal", )} - {...props} + {...withDefaults} /> diff --git a/pkgs/clan-app/ui/src/components/Form/Field.tsx b/pkgs/clan-app/ui/src/components/Form/Field.tsx index b8763f941..674526922 100644 --- a/pkgs/clan-app/ui/src/components/Form/Field.tsx +++ b/pkgs/clan-app/ui/src/components/Form/Field.tsx @@ -1,5 +1,4 @@ export interface FieldProps { - class?: string; label?: string; labelWeight?: "bold" | "normal"; description?: string; diff --git a/pkgs/clan-app/ui/src/components/Form/HostFileInput.tsx b/pkgs/clan-app/ui/src/components/Form/HostFileInput.tsx index 7fe1a1d29..080f148ab 100644 --- a/pkgs/clan-app/ui/src/components/Form/HostFileInput.tsx +++ b/pkgs/clan-app/ui/src/components/Form/HostFileInput.tsx @@ -11,7 +11,7 @@ import styles from "./HostFileInput.module.css"; import { PolymorphicProps } from "@kobalte/core/polymorphic"; import { FieldProps } from "./Field"; import { Orienter } from "./Orienter"; -import { createSignal, splitProps } from "solid-js"; +import { createSignal, mergeProps, splitProps } from "solid-js"; import { Tooltip } from "@kobalte/core/tooltip"; import { Typography } from "@/src/components/Typography/Typography"; import { keepTruthy } from "@/src/util"; @@ -24,7 +24,14 @@ export type HostFileInputProps = FieldProps & }; export const HostFileInput = (props: HostFileInputProps) => { - const [value, setValue] = createSignal(props.value || ""); + const withDefaults = mergeProps({ value: "" } as const, props); + const [local, other] = splitProps(withDefaults, [ + "size", + "orientation", + "inverted", + "ghost", + ]); + const [value, setValue] = createSignal(other.value); let actualInputElement: HTMLInputElement | undefined; @@ -41,13 +48,6 @@ export const HostFileInput = (props: HostFileInputProps) => { } }; - const [local, other] = splitProps(props, [ - "size", - "orientation", - "inverted", - "ghost", - ]); - return ( { in={keepTruthy( local.orientation == "horizontal" && "Orienter-horizontal", )} - {...props} + {...withDefaults} /> sortedOptions(uniqueOptions(options)); export const MachineTags = (props: MachineTagsProps) => { - const [local, rest] = splitProps(props, ["defaultValue"]); + const withDefaults = props; + const [local, rest] = splitProps(withDefaults, ["defaultValue"]); // // convert default value string[] into MachineTag[] const defaultValue = sortedAndUniqueOptions( @@ -199,7 +200,7 @@ export const MachineTags = (props: MachineTagsProps) => { in={keepTruthy( props.orientation == "horizontal" && "Orienter-horizontal", )} - {...props} + {...withDefaults} /> > & { - autoResize?: boolean; - minRows?: number; - maxRows?: number; - }; + input: PolymorphicProps<"textarea", TextFieldTextAreaProps<"input">>; + autoResize?: boolean; + minRows?: number; + maxRows?: number; }; export const TextArea = (props: TextAreaProps) => { + const withDefaults = mergeProps( + { size: "default", minRows: 1, maxRows: Infinity } as const, + props, + ); + const [local, other] = splitProps(withDefaults, [ + "autoResize", + "minRows", + "maxRows", + "size", + "orientation", + "inverted", + "ghost", + "input", + ]); + let textareaRef: HTMLTextAreaElement; const [lineHeight, setLineHeight] = createSignal(0); const autoResize = () => { - const input = props.input; - - if (!(textareaRef && input.autoResize && lineHeight() > 0)) return; + if (!(textareaRef && local.autoResize && lineHeight() > 0)) return; // Reset height to auto to get accurate scrollHeight textareaRef.style.height = "auto"; // Calculate min and max heights based on rows - const minHeight = (input.minRows || 1) * lineHeight(); - const maxHeight = input.maxRows ? input.maxRows * lineHeight() : Infinity; + const minHeight = local.minRows * lineHeight(); + const maxHeight = local.maxRows * lineHeight(); // Set the height based on content, respecting min/max const newHeight = Math.min( @@ -53,7 +65,7 @@ export const TextArea = (props: TextAreaProps) => { // Set up auto-resize effect createEffect(() => { - if (textareaRef && props.input.autoResize) { + if (textareaRef && local.autoResize) { // Get computed line height const computedStyle = window.getComputedStyle(textareaRef); const computedLineHeight = parseFloat(computedStyle.lineHeight); @@ -68,32 +80,14 @@ export const TextArea = (props: TextAreaProps) => { // Watch for value changes to trigger resize createEffect(() => { - if (props.input.autoResize && textareaRef) { + if (local.autoResize && textareaRef) { // Access the value to create a dependency - const _ = props.value || props.defaultValue || ""; + const _ = other.value || other.defaultValue; // Trigger resize on the next tick to ensure DOM is updated setTimeout(autoResize, 0); } }); - const input = props.input; - - // TextField.Textarea already has an `autoResize` prop - // We filter our props out to avoid conflicting behaviour - const [_, textareaProps] = splitProps(input, [ - "autoResize", - "minRows", - "maxRows", - ]); - - const [styleProps, otherProps] = splitProps(props, [ - "class", - "size", - "orientation", - "inverted", - "ghost", - ]); - return ( { @@ -102,46 +96,44 @@ export const TextArea = (props: TextAreaProps) => { textareaRef = el.querySelector("textarea")! as HTMLTextAreaElement; }} class={cx( - styleProps.class, - "form-field", - "textarea", - styleProps.size, - styleProps.orientation, + styles.textField, + local.size != "default" && styles[local.size], + local.orientation == "horizontal" && styles[local.orientation], { - inverted: styleProps.inverted, - ghost: styleProps.ghost, + [styles.inverted]: local.inverted, + [styles.ghost]: local.ghost, }, )} - {...otherProps} + {...other} > - + diff --git a/pkgs/clan-app/ui/src/components/Form/TextInput.css b/pkgs/clan-app/ui/src/components/Form/TextField.module.css similarity index 73% rename from pkgs/clan-app/ui/src/components/Form/TextInput.css rename to pkgs/clan-app/ui/src/components/Form/TextField.module.css index dc0eefb4f..b5b7959bb 100644 --- a/pkgs/clan-app/ui/src/components/Form/TextInput.css +++ b/pkgs/clan-app/ui/src/components/Form/TextField.module.css @@ -1,6 +1,6 @@ -div.form-field { - &.text input, - &.textarea textarea { +.textField { + 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; @@ -38,12 +38,12 @@ div.form-field { } } - &.textarea textarea { + textarea { &[data-readonly] { @apply overflow-y-hidden; } - &.auto-resize { + &.autoResize { @apply resize-none overflow-y-auto; transition: height 0.1s ease-out; } @@ -52,48 +52,15 @@ div.form-field { &.horizontal { @apply flex-row gap-2 justify-between; - &.text div.input-container, - &.textarea textarea { + .inputContainer, + textarea { @apply w-1/2 grow; } } - &.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; - } - - & > .start-component { - @apply absolute left-2 top-1/2 transform -translate-y-1/2; - } - - & > .end-component { - @apply absolute right-2 top-1/2 transform -translate-y-1/2; - } - - & > .start-component, - & > .end-component { - @apply size-fit; - } - } - &.s { - &.text input, - &.textarea textarea { + input, + textarea { @apply px-1.5 py-1; font-size: 0.75rem; @@ -102,26 +69,18 @@ div.form-field { } } - &.text div.input-container { + .inputContainer { @apply h-[1.25rem]; input { @apply h-[1.25rem]; } - - input.has-icon { - @apply pl-6; - } - - & > .icon { - @apply w-[0.6875rem] h-[0.6875rem]; - } } } &.inverted { - &.text input, - &.textarea textarea { + input, + textarea { @apply bg-inv-1 fg-inv-1 outline-inv-acc-1; &::placeholder { @@ -151,13 +110,33 @@ div.form-field { } &.ghost { - &.text input, - &.textarea textarea { + input, + textarea { @apply outline-none; + } + } - &:hover { - @apply outline-none; - } + .inputContainer { + @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]; + } + + & > .startComponent { + @apply absolute left-2 top-1/2 transform -translate-y-1/2; + } + + & > .endComponent { + @apply absolute right-2 top-1/2 transform -translate-y-1/2; + } + + & > .startComponent, + & > .endComponent { + @apply size-fit; } } } diff --git a/pkgs/clan-app/ui/src/components/Form/TextInput.tsx b/pkgs/clan-app/ui/src/components/Form/TextInput.tsx index 77a9f3dc8..cb9ea0a62 100644 --- a/pkgs/clan-app/ui/src/components/Form/TextInput.tsx +++ b/pkgs/clan-app/ui/src/components/Form/TextInput.tsx @@ -3,11 +3,10 @@ import { TextFieldInputProps, TextFieldRootProps, } from "@kobalte/core/text-field"; -import Icon, { IconVariant } from "@/src/components/Icon/Icon"; import cx from "classnames"; import { Label } from "./Label"; -import "./TextInput.css"; +import styles from "./TextField.module.css"; import { PolymorphicProps } from "@kobalte/core/polymorphic"; import { FieldProps } from "./Field"; import { Orienter } from "./Orienter"; @@ -15,6 +14,7 @@ import { Component, createEffect, createSignal, + mergeProps, onMount, splitProps, } from "solid-js"; @@ -22,19 +22,21 @@ import { keepTruthy } from "@/src/util"; export type TextInputProps = FieldProps & TextFieldRootProps & { - icon?: IconVariant; input?: PolymorphicProps<"input", TextFieldInputProps<"input">>; startComponent?: Component>; endComponent?: Component>; }; export const TextInput = (props: TextInputProps) => { - const [styleProps, otherProps] = splitProps(props, [ - "class", + const withDefaults = mergeProps({ size: "default" } as const, props); + const [local, other] = splitProps(withDefaults, [ "size", "orientation", "inverted", "ghost", + "input", + "startComponent", + "endComponent", ]); let inputRef: HTMLInputElement | undefined; @@ -73,50 +75,35 @@ export const TextInput = (props: TextInputProps) => { return ( - +