@trackany-device/components 1.0.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/package.json +185 -0
- package/src/assets/logo.png +0 -0
- package/src/assets/map/arrows/map-arrow-blue.png +0 -0
- package/src/assets/map/arrows/map-arrow-green.png +0 -0
- package/src/assets/map/arrows/map-arrow-purple.png +0 -0
- package/src/assets/map/arrows/map-arrow-red.png +0 -0
- package/src/assets/map/flags/flag-blue.png +0 -0
- package/src/assets/map/flags/flag-green.png +0 -0
- package/src/assets/map/flags/flag-red.png +0 -0
- package/src/assets/map/flags/flag-yellow.png +0 -0
- package/src/assets/map/pins/map-pin-blue.png +0 -0
- package/src/assets/map/pins/map-pin-green.png +0 -0
- package/src/assets/map/pins/map-pin-purple.png +0 -0
- package/src/assets/map/pins/map-pin-red.png +0 -0
- package/src/components/Card.tsx +9 -0
- package/src/components/alert-error.tsx +24 -0
- package/src/components/app-content.tsx +22 -0
- package/src/components/app-header.tsx +153 -0
- package/src/components/app-logo-icon.tsx +13 -0
- package/src/components/app-logo.tsx +21 -0
- package/src/components/app-shell.tsx +19 -0
- package/src/components/app-sidebar-header.tsx +68 -0
- package/src/components/app-sidebar.tsx +106 -0
- package/src/components/appearance-tabs.tsx +46 -0
- package/src/components/breadcrumbs.tsx +50 -0
- package/src/components/cms/blurred-image.tsx +111 -0
- package/src/components/cms/section-bg.tsx +473 -0
- package/src/components/cms/section-button.tsx +127 -0
- package/src/components/cms/sections/banner-5050-section.tsx +135 -0
- package/src/components/cms/sections/blogs-listing-section.tsx +270 -0
- package/src/components/cms/sections/cards-grid-section.tsx +185 -0
- package/src/components/cms/sections/contact-form-section.tsx +157 -0
- package/src/components/cms/sections/cta-section.tsx +101 -0
- package/src/components/cms/sections/featured-blog-slider-section.tsx +256 -0
- package/src/components/cms/sections/featured-products-grid-section.tsx +173 -0
- package/src/components/cms/sections/featured-solutions-grid-section.tsx +183 -0
- package/src/components/cms/sections/hero-section.tsx +180 -0
- package/src/components/cms/sections/solutions-with-filter-section.tsx +234 -0
- package/src/components/cms/sections/text-section.tsx +77 -0
- package/src/components/cutout-image.tsx +228 -0
- package/src/components/devices/devices-mini-map.tsx +275 -0
- package/src/components/docs/docs-shell.tsx +280 -0
- package/src/components/fleet-hero-animated.tsx +383 -0
- package/src/components/input-error.tsx +17 -0
- package/src/components/keenicons/assets/duotone/Read Me.txt +7 -0
- package/src/components/keenicons/assets/duotone/demo-files/demo.css +160 -0
- package/src/components/keenicons/assets/duotone/demo-files/demo.js +32 -0
- package/src/components/keenicons/assets/duotone/demo.html +12424 -0
- package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.svg +1109 -0
- package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.ttf +0 -0
- package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.woff +0 -0
- package/src/components/keenicons/assets/duotone/selection.json +17313 -0
- package/src/components/keenicons/assets/duotone/style.css +4931 -0
- package/src/components/keenicons/assets/filled/Read Me.txt +7 -0
- package/src/components/keenicons/assets/filled/demo-files/demo.css +160 -0
- package/src/components/keenicons/assets/filled/demo-files/demo.js +32 -0
- package/src/components/keenicons/assets/filled/demo.html +12370 -0
- package/src/components/keenicons/assets/filled/fonts/keenicons-filled.svg +1082 -0
- package/src/components/keenicons/assets/filled/fonts/keenicons-filled.ttf +0 -0
- package/src/components/keenicons/assets/filled/fonts/keenicons-filled.woff +0 -0
- package/src/components/keenicons/assets/filled/selection.json +17096 -0
- package/src/components/keenicons/assets/filled/style.css +4769 -0
- package/src/components/keenicons/assets/outline/Read Me.txt +7 -0
- package/src/components/keenicons/assets/outline/demo-files/demo.css +160 -0
- package/src/components/keenicons/assets/outline/demo-files/demo.js +32 -0
- package/src/components/keenicons/assets/outline/demo.html +11356 -0
- package/src/components/keenicons/assets/outline/fonts/keenicons-outline.svg +575 -0
- package/src/components/keenicons/assets/outline/fonts/keenicons-outline.ttf +0 -0
- package/src/components/keenicons/assets/outline/fonts/keenicons-outline.woff +0 -0
- package/src/components/keenicons/assets/outline/selection.json +13054 -0
- package/src/components/keenicons/assets/outline/style.css +1721 -0
- package/src/components/keenicons/assets/solid/Read Me.txt +7 -0
- package/src/components/keenicons/assets/solid/demo-files/demo.css +160 -0
- package/src/components/keenicons/assets/solid/demo-files/demo.js +32 -0
- package/src/components/keenicons/assets/solid/demo.html +11356 -0
- package/src/components/keenicons/assets/solid/fonts/keenicons-solid.svg +575 -0
- package/src/components/keenicons/assets/solid/fonts/keenicons-solid.ttf +0 -0
- package/src/components/keenicons/assets/solid/fonts/keenicons-solid.woff +0 -0
- package/src/components/keenicons/assets/solid/selection.json +13048 -0
- package/src/components/keenicons/assets/solid/style.css +1721 -0
- package/src/components/keenicons/assets/styles.css +4 -0
- package/src/components/keenicons/index.ts +2 -0
- package/src/components/keenicons/keenicons.tsx +16 -0
- package/src/components/keenicons/types.ts +7 -0
- package/src/components/nav-footer.tsx +49 -0
- package/src/components/nav-main.tsx +53 -0
- package/src/components/nav-user.tsx +59 -0
- package/src/components/notification-bell.tsx +190 -0
- package/src/components/products/product-card.tsx +159 -0
- package/src/components/text-link.tsx +23 -0
- package/src/components/ui/accordion-menu.tsx +322 -0
- package/src/components/ui/accordion.tsx +133 -0
- package/src/components/ui/alert-dialog.tsx +82 -0
- package/src/components/ui/alert.tsx +63 -0
- package/src/components/ui/avatar-group.tsx +129 -0
- package/src/components/ui/avatar.tsx +67 -0
- package/src/components/ui/badge.tsx +230 -0
- package/src/components/ui/breadcrumb.tsx +88 -0
- package/src/components/ui/button.tsx +412 -0
- package/src/components/ui/calendar.tsx +56 -0
- package/src/components/ui/card.tsx +147 -0
- package/src/components/ui/chart.tsx +290 -0
- package/src/components/ui/checkbox.tsx +47 -0
- package/src/components/ui/code.tsx +45 -0
- package/src/components/ui/collapsible.tsx +31 -0
- package/src/components/ui/command-palette.tsx +189 -0
- package/src/components/ui/command.tsx +138 -0
- package/src/components/ui/cookie-banner.tsx +220 -0
- package/src/components/ui/copy-button.tsx +60 -0
- package/src/components/ui/data-grid-column-filter.tsx +124 -0
- package/src/components/ui/data-grid-column-header.tsx +284 -0
- package/src/components/ui/data-grid-column-visibility.tsx +38 -0
- package/src/components/ui/data-grid-pagination.tsx +206 -0
- package/src/components/ui/data-grid-table-dnd-rows.tsx +147 -0
- package/src/components/ui/data-grid-table-dnd.tsx +175 -0
- package/src/components/ui/data-grid-table.tsx +500 -0
- package/src/components/ui/data-grid.tsx +193 -0
- package/src/components/ui/data-list.tsx +76 -0
- package/src/components/ui/datefield.tsx +91 -0
- package/src/components/ui/dialog.tsx +139 -0
- package/src/components/ui/divider.tsx +41 -0
- package/src/components/ui/drawer.tsx +59 -0
- package/src/components/ui/dropdown-menu.tsx +224 -0
- package/src/components/ui/empty-state.tsx +54 -0
- package/src/components/ui/file-upload.tsx +152 -0
- package/src/components/ui/form.tsx +88 -0
- package/src/components/ui/icon.tsx +14 -0
- package/src/components/ui/input-otp.tsx +71 -0
- package/src/components/ui/input.tsx +155 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +31 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/pagination.tsx +37 -0
- package/src/components/ui/placeholder-pattern.tsx +21 -0
- package/src/components/ui/popover.tsx +50 -0
- package/src/components/ui/progress.tsx +65 -0
- package/src/components/ui/radio-group.tsx +73 -0
- package/src/components/ui/resizable.tsx +39 -0
- package/src/components/ui/scroll-area.tsx +50 -0
- package/src/components/ui/select.tsx +234 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/sheet.tsx +147 -0
- package/src/components/ui/sidebar.tsx +721 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/slider.tsx +35 -0
- package/src/components/ui/sonner.tsx +28 -0
- package/src/components/ui/sortable.tsx +724 -0
- package/src/components/ui/spinner.tsx +17 -0
- package/src/components/ui/stat-card.tsx +82 -0
- package/src/components/ui/stepper.tsx +410 -0
- package/src/components/ui/switch.tsx +68 -0
- package/src/components/ui/table.tsx +42 -0
- package/src/components/ui/tabs.tsx +196 -0
- package/src/components/ui/timeline.tsx +90 -0
- package/src/components/ui/toggle-group.tsx +73 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/tooltip.tsx +55 -0
- package/src/components/user-info.tsx +33 -0
- package/src/components/user-menu-content.tsx +53 -0
- package/src/components/web/SiteFooter.tsx +154 -0
- package/src/components/web/SiteHeader.tsx +159 -0
- package/src/components/workflows/workflow-canvas.tsx +321 -0
- package/src/controls/Blockquote.tsx +25 -0
- package/src/controls/Button.tsx +101 -0
- package/src/controls/Checkbox.tsx +29 -0
- package/src/controls/DateField.tsx +37 -0
- package/src/controls/FormField.tsx +20 -0
- package/src/controls/Heading.tsx +28 -0
- package/src/controls/Input.tsx +21 -0
- package/src/controls/Label.tsx +18 -0
- package/src/controls/Paragraph.tsx +39 -0
- package/src/controls/PasswordInput.tsx +40 -0
- package/src/controls/RadioGroup.tsx +70 -0
- package/src/controls/Select.tsx +24 -0
- package/src/controls/Slider.tsx +33 -0
- package/src/controls/Switch.tsx +31 -0
- package/src/controls/Textarea.tsx +22 -0
- package/src/elements/ConfirmPasswordForm.tsx +43 -0
- package/src/elements/DeviceStatusBadge.tsx +38 -0
- package/src/elements/DriverCard.tsx +67 -0
- package/src/elements/ForgotPasswordForm.tsx +64 -0
- package/src/elements/IncidentCard.tsx +67 -0
- package/src/elements/LoginForm.tsx +100 -0
- package/src/elements/OtpForm.tsx +71 -0
- package/src/elements/RegisterForm.tsx +150 -0
- package/src/elements/ResetPasswordForm.tsx +72 -0
- package/src/elements/SmsChallengeForm.tsx +104 -0
- package/src/elements/VehicleCard.tsx +73 -0
- package/src/elements/VerifyEmailForm.tsx +39 -0
- package/src/hooks/use-appearance.tsx +117 -0
- package/src/hooks/use-applied-theme.ts +98 -0
- package/src/hooks/use-clipboard.ts +34 -0
- package/src/hooks/use-current-url.ts +83 -0
- package/src/hooks/use-dark-mode.ts +48 -0
- package/src/hooks/use-flash-toast.ts +29 -0
- package/src/hooks/use-initials.tsx +24 -0
- package/src/hooks/use-mobile-navigation.ts +12 -0
- package/src/hooks/use-mobile.tsx +38 -0
- package/src/index.ts +408 -0
- package/src/layouts/AppLayout.tsx +60 -0
- package/src/layouts/AuthLayout.tsx +32 -0
- package/src/layouts/SettingsLayout.tsx +21 -0
- package/src/layouts/app/AIChatLayout.tsx +73 -0
- package/src/layouts/app/AsideSidebarLayout.tsx +3 -0
- package/src/layouts/app/CalendarSidebarLayout.tsx +69 -0
- package/src/layouts/app/CommunitiesNavbarLayout.tsx +3 -0
- package/src/layouts/app/DualNavbarSidebarLayout.tsx +3 -0
- package/src/layouts/app/FocusSidebarLayout.tsx +75 -0
- package/src/layouts/app/MailLayout.tsx +69 -0
- package/src/layouts/app/MegaMenuHeaderLayout.tsx +3 -0
- package/src/layouts/app/MegaMenuLayout.tsx +81 -0
- package/src/layouts/app/MegaMenuNavbarLayout.tsx +88 -0
- package/src/layouts/app/MegaMenuSearchNavbarLayout.tsx +3 -0
- package/src/layouts/app/NavbarCollapsibleLayout.tsx +88 -0
- package/src/layouts/app/NavbarCollapsibleLinksLayout.tsx +3 -0
- package/src/layouts/app/NavbarMinimalLayout.tsx +3 -0
- package/src/layouts/app/NavbarMinimalSidebarLayout.tsx +3 -0
- package/src/layouts/app/NavbarSidebarDashboardLayout.tsx +3 -0
- package/src/layouts/app/NavbarSidebarLayout.tsx +92 -0
- package/src/layouts/app/NavbarSimpleSidebarLayout.tsx +3 -0
- package/src/layouts/app/NavbarTitledSidebarLayout.tsx +3 -0
- package/src/layouts/app/PanelSidebarLayout.tsx +3 -0
- package/src/layouts/app/SearchNavbarSidebarLayout.tsx +3 -0
- package/src/layouts/app/SidebarBreadcrumbLayout.tsx +3 -0
- package/src/layouts/app/SidebarCleanLayout.tsx +3 -0
- package/src/layouts/app/SidebarCommunitiesLayout.tsx +3 -0
- package/src/layouts/app/SidebarContentLayout.tsx +3 -0
- package/src/layouts/app/SidebarDualMenuLayout.tsx +104 -0
- package/src/layouts/app/SidebarFixedLayout.tsx +166 -0
- package/src/layouts/app/SidebarFooterNavbarLayout.tsx +3 -0
- package/src/layouts/app/SidebarHeaderMenuLayout.tsx +3 -0
- package/src/layouts/app/SidebarMegaMenuLayout.tsx +4 -0
- package/src/layouts/app/SidebarMinimalLayout.tsx +70 -0
- package/src/layouts/app/SidebarMobileSearchLayout.tsx +3 -0
- package/src/layouts/app/SidebarMultiPanelLayout.tsx +3 -0
- package/src/layouts/app/SidebarPrimarySecondaryLayout.tsx +3 -0
- package/src/layouts/app/SidebarSearchHeaderLayout.tsx +103 -0
- package/src/layouts/app/SidebarSearchToolbarLayout.tsx +3 -0
- package/src/layouts/app/SidebarTabsDualLayout.tsx +3 -0
- package/src/layouts/app/SidebarTabsLayout.tsx +98 -0
- package/src/layouts/app/SidebarTreeLayout.tsx +3 -0
- package/src/layouts/app/SplitNavbarLayout.tsx +3 -0
- package/src/layouts/app/SplitSidebarDashboardLayout.tsx +3 -0
- package/src/layouts/app/SplitSidebarLayout.tsx +99 -0
- package/src/layouts/app/TopNavLayout.tsx +105 -0
- package/src/layouts/app/TopNavLinksLayout.tsx +3 -0
- package/src/layouts/app/WorkspaceBreadcrumbLayout.tsx +3 -0
- package/src/layouts/app/WorkspaceCommunitiesLayout.tsx +3 -0
- package/src/layouts/app/WorkspaceNavbarLayout.tsx +3 -0
- package/src/layouts/app/WorkspaceSidebarLayout.tsx +98 -0
- package/src/layouts/app/WorkspaceSidebarTitleLayout.tsx +3 -0
- package/src/layouts/app/app-header-layout.tsx +45 -0
- package/src/layouts/app/app-sidebar-layout.tsx +56 -0
- package/src/layouts/app/layout-context.tsx +44 -0
- package/src/layouts/app/layout-types.ts +47 -0
- package/src/layouts/app/partials/Footer.tsx +35 -0
- package/src/layouts/app/partials/HeaderTopbar.tsx +96 -0
- package/src/layouts/app/partials/Navbar.tsx +85 -0
- package/src/layouts/app/partials/Toolbar.tsx +47 -0
- package/src/layouts/app-layout.tsx +29 -0
- package/src/layouts/auth/AuthBrandedLayout.tsx +58 -0
- package/src/layouts/auth/AuthCardLayout.tsx +31 -0
- package/src/layouts/auth/AuthCenteredLayout.tsx +41 -0
- package/src/layouts/auth/AuthClassicLayout.tsx +41 -0
- package/src/layouts/auth/AuthSimpleLayout.tsx +33 -0
- package/src/layouts/auth/AuthSplitLayout.tsx +89 -0
- package/src/layouts/web-app-layout.tsx +162 -0
- package/src/layouts/web-layout.tsx +23 -0
- package/src/lib/datetime.ts +188 -0
- package/src/lib/google-maps-loader.ts +99 -0
- package/src/lib/location.ts +127 -0
- package/src/lib/lucide-icon-map.ts +132 -0
- package/src/lib/map-markers.ts +124 -0
- package/src/lib/map-styles.ts +351 -0
- package/src/lib/utils.ts +11 -0
- package/src/platform/adapters/default.tsx +156 -0
- package/src/platform/adapters/inertia.tsx +88 -0
- package/src/platform/adapters/nextjs.ts +86 -0
- package/src/platform/context.tsx +106 -0
- package/src/platform/index.ts +27 -0
- package/src/platform/types.ts +105 -0
- package/src/styles/layouts/sidebar-fixed.css +161 -0
- package/src/styles/themes.css +583 -0
- package/src/types/assets.d.ts +5 -0
- package/src/types/auth.ts +25 -0
- package/src/types/global.d.ts +13 -0
- package/src/types/index.ts +9 -0
- package/src/types/navigation.ts +15 -0
- package/src/types/ui.ts +32 -0
|
@@ -0,0 +1,101 @@
|
|
|
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 '../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-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default:
|
|
13
|
+
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
|
14
|
+
// Alias: 'primary' maps to the default shadcn look
|
|
15
|
+
primary:
|
|
16
|
+
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
|
17
|
+
destructive:
|
|
18
|
+
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
|
|
19
|
+
outline:
|
|
20
|
+
'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
|
|
21
|
+
secondary:
|
|
22
|
+
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
|
23
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
24
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
25
|
+
},
|
|
26
|
+
size: {
|
|
27
|
+
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
|
28
|
+
// Legacy aliases used by existing auth forms
|
|
29
|
+
sm: 'h-8 rounded-md px-3 has-[>svg]:px-2.5',
|
|
30
|
+
md: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
|
31
|
+
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
|
32
|
+
icon: 'size-9',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: {
|
|
36
|
+
variant: 'default',
|
|
37
|
+
size: 'default',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
interface ButtonProps
|
|
43
|
+
extends React.ComponentProps<'button'>,
|
|
44
|
+
VariantProps<typeof buttonVariants> {
|
|
45
|
+
asChild?: boolean;
|
|
46
|
+
loading?: boolean;
|
|
47
|
+
fullWidth?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function Button({
|
|
51
|
+
className,
|
|
52
|
+
variant,
|
|
53
|
+
size,
|
|
54
|
+
asChild = false,
|
|
55
|
+
loading = false,
|
|
56
|
+
fullWidth = false,
|
|
57
|
+
disabled,
|
|
58
|
+
children,
|
|
59
|
+
...props
|
|
60
|
+
}: ButtonProps) {
|
|
61
|
+
const Comp = asChild ? Slot : 'button';
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Comp
|
|
65
|
+
data-slot="button"
|
|
66
|
+
disabled={disabled || loading}
|
|
67
|
+
className={cn(
|
|
68
|
+
buttonVariants({ variant, size }),
|
|
69
|
+
fullWidth && 'w-full',
|
|
70
|
+
className,
|
|
71
|
+
)}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
{loading && (
|
|
75
|
+
<svg
|
|
76
|
+
className="size-4 animate-spin shrink-0"
|
|
77
|
+
viewBox="0 0 24 24"
|
|
78
|
+
fill="none"
|
|
79
|
+
aria-hidden="true"
|
|
80
|
+
>
|
|
81
|
+
<circle
|
|
82
|
+
className="opacity-25"
|
|
83
|
+
cx="12"
|
|
84
|
+
cy="12"
|
|
85
|
+
r="10"
|
|
86
|
+
stroke="currentColor"
|
|
87
|
+
strokeWidth="4"
|
|
88
|
+
/>
|
|
89
|
+
<path
|
|
90
|
+
className="opacity-75"
|
|
91
|
+
fill="currentColor"
|
|
92
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
93
|
+
/>
|
|
94
|
+
</svg>
|
|
95
|
+
)}
|
|
96
|
+
{children}
|
|
97
|
+
</Comp>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { InputHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
4
|
+
label?: ReactNode;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Checkbox({ label, error, className = '', id, ...props }: Props) {
|
|
9
|
+
return (
|
|
10
|
+
<div>
|
|
11
|
+
<label
|
|
12
|
+
htmlFor={id}
|
|
13
|
+
className="flex items-center gap-2.5 cursor-pointer select-none"
|
|
14
|
+
>
|
|
15
|
+
<input
|
|
16
|
+
id={id}
|
|
17
|
+
type="checkbox"
|
|
18
|
+
className={`h-4 w-4 rounded border-border text-primary focus:ring-2 focus:ring-ring focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
|
|
19
|
+
aria-invalid={!!error}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
{label && (
|
|
23
|
+
<span className="text-sm text-foreground">{label}</span>
|
|
24
|
+
)}
|
|
25
|
+
</label>
|
|
26
|
+
{error && <p className="mt-1 text-xs text-destructive">{error}</p>}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { DateField as DateFieldPrimitive, DateInput, DateSegment } from '../components/ui/datefield';
|
|
2
|
+
import { Label } from './Label';
|
|
3
|
+
import { cn } from '../lib/utils';
|
|
4
|
+
import type { DateValue } from 'react-aria-components';
|
|
5
|
+
import type { DateFieldProps } from 'react-aria-components';
|
|
6
|
+
|
|
7
|
+
interface DateFieldControlProps<T extends DateValue> extends DateFieldProps<T> {
|
|
8
|
+
label?: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
inputClassName?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DateField<T extends DateValue>({
|
|
15
|
+
label,
|
|
16
|
+
error,
|
|
17
|
+
className,
|
|
18
|
+
inputClassName,
|
|
19
|
+
id,
|
|
20
|
+
...props
|
|
21
|
+
}: DateFieldControlProps<T>) {
|
|
22
|
+
return (
|
|
23
|
+
<div className={cn('flex flex-col gap-2', className)}>
|
|
24
|
+
{label && <Label htmlFor={id}>{label}</Label>}
|
|
25
|
+
<DateFieldPrimitive {...props}>
|
|
26
|
+
<DateInput className={cn(
|
|
27
|
+
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors',
|
|
28
|
+
'placeholder:text-muted-foreground focus-within:outline-none focus-within:ring-1 focus-within:ring-ring',
|
|
29
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
30
|
+
error && 'border-destructive focus-within:ring-destructive',
|
|
31
|
+
inputClassName,
|
|
32
|
+
)} />
|
|
33
|
+
</DateFieldPrimitive>
|
|
34
|
+
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Label } from './Label';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
label: string;
|
|
6
|
+
htmlFor?: string;
|
|
7
|
+
hint?: string;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function FormField({ label, htmlFor, hint, required, children }: Props) {
|
|
13
|
+
return (
|
|
14
|
+
<div>
|
|
15
|
+
<Label htmlFor={htmlFor} required={required}>{label}</Label>
|
|
16
|
+
{children}
|
|
17
|
+
{hint && <p className="mt-1 text-xs text-muted-foreground">{hint}</p>}
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
type Level = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
4
|
+
|
|
5
|
+
const sizes: Record<Level, string> = {
|
|
6
|
+
h1: 'text-4xl font-bold tracking-tight',
|
|
7
|
+
h2: 'text-3xl font-bold tracking-tight',
|
|
8
|
+
h3: 'text-2xl font-semibold tracking-tight',
|
|
9
|
+
h4: 'text-xl font-semibold',
|
|
10
|
+
h5: 'text-lg font-semibold',
|
|
11
|
+
h6: 'text-base font-semibold',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
interface Props extends HTMLAttributes<HTMLHeadingElement> {
|
|
15
|
+
as?: Level;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Heading({ as: Tag = 'h2', className = '', children, ...props }: Props) {
|
|
20
|
+
return (
|
|
21
|
+
<Tag
|
|
22
|
+
className={`text-foreground ${sizes[Tag]} ${className}`}
|
|
23
|
+
{...props}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</Tag>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { InputHTMLAttributes } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
|
4
|
+
error?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const base =
|
|
8
|
+
'w-full rounded-lg border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 transition-colors';
|
|
9
|
+
|
|
10
|
+
export function Input({ error, className = '', ...props }: Props) {
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<input
|
|
14
|
+
className={`${base} ${error ? 'border-destructive' : 'border-border'} ${className}`}
|
|
15
|
+
aria-invalid={!!error}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
{error && <p className="mt-1 text-xs text-destructive">{error}</p>}
|
|
19
|
+
</>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { LabelHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props extends LabelHTMLAttributes<HTMLLabelElement> {
|
|
4
|
+
required?: boolean;
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Label({ required, children, className = '', ...props }: Props) {
|
|
9
|
+
return (
|
|
10
|
+
<label
|
|
11
|
+
className={`block text-sm font-medium text-foreground mb-1 ${className}`}
|
|
12
|
+
{...props}
|
|
13
|
+
>
|
|
14
|
+
{children}
|
|
15
|
+
{required && <span className="ml-0.5 text-destructive" aria-hidden="true">*</span>}
|
|
16
|
+
</label>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
const sizes = {
|
|
4
|
+
xs: 'text-xs',
|
|
5
|
+
sm: 'text-sm',
|
|
6
|
+
md: 'text-base',
|
|
7
|
+
lg: 'text-lg',
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
const variants = {
|
|
11
|
+
default: 'text-foreground',
|
|
12
|
+
muted: 'text-muted-foreground',
|
|
13
|
+
lead: 'text-muted-foreground text-xl',
|
|
14
|
+
error: 'text-destructive',
|
|
15
|
+
success: 'text-green-600',
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
interface Props extends HTMLAttributes<HTMLParagraphElement> {
|
|
19
|
+
size?: keyof typeof sizes;
|
|
20
|
+
variant?: keyof typeof variants;
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Paragraph({
|
|
25
|
+
size = 'md',
|
|
26
|
+
variant = 'default',
|
|
27
|
+
className = '',
|
|
28
|
+
children,
|
|
29
|
+
...props
|
|
30
|
+
}: Props) {
|
|
31
|
+
return (
|
|
32
|
+
<p
|
|
33
|
+
className={`leading-relaxed ${sizes[size]} ${variants[variant]} ${className}`}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</p>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Eye, EyeOff } from 'lucide-react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import type { InputHTMLAttributes } from 'react';
|
|
6
|
+
|
|
7
|
+
interface Props extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const base =
|
|
12
|
+
'w-full rounded-lg border bg-background px-3 py-2 pr-10 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 transition-colors';
|
|
13
|
+
|
|
14
|
+
export function PasswordInput({ error, className = '', ...props }: Props) {
|
|
15
|
+
const [visible, setVisible] = useState(false);
|
|
16
|
+
const Icon = visible ? EyeOff : Eye;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<div className="relative">
|
|
21
|
+
<input
|
|
22
|
+
type={visible ? 'text' : 'password'}
|
|
23
|
+
className={`${base} ${error ? 'border-destructive' : 'border-border'} ${className}`}
|
|
24
|
+
aria-invalid={!!error}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
onClick={() => setVisible(v => !v)}
|
|
30
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
|
31
|
+
aria-label={visible ? 'Hide password' : 'Show password'}
|
|
32
|
+
tabIndex={-1}
|
|
33
|
+
>
|
|
34
|
+
<Icon className="h-4 w-4" />
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
{error && <p className="mt-1 text-xs text-destructive">{error}</p>}
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { RadioGroup as RadioGroupPrimitive, RadioGroupItem } from '../components/ui/radio-group';
|
|
3
|
+
import { Label } from './Label';
|
|
4
|
+
import { cn } from '../lib/utils';
|
|
5
|
+
|
|
6
|
+
interface RadioOption {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface RadioGroupProps {
|
|
14
|
+
name?: string;
|
|
15
|
+
value?: string;
|
|
16
|
+
defaultValue?: string;
|
|
17
|
+
onChange?: (value: string) => void;
|
|
18
|
+
options: RadioOption[];
|
|
19
|
+
error?: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
orientation?: 'horizontal' | 'vertical';
|
|
22
|
+
children?: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function RadioGroup({
|
|
26
|
+
name,
|
|
27
|
+
value,
|
|
28
|
+
defaultValue,
|
|
29
|
+
onChange,
|
|
30
|
+
options,
|
|
31
|
+
error,
|
|
32
|
+
className,
|
|
33
|
+
orientation = 'vertical',
|
|
34
|
+
children,
|
|
35
|
+
}: RadioGroupProps) {
|
|
36
|
+
return (
|
|
37
|
+
<div className={cn('space-y-1', className)}>
|
|
38
|
+
<RadioGroupPrimitive
|
|
39
|
+
name={name}
|
|
40
|
+
value={value}
|
|
41
|
+
defaultValue={defaultValue}
|
|
42
|
+
onValueChange={onChange}
|
|
43
|
+
className={cn(
|
|
44
|
+
'flex gap-3',
|
|
45
|
+
orientation === 'vertical' ? 'flex-col' : 'flex-row flex-wrap',
|
|
46
|
+
)}
|
|
47
|
+
>
|
|
48
|
+
{children ?? options.map((opt) => (
|
|
49
|
+
<div key={opt.value} className="flex items-start gap-2">
|
|
50
|
+
<RadioGroupItem
|
|
51
|
+
id={`${name}-${opt.value}`}
|
|
52
|
+
value={opt.value}
|
|
53
|
+
disabled={opt.disabled}
|
|
54
|
+
className="mt-0.5"
|
|
55
|
+
/>
|
|
56
|
+
<div className="flex flex-col gap-0.5">
|
|
57
|
+
<Label htmlFor={`${name}-${opt.value}`} className="font-normal cursor-pointer">
|
|
58
|
+
{opt.label}
|
|
59
|
+
</Label>
|
|
60
|
+
{opt.description && (
|
|
61
|
+
<span className="text-xs text-muted-foreground">{opt.description}</span>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
))}
|
|
66
|
+
</RadioGroupPrimitive>
|
|
67
|
+
{error && <p className="text-xs text-destructive mt-1">{error}</p>}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode, SelectHTMLAttributes } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props extends SelectHTMLAttributes<HTMLSelectElement> {
|
|
4
|
+
error?: string;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const base =
|
|
9
|
+
'w-full rounded-lg border bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 transition-colors appearance-none bg-[url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'16\' height=\'16\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'%236b7280\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\'%3E%3Cpath d=\'m6 9 6 6 6-6\'/%3E%3C/svg%3E")] bg-no-repeat bg-[right_0.5rem_center] pr-9';
|
|
10
|
+
|
|
11
|
+
export function Select({ error, className = '', children, ...props }: Props) {
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
<select
|
|
15
|
+
className={`${base} ${error ? 'border-destructive' : 'border-border'} ${className}`}
|
|
16
|
+
aria-invalid={!!error}
|
|
17
|
+
{...props}
|
|
18
|
+
>
|
|
19
|
+
{children}
|
|
20
|
+
</select>
|
|
21
|
+
{error && <p className="mt-1 text-xs text-destructive">{error}</p>}
|
|
22
|
+
</>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
import { Slider as SliderPrimitive, SliderThumb } from '../components/ui/slider';
|
|
3
|
+
import { Label } from './Label';
|
|
4
|
+
import { cn } from '../lib/utils';
|
|
5
|
+
|
|
6
|
+
interface SliderProps extends ComponentProps<typeof SliderPrimitive> {
|
|
7
|
+
label?: string;
|
|
8
|
+
showValue?: boolean;
|
|
9
|
+
error?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Slider({ label, showValue, error, className, ...props }: SliderProps) {
|
|
14
|
+
const currentValue = Array.isArray(props.value) ? props.value : props.defaultValue;
|
|
15
|
+
const displayValue = Array.isArray(currentValue) ? currentValue[0] : currentValue;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className={cn('space-y-2', className)}>
|
|
19
|
+
{(label || showValue) && (
|
|
20
|
+
<div className="flex items-center justify-between">
|
|
21
|
+
{label && <Label className="font-normal">{label}</Label>}
|
|
22
|
+
{showValue && displayValue !== undefined && (
|
|
23
|
+
<span className="text-sm text-muted-foreground">{displayValue}</span>
|
|
24
|
+
)}
|
|
25
|
+
</div>
|
|
26
|
+
)}
|
|
27
|
+
<SliderPrimitive {...props}>
|
|
28
|
+
<SliderThumb />
|
|
29
|
+
</SliderPrimitive>
|
|
30
|
+
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
import { Switch as SwitchPrimitive, SwitchWrapper } from '../components/ui/switch';
|
|
3
|
+
import { Label } from './Label';
|
|
4
|
+
import { cn } from '../lib/utils';
|
|
5
|
+
|
|
6
|
+
interface SwitchProps extends ComponentProps<typeof SwitchPrimitive> {
|
|
7
|
+
label?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
labelClassName?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Switch({ label, description, error, labelClassName, className, id, ...props }: SwitchProps) {
|
|
14
|
+
const switchId = id ?? `switch-${Math.random().toString(36).slice(2, 7)}`;
|
|
15
|
+
return (
|
|
16
|
+
<SwitchWrapper className={cn('flex items-center gap-3', className)}>
|
|
17
|
+
<SwitchPrimitive id={switchId} {...props} />
|
|
18
|
+
{(label || description) && (
|
|
19
|
+
<div className="flex flex-col gap-0.5">
|
|
20
|
+
{label && (
|
|
21
|
+
<Label htmlFor={switchId} className={cn('font-normal cursor-pointer', labelClassName)}>
|
|
22
|
+
{label}
|
|
23
|
+
</Label>
|
|
24
|
+
)}
|
|
25
|
+
{description && <span className="text-xs text-muted-foreground">{description}</span>}
|
|
26
|
+
</div>
|
|
27
|
+
)}
|
|
28
|
+
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
29
|
+
</SwitchWrapper>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TextareaHTMLAttributes } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
4
|
+
error?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const base =
|
|
8
|
+
'w-full rounded-lg border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 resize-y transition-colors';
|
|
9
|
+
|
|
10
|
+
export function Textarea({ error, className = '', rows = 4, ...props }: Props) {
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<textarea
|
|
14
|
+
rows={rows}
|
|
15
|
+
className={`${base} ${error ? 'border-destructive' : 'border-border'} ${className}`}
|
|
16
|
+
aria-invalid={!!error}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
{error && <p className="mt-1 text-xs text-destructive">{error}</p>}
|
|
20
|
+
</>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { FormEvent } from 'react';
|
|
2
|
+
import { Button } from '../controls/Button';
|
|
3
|
+
import { FormField } from '../controls/FormField';
|
|
4
|
+
import { PasswordInput } from '../controls/PasswordInput';
|
|
5
|
+
|
|
6
|
+
export interface ConfirmPasswordFormErrors {
|
|
7
|
+
password?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
password: string;
|
|
12
|
+
errors: ConfirmPasswordFormErrors;
|
|
13
|
+
processing?: boolean;
|
|
14
|
+
onChange: (value: string) => void;
|
|
15
|
+
onSubmit: (e: FormEvent) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ConfirmPasswordForm({ password, errors, processing = false, onChange, onSubmit }: Props) {
|
|
19
|
+
return (
|
|
20
|
+
<form onSubmit={onSubmit} className="space-y-4">
|
|
21
|
+
<p className="text-sm text-muted-foreground">
|
|
22
|
+
This is a secure area of the application. Please confirm your password before continuing.
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<FormField label="Password" htmlFor="confirm-password">
|
|
26
|
+
<PasswordInput
|
|
27
|
+
id="confirm-password"
|
|
28
|
+
value={password}
|
|
29
|
+
onChange={e => onChange(e.target.value)}
|
|
30
|
+
placeholder="••••••••"
|
|
31
|
+
error={errors.password}
|
|
32
|
+
required
|
|
33
|
+
autoFocus
|
|
34
|
+
autoComplete="current-password"
|
|
35
|
+
/>
|
|
36
|
+
</FormField>
|
|
37
|
+
|
|
38
|
+
<Button type="submit" loading={processing} fullWidth>
|
|
39
|
+
Confirm password
|
|
40
|
+
</Button>
|
|
41
|
+
</form>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { cn } from '../lib/utils';
|
|
2
|
+
|
|
3
|
+
export type DeviceStatus = 'online' | 'offline' | 'idle' | 'moving' | 'error' | 'unknown';
|
|
4
|
+
|
|
5
|
+
const STATUS_CONFIG: Record<DeviceStatus, { label: string; dotClass: string; textClass: string; bgClass: string }> = {
|
|
6
|
+
online: { label: 'Online', dotClass: 'bg-success', textClass: 'text-success', bgClass: 'bg-success/10 border-success/20' },
|
|
7
|
+
moving: { label: 'Moving', dotClass: 'bg-primary', textClass: 'text-primary', bgClass: 'bg-primary/10 border-primary/20' },
|
|
8
|
+
idle: { label: 'Idle', dotClass: 'bg-yellow-500', textClass: 'text-yellow-600', bgClass: 'bg-yellow-50 border-yellow-200' },
|
|
9
|
+
offline: { label: 'Offline', dotClass: 'bg-muted-foreground', textClass: 'text-muted-foreground', bgClass: 'bg-muted border-border' },
|
|
10
|
+
error: { label: 'Error', dotClass: 'bg-destructive', textClass: 'text-destructive', bgClass: 'bg-destructive/10 border-destructive/20' },
|
|
11
|
+
unknown: { label: 'Unknown', dotClass: 'bg-muted-foreground', textClass: 'text-muted-foreground', bgClass: 'bg-muted border-border' },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
interface DeviceStatusBadgeProps {
|
|
15
|
+
status: DeviceStatus;
|
|
16
|
+
showDot?: boolean;
|
|
17
|
+
size?: 'sm' | 'md';
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function DeviceStatusBadge({ status, showDot = true, size = 'md', className }: DeviceStatusBadgeProps) {
|
|
22
|
+
const conf = STATUS_CONFIG[status] ?? STATUS_CONFIG.unknown;
|
|
23
|
+
return (
|
|
24
|
+
<span className={cn(
|
|
25
|
+
'inline-flex items-center gap-1.5 rounded-full border font-medium',
|
|
26
|
+
size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-2.5 py-1 text-xs',
|
|
27
|
+
conf.bgClass, conf.textClass,
|
|
28
|
+
className,
|
|
29
|
+
)}>
|
|
30
|
+
{showDot && (
|
|
31
|
+
<span className={cn('size-1.5 rounded-full shrink-0', conf.dotClass,
|
|
32
|
+
(status === 'online' || status === 'moving') && 'animate-pulse',
|
|
33
|
+
)} />
|
|
34
|
+
)}
|
|
35
|
+
{conf.label}
|
|
36
|
+
</span>
|
|
37
|
+
);
|
|
38
|
+
}
|