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

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";
@@ -150,27 +150,25 @@ const childFixCSS = `
150
150
  const parentFixCSS = `
151
151
  flex-direction: column;
152
152
  `;
153
- const fullHeightCSS = `
154
- height: 100%;
155
- `;
156
- const blockCSS = `
157
- align-self: stretch;
158
- width: 100%;
159
- `;
160
- const childFixPosition = (isBlock) => `display: ${isBlock ? "flex" : "inline-flex"};`;
161
153
  const styles$1 = ({ theme: t, css }) => css`
162
- ${t.alignY === "block" && fullHeightCSS};
163
-
164
154
  ${alignContent({
165
155
  direction: t.direction,
166
156
  alignX: t.alignX,
167
157
  alignY: t.alignY
168
158
  })};
169
159
 
170
- ${t.block && blockCSS};
171
- ${t.alignY === "block" && t.block && fullHeightCSS};
160
+ /*
161
+ * Always emit a value for the block-related properties so a responsive
162
+ * theme that flips from \`block: true\` at one breakpoint to \`block: false\`
163
+ * at another resets cleanly. Previously \`align-self\` / \`width\` / \`height\`
164
+ * were only set when the truthy branch matched, which left the prior
165
+ * breakpoint's values cascading through.
166
+ */
167
+ ${`align-self: ${t.block ? "stretch" : "auto"};
168
+ width: ${t.block ? "100%" : "auto"};
169
+ height: ${t.alignY === "block" ? "100%" : "auto"};`};
172
170
 
173
- ${!t.childFix && childFixPosition(t.block)};
171
+ ${!t.childFix && `display: ${t.block ? "flex" : "inline-flex"};`};
174
172
  ${t.parentFix && parentFixCSS};
175
173
 
176
174
  ${t.extraStyles && extendCss(t.extraStyles)};
@@ -214,13 +212,12 @@ const isWebFixNeeded = (tag) => {
214
212
  //#region src/helpers/Wrapper/component.tsx
215
213
  /**
216
214
  * 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
215
+ * On web, it detects button/fieldset/legend tags and applies a two-layer flex fix
219
216
  * (parent + child Styled) because these HTML elements do not natively
220
217
  * support `display: flex` consistently across browsers.
221
218
  */
222
219
  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) => {
220
+ const Component$8 = ({ children, ref, tag, block, extendCss, direction, alignX, alignY, equalCols, isInline, ...props }) => {
224
221
  const COMMON_PROPS = {
225
222
  ...props,
226
223
  ...DEV_PROPS,
@@ -276,7 +273,7 @@ const Component$8 = forwardRef(({ children, tag, block, extendCss, direction, al
276
273
  children
277
274
  })
278
275
  });
279
- });
276
+ };
280
277
 
281
278
  //#endregion
282
279
  //#region src/helpers/Wrapper/index.ts
@@ -386,7 +383,7 @@ const defaultDirection = "inline";
386
383
  const defaultContentDirection = "rows";
387
384
  const defaultAlignX = "left";
388
385
  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) => {
386
+ 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
387
  const shouldBeEmpty = !!props.dangerouslySetInnerHTML || getShouldBeEmpty(tag);
391
388
  const isSimpleElement = !beforeContent && !afterContent;
392
389
  const CHILDREN = children ?? content ?? label;
@@ -425,7 +422,13 @@ const Component$7 = forwardRef(({ innerRef, tag, label, content, children, befor
425
422
  }, [externalRef]);
426
423
  useLayoutEffect(() => {
427
424
  if (!equalBeforeAfter || !beforeContent || !afterContent) return;
428
- if (equalizeRef.current) equalize(equalizeRef.current, direction);
425
+ const node = equalizeRef.current;
426
+ if (!node) return;
427
+ equalize(node, direction);
428
+ if (typeof ResizeObserver === "undefined") return;
429
+ const observer = new ResizeObserver(() => equalize(node, direction));
430
+ observer.observe(node);
431
+ return () => observer.disconnect();
429
432
  }, [
430
433
  equalBeforeAfter,
431
434
  beforeContent,
@@ -488,7 +491,7 @@ const Component$7 = forwardRef(({ innerRef, tag, label, content, children, befor
488
491
  })
489
492
  ]
490
493
  });
491
- });
494
+ };
492
495
  const name$5 = `${PKG_NAME}/Element`;
