Skip to content

Commit 1dfb18f

Browse files
authored
Input (#2059)
1 parent b814c50 commit 1dfb18f

File tree

12 files changed

+279
-13
lines changed

12 files changed

+279
-13
lines changed

.changeset/large-pandas-prove.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@theguild/components': minor
3+
---
4+
5+
Add Input component

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,13 @@
2929
"@storybook/addon-links": "8.4.2",
3030
"@storybook/core-common": "8.4.2",
3131
"@storybook/nextjs": "8.4.2",
32+
"@storybook/preview-api": "8.4.2",
3233
"@storybook/react": "8.4.2",
3334
"@storybook/theming": "8.4.2",
3435
"@svgr/webpack": "8.1.0",
3536
"@theguild/eslint-config": "0.13.2",
3637
"@theguild/prettier-config": "3.0.0",
37-
"@theguild/tailwind-config": "0.6.2",
38+
"@theguild/tailwind-config": "0.6.3",
3839
"@types/jest-image-snapshot": "6.4.0",
3940
"@types/react": "18.3.18",
4041
"@types/react-paginate": "7.1.4",

packages/components/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"types:check": "tsc --noEmit"
4545
},
4646
"peerDependencies": {
47-
"@theguild/tailwind-config": "^0.6.2",
47+
"@theguild/tailwind-config": "^0.6.3",
4848
"next": "^13 || ^14 || ^15.0.0",
4949
"react": "^18.2.0",
5050
"react-dom": "^18.2.0"
@@ -68,7 +68,7 @@
6868
"devDependencies": {
6969
"@svgr/plugin-svgo": "^8.1.0",
7070
"@theguild/editor": "workspace:*",
71-
"@theguild/tailwind-config": "0.6.2",
71+
"@theguild/tailwind-config": "0.6.3",
7272
"@types/dedent": "0.7.2",
7373
"@types/mdast": "4.0.4",
7474
"@types/react": "18.3.18",

packages/components/src/components/heading.stories.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Meta, StoryObj } from '@storybook/react';
2+
import { hiveThemeDecorator } from '../../../../.storybook/hive-theme-decorator';
23
import { Heading as _Heading, HeadingProps } from './heading';
34

45
export default {
@@ -23,6 +24,7 @@ export default {
2324
parameters: {
2425
padding: true,
2526
},
27+
decorators: [hiveThemeDecorator],
2628
} satisfies Meta<HeadingProps>;
2729

2830
export const Heading: StoryObj<HeadingProps> = {

packages/components/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export { HeroVideo } from './hero-video';
99
export * from './icons';
1010
export { Image } from './image';
1111
export { InfoList } from './info-list';
12+
export { Input } from './input';
1213
export { LegacyPackageCmd } from './legacy-package-cmd';
1314
export { MarketplaceList } from './marketplace-list';
1415
export { MarketplaceSearch } from './marketplace-search';
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { cn } from '../../cn';
2+
import { Severity } from '../../types/severity';
3+
import { InputShake } from './input-shake';
4+
5+
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
6+
severity?: Severity;
7+
message?: string;
8+
}
9+
10+
export function Input({ severity, message, ...props }: InputProps) {
11+
return (
12+
<div
13+
// todo: discuss colors with designers.
14+
// dark mode colors are kinda bad, but we don't really need
15+
// them just yet as this is used on yellow backgrounds
16+
className={cn(
17+
'rounded-[9px] border border-blue-400 bg-white outline-offset-2 focus-within:outline focus-within:outline-2 dark:border-neutral-400 dark:bg-neutral-800',
18+
'focus-visible:outline-green-800/40',
19+
'[&:focus-within:has([aria-invalid],:invalid)]:outline-critical-dark [&:has([aria-invalid],:invalid)]:border-critical-dark/50',
20+
severity === 'warning' &&
21+
'border-warning-bright/50 outline-warning-bright dark:border-warning-bright/50',
22+
severity === 'positive' &&
23+
'border-positive-dark/50 outline-positive-dark dark:border-positive-dark/50',
24+
)}
25+
>
26+
<InputShake severity={severity} />
27+
<input
28+
aria-invalid={severity === 'critical' ? true : undefined}
29+
className={cn(
30+
'w-full rounded-lg bg-white py-3 indent-4 font-medium transition-[background-color,padding] placeholder:text-green-800 placeholder-shown:bg-blue-100 autofill:shadow-[inset_0_0_0px_1000px_rgb(255,255,255)] autofill:[-webkit-text-fill-color:theme(colors.green.1000)] autofill:first-line:font-sans hover:bg-white focus:bg-white focus-visible:outline-none focus-visible:ring-0 dark:bg-neutral-800 dark:text-white dark:placeholder:text-neutral-300 dark:placeholder-shown:bg-neutral-900 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800',
31+
message && 'rounded-b-none py-2',
32+
props.className,
33+
)}
34+
{...props}
35+
/>
36+
<div
37+
style={{ height: message ? '25px' : '0px' }}
38+
className={cn(
39+
'overflow-hidden rounded-b-lg pl-4 pr-1 text-sm transition-all *:animate-in *:fade-in',
40+
severity === 'critical' && 'bg-critical-dark/10 dark:bg-critical-bright/20',
41+
severity === 'warning' && 'bg-warning-bright/10',
42+
severity === 'positive' && 'bg-positive-dark/10',
43+
)}
44+
>
45+
{message &&
46+
(severity === 'critical' ? (
47+
<p className="py-0.5 text-sm text-critical-dark dark:text-white">{message}</p>
48+
) : severity === 'warning' ? (
49+
<p className="py-0.5 text-sm text-warning-bright">{message}</p>
50+
) : (
51+
<p className="py-0.5 text-sm text-positive-dark">{message}</p>
52+
))}
53+
</div>
54+
</div>
55+
);
56+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use client';
2+
3+
import { useEffect, useRef } from 'react';
4+
import { Severity } from '../../types/severity';
5+
6+
interface InputShakeProps {
7+
severity?: Severity;
8+
}
9+
10+
/**
11+
* We need this hack to avoid shaking the input when it's first rendered
12+
* already as critical.
13+
*/
14+
export function InputShake({ severity }: InputShakeProps) {
15+
const ref = useRef<HTMLDivElement>(null);
16+
const prevSeverityRef = useRef<Severity | undefined>(severity);
17+
18+
useEffect(() => {
19+
const shouldShake = prevSeverityRef.current !== 'critical' && severity === 'critical';
20+
21+
prevSeverityRef.current = severity;
22+
const container = ref.current?.parentElement;
23+
if (container && shouldShake) {
24+
container.classList.add('animate-shake');
25+
const cleanUp = () => container.classList.remove('animate-shake');
26+
container.addEventListener('animationend', cleanUp, { once: true });
27+
}
28+
}, [severity]);
29+
30+
return <div ref={ref} className="hidden" />;
31+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { useArgs } from '@storybook/preview-api';
2+
import { Meta, StoryObj } from '@storybook/react';
3+
import { Input, InputProps } from '.';
4+
import { hiveThemeDecorator } from '../../../../../.storybook/hive-theme-decorator';
5+
6+
export default {
7+
title: 'Components/Input',
8+
component: Input,
9+
argTypes: {
10+
severity: {
11+
control: 'select',
12+
options: ['critical', 'warning', 'positive', undefined],
13+
},
14+
message: {
15+
control: 'text',
16+
},
17+
type: {
18+
control: 'select',
19+
options: ['text', 'email', 'password', 'number'],
20+
},
21+
disabled: {
22+
control: 'boolean',
23+
},
24+
required: {
25+
control: 'boolean',
26+
},
27+
},
28+
parameters: {
29+
padding: true,
30+
},
31+
decorators: [
32+
hiveThemeDecorator,
33+
(Story: React.FC) => (
34+
<div className="max-w-xl">
35+
<Story />
36+
</div>
37+
),
38+
],
39+
} satisfies Meta<InputProps>;
40+
41+
export const Default: StoryObj<InputProps> = {
42+
args: {
43+
placeholder: 'Email',
44+
type: 'text',
45+
},
46+
};
47+
48+
export const Critical: StoryObj<InputProps> = {
49+
args: {
50+
severity: 'critical',
51+
message: 'Please enter a valid email address',
52+
type: 'email',
53+
value: '+48 222 500 151',
54+
},
55+
render: () => {
56+
const [args, update] = useArgs<InputProps>();
57+
58+
return (
59+
<Input
60+
{...args}
61+
onChange={event => {
62+
if (
63+
event.target.value.toString().match(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)
64+
) {
65+
update({ severity: undefined, message: undefined });
66+
} else {
67+
update({ severity: 'critical', message: 'Please enter a valid email address' });
68+
}
69+
70+
update({ value: event.target.value });
71+
}}
72+
/>
73+
);
74+
},
75+
};
76+
77+
export const Warning: StoryObj<InputProps> = {
78+
args: {
79+
severity: 'warning',
80+
message: 'Weak password',
81+
type: 'password',
82+
value: '1234',
83+
},
84+
};
85+
86+
export const Positive: StoryObj<InputProps> = {
87+
args: {
88+
severity: 'positive',
89+
message: 'Very strong password',
90+
type: 'password',
91+
value: 'Wednesday, 2 April 2025, GraphQL will prevail!',
92+
},
93+
};
94+
95+
export const Disabled: StoryObj<InputProps> = {
96+
args: {
97+
disabled: true,
98+
placeholder: 'Disabled input',
99+
},
100+
};
101+
102+
export const Required: StoryObj<InputProps> = {
103+
args: {
104+
required: true,
105+
placeholder: 'This should not be empty',
106+
},
107+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type Severity = 'critical' | 'positive' | 'warning';

packages/components/style.css

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,62 @@
149149
--nextra-navbar-height: 64px;
150150
}
151151
}
152+
153+
:root {
154+
--hive-ease-overshoot-far: linear(
155+
0 0%,
156+
0.5007 7.21%,
157+
0.7803 12.29%,
158+
0.8883 14.93%,
159+
0.9724 17.63%,
160+
1.0343 20.44%,
161+
1.0754 23.44%,
162+
1.0898 25.22%,
163+
1.0984 27.11%,
164+
1.1014 29.15%,
165+
1.0989 31.4%,
166+
1.0854 35.23%,
167+
1.0196 48.86%,
168+
1.0043 54.06%,
169+
0.9956 59.6%,
170+
0.9925 68.11%,
171+
1 100%
172+
);
173+
174+
--hive-ease-overshoot-a-bit: linear(
175+
0 0%,
176+
0.5007 7.21%,
177+
0.7803 12.29%,
178+
0.8883 14.93%,
179+
0.9724 17.63%,
180+
1.011319 20.44%,
181+
1.024882 23.44%,
182+
1.029634 25.22%,
183+
1.032472 27.11%,
184+
1.033462 29.15%,
185+
1.032637 31.4%,
186+
1.028182 35.23%,
187+
1.006468 48.86%,
188+
1.001419 54.06%,
189+
0.9956 59.6%,
190+
0.9925 68.11%,
191+
1 100%
192+
);
193+
}
194+
195+
@keyframes hive-shake {
196+
0%,
197+
100% {
198+
transform: translateX(0);
199+
}
200+
25% {
201+
transform: rotate(-0.4deg);
202+
}
203+
75% {
204+
transform: rotate(0.3deg);
205+
}
206+
}
207+
208+
.animate-shake:not(:focus-within) {
209+
animation: hive-shake 0.3s ease;
210+
}

0 commit comments

Comments
 (0)