@vitus-labs/elements 2.6.1 → 2.7.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/lib/index.d.ts CHANGED
@@ -9,8 +9,7 @@ type SpreadTwo<L, R> = Id<Pick<L, Exclude<keyof L, keyof R>> & R>;
9
9
  type Spread<A extends readonly [...any]> = A extends [infer L, ...infer R] ? SpreadTwo<L, Spread<R>> : unknown;
10
10
  type MergeTypes<A extends readonly [...any]> = ExtractNullableKeys<Spread<A>>;
11
11
  type InnerRef = ForwardedRef<any>;
12
- type CssCallback = (css: typeof config.css) => ReturnType<typeof css>;
13
- type Css = CssCallback | ReturnType<typeof config.css> | string;
12
+ type Css = ((css: typeof config.css) => ReturnType<typeof css>) | ReturnType<typeof config.css> | string;
14
13
  type Content = Parameters<typeof render>['0'];
15
14
  type ContentAlignX = 'left' | 'center' | 'right' | 'spaceBetween' | 'spaceAround' | 'block';
16
15
  type ContentAlignY = 'top' | 'center' | 'bottom' | 'spaceBetween' | 'spaceAround' | 'block';
@@ -296,7 +295,7 @@ type VLElement<P extends Record<string, unknown> = {}> = ((props: Props & P & {
296
295
  }) => ReactElement | null) & VLStatic;
297
296
  //#endregion
298
297
  //#region src/Element/component.d.ts
299
- declare const Component: VLElement;
298
+ declare const MemoComponent: VLElement;
300
299
  //#endregion
301
300
  //#region src/helpers/Iterator/types.d.ts
302
301
  type MaybeNull = undefined | null;
@@ -447,7 +446,7 @@ interface Context {
447
446
  setBlocked: () => void;
448
447
  setUnblocked: () => void;
449
448
  }
