@axzydev/axzy_ui_system 1.3.3 → 1.3.5

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.
@@ -1,8 +1,30 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
2
  import { FaChevronDown } from "react-icons/fa";
3
- import { ITNavigationItem, ITSidebarProps } from "./sidebar.props";
3
+ import {
4
+ ITNavigationItem,
5
+ ITNavigationSubItem,
6
+ ITNavigationSubItemEntry,
7
+ ITNavigationSubItemGroup,
8
+ ITSidebarProps,
9
+ } from "./sidebar.props";
4
10
  import ITText from "@/components/atoms/text/text";
5
11
 
12
+ /** Narrows a submenu entry to a titled group. */
13
+ function isSubItemGroup(
14
+ entry: ITNavigationSubItemEntry,
15
+ ): entry is ITNavigationSubItemGroup {
16
+ return "items" in entry;
17
+ }
18
+
19
+ /** Flattens a parent's entries (plain items + groups) into a single list. */
20
+ function flattenSubItems(
21
+ entries: ITNavigationSubItemEntry[],
22
+ ): ITNavigationSubItem[] {
23
+ return entries.flatMap((entry) =>
24
+ isSubItemGroup(entry) ? entry.items : [entry],
25
+ );
26
+ }
27
+
6
28
  /**
7
29
  * Vertical navigation sidebar with submenu expand/collapse, hover tooltips in collapsed mode,
8
30
  * and glassmorphism styling. The collapsed rail expands automatically on pointer hover — there
@@ -76,7 +98,7 @@ export default function ITSidebar({
76
98
  useEffect(() => {
77
99
  const activeParents = new Set<string>();
78
100
  navigationItems.forEach(item => {
79
- if (item.subitems && item.subitems.some(sub => sub.isActive)) {
101
+ if (item.subitems && flattenSubItems(item.subitems).some(sub => sub.isActive)) {
80
102
  activeParents.add(item.id);
81
103
  }
82
104
  });
@@ -115,6 +137,86 @@ export default function ITSidebar({
115
137
  const isSidebarCollapsed = visibleOnMobile ? false : (!isHovering && isCollapsed);
116
138
  const sidebarWidth = isSidebarCollapsed ? "w-[88px]" : "w-[280px]";
117
139
 
140
+ const renderSubItem = (subitem: ITNavigationSubItem) => (
141
+ <li key={subitem.id} className="relative">
142
+ <button
143
+ aria-current={subitem.isActive ? "page" : undefined}
144
+ onClick={() => {
145
+ if (subitem.action) subitem.action();
146
+ if (onSubItemClick) onSubItemClick(subitem);
147
+ }}
148
+ className={`flex items-center gap-2 w-full text-left px-3 py-1.5 rounded-xl transition-all duration-300`}
149
+ style={{
150
+ color: subitem.isActive ? "var(--it-sidebar-active-color, var(--color-secondary-900))" : "var(--it-sidebar-label-color, var(--color-secondary-600))",
151
+ backgroundColor: subitem.isActive ? "var(--it-sidebar-active-bg, var(--color-secondary-50))" : 'transparent',
152
+ fontSize: '0.78rem',
153
+ fontWeight: subitem.isActive ? 600 : 500,
154
+ letterSpacing: '0.01em',
155
+ marginLeft: subitemConnector === '|' ? '-1px' : '0',
156
+ }}
157
+ onMouseEnter={(e) => {
158
+ if (!subitem.isActive) {
159
+ e.currentTarget.style.backgroundColor = "var(--it-sidebar-hover-bg, var(--color-secondary-100))";
160
+ e.currentTarget.style.transform = 'translateX(3px)';
161
+ }
162
+ }}
163
+ onMouseLeave={(e) => {
164
+ if (!subitem.isActive) {
165
+ e.currentTarget.style.backgroundColor = 'transparent';
166
+ e.currentTarget.style.transform = 'translateX(0)';
167
+ }
168
+ }}
169
+ >
170
+ {subitem.isActive && subitemConnector === '|' && (
171
+ <div
172
+ className="absolute left-0 top-1/3 bottom-1/3 w-[2.5px] rounded-r-full transition-all"
173
+ style={{
174
+ backgroundColor: "var(--it-sidebar-active-icon, var(--color-primary-500))",
175
+ boxShadow: "0 0 6px color-mix(in srgb, var(--it-sidebar-active-icon, var(--color-primary-500)) 25%, transparent)",
176
+ }}
177
+ />
178
+ )}
179
+ {subitemConnector === 'dot' && (
180
+ <span
181
+ className={`w-1.5 h-1.5 rounded-full flex-shrink-0 transition-all duration-300 ${subitem.isActive ? 'scale-125' : ''}`}
182
+ style={{
183
+ backgroundColor: subitem.isActive
184
+ ? "var(--it-sidebar-active-icon, var(--color-primary-500))"
185
+ : "var(--it-sidebar-icon-color, var(--color-secondary-400))"
186
+ }}
187
+ />
188
+ )}
189
+ <ITText as="span" className="truncate">{subitem.label}</ITText>
190
+ </button>
191
+ </li>
192
+ );
193
+
194
+ const renderSubItemGroup = (group: ITNavigationSubItemGroup, itemId: string) => {
195
+ const headingId = `it-sidebar-subgroup-${itemId}-${group.id}`;
196
+ return (
197
+ <li key={group.id} className="relative">
198
+ <div id={headingId} className="px-3 pt-2.5 pb-1 mt-1">
199
+ <ITText
200
+ as="span"
201
+ className="block"
202
+ style={{
203
+ color: "var(--it-sidebar-icon-color, var(--color-secondary-400))",
204
+ fontSize: "0.68rem",
205
+ fontWeight: 700,
206
+ letterSpacing: "0.08em",
207
+ textTransform: "uppercase",
208
+ }}
209
+ >
210
+ {group.label}
211
+ </ITText>
212
+ </div>
213
+ <ul aria-labelledby={headingId} className="flex flex-col gap-0">
214
+ {group.items.map(renderSubItem)}
215
+ </ul>
216
+ </li>
217
+ );
218
+ };
219
+
118
220
  return (
119
221
  <aside
120
222
  ref={sidebarRef}
@@ -256,7 +358,7 @@ export default function ITSidebar({
256
358
 
257
359
  {item.subitems && item.subitems.length > 0 ? (
258
360
  <div className="py-2">
259
- {item.subitems.map((subitem) => (
361
+ {flattenSubItems(item.subitems).map((subitem) => (
260
362
  <div
261
363
  key={subitem.id}
262
364
  className={`px-5 py-2.5 text-sm flex items-center gap-3 transition-colors relative`}
@@ -283,7 +385,7 @@ export default function ITSidebar({
283
385
 
284
386
  {/* Submenu - smooth height/opacity when not collapsed */}
