@umituz/web-design-system 4.1.0 → 4.3.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.
@@ -3,7 +3,7 @@
3
3
  * @description Tabbed interface
4
4
  */
5
5
 
6
- import { useState, useCallback, type ReactNode, type HTMLAttributes } from 'react';
6
+ import { useRef, useState, useCallback, type KeyboardEvent, type ReactNode, type HTMLAttributes } from 'react';
7
7
  import { cn } from '../../infrastructure/utils';
8
8
  import type { BaseProps } from '../../domain/types';
9
9
 
@@ -12,12 +12,17 @@ export interface Tab {
12
12
  label: string;
13
13
  content?: ReactNode;
14
14
  disabled?: boolean;
15
+ count?: number;
15
16
  }
16
17
 
17
18
  export interface TabsProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
18
19
  tabs: Tab[];
19
20
  defaultValue?: string;
21
+ value?: string;
22
+ onValueChange?: (value: string) => void;
20
23
  variant?: 'default' | 'pills' | 'underline';
24
+ ariaLabel?: string;
25
+ panelId?: string;
21
26
  }
22
27
 
23
28
  const variantStyles: Record<'default' | 'pills' | 'underline', string> = {
@@ -41,15 +46,48 @@ const tabVariantStyles: Record<'default' | 'pills' | 'underline', { base: string
41
46
  },
42
47
  };
43
48
 
