diff --git a/pkgs/clan-app/ui/src/components/Form/TextInput.css b/pkgs/clan-app/ui/src/components/Form/TextInput.css index e34fefa4e..dc0eefb4f 100644 --- a/pkgs/clan-app/ui/src/components/Form/TextInput.css +++ b/pkgs/clan-app/ui/src/components/Form/TextInput.css @@ -76,6 +76,19 @@ div.form-field { @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 { @@ -101,7 +114,7 @@ div.form-field { } & > .icon { - @apply w-[0.6875rem] h-[0.6875rem] transform -translate-y-1/2; + @apply w-[0.6875rem] h-[0.6875rem]; } } } diff --git a/pkgs/clan-app/ui/src/components/Form/TextInput.stories.tsx b/pkgs/clan-app/ui/src/components/Form/TextInput.stories.tsx index b51411b63..ea78930d0 100644 --- a/pkgs/clan-app/ui/src/components/Form/TextInput.stories.tsx +++ b/pkgs/clan-app/ui/src/components/Form/TextInput.stories.tsx @@ -1,6 +1,8 @@ import type { Meta, StoryContext, StoryObj } from "@kachurun/storybook-solid"; import cx from "classnames"; import { TextInput, TextInputProps } from "@/src/components/Form/TextInput"; +import Icon from "../Icon/Icon"; +import { Button } from "@kobalte/core/button"; const Examples = (props: TextInputProps) => (
@@ -83,16 +85,38 @@ export const Tooltip: Story = { }, }; -export const Icon: Story = { +export const WithIcon: Story = { args: { ...Tooltip.args, - icon: "Checkmark", + startComponent: () => , + }, +}; + +export const WithStartComponent: Story = { + args: { + ...Tooltip.args, + startComponent: (props: { inverted?: boolean }) => ( + + ), + }, +}; + +export const WithEndComponent: Story = { + args: { + ...Tooltip.args, + endComponent: (props: { inverted?: boolean }) => ( + + ), }, }; export const Ghost: Story = { args: { - ...Icon.args, + ...WithIcon.args, ghost: true, }, }; @@ -106,14 +130,14 @@ export const Invalid: Story = { export const Disabled: Story = { args: { - ...Icon.args, + ...WithIcon.args, disabled: true, }, }; export const ReadOnly: Story = { args: { - ...Icon.args, + ...WithIcon.args, readOnly: true, defaultValue: "14/05/02", }, diff --git a/pkgs/clan-app/ui/src/components/Form/TextInput.tsx b/pkgs/clan-app/ui/src/components/Form/TextInput.tsx index 2cbe1c673..f0157a458 100644 --- a/pkgs/clan-app/ui/src/components/Form/TextInput.tsx +++ b/pkgs/clan-app/ui/src/components/Form/TextInput.tsx @@ -11,12 +11,20 @@ import "./TextInput.css"; import { PolymorphicProps } from "@kobalte/core/polymorphic"; import { FieldProps } from "./Field"; import { Orienter } from "./Orienter"; -import { splitProps } from "solid-js"; +import { + Component, + createEffect, + createSignal, + onMount, + splitProps, +} from "solid-js"; export type TextInputProps = FieldProps & TextFieldRootProps & { icon?: IconVariant; input?: PolymorphicProps<"input", TextFieldInputProps<"input">>; + startComponent?: Component>; + endComponent?: Component>; }; export const TextInput = (props: TextInputProps) => { @@ -28,6 +36,39 @@ export const TextInput = (props: TextInputProps) => { "ghost", ]); + let inputRef: HTMLInputElement | undefined; + let startComponentRef: HTMLDivElement | undefined; + let endComponentRef: HTMLDivElement | undefined; + + const [startComponentSize, setStartComponentSize] = createSignal({ + width: 0, + height: 0, + }); + const [endComponentSize, setEndComponentSize] = createSignal({ + width: 0, + height: 0, + }); + + onMount(() => { + if (startComponentRef) { + const rect = startComponentRef.getBoundingClientRect(); + setStartComponentSize({ width: rect.width, height: rect.height }); + } + if (endComponentRef) { + const rect = endComponentRef.getBoundingClientRect(); + setEndComponentSize({ width: rect.width, height: rect.height }); + } + }); + + createEffect(() => { + if (inputRef) { + const padding = props.size == "s" ? 6 : 8; + + inputRef.style.paddingLeft = `${startComponentSize().width + padding * 2}px`; + inputRef.style.paddingRight = `${endComponentSize().width + padding * 2}px`; + } + }); + return ( { {...props} />
+ {props.startComponent && !props.readOnly && ( +
+ {props.startComponent({ inverted: props.inverted })} +
+ )} {props.icon && !props.readOnly && ( { /> )} + {props.endComponent && !props.readOnly && ( +
+ {props.endComponent({ inverted: props.inverted })} +
+ )}
diff --git a/pkgs/clan-app/ui/src/workflows/InstallMachine/InstallMachine.stories.tsx b/pkgs/clan-app/ui/src/workflows/InstallMachine/InstallMachine.stories.tsx index f50106022..dc3f313e1 100644 --- a/pkgs/clan-app/ui/src/workflows/InstallMachine/InstallMachine.stories.tsx +++ b/pkgs/clan-app/ui/src/workflows/InstallMachine/InstallMachine.stories.tsx @@ -75,7 +75,7 @@ const mockFetcher: Fetcher = ( { name: "gritty.foo", description: "Name of the gritty", - prompt_type: "line", + prompt_type: "hidden", display: { helperText: null, label: "(2) Password", @@ -113,7 +113,7 @@ const mockFetcher: Fetcher = ( { name: "gritty.foo", description: "Name of the gritty", - prompt_type: "line", + prompt_type: "hidden", display: { helperText: null, label: "(5) Password", diff --git a/pkgs/clan-app/ui/src/workflows/InstallMachine/steps/installSteps.tsx b/pkgs/clan-app/ui/src/workflows/InstallMachine/steps/installSteps.tsx index 9dbd01aeb..de6c0312e 100644 --- a/pkgs/clan-app/ui/src/workflows/InstallMachine/steps/installSteps.tsx +++ b/pkgs/clan-app/ui/src/workflows/InstallMachine/steps/installSteps.tsx @@ -18,7 +18,7 @@ import { } from "../InstallMachine"; import { TextInput } from "@/src/components/Form/TextInput"; import { Alert, AlertProps } from "@/src/components/Alert/Alert"; -import { createSignal, For, Match, Show, Switch } from "solid-js"; +import { createSignal, For, Match, Show, Switch, JSX } from "solid-js"; import { Divider } from "@/src/components/Divider/Divider"; import { Orienter } from "@/src/components/Form/Orienter"; import { Button } from "@/src/components/Button/Button"; @@ -35,6 +35,7 @@ import { useClanURI } from "@/src/hooks/clan"; import { useApiClient } from "@/src/hooks/ApiClient"; import { ProcessMessage, useNotifyOrigin } from "@/src/hooks/notify"; import { Loader } from "@/src/components/Loader/Loader"; +import { Button as KButton } from "@kobalte/core/button"; export const InstallHeader = (props: { machineName: string }) => { return ( @@ -566,35 +567,68 @@ const PromptsFields = (props: PromptsFieldsProps) => { - {(f, props) => ( - - )} + {(f, props) => { + const defaultInputType = + fieldInfo.prompt.prompt_type.includes("hidden") + ? "password" + : "text"; + + const [inputType, setInputType] = + createSignal(defaultInputType); + + let endComponent: + | ((props: { inverted?: boolean }) => JSX.Element) + | undefined = undefined; + + if (defaultInputType === "password") { + endComponent = (props) => ( + { + setInputType((type) => + type === "password" ? "text" : "password", + ); + }} + > + + + ); + } + + return ( + + ); + }} )}