@vitus-labs/elements 2.0.0-beta.0 → 2.0.0-beta.1

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
@@ -1,7 +1,7 @@
1
1
  import { Provider } from "@vitus-labs/unistyle";
2
2
  import { BreakpointKeys, HTMLTags, HTMLTextTags, config, render } from "@vitus-labs/core";
3
- import * as react from "react";
4
- import { ComponentType, FC, ForwardRefExoticComponent, ForwardedRef, PropsWithoutRef, ReactNode, RefAttributes } from "react";
3
+ import * as _$react from "react";
4
+ import { ComponentType, FC, ForwardRefExoticComponent, ForwardedRef, ReactElement, ReactNode } from "react";
5
5
 
6
6
  //#region src/types.d.ts
7
7
  type ExtractNullableKeys<T> = { [P in keyof T as T[P] extends null | undefined ? never : P]: T[P] };
@@ -24,8 +24,9 @@ type Direction = ContentDirection | ContentDirection[] | Partial<Record<Breakpoi
24
24
  type ResponsiveBoolType = ContentBoolean | ContentBoolean[] | Partial<Record<BreakpointKeys, ContentBoolean>>;
25
25
  type Responsive = ContentSimpleValue | ContentSimpleValue[] | Partial<Record<BreakpointKeys, number | string>>;
26
26
  type ExtendCss = Css | Css[] | Partial<Record<BreakpointKeys, Css>>;
27
- type VLForwardedComponent<P extends Record<string, unknown> = {}> = ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<any>> & VLStatic;
28
- type VLComponent<P extends Record<string, any> = {}> = FC<P> & VLStatic;
27
+ type VLComponent<P extends Record<string, any> = {}> = ((props: P & {
28
+ ref?: any;
29
+ }) => ReactElement | null) & VLStatic;
29
30
  interface VLStatic {
30
31
  /**
31
32
  * React displayName
@@ -291,7 +292,9 @@ type Props = Partial<{
291
292
  */
292
293
  afterContentCss: ExtendCss;
293
294
  }>;
294
- type VLElement<P extends Record<string, unknown> = {}> = ForwardRefExoticComponent<PropsWithoutRef<Props & P> & RefAttributes<any>> & VLStatic;
295
+ type VLElement<P extends Record<string, unknown> = {}> = ((props: Props & P & {
296
+ ref?: any;
297
+ }) => ReactElement | null) & VLStatic;
295
298
  //#endregion
296
299
  //#region src/Element/component.d.ts
297
300
  declare const Component: VLElement;
@@ -385,10 +388,13 @@ declare const Component$3: FC<Context & {
385
388
  children: ReactNode;
386
389
  }>;
387
390
  //#endregion
388
- //#region src/Overlay/useOverlay.d.ts
391
+ //#region src/Overlay/positionMath.d.ts
389
392
  type Align$1 = 'bottom' | 'top' | 'left' | 'right';
390
393
  type AlignX$2 = 'left' | 'center' | 'right';
391
394
  type AlignY$2 = 'bottom' | 'top' | 'center';
395
+ type OverlayType = 'dropdown' | 'tooltip' | 'popover' | 'modal' | 'custom';
396
+ //#endregion
397
+ //#region src/Overlay/useOverlay.d.ts
392
398
  type UseOverlayProps = Partial<{
393
399
  /**
394
400
  * Defines default state whether **Overlay** component should be active.
@@ -412,7 +418,7 @@ type UseOverlayProps = Partial<{
412
418
  * has different positioning calculations than others.
413
419
  * @defaultValue `dropdown`
414
420
  */
415
- type: 'dropdown' | 'tooltip' | 'popover' | 'modal' | 'custom';
421
+ type: OverlayType;
416
422
  /**
417
423
  * Defines how `content` is treated regarding CSS positioning.
418
424
  * @defaultValue `fixed`
@@ -461,6 +467,12 @@ type UseOverlayProps = Partial<{
461
467
  * @defaultValue `true`
462
468
  */
463
469
  closeOnEsc: boolean;
470
+ /**
471
+ * Delay in milliseconds before hiding content on hover leave. Bridges the
472
+ * gap between trigger and content elements to prevent flicker.
473
+ * @defaultValue `100`
474
+ */
475
+ hoverDelay: number;
464
476
  /**
465
477
  * When set to `true`, **Overlay** is automatically closed and is blocked for
466
478
  * being opened.
@@ -491,11 +503,12 @@ declare const useOverlay: ({
491
503
  throttleDelay,
492
504
  parentContainer,
493
505
  closeOnEsc,
506
+ hoverDelay,
494
507
  disabled,
495
508
  onOpen,
496
509
  onClose
497
510
  }?: Partial<UseOverlayProps>) => {
498
- triggerRef: react.RefObject<HTMLElement | null>;
511
+ triggerRef: _$react.RefObject<HTMLElement | null>;
499
512
  contentRef: (node: HTMLElement | null) => void;
500
513
  active: boolean;
501
514
  align: Align$1;
@@ -506,8 +519,8 @@ declare const useOverlay: ({
506
519
  blocked: boolean;
507
520
  setBlocked: () => void;
508
521
  setUnblocked: () => void;
509
- Provider: react.FC<Context & {
510
- children: react.ReactNode;
522
+ Provider: _$react.FC<Context & {
523
+ children: _$react.ReactNode;
511
524
  }>;
512
525
  };
513
526
  //#endregion
@@ -601,7 +614,7 @@ type Props$5 = Partial<{
601
614
  */
602
615
  css: ExtendCss;
603
616
  }>;
604
- declare const Component$5: VLForwardedComponent<Props$5> & {
617
+ declare const Component$5: VLComponent<Props$5> & {
605
618
  isText?: true;
606
619
  };
607
620
  //#endregion
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Provider, alignContent, extendCss, makeItResponsive, value } from "@vitus-labs/unistyle";
2
2
  import { config, context, isEmpty, omit, pick, render, throttle } from "@vitus-labs/core";