44
- export function Tabs({ tabs, defaultValue, variant = 'default', className, ...props }: TabsProps) {
45
- const [activeTab, setActiveTab] = useState(defaultValue || tabs[0]?.value);
49
+ export function Tabs({ tabs, defaultValue, value, onValueChange, variant = 'default', ariaLabel, panelId, className, ...props }: TabsProps) {
50
+ const listRef = useRef<HTMLDivElement>(null);
51
+ const [internalTab, setInternalTab] = useState(defaultValue || tabs[0]?.value);
52
+ const activeTab = value ?? internalTab;
46
53
 
47
- const handleTabChange = useCallback((value: string) => {
48
- const tab = tabs.find((t) => t.value === value);
49
- if (tab && !tab.disabled) {
50
- setActiveTab(value);
51
- }
52
- }, [tabs]);
54
+ const handleTabChange = useCallback(
55
+ (next: string) => {
56
+ const tab = tabs.find((t) => t.value === next);
57
+ if (tab && !tab.disabled) {
58
+ setInternalTab(next);
59
+ onValueChange?.(next);
60
+ }
61
+ },
62
+ [tabs, onValueChange]
63
+ );
64
+
65
+ const moveFocus = useCallback(
66
+ (direction: 1 | -1) => {
67
+ const buttons = Array.from(listRef.current?.querySelectorAll<HTMLButtonElement>('[role="tab"]:not([disabled])') ?? []);
68
+ if (buttons.length === 0) return;
69
+ const currentIndex = buttons.findIndex((button) => button.getAttribute('data-state') === 'active');
70
+ const next = buttons[(currentIndex + direction + buttons.length) % buttons.length];
71
+ if (next) {
72
+ next.focus();
73
+ next.click();
74
+ }
75
+ },
76
+ []
77
+ );
78
+
79
+ const handleKeyDown = useCallback(
80
+ (event: KeyboardEvent<HTMLDivElement>) => {
81
+ if (event.key === 'ArrowRight') {
82
+ event.preventDefault();
83
+ moveFocus(1);
84
+ } else if (event.key === 'ArrowLeft') {
85
+ event.preventDefault();
86
+ moveFocus(-1);
87
+ }
88
+ },
89
+ [moveFocus]
90
+ );
53
91
 
54
92
  const currentTab = tabs.find((t) => t.value === activeTab);
55
93
 
@@ -57,7 +95,10 @@ export function Tabs({ tabs, defaultValue, variant = 'default', className, ...pr
57
95
  <div className={cn('w-full', className)} {...props}>
58
96
  {/* Tab List */}
59
97
  <div
98
+ ref={listRef}
60
99
  role="tablist"
100
+ aria-label={ariaLabel}
101
+ onKeyDown={handleKeyDown}
61
102
  className={cn('inline-flex w-full', variantStyles[variant])}
62
103
  >
63
104
  {tabs.map((tab) => {
@@ -67,11 +108,16 @@ export function Tabs({ tabs, defaultValue, variant = 'default', className, ...pr
67
108
  return (
68
109
  <button
69
110
  key={tab.value}
111
+ type="button"
70
112
  role="tab"
71
113
  aria-selected={isActive}
114
+ data-state={isActive ? 'active' : 'inactive'}
115
+ aria-controls={panelId}
116
+ tabIndex={isActive ? 0 : -1}
72
117
  disabled={tab.disabled}
73
118
  onClick={() => handleTabChange(tab.value)}
74
119
  className={cn(
120
+ 'inline-flex items-center gap-1.5',
75
121
  styles.base,
76
122
  isActive
77
123
  ? styles.active
@@ -80,6 +126,11 @@ export function Tabs({ tabs, defaultValue, variant = 'default', className, ...pr
80
126
  )}
81
127
  >
82
128
  {tab.label}
129
+ {typeof tab.count === 'number' && (
130
+ <span className="rounded-full bg-muted px-1.5 text-xs leading-4 text-muted-foreground">
131
+ {tab.count}
132
+ </span>
133
+ )}
83
134
  </button>
84
135
  );
85
136
  })}
@@ -88,6 +139,7 @@ export function Tabs({ tabs, defaultValue, variant = 'default', className, ...pr
88
139
  {/* Tab Content */}
89
140
  <div
90
141
  role="tabpanel"
142
+ id={panelId}
91
143
  className="mt-4"
92
144
  >
93
145
  {currentTab?.content}
@@ -71,8 +71,16 @@ export type { EmptyStateProps } from './EmptyState';
71
71
  export { LoadingState } from './LoadingState';
72
72
  export type { LoadingStateProps } from './LoadingState';
73
73
 
74
- export { DataTable } from './DataTable';
75
- export type { DataTableProps, Column } from './DataTable';
74
+ export { DataTable, TablePagination } from './DataTable';
75
+ export type {
76
+ DataTableProps,
77
+ Column,
78
+ SortState,
79
+ TablePaginationProps,
80
+ ColumnAlign,
81
+ ColumnHideBelow,
82
+ SortDirection,
83
+ } from './DataTable';
76
84
 
77
85
  export { StatCard } from './StatCard';
78
86
  export type { StatCardProps } from './StatCard';
@@ -16,3 +16,40 @@
16
16
  @import "tw-animate-css";
17
17
  @import "./theme.css";
18
18
  @source "../src";
19
+
20
+ @media (max-width: 767.98px) {
21
+ .ds-datatable-stack table,
22
+ .ds-datatable-stack tbody,
23
+ .ds-datatable-stack tr,
24
+ .ds-datatable-stack td {
25
+ display: block;
26
+ width: 100%;
27
+ }
28
+ .ds-datatable-stack thead {
29
+ display: none;
30
+ }
31
+ .ds-datatable-stack tr {
32
+ border: 1px solid var(--color-border);
33
+ border-radius: 0.5rem;
34
+ margin-bottom: 0.625rem;
35
+ padding: 0.25rem 0.75rem 0.375rem;
36
+ }
37
+ .ds-datatable-stack tr:last-child {
38
+ margin-bottom: 0;
39
+ }
40
+ .ds-datatable-stack td:not(.hidden) {
41
+ display: flex;
42
+ align-items: baseline;
43
+ justify-content: space-between;
44
+ gap: 1rem;
45
+ padding: 0.375rem 0;
46
+ border-bottom: 1px solid var(--color-border);
47
+ text-align: right;
48
+ }
49
+ .ds-datatable-stack td:not(.hidden)::before {
50
+ content: attr(data-label);
51
+ font-weight: 500;
52
+ color: var(--color-muted-foreground);
53
+ text-align: left;
54
+ }
55
+ }