@checkstack/ui 0.0.2
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 +153 -0
- package/bunfig.toml +2 -0
- package/package.json +40 -0
- package/src/components/Accordion.tsx +55 -0
- package/src/components/Alert.tsx +90 -0
- package/src/components/AmbientBackground.tsx +105 -0
- package/src/components/AnimatedCounter.tsx +54 -0
- package/src/components/BackLink.tsx +56 -0
- package/src/components/Badge.tsx +38 -0
- package/src/components/Button.tsx +55 -0
- package/src/components/Card.tsx +56 -0
- package/src/components/Checkbox.tsx +46 -0
- package/src/components/ColorPicker.tsx +69 -0
- package/src/components/CommandPalette.tsx +74 -0
- package/src/components/ConfirmationModal.tsx +134 -0
- package/src/components/DateRangeFilter.tsx +128 -0
- package/src/components/DateTimePicker.tsx +65 -0
- package/src/components/Dialog.tsx +134 -0
- package/src/components/DropdownMenu.tsx +96 -0
- package/src/components/DynamicForm/DynamicForm.tsx +126 -0
- package/src/components/DynamicForm/DynamicOptionsField.tsx +220 -0
- package/src/components/DynamicForm/FormField.tsx +690 -0
- package/src/components/DynamicForm/JsonField.tsx +98 -0
- package/src/components/DynamicForm/index.ts +11 -0
- package/src/components/DynamicForm/types.ts +95 -0
- package/src/components/DynamicForm/utils.ts +39 -0
- package/src/components/DynamicIcon.tsx +45 -0
- package/src/components/EditableText.tsx +141 -0
- package/src/components/EmptyState.tsx +32 -0
- package/src/components/HealthBadge.tsx +57 -0
- package/src/components/InfoBanner.tsx +97 -0
- package/src/components/Input.tsx +20 -0
- package/src/components/Label.tsx +17 -0
- package/src/components/LoadingSpinner.tsx +29 -0
- package/src/components/Markdown.tsx +206 -0
- package/src/components/NavItem.tsx +112 -0
- package/src/components/Page.tsx +58 -0
- package/src/components/PageLayout.tsx +83 -0
- package/src/components/PaginatedList.tsx +135 -0
- package/src/components/Pagination.tsx +195 -0
- package/src/components/PermissionDenied.tsx +31 -0
- package/src/components/PermissionGate.tsx +97 -0
- package/src/components/PluginConfigForm.tsx +91 -0
- package/src/components/SectionHeader.tsx +30 -0
- package/src/components/Select.tsx +157 -0
- package/src/components/StatusCard.tsx +78 -0
- package/src/components/StatusUpdateTimeline.tsx +222 -0
- package/src/components/StrategyConfigCard.tsx +333 -0
- package/src/components/SubscribeButton.tsx +96 -0
- package/src/components/Table.tsx +119 -0
- package/src/components/Tabs.tsx +141 -0
- package/src/components/TemplateEditor.test.ts +156 -0
- package/src/components/TemplateEditor.tsx +435 -0
- package/src/components/TerminalFeed.tsx +152 -0
- package/src/components/Textarea.tsx +22 -0
- package/src/components/ThemeProvider.tsx +76 -0
- package/src/components/Toast.tsx +118 -0
- package/src/components/ToastProvider.tsx +126 -0
- package/src/components/Toggle.tsx +47 -0
- package/src/components/Tooltip.tsx +20 -0
- package/src/components/UserMenu.tsx +79 -0
- package/src/hooks/usePagination.e2e.ts +275 -0
- package/src/hooks/usePagination.ts +231 -0
- package/src/index.ts +53 -0
- package/src/themes.css +204 -0
- package/src/utils/strip-markdown.ts +44 -0
- package/src/utils.ts +8 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Ajv from "ajv";
|
|
3
|
+
import addFormats from "ajv-formats";
|
|
4
|
+
import Editor from "react-simple-code-editor";
|
|
5
|
+
// @ts-expect-error - prismjs doesn't have types for deep imports in some environments
|
|
6
|
+
import { highlight, languages } from "prismjs/components/prism-core";
|
|
7
|
+
import "prismjs/components/prism-json";
|
|
8
|
+
|
|
9
|
+
import type { JsonFieldProps } from "./types";
|
|
10
|
+
|
|
11
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
12
|
+
addFormats(ajv);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A JSON editor field with syntax highlighting and schema validation.
|
|
16
|
+
*/
|
|
17
|
+
export const JsonField: React.FC<JsonFieldProps> = ({
|
|
18
|
+
id,
|
|
19
|
+
value,
|
|
20
|
+
propSchema,
|
|
21
|
+
onChange,
|
|
22
|
+
}) => {
|
|
23
|
+
const [internalValue, setInternalValue] = React.useState(
|
|
24
|
+
JSON.stringify(value || {}, undefined, 2)
|
|
25
|
+
);
|
|
26
|
+
const [error, setError] = React.useState<string | undefined>();
|
|
27
|
+
const lastPropValue = React.useRef(value);
|
|
28
|
+
|
|
29
|
+
const validateFn = React.useMemo(() => {
|
|
30
|
+
try {
|
|
31
|
+
return ajv.compile(propSchema);
|
|
32
|
+
} catch {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}, [propSchema]);
|
|
36
|
+
|
|
37
|
+
// Sync internal value ONLY when external value changes from outside
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
const valueString = JSON.stringify(value);
|
|
40
|
+
const lastValueString = JSON.stringify(lastPropValue.current);
|
|
41
|
+
|
|
42
|
+
if (valueString !== lastValueString) {
|
|
43
|
+
setInternalValue(JSON.stringify(value || {}, undefined, 2));
|
|
44
|
+
lastPropValue.current = value;
|
|
45
|
+
setError(undefined);
|
|
46
|
+
}
|
|
47
|
+
}, [value]);
|
|
48
|
+
|
|
49
|
+
const validate = (val: string) => {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(val);
|
|
52
|
+
if (!validateFn) {
|
|
53
|
+
setError(undefined);
|
|
54
|
+
return parsed;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const valid = validateFn(parsed);
|
|
58
|
+
if (!valid) {
|
|
59
|
+
setError(
|
|
60
|
+
validateFn.errors
|
|
61
|
+
?.map((e) => `${e.instancePath} ${e.message}`)
|
|
62
|
+
.join(", ")
|
|
63
|
+
);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
setError(undefined);
|
|
67
|
+
return parsed;
|
|
68
|
+
} catch (error_: unknown) {
|
|
69
|
+
setError(`Invalid JSON: ${(error_ as Error).message}`);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="space-y-2">
|
|
76
|
+
<div className="min-h-[100px] w-full rounded-md border border-input bg-background font-mono text-sm focus-within:ring-2 focus-within:ring-ring focus-within:border-transparent transition-all overflow-hidden box-border">
|
|
77
|
+
<Editor
|
|
78
|
+
id={id}
|
|
79
|
+
value={internalValue}
|
|
80
|
+
onValueChange={(code) => {
|
|
81
|
+
setInternalValue(code);
|
|
82
|
+
const parsed = validate(code);
|
|
83
|
+
if (parsed !== false) {
|
|
84
|
+
lastPropValue.current = parsed;
|
|
85
|
+
onChange(parsed);
|
|
86
|
+
}
|
|
87
|
+
}}
|
|
88
|
+
highlight={(code) => highlight(code, languages.json)}
|
|
89
|
+
padding={10}
|
|
90
|
+
style={{
|
|
91
|
+
minHeight: "100px",
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
{error && <p className="text-xs text-destructive font-medium">{error}</p>}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { TemplateProperty } from "../TemplateEditor";
|
|
2
|
+
|
|
3
|
+
export interface JsonSchemaProperty {
|
|
4
|
+
type?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
enum?: string[];
|
|
7
|
+
const?: string | number | boolean; // For discriminator values
|
|
8
|
+
properties?: Record<string, JsonSchemaProperty>;
|
|
9
|
+
items?: JsonSchemaProperty;
|
|
10
|
+
required?: string[];
|
|
11
|
+
additionalProperties?: boolean | JsonSchemaProperty;
|
|
12
|
+
format?: string;
|
|
13
|
+
default?: unknown;
|
|
14
|
+
oneOf?: JsonSchemaProperty[]; // Discriminated union variants
|
|
15
|
+
anyOf?: JsonSchemaProperty[]; // Union variants
|
|
16
|
+
"x-secret"?: boolean; // Custom metadata for secret fields
|
|
17
|
+
"x-color"?: boolean; // Custom metadata for color fields
|
|
18
|
+
"x-options-resolver"?: string; // Name of a resolver function for dynamic options
|
|
19
|
+
"x-depends-on"?: string[]; // Field names this field depends on (triggers refetch when they change)
|
|
20
|
+
"x-hidden"?: boolean; // Field should be hidden in form (auto-populated)
|
|
21
|
+
"x-searchable"?: boolean; // Shows a search input for filtering dropdown options
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Option returned by an options resolver */
|
|
25
|
+
export interface ResolverOption {
|
|
26
|
+
value: string;
|
|
27
|
+
label: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Function that resolves dynamic options, receives form values as context */
|
|
31
|
+
export type OptionsResolver = (
|
|
32
|
+
formValues: Record<string, unknown>
|
|
33
|
+
) => Promise<ResolverOption[]>;
|
|
34
|
+
|
|
35
|
+
export interface JsonSchema {
|
|
36
|
+
properties?: Record<string, JsonSchemaProperty>;
|
|
37
|
+
required?: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DynamicFormProps {
|
|
41
|
+
schema: JsonSchema;
|
|
42
|
+
value: Record<string, unknown>;
|
|
43
|
+
onChange: (value: Record<string, unknown>) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Optional callback when form validity changes.
|
|
46
|
+
* Reports true if all required fields are filled.
|
|
47
|
+
*/
|
|
48
|
+
onValidChange?: (isValid: boolean) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Optional map of resolver names to functions that fetch dynamic options.
|
|
51
|
+
* Referenced by x-options-resolver in schema properties.
|
|
52
|
+
*/
|
|
53
|
+
optionsResolvers?: Record<string, OptionsResolver>;
|
|
54
|
+
/**
|
|
55
|
+
* Optional list of available template properties for template fields.
|
|
56
|
+
* Passed to TemplateEditor for autocompletion hints.
|
|
57
|
+
*/
|
|
58
|
+
templateProperties?: TemplateProperty[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Props for the FormField component */
|
|
62
|
+
export interface FormFieldProps {
|
|
63
|
+
id: string;
|
|
64
|
+
label: string;
|
|
65
|
+
propSchema: JsonSchemaProperty;
|
|
66
|
+
value: unknown;
|
|
67
|
+
isRequired?: boolean;
|
|
68
|
+
formValues: Record<string, unknown>;
|
|
69
|
+
optionsResolvers?: Record<string, OptionsResolver>;
|
|
70
|
+
templateProperties?: TemplateProperty[];
|
|
71
|
+
onChange: (val: unknown) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Props for the DynamicOptionsField component */
|
|
75
|
+
export interface DynamicOptionsFieldProps {
|
|
76
|
+
id: string;
|
|
77
|
+
label: string;
|
|
78
|
+
description?: string;
|
|
79
|
+
value: unknown;
|
|
80
|
+
isRequired?: boolean;
|
|
81
|
+
resolverName: string;
|
|
82
|
+
dependsOn?: string[];
|
|
83
|
+
searchable?: boolean;
|
|
84
|
+
formValues: Record<string, unknown>;
|
|
85
|
+
optionsResolvers: Record<string, OptionsResolver>;
|
|
86
|
+
onChange: (val: unknown) => void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Props for the JsonField component */
|
|
90
|
+
export interface JsonFieldProps {
|
|
91
|
+
id: string;
|
|
92
|
+
value: Record<string, unknown>;
|
|
93
|
+
propSchema: JsonSchemaProperty;
|
|
94
|
+
onChange: (val: Record<string, unknown>) => void;
|
|
95
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { JsonSchema } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cleans a description string by removing textarea markers.
|
|
5
|
+
* Returns undefined if the description is empty or just "textarea".
|
|
6
|
+
*/
|
|
7
|
+
export const getCleanDescription = (
|
|
8
|
+
description?: string
|
|
9
|
+
): string | undefined => {
|
|
10
|
+
if (!description || description === "textarea") return;
|
|
11
|
+
const cleaned = description.replace("[textarea]", "").trim();
|
|
12
|
+
if (!cleaned) return;
|
|
13
|
+
return cleaned;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extracts default values from a JSON schema recursively.
|
|
18
|
+
*/
|
|
19
|
+
export const extractDefaults = (
|
|
20
|
+
schema: JsonSchema
|
|
21
|
+
): Record<string, unknown> => {
|
|
22
|
+
const defaults: Record<string, unknown> = {};
|
|
23
|
+
|
|
24
|
+
if (!schema.properties) return defaults;
|
|
25
|
+
|
|
26
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
27
|
+
if (propSchema.default !== undefined) {
|
|
28
|
+
defaults[key] = propSchema.default;
|
|
29
|
+
} else if (propSchema.type === "object" && propSchema.properties) {
|
|
30
|
+
// Recursively extract defaults for nested objects
|
|
31
|
+
defaults[key] = extractDefaults(propSchema as JsonSchema);
|
|
32
|
+
} else if (propSchema.type === "array") {
|
|
33
|
+
// Arrays default to empty array
|
|
34
|
+
defaults[key] = [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return defaults;
|
|
39
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { icons, type LucideIcon } from "lucide-react";
|
|
2
|
+
import { Settings } from "lucide-react";
|
|
3
|
+
import type { LucideIconName } from "@checkstack/common";
|
|
4
|
+
|
|
5
|
+
// Re-export the type for convenience
|
|
6
|
+
export type { LucideIconName } from "@checkstack/common";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Props for the DynamicIcon component
|
|
10
|
+
*/
|
|
11
|
+
export interface DynamicIconProps {
|
|
12
|
+
/** Lucide icon name in PascalCase (e.g., 'AlertCircle', 'HeartPulse') */
|
|
13
|
+
name?: LucideIconName;
|
|
14
|
+
/** CSS class name to apply to the icon */
|
|
15
|
+
className?: string;
|
|
16
|
+
/** Fallback icon if name is not provided */
|
|
17
|
+
fallback?: LucideIcon;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Dynamically renders a Lucide icon by name.
|
|
22
|
+
* Falls back to Settings icon if the icon name is not provided.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* <DynamicIcon name="AlertCircle" />
|
|
26
|
+
* <DynamicIcon name="HeartPulse" className="h-6 w-6" />
|
|
27
|
+
*/
|
|
28
|
+
export function DynamicIcon({
|
|
29
|
+
name,
|
|
30
|
+
className = "h-5 w-5",
|
|
31
|
+
fallback: FallbackIcon = Settings,
|
|
32
|
+
}: DynamicIconProps) {
|
|
33
|
+
if (!name) {
|
|
34
|
+
return <FallbackIcon className={className} />;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const Icon = icons[name];
|
|
38
|
+
|
|
39
|
+
// Fallback if icon name doesn't exist in lucide-react
|
|
40
|
+
if (!Icon) {
|
|
41
|
+
return <FallbackIcon className={className} />;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return <Icon className={className} />;
|
|
45
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from "react";
|
|
2
|
+
import { cn } from "../utils";
|
|
3
|
+
import { Input } from "./Input";
|
|
4
|
+
import { Button } from "./Button";
|
|
5
|
+
import { Check, X } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
export interface EditableTextProps {
|
|
8
|
+
value: string;
|
|
9
|
+
onSave: (newValue: string) => Promise<void> | void;
|
|
10
|
+
className?: string;
|
|
11
|
+
inputClassName?: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
validate?: (value: string) => boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const EditableText: React.FC<EditableTextProps> = ({
|
|
18
|
+
value,
|
|
19
|
+
onSave,
|
|
20
|
+
className,
|
|
21
|
+
inputClassName,
|
|
22
|
+
placeholder,
|
|
23
|
+
validate,
|
|
24
|
+
disabled = false,
|
|
25
|
+
}) => {
|
|
26
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
27
|
+
const [editValue, setEditValue] = useState(value);
|
|
28
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
29
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (isEditing && inputRef.current) {
|
|
33
|
+
inputRef.current.focus();
|
|
34
|
+
inputRef.current.select();
|
|
35
|
+
}
|
|
36
|
+
}, [isEditing]);
|
|
37
|
+
|
|
38
|
+
const handleEdit = () => {
|
|
39
|
+
setEditValue(value);
|
|
40
|
+
setIsEditing(true);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleCancel = () => {
|
|
44
|
+
setEditValue(value);
|
|
45
|
+
setIsEditing(false);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleSave = async () => {
|
|
49
|
+
const trimmedValue = editValue.trim();
|
|
50
|
+
|
|
51
|
+
if (!trimmedValue) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (validate && !validate(trimmedValue)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (trimmedValue === value) {
|
|
60
|
+
setIsEditing(false);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setIsSaving(true);
|
|
65
|
+
try {
|
|
66
|
+
await onSave(trimmedValue);
|
|
67
|
+
setIsEditing(false);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error("Failed to save:", error);
|
|
70
|
+
} finally {
|
|
71
|
+
setIsSaving(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
76
|
+
if (e.key === "Enter") {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
handleSave();
|
|
79
|
+
} else if (e.key === "Escape") {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
handleCancel();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (isEditing) {
|
|
86
|
+
return (
|
|
87
|
+
<div className={cn("flex items-center gap-2", className)}>
|
|
88
|
+
<Input
|
|
89
|
+
ref={inputRef}
|
|
90
|
+
value={editValue}
|
|
91
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
92
|
+
setEditValue(e.target.value)
|
|
93
|
+
}
|
|
94
|
+
onKeyDown={handleKeyDown}
|
|
95
|
+
placeholder={placeholder}
|
|
96
|
+
disabled={isSaving}
|
|
97
|
+
className={cn("flex-1", inputClassName)}
|
|
98
|
+
/>
|
|
99
|
+
<Button
|
|
100
|
+
variant="ghost"
|
|
101
|
+
className="h-8 w-8 p-0 text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:bg-green-50 dark:hover:bg-green-950/20"
|
|
102
|
+
onClick={handleSave}
|
|
103
|
+
disabled={isSaving || !editValue.trim()}
|
|
104
|
+
type="button"
|
|
105
|
+
>
|
|
106
|
+
<Check className="w-4 h-4" />
|
|
107
|
+
</Button>
|
|
108
|
+
<Button
|
|
109
|
+
variant="ghost"
|
|
110
|
+
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground hover:bg-accent"
|
|
111
|
+
onClick={handleCancel}
|
|
112
|
+
disabled={isSaving}
|
|
113
|
+
type="button"
|
|
114
|
+
>
|
|
115
|
+
<X className="w-4 h-4" />
|
|
116
|
+
</Button>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div
|
|
123
|
+
className={cn(
|
|
124
|
+
"cursor-pointer hover:underline hover:decoration-dashed hover:decoration-1 hover:underline-offset-4 transition-all",
|
|
125
|
+
disabled && "cursor-default hover:no-underline opacity-50",
|
|
126
|
+
className
|
|
127
|
+
)}
|
|
128
|
+
onClick={disabled ? undefined : handleEdit}
|
|
129
|
+
role="button"
|
|
130
|
+
tabIndex={disabled ? -1 : 0}
|
|
131
|
+
onKeyDown={(e) => {
|
|
132
|
+
if (!disabled && (e.key === "Enter" || e.key === " ")) {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
handleEdit();
|
|
135
|
+
}
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
{value}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Card, CardContent } from "./Card";
|
|
3
|
+
import { cn } from "../utils";
|
|
4
|
+
|
|
5
|
+
interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
icon?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const EmptyState: React.FC<EmptyStateProps> = ({
|
|
12
|
+
title,
|
|
13
|
+
description,
|
|
14
|
+
icon,
|
|
15
|
+
className,
|
|
16
|
+
...props
|
|
17
|
+
}) => {
|
|
18
|
+
return (
|
|
19
|
+
<Card
|
|
20
|
+
className={cn("border-dashed border-2 bg-muted/30", className)}
|
|
21
|
+
{...props}
|
|
22
|
+
>
|
|
23
|
+
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
|
24
|
+
{icon && <div className="text-muted-foreground/40 mb-4">{icon}</div>}
|
|
25
|
+
<p className="text-foreground font-medium">{title}</p>
|
|
26
|
+
{description && (
|
|
27
|
+
<p className="text-sm text-muted-foreground mt-1">{description}</p>
|
|
28
|
+
)}
|
|
29
|
+
</CardContent>
|
|
30
|
+
</Card>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { CheckCircle2, AlertTriangle, XCircle } from "lucide-react";
|
|
3
|
+
import { cn } from "../utils";
|
|
4
|
+
|
|
5
|
+
export type HealthStatus = "healthy" | "degraded" | "unhealthy";
|
|
6
|
+
|
|
7
|
+
interface HealthBadgeProps {
|
|
8
|
+
status: HealthStatus;
|
|
9
|
+
variant?: "compact" | "full";
|
|
10
|
+
showIcon?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const statusConfig = {
|
|
15
|
+
healthy: {
|
|
16
|
+
icon: CheckCircle2,
|
|
17
|
+
label: "Healthy",
|
|
18
|
+
className: "bg-success/10 text-success border-success/30",
|
|
19
|
+
iconClassName: "text-success",
|
|
20
|
+
},
|
|
21
|
+
degraded: {
|
|
22
|
+
icon: AlertTriangle,
|
|
23
|
+
label: "Degraded",
|
|
24
|
+
className: "bg-warning/10 text-warning border-warning/30",
|
|
25
|
+
iconClassName: "text-warning",
|
|
26
|
+
},
|
|
27
|
+
unhealthy: {
|
|
28
|
+
icon: XCircle,
|
|
29
|
+
label: "Unhealthy",
|
|
30
|
+
className: "bg-destructive/10 text-destructive border-destructive/30",
|
|
31
|
+
iconClassName: "text-destructive",
|
|
32
|
+
},
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
export const HealthBadge: React.FC<HealthBadgeProps> = ({
|
|
36
|
+
status,
|
|
37
|
+
variant = "full",
|
|
38
|
+
showIcon = true,
|
|
39
|
+
className,
|
|
40
|
+
}) => {
|
|
41
|
+
const config = statusConfig[status];
|
|
42
|
+
const Icon = config.icon;
|
|
43
|
+
const isCompact = variant === "compact";
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<span
|
|
47
|
+
className={cn(
|
|
48
|
+
"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors",
|
|
49
|
+
config.className,
|
|
50
|
+
className
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
{showIcon && <Icon className={cn("h-3.5 w-3.5", config.iconClassName)} />}
|
|
54
|
+
{!isCompact && <span>{config.label}</span>}
|
|
55
|
+
</span>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
const infoBannerVariants = cva(
|
|
5
|
+
"relative w-full rounded-lg border px-3 py-2.5 text-sm",
|
|
6
|
+
{
|
|
7
|
+
variants: {
|
|
8
|
+
variant: {
|
|
9
|
+
default: "bg-muted/30 border-border/50 text-muted-foreground",
|
|
10
|
+
info: "bg-info/5 border-info/20 text-info",
|
|
11
|
+
warning: "bg-warning/5 border-warning/20 text-warning",
|
|
12
|
+
success: "bg-success/5 border-success/20 text-success",
|
|
13
|
+
error: "bg-destructive/5 border-destructive/20 text-destructive",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
variant: "default",
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export interface InfoBannerProps
|
|
23
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
24
|
+
VariantProps<typeof infoBannerVariants> {
|
|
25
|
+
children: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const InfoBanner = React.forwardRef<HTMLDivElement, InfoBannerProps>(
|
|
29
|
+
({ className, variant, children, ...props }, ref) => {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
ref={ref}
|
|
33
|
+
role="status"
|
|
34
|
+
className={infoBannerVariants({ variant, className })}
|
|
35
|
+
{...props}
|
|
36
|
+
>
|
|
37
|
+
<div className="flex gap-2.5 items-start">{children}</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
InfoBanner.displayName = "InfoBanner";
|
|
44
|
+
|
|
45
|
+
export const InfoBannerIcon = React.forwardRef<
|
|
46
|
+
HTMLDivElement,
|
|
47
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
48
|
+
>(({ className, children, ...props }, ref) => (
|
|
49
|
+
<div
|
|
50
|
+
ref={ref}
|
|
51
|
+
className={`flex-shrink-0 mt-0.5 opacity-70 ${className || ""}`}
|
|
52
|
+
{...props}
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
</div>
|
|
56
|
+
));
|
|
57
|
+
|
|
58
|
+
InfoBannerIcon.displayName = "InfoBannerIcon";
|
|
59
|
+
|
|
60
|
+
export const InfoBannerContent = React.forwardRef<
|
|
61
|
+
HTMLDivElement,
|
|
62
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
63
|
+
>(({ className, ...props }, ref) => (
|
|
64
|
+
<div
|
|
65
|
+
ref={ref}
|
|
66
|
+
className={`flex-1 space-y-0.5 ${className || ""}`}
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
));
|
|
70
|
+
|
|
71
|
+
InfoBannerContent.displayName = "InfoBannerContent";
|
|
72
|
+
|
|
73
|
+
export const InfoBannerTitle = React.forwardRef<
|
|
74
|
+
HTMLHeadingElement,
|
|
75
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
76
|
+
>(({ className, ...props }, ref) => (
|
|
77
|
+
<h6
|
|
78
|
+
ref={ref}
|
|
79
|
+
className={`font-medium text-sm leading-tight ${className || ""}`}
|
|
80
|
+
{...props}
|
|
81
|
+
/>
|
|
82
|
+
));
|
|
83
|
+
|
|
84
|
+
InfoBannerTitle.displayName = "InfoBannerTitle";
|
|
85
|
+
|
|
86
|
+
export const InfoBannerDescription = React.forwardRef<
|
|
87
|
+
HTMLParagraphElement,
|
|
88
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
89
|
+
>(({ className, ...props }, ref) => (
|
|
90
|
+
<div
|
|
91
|
+
ref={ref}
|
|
92
|
+
className={`text-sm leading-relaxed opacity-90 ${className || ""}`}
|
|
93
|
+
{...props}
|
|
94
|
+
/>
|
|
95
|
+
));
|
|
96
|
+
|
|
97
|
+
InfoBannerDescription.displayName = "InfoBannerDescription";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../utils";
|
|
3
|
+
|
|
4
|
+
export const Input = React.forwardRef<
|
|
5
|
+
HTMLInputElement,
|
|
6
|
+
React.InputHTMLAttributes<HTMLInputElement>
|
|
7
|
+
>(({ className, type, ...props }, ref) => {
|
|
8
|
+
return (
|
|
9
|
+
<input
|
|
10
|
+
type={type}
|
|
11
|
+
className={cn(
|
|
12
|
+
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent disabled:cursor-not-allowed disabled:opacity-50",
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
ref={ref}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
Input.displayName = "Input";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../utils";
|
|
3
|
+
|
|
4
|
+
export const Label = React.forwardRef<
|
|
5
|
+
HTMLLabelElement,
|
|
6
|
+
React.LabelHTMLAttributes<HTMLLabelElement>
|
|
7
|
+
>(({ className, ...props }, ref) => (
|
|
8
|
+
<label
|
|
9
|
+
ref={ref}
|
|
10
|
+
className={cn(
|
|
11
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-foreground",
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
));
|
|
17
|
+
Label.displayName = "Label";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "../utils";
|
|
3
|
+
|
|
4
|
+
interface LoadingSpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
size?: "sm" | "md" | "lg";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
|
|
9
|
+
size = "md",
|
|
10
|
+
className,
|
|
11
|
+
...props
|
|
12
|
+
}) => {
|
|
13
|
+
const sizeClasses = {
|
|
14
|
+
sm: "w-4 h-4 border-2",
|
|
15
|
+
md: "w-8 h-8 border-4",
|
|
16
|
+
lg: "w-12 h-12 border-4",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className={cn("flex justify-center py-12", className)} {...props}>
|
|
21
|
+
<div
|
|
22
|
+
className={cn(
|
|
23
|
+
"border-indigo-200 border-t-indigo-500 rounded-full animate-spin",
|
|
24
|
+
sizeClasses[size]
|
|
25
|
+
)}
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
};
|