493
496
  Component$7.displayName = name$5;
494
497
  Component$7.pkgName = PKG_NAME;
@@ -508,27 +511,6 @@ var Element_default = Component$7;
508
511
  * wrapped with `wrapComponent`. Children always take priority over the
509
512
  * component+data prop pattern.
510
513
  */
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
514
  const RESERVED_PROPS = [
533
515
  "children",
534
516
  "component",
@@ -539,7 +521,7 @@ const RESERVED_PROPS = [
539
521
  "itemProps",
540
522
  "wrapProps"
541
523
  ];
542
- const attachItemProps = ({ i, length }) => {
524
+ const buildExtendedProps = (i, length) => {
543
525
  const position = i + 1;
544
526
  return {
545
527
  index: i,
@@ -550,106 +532,96 @@ const attachItemProps = ({ i, length }) => {
550
532
  position
551
533
  };
552
534
  };
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;
535
+ const resolveCallback = (cb, source, ext) => {
536
+ if (!cb) return {};
537
+ return typeof cb === "function" ? cb(source, ext) : cb;
538
+ };
539
+ const renderSpec = (spec, ext, itemProps, wrapProps, Wrapper) => {
540
+ const finalItemProps = {
541
+ ...resolveCallback(itemProps, spec.source, ext),
542
+ ...spec.base
616
543
  };
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
- });
544
+ if (Wrapper && !spec.skipWrap) return /* @__PURE__ */ jsx(Wrapper, {
545
+ ...resolveCallback(wrapProps, spec.source, ext),
546
+ children: spec.isNode ? render(spec.target, finalItemProps) : render(spec.target, finalItemProps)
547
+ }, spec.key);
548
+ const propsWithKey = {
549
+ key: spec.key,
550
+ ...finalItemProps
641
551
  };
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;
552
+ return spec.isNode ? render(spec.target, propsWithKey) : render(spec.target, propsWithKey);
553
+ };
554
+ /** Normalize children (single, array, or fragment) into an array of nodes. */
555
+ const flattenChildren = (children) => {
556
+ if (Array.isArray(children)) return children;
557
+ if (isFragment(children)) return children.props.children;
558
+ return [children];
559
+ };
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) => {
564
+ 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;
568
+ if (kind === null) kind = t;
569
+ else if (kind !== t) return null;
570
+ }
571
+ return kind;
572
+ };
573
+ const objectKey = (item, index, itemKey) => {
574
+ if (!itemKey) return item.key ?? item.id ?? item.itemId ?? index;
575
+ if (typeof itemKey === "function") return itemKey(item, index);
576
+ if (typeof itemKey === "string") return item[itemKey] ?? index;
577
+ return index;
578
+ };
579
+ const buildChildrenSpecs = (children) => flattenChildren(children).map((node, i) => ({
580
+ key: i,
581
+ target: node,
582
+ source: {},
583
+ base: {},
584
+ isNode: true,
585
+ skipWrap: false
586
+ }));
587
+ const buildSimpleSpecs = (items, component, valueName, itemKey) => {
588
+ const keyName = valueName ?? "children";
589
+ return items.map((value, i) => ({
590
+ key: typeof itemKey === "function" ? itemKey(value, i) : i,
591
+ target: component,
592
+ source: { [keyName]: value },
593
+ base: { [keyName]: value },
594
+ isNode: false,
595
+ skipWrap: false
596
+ }));
597
+ };
598
+ const buildObjectSpecs = (items, component, itemKey) => items.map((item, i) => {
599
+ const { component: itemComponent, ...rest } = item;
600
+ return {
601
+ key: objectKey(rest, i, itemKey),
602
+ target: itemComponent ?? component,
603
+ source: item,
604
+ base: rest,
605
+ isNode: false,
606
+ skipWrap: Boolean(itemComponent)
651
607
  };
652
- return renderItems();
608
+ });
609
+ 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;
614
+ return kind === "simple" ? buildSimpleSpecs(items, component, valueName, itemKey) : buildObjectSpecs(items, component, itemKey);
615
+ };
616
+ const Component$6 = ({ itemKey, valueName, children, component, data, wrapComponent: Wrapper, wrapProps, itemProps }) => {
617
+ let specs = null;
618
+ if (children) {
619
+ if (!Array.isArray(children) && !isFragment(children) && !itemProps && !Wrapper) return children;
620
+ specs = buildChildrenSpecs(children);
621
+ } else if (component && Array.isArray(data)) specs = buildDataSpecs(data, component, valueName, itemKey);
622
+ if (!specs || specs.length === 0) return null;
623
+ const total = specs.length;
624
+ return Children.toArray(specs.map((spec, i) => renderSpec(spec, buildExtendedProps(i, total), itemProps, wrapProps, Wrapper)));
653
625
  };
