@djangocfg/ui-core 1.0.1

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.
Files changed (88) hide show
  1. package/README.md +135 -0
  2. package/package.json +111 -0
  3. package/src/components/accordion.tsx +56 -0
  4. package/src/components/alert-dialog.tsx +142 -0
  5. package/src/components/alert.tsx +59 -0
  6. package/src/components/aspect-ratio.tsx +7 -0
  7. package/src/components/avatar.tsx +50 -0
  8. package/src/components/badge.tsx +36 -0
  9. package/src/components/button-group.tsx +85 -0
  10. package/src/components/button.tsx +111 -0
  11. package/src/components/calendar.tsx +213 -0
  12. package/src/components/card.tsx +76 -0
  13. package/src/components/carousel.tsx +261 -0
  14. package/src/components/chart.tsx +369 -0
  15. package/src/components/checkbox.tsx +29 -0
  16. package/src/components/collapsible.tsx +11 -0
  17. package/src/components/combobox.tsx +182 -0
  18. package/src/components/command.tsx +170 -0
  19. package/src/components/context-menu.tsx +200 -0
  20. package/src/components/copy.tsx +144 -0
  21. package/src/components/dialog.tsx +122 -0
  22. package/src/components/drawer.tsx +137 -0
  23. package/src/components/empty.tsx +104 -0
  24. package/src/components/field.tsx +244 -0
  25. package/src/components/form.tsx +178 -0
  26. package/src/components/hover-card.tsx +29 -0
  27. package/src/components/image-with-fallback.tsx +170 -0
  28. package/src/components/index.ts +86 -0
  29. package/src/components/input-group.tsx +170 -0
  30. package/src/components/input-otp.tsx +81 -0
  31. package/src/components/input.tsx +22 -0
  32. package/src/components/item.tsx +195 -0
  33. package/src/components/kbd.tsx +28 -0
  34. package/src/components/label.tsx +26 -0
  35. package/src/components/multi-select.tsx +222 -0
  36. package/src/components/og-image.tsx +47 -0
  37. package/src/components/popover.tsx +33 -0
  38. package/src/components/portal.tsx +106 -0
  39. package/src/components/preloader.tsx +250 -0
  40. package/src/components/progress.tsx +28 -0
  41. package/src/components/radio-group.tsx +43 -0
  42. package/src/components/resizable.tsx +111 -0
  43. package/src/components/scroll-area.tsx +102 -0
  44. package/src/components/section.tsx +58 -0
  45. package/src/components/select.tsx +158 -0
  46. package/src/components/separator.tsx +31 -0
  47. package/src/components/sheet.tsx +140 -0
  48. package/src/components/skeleton.tsx +15 -0
  49. package/src/components/slider.tsx +28 -0
  50. package/src/components/spinner.tsx +16 -0
  51. package/src/components/sticky.tsx +117 -0
  52. package/src/components/switch.tsx +29 -0
  53. package/src/components/table.tsx +120 -0
  54. package/src/components/tabs.tsx +238 -0
  55. package/src/components/textarea.tsx +22 -0
  56. package/src/components/toast.tsx +129 -0
  57. package/src/components/toaster.tsx +41 -0
  58. package/src/components/toggle-group.tsx +61 -0
  59. package/src/components/toggle.tsx +45 -0
  60. package/src/components/token-icon.tsx +156 -0
  61. package/src/components/tooltip-provider-safe.tsx +43 -0
  62. package/src/components/tooltip.tsx +32 -0
  63. package/src/hooks/index.ts +15 -0
  64. package/src/hooks/useCopy.ts +41 -0
  65. package/src/hooks/useCountdown.ts +73 -0
  66. package/src/hooks/useDebounce.ts +25 -0
  67. package/src/hooks/useDebouncedCallback.ts +58 -0
  68. package/src/hooks/useDebugTools.ts +52 -0
  69. package/src/hooks/useEventsBus.ts +53 -0
  70. package/src/hooks/useImageLoader.ts +95 -0
  71. package/src/hooks/useMediaQuery.ts +40 -0
  72. package/src/hooks/useMobile.tsx +22 -0
  73. package/src/hooks/useToast.ts +194 -0
  74. package/src/index.ts +14 -0
  75. package/src/lib/index.ts +2 -0
  76. package/src/lib/og-image.ts +151 -0
  77. package/src/lib/utils.ts +6 -0
  78. package/src/styles/base.css +20 -0
  79. package/src/styles/globals.css +12 -0
  80. package/src/styles/index.css +25 -0
  81. package/src/styles/sources.css +11 -0
  82. package/src/styles/theme/animations.css +65 -0
  83. package/src/styles/theme/dark.css +49 -0
  84. package/src/styles/theme/light.css +50 -0
  85. package/src/styles/theme/tokens.css +134 -0
  86. package/src/styles/theme.css +22 -0
  87. package/src/styles/utilities.css +187 -0
  88. package/src/types/index.ts +0 -0
