From 4d3abe4c839b3304ea7b88f9c9cf9ed9cd1d3f9e Mon Sep 17 00:00:00 2001 From: charlesLoder Date: Tue, 2 Sep 2025 10:56:41 -0400 Subject: [PATCH 1/4] Refactor and move Input component --- src/components/Input/index.tsx | 45 +++++ src/components/Input/style.module.css | 113 ++++++++++++ src/components/index.ts | 3 + src/provider/components/Input/index.tsx | 58 ------ .../components/Input/style.module.css | 132 -------------- src/provider/index.tsx | 4 +- stories/components/Input.stories.ts | 170 ++++++++++++++++++ 7 files changed, 332 insertions(+), 193 deletions(-) create mode 100644 src/components/Input/index.tsx create mode 100644 src/components/Input/style.module.css delete mode 100644 src/provider/components/Input/index.tsx delete mode 100644 src/provider/components/Input/style.module.css create mode 100644 stories/components/Input.stories.ts diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx new file mode 100644 index 0000000..2828eaa --- /dev/null +++ b/src/components/Input/index.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import styles from "./style.module.css"; + +export interface InputProps extends React.InputHTMLAttributes { + customSize?: "small" | "medium" | "large"; + error?: string; + helperText?: string; + label?: string; +} + +export const Input: React.FC = ({ + label, + error, + helperText, + customSize = "medium", + style, + id, + ...props +}) => { + const inputId = id || `input-${Math.random().toString(36).substring(2, 9)}`; + + return ( +
+ {label && } + + {helperText && ( +
+ {helperText} +
+ )} + {error && ( +
+ {error} +
+ )} +
+ ); +}; diff --git a/src/components/Input/style.module.css b/src/components/Input/style.module.css new file mode 100644 index 0000000..65d4fcb --- /dev/null +++ b/src/components/Input/style.module.css @@ -0,0 +1,113 @@ +.container { + /* input container layout */ + width: 100%; + display: flex; + flex-direction: column; + gap: calc(var(--clover-ai-fontSizes-1) / var(--golden-ratio)); + + /* shared styles */ + --accent-color: var(--clover-ai-colors-accentMuted); + + input { + width: 100%; + outline: none; + transition: + border-color 0.2s ease-in-out, + box-shadow 0.2s ease-in-out; + font-size: var(--clover-ai-fontSizes-3); + + --input-border-radius: var(--clover-ai-sizes-2); + border-radius: var(--input-border-radius); + + --input-border-width: 1px; + border-width: var(--input-border-width); + + --input-border-style: solid; + border-style: var(--input-border-style); + + --input-border-color: var(--accent-color); + border-color: var(--input-border-color); + + --input-padding: var(--clover-ai-sizes-2); + padding: var(--input-padding); + + --input-background-color: initial; + background-color: var(--input-background-color); + + &:focus { + --input-box-shadow-color: color-mix(in srgb, var(--input-border-color) 10%, transparent); + --input-box-shadow: 0 0 0 var(--clover-ai-sizes-1) var(--input-box-shadow-color); + box-shadow: var(--input-box-shadow); + } + + &:user-invalid { + --input-border-color: var(--clover-ai-colors-error); + } + } + + label, + .helperText { + font-size: var(--clover-ai-fontSizes-2); + } + + * { + font-family: inherit; + } +} + +/* Error state */ +.container[data-error="true"] { + --accent-color: var(--clover-ai-colors-error); + --input-border-color: var(--clover-ai-colors-error); + + label { + color: var(--accent-color); + } + + .helperText { + color: var(--accent-color); + } +} + +.container:has(input:required) { + label::after { + content: " *"; + color: var(--clover-ai-colors-error); + } +} + +/* Size styles */ +.container[data-size="small"] { + /* decrease the font sizes by 1 */ + input { + font-size: var(--clover-ai-fontSizes-2); + } + + label, + .helperText { + font-size: var(--clover-ai-fontSizes-1); + } +} + +.container[data-size="large"] { + /* increase the font sizes by 2 */ + input { + --input-padding: var(--clover-ai-sizes-3); + font-size: var(--clover-ai-fontSizes-5); + } + + label, + .helperText { + font-size: var(--clover-ai-fontSizes-3); + } +} + +/* Input prop styles */ +input:disabled { + cursor: not-allowed; +} + +input:read-only:focus { + --input-box-shadow-color: none; + --input-box-shadow: none; +} diff --git a/src/components/index.ts b/src/components/index.ts index ce537e8..ed3f636 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -9,6 +9,9 @@ export { Message } from "./Messages/Message"; export { Heading } from "./Heading"; export type { HeadingProps } from "./Heading"; +export { Input } from "./Input"; +export type { InputProps } from "./Input"; + export { MessagesContainer } from "./Messages"; export type { MessagesContainerProps } from "./Messages"; diff --git a/src/provider/components/Input/index.tsx b/src/provider/components/Input/index.tsx deleted file mode 100644 index b11f0e4..0000000 --- a/src/provider/components/Input/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from "react"; -import styles from "./style.module.css"; - -export interface InputProps extends React.InputHTMLAttributes { - customSize?: "small" | "medium" | "large"; - error?: string; - helperText?: string; - label?: string; - leftIcon?: React.ReactNode; - rightIcon?: React.ReactNode; - variant?: "default" | "bordered" | "filled"; -} - -export const Input: React.FC = ({ - label, - error, - helperText, - variant = "default", - customSize = "medium", - leftIcon, - rightIcon, - className, - style, - id, - ...props -}) => { - const inputId = id || `input-${Math.random().toString(36).substring(2, 9)}`; - - return ( -
- {label && ( - - )} -
- {leftIcon &&
{leftIcon}
} - - {rightIcon &&
{rightIcon}
} -
- {(error || helperText) && ( -
- {error || helperText} -
- )} -
- ); -}; diff --git a/src/provider/components/Input/style.module.css b/src/provider/components/Input/style.module.css deleted file mode 100644 index 3f6a190..0000000 --- a/src/provider/components/Input/style.module.css +++ /dev/null @@ -1,132 +0,0 @@ -/* NOTE: these styles were created by AI and variants may need refinement */ - -/* Input Component Styles */ -.container { - width: 100%; -} - -.label { - display: block; - margin-bottom: 0.5rem; - font-size: 0.875rem; - font-weight: 500; -} - -.label[data-error="true"] { - color: #ef4444; -} - -.inputWrapper { - position: relative; -} - -.input { - width: 100%; - border-radius: 0.5rem; - outline: none; - transition: - border-color 0.2s ease-in-out, - box-shadow 0.2s ease-in-out; - font-family: inherit; -} - -.input:focus { - border-color: var(--colors-primary, #3b82f6); - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); -} - -/* Variant styles */ -.input[data-variant="default"] { - border: 1px solid var(--colors-accent, #d1d5db); - background-color: transparent; -} - -.input[data-variant="default"][data-error="true"] { - border-color: #ef4444; -} - -.input[data-variant="bordered"] { - border: 2px solid var(--colors-border, #e5e7eb); - background-color: transparent; -} - -.input[data-variant="bordered"][data-error="true"] { - border-color: #ef4444; -} - -.input[data-variant="filled"] { - border: 1px solid transparent; - background-color: var(--colors-background, #f9fafb); -} - -.input[data-variant="filled"][data-error="true"] { - border-color: #ef4444; -} - -/* Size styles */ -.input[data-size="small"] { - padding: 0.5rem; - font-size: 0.875rem; -} - -.input[data-size="small"][data-left-icon="true"] { - padding-left: 2.5rem; -} - -.input[data-size="small"][data-right-icon="true"] { - padding-right: 2.5rem; -} - -.input[data-size="medium"] { - padding: 0.618rem; - font-size: 1rem; -} - -.input[data-size="medium"][data-left-icon="true"] { - padding-left: 2.5rem; -} - -.input[data-size="medium"][data-right-icon="true"] { - padding-right: 2.5rem; -} - -.input[data-size="large"] { - padding: 0.75rem; - font-size: 1.125rem; -} - -.input[data-size="large"][data-left-icon="true"] { - padding-left: 2.5rem; -} - -.input[data-size="large"][data-right-icon="true"] { - padding-right: 2.5rem; -} - -/* Icon styles */ -.icon { - position: absolute; - top: 50%; - transform: translateY(-50%); - color: var(--colors-text-secondary, #6b7280); - pointer-events: none; -} - -.leftIcon { - left: 0.618rem; -} - -.rightIcon { - right: 0.618rem; -} - -/* Helper text and error message */ -.helperText { - margin-top: 0.25rem; - font-size: 0.875rem; - color: var(--colors-text-secondary, #6b7280); -} - -.helperText[data-error="true"] { - color: #ef4444; -} diff --git a/src/provider/index.tsx b/src/provider/index.tsx index ccf84be..55367a1 100644 --- a/src/provider/index.tsx +++ b/src/provider/index.tsx @@ -1,13 +1,12 @@ import { createAnthropic, type AnthropicProvider } from "@ai-sdk/anthropic"; import { createGoogleGenerativeAI, type google } from "@ai-sdk/google"; import { createOpenAI, type OpenAIProvider } from "@ai-sdk/openai"; -import { Button, Heading } from "@components"; +import { Button, Heading, Input } from "@components"; import { Tool } from "@langchain/core/tools"; import type { AssistantMessage, Message } from "@types"; import { streamText, tool } from "ai"; import React from "react"; import { BaseProvider } from "../plugin/base_provider"; -import { Input } from "./components/Input"; import { ModelSelection } from "./components/ModelSelection"; import { ProviderSelection } from "./components/ProviderSelection"; import styles from "./style.module.css"; @@ -319,7 +318,6 @@ export class UserTokenProvider extends BaseProvider { Enter API Key ; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Default: Story = { + args: { + placeholder: "Enter text...", + }, +}; + +export const WithLabel: Story = { + name: "With Label", + args: { + label: "Username", + placeholder: "Enter your username", + }, +}; + +export const WithHelperText: Story = { + name: "With Helper Text", + args: { + label: "Email", + placeholder: "Enter your email", + helperText: "We'll never share your email with anyone else.", + }, +}; + +export const WithError: Story = { + name: "With Error", + args: { + label: "Password", + placeholder: "Enter your password", + error: "Password must be at least 8 characters long.", + }, +}; + +export const DefaultVariant: Story = { + name: "Variant / Default", + args: { + label: "Default Input", + placeholder: "Default variant", + }, +}; + +export const Small: Story = { + name: "Size / Small", + args: { + customSize: "small", + label: "Small Input", + placeholder: "Small size", + }, +}; + +export const Medium: Story = { + name: "Size / Medium (Default)", + args: { + customSize: "medium", + label: "Medium Input", + placeholder: "Medium size", + }, +}; + +export const Large: Story = { + name: "Size / Large", + args: { + customSize: "large", + label: "Large Input", + placeholder: "Large size", + }, +}; + +export const Disabled: Story = { + name: "State / Disabled", + args: { + label: "Disabled Input", + placeholder: "This input is disabled", + disabled: true, + }, +}; + +export const ReadOnly: Story = { + name: "State / Read Only", + args: { + label: "Read Only Input", + value: "This is read only", + readOnly: true, + }, +}; + +export const Required: Story = { + name: "State / Required", + args: { + label: "Required Input", + placeholder: "This field is required", + required: true, + }, +}; + +export const TypeEmail: Story = { + name: "Type / Email", + args: { + type: "email", + label: "Email Address", + placeholder: "user@example.com", + }, +}; + +export const TypePassword: Story = { + name: "Type / Password", + args: { + type: "password", + label: "Password", + placeholder: "Enter your password", + }, +}; + +export const TypeNumber: Story = { + name: "Type / Number", + args: { + type: "number", + label: "Age", + min: 0, + max: 120, + }, +}; + +export const TypeSearch: Story = { + name: "Type / Search", + args: { + type: "search", + label: "Search", + placeholder: "Search for something...", + }, +}; + +export const TypeTel: Story = { + name: "Type / Tel", + args: { + type: "tel", + label: "Phone Number", + placeholder: "+1 (555) 123-4567", + }, +}; + +export const TypeUrl: Story = { + name: "Type / URL", + args: { + type: "url", + label: "Website", + placeholder: "https://example.com", + }, +}; From 487b9a58ee7a9f6f1889a912c7c442ce464c9ff3 Mon Sep 17 00:00:00 2001 From: charlesLoder Date: Tue, 2 Sep 2025 11:00:00 -0400 Subject: [PATCH 2/4] Fix linting errors --- src/components/Input/index.tsx | 8 ++++---- stories/components/Input.stories.ts | 3 --- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx index 2828eaa..d82d994 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -24,11 +24,11 @@ export const Input: React.FC = ({ {label && } {helperText && (
@@ -36,7 +36,7 @@ export const Input: React.FC = ({
)} {error && ( -
+
{error}
)} diff --git a/stories/components/Input.stories.ts b/stories/components/Input.stories.ts index eacd97c..6a1e427 100644 --- a/stories/components/Input.stories.ts +++ b/stories/components/Input.stories.ts @@ -27,7 +27,6 @@ export const Default: Story = { }; export const WithLabel: Story = { - name: "With Label", args: { label: "Username", placeholder: "Enter your username", @@ -35,7 +34,6 @@ export const WithLabel: Story = { }; export const WithHelperText: Story = { - name: "With Helper Text", args: { label: "Email", placeholder: "Enter your email", @@ -44,7 +42,6 @@ export const WithHelperText: Story = { }; export const WithError: Story = { - name: "With Error", args: { label: "Password", placeholder: "Enter your password", From 4c0e92359f89156c535b793e222cf4751f20cde6 Mon Sep 17 00:00:00 2001 From: Charles Loder Date: Tue, 2 Sep 2025 11:18:20 -0400 Subject: [PATCH 3/4] Allow multiple describedby ids for helper and error text Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/components/Input/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx index d82d994..99118c9 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -24,7 +24,11 @@ export const Input: React.FC = ({ {label && } Date: Tue, 2 Sep 2025 11:20:32 -0400 Subject: [PATCH 4/4] Fix linting error --- src/components/Input/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx index 99118c9..d8f870b 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -24,15 +24,15 @@ export const Input: React.FC = ({ {label && } {helperText && (