654
626
  var component_default = Object.assign(memo(Component$6), {
655
627
  isIterator: true,
@@ -669,7 +641,7 @@ var Iterator_default = component_default;
669
641
  * is wrapped in an Element that receives all non-iterator props (e.g.,
670
642
  * layout, alignment, css), allowing the list to be styled as a single block.
671
643
  */
672
- const Component$5 = forwardRef(({ rootElement = false, ...props }, ref) => {
644
+ const Component$5 = ({ rootElement = false, ref, ...props }) => {
673
645
  const renderedList = /* @__PURE__ */ jsx(Iterator_default, { ...pick(props, Iterator_default.RESERVED_PROPS) });
674
646
  if (!rootElement) return renderedList;
675
647
  return /* @__PURE__ */ jsx(Element_default, {
@@ -677,7 +649,7 @@ const Component$5 = forwardRef(({ rootElement = false, ...props }, ref) => {
677
649
  ...omit(props, Iterator_default.RESERVED_PROPS),
678
650
  children: renderedList
679
651
  });
680
- });
652
+ };
681
653
  const name$4 = `${PKG_NAME}/List`;
682
654
  Component$5.displayName = name$4;
683
655
  Component$5.pkgName = PKG_NAME;
@@ -746,14 +718,12 @@ const Component = ({ children, blocked, setBlocked, setUnblocked }) => {
746
718
  };
747
719
 
748
720
  //#endregion
749
- //#region src/Overlay/useOverlay.tsx
721
+ //#region src/Overlay/positionMath.ts
750
722
  /**
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.
723
+ * Pure positioning math for the Overlay component. No React, no DOM mutations.
724
+ * Given the current trigger and content rects (plus alignment options), computes
725
+ * the final viewport-relative position and the alignment that was actually used
726
+ * (which may differ from the requested alignment when an edge would be clipped).
757
727
  */
758
728
  const sel = (cond, a, b) => cond ? a : b;
759
729
  const devWarn = (msg) => {
@@ -923,24 +893,192 @@ const processVisibilityEvent = (e, active, openOn, closeOn, isTrigger, isContent
923
893
  else if (closeOn === "clickOnTrigger" && isTrigger(e)) hideContent();
924
894
  else if (closeOn === "clickOutsideContent" && !isContent(e)) hideContent();
925
895
  };
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 } = {}) => {
896
+
897
+ //#endregion
898
+ //#region src/Overlay/useEscapeKey.ts
899
+ /** Closes the overlay on Escape keypress when `enabled` and `active`. */
900
+ const useEscapeKey = (enabled, active, blocked, hide) => {
901
+ useEffect(() => {
902
+ if (!enabled || !active || blocked) return void 0;
903
+ const onKey = (e) => {
904
+ if (e.key === "Escape") hide();
905
+ };
906
+ window.addEventListener("keydown", onKey);
907
+ return () => window.removeEventListener("keydown", onKey);
908
+ }, [
909
+ enabled,
910
+ active,
911
+ blocked,
912
+ hide
913
+ ]);
914
+ };
915
+
916
+ //#endregion
917
+ //#region src/Overlay/useHoverListeners.ts
918
+ /**
919
+ * Hover-based open/close. Uses mouseenter/mouseleave on trigger + content
920
+ * (instead of window-level mousemove) and a configurable delay to bridge
921
+ * the gap between trigger and content elements without flicker.
922
+ */
923
+ const useHoverListeners = ({ triggerRef, contentRef, isContentLoaded, active, blocked, disabled, openOn, closeOn, hoverDelay, showContent, hideContent }) => {
924
+ const hoverTimeoutRef = useRef(null);
925
+ useEffect(() => {
926
+ if (blocked || disabled || !(openOn === "hover" || closeOn === "hover")) return void 0;
927
+ const trigger = triggerRef.current;
928
+ const content = contentRef.current;
929
+ const clearHoverTimeout = () => {
930
+ if (hoverTimeoutRef.current != null) {
931
+ clearTimeout(hoverTimeoutRef.current);
932
+ hoverTimeoutRef.current = null;
933
+ }
934
+ };
935
+ const scheduleHide = () => {
936
+ clearHoverTimeout();
937
+ hoverTimeoutRef.current = setTimeout(hideContent, hoverDelay);
938
+ };
939
+ const onTriggerEnter = () => {
940
+ clearHoverTimeout();
941
+ if (openOn === "hover" && !active) showContent();
942
+ };
943
+ const onTriggerLeave = () => {
944
+ if (closeOn === "hover" && active) scheduleHide();
945
+ };
946
+ const onContentEnter = () => {
947
+ clearHoverTimeout();
948
+ };
949
+ const onContentLeave = () => {
950
+ if (closeOn === "hover" && active) scheduleHide();
951
+ };
952
+ if (trigger) {
953
+ trigger.addEventListener("mouseenter", onTriggerEnter);
954
+ trigger.addEventListener("mouseleave", onTriggerLeave);
955
+ }
956
+ if (content) {
957
+ content.addEventListener("mouseenter", onContentEnter);
958
+ content.addEventListener("mouseleave", onContentLeave);
959
+ }
960
+ return () => {
961
+ clearHoverTimeout();
962
+ if (trigger) {
963
+ trigger.removeEventListener("mouseenter", onTriggerEnter);
964
+ trigger.removeEventListener("mouseleave", onTriggerLeave);
965
+ }
966
+ if (content) {
967
+ content.removeEventListener("mouseenter", onContentEnter);
968
+ content.removeEventListener("mouseleave", onContentLeave);
969
+ }
970
+ };
971
+ }, [
972
+ active,
973
+ isContentLoaded,
974
+ blocked,
975
+ disabled,
976
+ openOn,
977
+ closeOn,
978
+ hoverDelay,
979
+ showContent,
980
+ hideContent
981
+ ]);
982
+ };
983
+
984
+ //#endregion
985
+ //#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) => {
993
+ useEffect(() => {
994
+ if (!active) return void 0;
995
+ const shouldSetOverflow = type === "modal";
996
+ const onScroll = (e) => {
997
+ handleContentPosition();
998
+ handleVisibility(e);
999
+ };
1000
+ if (shouldSetOverflow) {
1001
+ modalOverflowCount++;
1002
+ if (modalOverflowCount === 1) document.body.style.overflow = "hidden";
1003
+ }
1004
+ window.addEventListener("resize", handleContentPosition);
1005
+ window.addEventListener("scroll", onScroll, { passive: true });
1006
+ return () => {
1007
+ handleContentPosition.cancel();
1008
+ handleVisibility.cancel();
1009
+ if (shouldSetOverflow) {
1010
+ modalOverflowCount--;
1011
+ if (modalOverflowCount === 0) document.body.style.overflow = "";
1012
+ }
1013
+ window.removeEventListener("resize", handleContentPosition);
1014
+ window.removeEventListener("scroll", onScroll);
1015
+ };
1016
+ }, [
1017
+ active,
1018
+ type,
1019
+ handleContentPosition,
1020
+ handleVisibility
1021
+ ]);
1022
+ };
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
+ const useParentContainerReposition = (active, parentContainer, closeOn, handleContentPosition, handleVisibility) => {
1029
+ useEffect(() => {
1030
+ if (!active || !parentContainer) return void 0;
1031
+ if (closeOn !== "hover") parentContainer.style.overflow = "hidden";
1032
+ const onScroll = (e) => {
1033
+ handleContentPosition();
1034
+ handleVisibility(e);
1035
+ };
1036
+ parentContainer.addEventListener("scroll", onScroll, { passive: true });
1037
+ return () => {
1038
+ parentContainer.style.overflow = "";
1039
+ parentContainer.removeEventListener("scroll", onScroll);
1040
+ };
1041
+ }, [
1042
+ active,
1043
+ parentContainer,
1044
+ closeOn,
1045
+ handleContentPosition,
1046
+ handleVisibility
1047
+ ]);
1048
+ };
1049
+ const useScrollReposition = ({ active, type, parentContainer, closeOn, handleContentPosition, handleVisibility }) => {
1050
+ useWindowReposition(active, type, handleContentPosition, handleVisibility);
1051
+ useParentContainerReposition(active, parentContainer, closeOn, handleContentPosition, handleVisibility);
1052
+ };
1053
+
1054
+ //#endregion
1055
+ //#region src/Overlay/useOverlay.tsx
1056
+ /**
1057
+ * Core hook powering the Overlay component. Manages open/close state, DOM
1058
+ * event listeners (click, hover, scroll, resize, ESC key), and dynamic
1059
+ * positioning of overlay content relative to its trigger. Supports dropdown,
1060
+ * tooltip, popover, and modal types with automatic edge-of-viewport flipping.
1061
+ *
1062
+ * Pure positioning math lives in `./positionMath`. Event-listener concerns
1063
+ * live in dedicated hooks: `./useEscapeKey`, `./useHoverListeners`,
1064
+ * `./useScrollReposition`.
1065
+ */
1066
+ 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
1067
  const { rootSize } = useContext(context);
928
1068
  const ctx = useOverlayContext();
929
1069
  const [isContentLoaded, setContentLoaded] = useState(false);
930
1070
  const [innerAlignX, setInnerAlignX] = useState(alignX);
931
1071
  const [innerAlignY, setInnerAlignY] = useState(alignY);
932
- const [blocked, handleBlocked] = useState(false);
933
- const [active, handleActive] = useState(isOpen);
1072
+ const [blockedCount, setBlockedCount] = useState(0);
1073
+ const blocked = blockedCount > 0;
1074
+ const [active, setActive] = useState(isOpen);
934
1075
  const triggerRef = useRef(null);
935
1076
  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
- }, []);
1077
+ const prevFocusRef = useRef(null);
1078
+ const setBlocked = useCallback(() => setBlockedCount((c) => c + 1), []);
1079
+ const setUnblocked = useCallback(() => setBlockedCount((c) => Math.max(0, c - 1)), []);
1080
+ const showContent = useCallback(() => setActive(true), []);
1081
+ const hideContent = useCallback(() => setActive(false), []);
944
1082
  const getAncestorOffset = useCallback(() => {
945
1083
  if (position !== "absolute" || !contentRef.current) return {
946
1084
  top: 0,
@@ -1009,7 +1147,7 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1009
1147
  const latestHandleVisibility = useRef(handleVisibilityByEventType);
1010
1148
  latestHandleVisibility.current = handleVisibilityByEventType;
1011
1149
  const handleContentPosition = useMemo(() => throttle(() => latestSetContentPosition.current(), throttleDelay), [throttleDelay]);
1012
- const handleClick = handleVisibilityByEventType;
1150
+ const handleClick = useCallback((e) => latestHandleVisibility.current(e), []);
1013
1151
  const handleVisibility = useMemo(() => throttle((e) => latestHandleVisibility.current(e), throttleDelay), [throttleDelay]);
1014
1152
  useEffect(() => {
1015
1153
  setInnerAlignX(alignX);
@@ -1056,62 +1194,30 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1056
1194
  onOpen
1057
1195
  ]);
1058
1196
  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
- };
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
+ }
1067
1207
  }, [
1068
1208
  active,
1069
- blocked,
1070
- closeOnEsc,
1071
- hideContent
1209
+ isContentLoaded,
1210
+ type
1072
1211
  ]);
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
- }, [
1212
+ useEscapeKey(closeOnEsc, active, blocked, hideContent);
1213
+ useScrollReposition({
1091
1214
  active,
1092
1215
  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
1216
  parentContainer,
1111
1217
  closeOn,
1112
1218
  handleContentPosition,
1113
1219
  handleVisibility
1114
- ]);
1220
+ });
1115
1221
  useEffect(() => {
1116
1222
  if (blocked || disabled) return void 0;
1117
1223
  if (openOn === "click" || [
@@ -1119,9 +1225,7 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1119
1225
  "clickOnTrigger",
1120
1226
  "clickOutsideContent"
1121
1227
  ].includes(closeOn)) window.addEventListener("click", handleClick);
1122
- return () => {
1123
- window.removeEventListener("click", handleClick);
1124
- };
1228
+ return () => window.removeEventListener("click", handleClick);
1125
1229
  }, [
1126
1230
  openOn,
1127
1231
  closeOn,
@@ -1129,63 +1233,19 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1129
1233
  disabled,
1130
1234
  handleClick
1131
1235
  ]);
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,
1236
+ useHoverListeners({
1237
+ triggerRef,
1238
+ contentRef,
1181
1239
  isContentLoaded,
1240
+ active,
1182
1241
  blocked,
1183
1242
  disabled,
1184
1243
  openOn,
1185
1244
  closeOn,
1245
+ hoverDelay,
1186
1246
  showContent,
1187
1247
  hideContent
1188
- ]);
1248
+ });
1189
1249
  return {
1190
1250
  triggerRef,
1191
1251
  contentRef: useCallback((node) => {
@@ -1217,11 +1277,22 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1217
1277
  const IS_BROWSER = typeof window !== "undefined";
1218
1278
  const Component$3 = ({ children, trigger, DOMLocation, triggerRefName = "ref", contentRefName = "ref", ...props }) => {
1219
1279
  const { active, triggerRef, contentRef, showContent, hideContent, align, alignX, alignY, Provider, ...ctx } = useOverlay(props);
1220
- const { openOn, closeOn } = props;
1280
+ const { openOn, closeOn, type } = props;
1281
+ const contentId = useId();
1221
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]);
1222
1290
  return /* @__PURE__ */ jsxs(Fragment, { children: [render(trigger, {
1223
1291
  [triggerRefName]: triggerRef,
1224
1292
  active,
1293
+ "aria-expanded": active,
1294
+ "aria-haspopup": ariaHasPopup,
1295
+ "aria-controls": active ? contentId : void 0,
1225
1296
  ...passHandlers ? {
1226
1297
  showContent,
1227
1298
  hideContent
@@ -1232,6 +1303,9 @@ const Component$3 = ({ children, trigger, DOMLocation, triggerRefName = "ref", c
1232
1303
  ...ctx,
1233
1304
  children: render(children, {
1234
1305
  [contentRefName]: contentRef,
1306
+ id: contentId,
1307
+ role: type === "modal" ? "dialog" : void 0,
1308
+ "aria-modal": type === "modal" ? true : void 0,
1235
1309
  active,
1236
1310
  align,
1237
1311
  alignX,
@@ -1282,7 +1356,7 @@ var styled_default = styled(textComponent)`
1282
1356
 
1283
1357
  //#endregion
1284
1358
  //#region src/Text/component.tsx
1285
- const Component$2 = forwardRef(({ paragraph, label, children, tag, css, ...props }, ref) => {
1359
+ const Component$2 = ({ paragraph, label, children, tag, css, ref, ...props }) => {
1286
1360
  const renderContent = (as = void 0) => /* @__PURE__ */ jsx(styled_default, {
1287
1361
  ref,
1288
1362
  as,
@@ -1294,7 +1368,7 @@ const Component$2 = forwardRef(({ paragraph, label, children, tag, css, ...props
1294
1368
  if (paragraph) finalTag = "p";
1295
1369
  else finalTag = tag;
1296
1370
  return renderContent(finalTag);
1297
- });
1371
+ };
1298
1372
  const name$1 = `${PKG_NAME}/Text`;
1299
1373
  Component$2.displayName = name$1;
1300
1374
  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
 
@@ -146,20 +146,20 @@ const childFixCSS = `
146
146
  width: 100%;
147
147
  height: 100%;
148
148
  `;
149
- const blockCSS = `
150
- align-self: stretch;
151
- width: 100%;
152
- `;
153
149
  const styles$1 = ({ theme: t, css }) => css`
154
- ${false};
155
-
156
150
  ${alignContent({
157
151
  direction: t.direction,
158
152
  alignX: t.alignX,
159
153
  alignY: t.alignY
160
154
  })};
161
155
 
162
- ${t.block && blockCSS};
156
+ /*
157
+ * Always emit a value for the block-related properties so a responsive
158
+ * theme that flips from \`block: true\` at one breakpoint to \`block: false\`
159
+ * at another resets cleanly. Previously \`align-self\` / \`width\` / \`height\`
160
+ * were only set when the truthy branch matched, which left the prior
161
+ * breakpoint's values cascading through.
162
+ */
163
163
  ${false};
164
164
 
165
165
  ${false};
@@ -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.2",
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.2",
62
+ "@vitus-labs/unistyle": "2.0.0-beta.2",
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.