Skip to content

Commit 78222d3

Browse files
committed
Reapply "feat(ui): add Button, Input, Label, Textarea components and update package dependencies"
This reverts commit acbe421.
1 parent eb75e9b commit 78222d3

12 files changed

Lines changed: 198 additions & 39 deletions

File tree

apps/frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
"@nbw/config": "workspace:*",
2121
"@nbw/song": "workspace:*",
2222
"@nbw/thumbnail": "workspace:*",
23+
"@nbw/validation": "workspace:*",
2324
"@next/mdx": "^16.0.8",
2425
"@next/third-parties": "^16.0.8",
2526
"@radix-ui/react-dialog": "^1.1.15",
27+
"@radix-ui/react-label": "^2.1.8",
2628
"@radix-ui/react-popover": "^1.1.15",
2729
"@radix-ui/react-select": "^2.2.6",
2830
"@radix-ui/react-slider": "^1.3.6",
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Slot } from '@radix-ui/react-slot';
2+
import { cva, type VariantProps } from 'class-variance-authority';
3+
import * as React from 'react';
4+
5+
import { cn } from '@web/lib/utils';
6+
7+
const buttonVariants = cva(
8+
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-oklch(0.705 0.015 286.067) disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:focus-visible:ring-oklch(0.552 0.016 285.938)',
9+
{
10+
variants: {
11+
variant: {
12+
default:
13+
'bg-oklch(0.21 0.006 285.885) text-oklch(0.985 0 0) shadow hover:bg-oklch(0.21 0.006 285.885)/90 dark:bg-oklch(0.92 0.004 286.32) dark:text-oklch(0.21 0.006 285.885) dark:hover:bg-oklch(0.92 0.004 286.32)/90',
14+
destructive:
15+
'bg-oklch(0.577 0.245 27.325) text-destructive-foreground shadow-sm hover:bg-oklch(0.577 0.245 27.325)/90 dark:bg-oklch(0.704 0.191 22.216) dark:hover:bg-oklch(0.704 0.191 22.216)/90',
16+
outline:
17+
'border border-oklch(0.92 0.004 286.32) bg-oklch(1 0 0) shadow-sm hover:bg-oklch(0.967 0.001 286.375) hover:text-oklch(0.21 0.006 285.885) dark:border-oklch(1 0 0 / 15%) dark:bg-oklch(0.141 0.005 285.823) dark:hover:bg-oklch(0.274 0.006 286.033) dark:hover:text-oklch(0.985 0 0)',
18+
secondary:
19+
'bg-oklch(0.967 0.001 286.375) text-oklch(0.21 0.006 285.885) shadow-sm hover:bg-oklch(0.967 0.001 286.375)/80 dark:bg-oklch(0.274 0.006 286.033) dark:text-oklch(0.985 0 0) dark:hover:bg-oklch(0.274 0.006 286.033)/80',
20+
ghost:
21+
'hover:bg-oklch(0.967 0.001 286.375) hover:text-oklch(0.21 0.006 285.885) dark:hover:bg-oklch(0.274 0.006 286.033) dark:hover:text-oklch(0.985 0 0)',
22+
link: 'text-oklch(0.21 0.006 285.885) underline-offset-4 hover:underline dark:text-oklch(0.92 0.004 286.32)',
23+
},
24+
size: {
25+
default: 'h-9 px-4 py-2',
26+
sm: 'h-8 rounded-md px-3 text-xs',
27+
lg: 'h-10 rounded-md px-8',
28+
icon: 'h-9 w-9',
29+
},
30+
},
31+
defaultVariants: {
32+
variant: 'default',
33+
size: 'default',
34+
},
35+
},
36+
);
37+
38+
export interface ButtonProps
39+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
40+
VariantProps<typeof buttonVariants> {
41+
asChild?: boolean;
42+
}
43+
44+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
45+
({ className, variant, size, asChild = false, ...props }, ref) => {
46+
const Comp = asChild ? Slot : 'button';
47+
return (
48+
<Comp
49+
className={cn(buttonVariants({ variant, size }), className)}
50+
ref={ref}
51+
{...props}
52+
/>
53+
);
54+
},
55+
);
56+
Button.displayName = 'Button';
57+
58+
export { Button, buttonVariants };
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as React from 'react';
2+
3+
import { cn } from '@web/lib/utils';
4+
5+
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
6+
({ className, type, ...props }, ref) => {
7+
return (
8+
<input
9+
type={type}
10+
className={cn(
11+
'flex h-9 w-full rounded-md border border-oklch(0.92 0.004 286.32) bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-oklch(0.141 0.005 285.823) placeholder:text-oklch(0.552 0.016 285.938) focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-oklch(0.705 0.015 286.067) disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:border-oklch(1 0 0 / 10%) dark:border-oklch(1 0 0 / 15%) dark:file:text-oklch(0.985 0 0) dark:placeholder:text-oklch(0.705 0.015 286.067) dark:focus-visible:ring-oklch(0.552 0.016 285.938)',
12+
className,
13+
)}
14+
ref={ref}
15+
{...props}
16+
/>
17+
);
18+
},
19+
);
20+
Input.displayName = 'Input';
21+
22+
export { Input };
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
'use client';
2+
3+
import * as LabelPrimitive from '@radix-ui/react-label';
4+
import { cva, type VariantProps } from 'class-variance-authority';
5+
import * as React from 'react';
6+
7+
import { cn } from '@web/lib/utils';
8+
9+
const labelVariants = cva(
10+
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
11+
);
12+
13+
const Label = React.forwardRef<
14+
React.ElementRef<typeof LabelPrimitive.Root>,
15+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
16+
VariantProps<typeof labelVariants>
17+
>(({ className, ...props }, ref) => (
18+
<LabelPrimitive.Root
19+
ref={ref}
20+
className={cn(labelVariants(), className)}
21+
{...props}
22+
/>
23+
));
24+
Label.displayName = LabelPrimitive.Root.displayName;
25+
26+
export { Label };
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as React from 'react';
2+
3+
import { cn } from '@web/lib/utils';
4+
5+
const Textarea = React.forwardRef<
6+
HTMLTextAreaElement,
7+
React.ComponentProps<'textarea'>
8+
>(({ className, ...props }, ref) => {
9+
return (
10+
<textarea
11+
className={cn(
12+
'flex min-h-[60px] w-full rounded-md border border-oklch(0.92 0.004 286.32) bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-oklch(0.552 0.016 285.938) focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-oklch(0.705 0.015 286.067) disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:border-oklch(1 0 0 / 10%) dark:border-oklch(1 0 0 / 15%) dark:placeholder:text-oklch(0.705 0.015 286.067) dark:focus-visible:ring-oklch(0.552 0.016 285.938)',
13+
className,
14+
)}
15+
ref={ref}
16+
{...props}
17+
/>
18+
);
19+
});
20+
Textarea.displayName = 'Textarea';
21+
22+
export { Textarea };

