Skip to content

Commit

Permalink
feat: new input component (#2909)
Browse files Browse the repository at this point in the history
* feat: new input component

* fix: add type table instead of harcoded types
  • Loading branch information
ogzhanolguncu authored Feb 24, 2025
1 parent 5429532 commit 59f642d
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 8 deletions.
52 changes: 45 additions & 7 deletions apps/engineering/app/components/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,33 @@ type Props = {

export const RenderComponentWithSnippet: React.FC<PropsWithChildren<Props>> = (props) => {
const [open, setOpen] = useState(false);

const snippet =
props.customCodeSnippet ??
reactElementToJSXString(props.children, {
showFunctions: true,
useBooleanShorthandSyntax: true,

displayName: (node) => {
// @ts-ignore
return node?.type.displayName ?? "Unknown";
try {
return getComponentDisplayName(node);
} catch (error) {
console.warn("Failed to get display name:", error);
return "Unknown";
}
},
functionValue: (fn) => {
return fn.name || "anonymous";
},
filterProps: (_, key) => {
// Filter out internal React props
return !key.startsWith("_") && !key.startsWith("$");
},
});

return (
<div className="rounded-lg border border-gray-6 bg-gray-1 overflow-hidden">
<div className="p-8 xl:p-12">{props.children}</div>

<div className="bg-gray-3 p-2 flex items-center justify-start border-t border-b border-gray-6">
<div className="bg-gray-3 p-2 flex items-center justify-start border-t border-b border-gray-6">
<Button variant="ghost" onClick={() => setOpen(!open)}>
<ChevronRight
className={cn("transition-all", {
Expand All @@ -36,9 +47,8 @@ export const RenderComponentWithSnippet: React.FC<PropsWithChildren<Props>> = (p
Code
</Button>
</div>

<div
className={cn("w-full bg-gray-2 transition-all max-h-96 overflow-y-scroll", {
className={cn("w-full bg-gray-2 transition-all max-h-96 overflow-y-scroll", {
hidden: !open,
})}
>
Expand All @@ -55,3 +65,31 @@ export const RenderComponentWithSnippet: React.FC<PropsWithChildren<Props>> = (p
</div>
);
};

const getComponentDisplayName = (element: any): string => {
// biome-ignore lint/style/useBlockStatements: <explanation>
if (!element) return "Unknown";

// Check for displayName
if (element.type?.displayName) {
return element.type.displayName;
}

// Check for name property
if (element.type?.name) {
return element.type.name;
}

// Check if it's a string (HTML element)
if (typeof element.type === "string") {
return element.type;
}

// Check function name for functional components
if (typeof element.type === "function") {
return element.type.name || "AnonymousComponent";
}

// Fallback
return "Unknown";
};
106 changes: 106 additions & 0 deletions apps/engineering/content/design/components/input.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
title: Input
description: A text input field component with different states, validations, and icon support.
---

import { Input } from "@unkey/ui"
import { RenderComponentWithSnippet } from "@/app/components/render"
import {
InputDefaultVariant,
InputSuccessVariant,
InputWarningVariant,
InputErrorVariant,
InputDisabledVariant,
InputWithDefaultValue,
InputWithPasswordToggle,
InputWithBothIcons
} from "./input/input.variants.tsx"

# Input

A versatile input component that supports various states, validations, and icon placements. Use it to collect user input with appropriate visual feedback and enhanced usability through icons.

## Default

The default input style with neutral colors. Can include optional icons for better visual context.

<InputDefaultVariant />

## States

Input components can reflect different states through visual styling:

### Success State

Use the success state to indicate valid input or successful validation. The checkmark icon provides immediate visual feedback.

<InputSuccessVariant />

### Warning State

The warning state can be used to show potential issues that don't prevent form submission. The alert icon draws attention to the warning state.

<InputWarningVariant />

### Error State

Use the error state to indicate invalid input that needs correction. The alert icon emphasizes the error state.

<InputErrorVariant />

### Disabled State

Use the disabled state when user interaction should be prevented, such as during form submission or when the input depends on other conditions.

<InputDisabledVariant />

### With Default Value State

Input pre-populated with an initial value that users can modify or build upon.

<InputWithDefaultValue />

## Interactive Elements

### Password Toggle

Example of an input with a clickable icon to toggle password visibility.

<InputWithPasswordToggle />

### Search with Icons

Example of an input with both leading and trailing icons for enhanced functionality.

<InputWithBothIcons />

## Props

The Input component accepts all standard HTML input attributes plus the following:

<AutoTypeTable
name="InputProps"
type={`import { ReactNode } from "react"
export interface InputProps {
/** Determines the visual style and state of the input */
variant?: 'default' | 'success' | 'warning' | 'error';
/** Optional icon component to display on the left side */
leftIcon?: ReactNode;
/** Optional icon component to display on the right side */
rightIcon?: ReactNode;
/** Additional classes to apply to the wrapper div */
wrapperClassName?: string;
}`}
/>


## Icon Guidelines

When using icons with the Input component:

- Icons should be sized appropriately (recommended: 16x16px using `h-4 w-4` classes)
- Icons inherit colors based on the input's variant state
- Interactive icons (like password toggle) should be wrapped in buttons
- Avoid using too many icons which might clutter the interface
- Left icons are typically used for input type indication (search, email, etc.)
- Right icons are commonly used for interactive elements (password toggle, clear button, etc.)
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"use client";

import { RenderComponentWithSnippet } from "@/app/components/render";
import { InputSearch } from "@unkey/icons";
import { Input } from "@unkey/ui";
import { EyeIcon, EyeOff } from "lucide-react";
import { useState } from "react";

export const InputDefaultVariant = () => {
return (
<RenderComponentWithSnippet>
<Input placeholder="All we have to decide is what to do with the time that is given us" />
</RenderComponentWithSnippet>
);
};

export const InputSuccessVariant = () => {
return (
<RenderComponentWithSnippet>
<Input variant="success" placeholder="Not all those who wander are lost" />
</RenderComponentWithSnippet>
);
};

export const InputWarningVariant = () => {
return (
<RenderComponentWithSnippet>
<Input variant="warning" placeholder="It's a dangerous business, going out your door" />
</RenderComponentWithSnippet>
);
};

export const InputErrorVariant = () => {
return (
<RenderComponentWithSnippet>
<Input variant="error" placeholder="One Ring to rule them all, One Ring to find them" />
</RenderComponentWithSnippet>
);
};

export const InputDisabledVariant = () => {
return (
<RenderComponentWithSnippet>
<Input disabled placeholder="Even the smallest person can change the course of the future" />
</RenderComponentWithSnippet>
);
};

export const InputWithDefaultValue = () => {
return (
<RenderComponentWithSnippet>
<Input defaultValue="Speak friend and enter" placeholder="The password is mellon" />
</RenderComponentWithSnippet>
);
};

export const InputWithPasswordToggle = () => {
const [showPassword, setShowPassword] = useState(false);

return (
<RenderComponentWithSnippet>
<Input
type={showPassword ? "text" : "password"}
placeholder="Speak friend and enter"
rightIcon={
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="focus:outline-none"
>
{showPassword ? <EyeIcon className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />}
</button>
}
/>
</RenderComponentWithSnippet>
);
};

export const InputWithBothIcons = () => {
return (
<RenderComponentWithSnippet>
<Input
placeholder="Search in emails"
leftIcon={<InputSearch className="h-4 w-4" />}
rightIcon={<InputSearch className="h-4 w-4" />}
/>
</RenderComponentWithSnippet>
);
};
84 changes: 84 additions & 0 deletions internal/ui/src/components/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { cn } from "../lib/utils";

const inputVariants = cva(
"flex min-h-9 w-full rounded-lg text-[13px] leading-5 transition-colors duration-300 disabled:cursor-not-allowed disabled:opacity-50 placeholder:text-gray-7 text-gray-12",
{
variants: {
variant: {
default: [
"border border-gray-5 hover:border-gray-8 bg-gray-2",
"focus:border focus:border-accent-12 focus:ring-4 focus:ring-gray-5 focus-visible:outline-none focus:ring-offset-0",
"[&:not(:placeholder-shown)]:focus:ring-0",
],
success: [
"border border-success-6 hover:border-success-7 bg-gray-2",
"focus:border-success-8 focus:ring-2 focus:ring-success-2 focus-visible:outline-none",
"[&:not(:placeholder-shown)]:focus:ring-success-3",
],
warning: [
"border border-warning-6 hover:border-warning-7 bg-gray-2",
"focus:border-warning-8 focus:ring-2 focus:ring-warning-2 focus-visible:outline-none",
"[&:not(:placeholder-shown)]:focus:ring-warning-3",
],
error: [
"border border-error-6 hover:border-error-7 bg-gray-2",
"focus:border-error-8 focus:ring-2 focus:ring-error-2 focus-visible:outline-none",
"[&:not(:placeholder-shown)]:focus:ring-error-3",
],
},
},
defaultVariants: {
variant: "default",
},
},
);

const inputWrapperVariants = cva("relative flex items-center w-full", {
variants: {
variant: {
default: "text-gray-11",
success: "text-success-11",
warning: "text-warning-11",
error: "text-error-11",
},
},
defaultVariants: {
variant: "default",
},
});

export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement>,
VariantProps<typeof inputVariants> {
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
wrapperClassName?: string;
}

export const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, variant, type, leftIcon, rightIcon, wrapperClassName, ...props }, ref) => {
return (
<div className={cn(inputWrapperVariants({ variant }), wrapperClassName)}>
{leftIcon && (
<div className="absolute left-3 flex items-center pointer-events-none">{leftIcon}</div>
)}
<input
type={type}
className={cn(
inputVariants({ variant, className }),
"px-3 py-2",
leftIcon && "pl-9",
rightIcon && "pr-9",
)}
ref={ref}
{...props}
/>
{rightIcon && <div className="absolute right-3 flex items-center">{rightIcon}</div>}
</div>
);
},
);

Input.displayName = "Input";
1 change: 1 addition & 0 deletions internal/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from "./components/button";
export * from "./components/id";
export * from "./components/tooltip";
export * from "./components/date-time/date-time";
export * from "./components/input";
export * from "./components/empty";
1 change: 0 additions & 1 deletion tools/migrate/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ async function main() {
.from(table)
.where(isNull(table.createdAtM))
.then((res) => res.at(0)?.count ?? 0);
console.log({ count });

let processed = 0;
let cursor = "";
Expand Down

0 comments on commit 59f642d

Please sign in to comment.