Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/input-semantic-tokens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@sipe-team/input": minor
---

Apply semantic design tokens and field API updates to Input

- Replace local color/spacing/typography with semantic `vars`
- Fix `fontWeight` to regular (prop removed pending design review)
- Move field chrome (border/focus) onto the `input` element; use `span` wrapper
- Add `validation` prop for border-only `error` / `success` states (FormField-ready)
- Style native `readOnly` distinctly from `disabled` (muted background, default cursor)
223 changes: 122 additions & 101 deletions packages/input/src/Input.css.ts
Original file line number Diff line number Diff line change
@@ -1,140 +1,161 @@
import { color } from '@sipe-team/tokens';
import { vars } from '@sipe-team/tokens';

import { style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';

import type { InputFontSize, InputFontWeight } from './Input';

// TODO ThemeProvider 적용
export const colors = {
inputRing: color.gray800,
defaultInputOutline: color.gray400,
disabledBackground: color.gray300,
} as const;

export const spacing = {
defaultInputPadding: '8px',
defaultBorderRadius: '8px',
defaultActionSize: '24px',
} as const;

export const weight = {
regular: 400,
medium: 500,
semiBold: 600,
bold: 700,
} as const;
import type { InputFontSize, InputValidation } from './Input';

export const defaultFontSize: InputFontSize = 16;
export const defaultFontWeight: InputFontWeight = 'regular';
export const defaultValidation: InputValidation = 'default';

export const inputWrapper = recipe({
export const inputWrapper = style({
position: 'relative',
display: 'block',
flex: 1,
minWidth: 0,
cursor: 'default',
userSelect: 'none',
});

export const inputField = recipe({
base: {
display: 'flex',
flex: 1,
alignItems: 'center',
boxSizing: 'border-box',
display: 'block',
width: '100%',
margin: 0,
fontStyle: 'normal',
textAlign: 'start',
padding: spacing.defaultInputPadding,
borderRadius: spacing.defaultBorderRadius,
outline: `1px solid ${colors.defaultInputOutline}`,
color: vars.color.foreground.default,
fontFamily: vars.typography.fontFamily,
fontWeight: vars.typography.fontWeight.regular,
backgroundColor: 'transparent',
borderRadius: vars.radius.component.md,
borderWidth: '1px',
borderStyle: 'solid',
borderColor: vars.color.border.default,
paddingBlock: vars.spacing.component.sm,
paddingInline: vars.spacing.component.md,
outline: 'none',
cursor: 'text',

'@supports': {
'selector(:has(*))': {
selectors: {
'&::-webkit-search-cancel-button': {
appearance: 'none',
},
'&::placeholder': {
color: vars.color.foreground.muted,
},
'&:hover:not(:disabled):not(:focus):not(:read-only)': {
borderColor: vars.color.border.strong,
},
'&:read-only:not(:disabled)': {
backgroundColor: vars.color.background.muted,
borderColor: vars.color.border.default,
color: vars.color.foreground.default,
cursor: 'default',
},
'&:disabled': {
backgroundColor: vars.color.background.muted,
borderColor: vars.color.border.default,
color: vars.color.foreground.subtle,
cursor: 'not-allowed',
},
'&:where(:autofill, [data-com-onepassword-filled])': {
backgroundClip: 'text',
WebkitTextFillColor: vars.color.foreground.default,
},
},
},
variants: {
fontSize: {
12: { fontSize: vars.typography.fontSize['050'] },
14: { fontSize: vars.typography.fontSize['100'] },
16: { fontSize: vars.typography.fontSize['200'] },
18: { fontSize: vars.typography.fontSize['300'] },
20: { fontSize: vars.typography.fontSize['400'] },
24: { fontSize: vars.typography.fontSize['500'] },
28: { fontSize: vars.typography.fontSize['600'] },
32: { fontSize: vars.typography.fontSize['700'] },
36: { fontSize: vars.typography.fontSize['800'] },
48: { fontSize: vars.typography.fontSize['900'] },
},
validation: {
default: {
selectors: {
'&:where(:has(input:focus))': {
outline: `2px solid ${colors.defaultInputOutline}`,
'&:focus': {
borderColor: vars.color.border.focus,
outlineWidth: '2px',
outlineStyle: 'solid',
outlineColor: vars.color.border.focus,
outlineOffset: '-1px',
},
'&:where(:has(input:disabled))': {
backgroundColor: colors.disabledBackground,
},
},
error: {
borderColor: vars.color.status.danger.border,
selectors: {
'&:hover:not(:disabled):not(:focus):not(:read-only)': {
borderColor: vars.color.status.danger.border,
},
'&:disabled': {
borderColor: vars.color.border.default,
},
},
},
'not selector(:has(*))': {
success: {
borderColor: vars.color.status.success.border,
selectors: {
'&:where(:focus-within)': {
outline: `2px solid ${colors.defaultInputOutline}`,
outlineOffset: '-1px',
'&:hover:not(:disabled):not(:focus):not(:read-only)': {
borderColor: vars.color.status.success.border,
},
'&:disabled': {
borderColor: vars.color.border.default,
},
},
},
},
},
variants: {
fontSize: {
12: { fontSize: '12px' },
14: { fontSize: '14px' },
16: { fontSize: '16px' },
18: { fontSize: '18px' },
20: { fontSize: '20px' },
24: { fontSize: '24px' },
28: { fontSize: '28px' },
32: { fontSize: '32px' },
36: { fontSize: '36px' },
48: { fontSize: '48px' },
},
fontWeight: {
regular: { fontWeight: weight.regular },
medium: { fontWeight: weight.medium },
semiBold: { fontWeight: weight.semiBold },
bold: { fontWeight: weight.bold },
},
},
defaultVariants: {
fontSize: defaultFontSize,
fontWeight: defaultFontWeight,
validation: defaultValidation,
},
});

export const inputElement = style({
width: '100%',
display: 'flex',
alignItems: 'center',
textAlign: 'inherit',
outline: '1px solid transparent',
border: 'none',
fontSize: 'inherit',
fontWeight: 'inherit',

selectors: {
'&::-webkit-search-cancel-button': {
appearance: 'none',
},
},

'@supports': {
'selector(:has(*))': {
selectors: {
'&:where(:autofill, [data-com-onepassword-filled])': {
backgroundClip: 'text',
WebkitTextFillColor: color.gray900,
},
'&:where(:disabled)': {
backgroundColor: 'transparent',
},
},
},
},
/** Reserve trailing space so text/caret never sit under the absolute action. */
export const inputFieldWithAction = style({
paddingInlineEnd: `calc(${vars.spacing.component.xl} + ${vars.spacing.component.md} + ${vars.spacing.component.xs})`,
});

export const inputAction = style({
all: 'unset',
width: spacing.defaultActionSize,
height: spacing.defaultActionSize,
boxSizing: 'border-box',
position: 'absolute',
top: '50%',
right: vars.spacing.component.md,
transform: 'translateY(-50%)',
zIndex: 1,
width: vars.spacing.component.xl,
height: vars.spacing.component.xl,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: spacing.defaultBorderRadius,
color: vars.color.foreground.subtle,
borderRadius: vars.radius.component.md,
cursor: 'pointer',
transition: 'color 0.15s ease, background-color 0.15s ease',

'@supports': {
'selector(:has(*))': {
selectors: {
'&:focus': {
outline: `2px solid ${colors.defaultInputOutline}`,
outlineOffset: '3px',
},
},
selectors: {
'&:hover': {
color: vars.color.foreground.default,
backgroundColor: vars.color.background.muted,
},
'&:active': {
color: vars.color.foreground.default,
backgroundColor: vars.color.background.subtle,
},
'&:focus-visible': {
outline: `2px solid ${vars.color.border.focus}`,
outlineOffset: '3px',
},
},
});
55 changes: 50 additions & 5 deletions packages/input/src/Input.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
import { useRef, useState } from 'react';
import { type ReactNode, useRef, useState } from 'react';

import type { StoryObj } from '@storybook/react';
import { vars } from '@sipe-team/tokens';

import type { Meta, StoryObj } from '@storybook/react';

import { Action, Input } from './Input';

const DarkSurface = ({ children }: { children: ReactNode }) => (
<div
style={{
backgroundColor: vars.color.background.subtle,
padding: 24,
minWidth: 320,
borderRadius: vars.radius.component.lg,
}}
>
{children}
</div>
);

const meta = {
title: 'Components/Input',
component: Input,
parameters: {
layout: 'centered',
},
decorators: [
(Story) => (
<DarkSurface>
<Story />
</DarkSurface>
),
],
args: {
disabled: false,
fontSize: 16,
fontWeight: 'regular',
placeholder: 'placeholder',
type: 'text',
},
Expand All @@ -22,16 +43,19 @@ const meta = {
options: ['email', 'password', 'search', 'tel', 'text', 'url'],
control: { type: 'radio' },
},
validation: {
options: ['default', 'error', 'success'],
control: { type: 'radio' },
},
},
};
} satisfies Meta<typeof Input>;
export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
type: 'password',
fontWeight: 'regular',
fontSize: 20,
},

Expand All @@ -52,6 +76,27 @@ export const disabled: Story = {
},
};

export const ReadOnly: Story = {
args: {
readOnly: true,
defaultValue: 'read only value',
},
};

export const ValidationError: Story = {
args: {
validation: 'error',
defaultValue: 'invalid value',
},
};

export const ValidationSuccess: Story = {
args: {
validation: 'success',
defaultValue: 'valid value',
},
};

export const WithActionButton: Story = {
render: (args) => {
const ref = useRef<HTMLInputElement>(null);
Expand Down
Loading
Loading