285
387
  {!isSidebarCollapsed && item.subitems && item.subitems.length > 0 && (
286
- <div id={submenuId} role="group" aria-label={item.label} className={`overflow-hidden transition-all duration-400 ease-[cubic-bezier(0.2,0,0,1)] ${expandedItems.has(item.id) ? "max-h-[500px] opacity-100 mt-1" : "max-h-0 opacity-0"}`}>
388
+ <div id={submenuId} role="group" aria-label={item.label} className={`overflow-hidden transition-all duration-400 ease-[cubic-bezier(0.2,0,0,1)] ${expandedItems.has(item.id) ? "max-h-[1000px] opacity-100 mt-1" : "max-h-0 opacity-0"}`}>
287
389
  <ul
288
390
  className="ml-4 flex flex-col gap-0 py-0.5"
289
391
  style={{
@@ -292,59 +394,11 @@ export default function ITSidebar({
292
394
  : 'none'
293
395
  }}
294
396
  >
295
- {item.subitems.map((subitem) => (
296
- <li key={subitem.id} className="relative">
297
- <button
298
- aria-current={subitem.isActive ? "page" : undefined}
299
- onClick={() => {
300
- if (subitem.action) subitem.action();
301
- if (onSubItemClick) onSubItemClick(subitem);
302
- }}
303
- className={`flex items-center gap-2 w-full text-left px-3 py-1.5 rounded-xl transition-all duration-300`}
304
- style={{
305
- color: subitem.isActive ? "var(--it-sidebar-active-color, var(--color-secondary-900))" : "var(--it-sidebar-label-color, var(--color-secondary-600))",
306
- backgroundColor: subitem.isActive ? "var(--it-sidebar-active-bg, var(--color-secondary-50))" : 'transparent',
307
- fontSize: '0.78rem',
308
- fontWeight: subitem.isActive ? 600 : 500,
309
- letterSpacing: '0.01em',
310
- marginLeft: subitemConnector === '|' ? '-1px' : '0',
311
- }}
312
- onMouseEnter={(e) => {
313
- if (!subitem.isActive) {
314
- e.currentTarget.style.backgroundColor = "var(--it-sidebar-hover-bg, var(--color-secondary-100))";
315
- e.currentTarget.style.transform = 'translateX(3px)';
316
- }
317
- }}
318
- onMouseLeave={(e) => {
319
- if (!subitem.isActive) {
320
- e.currentTarget.style.backgroundColor = 'transparent';
321
- e.currentTarget.style.transform = 'translateX(0)';
322
- }
323
- }}
324
- >
325
- {subitem.isActive && subitemConnector === '|' && (
326
- <div
327
- className="absolute left-0 top-1/3 bottom-1/3 w-[2.5px] rounded-r-full transition-all"
328
- style={{
329
- backgroundColor: "var(--it-sidebar-active-icon, var(--color-primary-500))",
330
- boxShadow: "0 0 6px color-mix(in srgb, var(--it-sidebar-active-icon, var(--color-primary-500)) 25%, transparent)",
331
- }}
332
- />
333
- )}
334
- {subitemConnector === 'dot' && (
335
- <span
336
- className={`w-1.5 h-1.5 rounded-full flex-shrink-0 transition-all duration-300 ${subitem.isActive ? 'scale-125' : ''}`}
337
- style={{
338
- backgroundColor: subitem.isActive
339
- ? "var(--it-sidebar-active-icon, var(--color-primary-500))"
340
- : "var(--it-sidebar-icon-color, var(--color-secondary-400))"
341
- }}
342
- />
343
- )}
344
- <ITText as="span" className="truncate">{subitem.label}</ITText>
345
- </button>
346
- </li>
347
- ))}
397
+ {item.subitems.map((entry) =>
398
+ isSubItemGroup(entry)
399
+ ? renderSubItemGroup(entry, item.id)
400
+ : renderSubItem(entry),
401
+ )}
348
402
  </ul>
349
403
  </div>
350
404
  )}
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useMemo, useState } from "react";
2
+ import type { ColumnFilters, ColumnFilterValue } from "@/types/table.types";
2
3
 
3
4
  interface SortConfig {
4
5
  key: string;
@@ -13,30 +14,35 @@ export interface UseTableStateOptions {
13
14
  export interface UseTableStateResult {
14
15
  currentPage: number;
15
16
  itemsPerPage: number;
16
- filters: Record<string, string | boolean | number>;
17
+ filters: ColumnFilters;
17
18
  sortConfig: SortConfig | null;
18
19
  totalPages: number;
19
20
  setTotalPages: (pages: number) => void;
20
21
  goToPage: (page: number) => void;
21
22
  handleItemsPerPageChange: (value: number) => void;
22
- handleFilterChange: (
23
- key: string,
24
- value: string | boolean | number | undefined
25
- ) => void;
23
+ handleFilterChange: (key: string, value: ColumnFilterValue | undefined) => void;
26
24
  handleSort: (key: string, sortable?: boolean) => void;
27
25
  resetPage: () => void;
28
26
  clearFilters: () => void;
29
27
  }
30
28
 
29
+ /**
30
+ * A filter value is considered empty when it should be removed from the filter
31
+ * map: `undefined`/`null`/`""`, or a date range whose both ends are null.
32
+ */
33
+ const isEmptyFilterValue = (value: ColumnFilterValue | undefined): boolean => {
34
+ if (value === undefined || value === null || value === "") return true;
35
+ if (Array.isArray(value)) return value[0] == null && value[1] == null;
36
+ return false;
37
+ };
38
+
31
39
  export function useTableState({
32
40
  defaultItemsPerPage = 10,
33
41
  initialSort = null,
34
42
  }: UseTableStateOptions = {}): UseTableStateResult {
35
43
  const [currentPage, setCurrentPage] = useState(1);
36
44
  const [itemsPerPage, setItemsPerPage] = useState(defaultItemsPerPage);
37
- const [filters, setFilters] = useState<
38
- Record<string, string | boolean | number>
39
- >({});
45
+ const [filters, setFilters] = useState<ColumnFilters>({});
40
46
  const [sortConfig, setSortConfig] = useState<SortConfig | null>(initialSort);
41
47
  const [totalPages, setTotalPages] = useState(1);
42
48
 
@@ -58,14 +64,14 @@ export function useTableState({
58
64
  }, []);
59
65
 
60
66
  const handleFilterChange = useCallback(
61
- (key: string, value: string | boolean | number | undefined) => {
67
+ (key: string, value: ColumnFilterValue | undefined) => {
62
68
  setFilters((prev) => {
63
- if (value === undefined || value === null || value === "") {
69
+ if (isEmptyFilterValue(value)) {
64
70
  const next = { ...prev };
65
71
  delete next[key];
66
72
  return next;
67
73
  }
68
- return { ...prev, [key]: value };
74
+ return { ...prev, [key]: value as ColumnFilterValue };
69
75
  });
70
76
  setCurrentPage(1);
71
77
  },
package/src/index.css CHANGED
@@ -160,4 +160,14 @@
160
160
  .it-stepper-slide-in-left {
161
161
  animation: it-slide-in-left 320ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
162
162
  }
163
+
164
+ /* Pinned (frozen) table columns: 1px edge separator on the frozen side.
165
+ Background + hover live in the component as Tailwind arbitrary utilities so
166
+ the cell matches --color-table-rowBg / --color-table-rowHover. */
167
+ :where(.it-table-pinned-left) {
168
+ box-shadow: inset -1px 0 0 0 var(--color-secondary-200);
169
+ }
170
+ :where(.it-table-pinned-right) {
171
+ box-shadow: inset 1px 0 0 0 var(--color-secondary-200);
172
+ }
163
173
  }
package/src/index.ts CHANGED
@@ -101,13 +101,13 @@ import type { ITSearchSelectProps } from "./components/molecules/search-select/s
101
101
  import type { ITSelectProps } from "./components/molecules/select/select.props";
102
102
  import type { ITSearchTableProps } from "./components/organisms/searchTable/searchTable.props";
103
103
  import type { ITSegmentedControlProps } from "./components/atoms/segmented-control/segmented-control.props";
104
- import type { ITSidebarProps, ITNavigationItem, ITNavigationSubItem } from "./components/organisms/sidebar/sidebar.props";
104
+ import type { ITSidebarProps, ITNavigationItem, ITNavigationSubItem, ITNavigationSubItemEntry, ITNavigationSubItemGroup } from "./components/organisms/sidebar/sidebar.props";
105
105
  import type { ITSkeletonProps } from "./components/atoms/skeleton/skeleton.props";
106
106
  import type { ITSlideToggleProps } from "./components/atoms/slide/slide.props";
107
107
  import type { ITSliderProps } from "./components/atoms/slider/slider.props";
108
108
  import type { ITStackProps } from "./components/atoms/stack/stack.props";
109
109
  import type { ITStatCardProps } from "./components/molecules/stat-card/stat-card.props";
110
- import type { ITTableProps, Column } from "./components/molecules/table/table.props";
110
+ import type { ITTableProps, Column, ColumnFilterMode } from "./components/molecules/table/table.props";
111
111
  import type { ITTextProps } from "./components/atoms/text/text.props";
112
112
  import type { ITTextareaProps } from "./components/atoms/textarea/textarea.props";
113
113
  import type { ITTabsProps, ITTabItem } from "./components/molecules/tabs/tabs.props";
@@ -115,7 +115,7 @@ import type { ITTripleFilterProps, ITTripleFilterOption } from "./components/org
115
115
  import type { ITToastProps } from "./components/organisms/toast/toast.props";
116
116
  import type { ITWysiwygProps } from "./components/molecules/wysiwyg/wysiwyg.props";
117
117
  import type { FieldConfig, FieldConfigV2 } from "./types/field.types";
118
- import type { TableDensity } from "./types/table.types";
118
+ import type { ColumnFilters, ColumnFilterValue, TableDensity } from "./types/table.types";
119
119
 
120
120
  // UTILS
121
121
  import ITDropfile, { UploadStatus, FileTypeEnum } from "./components/organisms/dropfile/dropfile";
@@ -157,7 +157,7 @@ export {
157
157
  };
158
158
 
159
159
  export type {
160
- Column, FieldConfig,
160
+ Column, ColumnFilters, ColumnFilterMode, ColumnFilterValue, FieldConfig,
161
161
  FieldConfigV2,
162
162
  ITAccordionItem, ITAccordionProps,
163
163
  ITAlertProps, ITAvatarProps, ITBadgetProps,
@@ -173,6 +173,7 @@ export {
173
173
  ITLayoutProps, ITLoaderProps, ITNavbarProps,
174
174
  ITPageProps, ITPageHeaderProps,
175
175
  ITNavigationItem, ITNavigationSubItem,
176
+ ITNavigationSubItemEntry, ITNavigationSubItemGroup,
176
177
  ITPaginationProps, ITPopoverProps, ITProgressProps,
177
178
  ITRadioGroupProps, ITRadioOption, ITSelectProps, ITSearchSelectProps,
178
179
  ITSearchTableProps,