3
- import { Children, createContext, forwardRef, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ import { Children, createContext, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { isFragment } from "react-is";
6
6
  import { createPortal } from "react-dom";
@@ -214,13 +214,12 @@ const isWebFixNeeded = (tag) => {
214
214
  //#region src/helpers/Wrapper/component.tsx
215
215
  /**
216
216
  * Wrapper component that serves as the outermost styled container for Element.
217
- * Uses forwardRef for ref forwarding to the underlying DOM node. On web, it
218
- * detects button/fieldset/legend tags and applies a two-layer flex fix
217
+ * On web, it detects button/fieldset/legend tags and applies a two-layer flex fix
219
218
  * (parent + child Styled) because these HTML elements do not natively
220
219
  * support `display: flex` consistently across browsers.
221
220
  */
222
221
  const DEV_PROPS = IS_DEVELOPMENT ? { "data-vl-element": "Element" } : {};
223
- const Component$8 = forwardRef(({ children, tag, block, extendCss, direction, alignX, alignY, equalCols, isInline, ...props }, ref) => {
222
+ const Component$8 = ({ children, ref, tag, block, extendCss, direction, alignX, alignY, equalCols, isInline, ...props }) => {
224
223
  const COMMON_PROPS = {
225
224
  ...props,
226
225
  ...DEV_PROPS,
@@ -276,7 +275,7 @@ const Component$8 = forwardRef(({ children, tag, block, extendCss, direction, al
276
275
  children
277
276
  })
278
277
  });
279
- });
278
+ };
280
279
 
281
280
  //#endregion
282
281
  //#region src/helpers/Wrapper/index.ts
@@ -386,7 +385,7 @@ const defaultDirection = "inline";
386
385
  const defaultContentDirection = "rows";
387
386
  const defaultAlignX = "left";
388
387
  const defaultAlignY = "center";
389
- const Component$7 = forwardRef(({ innerRef, 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 }, ref) => {
388
+ 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 }) => {
390
389
  const shouldBeEmpty = !!props.dangerouslySetInnerHTML || getShouldBeEmpty(tag);
391
390
  const isSimpleElement = !beforeContent && !afterContent;
392
391
  const CHILDREN = children ?? content ?? label;
@@ -425,7 +424,13 @@ const Component$7 = forwardRef(({ innerRef, tag, label, content, children, befor
425
424
  }, [externalRef]);
426
425
  useLayoutEffect(() => {
427
426
  if (!equalBeforeAfter || !beforeContent || !afterContent) return;
428
- if (equalizeRef.current) equalize(equalizeRef.current, direction);
427
+ const node = equalizeRef.current;
428
+ if (!node) return;
429
+ equalize(node, direction);
430
+ if (typeof ResizeObserver === "undefined") return;
431
+ const observer = new ResizeObserver(() => equalize(node, direction));
432
+ observer.observe(node);
433
+ return () => observer.disconnect();
429
434
  }, [
430
435
  equalBeforeAfter,
431
436
  beforeContent,
@@ -488,7 +493,7 @@ const Component$7 = forwardRef(({ innerRef, tag, label, content, children, befor
488
493
  })
489
494
  ]
490
495
  });
491
- });
496
+ };
492
497
  const name$5 = `${PKG_NAME}/Element`;
493
498
  Component$7.displayName = name$5;
494
499
  Component$7.pkgName = PKG_NAME;
@@ -508,27 +513,6 @@ var Element_default = Component$7;
508
513
  * wrapped with `wrapComponent`. Children always take priority over the
509
514
  * component+data prop pattern.
510
515
  */
511
- const classifyData = (data) => {
512
- const items = data.filter((item) => item != null && !(typeof item === "object" && isEmpty(item)));
513
- if (items.length === 0) return null;
514
- let isSimple = true;
515
- let isComplex = true;
516
- for (const item of items) if (typeof item === "string" || typeof item === "number") isComplex = false;
517
- else if (typeof item === "object") isSimple = false;
518
- else {
519
- isSimple = false;
520
- isComplex = false;
521
- }
522
- if (isSimple) return {
523
- type: "simple",
524
- data: items
525
- };
526
- if (isComplex) return {
527
- type: "complex",
528
- data: items
529
- };
530
- return null;
531
- };
532
516
  const RESERVED_PROPS = [
533
517
  "children",
534
518
  "component",
@@ -539,7 +523,7 @@ const RESERVED_PROPS = [
539
523
  "itemProps",
540
524
  "wrapProps"
541
525
  ];
542
- const attachItemProps = ({ i, length }) => {
526
+ const buildExtendedProps = (i, length) => {
543
527
  const position = i + 1;
544
528
  return {
545
529
  index: i,
@@ -550,106 +534,96 @@ const attachItemProps = ({ i, length }) => {
550
534
  position
551
535
  };
552
536
  };
553
- const Component$6 = (props) => {
554
- const { itemKey, valueName, children, component, data, wrapComponent: Wrapper, wrapProps, itemProps } = props;
555
- const injectItemProps = useMemo(() => typeof itemProps === "function" ? itemProps : () => itemProps, [itemProps]);
556
- const injectWrapItemProps = useMemo(() => typeof wrapProps === "function" ? wrapProps : () => wrapProps, [wrapProps]);
557
- const getKey = useCallback((item, index) => {
558
- if (typeof itemKey === "function") return itemKey(item, index);
559
- return index;
560
- }, [itemKey]);
561
- const renderChild = (child, total = 1, i = 0) => {
562
- if (!itemProps && !Wrapper) return child;
563
- const extendedProps = attachItemProps({
564
- i,
565
- length: total
566
- });
567
- const finalItemProps = itemProps ? injectItemProps({}, extendedProps) : {};
568
- if (Wrapper) return /* @__PURE__ */ jsx(Wrapper, {
569
- ...wrapProps ? injectWrapItemProps({}, extendedProps) : {},
570
- children: render(child, finalItemProps)
571
- }, i);
572
- return render(child, {
573
- key: i,
574
- ...finalItemProps
575
- });
576
- };
577
- const renderChildren = () => {
578
- if (!children) return null;
579
- if (Array.isArray(children)) return Children.map(children, (item, i) => renderChild(item, children.length, i));
580
- if (isFragment(children)) {
581
- const fragmentChildren = children.props.children;
582
- const childrenLength = fragmentChildren.length;
583
- return fragmentChildren.map((item, i) => renderChild(item, childrenLength, i));
584
- }
585
- return renderChild(children);
586
- };
587
- const renderSimpleArray = (data) => {
588
- const { length } = data;
589
- if (length === 0) return null;
590
- return data.map((item, i) => {
591
- const key = getKey(item, i);
592
- const keyName = valueName ?? "children";
593
- const extendedProps = attachItemProps({
594
- i,
595
- length
596
- });
597
- const finalItemProps = {
598
- ...itemProps ? injectItemProps({ [keyName]: item }, extendedProps) : {},
599
- [keyName]: item
600
- };
601
- if (Wrapper) return /* @__PURE__ */ jsx(Wrapper, {
602
- ...wrapProps ? injectWrapItemProps({ [keyName]: item }, extendedProps) : {},
603
- children: render(component, finalItemProps)
604
- }, key);
605
- return render(component, {
606
- key,
607
- ...finalItemProps
608
- });
609
- });
610
- };
611
- const getObjectKey = (item, index) => {
612
- if (!itemKey) return item.key ?? item.id ?? item.itemId ?? index;
613
- if (typeof itemKey === "function") return itemKey(item, index);
614
- if (typeof itemKey === "string") return item[itemKey];
615
- return index;
537
+ const resolveCallback = (cb, source, ext) => {
538
+ if (!cb) return {};
539
+ return typeof cb === "function" ? cb(source, ext) : cb;
540
+ };
541
+ const renderSpec = (spec, ext, itemProps, wrapProps, Wrapper) => {
542
+ const finalItemProps = {
543
+ ...resolveCallback(itemProps, spec.source, ext),
544
+ ...spec.base
616
545
  };
617
- const renderComplexArray = (data) => {
618
- const { length } = data;
619
- if (length === 0) return null;
620
- return data.map((item, i) => {
621
- const { component: itemComponent, ...restItem } = item;
622
- const renderItem = itemComponent ?? component;
623
- const key = getObjectKey(restItem, i);
624
- const extendedProps = attachItemProps({
625
- i,
626
- length
627
- });
628
- const finalItemProps = {
629
- ...itemProps ? injectItemProps(item, extendedProps) : {},
630
- ...restItem
631
- };
632
- if (Wrapper && !itemComponent) return /* @__PURE__ */ jsx(Wrapper, {
633
- ...wrapProps ? injectWrapItemProps(item, extendedProps) : {},
634
- children: render(renderItem, finalItemProps)
635
- }, key);
636
- return render(renderItem, {
637
- key,
638
- ...finalItemProps
639
- });
640
- });
546
+ if (Wrapper && !spec.skipWrap) return /* @__PURE__ */ jsx(Wrapper, {
547
+ ...resolveCallback(wrapProps, spec.source, ext),
548
+ children: spec.isNode ? render(spec.target, finalItemProps) : render(spec.target, finalItemProps)
549
+ }, spec.key);
550
+ const propsWithKey = {
551
+ key: spec.key,
552
+ ...finalItemProps
641
553
  };
642
- const renderItems = () => {
643
- if (children) return renderChildren();
644
- if (component && Array.isArray(data)) {
645
- const classified = classifyData(data);
646
- if (!classified) return null;
647
- if (classified.type === "simple") return renderSimpleArray(classified.data);
648
- return renderComplexArray(classified.data);
649
- }
650
- return null;
554
+ return spec.isNode ? render(spec.target, propsWithKey) : render(spec.target, propsWithKey);
555
+ };
556
+ /** Normalize children (single, array, or fragment) into an array of nodes. */
557
+ const flattenChildren = (children) => {
558
+ if (Array.isArray(children)) return children;
559
+ if (isFragment(children)) return children.props.children;
560
+ return [children];
561
+ };
562
+ /** Drop nullish entries and empty objects (matches legacy behavior). */
563
+ const filterValidItems = (data) => data.filter((item) => item != null && !(typeof item === "object" && isEmpty(item)));
564
+ /** Determine if the array is uniformly simple (string/number) or complex (object). Mixed → null. */
565
+ const detectKind = (items) => {
566
+ let kind = null;
567
+ for (const item of items) {
568
+ const t = typeof item === "string" || typeof item === "number" ? "simple" : typeof item === "object" ? "complex" : null;
569
+ if (t === null) return null;
570
+ if (kind === null) kind = t;
571
+ else if (kind !== t) return null;
572
+ }
573
+ return kind;
574
+ };
575
+ const objectKey = (item, index, itemKey) => {
576
+ if (!itemKey) return item.key ?? item.id ?? item.itemId ?? index;
577
+ if (typeof itemKey === "function") return itemKey(item, index);
578
+ if (typeof itemKey === "string") return item[itemKey] ?? index;
579
+ return index;
580
+ };
581
+ const buildChildrenSpecs = (children) => flattenChildren(children).map((node, i) => ({
582
+ key: i,
583
+ target: node,
584
+ source: {},
585
+ base: {},
586
+ isNode: true,
587
+ skipWrap: false
588
+ }));
589
+ const buildSimpleSpecs = (items, component, valueName, itemKey) => {
590
+ const keyName = valueName ?? "children";
591
+ return items.map((value, i) => ({
592
+ key: typeof itemKey === "function" ? itemKey(value, i) : i,
593
+ target: component,
594
+ source: { [keyName]: value },
595
+ base: { [keyName]: value },
596
+ isNode: false,
597
+ skipWrap: false
598
+ }));
599
+ };
600
+ const buildObjectSpecs = (items, component, itemKey) => items.map((item, i) => {
601
+ const { component: itemComponent, ...rest } = item;
602
+ return {
603
+ key: objectKey(rest, i, itemKey),
604
+ target: itemComponent ?? component,
605
+ source: item,
606
+ base: rest,
607
+ isNode: false,
608
+ skipWrap: Boolean(itemComponent)
651
609
  };
652
- return renderItems();
610
+ });
611
+ const buildDataSpecs = (data, component, valueName, itemKey) => {
612
+ const items = filterValidItems(data);
613
+ if (items.length === 0) return null;
614
+ const kind = detectKind(items);
615
+ if (!kind) return null;
616
+ return kind === "simple" ? buildSimpleSpecs(items, component, valueName, itemKey) : buildObjectSpecs(items, component, itemKey);
617
+ };
618
+ const Component$6 = ({ itemKey, valueName, children, component, data, wrapComponent: Wrapper, wrapProps, itemProps }) => {
619
+ let specs = null;
620
+ if (children) {
621
+ if (!Array.isArray(children) && !isFragment(children) && !itemProps && !Wrapper) return children;
622
+ specs = buildChildrenSpecs(children);
623
+ } else if (component && Array.isArray(data)) specs = buildDataSpecs(data, component, valueName, itemKey);
624
+ if (!specs || specs.length === 0) return null;
625
+ const total = specs.length;
626
+ return Children.toArray(specs.map((spec, i) => renderSpec(spec, buildExtendedProps(i, total), itemProps, wrapProps, Wrapper)));
653
627
  };
654
628
  var component_default = Object.assign(memo(Component$6), {
655
629
  isIterator: true,
@@ -669,7 +643,7 @@ var Iterator_default = component_default;
669
643
  * is wrapped in an Element that receives all non-iterator props (e.g.,
670
644
  * layout, alignment, css), allowing the list to be styled as a single block.
671
645
  */
672
- const Component$5 = forwardRef(({ rootElement = false, ...props }, ref) => {
646
+ const Component$5 = ({ rootElement = false, ref, ...props }) => {
673
647
  const renderedList = /* @__PURE__ */ jsx(Iterator_default, { ...pick(props, Iterator_default.RESERVED_PROPS) });
674
648
  if (!rootElement) return renderedList;
675
649
  return /* @__PURE__ */ jsx(Element_default, {
@@ -677,7 +651,7 @@ const Component$5 = forwardRef(({ rootElement = false, ...props }, ref) => {
677
651
  ...omit(props, Iterator_default.RESERVED_PROPS),
678
652
  children: renderedList
679
653
  });
680
- });
654
+ };
681
655
  const name$4 = `${PKG_NAME}/List`;
682
656
  Component$5.displayName = name$4;
683
657
  Component$5.pkgName = PKG_NAME;
@@ -746,14 +720,12 @@ const Component = ({ children, blocked, setBlocked, setUnblocked }) => {
746
720
  };
747
721
 
748
722
  //#endregion
749
- //#region src/Overlay/useOverlay.tsx
723
+ //#region src/Overlay/positionMath.ts
750
724
  /**
751
- * Core hook powering the Overlay component. Manages open/close state, DOM
752
- * event listeners (click, hover, scroll, resize, ESC key), and dynamic
753
- * positioning of overlay content relative to its trigger. Supports dropdown,
754
- * tooltip, popover, and modal types with automatic edge-of-viewport flipping.
755
- * Event handlers are throttled for performance, and nested overlay blocking
756
- * is coordinated through the overlay context.
725
+ * Pure positioning math for the Overlay component. No React, no DOM mutations.
726
+ * Given the current trigger and content rects (plus alignment options), computes
727
+ * the final viewport-relative position and the alignment that was actually used
728
+ * (which may differ from the requested alignment when an edge would be clipped).
757
729
  */
758
730
  const sel = (cond, a, b) => cond ? a : b;
759
731
  const devWarn = (msg) => {
@@ -923,24 +895,192 @@ const processVisibilityEvent = (e, active, openOn, closeOn, isTrigger, isContent
923
895
  else if (closeOn === "clickOnTrigger" && isTrigger(e)) hideContent();
924
896
  else if (closeOn === "clickOutsideContent" && !isContent(e)) hideContent();
925
897
  };
926
- 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, disabled, onOpen, onClose } = {}) => {
898
+
899
+ //#endregion
900
+ //#region src/Overlay/useEscapeKey.ts
901
+ /** Closes the overlay on Escape keypress when `enabled` and `active`. */
902
+ const useEscapeKey = (enabled, active, blocked, hide) => {
903
+ useEffect(() => {
904
+ if (!enabled || !active || blocked) return void 0;
905
+ const onKey = (e) => {
906
+ if (e.key === "Escape") hide();
907
+ };
908
+ window.addEventListener("keydown", onKey);
909
+ return () => window.removeEventListener("keydown", onKey);
910
+ }, [
911
+ enabled,
912
+ active,
913
+ blocked,
914
+ hide
915
+ ]);
916
+ };
917
+
918
+ //#endregion
919
+ //#region src/Overlay/useHoverListeners.ts
920
+ /**
921
+ * Hover-based open/close. Uses mouseenter/mouseleave on trigger + content
922
+ * (instead of window-level mousemove) and a configurable delay to bridge
923
+ * the gap between trigger and content elements without flicker.
924
+ */
925
+ const useHoverListeners = ({ triggerRef, contentRef, isContentLoaded, active, blocked, disabled, openOn, closeOn, hoverDelay, showContent, hideContent }) => {
926
+ const hoverTimeoutRef = useRef(null);
927
+ useEffect(() => {
928
+ if (blocked || disabled || !(openOn === "hover" || closeOn === "hover")) return void 0;
929
+ const trigger = triggerRef.current;
930
+ const content = contentRef.current;
931
+ const clearHoverTimeout = () => {
932
+ if (hoverTimeoutRef.current != null) {
933
+ clearTimeout(hoverTimeoutRef.current);
934
+ hoverTimeoutRef.current = null;
935
+ }
936
+ };
937
+ const scheduleHide = () => {
938
+ clearHoverTimeout();
939
+ hoverTimeoutRef.current = setTimeout(hideContent, hoverDelay);
940
+ };
941
+ const onTriggerEnter = () => {
942
+ clearHoverTimeout();
943
+ if (openOn === "hover" && !active) showContent();
944
+ };
945
+ const onTriggerLeave = () => {
946
+ if (closeOn === "hover" && active) scheduleHide();
947
+ };
948
+ const onContentEnter = () => {
949
+ clearHoverTimeout();
950
+ };
951
+ const onContentLeave = () => {
952
+ if (closeOn === "hover" && active) scheduleHide();
953
+ };
954
+ if (trigger) {
955
+ trigger.addEventListener("mouseenter", onTriggerEnter);
956
+ trigger.addEventListener("mouseleave", onTriggerLeave);
957
+ }
958
+ if (content) {
959
+ content.addEventListener("mouseenter", onContentEnter);
960
+ content.addEventListener("mouseleave", onContentLeave);
961
+ }
962
+ return () => {
963
+ clearHoverTimeout();
964
+ if (trigger) {
965
+ trigger.removeEventListener("mouseenter", onTriggerEnter);
966
+ trigger.removeEventListener("mouseleave", onTriggerLeave);
967
+ }
968
+ if (content) {
969
+ content.removeEventListener("mouseenter", onContentEnter);
970
+ content.removeEventListener("mouseleave", onContentLeave);
971
+ }
972
+ };
973
+ }, [
974
+ active,
975
+ isContentLoaded,
976
+ blocked,
977
+ disabled,
978
+ openOn,
979
+ closeOn,
980
+ hoverDelay,
981
+ showContent,
982
+ hideContent
983
+ ]);
984
+ };
985
+
986
+ //#endregion
987
+ //#region src/Overlay/useScrollReposition.ts
988
+ let modalOverflowCount = 0;
989
+ /**
990
+ * Window-level scroll/resize listeners that reposition active overlays and
991
+ * re-evaluate close-on-scroll behavior. Also manages the body overflow lock
992
+ * for modal overlays (refcounted across nested modals).
993
+ */
994
+ const useWindowReposition = (active, type, handleContentPosition, handleVisibility) => {
995
+ useEffect(() => {
996
+ if (!active) return void 0;
997
+ const shouldSetOverflow = type === "modal";
998
+ const onScroll = (e) => {
999
+ handleContentPosition();
1000
+ handleVisibility(e);
1001
+ };
1002
+ if (shouldSetOverflow) {
1003
+ modalOverflowCount++;
1004
+ if (modalOverflowCount === 1) document.body.style.overflow = "hidden";
1005
+ }
1006
+ window.addEventListener("resize", handleContentPosition);
1007
+ window.addEventListener("scroll", onScroll, { passive: true });
1008
+ return () => {
1009
+ handleContentPosition.cancel();
1010
+ handleVisibility.cancel();
1011
+ if (shouldSetOverflow) {
1012
+ modalOverflowCount--;
1013
+ if (modalOverflowCount === 0) document.body.style.overflow = "";
1014
+ }
1015
+ window.removeEventListener("resize", handleContentPosition);
1016
+ window.removeEventListener("scroll", onScroll);
1017
+ };
1018
+ }, [
1019
+ active,
1020
+ type,
1021
+ handleContentPosition,
1022
+ handleVisibility
1023
+ ]);
1024
+ };
1025
+ /**
1026
+ * Same as `useWindowReposition` but for a custom scrollable ancestor.
1027
+ * Locks the parent's overflow while the overlay is active (unless hover-driven,
1028
+ * which expects the parent to keep scrolling).
1029
+ */
1030
+ const useParentContainerReposition = (active, parentContainer, closeOn, handleContentPosition, handleVisibility) => {
1031
+ useEffect(() => {
1032
+ if (!active || !parentContainer) return void 0;
1033
+ if (closeOn !== "hover") parentContainer.style.overflow = "hidden";
1034
+ const onScroll = (e) => {
1035
+ handleContentPosition();
1036
+ handleVisibility(e);
1037
+ };
1038
+ parentContainer.addEventListener("scroll", onScroll, { passive: true });
1039
+ return () => {
1040
+ parentContainer.style.overflow = "";
1041
+ parentContainer.removeEventListener("scroll", onScroll);
1042
+ };
1043
+ }, [
1044
+ active,
1045
+ parentContainer,
1046
+ closeOn,
1047
+ handleContentPosition,
1048
+ handleVisibility
1049
+ ]);
1050
+ };
1051
+ const useScrollReposition = ({ active, type, parentContainer, closeOn, handleContentPosition, handleVisibility }) => {
1052
+ useWindowReposition(active, type, handleContentPosition, handleVisibility);
1053
+ useParentContainerReposition(active, parentContainer, closeOn, handleContentPosition, handleVisibility);
1054
+ };
1055
+
1056
+ //#endregion
1057
+ //#region src/Overlay/useOverlay.tsx
1058
+ /**
1059
+ * Core hook powering the Overlay component. Manages open/close state, DOM
1060
+ * event listeners (click, hover, scroll, resize, ESC key), and dynamic
1061
+ * positioning of overlay content relative to its trigger. Supports dropdown,
1062
+ * tooltip, popover, and modal types with automatic edge-of-viewport flipping.
1063
+ *
1064
+ * Pure positioning math lives in `./positionMath`. Event-listener concerns
1065
+ * live in dedicated hooks: `./useEscapeKey`, `./useHoverListeners`,
1066
+ * `./useScrollReposition`.
1067
+ */
1068
+ 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 } = {}) => {
927
1069
  const { rootSize } = useContext(context);
928
1070
  const ctx = useOverlayContext();
929
1071
  const [isContentLoaded, setContentLoaded] = useState(false);
930
1072
  const [innerAlignX, setInnerAlignX] = useState(alignX);
931
1073
  const [innerAlignY, setInnerAlignY] = useState(alignY);
932
- const [blocked, handleBlocked] = useState(false);
933
- const [active, handleActive] = useState(isOpen);
1074
+ const [blockedCount, setBlockedCount] = useState(0);
1075
+ const blocked = blockedCount > 0;
1076
+ const [active, setActive] = useState(isOpen);
934
1077
  const triggerRef = useRef(null);
935
1078
  const contentRef = useRef(null);
936
- const setBlocked = useCallback(() => handleBlocked(true), []);
937
- const setUnblocked = useCallback(() => handleBlocked(false), []);
938
- const showContent = useCallback(() => {
939
- handleActive(true);
940
- }, []);
941
- const hideContent = useCallback(() => {
942
- handleActive(false);
943
- }, []);
1079
+ const prevFocusRef = useRef(null);
1080
+ const setBlocked = useCallback(() => setBlockedCount((c) => c + 1), []);
1081
+ const setUnblocked = useCallback(() => setBlockedCount((c) => Math.max(0, c - 1)), []);
1082
+ const showContent = useCallback(() => setActive(true), []);
1083
+ const hideContent = useCallback(() => setActive(false), []);
944
1084
  const getAncestorOffset = useCallback(() => {
945
1085
  if (position !== "absolute" || !contentRef.current) return {
946
1086
  top: 0,
@@ -1009,7 +1149,7 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1009
1149
  const latestHandleVisibility = useRef(handleVisibilityByEventType);
1010
1150
  latestHandleVisibility.current = handleVisibilityByEventType;
1011
1151
  const handleContentPosition = useMemo(() => throttle(() => latestSetContentPosition.current(), throttleDelay), [throttleDelay]);
1012
- const handleClick = handleVisibilityByEventType;
1152
+ const handleClick = useCallback((e) => latestHandleVisibility.current(e), []);
1013
1153
  const handleVisibility = useMemo(() => throttle((e) => latestHandleVisibility.current(e), throttleDelay), [throttleDelay]);
1014
1154
  useEffect(() => {
1015
1155
  setInnerAlignX(alignX);
@@ -1056,62 +1196,30 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1056
1196
  onOpen
1057
1197
  ]);
1058
1198
  useEffect(() => {
1059
- if (!closeOnEsc || !active || blocked) return void 0;
1060
- const handleEscKey = (e) => {
1061
- if (e.key === "Escape") hideContent();
1062
- };
1063
- window.addEventListener("keydown", handleEscKey);
1064
- return () => {
1065
- window.removeEventListener("keydown", handleEscKey);
1066
- };
1199
+ if (type !== "modal") return;
1200
+ if (active && isContentLoaded && contentRef.current) {
1201
+ prevFocusRef.current = document.activeElement;
1202
+ if (contentRef.current.tabIndex < 0) contentRef.current.tabIndex = -1;
1203
+ contentRef.current.focus();
1204
+ }
1205
+ if (!active && prevFocusRef.current) {
1206
+ prevFocusRef.current.focus();
1207
+ prevFocusRef.current = null;
1208
+ }
1067
1209
  }, [
1068
1210
  active,
1069
- blocked,
1070
- closeOnEsc,
1071
- hideContent
1211
+ isContentLoaded,
1212
+ type
1072
1213
  ]);
1073
- useEffect(() => {
1074
- if (!active) return void 0;
1075
- const shouldSetOverflow = type === "modal";
1076
- const onScroll = (e) => {
1077
- handleContentPosition();
1078
- handleVisibility(e);
1079
- };
1080
- if (shouldSetOverflow) document.body.style.overflow = "hidden";
1081
- window.addEventListener("resize", handleContentPosition);
1082
- window.addEventListener("scroll", onScroll, { passive: true });
1083
- return () => {
1084
- handleContentPosition.cancel();
1085
- handleVisibility.cancel();
1086
- if (shouldSetOverflow) document.body.style.overflow = "";
1087
- window.removeEventListener("resize", handleContentPosition);
1088
- window.removeEventListener("scroll", onScroll);
1089
- };
1090
- }, [
1214
+ useEscapeKey(closeOnEsc, active, blocked, hideContent);
1215
+ useScrollReposition({
1091
1216
  active,
1092
1217
  type,
1093
- handleVisibility,
1094
- handleContentPosition
1095
- ]);
1096
- useEffect(() => {
1097
- if (!active || !parentContainer) return void 0;
1098
- if (closeOn !== "hover") parentContainer.style.overflow = "hidden";
1099
- const onScroll = (e) => {
1100
- handleContentPosition();
1101
- handleVisibility(e);
1102
- };
1103
- parentContainer.addEventListener("scroll", onScroll, { passive: true });
1104
- return () => {
1105
- parentContainer.style.overflow = "";
1106
- parentContainer.removeEventListener("scroll", onScroll);
1107
- };
1108
- }, [
1109
- active,
1110
1218
  parentContainer,
1111
1219
  closeOn,
1112
1220
  handleContentPosition,
1113
1221
  handleVisibility
1114
- ]);
1222
+ });
1115
1223
  useEffect(() => {
1116
1224
  if (blocked || disabled) return void 0;
1117
1225
  if (openOn === "click" || [
@@ -1119,9 +1227,7 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1119
1227
  "clickOnTrigger",
1120
1228
  "clickOutsideContent"
1121
1229
  ].includes(closeOn)) window.addEventListener("click", handleClick);
1122
- return () => {
1123
- window.removeEventListener("click", handleClick);
1124
- };
1230
+ return () => window.removeEventListener("click", handleClick);
1125
1231
  }, [
1126
1232
  openOn,
1127
1233
  closeOn,
@@ -1129,63 +1235,19 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1129
1235
  disabled,
1130
1236
  handleClick
1131
1237
  ]);
1132
- const hoverTimeoutRef = useRef(null);
1133
- useEffect(() => {
1134
- if (blocked || disabled || !(openOn === "hover" || closeOn === "hover")) return void 0;
1135
- const trigger = triggerRef.current;
1136
- const content = contentRef.current;
1137
- const clearHoverTimeout = () => {
1138
- if (hoverTimeoutRef.current != null) {
1139
- clearTimeout(hoverTimeoutRef.current);
1140
- hoverTimeoutRef.current = null;
1141
- }
1142
- };
1143
- const scheduleHide = () => {
1144
- clearHoverTimeout();
1145
- hoverTimeoutRef.current = setTimeout(hideContent, 100);
1146
- };
1147
- const onTriggerEnter = () => {
1148
- clearHoverTimeout();
1149
- if (openOn === "hover" && !active) showContent();
1150
- };
1151
- const onTriggerLeave = () => {
1152
- if (closeOn === "hover" && active) scheduleHide();
1153
- };
1154
- const onContentEnter = () => {
1155
- clearHoverTimeout();
1156
- };
1157
- const onContentLeave = () => {
1158
- if (closeOn === "hover" && active) scheduleHide();
1159
- };
1160
- if (trigger) {
1161
- trigger.addEventListener("mouseenter", onTriggerEnter);
1162
- trigger.addEventListener("mouseleave", onTriggerLeave);
1163
- }
1164
- if (content) {
1165
- content.addEventListener("mouseenter", onContentEnter);
1166
- content.addEventListener("mouseleave", onContentLeave);
1167
- }
1168
- return () => {
1169
- clearHoverTimeout();
1170
- if (trigger) {
1171
- trigger.removeEventListener("mouseenter", onTriggerEnter);
1172
- trigger.removeEventListener("mouseleave", onTriggerLeave);
1173
- }
1174
- if (content) {
1175
- content.removeEventListener("mouseenter", onContentEnter);
1176
- content.removeEventListener("mouseleave", onContentLeave);
1177
- }
1178
- };
1179
- }, [
1180
- active,
1238
+ useHoverListeners({
1239
+ triggerRef,
1240
+ contentRef,
1181
1241
  isContentLoaded,
1242
+ active,
1182
1243
  blocked,
1183
1244
  disabled,
1184
1245
  openOn,
1185
1246
  closeOn,
1247
+ hoverDelay,
1186
1248
  showContent,
1187
1249
  hideContent
1188
- ]);
1250
+ });
1189
1251
  return {
1190
1252
  triggerRef,
1191
1253
  contentRef: useCallback((node) => {
@@ -1217,11 +1279,22 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1217
1279
  const IS_BROWSER = typeof window !== "undefined";
1218
1280
  const Component$3 = ({ children, trigger, DOMLocation, triggerRefName = "ref", contentRefName = "ref", ...props }) => {
1219
1281
  const { active, triggerRef, contentRef, showContent, hideContent, align, alignX, alignY, Provider, ...ctx } = useOverlay(props);
1220
- const { openOn, closeOn } = props;
1282
+ const { openOn, closeOn, type } = props;
1283
+ const contentId = useId();
1221
1284
  const passHandlers = useMemo(() => openOn === "manual" || closeOn === "manual" || closeOn === "clickOutsideContent", [openOn, closeOn]);
1285
+ const ariaHasPopup = useMemo(() => {
1286
+ switch (type) {
1287
+ case "modal": return "dialog";
1288
+ case "tooltip": return "true";
1289
+ default: return "menu";
1290
+ }
1291
+ }, [type]);
1222
1292
  return /* @__PURE__ */ jsxs(Fragment, { children: [render(trigger, {
1223
1293
  [triggerRefName]: triggerRef,
1224
1294
  active,
1295
+ "aria-expanded": active,
1296
+ "aria-haspopup": ariaHasPopup,
1297
+ "aria-controls": active ? contentId : void 0,
1225
1298
  ...passHandlers ? {
1226
1299
  showContent,
1227
1300
  hideContent
@@ -1232,6 +1305,9 @@ const Component$3 = ({ children, trigger, DOMLocation, triggerRefName = "ref", c
1232
1305
  ...ctx,
1233
1306
  children: render(children, {
1234
1307
  [contentRefName]: contentRef,
1308
+ id: contentId,
1309
+ role: type === "modal" ? "dialog" : void 0,
1310
+ "aria-modal": type === "modal" ? true : void 0,
1235
1311
  active,
1236
1312
  align,
1237
1313
  alignX,
@@ -1282,7 +1358,7 @@ var styled_default = styled(textComponent)`
1282
1358
 
1283
1359
  //#endregion
1284
1360
  //#region src/Text/component.tsx
1285
- const Component$2 = forwardRef(({ paragraph, label, children, tag, css, ...props }, ref) => {
1361
+ const Component$2 = ({ paragraph, label, children, tag, css, ref, ...props }) => {
1286
1362
  const renderContent = (as = void 0) => /* @__PURE__ */ jsx(styled_default, {
1287
1363
  ref,
1288
1364
  as,
@@ -1294,7 +1370,7 @@ const Component$2 = forwardRef(({ paragraph, label, children, tag, css, ...props
1294
1370
  if (paragraph) finalTag = "p";
1295
1371
  else finalTag = tag;
1296
1372
  return renderContent(finalTag);
1297
- });
1373
+ };
1298
1374
  const name$1 = `${PKG_NAME}/Text`;
1299
1375
  Component$2.displayName = name$1;
1300
1376
  Component$2.pkgName = PKG_NAME;
@@ -1,6 +1,6 @@
1
1
  import { Provider, alignContent, extendCss, makeItResponsive, value } from "@vitus-labs/unistyle";
2
2
  import { config, isEmpty, omit, pick, render } from "@vitus-labs/core";
3
- import { Children, forwardRef, memo, useCallback, useLayoutEffect, useMemo, useRef } from "react";
3
+ import { Children, memo, useCallback, useLayoutEffect, useMemo, useRef } from "react";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  import { isFragment } from "react-is";
6
6
 
@@ -186,13 +186,12 @@ var styled_default$1 = styled$1(component)`
186
186
  //#region src/helpers/Wrapper/component.tsx
187
187
  /**
188
188
  * Wrapper component that serves as the outermost styled container for Element.
189
- * Uses forwardRef for ref forwarding to the underlying DOM node. On web, it
190
- * detects button/fieldset/legend tags and applies a two-layer flex fix
189
+ * On web, it detects button/fieldset/legend tags and applies a two-layer flex fix
191
190
  * (parent + child Styled) because these HTML elements do not natively
192
191
  * support `display: flex` consistently across browsers.
193
192
  */
194
193
  const DEV_PROPS = IS_DEVELOPMENT ? { "data-vl-element": "Element" } : {};
195
- const Component$5 = forwardRef(({ children, tag, block, extendCss, direction, alignX, alignY, equalCols, isInline, ...props }, ref) => {
194
+ const Component$5 = ({ children, ref, tag, block, extendCss, direction, alignX, alignY, equalCols, isInline, ...props }) => {
196
195
  const COMMON_PROPS = {
197
196
  ...props,
198
197
  ...DEV_PROPS,
@@ -236,7 +235,7 @@ const Component$5 = forwardRef(({ children, tag, block, extendCss, direction, al
236
235
  $element: normalElement,
237
236
  children
238
237
  });
239
- });
238
+ };
240
239
 
241
240
  //#endregion
242
241
  //#region src/helpers/Wrapper/index.ts
@@ -256,7 +255,7 @@ const defaultDirection = "inline";
256
255
  const defaultContentDirection = "rows";
257
256
  const defaultAlignX = "left";
258
257
  const defaultAlignY = "center";
259
- const Component$4 = forwardRef(({ innerRef, 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 }, ref) => {
258
+ 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 }) => {
260
259
  const isSimpleElement = !beforeContent && !afterContent;
261
260
  const CHILDREN = children ?? content ?? label;
262
261
  const isInline = false;
@@ -350,7 +349,7 @@ const Component$4 = forwardRef(({ innerRef, tag, label, content, children, befor
350
349
  })
351
350
  ]
352
351
  });
353
- });
352
+ };
354
353
  const name$3 = `${PKG_NAME}/Element`;
355
354
  Component$4.displayName = name$3;
356
355
  Component$4.pkgName = PKG_NAME;
@@ -370,27 +369,6 @@ var Element_default = Component$4;
370
369
  * wrapped with `wrapComponent`. Children always take priority over the
371
370
  * component+data prop pattern.
372
371
  */
373
- const classifyData = (data) => {
374
- const items = data.filter((item) => item != null && !(typeof item === "object" && isEmpty(item)));
375
- if (items.length === 0) return null;
376
- let isSimple = true;
377
- let isComplex = true;
378
- for (const item of items) if (typeof item === "string" || typeof item === "number") isComplex = false;
379
- else if (typeof item === "object") isSimple = false;
380
- else {
381
- isSimple = false;
382
- isComplex = false;
383
- }
384
- if (isSimple) return {
385
- type: "simple",
386
- data: items
387
- };
388
- if (isComplex) return {
389
- type: "complex",
390
- data: items
391
- };
392
- return null;
393
- };
394
372
  const RESERVED_PROPS = [
395
373
  "children",
396
374
  "component",
@@ -401,7 +379,7 @@ const RESERVED_PROPS = [
401
379
  "itemProps",
402
380
  "wrapProps"
403
381
  ];
404
- const attachItemProps = ({ i, length }) => {
382
+ const buildExtendedProps = (i, length) => {
405
383
  const position = i + 1;
406
384
  return {
407
385
  index: i,
@@ -412,106 +390,96 @@ const attachItemProps = ({ i, length }) => {
412
390
  position
413
391
  };
414
392
  };
415
- const Component$3 = (props) => {
416
- const { itemKey, valueName, children, component, data, wrapComponent: Wrapper, wrapProps, itemProps } = props;
417
- const injectItemProps = useMemo(() => typeof itemProps === "function" ? itemProps : () => itemProps, [itemProps]);
418
- const injectWrapItemProps = useMemo(() => typeof wrapProps === "function" ? wrapProps : () => wrapProps, [wrapProps]);
419
- const getKey = useCallback((item, index) => {
420
- if (typeof itemKey === "function") return itemKey(item, index);
421
- return index;
422
- }, [itemKey]);
423
- const renderChild = (child, total = 1, i = 0) => {
424
- if (!itemProps && !Wrapper) return child;
425
- const extendedProps = attachItemProps({
426
- i,
427
- length: total
428
- });
429
- const finalItemProps = itemProps ? injectItemProps({}, extendedProps) : {};
430
- if (Wrapper) return /* @__PURE__ */ jsx(Wrapper, {
431
- ...wrapProps ? injectWrapItemProps({}, extendedProps) : {},
432
- children: render(child, finalItemProps)
433
- }, i);
434
- return render(child, {
435
- key: i,
436
- ...finalItemProps
437
- });
438
- };
439
- const renderChildren = () => {
440
- if (!children) return null;
441
- if (Array.isArray(children)) return Children.map(children, (item, i) => renderChild(item, children.length, i));
442
- if (isFragment(children)) {
443
- const fragmentChildren = children.props.children;
444
- const childrenLength = fragmentChildren.length;
445
- return fragmentChildren.map((item, i) => renderChild(item, childrenLength, i));
446
- }
447
- return renderChild(children);
448
- };
449
- const renderSimpleArray = (data) => {
450
- const { length } = data;
451
- if (length === 0) return null;
452
- return data.map((item, i) => {
453
- const key = getKey(item, i);
454
- const keyName = valueName ?? "children";
455
- const extendedProps = attachItemProps({
456
- i,
457
- length
458
- });
459
- const finalItemProps = {
460
- ...itemProps ? injectItemProps({ [keyName]: item }, extendedProps) : {},
461
- [keyName]: item
462
- };
463
- if (Wrapper) return /* @__PURE__ */ jsx(Wrapper, {
464
- ...wrapProps ? injectWrapItemProps({ [keyName]: item }, extendedProps) : {},
465
- children: render(component, finalItemProps)
466
- }, key);
467
- return render(component, {
468
- key,
469
- ...finalItemProps
470
- });
471
- });
472
- };
473
- const getObjectKey = (item, index) => {
474
- if (!itemKey) return item.key ?? item.id ?? item.itemId ?? index;
475
- if (typeof itemKey === "function") return itemKey(item, index);
476
- if (typeof itemKey === "string") return item[itemKey];
477
- return index;
393
+ const resolveCallback = (cb, source, ext) => {
394
+ if (!cb) return {};
395
+ return typeof cb === "function" ? cb(source, ext) : cb;
396
+ };
397
+ const renderSpec = (spec, ext, itemProps, wrapProps, Wrapper) => {
398
+ const finalItemProps = {
399
+ ...resolveCallback(itemProps, spec.source, ext),
400
+ ...spec.base
478
401
  };
479
- const renderComplexArray = (data) => {
480
- const { length } = data;
481
- if (length === 0) return null;
482
- return data.map((item, i) => {
483
- const { component: itemComponent, ...restItem } = item;
484
- const renderItem = itemComponent ?? component;
485
- const key = getObjectKey(restItem, i);
486
- const extendedProps = attachItemProps({
487
- i,
488
- length
489
- });
490
- const finalItemProps = {
491
- ...itemProps ? injectItemProps(item, extendedProps) : {},
492
- ...restItem
493
- };
494
- if (Wrapper && !itemComponent) return /* @__PURE__ */ jsx(Wrapper, {
495
- ...wrapProps ? injectWrapItemProps(item, extendedProps) : {},
496
- children: render(renderItem, finalItemProps)
497
- }, key);
498
- return render(renderItem, {
499
- key,
500
- ...finalItemProps
501
- });
502
- });
402
+ if (Wrapper && !spec.skipWrap) return /* @__PURE__ */ jsx(Wrapper, {
403
+ ...resolveCallback(wrapProps, spec.source, ext),
404
+ children: spec.isNode ? render(spec.target, finalItemProps) : render(spec.target, finalItemProps)
405
+ }, spec.key);
406
+ const propsWithKey = {
407
+ key: spec.key,
408
+ ...finalItemProps
503
409
  };
504
- const renderItems = () => {
505
- if (children) return renderChildren();
506
- if (component && Array.isArray(data)) {
507
- const classified = classifyData(data);
508
- if (!classified) return null;
509
- if (classified.type === "simple") return renderSimpleArray(classified.data);
510
- return renderComplexArray(classified.data);
511
- }
512
- return null;
410
+ return spec.isNode ? render(spec.target, propsWithKey) : render(spec.target, propsWithKey);
411
+ };
412
+ /** Normalize children (single, array, or fragment) into an array of nodes. */
413
+ const flattenChildren = (children) => {
414
+ if (Array.isArray(children)) return children;
415
+ if (isFragment(children)) return children.props.children;
416
+ return [children];
417
+ };
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) => {
422
+ 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;
426
+ if (kind === null) kind = t;
427
+ else if (kind !== t) return null;
428
+ }
429
+ return kind;
430
+ };
431
+ const objectKey = (item, index, itemKey) => {
432
+ if (!itemKey) return item.key ?? item.id ?? item.itemId ?? index;
433
+ if (typeof itemKey === "function") return itemKey(item, index);
434
+ if (typeof itemKey === "string") return item[itemKey] ?? index;
435
+ return index;
436
+ };
437
+ const buildChildrenSpecs = (children) => flattenChildren(children).map((node, i) => ({
438
+ key: i,
439
+ target: node,
440
+ source: {},
441
+ base: {},
442
+ isNode: true,
443
+ skipWrap: false
444
+ }));
445
+ const buildSimpleSpecs = (items, component, valueName, itemKey) => {
446
+ const keyName = valueName ?? "children";
447
+ return items.map((value, i) => ({
448
+ key: typeof itemKey === "function" ? itemKey(value, i) : i,
449
+ target: component,
450
+ source: { [keyName]: value },
451
+ base: { [keyName]: value },
452
+ isNode: false,
453
+ skipWrap: false
454
+ }));
455
+ };
456
+ const buildObjectSpecs = (items, component, itemKey) => items.map((item, i) => {
457
+ const { component: itemComponent, ...rest } = item;
458
+ return {
459
+ key: objectKey(rest, i, itemKey),
460
+ target: itemComponent ?? component,
461
+ source: item,
462
+ base: rest,
463
+ isNode: false,
464
+ skipWrap: Boolean(itemComponent)
513
465
  };
514
- return renderItems();
466
+ });
467
+ 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;
472
+ return kind === "simple" ? buildSimpleSpecs(items, component, valueName, itemKey) : buildObjectSpecs(items, component, itemKey);
473
+ };
474
+ const Component$3 = ({ itemKey, valueName, children, component, data, wrapComponent: Wrapper, wrapProps, itemProps }) => {
475
+ let specs = null;
476
+ if (children) {
477
+ if (!Array.isArray(children) && !isFragment(children) && !itemProps && !Wrapper) return children;
478
+ specs = buildChildrenSpecs(children);
479
+ } else if (component && Array.isArray(data)) specs = buildDataSpecs(data, component, valueName, itemKey);
480
+ if (!specs || specs.length === 0) return null;
481
+ const total = specs.length;
482
+ return Children.toArray(specs.map((spec, i) => renderSpec(spec, buildExtendedProps(i, total), itemProps, wrapProps, Wrapper)));
515
483
  };
516
484
  var component_default = Object.assign(memo(Component$3), {
517
485
  isIterator: true,
@@ -531,7 +499,7 @@ var Iterator_default = component_default;
531
499
  * is wrapped in an Element that receives all non-iterator props (e.g.,
532
500
  * layout, alignment, css), allowing the list to be styled as a single block.
533
501
  */
534
- const Component$2 = forwardRef(({ rootElement = false, ...props }, ref) => {
502
+ const Component$2 = ({ rootElement = false, ref, ...props }) => {
535
503
  const renderedList = /* @__PURE__ */ jsx(Iterator_default, { ...pick(props, Iterator_default.RESERVED_PROPS) });
536
504
  if (!rootElement) return renderedList;
537
505
  return /* @__PURE__ */ jsx(Element_default, {
@@ -539,7 +507,7 @@ const Component$2 = forwardRef(({ rootElement = false, ...props }, ref) => {
539
507
  ...omit(props, Iterator_default.RESERVED_PROPS),
540
508
  children: renderedList
541
509
  });
542
- });
510
+ };
543
511
  const name$2 = `${PKG_NAME}/List`;
544
512
  Component$2.displayName = name$2;
545
513
  Component$2.pkgName = PKG_NAME;
@@ -574,7 +542,7 @@ var styled_default = styled(textComponent)`
574
542
 
575
543
  //#endregion
576
544
  //#region src/Text/component.tsx
577
- const Component$1 = forwardRef(({ paragraph, label, children, tag, css, ...props }, ref) => {
545
+ const Component$1 = ({ paragraph, label, children, tag, css, ref, ...props }) => {
578
546
  const renderContent = (as = void 0) => /* @__PURE__ */ jsx(styled_default, {
579
547
  ref,
580
548
  as,
@@ -584,7 +552,7 @@ const Component$1 = forwardRef(({ paragraph, label, children, tag, css, ...props
584
552
  });
585
553
  let finalTag;
586
554
  return renderContent(finalTag);
587
- });
555
+ };
588
556
  const name$1 = `${PKG_NAME}/Text`;
589
557
  Component$1.displayName = name$1;
590
558
  Component$1.pkgName = PKG_NAME;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitus-labs/elements",
3
- "version": "2.0.0-beta.0",
3
+ "version": "2.0.0-beta.1",
4
4
  "license": "MIT",
5
5
  "author": "Vit Bokisch <vit@bokisch.cz>",
6
6
  "maintainers": [
@@ -58,8 +58,8 @@
58
58
  "version": "node ../../scripts/sync-peer-deps.mjs"
59
59
  },
60
60
  "peerDependencies": {
61
- "@vitus-labs/core": "2.0.0-alpha.27",
62
- "@vitus-labs/unistyle": "2.0.0-alpha.27",
61
+ "@vitus-labs/core": "2.0.0-beta.1",
62
+ "@vitus-labs/unistyle": "2.0.0-beta.1",
63
63
  "react": ">= 19",
64
64
  "react-dom": ">= 19",
65
65
  "react-native": ">= 0.76"
@@ -73,15 +73,14 @@
73
73
  }
74
74
  },
75
75
  "devDependencies": {
76
- "@vitus-labs/core": "2.0.0-beta.0",
77
- "@vitus-labs/rocketstyle": "2.0.0-beta.0",
78
- "@vitus-labs/tools-rolldown": "1.10.0",
79
- "@vitus-labs/tools-storybook": "1.10.0",
80
- "@vitus-labs/tools-typescript": "1.10.0",
81
- "@vitus-labs/unistyle": "2.0.0-beta.0"
76
+ "@vitus-labs/core": "workspace:*",
77
+ "@vitus-labs/rocketstyle": "workspace:*",
78
+ "@vitus-labs/tools-rolldown": "2.2.0",
79
+ "@vitus-labs/tools-storybook": "2.2.0",
80
+ "@vitus-labs/tools-typescript": "2.1.0",
81
+ "@vitus-labs/unistyle": "workspace:*"
82
82
  },
83
83
  "dependencies": {
84
84
  "react-is": "^19.2.4"
85
- },
86
- "gitHead": "dd8b9f356086ecd8bfb69c87fcad1e8bfa9ab1f4"
85
+ }
87
86
  }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2023-present Vit Bokisch
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.