@umituz/web-design-system 5.3.0 → 6.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/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/__tests__/cn.test.ts +0 -3
- package/src/__tests__/component-slots.test.tsx +0 -4
- package/src/__tests__/components.smoke.test.tsx +3 -11
- package/src/__tests__/money.test.tsx +0 -3
- package/src/__tests__/productivity-components.test.tsx +135 -0
- package/src/__tests__/styles.contract.test.ts +0 -13
- package/src/domain/tokens/index.ts +0 -7
- package/src/domain/tokens/responsive-map.tokens.ts +18 -107
- package/src/domain/types/breakpoint.types.ts +9 -13
- package/src/domain/types/component.types.ts +0 -4
- package/src/domain/types/index.ts +0 -5
- package/src/global.d.ts +0 -4
- package/src/index.ts +82 -29
- package/src/infrastructure/constants/breakpoint.constants.ts +0 -31
- package/src/infrastructure/constants/component.constants.ts +0 -4
- package/src/infrastructure/constants/index.ts +0 -5
- package/src/infrastructure/constants/size-variant.constants.ts +0 -33
- package/src/infrastructure/constants/spacing.constants.ts +34 -63
- package/src/infrastructure/constants/timing.constants.ts +0 -18
- package/src/infrastructure/constants/typography.constants.ts +22 -54
- package/src/infrastructure/utils/calculation.utils.ts +0 -9
- package/src/infrastructure/utils/cn.util.ts +0 -18
- package/src/infrastructure/utils/hash.util.ts +0 -5
- package/src/infrastructure/utils/index.ts +0 -5
- package/src/infrastructure/utils/money.utils.ts +0 -7
- package/src/infrastructure/utils/responsive.utils.ts +0 -66
- package/src/presentation/atoms/AspectRatio.tsx +0 -10
- package/src/presentation/atoms/Badge.tsx +0 -5
- package/src/presentation/atoms/BatteryPill.tsx +0 -5
- package/src/presentation/atoms/Button.tsx +0 -2
- package/src/presentation/atoms/Checkbox.tsx +0 -4
- package/src/presentation/atoms/CopyButton.tsx +93 -0
- package/src/presentation/atoms/Divider.tsx +0 -4
- package/src/presentation/atoms/HeatmapStrip.tsx +0 -10
- package/src/presentation/atoms/Hide.tsx +0 -24
- package/src/presentation/atoms/Icon.tsx +0 -4
- package/src/presentation/atoms/IconButton.tsx +0 -4
- package/src/presentation/atoms/Input.tsx +0 -4
- package/src/presentation/atoms/Kbd.tsx +0 -4
- package/src/presentation/atoms/Label.tsx +0 -4
- package/src/presentation/atoms/Link.tsx +0 -4
- package/src/presentation/atoms/Money.tsx +0 -6
- package/src/presentation/atoms/Progress.tsx +0 -6
- package/src/presentation/atoms/Radio.tsx +0 -4
- package/src/presentation/atoms/Separator.tsx +0 -4
- package/src/presentation/atoms/Show.tsx +0 -24
- package/src/presentation/atoms/Skeleton.tsx +0 -4
- package/src/presentation/atoms/Slider.tsx +0 -4
- package/src/presentation/atoms/Sparkline.tsx +0 -4
- package/src/presentation/atoms/Spinner.tsx +0 -4
- package/src/presentation/atoms/StatusDot.tsx +79 -0
- package/src/presentation/atoms/Switch.tsx +0 -4
- package/src/presentation/atoms/Text.tsx +0 -4
- package/src/presentation/atoms/Textarea.tsx +0 -4
- package/src/presentation/atoms/Toggle.tsx +0 -4
- package/src/presentation/atoms/Tooltip.tsx +0 -4
- package/src/presentation/atoms/index.ts +9 -9
- package/src/presentation/hooks/index.ts +0 -5
- package/src/presentation/hooks/useClickOutside.ts +0 -6
- package/src/presentation/hooks/useClipboard.ts +0 -4
- package/src/presentation/hooks/useDebounce.ts +0 -8
- package/src/presentation/hooks/useKeyboard.ts +0 -6
- package/src/presentation/hooks/useLocalStorage.ts +0 -4
- package/src/presentation/hooks/useMediaQuery.ts +0 -37
- package/src/presentation/hooks/useScrollLock.ts +0 -5
- package/src/presentation/hooks/useTheme.ts +9 -13
- package/src/presentation/hooks/useToggle.ts +0 -4
- package/src/presentation/molecules/ActiveFilterTags.tsx +0 -4
- package/src/presentation/molecules/Avatar.tsx +0 -5
- package/src/presentation/molecules/BookmarkButton.tsx +0 -15
- package/src/presentation/molecules/CheckboxGroup.tsx +0 -4
- package/src/presentation/molecules/Chip.tsx +0 -5
- package/src/presentation/molecules/CodeBlock.tsx +0 -5
- package/src/presentation/molecules/ConfirmButton.tsx +125 -0
- package/src/presentation/molecules/Field.tsx +0 -7
- package/src/presentation/molecules/FormField.tsx +1 -5
- package/src/presentation/molecules/InputGroup.tsx +0 -4
- package/src/presentation/molecules/ListItem.tsx +0 -4
- package/src/presentation/molecules/OptionTile.tsx +0 -10
- package/src/presentation/molecules/PasswordInput.tsx +43 -0
- package/src/presentation/molecules/RadioGroup.tsx +0 -4
- package/src/presentation/molecules/RatingStars.tsx +0 -8
- package/src/presentation/molecules/ScrollArea.tsx +0 -4
- package/src/presentation/molecules/SearchBox.tsx +0 -4
- package/src/presentation/molecules/SearchInput.tsx +110 -0
- package/src/presentation/molecules/Select.tsx +0 -4
- package/src/presentation/molecules/StatPill.tsx +0 -5
- package/src/presentation/molecules/TagInput.tsx +0 -7
- package/src/presentation/molecules/Textarea.tsx +0 -4
- package/src/presentation/molecules/Toggle.tsx +0 -4
- package/src/presentation/molecules/Toolbar.tsx +36 -0
- package/src/presentation/molecules/index.ts +25 -8
- package/src/presentation/organisms/Accordion.tsx +0 -4
- package/src/presentation/organisms/Alert.tsx +0 -4
- package/src/presentation/organisms/AvatarStack.tsx +0 -5
- package/src/presentation/organisms/Breadcrumb.tsx +0 -7
- package/src/presentation/organisms/Card.tsx +0 -6
- package/src/presentation/organisms/Checklist.tsx +0 -4
- package/src/presentation/organisms/Collapsible.tsx +0 -4
- package/src/presentation/organisms/ConfirmDialog.tsx +0 -8
- package/src/presentation/organisms/CoverArt.tsx +0 -9
- package/src/presentation/organisms/DataTable.tsx +0 -8
- package/src/presentation/organisms/DateRangePicker.tsx +0 -9
- package/src/presentation/organisms/DescriptionList.tsx +113 -0
- package/src/presentation/organisms/Dialog.tsx +0 -4
- package/src/presentation/organisms/Drawer.tsx +0 -6
- package/src/presentation/organisms/DropdownMenu.tsx +0 -4
- package/src/presentation/organisms/ErrorBoundary.tsx +0 -12
- package/src/presentation/organisms/FileUploader.tsx +0 -9
- package/src/presentation/organisms/FilterPopover.tsx +0 -8
- package/src/presentation/organisms/FormModal.tsx +0 -5
- package/src/presentation/organisms/HoverCard.tsx +0 -4
- package/src/presentation/organisms/KeyValueList.tsx +0 -4
- package/src/presentation/organisms/KpiCard.tsx +0 -6
- package/src/presentation/organisms/Lightbox.tsx +0 -7
- package/src/presentation/organisms/LoadingState.tsx +0 -4
- package/src/presentation/organisms/MapPreview.tsx +0 -9
- package/src/presentation/organisms/MetricCard.tsx +1 -5
- package/src/presentation/organisms/Modal.tsx +0 -4
- package/src/presentation/organisms/PhotoGallery.tsx +0 -9
- package/src/presentation/organisms/PlanPicker.tsx +0 -5
- package/src/presentation/organisms/Popover.tsx +0 -4
- package/src/presentation/organisms/Sheet.tsx +0 -4
- package/src/presentation/organisms/StatCard.tsx +0 -5
- package/src/presentation/organisms/StatsGrid.tsx +110 -0
- package/src/presentation/organisms/StatusBadge.tsx +0 -12
- package/src/presentation/organisms/Stepper.tsx +0 -5
- package/src/presentation/organisms/Table.tsx +0 -4
- package/src/presentation/organisms/Tabs.tsx +0 -7
- package/src/presentation/organisms/Timeline.tsx +0 -8
- package/src/presentation/organisms/Toaster.tsx +0 -9
- package/src/presentation/organisms/ToggleGroup.tsx +0 -4
- package/src/presentation/organisms/index.ts +10 -11
- package/src/presentation/templates/CrudPageTemplate.tsx +69 -0
- package/src/presentation/templates/Form.tsx +0 -4
- package/src/presentation/templates/List.tsx +0 -4
- package/src/presentation/templates/PageHeader.tsx +0 -4
- package/src/presentation/templates/PageLayout.tsx +0 -4
- package/src/presentation/templates/ProjectSkeleton.tsx +0 -4
- package/src/presentation/templates/ResponsiveContainer.tsx +0 -43
- package/src/presentation/templates/Section.tsx +0 -4
- package/src/presentation/templates/SettingsLayout.tsx +102 -0
- package/src/presentation/templates/index.ts +7 -6
- package/src/styles/app.css +0 -18
- package/src/styles/bridge.css +0 -34
- package/src/styles/compile.css +0 -13
- package/src/styles/source.css +0 -12
- package/src/styles/theme.css +0 -27
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useMediaQuery & useBreakpoint Hooks
|
|
3
|
-
* @description Enhanced responsive breakpoint detection with helper functions
|
|
4
|
-
*/
|
|
5
1
|
|
|
6
2
|
import { useEffect, useState, useCallback, useRef, useMemo, useSyncExternalStore } from 'react';
|
|
7
3
|
import type {
|
|
@@ -16,16 +12,6 @@ import {
|
|
|
16
12
|
} from '../../infrastructure/constants/breakpoint.constants';
|
|
17
13
|
import { TIMING } from '../../infrastructure/constants/timing.constants';
|
|
18
14
|
|
|
19
|
-
/**
|
|
20
|
-
* Simple media query hook for a single breakpoint
|
|
21
|
-
* @param breakpoint - The breakpoint to check
|
|
22
|
-
* @returns Whether the media query matches
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```tsx
|
|
26
|
-
* const isDesktop = useMediaQuery('lg') // true on lg screens and above
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
15
|
export function useMediaQuery(breakpoint: Breakpoint): boolean {
|
|
30
16
|
const query = useMemo(() => createMediaQuery(breakpoint), [breakpoint]);
|
|
31
17
|
|
|
@@ -49,24 +35,7 @@ export function useMediaQuery(breakpoint: Breakpoint): boolean {
|
|
|
49
35
|
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
50
36
|
}
|
|
51
37
|
|
|
52
|
-
/**
|
|
53
|
-
* Enhanced breakpoint hook with helper functions
|
|
54
|
-
* @returns Object containing current breakpoint and helper functions
|
|
55
|
-
*
|
|
56
|
-
* @example
|
|
57
|
-
* ```tsx
|
|
58
|
-
* const { currentBreakpoint, isMobile, isDesktop } = useBreakpoint()
|
|
59
|
-
*
|
|
60
|
-
* return (
|
|
61
|
-
* <div>
|
|
62
|
-
* {isMobile && <MobileNav />}
|
|
63
|
-
* {isDesktop && <DesktopNav />}
|
|
64
|
-
* </div>
|
|
65
|
-
* )
|
|
66
|
-
* ```
|
|
67
|
-
*/
|
|
68
38
|
export function useBreakpoint(): UseBreakpointReturn {
|
|
69
|
-
// FIX: Sort breakpoints once and memoize
|
|
70
39
|
const sortedBreakpoints = useMemo(() => {
|
|
71
40
|
return Object.entries(BREAKPOINTS).sort(
|
|
72
41
|
([, a], [, b]) => b.min - a.min
|
|
@@ -74,7 +43,6 @@ export function useBreakpoint(): UseBreakpointReturn {
|
|
|
74
43
|
}, []);
|
|
75
44
|
|
|
76
45
|
const [currentBreakpoint, setCurrentBreakpoint] = useState<Breakpoint>(() => {
|
|
77
|
-
// FIX: SSR-safe initialization
|
|
78
46
|
if (typeof window === 'undefined') return 'lg';
|
|
79
47
|
|
|
80
48
|
const width = window.innerWidth;
|
|
@@ -84,7 +52,6 @@ export function useBreakpoint(): UseBreakpointReturn {
|
|
|
84
52
|
return 'xs';
|
|
85
53
|
});
|
|
86
54
|
|
|
87
|
-
// FIX: Use useRef for resize timer to prevent timer issues
|
|
88
55
|
const resizeTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
89
56
|
|
|
90
57
|
useEffect(() => {
|
|
@@ -99,10 +66,8 @@ export function useBreakpoint(): UseBreakpointReturn {
|
|
|
99
66
|
}
|
|
100
67
|
};
|
|
101
68
|
|
|
102
|
-
// Initial check
|
|
103
69
|
updateBreakpoint();
|
|
104
70
|
|
|
105
|
-
// Debounced resize listener
|
|
106
71
|
const handleResize = () => {
|
|
107
72
|
if (resizeTimerRef.current) {
|
|
108
73
|
clearTimeout(resizeTimerRef.current);
|
|
@@ -120,7 +85,6 @@ export function useBreakpoint(): UseBreakpointReturn {
|
|
|
120
85
|
};
|
|
121
86
|
}, [sortedBreakpoints]);
|
|
122
87
|
|
|
123
|
-
// Helper functions
|
|
124
88
|
const matches = useCallback(
|
|
125
89
|
(breakpoint: Breakpoint | Breakpoint[]): boolean => {
|
|
126
90
|
if (Array.isArray(breakpoint)) {
|
|
@@ -145,7 +109,6 @@ export function useBreakpoint(): UseBreakpointReturn {
|
|
|
145
109
|
[currentBreakpoint]
|
|
146
110
|
);
|
|
147
111
|
|
|
148
|
-
// Computed values
|
|
149
112
|
const isXs = currentBreakpoint === 'xs';
|
|
150
113
|
const isSm = currentBreakpoint === 'sm';
|
|
151
114
|
const isMd = currentBreakpoint === 'md';
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useScrollLock Hook
|
|
3
|
-
* @description Lock/unlock body scroll while preserving scrollbar gutter
|
|
4
|
-
*/
|
|
5
1
|
|
|
6
2
|
import { useEffect } from 'react';
|
|
7
3
|
|
|
8
|
-
/** Width of the vertical scrollbar, so locking scroll doesn't shift layout. */
|
|
9
4
|
const measureScrollbarWidth = (): number => {
|
|
10
5
|
if (typeof window === 'undefined' || typeof document === 'undefined') return 0;
|
|
11
6
|
return Math.max(0, window.innerWidth - document.documentElement.clientWidth);
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useTheme Hook
|
|
3
|
-
* @description Theme toggle functionality with SSR support and system preference detection
|
|
4
|
-
*/
|
|
5
1
|
|
|
6
2
|
import { useEffect, useState, useCallback } from 'react';
|
|
7
3
|
|
|
@@ -25,9 +21,16 @@ const getSystemTheme = (): 'light' | 'dark' => {
|
|
|
25
21
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
26
22
|
};
|
|
27
23
|
|
|
24
|
+
const writeThemeStorage = (theme: Theme): void => {
|
|
25
|
+
try {
|
|
26
|
+
localStorage.setItem(THEME_STORAGE_KEY, theme);
|
|
27
|
+
} catch {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
28
32
|
export function useTheme(): UseThemeReturn {
|
|
29
33
|
const [theme, setThemeState] = useState<Theme>(() => {
|
|
30
|
-
// FIX: SSR-safe initialization
|
|
31
34
|
if (typeof window === 'undefined') return 'system';
|
|
32
35
|
|
|
33
36
|
try {
|
|
@@ -47,7 +50,6 @@ export function useTheme(): UseThemeReturn {
|
|
|
47
50
|
return theme;
|
|
48
51
|
});
|
|
49
52
|
|
|
50
|
-
// Listen for system theme changes
|
|
51
53
|
useEffect(() => {
|
|
52
54
|
if (theme !== 'system') return;
|
|
53
55
|
|
|
@@ -66,15 +68,9 @@ export function useTheme(): UseThemeReturn {
|
|
|
66
68
|
|
|
67
69
|
root.classList.remove('light', 'dark');
|
|
68
70
|
root.classList.add(themeToApply);
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
localStorage.setItem(THEME_STORAGE_KEY, theme);
|
|
72
|
-
} catch {
|
|
73
|
-
// Ignore storage errors
|
|
74
|
-
}
|
|
71
|
+
writeThemeStorage(theme);
|
|
75
72
|
}, [theme]);
|
|
76
73
|
|
|
77
|
-
// FIX: Memoize callbacks to prevent unnecessary re-renders
|
|
78
74
|
const toggleTheme = useCallback(() => {
|
|
79
75
|
setThemeState((prev) => {
|
|
80
76
|
if (prev === 'light') return 'dark';
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Avatar Component (Molecule)
|
|
3
|
-
* @description User avatar with image and fallback (shadcn/ui compatible)
|
|
4
|
-
*/
|
|
5
1
|
|
|
6
2
|
import { forwardRef, type HTMLAttributes, type ComponentPropsWithoutRef } from 'react';
|
|
7
3
|
import { cn } from '../../infrastructure/utils';
|
|
@@ -11,7 +7,6 @@ import type { ExtendedSizes } from '../../infrastructure/constants';
|
|
|
11
7
|
export interface AvatarProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
|
|
12
8
|
name?: string;
|
|
13
9
|
size?: ExtendedSizes;
|
|
14
|
-
/** Accent hue (0-360) for the initials fallback, overriding the name-hash tone. */
|
|
15
10
|
hue?: number;
|
|
16
11
|
}
|
|
17
12
|
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BookmarkButton Component (Molecule)
|
|
3
|
-
* @description Accessible toggleable button for saving, bookmarking, or favoriting items.
|
|
4
|
-
* Supports pill (icon + label) and icon-only variants, multiple sizes, semantic tones, and custom icons.
|
|
5
|
-
*/
|
|
6
1
|
|
|
7
2
|
import React, { forwardRef } from 'react';
|
|
8
3
|
import { Star } from 'lucide-react';
|
|
@@ -15,25 +10,15 @@ export type BookmarkTone = 'warning' | 'primary' | 'success' | 'default';
|
|
|
15
10
|
export interface BookmarkButtonProps
|
|
16
11
|
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'onChange' | 'onToggle'>,
|
|
17
12
|
BaseProps {
|
|
18
|
-
/** Whether the item is currently saved / bookmarked / active. */
|
|
19
13
|
active?: boolean;
|
|
20
|
-
/** Callback fired when clicked or toggled. Passes the inverted active state. */
|
|
21
14
|
onToggle?: (nextActive: boolean) => void;
|
|
22
|
-
/** Display variant: 'pill' shows icon + label, 'icon' renders a square/round icon-only button. */
|
|
23
15
|
variant?: 'pill' | 'icon';
|
|
24
|
-
/** Size variant */
|
|
25
16
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
26
|
-
/** Inactive label (default: "Save"). Used as text in pill mode and aria-label in icon mode. */
|
|
27
17
|
label?: string;
|
|
28
|
-
/** Active label (default: "Saved"). */
|
|
29
18
|
activeLabel?: string;
|
|
30
|
-
/** Custom icon when inactive. Defaults to a Star outline. */
|
|
31
19
|
icon?: React.ReactNode;
|
|
32
|
-
/** Custom icon when active. Defaults to a filled Star. */
|
|
33
20
|
activeIcon?: React.ReactNode;
|
|
34
|
-
/** Color theme tone when active (default: 'warning' for warm bookmark gold). */
|
|
35
21
|
tone?: BookmarkTone;
|
|
36
|
-
/** Loading indicator */
|
|
37
22
|
loading?: boolean;
|
|
38
23
|
}
|
|
39
24
|
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Chip Component (Molecule)
|
|
3
|
-
* @description Selectable/removable tag
|
|
4
|
-
*/
|
|
5
1
|
|
|
6
2
|
import { forwardRef } from 'react';
|
|
7
3
|
import { cn } from '../../infrastructure/utils';
|
|
@@ -15,7 +11,6 @@ export interface ChipProps extends BaseProps {
|
|
|
15
11
|
onRemove?: () => void;
|
|
16
12
|
onClick?: () => void;
|
|
17
13
|
removable?: boolean;
|
|
18
|
-
/** Toggle state for filter chips — renders as a pressed button when set. */
|
|
19
14
|
selected?: boolean;
|
|
20
15
|
}
|
|
21
16
|
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CodeBlock Component (Molecule)
|
|
3
|
-
* @description Syntax-highlighted code block with copy button and expand/collapse
|
|
4
|
-
*/
|
|
5
1
|
|
|
6
2
|
import { useState, useEffect } from 'react';
|
|
7
3
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
@@ -15,7 +11,6 @@ export interface CodeBlockProps extends BaseProps {
|
|
|
15
11
|
language?: string;
|
|
16
12
|
filename?: string;
|
|
17
13
|
showLineNumbers?: boolean;
|
|
18
|
-
/** Lines threshold to enable collapse/expand. */
|
|
19
14
|
collapseThreshold?: number;
|
|
20
15
|
}
|
|
21
16
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Button, type ButtonProps } from '../atoms/Button';
|
|
3
|
+
import { Check, X } from 'lucide-react';
|
|
4
|
+
import { cn } from '../../infrastructure/utils';
|
|
5
|
+
|
|
6
|
+
export interface ConfirmButtonProps extends Omit<ButtonProps, 'onConfirm'> {
|
|
7
|
+
onConfirm: () => void | Promise<void>;
|
|
8
|
+
confirmText?: string;
|
|
9
|
+
confirmLabel?: string;
|
|
10
|
+
cancelLabel?: string;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ConfirmButton = React.forwardRef<HTMLButtonElement, ConfirmButtonProps>(
|
|
15
|
+
(
|
|
16
|
+
{
|
|
17
|
+
onConfirm,
|
|
18
|
+
confirmText = 'Emin misiniz?',
|
|
19
|
+
confirmLabel = 'Evet',
|
|
20
|
+
cancelLabel = 'İptal',
|
|
21
|
+
timeoutMs = 4000,
|
|
22
|
+
children,
|
|
23
|
+
variant = 'destructive',
|
|
24
|
+
size = 'default',
|
|
25
|
+
className,
|
|
26
|
+
disabled,
|
|
27
|
+
...props
|
|
28
|
+
},
|
|
29
|
+
ref
|
|
30
|
+
) => {
|
|
31
|
+
const [isConfirming, setIsConfirming] = React.useState(false);
|
|
32
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
33
|
+
const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
34
|
+
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
return () => {
|
|
37
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
38
|
+
};
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const resetConfirmation = () => {
|
|
42
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
43
|
+
setIsConfirming(false);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleInitialClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
e.stopPropagation();
|
|
49
|
+
setIsConfirming(true);
|
|
50
|
+
|
|
51
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
52
|
+
timeoutRef.current = setTimeout(() => {
|
|
53
|
+
setIsConfirming(false);
|
|
54
|
+
}, timeoutMs);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleConfirm = async (e: React.MouseEvent) => {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
e.stopPropagation();
|
|
60
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
setIsLoading(true);
|
|
64
|
+
await onConfirm();
|
|
65
|
+
} finally {
|
|
66
|
+
setIsLoading(false);
|
|
67
|
+
setIsConfirming(false);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleCancel = (e: React.MouseEvent) => {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
e.stopPropagation();
|
|
74
|
+
resetConfirmation();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (isConfirming) {
|
|
78
|
+
return (
|
|
79
|
+
<div className={cn('inline-flex items-center gap-1.5 animate-in fade-in zoom-in-95 duration-150', className)}>
|
|
80
|
+
<span className="text-xs font-semibold text-destructive px-1 select-none">
|
|
81
|
+
{confirmText}
|
|
82
|
+
</span>
|
|
83
|
+
<Button
|
|
84
|
+
type="button"
|
|
85
|
+
size="xs"
|
|
86
|
+
variant="destructive"
|
|
87
|
+
loading={isLoading}
|
|
88
|
+
onClick={handleConfirm}
|
|
89
|
+
className="gap-1 h-7 px-2"
|
|
90
|
+
>
|
|
91
|
+
<Check className="size-3" />
|
|
92
|
+
<span>{confirmLabel}</span>
|
|
93
|
+
</Button>
|
|
94
|
+
<Button
|
|
95
|
+
type="button"
|
|
96
|
+
size="xs"
|
|
97
|
+
variant="ghost"
|
|
98
|
+
disabled={isLoading}
|
|
99
|
+
onClick={handleCancel}
|
|
100
|
+
className="h-7 px-1.5 text-muted-foreground hover:text-foreground"
|
|
101
|
+
>
|
|
102
|
+
<X className="size-3" />
|
|
103
|
+
<span className="sr-only">{cancelLabel}</span>
|
|
104
|
+
</Button>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Button
|
|
111
|
+
ref={ref}
|
|
112
|
+
variant={variant}
|
|
113
|
+
size={size}
|
|
114
|
+
disabled={disabled || isLoading}
|
|
115
|
+
onClick={handleInitialClick}
|
|
116
|
+
className={className}
|
|
117
|
+
{...props}
|
|
118
|
+
>
|
|
119
|
+
{children}
|
|
120
|
+
</Button>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
ConfirmButton.displayName = 'ConfirmButton';
|
|
@@ -13,18 +13,11 @@ export interface FieldProps {
|
|
|
13
13
|
error?: ReactNode;
|
|
14
14
|
required?: boolean;
|
|
15
15
|
htmlFor?: string;
|
|
16
|
-
/** Keep the label in the accessibility tree while hiding it visually. */
|
|
17
16
|
hideLabel?: boolean;
|
|
18
17
|
className?: string;
|
|
19
18
|
children: ReactNode;
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
/**
|
|
23
|
-
* Label + hint/error wrapper for a single form control. When the child is a
|
|
24
|
-
* valid element, the control id (existing id wins, then htmlFor, then a
|
|
25
|
-
* generated one) and the aria wiring for hint/error are injected into it so
|
|
26
|
-
* the label association is automatic.
|
|
27
|
-
*/
|
|
28
21
|
export function Field({ label, hint, error, required, htmlFor, hideLabel = false, className, children }: FieldProps) {
|
|
29
22
|
const autoId = useId();
|
|
30
23
|
const childId = isValidElement(children) ? (children as ReactElement<FieldControlProps>).props.id : undefined;
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FormField Component (Molecule)
|
|
3
|
-
* @description Label + Input combination (Responsive)
|
|
4
|
-
*/
|
|
5
1
|
|
|
6
2
|
import { forwardRef, useId } from 'react';
|
|
7
3
|
import { cn, getSpaceY } from '../../infrastructure/utils';
|
|
@@ -16,7 +12,7 @@ export interface FormFieldProps extends BaseProps {
|
|
|
16
12
|
required?: boolean;
|
|
17
13
|
id?: string;
|
|
18
14
|
inputProps?: React.ComponentProps<typeof Input>;
|
|
19
|
-
fullWidth?: boolean;
|
|
15
|
+
fullWidth?: boolean;
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
export const FormField = forwardRef<HTMLInputElement, FormFieldProps>(
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OptionTile Component (Molecule)
|
|
3
|
-
* @description Graded answer tile for exercises — a native input (radio or
|
|
4
|
-
* checkbox) wrapped in its label, with an optional A/B/C/D key badge and
|
|
5
|
-
* reveal states (correct / wrong / missed) for post-submission feedback.
|
|
6
|
-
*/
|
|
7
1
|
|
|
8
2
|
import { forwardRef } from 'react';
|
|
9
3
|
import { Check, X } from 'lucide-react';
|
|
@@ -14,15 +8,11 @@ export type OptionTileState = 'correct' | 'wrong' | 'missed';
|
|
|
14
8
|
|
|
15
9
|
export interface OptionTileProps extends BaseProps {
|
|
16
10
|
label: React.ReactNode;
|
|
17
|
-
/** Key badge rendered before the label (A, B, C…). */
|
|
18
11
|
keyBadge?: string;
|
|
19
|
-
/** Native input type — radio (single) or checkbox (multi-select). */
|
|
20
12
|
type?: 'radio' | 'checkbox';
|
|
21
13
|
checked?: boolean;
|
|
22
|
-
/** Reveal semantics — overrides selection styling after grading. */
|
|
23
14
|
state?: OptionTileState;
|
|
24
15
|
disabled?: boolean;
|
|
25
|
-
/** Group name for radio inputs. */
|
|
26
16
|
name?: string;
|
|
27
17
|
onChange: (checked: boolean) => void;
|
|
28
18
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Eye, EyeOff } from 'lucide-react';
|
|
3
|
+
import { Input, type InputProps } from '../atoms/Input';
|
|
4
|
+
import { cn } from '../../infrastructure/utils';
|
|
5
|
+
|
|
6
|
+
export interface PasswordInputProps extends Omit<InputProps, 'type'> {
|
|
7
|
+
showToggle?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
|
11
|
+
({ showToggle = true, className, ...props }, ref) => {
|
|
12
|
+
const [visible, setVisible] = React.useState(false);
|
|
13
|
+
|
|
14
|
+
const toggleVisibility = (e: React.MouseEvent) => {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
e.stopPropagation();
|
|
17
|
+
setVisible((prev) => !prev);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className={cn('relative flex items-center w-full', className)}>
|
|
22
|
+
<Input
|
|
23
|
+
ref={ref}
|
|
24
|
+
type={visible ? 'text' : 'password'}
|
|
25
|
+
className="pr-10"
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
{showToggle && (
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
onClick={toggleVisibility}
|
|
32
|
+
aria-label={visible ? 'Şifreyi gizle' : 'Şifreyi göster'}
|
|
33
|
+
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground focus:outline-none transition-colors p-1"
|
|
34
|
+
>
|
|
35
|
+
{visible ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
|
36
|
+
</button>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
PasswordInput.displayName = 'PasswordInput';
|
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RatingStars Component (Molecule)
|
|
3
|
-
* @description 0–5 rating with visual half-star precision, numeric value and
|
|
4
|
-
* optional review count
|
|
5
|
-
*/
|
|
6
1
|
|
|
7
2
|
import { cn } from '../../infrastructure/utils';
|
|
8
3
|
import type { BaseProps } from '../../domain/types';
|
|
9
4
|
|
|
10
5
|
export interface RatingStarsProps extends BaseProps {
|
|
11
|
-
/** 0–5 rating; halves render as partial star fills. */
|
|
12
6
|
value: number;
|
|
13
|
-
/** Optional review count rendered in parentheses. */
|
|
14
7
|
count?: number;
|
|
15
|
-
/** Locale-formatted count override (e.g. 1,204). */
|
|
16
8
|
formatCount?: (count: number) => string;
|
|
17
9
|
size?: 'sm' | 'md';
|
|
18
10
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Search, X } from 'lucide-react';
|
|
3
|
+
import { Input, type InputProps } from '../atoms/Input';
|
|
4
|
+
import { Spinner } from '../atoms/Spinner';
|
|
5
|
+
import { cn } from '../../infrastructure/utils';
|
|
6
|
+
|
|
7
|
+
export interface SearchInputProps extends Omit<InputProps, 'onChange' | 'value' | 'defaultValue'> {
|
|
8
|
+
value?: string;
|
|
9
|
+
defaultValue?: string;
|
|
10
|
+
onChange?: (value: string) => void;
|
|
11
|
+
onSearch?: (value: string) => void;
|
|
12
|
+
onClear?: () => void;
|
|
13
|
+
debounceMs?: number;
|
|
14
|
+
loading?: boolean;
|
|
15
|
+
clearable?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(
|
|
19
|
+
(
|
|
20
|
+
{
|
|
21
|
+
value: controlledValue,
|
|
22
|
+
defaultValue = '',
|
|
23
|
+
onChange,
|
|
24
|
+
onSearch,
|
|
25
|
+
onClear,
|
|
26
|
+
debounceMs = 0,
|
|
27
|
+
loading = false,
|
|
28
|
+
clearable = true,
|
|
29
|
+
placeholder = 'Ara...',
|
|
30
|
+
className,
|
|
31
|
+
...props
|
|
32
|
+
},
|
|
33
|
+
ref
|
|
34
|
+
) => {
|
|
35
|
+
const isControlled = controlledValue !== undefined;
|
|
36
|
+
const [internalValue, setInternalValue] = React.useState(defaultValue);
|
|
37
|
+
const currentValue = isControlled ? controlledValue : internalValue;
|
|
38
|
+
|
|
39
|
+
const debounceTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
40
|
+
|
|
41
|
+
React.useEffect(() => {
|
|
42
|
+
return () => {
|
|
43
|
+
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
|
44
|
+
};
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
48
|
+
const nextVal = e.target.value;
|
|
49
|
+
if (!isControlled) {
|
|
50
|
+
setInternalValue(nextVal);
|
|
51
|
+
}
|
|
52
|
+
onChange?.(nextVal);
|
|
53
|
+
|
|
54
|
+
if (debounceMs > 0 && onSearch) {
|
|
55
|
+
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
|
56
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
57
|
+
onSearch(nextVal);
|
|
58
|
+
}, debounceMs);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
63
|
+
if (e.key === 'Enter') {
|
|
64
|
+
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
|
65
|
+
onSearch?.(currentValue);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const handleClear = () => {
|
|
70
|
+
if (!isControlled) {
|
|
71
|
+
setInternalValue('');
|
|
72
|
+
}
|
|
73
|
+
onChange?.('');
|
|
74
|
+
onClear?.();
|
|
75
|
+
onSearch?.('');
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className={cn('relative flex items-center w-full', className)}>
|
|
80
|
+
<Search className="absolute left-3 size-4 text-muted-foreground pointer-events-none shrink-0" />
|
|
81
|
+
<Input
|
|
82
|
+
ref={ref}
|
|
83
|
+
type="text"
|
|
84
|
+
value={currentValue}
|
|
85
|
+
onChange={handleChange}
|
|
86
|
+
onKeyDown={handleKeyDown}
|
|
87
|
+
placeholder={placeholder}
|
|
88
|
+
className="pl-9 pr-9"
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
<div className="absolute right-2.5 flex items-center">
|
|
92
|
+
{loading ? (
|
|
93
|
+
<Spinner size="sm" />
|
|
94
|
+
) : clearable && currentValue ? (
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
aria-label="Aramayı temizle"
|
|
98
|
+
onClick={handleClear}
|
|
99
|
+
className="text-muted-foreground hover:text-foreground transition-colors p-1"
|
|
100
|
+
>
|
|
101
|
+
<X className="size-3.5" />
|
|
102
|
+
</button>
|
|
103
|
+
) : null}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
SearchInput.displayName = 'SearchInput';
|