450
- declare const Component$2: FC<Context & {
449
+ declare const Component$1: FC<Context & {
451
450
  children: ReactNode;
452
451
  }>;
453
452
  //#endregion
@@ -634,7 +633,7 @@ type Props$3 = {
634
633
  */
635
634
  contentRefName?: string;
636
635
  } & UseOverlayProps;
637
- declare const Component$1: VLComponent<Props$3>;
636
+ declare const Component: VLComponent<Props$3>;
638
637
  //#endregion
639
638
  //#region src/Portal/component.d.ts
640
639
  interface Props$4 {
@@ -652,7 +651,7 @@ interface Props$4 {
652
651
  */
653
652
  tag?: string;
654
653
  }
655
- declare const Component$3: VLComponent<Props$4>;
654
+ declare const Component$2: VLComponent<Props$4>;
656
655
  //#endregion
657
656
  //#region src/Text/component.d.ts
658
657
  type Props$5 = Partial<{
@@ -677,7 +676,7 @@ type Props$5 = Partial<{
677
676
  */
678
677
  css: ExtendCss;
679
678
  }>;
680
- declare const Component$4: VLComponent<Props$5> & {
679
+ declare const Component$3: VLComponent<Props$5> & {
681
680
  isText?: true;
682
681
  };
683
682
  //#endregion
@@ -696,7 +695,7 @@ interface Props$6 {
696
695
  */
697
696
  style?: Record<string, unknown>;
698
697
  }
699
- declare const Component$5: VLComponent<Props$6>;
698
+ declare const Component$4: VLComponent<Props$6>;
700
699
  //#endregion
701
- export { type AlignX, type AlignY, type Content, type ContentBoolean, type Direction, Component as Element, type Props as ElementProps, type ElementType, type ExtendCss, type ExtendedProps, type InnerRef, type Props$1 as IteratorProps, _default as List, type Props$2 as ListProps, type ObjectValue, Component$1 as Overlay, type Props$3 as OverlayProps, Component$2 as OverlayProvider, Component$3 as Portal, type Props$4 as PortalProps, type PropsCallback, Provider, type Responsive, type ResponsiveBoolType, Component$4 as Text, type Props$5 as TextProps, type UseOverlayProps, Component$5 as Util, type Props$6 as UtilProps, type VLStatic, useOverlay };
700
+ export { type AlignX, type AlignY, type Content, type ContentBoolean, type Direction, MemoComponent as Element, type Props as ElementProps, type ElementType, type ExtendCss, type ExtendedProps, type InnerRef, type Props$1 as IteratorProps, _default as List, type Props$2 as ListProps, type ObjectValue, Component as Overlay, type Props$3 as OverlayProps, Component$1 as OverlayProvider, Component$2 as Portal, type Props$4 as PortalProps, type PropsCallback, Provider, type Responsive, type ResponsiveBoolType, Component$3 as Text, type Props$5 as TextProps, type UseOverlayProps, Component$4 as Util, type Props$6 as UtilProps, type VLStatic, useOverlay };
702
701
  //# sourceMappingURL=index2.d.ts.map
package/lib/index.js CHANGED
@@ -22,10 +22,7 @@ const IS_DEVELOPMENT = process.env.NODE_ENV !== "production";
22
22
  * fill remaining space between before and after.
23
23
  */
24
24
  const { styled: styled$2, css: css$2, component: component$1 } = config;
25
- const equalColsCSS = `
26
- flex: 1;
27
- `;
28
- const typeContentCSS = `
25
+ const FLEX_1 = `
29
26
  flex: 1;
30
27
  `;
31
28
  const gapDimensions = {
@@ -57,7 +54,7 @@ const styles$2 = ({ css, theme: t, rootSize }) => css`
57
54
  alignY: t.alignY
58
55
  })};
59
56
 
60
- ${t.equalCols && equalColsCSS};
57
+ ${t.equalCols && FLEX_1};
61
58
 
62
59
  ${t.gap && t.contentType && calculateGap({
63
60
  direction: t.parentDirection,
@@ -74,7 +71,7 @@ const StyledComponent = styled$2(component$1)`
74
71
  align-self: stretch;
75
72
  flex-wrap: wrap;
76
73
 
77
- ${({ $contentType }) => $contentType === "content" && typeContentCSS};
74
+ ${({ $contentType }) => $contentType === "content" && FLEX_1};
78
75
 
79
76
  ${makeItResponsive({
80
77
  key: "$element",
@@ -125,11 +122,11 @@ const Component$9 = ({ contentType, tag, parentDirection, direction, alignX, ali
125
122
  children: render(children)
126
123
  });
127
124
  };
128
- var component_default = memo(Component$9);
125
+ var component_default$1 = memo(Component$9);
129
126
 
130
127
  //#endregion
131
128
  //#region src/helpers/Content/index.ts
132
- var Content_default = component_default;
129
+ var Content_default = component_default$1;
133
130
 
134
131
  //#endregion
135
132
  //#region src/helpers/Wrapper/styled.ts
@@ -216,23 +213,34 @@ const isWebFixNeeded = (tag) => {
216
213
  * (parent + child Styled) because these HTML elements do not natively
217
214
  * support `display: flex` consistently across browsers.
218
215
  */
219
- const DEV_PROPS = IS_DEVELOPMENT ? { "data-vl-element": "Element" } : {};
216
+ const DEV_PROPS = IS_DEVELOPMENT ? { "data-vl-element": "Element" } : null;
220
217
  const Component$8 = ({ children, ref, tag, block, extendCss, direction, alignX, alignY, equalCols, isInline, ...props }) => {
221
- const COMMON_PROPS = {
222
- ...props,
223
- ...DEV_PROPS,
224
- ref,
225
- as: tag
226
- };
227
218
  const needsFix = !props.dangerouslySetInnerHTML && isWebFixNeeded(tag);
228
- const normalElement = useMemo(() => ({
229
- block,
230
- direction,
231
- alignX,
232
- alignY,
233
- equalCols,
234
- extraStyles: extendCss
235
- }), [
219
+ const $element = useMemo(() => {
220
+ if (!needsFix) return {
221
+ block,
222
+ direction,
223
+ alignX,
224
+ alignY,
225
+ equalCols,
226
+ extraStyles: extendCss
227
+ };
228
+ return {
229
+ parent: {
230
+ parentFix: true,
231
+ block,
232
+ extraStyles: extendCss
233
+ },
234
+ child: {
235
+ childFix: true,
236
+ direction,
237
+ alignX,
238
+ alignY,
239
+ equalCols
240
+ }
241
+ };
242
+ }, [
243
+ needsFix,
236
244
  block,
237
245
  direction,
238
246
  alignX,
@@ -240,44 +248,35 @@ const Component$8 = ({ children, ref, tag, block, extendCss, direction, alignX,
240
248
  equalCols,
241
249
  extendCss
242
250
  ]);
243
- const parentFixElement = useMemo(() => ({
244
- parentFix: true,
245
- block,
246
- extraStyles: extendCss
247
- }), [block, extendCss]);
248
- const childFixElement = useMemo(() => ({
249
- childFix: true,
250
- direction,
251
- alignX,
252
- alignY,
253
- equalCols
254
- }), [
255
- direction,
256
- alignX,
257
- alignY,
258
- equalCols
259
- ]);
260
251
  if (!needsFix || false) return /* @__PURE__ */ jsx(styled_default$1, {
261
- ...COMMON_PROPS,
262
- $element: normalElement,
252
+ ...props,
253
+ ...DEV_PROPS,
254
+ ref,
255
+ as: tag,
256
+ $element,
263
257
  children
264
258
  });
265
259
  const asTag = isInline ? "span" : "div";
260
+ const fix = $element;
266
261
  return /* @__PURE__ */ jsx(styled_default$1, {
267
- ...COMMON_PROPS,
268
- $element: parentFixElement,
262
+ ...props,
263
+ ...DEV_PROPS,
264
+ ref,
265
+ as: tag,
266
+ $element: fix.parent,
269
267
  children: /* @__PURE__ */ jsx(styled_default$1, {
270
268
  as: asTag,
271
269
  $childFix: true,
272
- $element: childFixElement,
270
+ $element: fix.child,
273
271
  children
274
272
  })
275
273
  });
276
274
  };
275
+ var component_default = memo(Component$8);
277
276
 
278
277
  //#endregion
279
278
  //#region src/helpers/Wrapper/index.ts
280
- var Wrapper_default = Component$8;
279
+ var Wrapper_default = component_default;
281
280
 
282
281
  //#endregion
283
282
  //#region src/Element/constants.ts
@@ -333,7 +332,6 @@ const EMPTY_ELEMENTS = {
333
332
  hr: true,
334
333
  img: true,
335
334
  input: true,
336
- keygen: true,
337
335
  link: true,
338
336
  textarea: true,
339
337
  source: true,
@@ -367,22 +365,22 @@ const getShouldBeEmpty = (tag) => {
367
365
  const equalize = (el, direction) => {
368
366
  const beforeEl = el.firstElementChild;
369
367
  const afterEl = el.lastElementChild;
370
- if (beforeEl && afterEl && beforeEl !== afterEl) {
371
- const type = direction === "rows" ? "height" : "width";
372
- const prop = type === "height" ? "offsetHeight" : "offsetWidth";
373
- const beforeSize = beforeEl[prop];
374
- const afterSize = afterEl[prop];
375
- if (Number.isInteger(beforeSize) && Number.isInteger(afterSize)) {
376
- const maxSize = `${Math.max(beforeSize, afterSize)}px`;
377
- beforeEl.style[type] = maxSize;
378
- afterEl.style[type] = maxSize;
379
- }
380
- }
368
+ if (!beforeEl || !afterEl || beforeEl === afterEl) return;
369
+ const type = direction === "rows" ? "height" : "width";
370
+ const prop = type === "height" ? "offsetHeight" : "offsetWidth";
371
+ const beforeSize = beforeEl[prop];
372
+ const afterSize = afterEl[prop];
373
+ if (!Number.isInteger(beforeSize) || !Number.isInteger(afterSize)) return;
374
+ if (beforeSize === afterSize && beforeSize > 0) return;
375
+ const maxSize = `${Math.max(beforeSize, afterSize)}px`;
376
+ beforeEl.style[type] = maxSize;
377
+ afterEl.style[type] = maxSize;
381
378
  };
382
379
  const defaultDirection = "inline";
383
380
  const defaultContentDirection = "rows";
384
381
  const defaultAlignX = "left";
385
382
  const defaultAlignY = "center";
383
+ const AS_RESET = { as: void 0 };
386
384
  const Component$7 = ({ innerRef, ref, tag, label, content, children, beforeContent, afterContent, equalBeforeAfter, block, equalCols, gap, direction, alignX = defaultAlignX, alignY = defaultAlignY, css, contentCss, beforeContentCss, afterContentCss, contentDirection = defaultContentDirection, contentAlignX = defaultAlignX, contentAlignY = defaultAlignY, beforeContentDirection = defaultDirection, beforeContentAlignX = defaultAlignX, beforeContentAlignY = defaultAlignY, afterContentDirection = defaultDirection, afterContentAlignX = defaultAlignX, afterContentAlignY = defaultAlignY, ...props }) => {
387
385
  const shouldBeEmpty = !!props.dangerouslySetInnerHTML || getShouldBeEmpty(tag);
388
386
  const isSimpleElement = !beforeContent && !afterContent;
@@ -415,11 +413,14 @@ const Component$7 = ({ innerRef, ref, tag, label, content, children, beforeConte
415
413
  ]);
416
414
  const equalizeRef = useRef(null);
417
415
  const externalRef = ref ?? innerRef;
416
+ const latestExternalRef = useRef(externalRef);
417
+ latestExternalRef.current = externalRef;
418
418
  const mergedRef = useCallback((node) => {
419
419
  equalizeRef.current = node;
420
- if (typeof externalRef === "function") externalRef(node);
421
- else if (externalRef != null) externalRef.current = node;
422
- }, [externalRef]);
420
+ const r = latestExternalRef.current;
421
+ if (typeof r === "function") r(node);
422
+ else if (r != null) r.current = node;
423
+ }, []);
423
424
  useLayoutEffect(() => {
424
425
  if (!equalBeforeAfter || !beforeContent || !afterContent) return;
425
426
  const node = equalizeRef.current;
@@ -435,7 +436,8 @@ const Component$7 = ({ innerRef, ref, tag, label, content, children, beforeConte
435
436
  afterContent,
436
437
  direction
437
438
  ]);
438
- const WRAPPER_PROPS = {
439
+ if (shouldBeEmpty) return /* @__PURE__ */ jsx(Wrapper_default, {
440
+ ...props,
439
441
  ref: mergedRef,
440
442
  extendCss: css,
441
443
  tag,
@@ -443,16 +445,19 @@ const Component$7 = ({ innerRef, ref, tag, label, content, children, beforeConte
443
445
  direction: wrapperDirection,
444
446
  alignX: wrapperAlignX,
445
447
  alignY: wrapperAlignY,
446
- as: void 0
447
- };
448
- if (shouldBeEmpty) return /* @__PURE__ */ jsx(Wrapper_default, {
449
- ...props,
450
- ...WRAPPER_PROPS
448
+ ...AS_RESET
451
449
  });
452
450
  return /* @__PURE__ */ jsxs(Wrapper_default, {
453
451
  ...props,
454
- ...WRAPPER_PROPS,
452
+ ref: mergedRef,
453
+ extendCss: css,
454
+ tag,
455
+ block,
456
+ direction: wrapperDirection,
457
+ alignX: wrapperAlignX,
458
+ alignY: wrapperAlignY,
455
459
  isInline,
460
+ ...AS_RESET,
456
461
  children: [
457
462
  beforeContent && /* @__PURE__ */ jsx(Content_default, {
458
463
  tag: SUB_TAG,
@@ -493,13 +498,14 @@ const Component$7 = ({ innerRef, ref, tag, label, content, children, beforeConte
493
498
  });
494
499
  };
495
500
  const name$5 = `${PKG_NAME}/Element`;
496
- Component$7.displayName = name$5;
497
- Component$7.pkgName = PKG_NAME;
498
- Component$7.VITUS_LABS__COMPONENT = name$5;
501
+ const MemoComponent = memo(Component$7);
502
+ MemoComponent.displayName = name$5;
503
+ MemoComponent.pkgName = PKG_NAME;
504
+ MemoComponent.VITUS_LABS__COMPONENT = name$5;
499
505
 
500
506
  //#endregion
501
507
  //#region src/Element/index.ts
502
- var Element_default = Component$7;
508
+ var Element_default = MemoComponent;
503
509
 
504
510
  //#endregion
505
511
  //#region src/helpers/Iterator/component.tsx
@@ -557,18 +563,41 @@ const flattenChildren = (children) => {
557
563
  if (isFragment(children)) return children.props.children;
558
564
  return [children];
559
565
  };
560
- /** Drop nullish entries and empty objects (matches legacy behavior). */
561
- const filterValidItems = (data) => data.filter((item) => item != null && !(typeof item === "object" && isEmpty(item)));
562
- /** Determine if the array is uniformly simple (string/number) or complex (object). Mixed → null. */
563
- const detectKind = (items) => {
566
+ const classifyItem = (item) => {
567
+ const t = typeof item;
568
+ if (t === "string" || t === "number") return "simple";
569
+ if (t === "object") return "complex";
570
+ return null;
571
+ };
572
+ /**
573
+ * Single-pass: filter out nullish + empty-object entries and detect whether
574
+ * the surviving items form a uniformly simple (string/number) or complex
575
+ * (object) collection. Mixed shapes → kind `null`. Replaces the prior
576
+ * `filterValidItems` + `detectKind` two-pass which allocated an intermediate
577
+ * filtered array; this fuses both into one scan.
578
+ */
579
+ const filterAndDetectKind = (data) => {
580
+ const items = [];
564
581
  let kind = null;
565
- for (const item of items) {
566
- const t = typeof item === "string" || typeof item === "number" ? "simple" : typeof item === "object" ? "complex" : null;
567
- if (t === null) return null;
582
+ for (const item of data) {
583
+ if (item == null) continue;
584
+ if (typeof item === "object" && isEmpty(item)) continue;
585
+ items.push(item);
586
+ const t = classifyItem(item);
587
+ if (t === null) return {
588
+ items,
589
+ kind: null
590
+ };
568
591
  if (kind === null) kind = t;
569
- else if (kind !== t) return null;
592
+ else if (kind !== t) return {
593
+ items,
594
+ kind: null
595
+ };
570
596
  }
571
- return kind;
597
+ return {
598
+ items,
599
+ kind
600
+ };
572
601
  };
573
602
  const objectKey = (item, index, itemKey) => {
574
603
  if (!itemKey) return item.key ?? item.id ?? item.itemId ?? index;
@@ -607,10 +636,8 @@ const buildObjectSpecs = (items, component, itemKey) => items.map((item, i) => {
607
636
  };
608
637
  });
609
638
  const buildDataSpecs = (data, component, valueName, itemKey) => {
610
- const items = filterValidItems(data);
611
- if (items.length === 0) return null;
612
- const kind = detectKind(items);
613
- if (!kind) return null;
639
+ const { items, kind } = filterAndDetectKind(data);
640
+ if (items.length === 0 || !kind) return null;
614
641
  return kind === "simple" ? buildSimpleSpecs(items, component, valueName, itemKey) : buildObjectSpecs(items, component, itemKey);
615
642
  };
616
643
  const Component$6 = ({ itemKey, valueName, children, component, data, wrapComponent: Wrapper, wrapProps, itemProps }) => {
@@ -641,12 +668,13 @@ var Iterator_default = Iterator;
641
668
  * is wrapped in an Element that receives all non-iterator props (e.g.,
642
669
  * layout, alignment, css), allowing the list to be styled as a single block.
643
670
  */
671
+ const RESERVED_PROPS_SET = new Set(Iterator_default.RESERVED_PROPS);
644
672
  const Component$5 = ({ rootElement = false, ref, ...props }) => {
645
673
  const renderedList = /* @__PURE__ */ jsx(Iterator_default, { ...pick(props, Iterator_default.RESERVED_PROPS) });
646
674
  if (!rootElement) return renderedList;
647
675
  return /* @__PURE__ */ jsx(Element_default, {
648
676
  ref,
649
- ...omit(props, Iterator_default.RESERVED_PROPS),
677
+ ...omit(props, RESERVED_PROPS_SET),
650
678
  children: renderedList
651
679
  });
652
680
  };
@@ -725,6 +753,11 @@ const Component = ({ children, blocked, setBlocked, setUnblocked }) => {
725
753
  * the final viewport-relative position and the alignment that was actually used
726
754
  * (which may differ from the requested alignment when an edge would be clipped).
727
755
  */
756
+ const DROPDOWN_TYPES = new Set([
757
+ "dropdown",
758
+ "tooltip",
759
+ "popover"
760
+ ]);
728
761
  const sel = (cond, a, b) => cond ? a : b;
729
762
  const devWarn = (msg) => {
730
763
  if (!IS_DEVELOPMENT) return;
@@ -850,11 +883,7 @@ const adjustForAncestor = (pos, ancestor) => {
850
883
  return result;
851
884
  };
852
885
  const computePosition = (type, align, alignX, alignY, offsetX, offsetY, triggerEl, contentEl, ancestorOffset) => {
853
- const isDropdown = [
854
- "dropdown",
855
- "tooltip",
856
- "popover"
857
- ].includes(type);
886
+ const isDropdown = DROPDOWN_TYPES.has(type);
858
887
  if (isDropdown && (!triggerEl || !contentEl)) {
859
888
  devWarn(`[@vitus-labs/elements] Overlay (${type}): ${triggerEl ? "contentRef" : "triggerRef"} is not attached. Position cannot be calculated without both refs.`);
860
889
  return { pos: {} };
@@ -913,6 +942,80 @@ const useEscapeKey = (enabled, active, blocked, hide) => {
913
942
  ]);
914
943
  };
915
944
 
945
+ //#endregion
946
+ //#region src/Overlay/useFocusTrap.ts
947
+ const FOCUSABLE = [
948
+ "a[href]",
949
+ "button:not([disabled])",
950
+ "input:not([disabled])",
951
+ "select:not([disabled])",
952
+ "textarea:not([disabled])",
953
+ "[contenteditable]:not([contenteditable=\"false\"])",
954
+ "video[controls]",
955
+ "audio[controls]",
956
+ "summary",
957
+ "[tabindex]:not([tabindex=\"-1\"])"
958
+ ].join(",");
959
+ const isTabbable = (el) => el.getAttribute("aria-disabled") !== "true";
960
+ const useFocusTrap = (ref, enabled = true, options) => {
961
+ const autoFocus = options?.autoFocus !== false;
962
+ useEffect(() => {
963
+ if (!enabled) return void 0;
964
+ const container = ref.current;
965
+ if (!container) return void 0;
966
+ let focusable = [];
967
+ const refresh = () => {
968
+ const all = container.querySelectorAll(FOCUSABLE);
969
+ const result = [];
970
+ for (let i = 0; i < all.length; i++) {
971
+ const el = all[i];
972
+ if (el && isTabbable(el)) result.push(el);
973
+ }
974
+ focusable = result;
975
+ };
976
+ refresh();
977
+ const observer = new MutationObserver(refresh);
978
+ observer.observe(container, {
979
+ childList: true,
980
+ subtree: true
981
+ });
982
+ const prevFocus = document.activeElement;
983
+ if (autoFocus) if (focusable.length > 0) focusable[0].focus();
984
+ else {
985
+ if (container.tabIndex < 0) container.tabIndex = -1;
986
+ container.focus();
987
+ }
988
+ const handler = (e) => {
989
+ if (e.key !== "Tab" || focusable.length === 0) return;
990
+ const first = focusable[0];
991
+ const last = focusable[focusable.length - 1];
992
+ const active = document.activeElement;
993
+ if (active && !container.contains(active)) {
994
+ e.preventDefault();
995
+ first.focus();
996
+ return;
997
+ }
998
+ if (e.shiftKey && active === first) {
999
+ e.preventDefault();
1000
+ last.focus();
1001
+ } else if (!e.shiftKey && active === last) {
1002
+ e.preventDefault();
1003
+ first.focus();
1004
+ }
1005
+ };
1006
+ document.addEventListener("keydown", handler);
1007
+ return () => {
1008
+ observer.disconnect();
1009
+ document.removeEventListener("keydown", handler);
1010
+ if (autoFocus && prevFocus && typeof prevFocus.focus === "function") prevFocus.focus();
1011
+ };
1012
+ }, [
1013
+ ref,
1014
+ enabled,
1015
+ autoFocus
1016
+ ]);
1017
+ };
1018
+
916
1019
  //#endregion
917
1020
  //#region src/Overlay/useHoverListeners.ts
918
1021
  /**
@@ -981,50 +1084,49 @@ const useHoverListeners = ({ triggerRef, contentRef, isContentLoaded, active, bl
981
1084
  ]);
982
1085
  };
983
1086
 
1087
+ //#endregion
1088
+ //#region src/Overlay/useScrollLock.ts
1089
+ let lockCount = 0;
1090
+ let originalOverflow;
1091
+ const useScrollLock = (enabled) => {
1092
+ useEffect(() => {
1093
+ if (!enabled) return void 0;
1094
+ if (lockCount === 0) originalOverflow = document.body.style.overflow;
1095
+ lockCount++;
1096
+ document.body.style.overflow = "hidden";
1097
+ return () => {
1098
+ lockCount--;
1099
+ if (lockCount === 0) {
1100
+ document.body.style.overflow = originalOverflow ?? "";
1101
+ originalOverflow = void 0;
1102
+ }
1103
+ };
1104
+ }, [enabled]);
1105
+ };
1106
+
984
1107
  //#endregion
985
1108
  //#region src/Overlay/useScrollReposition.ts
986
- let modalOverflowCount = 0;
987
- /**
988
- * Window-level scroll/resize listeners that reposition active overlays and
989
- * re-evaluate close-on-scroll behavior. Also manages the body overflow lock
990
- * for modal overlays (refcounted across nested modals).
991
- */
992
- const useWindowReposition = (active, type, handleContentPosition, handleVisibility) => {
1109
+ const useWindowReposition = (active, handleContentPosition, handleVisibility) => {
993
1110
  useEffect(() => {
994
1111
  if (!active) return void 0;
995
- const shouldSetOverflow = type === "modal";
996
1112
  const onScroll = (e) => {
997
1113
  handleContentPosition();
998
1114
  handleVisibility(e);
999
1115
  };
1000
- if (shouldSetOverflow) {
1001
- modalOverflowCount++;
1002
- if (modalOverflowCount === 1) document.body.style.overflow = "hidden";
1003
- }
1004
1116
  window.addEventListener("resize", handleContentPosition);
1005
1117
  window.addEventListener("scroll", onScroll, { passive: true });
1006
1118
  return () => {
1007
1119
  handleContentPosition.cancel();
1008
1120
  handleVisibility.cancel();
1009
- if (shouldSetOverflow) {
1010
- modalOverflowCount--;
1011
- if (modalOverflowCount === 0) document.body.style.overflow = "";
1012
- }
1013
1121
  window.removeEventListener("resize", handleContentPosition);
1014
1122
  window.removeEventListener("scroll", onScroll);
1015
1123
  };
1016
1124
  }, [
1017
1125
  active,
1018
- type,
1019
1126
  handleContentPosition,
1020
1127
  handleVisibility
1021
1128
  ]);
1022
1129
  };
1023
- /**
1024
- * Same as `useWindowReposition` but for a custom scrollable ancestor.
1025
- * Locks the parent's overflow while the overlay is active (unless hover-driven,
1026
- * which expects the parent to keep scrolling).
1027
- */
1028
1130
  const useParentContainerReposition = (active, parentContainer, closeOn, handleContentPosition, handleVisibility) => {
1029
1131
  useEffect(() => {
1030
1132
  if (!active || !parentContainer) return void 0;
@@ -1046,8 +1148,8 @@ const useParentContainerReposition = (active, parentContainer, closeOn, handleCo
1046
1148
  handleVisibility
1047
1149
  ]);
1048
1150
  };
1049
- const useScrollReposition = ({ active, type, parentContainer, closeOn, handleContentPosition, handleVisibility }) => {
1050
- useWindowReposition(active, type, handleContentPosition, handleVisibility);
1151
+ const useScrollReposition = ({ active, parentContainer, closeOn, handleContentPosition, handleVisibility }) => {
1152
+ useWindowReposition(active, handleContentPosition, handleVisibility);
1051
1153
  useParentContainerReposition(active, parentContainer, closeOn, handleContentPosition, handleVisibility);
1052
1154
  };
1053
1155
 
@@ -1063,6 +1165,12 @@ const useScrollReposition = ({ active, type, parentContainer, closeOn, handleCon
1063
1165
  * live in dedicated hooks: `./useEscapeKey`, `./useHoverListeners`,
1064
1166
  * `./useScrollReposition`.
1065
1167
  */
1168
+ const CLICK_CLOSE_KINDS = new Set([
1169
+ "click",
1170
+ "clickOnTrigger",
1171
+ "clickOutsideContent"
1172
+ ]);
1173
+ const isNodeOrChildOf = (node, target) => !!(node && target && (node.contains(target) || target === node));
1066
1174
  const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type = "dropdown", position = "fixed", align = "bottom", alignX = "left", alignY = "bottom", offsetX = 0, offsetY = 0, throttleDelay = 200, parentContainer, closeOnEsc = true, hoverDelay = 100, disabled, onOpen, onClose } = {}) => {
1067
1175
  const { rootSize } = useContext(context);
1068
1176
  const ctx = useOverlayContext();
@@ -1074,7 +1182,6 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1074
1182
  const [active, setActive] = useState(isOpen);
1075
1183
  const triggerRef = useRef(null);
1076
1184
  const contentRef = useRef(null);
1077
- const prevFocusRef = useRef(null);
1078
1185
  const setBlocked = useCallback(() => setBlockedCount((c) => c + 1), []);
1079
1186
  const setUnblocked = useCallback(() => setBlockedCount((c) => Math.max(0, c - 1)), []);
1080
1187
  const showContent = useCallback(() => setActive(true), []);
@@ -1125,13 +1232,11 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1125
1232
  const setContentPosition = useCallback(() => {
1126
1233
  assignContentPosition(calculateContentPosition());
1127
1234
  }, [assignContentPosition, calculateContentPosition]);
1128
- const isNodeOrChild = useCallback((ref) => (e) => {
1129
- if (e?.target && ref.current) return ref.current.contains(e.target) || e.target === ref.current;
1130
- return false;
1131
- }, []);
1235
+ const isTriggerOrChild = useCallback((e) => isNodeOrChildOf(triggerRef.current, e?.target), []);
1236
+ const isContentOrChild = useCallback((e) => isNodeOrChildOf(contentRef.current, e?.target), []);
1132
1237
  const handleVisibilityByEventType = useCallback((e) => {
1133
1238
  if (blocked || disabled) return;
1134
- processVisibilityEvent(e, active, openOn, closeOn, isNodeOrChild(triggerRef), isNodeOrChild(contentRef), showContent, hideContent);
1239
+ processVisibilityEvent(e, active, openOn, closeOn, isTriggerOrChild, isContentOrChild, showContent, hideContent);
1135
1240
  }, [
1136
1241
  active,
1137
1242
  blocked,
@@ -1140,7 +1245,8 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1140
1245
  closeOn,
1141
1246
  hideContent,
1142
1247
  showContent,
1143
- isNodeOrChild
1248
+ isTriggerOrChild,
1249
+ isContentOrChild
1144
1250
  ]);
1145
1251
  const latestSetContentPosition = useRef(setContentPosition);
1146
1252
  latestSetContentPosition.current = setContentPosition;
@@ -1169,50 +1275,35 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1169
1275
  isContentLoaded,
1170
1276
  setContentPosition
1171
1277
  ]);
1278
+ const latestOnOpen = useRef(onOpen);
1279
+ latestOnOpen.current = onOpen;
1280
+ const latestOnClose = useRef(onClose);
1281
+ latestOnClose.current = onClose;
1172
1282
  const prevActiveRef = useRef(false);
1173
1283
  useEffect(() => {
1174
1284
  const wasActive = prevActiveRef.current;
1175
1285
  prevActiveRef.current = active;
1176
1286
  if (active && !wasActive) {
1177
- onOpen?.();
1287
+ latestOnOpen.current?.();
1178
1288
  ctx.setBlocked?.();
1179
1289
  } else if (!active && wasActive) {
1180
1290
  setContentLoaded(false);
1181
- onClose?.();
1291
+ latestOnClose.current?.();
1182
1292
  ctx.setUnblocked?.();
1183
1293
  } else if (!active) setContentLoaded(false);
1184
1294
  return () => {
1185
1295
  if (active) {
1186
- onClose?.();
1296
+ latestOnClose.current?.();
1187
1297
  ctx.setUnblocked?.();
1188
1298
  }
1189
1299
  };
1190
- }, [
1191
- active,
1192
- ctx,
1193
- onClose,
1194
- onOpen
1195
- ]);
1196
- useEffect(() => {
1197
- if (type !== "modal") return;
1198
- if (active && isContentLoaded && contentRef.current) {
1199
- prevFocusRef.current = document.activeElement;
1200
- if (contentRef.current.tabIndex < 0) contentRef.current.tabIndex = -1;
1201
- contentRef.current.focus();
1202
- }
1203
- if (!active && prevFocusRef.current) {
1204
- prevFocusRef.current.focus();
1205
- prevFocusRef.current = null;
1206
- }
1207
- }, [
1208
- active,
1209
- isContentLoaded,
1210
- type
1211
- ]);
1300
+ }, [active, ctx]);
1301
+ const modalActive = type === "modal" && active && isContentLoaded;
1302
+ useFocusTrap(contentRef, modalActive);
1303
+ useScrollLock(modalActive);
1212
1304
  useEscapeKey(closeOnEsc, active, blocked, hideContent);
1213
1305
  useScrollReposition({
1214
1306
  active,
1215
- type,
1216
1307
  parentContainer,
1217
1308
  closeOn,
1218
1309
  handleContentPosition,
@@ -1220,12 +1311,8 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1220
1311
  });
1221
1312
  useEffect(() => {
1222
1313
  if (blocked || disabled) return void 0;
1223
- if (openOn === "click" || [
1224
- "click",
1225
- "clickOnTrigger",
1226
- "clickOutsideContent"
1227
- ].includes(closeOn)) window.addEventListener("click", handleClick);
1228
- return () => window.removeEventListener("click", handleClick);
1314
+ if (openOn === "click" || CLICK_CLOSE_KINDS.has(closeOn)) document.addEventListener("click", handleClick);
1315
+ return () => document.removeEventListener("click", handleClick);
1229
1316
  }, [
1230
1317
  openOn,
1231
1318
  closeOn,
@@ -1279,44 +1366,42 @@ const Component$3 = ({ children, trigger, DOMLocation, triggerRefName = "ref", c
1279
1366
  const { active, triggerRef, contentRef, showContent, hideContent, align, alignX, alignY, Provider, ...ctx } = useOverlay(props);
1280
1367
  const { openOn, closeOn, type } = props;
1281
1368
  const contentId = useId();
1282
- const passHandlers = useMemo(() => openOn === "manual" || closeOn === "manual" || closeOn === "clickOutsideContent", [openOn, closeOn]);
1283
- const ariaHasPopup = useMemo(() => {
1284
- switch (type) {
1285
- case "modal": return "dialog";
1286
- case "tooltip": return "true";
1287
- default: return "menu";
1288
- }
1289
- }, [type]);
1290
- return /* @__PURE__ */ jsxs(Fragment, { children: [render(trigger, {
1369
+ const passHandlers = openOn === "manual" || closeOn === "manual" || closeOn === "clickOutsideContent";
1370
+ const ariaHasPopup = type === "modal" ? "dialog" : type === "tooltip" ? "true" : "menu";
1371
+ const triggerProps = {
1291
1372
  [triggerRefName]: triggerRef,
1292
1373
  active,
1293
1374
  "aria-expanded": active,
1294
1375
  "aria-haspopup": ariaHasPopup,
1295
- "aria-controls": active ? contentId : void 0,
1296
- ...passHandlers ? {
1297
- showContent,
1298
- hideContent
1299
- } : {}
1300
- }), IS_BROWSER && active && /* @__PURE__ */ jsx(Portal_default, {
1301
- DOMLocation,
1302
- children: /* @__PURE__ */ jsx(Provider, {
1303
- ...ctx,
1304
- children: render(children, {
1305
- [contentRefName]: contentRef,
1306
- id: contentId,
1307
- role: type === "modal" ? "dialog" : void 0,
1308
- "aria-modal": type === "modal" ? true : void 0,
1309
- active,
1310
- align,
1311
- alignX,
1312
- alignY,
1313
- ...passHandlers ? {
1314
- showContent,
1315
- hideContent
1316
- } : {}
1376
+ "aria-controls": active ? contentId : void 0
1377
+ };
1378
+ if (passHandlers) {
1379
+ triggerProps.showContent = showContent;
1380
+ triggerProps.hideContent = hideContent;
1381
+ }
1382
+ return /* @__PURE__ */ jsxs(Fragment, { children: [render(trigger, triggerProps), IS_BROWSER && active && (() => {
1383
+ const contentProps = {
1384
+ [contentRefName]: contentRef,
1385
+ id: contentId,
1386
+ role: type === "modal" ? "dialog" : void 0,
1387
+ "aria-modal": type === "modal" ? true : void 0,
1388
+ active,
1389
+ align,
1390
+ alignX,
1391
+ alignY
1392
+ };
1393
+ if (passHandlers) {
1394
+ contentProps.showContent = showContent;
1395
+ contentProps.hideContent = hideContent;
1396
+ }
1397
+ return /* @__PURE__ */ jsx(Portal_default, {
1398
+ DOMLocation,
1399
+ children: /* @__PURE__ */ jsx(Provider, {
1400
+ ...ctx,
1401
+ children: render(children, contentProps)
1317
1402
  })
1318
- })
1319
- })] });
1403
+ });
1404
+ })()] });
1320
1405
  };
1321
1406
  const name$2 = `${PKG_NAME}/Overlay`;
1322
1407
  Component$3.displayName = name$2;
@@ -1357,17 +1442,13 @@ var styled_default = styled(textComponent)`
1357
1442
  //#endregion
1358
1443
  //#region src/Text/component.tsx
1359
1444
  const Component$2 = ({ paragraph, label, children, tag, css, ref, ...props }) => {
1360
- const renderContent = (as = void 0) => /* @__PURE__ */ jsx(styled_default, {
1445
+ return /* @__PURE__ */ jsx(styled_default, {
1361
1446
  ref,
1362
- as,
1447
+ as: paragraph ? "p" : tag,
1363
1448
  $text: { extraStyles: css },
1364
1449
  ...props,
1365
1450
  children: children ?? label
1366
1451
  });
1367
- let finalTag;
1368
- if (paragraph) finalTag = "p";
1369
- else finalTag = tag;
1370
- return renderContent(finalTag);
1371
1452
  };
1372
1453
  const name$1 = `${PKG_NAME}/Text`;
1373
1454
  Component$2.displayName = name$1;
@@ -1387,7 +1468,7 @@ var Text_default = Component$2;
1387
1468
  * helper to clone children with the merged props.
1388
1469
  */
1389
1470
  const Component$1 = ({ children, className, style }) => {
1390
- const mergedClasses = useMemo(() => Array.isArray(className) ? className.join(" ") : className, [className]);
1471
+ const mergedClasses = Array.isArray(className) ? className.join(" ") : className;
1391
1472
  const finalProps = {};
1392
1473
  if (style) finalProps.style = style;
1393
1474
  if (mergedClasses) finalProps.className = mergedClasses;
@@ -21,10 +21,7 @@ const IS_DEVELOPMENT = process.env.NODE_ENV !== "production";
21
21
  * fill remaining space between before and after.
22
22
  */
23
23
  const { styled: styled$2, css: css$2, component: component$1 } = config;
24
- const equalColsCSS = `
25
- flex: 1;
26
- `;
27
- const typeContentCSS = `
24
+ const FLEX_1 = `
28
25
  flex: 1;
29
26
  `;
30
27
  const gapDimensions = {
@@ -56,7 +53,7 @@ const styles$2 = ({ css, theme: t, rootSize }) => css`
56
53
  alignY: t.alignY
57
54
  })};
58
55
 
59
- ${t.equalCols && equalColsCSS};
56
+ ${t.equalCols && FLEX_1};
60
57
 
61
58
  ${t.gap && t.contentType && calculateGap({
62
59
  direction: t.parentDirection,
@@ -73,7 +70,7 @@ const StyledComponent = styled$2(component$1)`
73
70
  align-self: stretch;
74
71
  flex-wrap: wrap;
75
72
 
76
- ${({ $contentType }) => $contentType === "content" && typeContentCSS};
73
+ ${({ $contentType }) => $contentType === "content" && FLEX_1};
77
74
 
78
75
  ${makeItResponsive({
79
76
  key: "$element",
@@ -124,11 +121,11 @@ const Component$6 = ({ contentType, tag, parentDirection, direction, alignX, ali
124
121
  children: render(children)
125
122
  });
126
123
  };
127
- var component_default = memo(Component$6);
124
+ var component_default$1 = memo(Component$6);
128
125
 
129
126
  //#endregion
130
127
  //#region src/helpers/Content/index.ts
131
- var Content_default = component_default;
128
+ var Content_default = component_default$1;
132
129
 
133
130
  //#endregion
134
131
  //#region src/helpers/Wrapper/styled.ts
@@ -190,22 +187,19 @@ var styled_default$1 = styled$1(component)`
190
187
  * (parent + child Styled) because these HTML elements do not natively
191
188
  * support `display: flex` consistently across browsers.
192
189
  */
193
- const DEV_PROPS = IS_DEVELOPMENT ? { "data-vl-element": "Element" } : {};
190
+ const DEV_PROPS = IS_DEVELOPMENT ? { "data-vl-element": "Element" } : null;
194
191
  const Component$5 = ({ children, ref, tag, block, extendCss, direction, alignX, alignY, equalCols, isInline, ...props }) => {
195
- const COMMON_PROPS = {
196
- ...props,
197
- ...DEV_PROPS,
198
- ref,
199
- as: tag
200
- };
201
- const normalElement = useMemo(() => ({
202
- block,
203
- direction,
204
- alignX,
205
- alignY,
206
- equalCols,
207
- extraStyles: extendCss
208
- }), [
192
+ const $element = useMemo(() => {
193
+ return {
194
+ block,
195
+ direction,
196
+ alignX,
197
+ alignY,
198
+ equalCols,
199
+ extraStyles: extendCss
200
+ };
201
+ }, [
202
+ false,
209
203
  block,
210
204
  direction,
211
205
  alignX,
@@ -213,33 +207,20 @@ const Component$5 = ({ children, ref, tag, block, extendCss, direction, alignX,
213
207
  equalCols,
214
208
  extendCss
215
209
  ]);
216
- useMemo(() => ({
217
- parentFix: true,
218
- block,
219
- extraStyles: extendCss
220
- }), [block, extendCss]);
221
- useMemo(() => ({
222
- childFix: true,
223
- direction,
224
- alignX,
225
- alignY,
226
- equalCols
227
- }), [
228
- direction,
229
- alignX,
230
- alignY,
231
- equalCols
232
- ]);
233
210
  return /* @__PURE__ */ jsx(styled_default$1, {
234
- ...COMMON_PROPS,
235
- $element: normalElement,
211
+ ...props,
212
+ ...DEV_PROPS,
213
+ ref,
214
+ as: tag,
215
+ $element,
236
216
  children
237
217
  });
238
218
  };
219
+ var component_default = memo(Component$5);
239
220
 
240
221
  //#endregion
241
222
  //#region src/helpers/Wrapper/index.ts
242
- var Wrapper_default = Component$5;
223
+ var Wrapper_default = component_default;
243
224
 
244
225
  //#endregion
245
226
  //#region src/Element/component.tsx
@@ -255,6 +236,7 @@ const defaultDirection = "inline";
255
236
  const defaultContentDirection = "rows";
256
237
  const defaultAlignX = "left";
257
238
  const defaultAlignY = "center";
239
+ const AS_RESET = { as: void 0 };
258
240
  const Component$4 = ({ innerRef, ref, tag, label, content, children, beforeContent, afterContent, equalBeforeAfter, block, equalCols, gap, direction, alignX = defaultAlignX, alignY = defaultAlignY, css, contentCss, beforeContentCss, afterContentCss, contentDirection = defaultContentDirection, contentAlignX = defaultAlignX, contentAlignY = defaultAlignY, beforeContentDirection = defaultDirection, beforeContentAlignX = defaultAlignX, beforeContentAlignY = defaultAlignY, afterContentDirection = defaultDirection, afterContentAlignX = defaultAlignX, afterContentAlignY = defaultAlignY, ...props }) => {
259
241
  const isSimpleElement = !beforeContent && !afterContent;
260
242
  const CHILDREN = children ?? content ?? label;
@@ -286,18 +268,22 @@ const Component$4 = ({ innerRef, ref, tag, label, content, children, beforeConte
286
268
  ]);
287
269
  const equalizeRef = useRef(null);
288
270
  const externalRef = ref ?? innerRef;
271
+ const latestExternalRef = useRef(externalRef);
272
+ latestExternalRef.current = externalRef;
289
273
  const mergedRef = useCallback((node) => {
290
274
  equalizeRef.current = node;
291
- if (typeof externalRef === "function") externalRef(node);
292
- else if (externalRef != null) externalRef.current = node;
293
- }, [externalRef]);
275
+ const r = latestExternalRef.current;
276
+ if (typeof r === "function") r(node);
277
+ else if (r != null) r.current = node;
278
+ }, []);
294
279
  useLayoutEffect(() => {}, [
295
280
  equalBeforeAfter,
296
281
  beforeContent,
297
282
  afterContent,
298
283
  direction
299
284
  ]);
300
- const WRAPPER_PROPS = {
285
+ return /* @__PURE__ */ jsxs(Wrapper_default, {
286
+ ...props,
301
287
  ref: mergedRef,
302
288
  extendCss: css,
303
289
  tag,
@@ -305,12 +291,8 @@ const Component$4 = ({ innerRef, ref, tag, label, content, children, beforeConte
305
291
  direction: wrapperDirection,
306
292
  alignX: wrapperAlignX,
307
293
  alignY: wrapperAlignY,
308
- as: void 0
309
- };
310
- return /* @__PURE__ */ jsxs(Wrapper_default, {
311
- ...props,
312
- ...WRAPPER_PROPS,
313
294
  isInline,
295
+ ...AS_RESET,
314
296
  children: [
315
297
  beforeContent && /* @__PURE__ */ jsx(Content_default, {
316
298
  tag: SUB_TAG,
@@ -351,13 +333,14 @@ const Component$4 = ({ innerRef, ref, tag, label, content, children, beforeConte
351
333
  });
352
334
  };
353
335
  const name$3 = `${PKG_NAME}/Element`;
354
- Component$4.displayName = name$3;
355
- Component$4.pkgName = PKG_NAME;
356
- Component$4.VITUS_LABS__COMPONENT = name$3;
336
+ const MemoComponent = memo(Component$4);
337
+ MemoComponent.displayName = name$3;
338
+ MemoComponent.pkgName = PKG_NAME;
339
+ MemoComponent.VITUS_LABS__COMPONENT = name$3;
357
340
 
358
341
  //#endregion
359
342
  //#region src/Element/index.ts
360
- var Element_default = Component$4;
343
+ var Element_default = MemoComponent;
361
344
 
362
345
  //#endregion
363
346
  //#region src/helpers/Iterator/component.tsx
@@ -415,18 +398,41 @@ const flattenChildren = (children) => {
415
398
  if (isFragment(children)) return children.props.children;
416
399
  return [children];
417
400
  };
418
- /** Drop nullish entries and empty objects (matches legacy behavior). */
419
- const filterValidItems = (data) => data.filter((item) => item != null && !(typeof item === "object" && isEmpty(item)));
420
- /** Determine if the array is uniformly simple (string/number) or complex (object). Mixed → null. */
421
- const detectKind = (items) => {
401
+ const classifyItem = (item) => {
402
+ const t = typeof item;
403
+ if (t === "string" || t === "number") return "simple";
404
+ if (t === "object") return "complex";
405
+ return null;
406
+ };
407
+ /**
408
+ * Single-pass: filter out nullish + empty-object entries and detect whether
409
+ * the surviving items form a uniformly simple (string/number) or complex
410
+ * (object) collection. Mixed shapes → kind `null`. Replaces the prior
411
+ * `filterValidItems` + `detectKind` two-pass which allocated an intermediate
412
+ * filtered array; this fuses both into one scan.
413
+ */
414
+ const filterAndDetectKind = (data) => {
415
+ const items = [];
422
416
  let kind = null;
423
- for (const item of items) {
424
- const t = typeof item === "string" || typeof item === "number" ? "simple" : typeof item === "object" ? "complex" : null;
425
- if (t === null) return null;
417
+ for (const item of data) {
418
+ if (item == null) continue;
419
+ if (typeof item === "object" && isEmpty(item)) continue;
420
+ items.push(item);
421
+ const t = classifyItem(item);
422
+ if (t === null) return {
423
+ items,
424
+ kind: null
425
+ };
426
426
  if (kind === null) kind = t;
427
- else if (kind !== t) return null;
427
+ else if (kind !== t) return {
428
+ items,
429
+ kind: null
430
+ };
428
431
  }
429
- return kind;
432
+ return {
433
+ items,
434
+ kind
435
+ };
430
436
  };
431
437
  const objectKey = (item, index, itemKey) => {
432
438
  if (!itemKey) return item.key ?? item.id ?? item.itemId ?? index;
@@ -465,10 +471,8 @@ const buildObjectSpecs = (items, component, itemKey) => items.map((item, i) => {
465
471
  };
466
472
  });
467
473
  const buildDataSpecs = (data, component, valueName, itemKey) => {
468
- const items = filterValidItems(data);
469
- if (items.length === 0) return null;
470
- const kind = detectKind(items);
471
- if (!kind) return null;
474
+ const { items, kind } = filterAndDetectKind(data);
475
+ if (items.length === 0 || !kind) return null;
472
476
  return kind === "simple" ? buildSimpleSpecs(items, component, valueName, itemKey) : buildObjectSpecs(items, component, itemKey);
473
477
  };
474
478
  const Component$3 = ({ itemKey, valueName, children, component, data, wrapComponent: Wrapper, wrapProps, itemProps }) => {
@@ -499,12 +503,13 @@ var Iterator_default = Iterator;
499
503
  * is wrapped in an Element that receives all non-iterator props (e.g.,
500
504
  * layout, alignment, css), allowing the list to be styled as a single block.
501
505
  */
506
+ const RESERVED_PROPS_SET = new Set(Iterator_default.RESERVED_PROPS);
502
507
  const Component$2 = ({ rootElement = false, ref, ...props }) => {
503
508
  const renderedList = /* @__PURE__ */ jsx(Iterator_default, { ...pick(props, Iterator_default.RESERVED_PROPS) });
504
509
  if (!rootElement) return renderedList;
505
510
  return /* @__PURE__ */ jsx(Element_default, {
506
511
  ref,
507
- ...omit(props, Iterator_default.RESERVED_PROPS),
512
+ ...omit(props, RESERVED_PROPS_SET),
508
513
  children: renderedList
509
514
  });
510
515
  };
@@ -543,15 +548,13 @@ var styled_default = styled(textComponent)`
543
548
  //#endregion
544
549
  //#region src/Text/component.tsx
545
550
  const Component$1 = ({ paragraph, label, children, tag, css, ref, ...props }) => {
546
- const renderContent = (as = void 0) => /* @__PURE__ */ jsx(styled_default, {
551
+ return /* @__PURE__ */ jsx(styled_default, {
547
552
  ref,
548
- as,
553
+ as: void 0,
549
554
  $text: { extraStyles: css },
550
555
  ...props,
551
556
  children: children ?? label
552
557
  });
553
- let finalTag;
554
- return renderContent(finalTag);
555
558
  };
556
559
  const name$1 = `${PKG_NAME}/Text`;
557
560
  Component$1.displayName = name$1;
@@ -571,7 +574,7 @@ var Text_default = Component$1;
571
574
  * helper to clone children with the merged props.
572
575
  */
573
576
  const Component = ({ children, className, style }) => {
574
- const mergedClasses = useMemo(() => Array.isArray(className) ? className.join(" ") : className, [className]);
577
+ const mergedClasses = Array.isArray(className) ? className.join(" ") : className;
575
578
  const finalProps = {};
576
579
  if (style) finalProps.style = style;
577
580
  if (mergedClasses) finalProps.className = mergedClasses;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitus-labs/elements",
3
- "version": "2.6.1",
3
+ "version": "2.7.0",
4
4
  "license": "MIT",
5
5
  "author": "Vit Bokisch <vit@bokisch.cz>",
6
6
  "maintainers": [
@@ -49,6 +49,7 @@
49
49
  "prepublish": "bun run build",
50
50
  "build": "bun run vl_rolldown_build",
51
51
  "build:watch": "bun run vl_rolldown_build-watch",
52
+ "bench": "bun benchmarks/render-bench.tsx",
52
53
  "lint": "biome check src/",
53
54
  "test": "vitest run",
54
55
  "test:coverage": "vitest run --coverage",
@@ -57,27 +58,27 @@
57
58
  "typecheck": "tsc --noEmit"
58
59
  },
59
60
  "peerDependencies": {
60
- "@vitus-labs/core": "^2.6.1",
61
- "@vitus-labs/unistyle": "^2.6.1",
61
+ "@vitus-labs/core": "^2.7.0",
62
+ "@vitus-labs/unistyle": "^2.7.0",
62
63
  "react": ">= 19",
63
- "react-dom": ">= 19",
64
- "react-native": ">= 0.76"
64
+ "react-dom": ">= 19"
65
65
  },
66
66
  "peerDependenciesMeta": {
67
67
  "react-dom": {
68
68
  "optional": true
69
- },
70
- "react-native": {
71
- "optional": true
72
69
  }
73
70
  },
74
71
  "devDependencies": {
72
+ "@vitus-labs/connector-styler": "workspace:*",
75
73
  "@vitus-labs/core": "workspace:*",
76
74
  "@vitus-labs/rocketstyle": "workspace:*",
77
- "@vitus-labs/tools-rolldown": "2.3.1",
78
- "@vitus-labs/tools-storybook": "2.3.1",
79
- "@vitus-labs/tools-typescript": "2.3.1",
80
- "@vitus-labs/unistyle": "workspace:*"
75
+ "@vitus-labs/styler": "workspace:*",
76
+ "@vitus-labs/tools-rolldown": "2.5.0",
77
+ "@vitus-labs/tools-storybook": "2.5.0",
78
+ "@vitus-labs/tools-typescript": "2.5.0",
79
+ "@vitus-labs/unistyle": "workspace:*",
80
+ "jsdom": "^29.1.1",
81
+ "tinybench": "^6.0.1"
81
82
  },
82
83
  "dependencies": {
83
84
  "react-is": "^19.2.4"