mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-02-28 03:42:50 +01:00
Shadcn Form and Label Components (#1958)
The first step in setting up the connmanager. Adds shadcn components for form and label in addition to shadcn utils.
This commit is contained in:
parent
ef30221e0b
commit
075d3b1302
@ -1,9 +1,16 @@
|
||||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// This file is based on components from shadcn/ui, which is licensed under the MIT License.
|
||||
// Original source: https://github.com/shadcn/ui
|
||||
// Modifications made by Command Line Inc.
|
||||
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
|
||||
import cn from "clsx";
|
||||
import { cn } from "@/shadcn/lib/utils";
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const;
|
||||
|
151
frontend/app/shadcn/form.tsx
Normal file
151
frontend/app/shadcn/form.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// This file is based on components from shadcn/ui, which is licensed under the MIT License.
|
||||
// Original source: https://github.com/shadcn/ui
|
||||
// Modifications made by Command Line Inc.
|
||||
|
||||
"use client";
|
||||
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form";
|
||||
|
||||
import { Label } from "@/shadcn/label";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/shadcn/lib/utils";
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState, formState } = useFormContext();
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>");
|
||||
}
|
||||
|
||||
const { id } = itemContext;
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
};
|
||||
};
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
|
||||
|
||||
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
FormItem.displayName = "FormItem";
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return <Label ref={ref} className={cn(error && "text-destructive", className)} htmlFor={formItemId} {...props} />;
|
||||
});
|
||||
FormLabel.displayName = "FormLabel";
|
||||
|
||||
const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
|
||||
({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
FormControl.displayName = "FormControl";
|
||||
|
||||
const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
FormDescription.displayName = "FormDescription";
|
||||
|
||||
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message) : children;
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
);
|
||||
FormMessage.displayName = "FormMessage";
|
||||
|
||||
export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField };
|
26
frontend/app/shadcn/label.tsx
Normal file
26
frontend/app/shadcn/label.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// This file is based on components from shadcn/ui, which is licensed under the MIT License.
|
||||
// Original source: https://github.com/shadcn/ui
|
||||
// Modifications made by Command Line Inc.
|
||||
|
||||
"use client";
|
||||
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import cn from "clsx";
|
||||
|
||||
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
26
frontend/app/shadcn/lib/utils.ts
Normal file
26
frontend/app/shadcn/lib/utils.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// This file is based on components from shadcn/ui, which is licensed under the MIT License.
|
||||
// Original source: https://github.com/shadcn/ui
|
||||
// Modifications made by Command Line Inc.
|
||||
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function formatDate(input: string | number): string {
|
||||
const date = new Date(input);
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
export function absoluteUrl(path: string) {
|
||||
return `${process.env.NEXT_PUBLIC_APP_URL}${path}`;
|
||||
}
|
@ -95,6 +95,8 @@
|
||||
"@monaco-editor/loader": "^1.4.0",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@observablehq/plot": "^0.6.16",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@react-hook/resize-observer": "^2.0.2",
|
||||
"@table-nav/core": "^0.0.7",
|
||||
"@table-nav/react": "^0.0.7",
|
||||
@ -106,6 +108,7 @@
|
||||
"@xterm/addon-webgl": "^0.18.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"color": "^4.2.3",
|
||||
"colord": "^2.9.3",
|
||||
@ -133,6 +136,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-frame-component": "^5.2.7",
|
||||
"react-gauge-chart": "^0.5.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-zoom-pan-pinch": "^3.7.0",
|
||||
"recharts": "^2.15.1",
|
||||
@ -147,6 +151,7 @@
|
||||
"sharp": "^0.33.5",
|
||||
"shell-quote": "^1.8.2",
|
||||
"sprintf-js": "^1.1.3",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"use-device-pixel-ratio": "^1.1.2",
|
||||
|
96
yarn.lock
96
yarn.lock
@ -3911,6 +3911,72 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-compose-refs@npm:1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "@radix-ui/react-compose-refs@npm:1.1.1"
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 10c0/3e84580024e66e3cc5b9ae79355e787815c1d2a3c7d46e7f47900a29c33751ca24cf4ac8903314957ab1f7788aebe1687e2258641c188cf94653f7ddf8f70627
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-label@npm:^2.1.2":
|
||||
version: 2.1.2
|
||||
resolution: "@radix-ui/react-label@npm:2.1.2"
|
||||
dependencies:
|
||||
"@radix-ui/react-primitive": "npm:2.0.2"
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: 10c0/c425ea25a67f60142645e6dd7669aa90bd9017e8d99c347736c9c19c44cea52e33224e4d086fd7e4945a7e9baa49335d42a5801d3bead884305515023e3ab31c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-primitive@npm:2.0.2":
|
||||
version: 2.0.2
|
||||
resolution: "@radix-ui/react-primitive@npm:2.0.2"
|
||||
dependencies:
|
||||
"@radix-ui/react-slot": "npm:1.1.2"
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: 10c0/1af7a33a86f8bd2467f2300b1bb6ca9af67cae3950953ba543d2a625c17f341dff05d19056ece7b03e5ced8b9f8de99c74f806710ce0da6b9a000f2af063fffe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-slot@npm:1.1.2, @radix-ui/react-slot@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "@radix-ui/react-slot@npm:1.1.2"
|
||||
dependencies:
|
||||
"@radix-ui/react-compose-refs": "npm:1.1.1"
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 10c0/81d45091806c52b507cec80b4477e4f31189d76ffcd7845b382eb3a034e6cf1faef71b881612028d5893f7580bf9ab59daa18fbf2792042dccd755c99a18df67
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-dnd/asap@npm:^5.0.1":
|
||||
version: 5.0.2
|
||||
resolution: "@react-dnd/asap@npm:5.0.2"
|
||||
@ -8163,6 +8229,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"class-variance-authority@npm:^0.7.1":
|
||||
version: 0.7.1
|
||||
resolution: "class-variance-authority@npm:0.7.1"
|
||||
dependencies:
|
||||
clsx: "npm:^2.1.1"
|
||||
checksum: 10c0/0f438cea22131808b99272de0fa933c2532d5659773bfec0c583de7b3f038378996d3350683426b8e9c74a6286699382106d71fbec52f0dd5fbb191792cccb5b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"clean-css@npm:^5.2.2, clean-css@npm:^5.3.2, clean-css@npm:~5.3.2":
|
||||
version: 5.3.3
|
||||
resolution: "clean-css@npm:5.3.3"
|
||||
@ -17400,6 +17475,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-hook-form@npm:^7.54.2":
|
||||
version: 7.54.2
|
||||
resolution: "react-hook-form@npm:7.54.2"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||
checksum: 10c0/6eebead2900e3d369a989e7a20429f390dc75b3897142aa3107f1f6dabb9ae64fed201ea98cdcd8676e40466c97748aeb0c0d83264f5bd3a84dbc0b8e4863415
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-is@npm:^16.13.1, react-is@npm:^16.6.0, react-is@npm:^16.7.0":
|
||||
version: 16.13.1
|
||||
resolution: "react-is@npm:16.13.1"
|
||||
@ -20096,6 +20180,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tailwind-merge@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "tailwind-merge@npm:3.0.1"
|
||||
checksum: 10c0/973d754687ad82d25cb55bf16958153999d080cd68dd0a6c1c74909bd92d05bcca01c24d9642e9e6ec95b3a207b15cd4b32bca49d586f2e743d311358ecb584b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tailwindcss-animate@npm:^1.0.7":
|
||||
version: 1.0.7
|
||||
resolution: "tailwindcss-animate@npm:1.0.7"
|
||||
@ -21616,6 +21707,8 @@ __metadata:
|
||||
"@monaco-editor/loader": "npm:^1.4.0"
|
||||
"@monaco-editor/react": "npm:^4.6.0"
|
||||
"@observablehq/plot": "npm:^0.6.16"
|
||||
"@radix-ui/react-label": "npm:^2.1.2"
|
||||
"@radix-ui/react-slot": "npm:^1.1.2"
|
||||
"@react-hook/resize-observer": "npm:^2.0.2"
|
||||
"@rollup/plugin-node-resolve": "npm:^16.0.0"
|
||||
"@storybook/addon-essentials": "npm:^8.5.4"
|
||||
@ -21657,6 +21750,7 @@ __metadata:
|
||||
"@xterm/addon-webgl": "npm:^0.18.0"
|
||||
"@xterm/xterm": "npm:^5.5.0"
|
||||
base64-js: "npm:^1.5.1"
|
||||
class-variance-authority: "npm:^0.7.1"
|
||||
clsx: "npm:^2.1.1"
|
||||
color: "npm:^4.2.3"
|
||||
colord: "npm:^2.9.3"
|
||||
@ -21693,6 +21787,7 @@ __metadata:
|
||||
react-dom: "npm:^18.3.1"
|
||||
react-frame-component: "npm:^5.2.7"
|
||||
react-gauge-chart: "npm:^0.5.1"
|
||||
react-hook-form: "npm:^7.54.2"
|
||||
react-markdown: "npm:^9.0.3"
|
||||
react-zoom-pan-pinch: "npm:^3.7.0"
|
||||
recharts: "npm:^2.15.1"
|
||||
@ -21712,6 +21807,7 @@ __metadata:
|
||||
sprintf-js: "npm:^1.1.3"
|
||||
storybook: "npm:^8.5.4"
|
||||
storybook-dark-mode: "npm:^4.0.2"
|
||||
tailwind-merge: "npm:^3.0.1"
|
||||
tailwindcss: "npm:^4.0.6"
|
||||
tailwindcss-animate: "npm:^1.0.7"
|
||||
throttle-debounce: "npm:^5.0.2"
|
||||
|
Loading…
Reference in New Issue
Block a user