@suphark/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/components.json +21 -0
- package/package.json +108 -0
- package/scripts/audit-base-ui-props.mjs +179 -0
- package/scripts/audit-tailwind-typography.mjs +100 -0
- package/src/components/design-system/design-system-showcase.tsx +138 -0
- package/src/components/design-system/sections/showcase-data-display.tsx +454 -0
- package/src/components/design-system/sections/showcase-feedback.tsx +273 -0
- package/src/components/design-system/sections/showcase-forms.tsx +373 -0
- package/src/components/design-system/sections/showcase-foundations.tsx +384 -0
- package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +397 -0
- package/src/components/design-system/sections/showcase-overlays-navigation.tsx +479 -0
- package/src/components/design-system/sections/showcase-overview.tsx +368 -0
- package/src/components/design-system/sections/showcase-primitives.tsx +463 -0
- package/src/components/shared/__tests__/multi-select-combobox.test.tsx +180 -0
- package/src/components/shared/__tests__/name-id-fields.test.ts +24 -0
- package/src/components/shared/button/add-new-button.tsx +19 -0
- package/src/components/shared/button/base/action-button.tsx +134 -0
- package/src/components/shared/button/base/link-button.tsx +130 -0
- package/src/components/shared/button/cancel-button.tsx +27 -0
- package/src/components/shared/button/confirm-button.tsx +26 -0
- package/src/components/shared/button/delete-button.tsx +36 -0
- package/src/components/shared/button/floating-save-button.tsx +89 -0
- package/src/components/shared/button/index.ts +13 -0
- package/src/components/shared/button/print-button.tsx +67 -0
- package/src/components/shared/button/refresh-button.tsx +63 -0
- package/src/components/shared/button/save-button.tsx +29 -0
- package/src/components/shared/button/submit-button.tsx +58 -0
- package/src/components/shared/client-redirect.tsx +11 -0
- package/src/components/shared/combobox.tsx +116 -0
- package/src/components/shared/data-list.tsx +65 -0
- package/src/components/shared/date/date-range-picker.tsx +106 -0
- package/src/components/shared/description-list.tsx +27 -0
- package/src/components/shared/empty-row.tsx +13 -0
- package/src/components/shared/empty-state.tsx +31 -0
- package/src/components/shared/env-snippet.tsx +62 -0
- package/src/components/shared/form/__tests__/simple-select.test.tsx +72 -0
- package/src/components/shared/form/form-field-boolean.tsx +93 -0
- package/src/components/shared/form/form-field-color-select.tsx +86 -0
- package/src/components/shared/form/form-field-combobox.tsx +167 -0
- package/src/components/shared/form/form-field-date.tsx +55 -0
- package/src/components/shared/form/form-field-formatted-number.tsx +103 -0
- package/src/components/shared/form/form-field-input.tsx +372 -0
- package/src/components/shared/form/form-field-radio-cards.tsx +127 -0
- package/src/components/shared/form/form-field-radio-group.tsx +56 -0
- package/src/components/shared/form/form-field-select.tsx +312 -0
- package/src/components/shared/form/simple-select.tsx +152 -0
- package/src/components/shared/form-dialog.tsx +109 -0
- package/src/components/shared/form-message.tsx +30 -0
- package/src/components/shared/initials.tsx +33 -0
- package/src/components/shared/layout/error-state.tsx +160 -0
- package/src/components/shared/layout/page-shell.tsx +57 -0
- package/src/components/shared/link/transition-link.tsx +72 -0
- package/src/components/shared/multi-select-combobox.tsx +392 -0
- package/src/components/shared/name-id-fields.tsx +133 -0
- package/src/components/shared/navigation/page-breadcrumb.tsx +100 -0
- package/src/components/shared/notice.tsx +35 -0
- package/src/components/shared/number/formatted-number.tsx +133 -0
- package/src/components/shared/page-header.tsx +25 -0
- package/src/components/shared/page-skeleton.tsx +18 -0
- package/src/components/shared/row-actions.tsx +93 -0
- package/src/components/shared/row-menu.tsx +55 -0
- package/src/components/shared/secret-reveal.tsx +53 -0
- package/src/components/shared/section-card.tsx +44 -0
- package/src/components/shared/stat.tsx +26 -0
- package/src/components/shared/status-badge.tsx +47 -0
- package/src/components/shared/text/primary-secondary-text.tsx +186 -0
- package/src/components/shared/theme/index.ts +4 -0
- package/src/components/shared/theme/theme-menu-items.tsx +43 -0
- package/src/components/shared/theme/theme-provider.tsx +39 -0
- package/src/components/shared/theme/theme-switch.tsx +36 -0
- package/src/components/shared/theme/theme-toggle.tsx +76 -0
- package/src/components/shared/time/client-date-formatter.tsx +188 -0
- package/src/components/shared/ui-text.tsx +47 -0
- package/src/components/ui/__tests__/combobox.test.tsx +40 -0
- package/src/components/ui/__tests__/date-picker.test.tsx +53 -0
- package/src/components/ui/__tests__/slider.test.tsx +18 -0
- package/src/components/ui/__tests__/tabs.test.tsx +33 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert-dialog.tsx +161 -0
- package/src/components/ui/alert.tsx +68 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/attachment.tsx +194 -0
- package/src/components/ui/avatar.tsx +92 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/breadcrumb.tsx +102 -0
- package/src/components/ui/button-group.tsx +77 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/calendar.tsx +184 -0
- package/src/components/ui/card.tsx +87 -0
- package/src/components/ui/carousel.tsx +230 -0
- package/src/components/ui/chart.tsx +338 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +17 -0
- package/src/components/ui/combobox.tsx +272 -0
- package/src/components/ui/command.tsx +180 -0
- package/src/components/ui/context-menu.tsx +244 -0
- package/src/components/ui/date-picker.tsx +308 -0
- package/src/components/ui/dialog.tsx +137 -0
- package/src/components/ui/drawer.tsx +208 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/empty.tsx +93 -0
- package/src/components/ui/field.tsx +223 -0
- package/src/components/ui/form.tsx +161 -0
- package/src/components/ui/hover-card.tsx +46 -0
- package/src/components/ui/input-group.tsx +145 -0
- package/src/components/ui/input-otp.tsx +85 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/item.tsx +187 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/menubar.tsx +266 -0
- package/src/components/ui/navigation-menu.tsx +160 -0
- package/src/components/ui/pagination.tsx +117 -0
- package/src/components/ui/popover.tsx +76 -0
- package/src/components/ui/progress.tsx +66 -0
- package/src/components/ui/radio-group.tsx +38 -0
- package/src/components/ui/relative-date.tsx +40 -0
- package/src/components/ui/resizable.tsx +41 -0
- package/src/components/ui/scroll-area.tsx +67 -0
- package/src/components/ui/searchable-select.tsx +124 -0
- package/src/components/ui/select.tsx +189 -0
- package/src/components/ui/separator.tsx +21 -0
- package/src/components/ui/sheet.tsx +124 -0
- package/src/components/ui/sidebar.tsx +691 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +55 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/table.tsx +95 -0
- package/src/components/ui/tabs.tsx +79 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toggle-group.tsx +86 -0
- package/src/components/ui/toggle.tsx +44 -0
- package/src/components/ui/tooltip.tsx +54 -0
- package/src/hooks/use-copy-to-clipboard.ts +27 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/index.ts +156 -0
- package/src/lib/formatters.ts +44 -0
- package/src/lib/permission.ts +75 -0
- package/src/lib/status-colors.ts +52 -0
- package/src/lib/utils.ts +6 -0
- package/src/next.ts +15 -0
- package/src/styles.css +214 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Label } from "@suphark/ui/components/ui/label";
|
|
4
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import {
|
|
7
|
+
Controller,
|
|
8
|
+
type ControllerProps,
|
|
9
|
+
type FieldPath,
|
|
10
|
+
type FieldValues,
|
|
11
|
+
FormProvider,
|
|
12
|
+
useFormContext,
|
|
13
|
+
useFormState,
|
|
14
|
+
} from "react-hook-form";
|
|
15
|
+
|
|
16
|
+
const Form = FormProvider;
|
|
17
|
+
|
|
18
|
+
type FormFieldContextValue<
|
|
19
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
20
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
21
|
+
> = {
|
|
22
|
+
name: TName;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
|
|
26
|
+
|
|
27
|
+
const FormField = <
|
|
28
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
29
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
30
|
+
>({
|
|
31
|
+
...props
|
|
32
|
+
}: ControllerProps<TFieldValues, TName>) => {
|
|
33
|
+
return (
|
|
34
|
+
<FormFieldContext.Provider value={{ name: props.name }}>
|
|
35
|
+
<Controller {...props} />
|
|
36
|
+
</FormFieldContext.Provider>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const useFormField = () => {
|
|
41
|
+
const fieldContext = React.useContext(FormFieldContext);
|
|
42
|
+
const itemContext = React.useContext(FormItemContext);
|
|
43
|
+
const { getFieldState } = useFormContext();
|
|
44
|
+
const formState = useFormState({ name: fieldContext.name });
|
|
45
|
+
const fieldState = getFieldState(fieldContext.name, formState);
|
|
46
|
+
|
|
47
|
+
if (!fieldContext) {
|
|
48
|
+
throw new Error("useFormField should be used within <FormField>");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { id } = itemContext;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
id,
|
|
55
|
+
name: fieldContext.name,
|
|
56
|
+
formItemId: `${id}-form-item`,
|
|
57
|
+
formDescriptionId: `${id}-form-item-description`,
|
|
58
|
+
formMessageId: `${id}-form-item-message`,
|
|
59
|
+
...fieldState,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type FormItemContextValue = {
|
|
64
|
+
id: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
|
|
68
|
+
|
|
69
|
+
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
|
70
|
+
const id = React.useId();
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<FormItemContext.Provider value={{ id }}>
|
|
74
|
+
<div data-slot="form-item" className={cn("grid gap-2", className)} {...props} />
|
|
75
|
+
</FormItemContext.Provider>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function FormLabel({ className, ...props }: React.ComponentProps<typeof Label>) {
|
|
80
|
+
const { error, formItemId } = useFormField();
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Label
|
|
84
|
+
data-slot="form-label"
|
|
85
|
+
data-error={!!error}
|
|
86
|
+
className={cn("data-[error=true]:text-destructive", className)}
|
|
87
|
+
htmlFor={formItemId}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function FormControl({ children, ...props }: React.ComponentProps<"div">) {
|
|
94
|
+
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
|
|
95
|
+
|
|
96
|
+
if (React.isValidElement(children)) {
|
|
97
|
+
return React.cloneElement(children as React.ReactElement<any>, {
|
|
98
|
+
id: formItemId,
|
|
99
|
+
"aria-describedby": !error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`,
|
|
100
|
+
"aria-invalid": !!error,
|
|
101
|
+
...props,
|
|
102
|
+
...(children.props as Record<string, unknown>),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div
|
|
108
|
+
data-slot="form-control"
|
|
109
|
+
id={formItemId}
|
|
110
|
+
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
|
|
111
|
+
aria-invalid={!!error}
|
|
112
|
+
{...props}
|
|
113
|
+
>
|
|
114
|
+
{children}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
120
|
+
const { formDescriptionId } = useFormField();
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<p
|
|
124
|
+
data-slot="form-description"
|
|
125
|
+
id={formDescriptionId}
|
|
126
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
127
|
+
{...props}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
|
133
|
+
const { error, formMessageId } = useFormField();
|
|
134
|
+
const body = error ? String(error?.message ?? "") : props.children;
|
|
135
|
+
|
|
136
|
+
if (!body) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<p
|
|
142
|
+
data-slot="form-message"
|
|
143
|
+
id={formMessageId}
|
|
144
|
+
className={cn("text-destructive text-sm", className)}
|
|
145
|
+
{...props}
|
|
146
|
+
>
|
|
147
|
+
{body}
|
|
148
|
+
</p>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export {
|
|
153
|
+
Form,
|
|
154
|
+
FormControl,
|
|
155
|
+
FormDescription,
|
|
156
|
+
FormField,
|
|
157
|
+
FormItem,
|
|
158
|
+
FormLabel,
|
|
159
|
+
FormMessage,
|
|
160
|
+
useFormField,
|
|
161
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
6
|
+
|
|
7
|
+
function HoverCard({ ...props }: PreviewCardPrimitive.Root.Props) {
|
|
8
|
+
return <PreviewCardPrimitive.Root data-slot="hover-card" {...props} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function HoverCardTrigger({ ...props }: PreviewCardPrimitive.Trigger.Props) {
|
|
12
|
+
return <PreviewCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function HoverCardContent({
|
|
16
|
+
className,
|
|
17
|
+
side = "bottom",
|
|
18
|
+
sideOffset = 4,
|
|
19
|
+
align = "center",
|
|
20
|
+
alignOffset = 4,
|
|
21
|
+
...props
|
|
22
|
+
}: PreviewCardPrimitive.Popup.Props &
|
|
23
|
+
Pick<PreviewCardPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
|
|
24
|
+
return (
|
|
25
|
+
<PreviewCardPrimitive.Portal data-slot="hover-card-portal">
|
|
26
|
+
<PreviewCardPrimitive.Positioner
|
|
27
|
+
align={align}
|
|
28
|
+
alignOffset={alignOffset}
|
|
29
|
+
side={side}
|
|
30
|
+
sideOffset={sideOffset}
|
|
31
|
+
className="isolate z-50"
|
|
32
|
+
>
|
|
33
|
+
<PreviewCardPrimitive.Popup
|
|
34
|
+
data-slot="hover-card-content"
|
|
35
|
+
className={cn(
|
|
36
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:fade-in-0 data-open:zoom-in-95 data-closed:fade-out-0 data-closed:zoom-out-95 z-50 w-64 origin-(--transform-origin) rounded-lg bg-popover p-2.5 text-popover-foreground text-sm shadow-md outline-hidden ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in",
|
|
37
|
+
className,
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
</PreviewCardPrimitive.Positioner>
|
|
42
|
+
</PreviewCardPrimitive.Portal>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { HoverCard, HoverCardContent, HoverCardTrigger };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@suphark/ui/components/ui/button";
|
|
4
|
+
import { Input } from "@suphark/ui/components/ui/input";
|
|
5
|
+
import { Textarea } from "@suphark/ui/components/ui/textarea";
|
|
6
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
7
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
8
|
+
import type * as React from "react";
|
|
9
|
+
|
|
10
|
+
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
data-slot="input-group"
|
|
14
|
+
role="group"
|
|
15
|
+
className={cn(
|
|
16
|
+
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input outline-none transition-colors in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-start]]:h-auto has-[>textarea]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:flex-col has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot][aria-invalid=true]]:border-destructive has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const inputGroupAddonVariants = cva(
|
|
25
|
+
"flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 font-medium text-muted-foreground text-sm group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
|
26
|
+
{
|
|
27
|
+
variants: {
|
|
28
|
+
align: {
|
|
29
|
+
"inline-start": "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
|
|
30
|
+
"inline-end": "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
|
|
31
|
+
"block-start":
|
|
32
|
+
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
|
|
33
|
+
"block-end":
|
|
34
|
+
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
defaultVariants: {
|
|
38
|
+
align: "inline-start",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
function InputGroupAddon({
|
|
44
|
+
className,
|
|
45
|
+
align = "inline-start",
|
|
46
|
+
...props
|
|
47
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
role="group"
|
|
51
|
+
data-slot="input-group-addon"
|
|
52
|
+
data-align={align}
|
|
53
|
+
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
54
|
+
onClick={(e) => {
|
|
55
|
+
if ((e.target as HTMLElement).closest("button")) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
e.currentTarget.parentElement?.querySelector("input")?.focus();
|
|
59
|
+
}}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const inputGroupButtonVariants = cva("flex items-center gap-2 text-sm shadow-none", {
|
|
66
|
+
variants: {
|
|
67
|
+
size: {
|
|
68
|
+
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
|
|
69
|
+
sm: "",
|
|
70
|
+
"icon-xs": "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
|
|
71
|
+
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
defaultVariants: {
|
|
75
|
+
size: "xs",
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
function InputGroupButton({
|
|
80
|
+
className,
|
|
81
|
+
type = "button",
|
|
82
|
+
variant = "ghost",
|
|
83
|
+
size = "xs",
|
|
84
|
+
...props
|
|
85
|
+
}: Omit<React.ComponentProps<typeof Button>, "size" | "type"> &
|
|
86
|
+
VariantProps<typeof inputGroupButtonVariants> & {
|
|
87
|
+
type?: "button" | "submit" | "reset";
|
|
88
|
+
}) {
|
|
89
|
+
return (
|
|
90
|
+
<Button
|
|
91
|
+
type={type}
|
|
92
|
+
data-size={size}
|
|
93
|
+
variant={variant}
|
|
94
|
+
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
95
|
+
{...props}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
|
101
|
+
return (
|
|
102
|
+
<span
|
|
103
|
+
className={cn(
|
|
104
|
+
"flex items-center gap-2 text-muted-foreground text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
|
105
|
+
className,
|
|
106
|
+
)}
|
|
107
|
+
{...props}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function InputGroupInput({ className, ...props }: React.ComponentProps<"input">) {
|
|
113
|
+
return (
|
|
114
|
+
<Input
|
|
115
|
+
data-slot="input-group-control"
|
|
116
|
+
className={cn(
|
|
117
|
+
"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
|
|
118
|
+
className,
|
|
119
|
+
)}
|
|
120
|
+
{...props}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function InputGroupTextarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
126
|
+
return (
|
|
127
|
+
<Textarea
|
|
128
|
+
data-slot="input-group-control"
|
|
129
|
+
className={cn(
|
|
130
|
+
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
|
|
131
|
+
className,
|
|
132
|
+
)}
|
|
133
|
+
{...props}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export {
|
|
139
|
+
InputGroup,
|
|
140
|
+
InputGroupAddon,
|
|
141
|
+
InputGroupButton,
|
|
142
|
+
InputGroupInput,
|
|
143
|
+
InputGroupText,
|
|
144
|
+
InputGroupTextarea,
|
|
145
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
4
|
+
import { OTPInput, OTPInputContext } from "input-otp";
|
|
5
|
+
import { MinusIcon } from "lucide-react";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
|
|
8
|
+
function InputOTP({
|
|
9
|
+
className,
|
|
10
|
+
containerClassName,
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<typeof OTPInput> & {
|
|
13
|
+
containerClassName?: string;
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<OTPInput
|
|
17
|
+
data-slot="input-otp"
|
|
18
|
+
containerClassName={cn(
|
|
19
|
+
"cn-input-otp flex items-center has-disabled:opacity-50",
|
|
20
|
+
containerClassName,
|
|
21
|
+
)}
|
|
22
|
+
spellCheck={false}
|
|
23
|
+
className={cn("disabled:cursor-not-allowed", className)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
data-slot="input-otp-group"
|
|
33
|
+
className={cn(
|
|
34
|
+
"flex items-center rounded-lg has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40",
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function InputOTPSlot({
|
|
43
|
+
index,
|
|
44
|
+
className,
|
|
45
|
+
...props
|
|
46
|
+
}: React.ComponentProps<"div"> & {
|
|
47
|
+
index: number;
|
|
48
|
+
}) {
|
|
49
|
+
const inputOTPContext = React.useContext(OTPInputContext);
|
|
50
|
+
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
data-slot="input-otp-slot"
|
|
55
|
+
data-active={isActive}
|
|
56
|
+
className={cn(
|
|
57
|
+
"relative flex size-8 items-center justify-center border-input border-y border-r text-sm outline-none transition-all first:rounded-l-lg first:border-l last:rounded-r-lg aria-invalid:border-destructive data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-3 data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:border-destructive data-[active=true]:aria-invalid:ring-destructive/20 dark:bg-input/30 dark:data-[active=true]:aria-invalid:ring-destructive/40",
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
60
|
+
{...props}
|
|
61
|
+
>
|
|
62
|
+
{char}
|
|
63
|
+
{hasFakeCaret && (
|
|
64
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
65
|
+
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
data-slot="input-otp-separator"
|
|
76
|
+
className="flex items-center [&_svg:not([class*='size-'])]:size-4"
|
|
77
|
+
role="separator"
|
|
78
|
+
{...props}
|
|
79
|
+
>
|
|
80
|
+
<MinusIcon />
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Input as InputPrimitive } from "@base-ui/react/input";
|
|
2
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
3
|
+
import type * as React from "react";
|
|
4
|
+
|
|
5
|
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
6
|
+
return (
|
|
7
|
+
<InputPrimitive
|
|
8
|
+
type={type}
|
|
9
|
+
data-slot="input"
|
|
10
|
+
className={cn(
|
|
11
|
+
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base outline-none transition-colors file:inline-flex file:h-6 file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-sm placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 dark:disabled:bg-input/80",
|
|
12
|
+
className,
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { Input };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
import { Separator } from "@suphark/ui/components/ui/separator";
|
|
4
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import type * as React from "react";
|
|
7
|
+
|
|
8
|
+
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
role="list"
|
|
12
|
+
data-slot="item-group"
|
|
13
|
+
className={cn(
|
|
14
|
+
"group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2",
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ItemSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
|
|
23
|
+
return (
|
|
24
|
+
<Separator
|
|
25
|
+
data-slot="item-separator"
|
|
26
|
+
orientation="horizontal"
|
|
27
|
+
className={cn("my-2", className)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const itemVariants = cva(
|
|
34
|
+
"group/item flex w-full flex-wrap items-center rounded-lg border text-sm outline-none transition-colors duration-100 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors [a]:hover:bg-muted",
|
|
35
|
+
{
|
|
36
|
+
variants: {
|
|
37
|
+
variant: {
|
|
38
|
+
default: "border-transparent",
|
|
39
|
+
outline: "border-border",
|
|
40
|
+
muted: "border-transparent bg-muted/50",
|
|
41
|
+
},
|
|
42
|
+
size: {
|
|
43
|
+
default: "gap-2.5 px-3 py-2.5",
|
|
44
|
+
sm: "gap-2.5 px-3 py-2.5",
|
|
45
|
+
xs: "gap-2 in-data-[slot=dropdown-menu-content]:p-0 px-2.5 py-2",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
defaultVariants: {
|
|
49
|
+
variant: "default",
|
|
50
|
+
size: "default",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
function Item({
|
|
56
|
+
className,
|
|
57
|
+
variant = "default",
|
|
58
|
+
size = "default",
|
|
59
|
+
render,
|
|
60
|
+
...props
|
|
61
|
+
}: useRender.ComponentProps<"div"> & VariantProps<typeof itemVariants>) {
|
|
62
|
+
return useRender({
|
|
63
|
+
defaultTagName: "div",
|
|
64
|
+
props: mergeProps<"div">(
|
|
65
|
+
{
|
|
66
|
+
className: cn(itemVariants({ variant, size, className })),
|
|
67
|
+
},
|
|
68
|
+
props,
|
|
69
|
+
),
|
|
70
|
+
render,
|
|
71
|
+
state: {
|
|
72
|
+
slot: "item",
|
|
73
|
+
variant,
|
|
74
|
+
size,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const itemMediaVariants = cva(
|
|
80
|
+
"flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none",
|
|
81
|
+
{
|
|
82
|
+
variants: {
|
|
83
|
+
variant: {
|
|
84
|
+
default: "bg-transparent",
|
|
85
|
+
icon: "[&_svg:not([class*='size-'])]:size-4",
|
|
86
|
+
image:
|
|
87
|
+
"size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
defaultVariants: {
|
|
91
|
+
variant: "default",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
function ItemMedia({
|
|
97
|
+
className,
|
|
98
|
+
variant = "default",
|
|
99
|
+
...props
|
|
100
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
data-slot="item-media"
|
|
104
|
+
data-variant={variant}
|
|
105
|
+
className={cn(itemMediaVariants({ variant, className }))}
|
|
106
|
+
{...props}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
112
|
+
return (
|
|
113
|
+
<div
|
|
114
|
+
data-slot="item-content"
|
|
115
|
+
className={cn(
|
|
116
|
+
"flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none",
|
|
117
|
+
className,
|
|
118
|
+
)}
|
|
119
|
+
{...props}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
125
|
+
return (
|
|
126
|
+
<div
|
|
127
|
+
data-slot="item-title"
|
|
128
|
+
className={cn(
|
|
129
|
+
"line-clamp-1 flex w-fit items-center gap-2 font-medium text-sm leading-snug underline-offset-4",
|
|
130
|
+
className,
|
|
131
|
+
)}
|
|
132
|
+
{...props}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
138
|
+
return (
|
|
139
|
+
<p
|
|
140
|
+
data-slot="item-description"
|
|
141
|
+
className={cn(
|
|
142
|
+
"line-clamp-2 text-left font-normal text-muted-foreground text-sm leading-normal group-data-[size=xs]/item:text-xs [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
|
143
|
+
className,
|
|
144
|
+
)}
|
|
145
|
+
{...props}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
|
|
151
|
+
return (
|
|
152
|
+
<div data-slot="item-actions" className={cn("flex items-center gap-2", className)} {...props} />
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
157
|
+
return (
|
|
158
|
+
<div
|
|
159
|
+
data-slot="item-header"
|
|
160
|
+
className={cn("flex basis-full items-center justify-between gap-2", className)}
|
|
161
|
+
{...props}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
167
|
+
return (
|
|
168
|
+
<div
|
|
169
|
+
data-slot="item-footer"
|
|
170
|
+
className={cn("flex basis-full items-center justify-between gap-2", className)}
|
|
171
|
+
{...props}
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export {
|
|
177
|
+
Item,
|
|
178
|
+
ItemActions,
|
|
179
|
+
ItemContent,
|
|
180
|
+
ItemDescription,
|
|
181
|
+
ItemFooter,
|
|
182
|
+
ItemGroup,
|
|
183
|
+
ItemHeader,
|
|
184
|
+
ItemMedia,
|
|
185
|
+
ItemSeparator,
|
|
186
|
+
ItemTitle,
|
|
187
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
2
|
+
|
|
3
|
+
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
|
4
|
+
return (
|
|
5
|
+
<kbd
|
|
6
|
+
data-slot="kbd"
|
|
7
|
+
className={cn(
|
|
8
|
+
"pointer-events-none inline-flex h-5 w-fit min-w-5 select-none items-center justify-center gap-1 rounded-sm bg-muted in-data-[slot=tooltip-content]:bg-background/20 px-1 font-medium font-sans in-data-[slot=tooltip-content]:text-background text-muted-foreground text-xs dark:in-data-[slot=tooltip-content]:bg-background/10 [&_svg:not([class*='size-'])]:size-3",
|
|
9
|
+
className,
|
|
10
|
+
)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
17
|
+
return (
|
|
18
|
+
<kbd
|
|
19
|
+
data-slot="kbd-group"
|
|
20
|
+
className={cn("inline-flex items-center gap-1", className)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { Kbd, KbdGroup };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
4
|
+
import type * as React from "react";
|
|
5
|
+
|
|
6
|
+
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
|
7
|
+
return (
|
|
8
|
+
<label
|
|
9
|
+
data-slot="label"
|
|
10
|
+
className={cn(
|
|
11
|
+
"flex select-none items-center gap-2 font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
|
|
12
|
+
className,
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { Label };
|