apps/frontend/src/modules/song-upload/components/client/context/UploadSong.context.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import { create } from 'zustand';
1313

1414
import { BG_COLORS, THUMBNAIL_CONSTANTS, UPLOAD_CONSTANTS } from '@nbw/config';
1515
import { parseSongFromBuffer, type SongFileType } from '@nbw/song';
16-
import axiosInstance from '@web/lib/axios';
17-
import { InvalidTokenError, getTokenLocal } from '@web/lib/axios/token.utils';
1816
import {
1917
UploadSongFormInput,
2018
UploadSongFormOutput,
2119
uploadSongFormSchema,
2220
} from '@nbw/validation';
21+
import axiosInstance from '@web/lib/axios';
22+
import { InvalidTokenError, getTokenLocal } from '@web/lib/axios/token.utils';
2323

2424
import UploadCompleteModal from '../UploadCompleteModal';
2525

apps/frontend/src/modules/song/components/client/SongThumbnailInput.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
22
import { UseFormReturn } from 'react-hook-form';
33

44
import { BG_COLORS, THUMBNAIL_CONSTANTS } from '@nbw/config';
5+
import { EditSongFormInput, UploadSongFormInput } from '@nbw/validation';
56
import { cn } from '@web/lib/utils';
67
import { Slider } from '@web/modules/shared/components/client/FormElements';
78
import {
@@ -11,7 +12,6 @@ import {
1112
} from '@web/modules/shared/components/tooltip';
1213

1314
import { useSongProvider } from './context/Song.context';
14-
import { EditSongFormInput, UploadSongFormInput } from '@nbw/validation';
1515
import { ThumbnailRendererCanvas } from './ThumbnailRenderer';
1616

1717
const formatZoomLevel = (zoomLevel: number) => {

apps/frontend/src/modules/song/components/client/ThumbnailRenderer.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { UseFormReturn } from 'react-hook-form';
66
import { THUMBNAIL_CONSTANTS } from '@nbw/config';
77
import { NoteQuadTree } from '@nbw/song';
88
import { drawNotesOffscreen, swap } from '@nbw/thumbnail/browser';
9-
109
import { UploadSongFormInput } from '@nbw/validation';
1110

1211
type ThumbnailRendererCanvasProps = {

apps/frontend/src/modules/user/components/client/ProfileBioEditor.tsx

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import axiosInstance from '@web/lib/axios';
1010
import { getTokenLocal } from '@web/lib/axios/token.utils';
1111
import { type SocialLinks } from '@web/modules/auth/types/User';
1212
import { ProfileBioMarkdown } from '@web/modules/shared/components/ProfileBioMarkdown';
13+
import { Button } from '@web/modules/shared/components/ui/button';
14+
import { Input } from '@web/modules/shared/components/ui/input';
15+
import { Label } from '@web/modules/shared/components/ui/label';
16+
import { Textarea } from '@web/modules/shared/components/ui/textarea';
1317

1418
import { socialKeys, SOCIAL_LINK_ICONS } from './socialKeys';
1519

@@ -42,6 +46,9 @@ function cleanSocialLinks(links: SocialLinks): SocialLinks {
4246
return out;
4347
}
4448

49+
const fieldInputClass =
50+
'border-zinc-600 bg-zinc-900 text-zinc-100 placeholder:text-zinc-500';
51+
4552
export function ProfileBioEditor({ profile, isOwner }: ProfileBioEditorProps) {
4653
const router = useRouter();
4754
const [isEditing, setIsEditing] = useState(false);
@@ -99,34 +106,40 @@ export function ProfileBioEditor({ profile, isOwner }: ProfileBioEditorProps) {
99106
<div className='flex items-start justify-between gap-2'>
100107
<h2 className='text-lg font-semibold text-zinc-200'>About</h2>
101108
{isOwner && !isEditing && (
102-
<button
109+
<Button
103110
type='button'
111+
variant='ghost'
112+
size='icon'
104113
onClick={() => setIsEditing(true)}
105-
className='text-zinc-500 hover:text-zinc-300 p-1 rounded'
114+
className='text-zinc-500 hover:bg-transparent hover:text-zinc-300'
106115
aria-label='Edit profile'
107116
>
108-
<Pencil className='w-5 h-5' />
109-
</button>
117+
<Pencil className='h-5 w-5' />
118+
</Button>
110119
)}
111120
{isOwner && isEditing && (
112121
<div className='flex gap-1'>
113-
<button
122+
<Button
114123
type='button'
124+
variant='ghost'
125+
size='icon'
115126
onClick={cancel}
116-
className='text-zinc-500 hover:text-zinc-300 p-1 rounded'
127+
className='text-zinc-500 hover:bg-transparent hover:text-zinc-300'
117128
aria-label='Cancel'
118129
>
119-
<X className='w-5 h-5' />
120-
</button>
121-
<button
130+
<X className='h-5 w-5' />
131+
</Button>
132+
<Button
122133
type='button'
134+
variant='ghost'
135+
size='icon'
123136
onClick={() => void save()}
124137
disabled={saving}
125-
className='text-zinc-500 hover:text-zinc-300 p-1 rounded disabled:opacity-50'
138+
className='text-zinc-500 hover:bg-transparent hover:text-zinc-300 disabled:opacity-50'
126139
aria-label='Save'
127140
>
128-
<Check className='w-5 h-5' />
129-
</button>
141+
<Check className='h-5 w-5' />
142+
</Button>
130143
</div>
131144
)}
132145
</div>
@@ -135,25 +148,30 @@ export function ProfileBioEditor({ profile, isOwner }: ProfileBioEditorProps) {
135148

136149
{isEditing ? (
137150
<div className='mt-3 flex flex-col gap-4'>
138-
<textarea
151+
<Textarea
139152
value={description}
140153
onChange={(e) => setDescription(e.target.value)}
141154
rows={12}
142-
className='w-full rounded-lg border border-zinc-600 bg-zinc-950 p-3 text-zinc-100 font-mono text-sm'
155+
className={`font-mono text-sm min-h-48 ${fieldInputClass}`}
143156
/>
144157
<div className='grid grid-cols-1 sm:grid-cols-2 gap-3'>
145158
{socialKeys.map((key) => {
146159
const Icon = SOCIAL_LINK_ICONS[key];
160+
const id = `profile-social-${key}`;
147161
return (
148-
<label key={key} className='flex flex-col gap-1 text-sm'>
149-
<span className='text-zinc-400 flex items-center gap-2 capitalize'>
162+
<div key={key} className='flex flex-col gap-1 text-sm'>
163+
<Label
164+
htmlFor={id}
165+
className='text-zinc-400 flex items-center gap-2 capitalize font-normal'
166+
>
150167
<Icon
151168
className='w-4 h-4 shrink-0 text-zinc-500'
152169
aria-hidden
153170
/>
154171
{key}
155-
</span>
156-
<input
172+
</Label>
173+
<Input
174+
id={id}
157175
type='url'
158176
value={socialLinks[key] ?? ''}
159177
onChange={(e) =>
@@ -162,10 +180,10 @@ export function ProfileBioEditor({ profile, isOwner }: ProfileBioEditorProps) {
162180
[key]: e.target.value,
163181
}))
164182
}
165-
className='rounded border border-zinc-600 bg-zinc-900 px-2 py-1 text-zinc-100'
183+
className={`h-8 px-2 py-1 text-sm ${fieldInputClass}`}
166184
placeholder='https://'
167185
/>
168-
</label>
186+
</div>
169187
);
170188
})}
171189
</div>

apps/frontend/src/modules/user/components/client/ProfilePublicNameEditor.tsx

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { useCallback, useEffect, useState } from 'react';
77
import type { PublicProfileDto } from '@nbw/validation';
88
import axiosInstance from '@web/lib/axios';
99
import { getTokenLocal } from '@web/lib/axios/token.utils';
10+
import { Button } from '@web/modules/shared/components/ui/button';
11+
import { Input } from '@web/modules/shared/components/ui/input';
1012

1113
type ProfilePublicNameEditorProps = {
1214
profile: PublicProfileDto;
@@ -66,12 +68,12 @@ export function ProfilePublicNameEditor({
6668
<div className='min-w-0'>
6769
<div className='flex items-center gap-2'>
6870
{isEditing ? (
69-
<input
71+
<Input
7072
type='text'
7173
value={publicName}
7274
onChange={(e) => setPublicName(e.target.value)}
7375
maxLength={100}
74-
className='flex-1 min-w-0 text-2xl font-bold text-zinc-100 bg-zinc-900 border border-zinc-600 rounded-lg px-2 py-1'
76+
className='flex-1 min-w-0 text-2xl font-bold text-zinc-100 h-auto py-1 border-zinc-600 bg-zinc-900'
7577
aria-label='Display name'
7678
/>
7779
) : (
@@ -80,34 +82,40 @@ export function ProfilePublicNameEditor({
8082
</h1>
8183
)}
8284
{!isEditing && (
83-
<button
85+
<Button
8486
type='button'
87+
variant='ghost'
88+
size='icon'
8589
onClick={() => setIsEditing(true)}
86-
className='text-zinc-500 hover:text-zinc-300 p-1 rounded shrink-0'
90+
className='shrink-0 text-zinc-500 hover:bg-transparent hover:text-zinc-300'
8791
aria-label='Edit display name'
8892
>
89-
<Pencil className='w-5 h-5' />
90-
</button>
93+
<Pencil className='h-5 w-5' />
94+
</Button>
9195
)}
9296
{isEditing && (
9397
<div className='flex gap-1 shrink-0'>
94-
<button
98+
<Button
9599
type='button'
100+
variant='ghost'
101+
size='icon'
96102
onClick={cancel}
97-
className='text-zinc-500 hover:text-zinc-300 p-1 rounded'
103+
className='text-zinc-500 hover:bg-transparent hover:text-zinc-300'
98104
aria-label='Cancel'
99105
>
100-
<X className='w-5 h-5' />
101-
</button>
102-
<button
106+
<X className='h-5 w-5' />
107+
</Button>
108+
<Button
103109
type='button'
110+
variant='ghost'
111+
size='icon'
104112
onClick={() => void save()}
105113
disabled={saving || publicName.trim().length === 0}
106-
className='text-zinc-500 hover:text-zinc-300 p-1 rounded disabled:opacity-50'
114+
className='text-zinc-500 hover:bg-transparent hover:text-zinc-300 disabled:opacity-50'
107115
aria-label='Save'
108116
>
109-
<Check className='w-5 h-5' />
110-
</button>
117+
<Check className='h-5 w-5' />
118+
</Button>
111119
</div>
112120
)}
113121
</div>

0 commit comments

Comments
 (0)