@inf-monkeys-tech/monkeys-design 2.0.271 → 2.0.272

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.
@@ -27,9 +27,13 @@ export function normalizeCardGridRange(count: number, range?: CardGridWindowRang
27
27
  return { start, end };
28
28
  }
29
29
 
30
- export function calculateCardGridRange(count: number, offset: number, viewport: number, extent: number, gap: number, overscan: number): CardGridWindowRange {
30
+ export function calculateCardGridRange(count: number, offset: number, viewport: number, extent: number, gap: number, overscan: number, columns = 1): CardGridWindowRange {
31
31
  const stride = extent + gap;
32
32
  const extra = Math.max(0, Math.trunc(overscan));
33
+ if (columns > 1) {
34
+ const rows = calculateCardGridRange(Math.ceil(count / columns), offset, viewport, extent, gap, overscan);
35
+ return { start: rows.start * columns, end: Math.min(count, rows.end * columns) };
36
+ }
33
37
  return normalizeCardGridRange(count, { start: Math.max(0, Math.floor(offset / stride) - extra), end: Math.ceil(Math.max(0, offset + viewport) / stride) + extra });
34
38
  }
35
39
 
@@ -56,7 +60,7 @@ export function useCardGridWindow(config: CardGridWindowing | undefined, count:
56
60
  const currentRange = normalizeCardGridRange(count, range);
57
61
  const committed = useRef(currentRange);
58
62
  committed.current = currentRange;
59
- const [geometry, setGeometry] = useState({ extent: config?.itemExtent ?? 0, gap: config?.gap ?? 0, fallback: false, grid: false });
63
+ const [geometry, setGeometry] = useState({ extent: config?.itemExtent ?? 0, gap: config?.gap ?? 0, fallback: false, grid: false, columns: 1 });
60
64
  const [focused, setFocused] = useState(false);
61
65
  const focusedElement = useRef<Element | null>(null);
62
66
  const axis = config?.axis ?? 'vertical';
@@ -72,7 +76,7 @@ export function useCardGridWindow(config: CardGridWindowing | undefined, count:
72
76
  callback.current?.(next);
73
77
  }, [count]);
74
78
  useEffect(() => {
75
- setGeometry({ extent: config?.itemExtent ?? 0, gap: config?.gap ?? 0, fallback: false, grid: false });
79
+ setGeometry({ extent: config?.itemExtent ?? 0, gap: config?.gap ?? 0, fallback: false, grid: false, columns: 1 });
76
80
  lastSent.current = undefined;
77
81
  }, [config?.itemExtent, config?.gap, config?.enabled, axis]);
78
82
  useEffect(() => {
@@ -97,7 +101,9 @@ export function useCardGridWindow(config: CardGridWindowing | undefined, count:
97
101
  const horizontal = axis === 'horizontal';
98
102
  // Retain the complete layout rather than estimate wrapping or mixed-size cards.
99
103
  const crossTracks = (horizontal ? css.gridTemplateRows : css.gridTemplateColumns).trim().split(/\s+/);
100
- const grid = css.display === 'grid' && css.gridAutoFlow === (horizontal ? 'column' : 'row') && crossTracks.length === 1 && Number.parseFloat(horizontal ? css.gridAutoColumns : css.gridAutoRows) > 0;
104
+ const rowGrid = !horizontal && css.display === 'grid' && css.gridAutoFlow === 'row' && crossTracks.every(track => Number.parseFloat(track) > 0);
105
+ const grid = rowGrid || css.display === 'grid' && css.gridAutoFlow === (horizontal ? 'column' : 'row') && crossTracks.length === 1 && Number.parseFloat(horizontal ? css.gridAutoColumns : css.gridAutoRows) > 0;
106
+ const columns = rowGrid ? crossTracks.length : 1;
101
107
  const flex = css.display === 'flex' && css.flexWrap !== 'wrap' && css.flexWrap !== 'wrap-reverse' && (horizontal ? css.flexDirection !== 'column' : css.flexDirection === 'column');
102
108
  if (!grid && !flex) {
103
109
  setGeometry(previous => previous.fallback ? previous : ({ ...previous, fallback: true }));
@@ -116,8 +122,8 @@ export function useCardGridWindow(config: CardGridWindowing | undefined, count:
116
122
  }
117
123
  }
118
124
  }
119
- const geometryChanged = Math.abs(extent - geometry.extent) > .25 || Math.abs(gap - geometry.gap) > .25 || grid !== geometry.grid || geometry.fallback;
120
- if (geometryChanged) setGeometry(previous => ({ ...previous, extent, gap, grid, fallback: false }));
125
+ const geometryChanged = Math.abs(extent - geometry.extent) > .25 || Math.abs(gap - geometry.gap) > .25 || grid !== geometry.grid || columns !== geometry.columns || geometry.fallback;
126
+ if (geometryChanged) setGeometry(previous => ({ ...previous, extent, gap, grid, columns, fallback: false }));
121
127
  // Restore after measured spacer tracks exist; restoring earlier can clamp the offset.
122
128
  if (windowKey && restored.current !== windowKey) {
123
129
  const saved = retention.get(windowKey);
@@ -134,7 +140,7 @@ export function useCardGridWindow(config: CardGridWindowing | undefined, count:
134
140
  const selfOffset = owner === root ? (horizontal ? root.scrollLeft : root.scrollTop) : 0;
135
141
  const offset = Math.max(0, viewportStart - start + selfOffset - lead);
136
142
  const visibleEnd = Math.max(0, viewportEnd - start + selfOffset - lead);
137
- const next = calculateCardGridRange(count, offset, Math.max(0, visibleEnd - offset), extent, gap, config?.overscan ?? 2);
143
+ const next = calculateCardGridRange(count, offset, Math.max(0, visibleEnd - offset), extent, gap, config?.overscan ?? 2, columns);
138
144
  if (focused && config?.preserveFocus !== false) {
139
145
  let item = focusedElement.current?.closest<HTMLElement>('[data-collection-window-item]') ?? null;
140
146
  while (item && item.closest('.monkeys-ui-card-grid') !== root) item = item.parentElement?.closest<HTMLElement>('[data-collection-window-item]') ?? null;
@@ -158,7 +164,7 @@ export function useCardGridWindow(config: CardGridWindowing | undefined, count:
158
164
  root.querySelectorAll<HTMLElement>('[data-collection-window-item]').forEach(item => observer?.observe(item));
159
165
  schedule();
160
166
  return () => { disposed = true; retain(); if (frame) cancelAnimationFrame(frame); observer?.disconnect(); owner.removeEventListener('scroll', onScroll); window.removeEventListener('resize', schedule); };
161
- }, [requested, active, suspended, count, axis, geometry.extent, geometry.gap, geometry.grid, config?.itemExtent, config?.gap, config?.leadingExtent, config?.overscan, config?.scrollOwner, config?.measureItems, config?.sizeTolerance, currentRange.start, currentRange.end, focused, config?.preserveFocus, send, windowKey, retention, config?.retentionLimit]);
167
+ }, [requested, active, suspended, count, axis, geometry.extent, geometry.gap, geometry.grid, geometry.columns, config?.itemExtent, config?.gap, config?.leadingExtent, config?.overscan, config?.scrollOwner, config?.measureItems, config?.sizeTolerance, currentRange.start, currentRange.end, focused, config?.preserveFocus, send, windowKey, retention, config?.retentionLimit]);
162
168
  useEffect(() => {
163
169
  if (!focused) return;
164
170
  const release = () => {
@@ -174,7 +180,7 @@ export function useCardGridWindow(config: CardGridWindowing | undefined, count:
174
180
  }, [focused]);
175
181
  const stride = geometry.extent + geometry.gap;
176
182
  // Keep spacers until the author has applied the full-range fallback request.
177
- const before = requested && currentRange.start > 0 ? Math.max(0, currentRange.start * stride - geometry.gap) : 0;
178
- const after = requested && currentRange.end < count ? Math.max(0, (count - currentRange.end) * stride - geometry.gap) : 0;
179
- return { ref, before, after, beforeCount: currentRange.start, afterCount: count - currentRange.end, grid: geometry.grid, axis, retention, onFocus: (target: EventTarget | null) => { if (requested && config?.preserveFocus !== false) { focusedElement.current = target instanceof Element ? target : null; setFocused(true); } } };
183
+ const before = requested && currentRange.start > 0 ? Math.max(0, Math.floor(currentRange.start / geometry.columns) * stride - geometry.gap) : 0;
184
+ const after = requested && currentRange.end < count ? Math.max(0, Math.ceil((count - currentRange.end) / geometry.columns) * stride - geometry.gap) : 0;
185
+ return { ref, before, after, beforeCount: currentRange.start, afterCount: count - currentRange.end, grid: geometry.grid, columns: geometry.columns, axis, retention, onFocus: (target: EventTarget | null) => { if (requested && config?.preserveFocus !== false) { focusedElement.current = target instanceof Element ? target : null; setFocused(true); } } };
180
186
  }
@@ -100,7 +100,7 @@ export const CardGrid = forwardRef<HTMLDivElement, CardGridProps>(function CardG
100
100
  const window = useCardGridWindow(windowing, windowItemCount, windowRange, onWindowRangeChange, sorting, windowKey);
101
101
  useCardGridRowCapacity(window.ref, rowPreview, onRowCapacityChange);
102
102
  const attach = useCallback((node: HTMLDivElement | null) => { window.ref.current = node; if (typeof forwardedRef === 'function') forwardedRef(node); else if (forwardedRef) forwardedRef.current = node; }, [forwardedRef, window.ref]);
103
- const spacer = (size: number, key: string) => size > 0 ? <div key={key} aria-hidden="true" data-collection-window-spacer={key} style={{ flex: '0 0 auto', pointerEvents: 'none', ...(window.grid ? window.axis === 'horizontal' ? { gridColumn: 'span ' + (key === 'before' ? window.beforeCount : window.afterCount) } : { gridRow: 'span ' + (key === 'before' ? window.beforeCount : window.afterCount) } : {}), ...(window.axis === 'horizontal' ? { width: size, height: 1 } : { height: size, width: 1 }) }} /> : null;
103
+ const spacer = (size: number, key: string) => size > 0 ? <div key={key} aria-hidden="true" data-collection-window-spacer={key} style={{ flex: '0 0 auto', pointerEvents: 'none', ...(window.grid ? window.axis === 'horizontal' ? { gridColumn: 'span ' + (key === 'before' ? window.beforeCount : window.afterCount) } : window.columns > 1 ? { gridColumn: '1 / -1' } : { gridRow: 'span ' + (key === 'before' ? window.beforeCount : window.afterCount) } : {}), ...(window.axis === 'horizontal' ? { width: size, height: 1 } : { height: size, width: 1 }) }} /> : null;
104
104
  return <CardGridWindowRetention.Provider value={window.retention}><div {...props} ref={attach} role="list" data-view={view} className={cx('monkeys-ui-card-grid', className)} onFocusCapture={event => { onFocusCapture?.(event); window.onFocus(event.target); }} style={{ '--monkeys-ui-card-width': minCardWidth === undefined ? 'var(--monkeys-ui-layout-card-min-width,var(--monkeys-ui-default-layout-card-min-width))' : minCardWidth + 'px', ...(columnGap !== undefined ? { '--monkeys-ui-grid-column-gap': columnGap + 'px' } : {}), ...style } as CSSProperties}>
105
105
  <SortableCollection {...{reorderItems, reorderAxis, reorderGroupField, reorderDisabled, onReorder}} onSortingActiveChange={setSorting}>{leading}{spacer(window.before, 'before')}{children}{spacer(window.after, 'after')}{trailing}</SortableCollection>
106
106
  </div></CardGridWindowRetention.Provider>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inf-monkeys-tech/monkeys-design",
3
- "version": "2.0.271",
3
+ "version": "2.0.272",
4
4
  "description": "Monkeys Design Components Library",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",