@pyreon/elements 0.0.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.js ADDED
@@ -0,0 +1,1147 @@
1
+ import { Provider, alignContent, extendCss, makeItResponsive, value } from "@pyreon/unistyle";
2
+ import { Fragment, Portal, createContext, onMount, onUnmount, popContext, pushContext, useContext } from "@pyreon/core";
3
+ import { config, isEmpty, omit, pick, render, throttle } from "@pyreon/ui-core";
4
+ import { Fragment as Fragment$1, jsx, jsxs } from "@pyreon/core/jsx-runtime";
5
+ import { signal } from "@pyreon/reactivity";
6
+
7
+ //#region src/constants.ts
8
+ const PKG_NAME = "@pyreon/elements";
9
+
10
+ //#endregion
11
+ //#region src/utils.ts
12
+ const IS_DEVELOPMENT = process.env.NODE_ENV !== "production";
13
+
14
+ //#endregion
15
+ //#region src/helpers/Content/styled.ts
16
+ /**
17
+ * Styled component for content areas (before/content/after). Applies
18
+ * responsive flex alignment, gap spacing between slots based on parent
19
+ * direction (margin-right for inline, margin-bottom for rows), and
20
+ * equalCols flex distribution. The "content" slot gets `flex: 1` to
21
+ * fill remaining space between before and after.
22
+ */
23
+ const { styled: styled$2, css: css$2, component: component$1 } = config;
24
+ const equalColsCSS = `
25
+ flex: 1;
26
+ `;
27
+ const typeContentCSS = `
28
+ flex: 1;
29
+ `;
30
+ const gapDimensions = {
31
+ inline: {
32
+ before: "margin-right",
33
+ after: "margin-left"
34
+ },
35
+ reverseInline: {
36
+ before: "margin-right",
37
+ after: "margin-left"
38
+ },
39
+ rows: {
40
+ before: "margin-bottom",
41
+ after: "margin-top"
42
+ },
43
+ reverseRows: {
44
+ before: "margin-bottom",
45
+ after: "margin-top"
46
+ }
47
+ };
48
+ const calculateGap = ({ direction, type, value: gapValue }) => {
49
+ if (!direction || !type || type === "content") return void 0;
50
+ return `${gapDimensions[direction][type]}: ${gapValue};`;
51
+ };
52
+ const styles$2 = ({ css: cssFn, theme: t, rootSize }) => cssFn`
53
+ ${alignContent({
54
+ direction: t.direction,
55
+ alignX: t.alignX,
56
+ alignY: t.alignY
57
+ })};
58
+
59
+ ${t.equalCols && equalColsCSS};
60
+
61
+ ${t.gap && t.contentType && calculateGap({
62
+ direction: t.parentDirection,
63
+ type: t.contentType,
64
+ value: value(t.gap, rootSize)
65
+ })};
66
+
67
+ ${t.extraStyles && extendCss(t.extraStyles)};
68
+ `;
69
+ const StyledComponent = styled$2(component$1)`
70
+ ${`box-sizing: border-box;`};
71
+
72
+ display: flex;
73
+ align-self: stretch;
74
+ flex-wrap: wrap;
75
+
76
+ ${(({ $contentType }) => $contentType === "content" && typeContentCSS)};
77
+
78
+ ${makeItResponsive({
79
+ key: "$element",
80
+ styles: styles$2,
81
+ css: css$2,
82
+ normalize: true
83
+ })};
84
+ `;
85
+
86
+ //#endregion
87
+ //#region src/helpers/Content/component.tsx
88
+ /**
89
+ * Content area used inside Element to render one of the three
90
+ * layout slots (before, content, after). Passes alignment, direction,
91
+ * gap, and equalCols styling props to the underlying styled component.
92
+ * Adds a `data-pyr-element` attribute in development for debugging.
93
+ *
94
+ * Children are rendered via core `render()`.
95
+ */
96
+ const Component$9 = ({ contentType, tag, parentDirection, direction, alignX, alignY, equalCols, gap, extendCss, children, ...props }) => {
97
+ return /* @__PURE__ */ jsx(StyledComponent, {
98
+ as: tag,
99
+ $contentType: contentType,
100
+ $element: {
101
+ contentType,
102
+ parentDirection,
103
+ direction,
104
+ alignX,
105
+ alignY,
106
+ equalCols,
107
+ gap,
108
+ extraStyles: extendCss
109
+ },
110
+ ...IS_DEVELOPMENT ? { "data-pyr-element": contentType } : {},
111
+ ...props,
112
+ children: render(children)
113
+ });
114
+ };
115
+
116
+ //#endregion
117
+ //#region src/helpers/Content/index.ts
118
+ var Content_default = Component$9;
119
+
120
+ //#endregion
121
+ //#region src/helpers/Wrapper/styled.ts
122
+ /**
123
+ * Styled component for the Element wrapper layer. Handles responsive
124
+ * block/inline-flex display, direction, alignment, and custom CSS injection.
125
+ * Includes special handling for the `parentFix` / `childFix` flags that
126
+ * split flex behavior across two DOM nodes for button/fieldset/legend
127
+ * elements where a single flex container is insufficient.
128
+ */
129
+ const { styled: styled$1, css: css$1, component } = config;
130
+ const childFixCSS = `
131
+ display: flex;
132
+ flex: 1;
133
+ width: 100%;
134
+ height: 100%;
135
+ `;
136
+ const parentFixCSS = `
137
+ flex-direction: column;
138
+ `;
139
+ const fullHeightCSS = `
140
+ height: 100%;
141
+ `;
142
+ const blockCSS = `
143
+ align-self: stretch;
144
+ width: 100%;
145
+ `;
146
+ const childFixPosition = (isBlock) => `display: ${isBlock ? "flex" : "inline-flex"};`;
147
+ const styles$1 = ({ theme: t, css: cssFn }) => cssFn`
148
+ ${t.alignY === "block" && fullHeightCSS};
149
+
150
+ ${alignContent({
151
+ direction: t.direction,
152
+ alignX: t.alignX,
153
+ alignY: t.alignY
154
+ })};
155
+
156
+ ${t.block && blockCSS};
157
+ ${t.alignY === "block" && t.block && fullHeightCSS};
158
+
159
+ ${!t.childFix && childFixPosition(t.block)};
160
+ ${t.parentFix && parentFixCSS};
161
+
162
+ ${t.extraStyles && extendCss(t.extraStyles)};
163
+ `;
164
+ const platformCSS = `box-sizing: border-box;`;
165
+ var styled_default$1 = styled$1(component)`
166
+ position: relative;
167
+ ${platformCSS};
168
+
169
+ ${(({ $childFix }) => $childFix && childFixCSS)};
170
+
171
+ ${makeItResponsive({
172
+ key: "$element",
173
+ styles: styles$1,
174
+ css: css$1,
175
+ normalize: true
176
+ })};
177
+ `;
178
+
179
+ //#endregion
180
+ //#region src/helpers/Wrapper/constants.ts
181
+ /**
182
+ * HTML elements that need a two-layer DOM workaround because browsers do not
183
+ * fully support flexbox layout on button, fieldset, and legend elements.
184
+ * @see https://stackoverflow.com/questions/35464067/flexbox-not-working-on-button-or-fieldset-elements
185
+ */
186
+ const INLINE_ELEMENTS_FLEX_FIX = {
187
+ button: true,
188
+ fieldset: true,
189
+ legend: true
190
+ };
191
+
192
+ //#endregion
193
+ //#region src/helpers/Wrapper/utils.ts
194
+ const isWebFixNeeded = (tag) => {
195
+ if (tag && tag in INLINE_ELEMENTS_FLEX_FIX) return true;
196
+ return false;
197
+ };
198
+
199
+ //#endregion
200
+ //#region src/helpers/Wrapper/component.tsx
201
+ /**
202
+ * Wrapper component that serves as the outermost styled container for Element.
203
+ * On web, it detects button/fieldset/legend tags and applies a two-layer flex
204
+ * fix (parent + child Styled) because these HTML elements do not natively
205
+ * support `display: flex` consistently across browsers.
206
+ */
207
+ const DEV_PROPS = IS_DEVELOPMENT ? { "data-pyr-element": "Element" } : {};
208
+ const Component$8 = ({ children, tag, block, extendCss, direction, alignX, alignY, equalCols, isInline, ref, ...props }) => {
209
+ const COMMON_PROPS = {
210
+ ...props,
211
+ ...DEV_PROPS,
212
+ ref,
213
+ as: tag
214
+ };
215
+ const needsFix = !props.dangerouslySetInnerHTML && isWebFixNeeded(tag);
216
+ const normalElement = {
217
+ block,
218
+ direction,
219
+ alignX,
220
+ alignY,
221
+ equalCols,
222
+ extraStyles: extendCss
223
+ };
224
+ const parentFixElement = {
225
+ parentFix: true,
226
+ block,
227
+ extraStyles: extendCss
228
+ };
229
+ const childFixElement = {
230
+ childFix: true,
231
+ direction,
232
+ alignX,
233
+ alignY,
234
+ equalCols
235
+ };
236
+ if (!needsFix) return /* @__PURE__ */ jsx(styled_default$1, {
237
+ ...COMMON_PROPS,
238
+ $element: normalElement,
239
+ children
240
+ });
241
+ const asTag = isInline ? "span" : "div";
242
+ return /* @__PURE__ */ jsx(styled_default$1, {
243
+ ...COMMON_PROPS,
244
+ $element: parentFixElement,
245
+ children: /* @__PURE__ */ jsx(styled_default$1, {
246
+ as: asTag,
247
+ $childFix: true,
248
+ $element: childFixElement,
249
+ children
250
+ })
251
+ });
252
+ };
253
+
254
+ //#endregion
255
+ //#region src/helpers/Wrapper/index.ts
256
+ var Wrapper_default = Component$8;
257
+
258
+ //#endregion
259
+ //#region src/Element/constants.ts
260
+ /**
261
+ * HTML tags that are inline-level by default. When Element renders one of
262
+ * these tags, child Content wrappers use `span` instead of `div` to
263
+ * preserve valid HTML nesting.
264
+ */
265
+ const INLINE_ELEMENTS = {
266
+ span: true,
267
+ a: true,
268
+ button: true,
269
+ input: true,
270
+ label: true,
271
+ select: true,
272
+ textarea: true,
273
+ br: true,
274
+ img: true,
275
+ strong: true,
276
+ small: true,
277
+ code: true,
278
+ b: true,
279
+ big: true,
280
+ i: true,
281
+ tt: true,
282
+ abbr: true,
283
+ acronym: true,
284
+ cite: true,
285
+ dfn: true,
286
+ em: true,
287
+ kbd: true,
288
+ samp: true,
289
+ var: true,
290
+ bdo: true,
291
+ map: true,
292
+ object: true,
293
+ q: true,
294
+ script: true,
295
+ sub: true,
296
+ sup: true
297
+ };
298
+ /**
299
+ * HTML void/self-closing elements that cannot have children. When Element
300
+ * detects one of these tags, it skips rendering beforeContent/content/afterContent
301
+ * and returns the Wrapper alone.
302
+ */
303
+ const EMPTY_ELEMENTS = {
304
+ area: true,
305
+ base: true,
306
+ br: true,
307
+ col: true,
308
+ embed: true,
309
+ hr: true,
310
+ img: true,
311
+ input: true,
312
+ keygen: true,
313
+ link: true,
314
+ textarea: true,
315
+ source: true,
316
+ track: true,
317
+ wbr: true
318
+ };
319
+
320
+ //#endregion
321
+ //#region src/Element/utils.ts
322
+ /** Checks whether the given HTML tag is an inline-level element, used to determine sub-tag nesting. */
323
+ const isInlineElement = (tag) => {
324
+ if (tag && tag in INLINE_ELEMENTS) return true;
325
+ return false;
326
+ };
327
+ /** Checks whether the given HTML tag is a void element that cannot have children. */
328
+ const getShouldBeEmpty = (tag) => {
329
+ if (tag && tag in EMPTY_ELEMENTS) return true;
330
+ return false;
331
+ };
332
+
333
+ //#endregion
334
+ //#region src/Element/component.tsx
335
+ /**
336
+ * Core building block of the elements package. Renders a three-section layout
337
+ * (beforeContent / content / afterContent) inside a flex Wrapper. When only
338
+ * content is present, the Wrapper inherits content-level alignment directly
339
+ * to avoid an unnecessary nesting layer. Handles HTML-specific edge cases
340
+ * like void elements (input, img) and inline elements (span, a) by
341
+ * skipping children or switching sub-tags accordingly.
342
+ */
343
+ const equalize = (el, direction) => {
344
+ const beforeEl = el.firstElementChild;
345
+ const afterEl = el.lastElementChild;
346
+ if (beforeEl && afterEl && beforeEl !== afterEl) {
347
+ const type = direction === "rows" ? "height" : "width";
348
+ const prop = type === "height" ? "offsetHeight" : "offsetWidth";
349
+ const beforeSize = beforeEl[prop];
350
+ const afterSize = afterEl[prop];
351
+ if (Number.isInteger(beforeSize) && Number.isInteger(afterSize)) {
352
+ const maxSize = `${Math.max(beforeSize, afterSize)}px`;
353
+ beforeEl.style[type] = maxSize;
354
+ afterEl.style[type] = maxSize;
355
+ }
356
+ }
357
+ };
358
+ const defaultDirection = "inline";
359
+ const defaultContentDirection = "rows";
360
+ const defaultAlignX = "left";
361
+ const defaultAlignY = "center";
362
+ const Component = ({ 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, ref, ...props }) => {
363
+ const shouldBeEmpty = !!props.dangerouslySetInnerHTML || getShouldBeEmpty(tag);
364
+ const isSimpleElement = !beforeContent && !afterContent;
365
+ const CHILDREN = children ?? content ?? label;
366
+ const isInline = isInlineElement(tag);
367
+ const SUB_TAG = isInline ? "span" : void 0;
368
+ let wrapperDirection = direction;
369
+ let wrapperAlignX = alignX;
370
+ let wrapperAlignY = alignY;
371
+ if (isSimpleElement) {
372
+ if (contentDirection) wrapperDirection = contentDirection;
373
+ if (contentAlignX) wrapperAlignX = contentAlignX;
374
+ if (contentAlignY) wrapperAlignY = contentAlignY;
375
+ } else if (direction) wrapperDirection = direction;
376
+ else wrapperDirection = defaultDirection;
377
+ let equalizeRef = null;
378
+ const externalRef = ref ?? innerRef;
379
+ const mergedRef = (node) => {
380
+ equalizeRef = node;
381
+ if (typeof externalRef === "function") externalRef(node);
382
+ else if (externalRef != null) externalRef.current = node;
383
+ };
384
+ if (equalBeforeAfter && beforeContent && afterContent) onMount(() => {
385
+ if (equalizeRef) equalize(equalizeRef, direction);
386
+ });
387
+ const WRAPPER_PROPS = {
388
+ ref: mergedRef,
389
+ extendCss: css,
390
+ tag,
391
+ block,
392
+ direction: wrapperDirection,
393
+ alignX: wrapperAlignX,
394
+ alignY: wrapperAlignY,
395
+ as: void 0
396
+ };
397
+ if (shouldBeEmpty) return /* @__PURE__ */ jsx(Wrapper_default, {
398
+ ...props,
399
+ ...WRAPPER_PROPS
400
+ });
401
+ return /* @__PURE__ */ jsxs(Wrapper_default, {
402
+ ...props,
403
+ ...WRAPPER_PROPS,
404
+ isInline,
405
+ children: [
406
+ beforeContent && /* @__PURE__ */ jsx(Content_default, {
407
+ tag: SUB_TAG,
408
+ contentType: "before",
409
+ parentDirection: wrapperDirection,
410
+ extendCss: beforeContentCss,
411
+ direction: beforeContentDirection,
412
+ alignX: beforeContentAlignX,
413
+ alignY: beforeContentAlignY,
414
+ equalCols,
415
+ gap,
416
+ children: beforeContent
417
+ }),
418
+ isSimpleElement ? render(CHILDREN) : /* @__PURE__ */ jsx(Content_default, {
419
+ tag: SUB_TAG,
420
+ contentType: "content",
421
+ parentDirection: wrapperDirection,
422
+ extendCss: contentCss,
423
+ direction: contentDirection,
424
+ alignX: contentAlignX,
425
+ alignY: contentAlignY,
426
+ equalCols,
427
+ children: CHILDREN
428
+ }),
429
+ afterContent && /* @__PURE__ */ jsx(Content_default, {
430
+ tag: SUB_TAG,
431
+ contentType: "after",
432
+ parentDirection: wrapperDirection,
433
+ extendCss: afterContentCss,
434
+ direction: afterContentDirection,
435
+ alignX: afterContentAlignX,
436
+ alignY: afterContentAlignY,
437
+ equalCols,
438
+ gap,
439
+ children: afterContent
440
+ })
441
+ ]
442
+ });
443
+ };
444
+ const name$5 = `${PKG_NAME}/Element`;
445
+ Component.displayName = name$5;
446
+ Component.pkgName = PKG_NAME;
447
+ Component.PYREON__COMPONENT = name$5;
448
+
449
+ //#endregion
450
+ //#region src/helpers/Iterator/component.tsx
451
+ const classifyData = (data) => {
452
+ const items = data.filter((item) => item != null && !(typeof item === "object" && isEmpty(item)));
453
+ if (items.length === 0) return null;
454
+ let isSimple = true;
455
+ let isComplex = true;
456
+ for (const item of items) if (typeof item === "string" || typeof item === "number") isComplex = false;
457
+ else if (typeof item === "object") isSimple = false;
458
+ else {
459
+ isSimple = false;
460
+ isComplex = false;
461
+ }
462
+ if (isSimple) return {
463
+ type: "simple",
464
+ data: items
465
+ };
466
+ if (isComplex) return {
467
+ type: "complex",
468
+ data: items
469
+ };
470
+ return null;
471
+ };
472
+ const RESERVED_PROPS = [
473
+ "children",
474
+ "component",
475
+ "wrapComponent",
476
+ "data",
477
+ "itemKey",
478
+ "valueName",
479
+ "itemProps",
480
+ "wrapProps"
481
+ ];
482
+ const attachItemProps = ({ i, length }) => {
483
+ const position = i + 1;
484
+ return {
485
+ index: i,
486
+ first: position === 1,
487
+ last: position === length,
488
+ odd: position % 2 === 1,
489
+ even: position % 2 === 0,
490
+ position
491
+ };
492
+ };
493
+ const Component$7 = (props) => {
494
+ const { itemKey, valueName, children, component, data, wrapComponent: Wrapper, wrapProps, itemProps } = props;
495
+ const injectItemProps = typeof itemProps === "function" ? itemProps : () => itemProps;
496
+ const injectWrapItemProps = typeof wrapProps === "function" ? wrapProps : () => wrapProps;
497
+ const getKey = (item, index) => {
498
+ if (typeof itemKey === "function") return itemKey(item, index);
499
+ return index;
500
+ };
501
+ const renderChild = (child, total = 1, i = 0) => {
502
+ if (!itemProps && !Wrapper) return child;
503
+ const extendedProps = attachItemProps({
504
+ i,
505
+ length: total
506
+ });
507
+ const finalItemProps = itemProps ? injectItemProps({}, extendedProps) : {};
508
+ if (Wrapper) return /* @__PURE__ */ jsx(Wrapper, {
509
+ ...wrapProps ? injectWrapItemProps({}, extendedProps) : {},
510
+ children: render(child, finalItemProps)
511
+ }, i);
512
+ return render(child, {
513
+ key: i,
514
+ ...finalItemProps
515
+ });
516
+ };
517
+ const renderChildren = () => {
518
+ if (!children) return null;
519
+ if (Array.isArray(children)) return children.map((item, i) => renderChild(item, children.length, i));
520
+ if (children && typeof children === "object" && "type" in children && children.type === Fragment) {
521
+ const fragmentChildren = children.children;
522
+ const childrenLength = fragmentChildren.length;
523
+ return fragmentChildren.map((item, i) => renderChild(item, childrenLength, i));
524
+ }
525
+ return renderChild(children);
526
+ };
527
+ const renderSimpleArray = (simpleData) => {
528
+ const { length } = simpleData;
529
+ if (length === 0) return null;
530
+ return simpleData.map((item, i) => {
531
+ const key = getKey(item, i);
532
+ const keyName = valueName ?? "children";
533
+ const extendedProps = attachItemProps({
534
+ i,
535
+ length
536
+ });
537
+ const finalItemProps = {
538
+ ...itemProps ? injectItemProps({ [keyName]: item }, extendedProps) : {},
539
+ [keyName]: item
540
+ };
541
+ if (Wrapper) return /* @__PURE__ */ jsx(Wrapper, {
542
+ ...wrapProps ? injectWrapItemProps({ [keyName]: item }, extendedProps) : {},
543
+ children: render(component, finalItemProps)
544
+ }, key);
545
+ return render(component, {
546
+ key,
547
+ ...finalItemProps
548
+ });
549
+ });
550
+ };
551
+ const getObjectKey = (item, index) => {
552
+ if (!itemKey) return item.key ?? item.id ?? item.itemId ?? index;
553
+ if (typeof itemKey === "function") return itemKey(item, index);
554
+ if (typeof itemKey === "string") return item[itemKey];
555
+ return index;
556
+ };
557
+ const renderComplexArray = (complexData) => {
558
+ const { length } = complexData;
559
+ if (length === 0) return null;
560
+ return complexData.map((item, i) => {
561
+ const { component: itemComponent, ...restItem } = item;
562
+ const renderItem = itemComponent ?? component;
563
+ const key = getObjectKey(restItem, i);
564
+ const extendedProps = attachItemProps({
565
+ i,
566
+ length
567
+ });
568
+ const finalItemProps = {
569
+ ...itemProps ? injectItemProps(item, extendedProps) : {},
570
+ ...restItem
571
+ };
572
+ if (Wrapper && !itemComponent) return /* @__PURE__ */ jsx(Wrapper, {
573
+ ...wrapProps ? injectWrapItemProps(item, extendedProps) : {},
574
+ children: render(renderItem, finalItemProps)
575
+ }, key);
576
+ return render(renderItem, {
577
+ key,
578
+ ...finalItemProps
579
+ });
580
+ });
581
+ };
582
+ const renderItems = () => {
583
+ if (children) return renderChildren();
584
+ if (component && Array.isArray(data)) {
585
+ const classified = classifyData(data);
586
+ if (!classified) return null;
587
+ if (classified.type === "simple") return renderSimpleArray(classified.data);
588
+ return renderComplexArray(classified.data);
589
+ }
590
+ return null;
591
+ };
592
+ return renderItems();
593
+ };
594
+ var component_default = Object.assign(Component$7, {
595
+ isIterator: true,
596
+ RESERVED_PROPS
597
+ });
598
+
599
+ //#endregion
600
+ //#region src/helpers/Iterator/index.ts
601
+ var Iterator_default = component_default;
602
+
603
+ //#endregion
604
+ //#region src/List/component.tsx
605
+ /**
606
+ * List component that combines Iterator (data-driven rendering) with an
607
+ * optional Element root wrapper. When `rootElement` is false (default),
608
+ * it renders a bare Iterator as a fragment. When true, the Iterator output
609
+ * is wrapped in an Element that receives all non-iterator props (e.g.,
610
+ * layout, alignment, css), allowing the list to be styled as a single block.
611
+ */
612
+ const Component$1 = (({ rootElement = false, ref, ...props }) => {
613
+ const renderedList = /* @__PURE__ */ jsx(Iterator_default, { ...pick(props, Iterator_default.RESERVED_PROPS) });
614
+ if (!rootElement) return renderedList;
615
+ return /* @__PURE__ */ jsx(Component, {
616
+ ref,
617
+ ...omit(props, Iterator_default.RESERVED_PROPS),
618
+ children: renderedList
619
+ });
620
+ });
621
+ const name$4 = `${PKG_NAME}/List`;
622
+ Component$1.displayName = name$4;
623
+ Component$1.pkgName = PKG_NAME;
624
+ Component$1.PYREON__COMPONENT = name$4;
625
+
626
+ //#endregion
627
+ //#region src/Overlay/context.tsx
628
+ const context = createContext({});
629
+ const useOverlayContext = () => useContext(context);
630
+ const Component$3 = ({ children, blocked, setBlocked, setUnblocked }) => {
631
+ const ctx = {
632
+ blocked,
633
+ setBlocked,
634
+ setUnblocked
635
+ };
636
+ pushContext(new Map([[context.id, ctx]]));
637
+ onUnmount(() => popContext());
638
+ return /* @__PURE__ */ jsx(Fragment$1, { children });
639
+ };
640
+
641
+ //#endregion
642
+ //#region src/Overlay/useOverlay.tsx
643
+ /**
644
+ * Core hook powering the Overlay component. Manages open/close state, DOM
645
+ * event listeners (click, hover, scroll, resize, ESC key), and dynamic
646
+ * positioning of overlay content relative to its trigger. Supports dropdown,
647
+ * tooltip, popover, and modal types with automatic edge-of-viewport flipping.
648
+ * Event handlers are throttled for performance, and nested overlay blocking
649
+ * is coordinated through the overlay context.
650
+ */
651
+ let modalOverflowCount = 0;
652
+ const sel = (cond, a, b) => cond ? a : b;
653
+ const devWarn = (msg) => {
654
+ if (!IS_DEVELOPMENT) return;
655
+ console.warn(msg);
656
+ };
657
+ const calcDropdownVertical = (c, t, align, alignX, offsetX, offsetY) => {
658
+ const pos = {};
659
+ const topPos = t.top - offsetY - c.height;
660
+ const bottomPos = t.bottom + offsetY;
661
+ const leftPos = t.left + offsetX;
662
+ const rightPos = t.right - offsetX - c.width;
663
+ const fitsTop = topPos >= 0;
664
+ const fitsBottom = bottomPos + c.height <= window.innerHeight;
665
+ const fitsLeft = leftPos + c.width <= window.innerWidth;
666
+ const fitsRight = rightPos >= 0;
667
+ const useTop = sel(align === "top", fitsTop, !fitsBottom);
668
+ pos.top = sel(useTop, topPos, bottomPos);
669
+ const resolvedAlignY = sel(useTop, "top", "bottom");
670
+ let resolvedAlignX = alignX;
671
+ if (alignX === "left") {
672
+ pos.left = sel(fitsLeft, leftPos, rightPos);
673
+ resolvedAlignX = sel(fitsLeft, "left", "right");
674
+ } else if (alignX === "right") {
675
+ pos.left = sel(fitsRight, rightPos, leftPos);
676
+ resolvedAlignX = sel(fitsRight, "right", "left");
677
+ } else {
678
+ const center = t.left + (t.right - t.left) / 2 - c.width / 2;
679
+ const fitsCL = center >= 0;
680
+ const fitsCR = center + c.width <= window.innerWidth;
681
+ if (fitsCL && fitsCR) {
682
+ resolvedAlignX = "center";
683
+ pos.left = center;
684
+ } else if (fitsCL) {
685
+ resolvedAlignX = "left";
686
+ pos.left = leftPos;
687
+ } else if (fitsCR) {
688
+ resolvedAlignX = "right";
689
+ pos.left = rightPos;
690
+ }
691
+ }
692
+ return {
693
+ pos,
694
+ resolvedAlignX,
695
+ resolvedAlignY
696
+ };
697
+ };
698
+ const calcDropdownHorizontal = (c, t, align, alignY, offsetX, offsetY) => {
699
+ const pos = {};
700
+ const leftPos = t.left - offsetX - c.width;
701
+ const rightPos = t.right + offsetX;
702
+ const topPos = t.top + offsetY;
703
+ const bottomPos = t.bottom - offsetY - c.height;
704
+ const fitsLeft = leftPos >= 0;
705
+ const fitsRight = rightPos + c.width <= window.innerWidth;
706
+ const fitsTop = topPos + c.height <= window.innerHeight;
707
+ const fitsBottom = bottomPos >= 0;
708
+ const useLeft = sel(align === "left", fitsLeft, !fitsRight);
709
+ pos.left = sel(useLeft, leftPos, rightPos);
710
+ const resolvedAlignX = sel(useLeft, "left", "right");
711
+ let resolvedAlignY = alignY;
712
+ if (alignY === "top") {
713
+ pos.top = sel(fitsTop, topPos, bottomPos);
714
+ resolvedAlignY = sel(fitsTop, "top", "bottom");
715
+ } else if (alignY === "bottom") {
716
+ pos.top = sel(fitsBottom, bottomPos, topPos);
717
+ resolvedAlignY = sel(fitsBottom, "bottom", "top");
718
+ } else {
719
+ const center = t.top + (t.bottom - t.top) / 2 - c.height / 2;
720
+ const fitsCT = center >= 0;
721
+ const fitsCB = center + c.height <= window.innerHeight;
722
+ if (fitsCT && fitsCB) {
723
+ resolvedAlignY = "center";
724
+ pos.top = center;
725
+ } else if (fitsCT) {
726
+ resolvedAlignY = "top";
727
+ pos.top = topPos;
728
+ } else if (fitsCB) {
729
+ resolvedAlignY = "bottom";
730
+ pos.top = bottomPos;
731
+ }
732
+ }
733
+ return {
734
+ pos,
735
+ resolvedAlignX,
736
+ resolvedAlignY
737
+ };
738
+ };
739
+ const calcModalPos = (c, alignX, alignY, offsetX, offsetY) => {
740
+ const pos = {};
741
+ switch (alignX) {
742
+ case "right":
743
+ pos.right = offsetX;
744
+ break;
745
+ case "left":
746
+ pos.left = offsetX;
747
+ break;
748
+ case "center":
749
+ pos.left = window.innerWidth / 2 - c.width / 2;
750
+ break;
751
+ default: pos.right = offsetX;
752
+ }
753
+ switch (alignY) {
754
+ case "top":
755
+ pos.top = offsetY;
756
+ break;
757
+ case "center":
758
+ pos.top = window.innerHeight / 2 - c.height / 2;
759
+ break;
760
+ case "bottom":
761
+ pos.bottom = offsetY;
762
+ break;
763
+ default: pos.top = offsetY;
764
+ }
765
+ return pos;
766
+ };
767
+ const adjustForAncestor = (pos, ancestor) => {
768
+ if (ancestor.top === 0 && ancestor.left === 0) return pos;
769
+ const result = { ...pos };
770
+ if (typeof result.top === "number") result.top -= ancestor.top;
771
+ if (typeof result.bottom === "number") result.bottom += ancestor.top;
772
+ if (typeof result.left === "number") result.left -= ancestor.left;
773
+ if (typeof result.right === "number") result.right += ancestor.left;
774
+ return result;
775
+ };
776
+ const computePosition = (type, align, alignX, alignY, offsetX, offsetY, triggerEl, contentEl, ancestorOffset) => {
777
+ const isDropdown = [
778
+ "dropdown",
779
+ "tooltip",
780
+ "popover"
781
+ ].includes(type);
782
+ if (isDropdown && (!triggerEl || !contentEl)) {
783
+ devWarn(`[@pyreon/elements] Overlay (${type}): ${triggerEl ? "contentRef" : "triggerRef"} is not attached. Position cannot be calculated without both refs.`);
784
+ return { pos: {} };
785
+ }
786
+ if (isDropdown && triggerEl && contentEl) {
787
+ const c = contentEl.getBoundingClientRect();
788
+ const t = triggerEl.getBoundingClientRect();
789
+ const result = align === "top" || align === "bottom" ? calcDropdownVertical(c, t, align, alignX, offsetX, offsetY) : calcDropdownHorizontal(c, t, align, alignY, offsetX, offsetY);
790
+ return {
791
+ pos: adjustForAncestor(result.pos, ancestorOffset),
792
+ resolvedAlignX: result.resolvedAlignX,
793
+ resolvedAlignY: result.resolvedAlignY
794
+ };
795
+ }
796
+ if (type === "modal") {
797
+ if (!contentEl) {
798
+ devWarn("[@pyreon/elements] Overlay (modal): contentRef is not attached. Modal position cannot be calculated without a content element.");
799
+ return { pos: {} };
800
+ }
801
+ return { pos: adjustForAncestor(calcModalPos(contentEl.getBoundingClientRect(), alignX, alignY, offsetX, offsetY), ancestorOffset) };
802
+ }
803
+ return { pos: {} };
804
+ };
805
+ const processVisibilityEvent = (e, active, openOn, closeOn, isTrigger, isContent, showContent, hideContent) => {
806
+ if (!active && openOn === "click" && e.type === "click" && isTrigger(e)) {
807
+ showContent();
808
+ return;
809
+ }
810
+ if (!active) return;
811
+ if (closeOn === "hover" && e.type === "scroll") {
812
+ hideContent();
813
+ return;
814
+ }
815
+ if (e.type !== "click") return;
816
+ if (closeOn === "click") hideContent();
817
+ else if (closeOn === "clickOnTrigger" && isTrigger(e)) hideContent();
818
+ else if (closeOn === "clickOutsideContent" && !isContent(e)) hideContent();
819
+ };
820
+ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type = "dropdown", position = "fixed", align = "bottom", alignX: propAlignX = "left", alignY: propAlignY = "bottom", offsetX = 0, offsetY = 0, throttleDelay = 200, parentContainer, closeOnEsc = true, hoverDelay = 100, disabled, onOpen, onClose } = {}) => {
821
+ const ctx = useOverlayContext();
822
+ const active = signal(isOpen);
823
+ const isContentLoaded = signal(false);
824
+ const innerAlignX = signal(propAlignX);
825
+ const innerAlignY = signal(propAlignY);
826
+ const blockedCount = signal(0);
827
+ const blocked = () => blockedCount() > 0;
828
+ let triggerEl = null;
829
+ let contentEl = null;
830
+ let hoverTimeout = null;
831
+ const triggerRef = (node) => {
832
+ triggerEl = node;
833
+ };
834
+ const contentRefCallback = (node) => {
835
+ contentEl = node;
836
+ isContentLoaded.set(!!node);
837
+ };
838
+ const setBlocked = () => blockedCount.update((c) => c + 1);
839
+ const setUnblocked = () => blockedCount.update((c) => Math.max(0, c - 1));
840
+ const showContent = () => {
841
+ active.set(true);
842
+ onOpen?.();
843
+ ctx.setBlocked?.();
844
+ };
845
+ const hideContent = () => {
846
+ active.set(false);
847
+ isContentLoaded.set(false);
848
+ onClose?.();
849
+ ctx.setUnblocked?.();
850
+ };
851
+ const getAncestorOffset = () => {
852
+ if (position !== "absolute" || !contentEl) return {
853
+ top: 0,
854
+ left: 0
855
+ };
856
+ const offsetParent = contentEl.offsetParent;
857
+ if (!offsetParent || offsetParent === document.body) return {
858
+ top: 0,
859
+ left: 0
860
+ };
861
+ const rect = offsetParent.getBoundingClientRect();
862
+ return {
863
+ top: rect.top,
864
+ left: rect.left
865
+ };
866
+ };
867
+ const calculateContentPosition = () => {
868
+ if (!active() || !isContentLoaded()) return {};
869
+ const result = computePosition(type, align, propAlignX, propAlignY, offsetX, offsetY, triggerEl, contentEl, getAncestorOffset());
870
+ if (result.resolvedAlignX) innerAlignX.set(result.resolvedAlignX);
871
+ if (result.resolvedAlignY) innerAlignY.set(result.resolvedAlignY);
872
+ return result.pos;
873
+ };
874
+ const assignContentPosition = (values = {}) => {
875
+ if (!contentEl) return;
876
+ const el = contentEl;
877
+ const setValue = (param) => value(param, 16);
878
+ el.style.position = position;
879
+ el.style.top = values.top != null ? setValue(values.top) : "";
880
+ el.style.bottom = values.bottom != null ? setValue(values.bottom) : "";
881
+ el.style.left = values.left != null ? setValue(values.left) : "";
882
+ el.style.right = values.right != null ? setValue(values.right) : "";
883
+ };
884
+ const setContentPosition = () => {
885
+ assignContentPosition(calculateContentPosition());
886
+ };
887
+ const isNodeOrChild = (getRef) => (e) => {
888
+ const ref = getRef();
889
+ if (e?.target && ref) return ref.contains(e.target) || e.target === ref;
890
+ return false;
891
+ };
892
+ const handleVisibilityByEventType = (e) => {
893
+ if (blocked() || disabled) return;
894
+ processVisibilityEvent(e, active(), openOn, closeOn, isNodeOrChild(() => triggerEl), isNodeOrChild(() => contentEl), showContent, hideContent);
895
+ };
896
+ const handleContentPosition = throttle(() => setContentPosition(), throttleDelay);
897
+ const handleClick = (e) => handleVisibilityByEventType(e);
898
+ const handleVisibility = throttle((e) => handleVisibilityByEventType(e), throttleDelay);
899
+ const setupListeners = () => {
900
+ const cleanups = [];
901
+ if (openOn === "click" || [
902
+ "click",
903
+ "clickOnTrigger",
904
+ "clickOutsideContent"
905
+ ].includes(closeOn)) {
906
+ window.addEventListener("click", handleClick);
907
+ cleanups.push(() => window.removeEventListener("click", handleClick));
908
+ }
909
+ if (closeOnEsc) {
910
+ const handleEscKey = (e) => {
911
+ if (e.key === "Escape" && active() && !blocked()) hideContent();
912
+ };
913
+ window.addEventListener("keydown", handleEscKey);
914
+ cleanups.push(() => window.removeEventListener("keydown", handleEscKey));
915
+ }
916
+ if (openOn === "hover" || closeOn === "hover") {
917
+ const clearHoverTimeout = () => {
918
+ if (hoverTimeout != null) {
919
+ clearTimeout(hoverTimeout);
920
+ hoverTimeout = null;
921
+ }
922
+ };
923
+ const scheduleHide = () => {
924
+ clearHoverTimeout();
925
+ hoverTimeout = setTimeout(hideContent, hoverDelay);
926
+ };
927
+ const onTriggerEnter = () => {
928
+ clearHoverTimeout();
929
+ if (openOn === "hover" && !active()) showContent();
930
+ };
931
+ const onTriggerLeave = () => {
932
+ if (closeOn === "hover" && active()) scheduleHide();
933
+ };
934
+ const onContentEnter = () => {
935
+ clearHoverTimeout();
936
+ };
937
+ const onContentLeave = () => {
938
+ if (closeOn === "hover" && active()) scheduleHide();
939
+ };
940
+ const attachHoverListeners = () => {
941
+ if (triggerEl) {
942
+ triggerEl.addEventListener("mouseenter", onTriggerEnter);
943
+ triggerEl.addEventListener("mouseleave", onTriggerLeave);
944
+ }
945
+ if (contentEl) {
946
+ contentEl.addEventListener("mouseenter", onContentEnter);
947
+ contentEl.addEventListener("mouseleave", onContentLeave);
948
+ }
949
+ };
950
+ attachHoverListeners();
951
+ cleanups.push(() => {
952
+ clearHoverTimeout();
953
+ if (triggerEl) {
954
+ triggerEl.removeEventListener("mouseenter", onTriggerEnter);
955
+ triggerEl.removeEventListener("mouseleave", onTriggerLeave);
956
+ }
957
+ if (contentEl) {
958
+ contentEl.removeEventListener("mouseenter", onContentEnter);
959
+ contentEl.removeEventListener("mouseleave", onContentLeave);
960
+ }
961
+ });
962
+ }
963
+ const shouldSetOverflow = type === "modal";
964
+ const onScroll = (e) => {
965
+ handleContentPosition();
966
+ handleVisibility(e);
967
+ };
968
+ if (shouldSetOverflow) {
969
+ modalOverflowCount++;
970
+ if (modalOverflowCount === 1) document.body.style.overflow = "hidden";
971
+ }
972
+ window.addEventListener("resize", handleContentPosition);
973
+ window.addEventListener("scroll", onScroll, { passive: true });
974
+ cleanups.push(() => {
975
+ handleContentPosition.cancel();
976
+ handleVisibility.cancel();
977
+ if (shouldSetOverflow) {
978
+ modalOverflowCount--;
979
+ if (modalOverflowCount === 0) document.body.style.overflow = "";
980
+ }
981
+ window.removeEventListener("resize", handleContentPosition);
982
+ window.removeEventListener("scroll", onScroll);
983
+ });
984
+ if (parentContainer) {
985
+ if (closeOn !== "hover") parentContainer.style.overflow = "hidden";
986
+ const onParentScroll = (e) => {
987
+ handleContentPosition();
988
+ handleVisibility(e);
989
+ };
990
+ parentContainer.addEventListener("scroll", onParentScroll, { passive: true });
991
+ cleanups.push(() => {
992
+ parentContainer.style.overflow = "";
993
+ parentContainer.removeEventListener("scroll", onParentScroll);
994
+ });
995
+ }
996
+ return () => {
997
+ for (const cleanup of cleanups) cleanup();
998
+ };
999
+ };
1000
+ if (disabled) active.set(false);
1001
+ return {
1002
+ triggerRef,
1003
+ contentRef: contentRefCallback,
1004
+ active,
1005
+ align,
1006
+ alignX: innerAlignX,
1007
+ alignY: innerAlignY,
1008
+ showContent,
1009
+ hideContent,
1010
+ blocked,
1011
+ setBlocked,
1012
+ setUnblocked,
1013
+ setupListeners,
1014
+ Provider: Component$3
1015
+ };
1016
+ };
1017
+
1018
+ //#endregion
1019
+ //#region src/Overlay/component.tsx
1020
+ const IS_BROWSER = typeof window !== "undefined";
1021
+ const Component$2 = ({ children, trigger, DOMLocation, triggerRefName = "ref", contentRefName = "ref", ...props }) => {
1022
+ const { active, triggerRef, contentRef, showContent, hideContent, align, alignX, alignY, setupListeners, Provider, ...ctx } = useOverlay(props);
1023
+ const { openOn, closeOn, type } = props;
1024
+ const passHandlers = openOn === "manual" || closeOn === "manual" || closeOn === "clickOutsideContent";
1025
+ const ariaHasPopup = (() => {
1026
+ switch (type) {
1027
+ case "modal": return "dialog";
1028
+ case "tooltip": return "true";
1029
+ default: return "menu";
1030
+ }
1031
+ })();
1032
+ onMount(() => {
1033
+ return setupListeners();
1034
+ });
1035
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [render(trigger, {
1036
+ [triggerRefName]: triggerRef,
1037
+ active: active(),
1038
+ "aria-expanded": active(),
1039
+ "aria-haspopup": ariaHasPopup,
1040
+ ...passHandlers ? {
1041
+ showContent,
1042
+ hideContent
1043
+ } : {}
1044
+ }), () => IS_BROWSER && active() ? /* @__PURE__ */ jsx(Portal, {
1045
+ target: DOMLocation ?? document.body,
1046
+ children: /* @__PURE__ */ jsx(Provider, {
1047
+ ...ctx,
1048
+ children: render(children, {
1049
+ [contentRefName]: contentRef,
1050
+ role: type === "modal" ? "dialog" : void 0,
1051
+ "aria-modal": type === "modal" ? true : void 0,
1052
+ active: active(),
1053
+ align,
1054
+ alignX: alignX(),
1055
+ alignY: alignY(),
1056
+ ...passHandlers ? {
1057
+ showContent,
1058
+ hideContent
1059
+ } : {}
1060
+ })
1061
+ })
1062
+ }) : null] });
1063
+ };
1064
+ const name$3 = `${PKG_NAME}/Overlay`;
1065
+ Component$2.displayName = name$3;
1066
+ Component$2.pkgName = PKG_NAME;
1067
+ Component$2.PYREON__COMPONENT = name$3;
1068
+
1069
+ //#endregion
1070
+ //#region src/Portal/component.tsx
1071
+ const Component$4 = ({ DOMLocation, tag: _tag = "div", children }) => {
1072
+ const target = DOMLocation ?? (typeof document !== "undefined" ? document.body : void 0);
1073
+ if (!target) return null;
1074
+ return /* @__PURE__ */ jsx(Portal, {
1075
+ target,
1076
+ children
1077
+ });
1078
+ };
1079
+ const name$2 = `${PKG_NAME}/Portal`;
1080
+ Component$4.displayName = name$2;
1081
+ Component$4.pkgName = PKG_NAME;
1082
+ Component$4.PYREON__COMPONENT = name$2;
1083
+
1084
+ //#endregion
1085
+ //#region src/Text/styled.ts
1086
+ /**
1087
+ * Styled text primitive that inherits color, font-weight, and line-height
1088
+ * from its parent so it blends seamlessly into any context. Additional
1089
+ * styles can be injected via the responsive `extraStyles` prop processed
1090
+ * through makeItResponsive.
1091
+ */
1092
+ const { styled, css, textComponent } = config;
1093
+ const styles = ({ css: cssFn, theme: t }) => cssFn`
1094
+ ${t.extraStyles && extendCss(t.extraStyles)};
1095
+ `;
1096
+ var styled_default = styled(textComponent)`
1097
+ ${css`
1098
+ color: inherit;
1099
+ font-weight: inherit;
1100
+ line-height: 1;
1101
+ `};
1102
+
1103
+ ${makeItResponsive({
1104
+ key: "$text",
1105
+ styles,
1106
+ css,
1107
+ normalize: false
1108
+ })};
1109
+ `;
1110
+
1111
+ //#endregion
1112
+ //#region src/Text/component.tsx
1113
+ const Component$5 = ({ paragraph, label, children, tag, css, ref, ...props }) => {
1114
+ let finalTag;
1115
+ if (paragraph) finalTag = "p";
1116
+ else finalTag = tag;
1117
+ return /* @__PURE__ */ jsx(styled_default, {
1118
+ ref,
1119
+ as: finalTag,
1120
+ $text: { extraStyles: css },
1121
+ ...props,
1122
+ children: children ?? label
1123
+ });
1124
+ };
1125
+ const name$1 = `${PKG_NAME}/Text`;
1126
+ Component$5.displayName = name$1;
1127
+ Component$5.pkgName = PKG_NAME;
1128
+ Component$5.PYREON__COMPONENT = name$1;
1129
+ Component$5.isText = true;
1130
+
1131
+ //#endregion
1132
+ //#region src/Util/component.tsx
1133
+ const Component$6 = (({ children, className, style }) => {
1134
+ const mergedClasses = Array.isArray(className) ? className.join(" ") : className;
1135
+ const finalProps = {};
1136
+ if (style) finalProps.style = style;
1137
+ if (mergedClasses) finalProps.className = mergedClasses;
1138
+ return render(children, finalProps);
1139
+ });
1140
+ const name = `${PKG_NAME}/Util`;
1141
+ Component$6.displayName = name;
1142
+ Component$6.pkgName = PKG_NAME;
1143
+ Component$6.PYREON__COMPONENT = name;
1144
+
1145
+ //#endregion
1146
+ export { Component as Element, Iterator_default as Iterator, Component$1 as List, Component$2 as Overlay, Component$3 as OverlayProvider, Component$4 as Portal, Provider, Component$5 as Text, Component$6 as Util, useOverlay };
1147
+ //# sourceMappingURL=index.js.map