diff --git a/pkgs/clan-app/ui/src/components/Form/TextArea.stories.tsx b/pkgs/clan-app/ui/src/components/Form/TextArea.stories.tsx
index c08db1204..d251d6dcc 100644
--- a/pkgs/clan-app/ui/src/components/Form/TextArea.stories.tsx
+++ b/pkgs/clan-app/ui/src/components/Form/TextArea.stories.tsx
@@ -114,3 +114,30 @@ export const ReadOnly: Story = {
"Good evening. I'm Ron Burgundy, and this is what's happening in your world tonight. ",
},
};
+
+export const AutoResize: Story = {
+ args: {
+ label: "Auto-resizing TextArea",
+ description:
+ "This textarea automatically adjusts its height based on content",
+ tooltip: "Try typing multiple lines to see it grow",
+ input: {
+ placeholder: "Start typing to see the textarea grow...",
+ autoResize: true,
+ minRows: 2,
+ maxRows: 10,
+ },
+ },
+};
+
+export const AutoResizeNoMax: Story = {
+ args: {
+ label: "Auto-resize without max height",
+ description: "This textarea grows indefinitely with content",
+ input: {
+ placeholder: "This will grow as much as needed...",
+ autoResize: true,
+ minRows: 3,
+ },
+ },
+};
diff --git a/pkgs/clan-app/ui/src/components/Form/TextArea.tsx b/pkgs/clan-app/ui/src/components/Form/TextArea.tsx
index b2931ddc0..fddbddbc0 100644
--- a/pkgs/clan-app/ui/src/components/Form/TextArea.tsx
+++ b/pkgs/clan-app/ui/src/components/Form/TextArea.tsx
@@ -7,6 +7,7 @@ import {
import cx from "classnames";
import { Label } from "./Label";
import { PolymorphicProps } from "@kobalte/core/polymorphic";
+import { createEffect, createSignal, splitProps } from "solid-js";
import "./TextInput.css";
import { FieldProps } from "./Field";
@@ -14,24 +15,119 @@ import { Orienter } from "./Orienter";
export type TextAreaProps = FieldProps &
TextFieldRootProps & {
- input?: PolymorphicProps<"textarea", TextFieldTextAreaProps<"input">>;
+ input: PolymorphicProps<"textarea", TextFieldTextAreaProps<"input">> & {
+ autoResize?: boolean;
+ minRows?: number;
+ maxRows?: number;
+ };
};
-export const TextArea = (props: TextAreaProps) => (
-
-
-
-
-
-
-);
+export const TextArea = (props: TextAreaProps) => {
+ let textareaRef: HTMLTextAreaElement;
+
+ const [lineHeight, setLineHeight] = createSignal(0);
+
+ const autoResize = () => {
+ const input = props.input;
+
+ if (!(textareaRef && input.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;
+
+ // Set the height based on content, respecting min/max
+ const newHeight = Math.min(
+ Math.max(textareaRef.scrollHeight, minHeight),
+ maxHeight,
+ );
+
+ // Update the height
+ textareaRef.style.height = `${newHeight}px`;
+ textareaRef.style.maxHeight = `${maxHeight}px`;
+
+ console.log("min/max height", minHeight, maxHeight);
+ console.log("textarea ref style", textareaRef.style);
+ };
+
+ // Set up auto-resize effect
+ createEffect(() => {
+ if (textareaRef && props.input.autoResize) {
+ // Get computed line height
+ const computedStyle = window.getComputedStyle(textareaRef);
+ const computedLineHeight = parseFloat(computedStyle.lineHeight);
+ if (!isNaN(computedLineHeight)) {
+ setLineHeight(computedLineHeight);
+ }
+
+ // Initial resize
+ autoResize();
+ }
+ });
+
+ // Watch for value changes to trigger resize
+ createEffect(() => {
+ if (props.input.autoResize && textareaRef) {
+ // Access the value to create a dependency
+ const _ = props.value || props.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",
+ ]);
+
+ return (
+ {
+ // for some reason capturing the ref directly on TextField.TextArea works in Chrome
+ // but not in webkit, so we capture the parent ref and query for the textarea
+ textareaRef = el.querySelector("textarea")! as HTMLTextAreaElement;
+ }}
+ class={cx("form-field", "textarea", props.size, props.orientation, {
+ inverted: props.inverted,
+ ghost: props.ghost,
+ })}
+ {...props}
+ >
+
+
+ {
+ autoResize();
+
+ if (!input.onInput) {
+ return;
+ }
+
+ // Call original onInput if it exists
+ if (typeof input.onInput === "function") {
+ input.onInput(e);
+ } else if (Array.isArray(input.onInput)) {
+ input.onInput.forEach((handler) => handler(e));
+ }
+ }}
+ {...textareaProps}
+ />
+
+
+ );
+};
diff --git a/pkgs/clan-app/ui/src/components/Form/TextInput.css b/pkgs/clan-app/ui/src/components/Form/TextInput.css
index eceb5fe61..900c07a42 100644
--- a/pkgs/clan-app/ui/src/components/Form/TextInput.css
+++ b/pkgs/clan-app/ui/src/components/Form/TextInput.css
@@ -42,6 +42,11 @@ div.form-field {
&[data-readonly] {
@apply overflow-y-hidden;
}
+
+ &.auto-resize {
+ @apply resize-none overflow-y-auto;
+ transition: height 0.1s ease-out;
+ }
}
&.horizontal {
diff --git a/pkgs/clan-app/ui/src/components/Sidebar/SidebarSection.css b/pkgs/clan-app/ui/src/components/Sidebar/SidebarSection.css
index 9200dee88..fdcdb4c3a 100644
--- a/pkgs/clan-app/ui/src/components/Sidebar/SidebarSection.css
+++ b/pkgs/clan-app/ui/src/components/Sidebar/SidebarSection.css
@@ -10,6 +10,6 @@ div.sidebar-section {
}
& > div.content {
- @apply w-full h-full px-1.5 py-3 rounded-md bg-inv-4;
+ @apply w-full h-fit px-1.5 py-3 rounded-md bg-inv-4;
}
}
diff --git a/pkgs/clan-app/ui/src/routes/Machine/SectionGeneral.tsx b/pkgs/clan-app/ui/src/routes/Machine/SectionGeneral.tsx
index aa5c455ae..9134daf93 100644
--- a/pkgs/clan-app/ui/src/routes/Machine/SectionGeneral.tsx
+++ b/pkgs/clan-app/ui/src/routes/Machine/SectionGeneral.tsx
@@ -116,7 +116,13 @@ export const SectionGeneral = () => {
inverted
readOnly={!editing}
orientation="horizontal"
- input={{ ...input, rows: 4, placeholder: "No description" }}
+ input={{
+ ...input,
+ autoResize: true,
+ minRows: 2,
+ maxRows: 4,
+ placeholder: "No description",
+ }}
/>
)}