@@ -0,0 +1,43 @@
1
+ "use client"
2
+
3
+ import * as React from 'react';
4
+ import { TooltipProvider as RadixTooltipProvider } from '@radix-ui/react-tooltip';
5
+
6
+ interface SafeTooltipProviderProps {
7
+ children: React.ReactNode;
8
+ delayDuration?: number;
9
+ skipDelayDuration?: number;
10
+ disableHoverableContent?: boolean;
11
+ }
12
+
13
+ /**
14
+ * SafeTooltipProvider - SSR-safe wrapper for Radix TooltipProvider
15
+ * Only renders on client-side to avoid hydration mismatches
16
+ */
17
+ export function SafeTooltipProvider({
18
+ children,
19
+ delayDuration = 700,
20
+ skipDelayDuration = 300,
21
+ disableHoverableContent,
22
+ }: SafeTooltipProviderProps) {
23
+ const [mounted, setMounted] = React.useState(false);
24
+
25
+ React.useEffect(() => {
26
+ setMounted(true);
27
+ }, []);
28
+
29
+ if (!mounted) {
30
+ // During SSR, return children without TooltipProvider
31
+ return <>{children}</>;
32
+ }
33
+
34
+ return (
35
+ <RadixTooltipProvider
36
+ delayDuration={delayDuration}
37
+ skipDelayDuration={skipDelayDuration}
38
+ disableHoverableContent={disableHoverableContent}
39
+ >
40
+ {children}
41
+ </RadixTooltipProvider>
42
+ );
43
+ }
@@ -0,0 +1,32 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5
+
6
+ import { cn } from "../lib/utils"
7
+
8
+ const TooltipProvider = TooltipPrimitive.Provider
9
+
10
+ const Tooltip = TooltipPrimitive.Root
11
+
12
+ const TooltipTrigger = TooltipPrimitive.Trigger
13
+
14
+ const TooltipContent = React.forwardRef<
15
+ React.ElementRef<typeof TooltipPrimitive.Content>,
16
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
17
+ >(({ className, sideOffset = 4, ...props }, ref) => (
18
+ <TooltipPrimitive.Portal>
19
+ <TooltipPrimitive.Content
20
+ ref={ref}
21
+ sideOffset={sideOffset}
22
+ className={cn(
23
+ "z-150 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-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",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ </TooltipPrimitive.Portal>
29
+ ))
30
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName
31
+
32
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,15 @@
1
+ // ============================================================================
2
+ // Hooks - Reusable React Hooks (No Next.js/Browser Storage dependencies)
3
+ // For use in Electron, Vite, CRA apps
4
+ // ============================================================================
5
+
6
+ export { useCountdown } from './useCountdown';
7
+ export { useDebouncedCallback } from './useDebouncedCallback';
8
+ export { useDebounce } from './useDebounce';
9
+ export { useDebugTools } from './useDebugTools';
10
+ export { useEventListener, events } from './useEventsBus';
11
+ export { useIsMobile } from './useMobile';
12
+ export { useMediaQuery } from './useMediaQuery';
13
+ export { useCopy } from './useCopy';
14
+ export { useImageLoader } from './useImageLoader';
15
+ export { useToast, toast } from './useToast';
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+
5
+ import { useToast } from './useToast';
6
+
7
+ interface UseCopyOptions {
8
+ successMessage?: string;
9
+ errorMessage?: string;
10
+ }
11
+
12
+ export const useCopy = (options: UseCopyOptions = {}) => {
13
+ const { toast } = useToast();
14
+
15
+ const {
16
+ successMessage = "Copied to clipboard",
17
+ errorMessage = "Failed to copy to clipboard"
18
+ } = options;
19
+
20
+ const copyToClipboard = useCallback(async (text: string, customSuccessMessage?: string) => {
21
+ try {
22
+ await navigator.clipboard.writeText(text);
23
+ toast({
24
+ title: "Success!",
25
+ description: customSuccessMessage || successMessage,
26
+ variant: "default",
27
+ });
28
+ return true;
29
+ } catch (error) {
30
+ console.error('Failed to copy:', error);
31
+ toast({
32
+ title: "Error",
33
+ description: errorMessage,
34
+ variant: "destructive",
35
+ });
36
+ return false;
37
+ }
38
+ }, [toast, successMessage, errorMessage]);
39
+
40
+ return { copyToClipboard };
41
+ };
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+
3
+ import moment from 'moment';
4
+ import { useEffect, useState } from 'react';
5
+
6
+ interface CountdownState {
7
+ days: number;
8
+ hours: number;
9
+ minutes: number;
10
+ seconds: number;
11
+ isExpired: boolean;
12
+ totalSeconds: number;
13
+ }
14
+
15
+ export const useCountdown = (targetDate: string | null): CountdownState => {
16
+ const [countdown, setCountdown] = useState<CountdownState>({
17
+ days: 0,
18
+ hours: 0,
19
+ minutes: 0,
20
+ seconds: 0,
21
+ isExpired: false,
22
+ totalSeconds: 0,
23
+ });
24
+
25
+ useEffect(() => {
26
+ if (!targetDate) {
27
+ return;
28
+ }
29
+
30
+ const target = moment.utc(targetDate);
31
+
32
+ const updateCountdown = () => {
33
+ const now = moment.utc();
34
+ const diff = target.diff(now, 'seconds');
35
+
36
+ if (diff <= 0) {
37
+ setCountdown({
38
+ days: 0,
39
+ hours: 0,
40
+ minutes: 0,
41
+ seconds: 0,
42
+ isExpired: true,
43
+ totalSeconds: 0,
44
+ });
45
+ return;
46
+ }
47
+
48
+ const days = Math.floor(diff / (24 * 60 * 60));
49
+ const hours = Math.floor((diff % (24 * 60 * 60)) / (60 * 60));
50
+ const minutes = Math.floor((diff % (60 * 60)) / 60);
51
+ const seconds = diff % 60;
52
+
53
+ setCountdown({
54
+ days,
55
+ hours,
56
+ minutes,
57
+ seconds,
58
+ isExpired: false,
59
+ totalSeconds: diff,
60
+ });
61
+ };
62
+
63
+ // Update immediately
64
+ updateCountdown();
65
+
66
+ // Update every second
67
+ const interval = setInterval(updateCountdown, 1000);
68
+
69
+ return () => clearInterval(interval);
70
+ }, [targetDate]);
71
+
72
+ return countdown;
73
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * useDebounce hook
3
+ *
4
+ * Debounces a value by delaying its update until after a specified delay.
5
+ */
6
+
7
+ 'use client';
8
+
9
+ import { useState, useEffect } from 'react';
10
+
11
+ export function useDebounce<T>(value: T, delay: number = 300): T {
12
+ const [debouncedValue, setDebouncedValue] = useState<T>(value);
13
+
14
+ useEffect(() => {
15
+ const handler = setTimeout(() => {
16
+ setDebouncedValue(value);
17
+ }, delay);
18
+
19
+ return () => {
20
+ clearTimeout(handler);
21
+ };
22
+ }, [value, delay]);
23
+
24
+ return debouncedValue;
25
+ }
@@ -0,0 +1,58 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useRef } from 'react';
4
+
5
+ /**
6
+ * Creates a debounced version of a callback function.
7
+ *
8
+ * @param callback The function to debounce.
9
+ * @param delay The debounce delay in milliseconds.
10
+ * @returns A debounced callback function.
11
+ */
12
+ export function useDebouncedCallback<T extends (...args: any[]) => any>(
13
+ callback: T,
14
+ delay: number
15
+ ): (...args: Parameters<T>) => void {
16
+ const callbackRef = useRef(callback);
17
+ const timeoutRef = useRef<NodeJS.Timeout | null>(null);
18
+
19
+ // Update ref when callback changes, but don't trigger effect
20
+ useEffect(() => {
21
+ callbackRef.current = callback;
22
+ }, [callback]);
23
+
24
+ // Cleanup timeout on unmount
25
+ useEffect(() => {
26
+ return () => {
27
+ if (timeoutRef.current) {
28
+ clearTimeout(timeoutRef.current);
29
+ }
30
+ };
31
+ }, []);
32
+
33
+ const debouncedCallback = useCallback(
34
+ (...args: Parameters<T>) => {
35
+ if (timeoutRef.current) {
36
+ clearTimeout(timeoutRef.current);
37
+ }
38
+
39
+ timeoutRef.current = setTimeout(() => {
40
+ callbackRef.current(...args);
41
+ }, delay);
42
+ },
43
+ [delay]
44
+ );
45
+
46
+ // Add a cancel method to the debounced function
47
+ // We attach it directly to the function object
48
+ (debouncedCallback as any).cancel = useCallback(() => {
49
+ if (timeoutRef.current) {
50
+ clearTimeout(timeoutRef.current);
51
+ timeoutRef.current = null;
52
+ }
53
+ }, []);
54
+
55
+ return debouncedCallback;
56
+ }
57
+
58
+ export default useDebouncedCallback;
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+
3
+ import { useDebugValue } from 'react';
4
+
5
+ type DebugValue = Record<string, unknown> | unknown[] | null | undefined;
6
+
7
+ export function useDebugTools(values: DebugValue, prefix = '') {
8
+ if (values === null || values === undefined) {
9
+ useDebugValue(values, () => `${prefix}: ${values === null ? 'null' : 'undefined'}`);
10
+ return;
11
+ }
12
+
13
+ if (Array.isArray(values)) {
14
+ values.forEach((value, index) => {
15
+ useDebugValue(value, (v) => {
16
+ const label = prefix ? `${prefix}[${index}]` : `[${index}]`;
17
+ return `${label}: ${formatValue(v)}`;
18
+ });
19
+ });
20
+ return;
21
+ }
22
+
23
+ if (typeof values === 'object') {
24
+ for (const [key, value] of Object.entries(values)) {
25
+ useDebugValue(value, (v) => {
26
+ const label = prefix ? `${prefix}.${key}` : key;
27
+ return `${label}: ${formatValue(v)}`;
28
+ });
29
+ }
30
+ return;
31
+ }
32
+
33
+ // Handle primitive values
34
+ useDebugValue(values, (v) => `${prefix}: ${formatValue(v)}`);
35
+ }
36
+
37
+ function formatValue(value: unknown): string {
38
+ if (value === null) return 'null';
39
+ if (value === undefined) return 'undefined';
40
+
41
+ try {
42
+ if (typeof value === 'object') {
43
+ if (Array.isArray(value)) {
44
+ return `Array(${value.length})`;
45
+ }
46
+ return JSON.stringify(value);
47
+ }
48
+ return String(value);
49
+ } catch {
50
+ return '[Unserializable]';
51
+ }
52
+ }
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef } from 'react';
4
+
5
+ export type FormEvent<T extends string = string, P = any> = {
6
+ type: T;
7
+ payload?: P;
8
+ timestamp?: number;
9
+ };
10
+
11
+ type EventListener<T extends FormEvent> = (event: T) => void;
12
+
13
+ class EventBus {
14
+ private listeners: Set<EventListener<any>> = new Set();
15
+
16
+ publish<T extends FormEvent>(event: T) {
17
+ this.listeners.forEach(listener => listener({
18
+ ...event,
19
+ timestamp: event.timestamp || Date.now(),
20
+ }));
21
+ }
22
+
23
+ subscribe<T extends FormEvent>(listener: EventListener<T>) {
24
+ this.listeners.add(listener);
25
+ return () => {
26
+ this.listeners.delete(listener);
27
+ };
28
+ }
29
+ }
30
+
31
+ export const events = new EventBus();
32
+
33
+ export function useEventListener<T extends string, P>(
34
+ eventType: T,
35
+ handler: (payload: P) => void
36
+ ) {
37
+ const savedHandler = useRef(handler);
38
+
39
+ useEffect(() => {
40
+ savedHandler.current = handler;
41
+ }, [handler]);
42
+
43
+ useEffect(() => {
44
+ const listener = (event: FormEvent) => {
45
+ if (event.type === eventType) {
46
+ savedHandler.current(event.payload);
47
+ }
48
+ };
49
+
50
+ const unsubscribe = events.subscribe(listener);
51
+ return () => unsubscribe();
52
+ }, [eventType]);
53
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Enhanced Image Loader Hook
3
+ *
4
+ * Hook to check if an image is loaded successfully with callback support
5
+ */
6
+
7
+ 'use client';
8
+
9
+ import { useState, useEffect, useCallback } from 'react';
10
+
11
+ export interface ImageLoaderState {
12
+ isLoading: boolean;
13
+ isLoaded: boolean;
14
+ hasError: boolean;
15
+ }
16
+
17
+ export interface ImageLoaderCallbacks {
18
+ onLoadStart?: () => void;
19
+ onLoad?: (event: React.SyntheticEvent<HTMLImageElement>) => void;
20
+ onError?: (event: React.SyntheticEvent<HTMLImageElement>) => void;
21
+ }
22
+
23
+ export const useImageLoader = (
24
+ src?: string,
25
+ callbacks?: ImageLoaderCallbacks
26
+ ): ImageLoaderState => {
27
+ const [state, setState] = useState<ImageLoaderState>({
28
+ isLoading: false,
29
+ isLoaded: false,
30
+ hasError: false,
31
+ });
32
+
33
+ const handleLoad = useCallback((event: Event) => {
34
+ setState({
35
+ isLoading: false,
36
+ isLoaded: true,
37
+ hasError: false,
38
+ });
39
+
40
+ // Call user callback if provided
41
+ if (callbacks?.onLoad) {
42
+ callbacks.onLoad(event as unknown as React.SyntheticEvent<HTMLImageElement>);
43
+ }
44
+ }, [callbacks?.onLoad]);
45
+
46
+ const handleError = useCallback((event: Event) => {
47
+ setState({
48
+ isLoading: false,
49
+ isLoaded: false,
50
+ hasError: true,
51
+ });
52
+
53
+ // Call user callback if provided
54
+ if (callbacks?.onError) {
55
+ callbacks.onError(event as unknown as React.SyntheticEvent<HTMLImageElement>);
56
+ }
57
+ }, [callbacks?.onError]);
58
+
59
+ useEffect(() => {
60
+ if (!src) {
61
+ setState({
62
+ isLoading: false,
63
+ isLoaded: false,
64
+ hasError: true,
65
+ });
66
+ return;
67
+ }
68
+
69
+ setState({
70
+ isLoading: true,
71
+ isLoaded: false,
72
+ hasError: false,
73
+ });
74
+
75
+ // Call load start callback
76
+ if (callbacks?.onLoadStart) {
77
+ callbacks.onLoadStart();
78
+ }
79
+
80
+ const img = new Image();
81
+
82
+ img.addEventListener('load', handleLoad);
83
+ img.addEventListener('error', handleError);
84
+
85
+ img.src = src;
86
+
87
+ // Cleanup function
88
+ return () => {
89
+ img.removeEventListener('load', handleLoad);
90
+ img.removeEventListener('error', handleError);
91
+ };
92
+ }, [src, handleLoad, handleError, callbacks?.onLoadStart]);
93
+
94
+ return state;
95
+ };
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+
5
+ /**
6
+ * Hook to check if a media query matches
7
+ *
8
+ * @param query - CSS media query string
9
+ * @returns boolean indicating if the query matches
10
+ *
11
+ * @example
12
+ * const isMobile = useMediaQuery('(max-width: 768px)');
13
+ * const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
14
+ * const isLandscape = useMediaQuery('(orientation: landscape)');
15
+ */
16
+ export function useMediaQuery(query: string): boolean {
17
+ const [matches, setMatches] = useState<boolean>(false);
18
+
19
+ useEffect(() => {
20
+ const mediaQuery = window.matchMedia(query);
21
+
22
+ // Set initial value
23
+ setMatches(mediaQuery.matches);
24
+
25
+ // Create event listener
26
+ const handler = (event: MediaQueryListEvent) => {
27
+ setMatches(event.matches);
28
+ };
29
+
30
+ // Add listener
31
+ mediaQuery.addEventListener('change', handler);
32
+
33
+ // Cleanup
34
+ return () => {
35
+ mediaQuery.removeEventListener('change', handler);
36
+ };
37
+ }, [query]);
38
+
39
+ return matches;
40
+ }
@@ -0,0 +1,22 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+
5
+ // Увеличил breakpoint до 1024px чтобы включить планшеты
6
+ const MOBILE_BREAKPOINT = 1024
7
+
8
+ export function useIsMobile() {
9
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
10
+
11
+ React.useEffect(() => {
12
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
13
+ const onChange = () => {
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ }
16
+ mql.addEventListener("change", onChange)
17
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
18
+ return () => mql.removeEventListener("change", onChange)
19
+ }, [])
20
+
21
+ return !!isMobile
22
+ }