@idealyst/components 1.2.128 → 1.2.130

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/components",
3
- "version": "1.2.128",
3
+ "version": "1.2.130",
4
4
  "description": "Shared component library for React and React Native",
5
5
  "documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/components#readme",
6
6
  "readme": "README.md",
@@ -56,7 +56,7 @@
56
56
  "publish:npm": "npm publish"
57
57
  },
58
58
  "peerDependencies": {
59
- "@idealyst/theme": "^1.2.128",
59
+ "@idealyst/theme": "^1.2.130",
60
60
  "@mdi/js": ">=7.0.0",
61
61
  "@mdi/react": ">=1.0.0",
62
62
  "@react-native-vector-icons/common": ">=12.0.0",
@@ -81,10 +81,10 @@
81
81
  "@mdi/react": {
82
82
  "optional": true
83
83
  },
84
- "@react-navigation/bottom-tabs": {
84
+ "@react-native/normalize-colors": {
85
85
  "optional": true
86
86
  },
87
- "@react-native/normalize-colors": {
87
+ "@react-navigation/bottom-tabs": {
88
88
  "optional": true
89
89
  },
90
90
  "react-native": {
@@ -111,8 +111,8 @@
111
111
  },
112
112
  "devDependencies": {
113
113
  "@idealyst/blur": "^1.2.40",
114
- "@idealyst/theme": "^1.2.128",
115
- "@idealyst/tooling": "^1.2.128",
114
+ "@idealyst/theme": "^1.2.130",
115
+ "@idealyst/tooling": "^1.2.130",
116
116
  "@mdi/react": "^1.6.1",
117
117
  "@types/react": "^19.1.0",
118
118
  "react": "^19.1.0",
@@ -256,6 +256,96 @@ function TableInner<T = any>({
256
256
  return column.footer;
257
257
  };
258
258
 
259
+ // Split columns into sticky left, scrollable, and sticky right
260
+ const leftCols = useMemo(() => columns.filter((c) => c.sticky === true || c.sticky === 'left'), [columns]);
261
+ const rightCols = useMemo(() => columns.filter((c) => c.sticky === 'right'), [columns]);
262
+ const scrollCols = useMemo(() => columns.filter((c) => !c.sticky), [columns]);
263
+ const hasStickyColumns = leftCols.length > 0 || rightCols.length > 0;
264
+
265
+ // Renders a column group (header + body + footer) for a set of columns
266
+ const renderColumnGroup = (cols: TableColumn<T>[]) => (
267
+ <View>
268
+ {/* Header */}
269
+ <View style={theadStyle}>
270
+ <View style={{ flexDirection: 'row' }}>
271
+ {cols.map((column) => (
272
+ <TH key={column.key} size={size} type={type} align={column.align} width={column.width}>
273
+ {column.title}
274
+ </TH>
275
+ ))}
276
+ </View>
277
+ </View>
278
+
279
+ {/* Body */}
280
+ <View style={tbodyStyle}>
281
+ {data.length === 0 && emptyState ? (
282
+ <View style={{ alignItems: 'center', padding: 16 }}>
283
+ {emptyState}
284
+ </View>
285
+ ) : (
286
+ data.map((row, rowIndex) => (
287
+ <TR
288
+ key={rowIndex}
289
+ size={size}
290
+ type={type}
291
+ clickable={isClickable}
292
+ onPress={() => onRowPress?.(row, rowIndex)}
293
+ testID={testID ? `${testID}-row-${rowIndex}` : undefined}
294
+ >
295
+ {cols.map((column) => (
296
+ <TD key={column.key} size={size} type={type} align={column.align} width={column.width}>
297
+ {getCellValue(column, row, rowIndex)}
298
+ </TD>
299
+ ))}
300
+ </TR>
301
+ ))
302
+ )}
303
+ </View>
304
+
305
+ {/* Footer */}
306
+ {hasFooter && (
307
+ <View style={(tableStyles.tfoot as any)({})}>
308
+ <View style={{ flexDirection: 'row' }}>
309
+ {cols.map((column) => (
310
+ <TF key={column.key} size={size} type={type} align={column.align} width={column.width}>
311
+ {getFooterContent(column) ?? null}
312
+ </TF>
313
+ ))}
314
+ </View>
315
+ </View>
316
+ )}
317
+ </View>
318
+ );
319
+
320
+ // When there are sticky columns, render as: [left sticky] [scrollable] [right sticky]
321
+ if (hasStickyColumns) {
322
+ return (
323
+ <View
324
+ nativeID={id}
325
+ style={[containerStyle, { flexDirection: 'row' }, style]}
326
+ testID={testID}
327
+ {...nativeA11yProps}
328
+ >
329
+ {leftCols.length > 0 && (
330
+ <View style={{ zIndex: 1, backgroundColor: theadStyle.backgroundColor || containerStyle.backgroundColor }}>
331
+ {renderColumnGroup(leftCols)}
332
+ </View>
333
+ )}
334
+ <ScrollView ref={ref} horizontal style={{ flex: 1 }}>
335
+ <View style={tableStyle}>
336
+ {renderColumnGroup(scrollCols)}
337
+ </View>
338
+ </ScrollView>
339
+ {rightCols.length > 0 && (
340
+ <View style={{ zIndex: 1, backgroundColor: theadStyle.backgroundColor || containerStyle.backgroundColor }}>
341
+ {renderColumnGroup(rightCols)}
342
+ </View>
343
+ )}
344
+ </View>
345
+ );
346
+ }
347
+
348
+ // No sticky columns — original simple layout
259
349
  return (
260
350
  <ScrollView
261
351
  ref={ref}
@@ -266,73 +356,7 @@ function TableInner<T = any>({
266
356
  {...nativeA11yProps}
267
357
  >
268
358
  <View style={tableStyle}>
269
- {/* Header */}
270
- <View style={theadStyle}>
271
- <View style={{ flexDirection: 'row' }}>
272
- {columns.map((column) => (
273
- <TH
274
- key={column.key}
275
- size={size}
276
- type={type}
277
- align={column.align}
278
- width={column.width}
279
- >
280
- {column.title}
281
- </TH>
282
- ))}
283
- </View>
284
- </View>
285
-
286
- {/* Body */}
287
- <View style={tbodyStyle}>
288
- {data.length === 0 && emptyState ? (
289
- <View style={{ alignItems: 'center', padding: 16 }}>
290
- {emptyState}
291
- </View>
292
- ) : (
293
- data.map((row, rowIndex) => (
294
- <TR
295
- key={rowIndex}
296
- size={size}
297
- type={type}
298
- clickable={isClickable}
299
- onPress={() => onRowPress?.(row, rowIndex)}
300
- testID={testID ? `${testID}-row-${rowIndex}` : undefined}
301
- >
302
- {columns.map((column) => (
303
- <TD
304
- key={column.key}
305
- size={size}
306
- type={type}
307
- align={column.align}
308
- width={column.width}
309
- >
310
- {getCellValue(column, row, rowIndex)}
311
- </TD>
312
- ))}
313
- </TR>
314
- ))
315
- )}
316
- </View>
317
-
318
- {/* Footer */}
319
- {hasFooter && (
320
- <View style={(tableStyles.tfoot as any)({})}>
321
- <View style={{ flexDirection: 'row' }}>
322
- {columns.map((column) => (
323
- <TF
324
- key={column.key}
325
- size={size}
326
- type={type}
327
- align={column.align}
328
- width={column.width}
329
- >
330
- {getFooterContent(column) ?? null}
331
- </TF>
332
- ))}
333
- </View>
334
- </View>
335
- )}
359
+ {renderColumnGroup(columns)}
336
360
  </View>
337
361
  </ScrollView>
338
362
  );
@@ -4,6 +4,25 @@ import { tableStyles } from './Table.styles';
4
4
  import type { TableProps, TableColumn, TableType, TableSizeVariant, TableAlignVariant } from './types';
5
5
  import { getWebAriaProps } from '../utils/accessibility';
6
6
 
7
+ // ============================================================================
8
+ // Helpers
9
+ // ============================================================================
10
+
11
+ function getStickyStyle(
12
+ sticky: boolean | 'left' | 'right' | undefined,
13
+ offset: number | string | undefined,
14
+ zIndex: number,
15
+ ): React.CSSProperties | undefined {
16
+ if (!sticky) return undefined;
17
+ const side = sticky === 'right' ? 'right' : 'left';
18
+ return {
19
+ position: 'sticky',
20
+ [side]: offset ?? 0,
21
+ zIndex,
22
+ backgroundColor: 'inherit',
23
+ };
24
+ }
25
+
7
26
  // ============================================================================
8
27
  // Sub-component Props
9
28
  // ============================================================================
@@ -23,8 +42,8 @@ interface THProps {
23
42
  type?: TableType;
24
43
  align?: TableAlignVariant;
25
44
  width?: number | string;
26
- sticky?: boolean;
27
- stickyLeft?: number | string;
45
+ sticky?: boolean | 'left' | 'right';
46
+ stickyOffset?: number | string;
28
47
  resizable?: boolean;
29
48
  onResize?: (width: number) => void;
30
49
  minWidth?: number;
@@ -37,8 +56,8 @@ interface TDProps {
37
56
  type?: TableType;
38
57
  align?: TableAlignVariant;
39
58
  width?: number | string;
40
- sticky?: boolean;
41
- stickyLeft?: number | string;
59
+ sticky?: boolean | 'left' | 'right';
60
+ stickyOffset?: number | string;
42
61
  }
43
62
 
44
63
  // ============================================================================
@@ -83,7 +102,7 @@ function TH({
83
102
  align = 'left',
84
103
  width,
85
104
  sticky,
86
- stickyLeft,
105
+ stickyOffset,
87
106
  resizable,
88
107
  onResize,
89
108
  minWidth = 50,
@@ -129,20 +148,13 @@ function TH({
129
148
  document.addEventListener('pointerup', handlePointerUp);
130
149
  }, [minWidth, onResize]);
131
150
 
132
- const stickyStyle: React.CSSProperties | undefined = sticky ? {
133
- position: 'sticky',
134
- left: stickyLeft ?? 0,
135
- zIndex: 11,
136
- backgroundColor: 'inherit',
137
- } : undefined;
138
-
139
151
  return (
140
152
  <th
141
153
  {...headerCellProps}
142
154
  ref={thRef}
143
155
  scope="col"
144
156
  aria-sort={accessibilitySort}
145
- style={{ width, ...stickyStyle }}
157
+ style={{ width, ...getStickyStyle(sticky, stickyOffset, 11) }}
146
158
  >
147
159
  {children}
148
160
  {resizable && (
@@ -174,7 +186,7 @@ function TD({
174
186
  align = 'left',
175
187
  width,
176
188
  sticky,
177
- stickyLeft,
189
+ stickyOffset,
178
190
  }: TDProps) {
179
191
  tableStyles.useVariants({
180
192
  size,
@@ -184,17 +196,10 @@ function TD({
184
196
 
185
197
  const cellProps = getWebProps([(tableStyles.cell as any)({})]);
186
198
 
187
- const stickyStyle: React.CSSProperties | undefined = sticky ? {
188
- position: 'sticky',
189
- left: stickyLeft ?? 0,
190
- zIndex: 1,
191
- backgroundColor: 'inherit',
192
- } : undefined;
193
-
194
199
  return (
195
200
  <td
196
201
  {...cellProps}
197
- style={{ width, ...stickyStyle }}
202
+ style={{ width, ...getStickyStyle(sticky, stickyOffset, 1) }}
198
203
  >
199
204
  {children}
200
205
  </td>
@@ -211,8 +216,8 @@ interface TFProps {
211
216
  type?: TableType;
212
217
  align?: TableAlignVariant;
213
218
  width?: number | string;
214
- sticky?: boolean;
215
- stickyLeft?: number | string;
219
+ sticky?: boolean | 'left' | 'right';
220
+ stickyOffset?: number | string;
216
221
  }
217
222
 
218
223
  function TF({
@@ -222,7 +227,7 @@ function TF({
222
227
  align = 'left',
223
228
  width,
224
229
  sticky,
225
- stickyLeft,
230
+ stickyOffset,
226
231
  }: TFProps) {
227
232
  tableStyles.useVariants({
228
233
  size,
@@ -232,17 +237,10 @@ function TF({
232
237
 
233
238
  const footerCellProps = getWebProps([(tableStyles.footerCell as any)({})]);
234
239
 
235
- const stickyStyle: React.CSSProperties | undefined = sticky ? {
236
- position: 'sticky',
237
- left: stickyLeft ?? 0,
238
- zIndex: 1,
239
- backgroundColor: 'inherit',
240
- } : undefined;
241
-
242
240
  return (
243
241
  <td
244
242
  {...footerCellProps}
245
- style={{ width, ...stickyStyle }}
243
+ style={{ width, ...getStickyStyle(sticky, stickyOffset, 1) }}
246
244
  >
247
245
  {children}
248
246
  </td>
@@ -329,21 +327,32 @@ function Table<T = any>({
329
327
  return column.footer;
330
328
  };
331
329
 
332
- // Compute cumulative left offsets for sticky columns
333
- const stickyLeftMap = useMemo(() => {
334
- const map = new Map<string, number | string>();
330
+ // Compute cumulative offsets for sticky columns (left and right independently)
331
+ const stickyOffsetMap = useMemo(() => {
332
+ const map = new Map<string, number>();
333
+
334
+ // Left sticky: accumulate left-to-right
335
335
  let cumulativeLeft = 0;
336
336
  for (const col of columns) {
337
- if (!col.sticky) continue;
337
+ const side = col.sticky === 'right' ? 'right' : col.sticky ? 'left' : null;
338
+ if (side !== 'left') continue;
338
339
  map.set(col.key, cumulativeLeft);
339
340
  if (typeof col.width === 'number') {
340
341
  cumulativeLeft += col.width;
341
- } else if (typeof col.width === 'string') {
342
- // For string widths (e.g. '200px'), use as-is for the first, then can't accumulate
343
- // Only numeric widths support proper multi-column stacking
344
- map.set(col.key, cumulativeLeft > 0 ? cumulativeLeft : 0);
345
342
  }
346
343
  }
344
+
345
+ // Right sticky: accumulate right-to-left
346
+ let cumulativeRight = 0;
347
+ for (let i = columns.length - 1; i >= 0; i--) {
348
+ const col = columns[i];
349
+ if (col.sticky !== 'right') continue;
350
+ map.set(col.key, cumulativeRight);
351
+ if (typeof col.width === 'number') {
352
+ cumulativeRight += col.width;
353
+ }
354
+ }
355
+
347
356
  return map;
348
357
  }, [columns]);
349
358
 
@@ -360,7 +369,7 @@ function Table<T = any>({
360
369
  align={column.align}
361
370
  width={column.width}
362
371
  sticky={column.sticky}
363
- stickyLeft={stickyLeftMap.get(column.key)}
372
+ stickyOffset={stickyOffsetMap.get(column.key)}
364
373
  resizable={column.resizable}
365
374
  minWidth={column.minWidth}
366
375
  onResize={onColumnResize ? (w) => onColumnResize(column.key, w) : undefined}
@@ -396,7 +405,7 @@ function Table<T = any>({
396
405
  align={column.align}
397
406
  width={column.width}
398
407
  sticky={column.sticky}
399
- stickyLeft={stickyLeftMap.get(column.key)}
408
+ stickyOffset={stickyOffsetMap.get(column.key)}
400
409
  >
401
410
  {getCellValue(column, row, rowIndex)}
402
411
  </TD>
@@ -416,7 +425,7 @@ function Table<T = any>({
416
425
  align={column.align}
417
426
  width={column.width}
418
427
  sticky={column.sticky}
419
- stickyLeft={stickyLeftMap.get(column.key)}
428
+ stickyOffset={stickyOffsetMap.get(column.key)}
420
429
  >
421
430
  {getFooterContent(column) ?? null}
422
431
  </TF>
@@ -19,9 +19,10 @@ export interface TableColumn<T = any> extends SortableAccessibilityProps {
19
19
  align?: TableAlignVariant;
20
20
  /**
21
21
  * Makes this column sticky (pinned) when scrolling horizontally.
22
- * Web only uses CSS `position: sticky`.
22
+ * `true` or `'left'` pins to the left, `'right'` pins to the right.
23
+ * On web uses CSS `position: sticky`, on native renders outside the ScrollView.
23
24
  */
24
- sticky?: boolean;
25
+ sticky?: boolean | 'left' | 'right';
25
26
  /**
26
27
  * Allows the column to be resized by dragging the right edge of the header.
27
28
  * Web only.