@pyreon/elements 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Provider, alignContent, extendCss, makeItResponsive, value } from "@pyreon/unistyle";
2
- import { Fragment, Portal, createContext, onMount, provide, splitProps, useContext } from "@pyreon/core";
2
+ import { Fragment, Portal, createContext, nativeCompat, onMount, onUnmount, provide, splitProps, useContext } from "@pyreon/core";
3
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";
4
5
  import { signal } from "@pyreon/reactivity";
5
6
 
6
7
  //#region src/constants.ts
@@ -8,7 +9,7 @@ const PKG_NAME = "@pyreon/elements";
8
9
 
9
10
  //#endregion
10
11
  //#region src/utils.ts
11
- const IS_DEVELOPMENT = import.meta.env?.DEV === true;
12
+ const IS_DEVELOPMENT = process.env.NODE_ENV !== "production";
12
13
 
13
14
  //#endregion
14
15
  //#region src/helpers/Content/styled.ts
@@ -82,58 +83,6 @@ const StyledComponent = styled$2(component$1, { layer: "elements" })`
82
83
  })};
83
84
  `;
84
85
 
85
- //#endregion
86
- //#region ../../core/core/lib/jsx-runtime.js
87
- /** Marker for fragment nodes — renders children without a wrapper element */
88
- const Fragment$1 = Symbol("Pyreon.Fragment");
89
- /**
90
- * Hyperscript function — the compiled output of JSX.
91
- * `<div class="x">hello</div>` → `h("div", { class: "x" }, "hello")`
92
- *
93
- * Generic on P so TypeScript validates props match the component's signature
94
- * at the call site, then stores the result in the loosely-typed VNode.
95
- */
96
- /** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */
97
- const EMPTY_PROPS = {};
98
- function h(type, props, ...children) {
99
- return {
100
- type,
101
- props: props ?? EMPTY_PROPS,
102
- children: normalizeChildren(children),
103
- key: props?.key ?? null
104
- };
105
- }
106
- function normalizeChildren(children) {
107
- for (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);
108
- return children;
109
- }
110
- function flattenChildren(children) {
111
- const result = [];
112
- for (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));
113
- else result.push(child);
114
- return result;
115
- }
116
- /**
117
- * JSX automatic runtime.
118
- *
119
- * When tsconfig has `"jsxImportSource": "@pyreon/core"`, the TS/bundler compiler
120
- * rewrites JSX to imports from this file automatically:
121
- * <div class="x" /> → jsx("div", { class: "x" })
122
- */
123
- function jsx(type, props, key) {
124
- const { children, ...rest } = props;
125
- const propsWithKey = key != null ? {
126
- ...rest,
127
- key
128
- } : rest;
129
- if (typeof type === "function") return h(type, children !== void 0 ? {
130
- ...propsWithKey,
131
- children
132
- } : propsWithKey);
133
- return h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);
134
- }
135
- const jsxs = jsx;
136
-
137
86
  //#endregion
138
87
  //#region src/helpers/Content/component.tsx
139
88
  /**
@@ -182,6 +131,120 @@ const Component$9 = (props) => {
182
131
  //#region src/helpers/Content/index.ts
183
132
  var Content_default = Component$9;
184
133
 
134
+ //#endregion
135
+ //#region src/Element/constants.ts
136
+ /**
137
+ * HTML tags that are inline-level by default. When Element renders one of
138
+ * these tags, child Content wrappers use `span` instead of `div` to
139
+ * preserve valid HTML nesting.
140
+ */
141
+ const INLINE_ELEMENTS = {
142
+ span: true,
143
+ a: true,
144
+ button: true,
145
+ input: true,
146
+ label: true,
147
+ select: true,
148
+ textarea: true,
149
+ br: true,
150
+ img: true,
151
+ strong: true,
152
+ small: true,
153
+ code: true,
154
+ b: true,
155
+ big: true,
156
+ i: true,
157
+ tt: true,
158
+ abbr: true,
159
+ acronym: true,
160
+ cite: true,
161
+ dfn: true,
162
+ em: true,
163
+ kbd: true,
164
+ samp: true,
165
+ var: true,
166
+ bdo: true,
167
+ map: true,
168
+ object: true,
169
+ q: true,
170
+ script: true,
171
+ sub: true,
172
+ sup: true
173
+ };
174
+ /**
175
+ * HTML void/self-closing elements that cannot have children. When Element
176
+ * detects one of these tags, it skips rendering beforeContent/content/afterContent
177
+ * and returns the Wrapper alone.
178
+ */
179
+ const EMPTY_ELEMENTS = {
180
+ area: true,
181
+ base: true,
182
+ br: true,
183
+ col: true,
184
+ embed: true,
185
+ hr: true,
186
+ img: true,
187
+ input: true,
188
+ keygen: true,
189
+ link: true,
190
+ textarea: true,
191
+ source: true,
192
+ track: true,
193
+ wbr: true
194
+ };
195
+
196
+ //#endregion
197
+ //#region src/Element/utils.ts
198
+ /** Checks whether the given HTML tag is an inline-level element, used to determine sub-tag nesting. */
199
+ const isInlineElement = (tag) => {
200
+ if (tag && tag in INLINE_ELEMENTS) return true;
201
+ return false;
202
+ };
203
+ /** Checks whether the given HTML tag is a void element that cannot have children. */
204
+ const getShouldBeEmpty = (tag) => {
205
+ if (tag && tag in EMPTY_ELEMENTS) return true;
206
+ return false;
207
+ };
208
+
209
+ //#endregion
210
+ //#region src/helpers/internElementBundle.ts
211
+ /**
212
+ * Module-scope intern cache for `$element` bundles passed to Wrapper's styled
213
+ * component. Same primitive prop tuple → same object identity, so the styler's
214
+ * `elClassCache` (added 2026-Q2 alongside this) hits and skips the resolve
215
+ * pipeline. Analogous to `@pyreon/rocketstyle`'s dimension-prop memo (PR #344)
216
+ * but at the layer below — covers non-rocketstyle Element / Wrapper / Text usage
217
+ * AND the residual styled wrappers under any rocketstyle component.
218
+ *
219
+ * Cache key is a JSON-stringified shallow snapshot of the bundle. LRU-bound at
220
+ * 256 entries; oldest-first eviction. Bail (return the input as-is, no cache)
221
+ * when any value is a function (signal accessor) or a non-string object (CSS
222
+ * callback / CSSResult / nested object) — those cannot be safely round-tripped
223
+ * through JSON without losing identity guarantees.
224
+ */
225
+ const _bundleCache = /* @__PURE__ */ new Map();
226
+ const BUNDLE_CAP = 256;
227
+ const internElementBundle = (bundle) => {
228
+ for (const k in bundle) {
229
+ const v = bundle[k];
230
+ if (typeof v === "function") return bundle;
231
+ if (v != null && typeof v === "object") return bundle;
232
+ }
233
+ const key = JSON.stringify(bundle);
234
+ const existing = _bundleCache.get(key);
235
+ if (existing) {
236
+ _bundleCache.delete(key);
237
+ _bundleCache.set(key, existing);
238
+ return existing;
239
+ }
240
+ if (_bundleCache.size >= BUNDLE_CAP) {
241
+ const oldest = _bundleCache.keys().next().value;
242
+ if (oldest !== void 0) _bundleCache.delete(oldest);
243
+ }
244
+ _bundleCache.set(key, bundle);
245
+ return bundle;
246
+ };
247
+
185
248
  //#endregion
186
249
  //#region src/helpers/Wrapper/styled.ts
187
250
  /**
@@ -201,28 +264,25 @@ const childFixCSS = `
201
264
  const parentFixCSS = `
202
265
  flex-direction: column;
203
266
  `;
204
- const fullHeightCSS = `
205
- height: 100%;
206
- `;
207
- const blockCSS = `
208
- align-self: stretch;
209
- flex: 1;
210
- min-width: 0;
211
- `;
212
- const childFixPosition = (isBlock) => `display: ${isBlock ? "flex" : "inline-flex"};`;
213
267
  const styles$1 = ({ theme: t, css: cssFn }) => cssFn`
214
- ${t.alignY === "block" && fullHeightCSS};
215
-
216
268
  ${alignContent({
217
269
  direction: t.direction,
218
270
  alignX: t.alignX,
219
271
  alignY: t.alignY
220
272
  })};
221
273
 
222
- ${t.block && blockCSS};
223
- ${t.alignY === "block" && t.block && fullHeightCSS};
274
+ /*
275
+ * Always emit a value for the block-related properties so a responsive
276
+ * theme that flips from \`block: true\` at one breakpoint to \`block: false\`
277
+ * at another resets cleanly. Previously \`align-self\` / \`width\` / \`height\`
278
+ * were only set when the truthy branch matched, which left the prior
279
+ * breakpoint's values cascading through.
280
+ */
281
+ ${`align-self: ${t.block ? "stretch" : "auto"};
282
+ width: ${t.block ? "100%" : "auto"};
283
+ height: ${t.alignY === "block" ? "100%" : "auto"};`};
224
284
 
225
- ${!t.childFix && childFixPosition(t.block)};
285
+ ${!t.childFix && `display: ${t.block ? "flex" : "inline-flex"};`};
226
286
  ${t.parentFix && parentFixCSS};
227
287
 
228
288
  ${t.extraStyles && extendCss(t.extraStyles)};
@@ -292,36 +352,63 @@ const Component$8 = (props) => {
292
352
  ref: own.ref,
293
353
  as: own.tag
294
354
  };
295
- if (!(!own.dangerouslySetInnerHTML && isWebFixNeeded(own.tag))) return /* @__PURE__ */ jsx(styled_default$1, {
296
- ...commonProps,
297
- $element: {
355
+ const needsFix = !own.dangerouslySetInnerHTML && isWebFixNeeded(own.tag);
356
+ const isVoidTag = !own.dangerouslySetInnerHTML && getShouldBeEmpty(own.tag);
357
+ const innerHTML = own.dangerouslySetInnerHTML;
358
+ if (!needsFix) {
359
+ const bundle = internElementBundle({
298
360
  block: own.block,
299
361
  direction: own.direction,
300
362
  alignX: own.alignX,
301
363
  alignY: own.alignY,
302
364
  equalCols: own.equalCols,
303
365
  extraStyles: own.extendCss
304
- },
305
- children: own.children
306
- });
366
+ });
367
+ if (isVoidTag) return /* @__PURE__ */ jsx(styled_default$1, {
368
+ ...commonProps,
369
+ $element: bundle
370
+ });
371
+ if (innerHTML) return /* @__PURE__ */ jsx(styled_default$1, {
372
+ ...commonProps,
373
+ $element: bundle,
374
+ dangerouslySetInnerHTML: innerHTML
375
+ });
376
+ return /* @__PURE__ */ jsx(styled_default$1, {
377
+ ...commonProps,
378
+ $element: bundle,
379
+ children: own.children
380
+ });
381
+ }
307
382
  const asTag = own.isInline ? "span" : "div";
383
+ const parentBundle = internElementBundle({
384
+ parentFix: true,
385
+ block: own.block,
386
+ extraStyles: own.extendCss
387
+ });
388
+ const childBundle = internElementBundle({
389
+ childFix: true,
390
+ direction: own.direction,
391
+ alignX: own.alignX,
392
+ alignY: own.alignY,
393
+ equalCols: own.equalCols
394
+ });
395
+ if (innerHTML) return /* @__PURE__ */ jsx(styled_default$1, {
396
+ ...commonProps,
397
+ $element: parentBundle,
398
+ children: /* @__PURE__ */ jsx(styled_default$1, {
399
+ as: asTag,
400
+ $childFix: true,
401
+ $element: childBundle,
402
+ dangerouslySetInnerHTML: innerHTML
403
+ })
404
+ });
308
405
  return /* @__PURE__ */ jsx(styled_default$1, {
309
406
  ...commonProps,
310
- $element: {
311
- parentFix: true,
312
- block: own.block,
313
- extraStyles: own.extendCss
314
- },
407
+ $element: parentBundle,
315
408
  children: /* @__PURE__ */ jsx(styled_default$1, {
316
409
  as: asTag,
317
410
  $childFix: true,
318
- $element: {
319
- childFix: true,
320
- direction: own.direction,
321
- alignX: own.alignX,
322
- alignY: own.alignY,
323
- equalCols: own.equalCols
324
- },
411
+ $element: childBundle,
325
412
  children: own.children
326
413
  })
327
414
  });
@@ -331,81 +418,6 @@ const Component$8 = (props) => {
331
418
  //#region src/helpers/Wrapper/index.ts
332
419
  var Wrapper_default = Component$8;
333
420
 
334
- //#endregion
335
- //#region src/Element/constants.ts
336
- /**
337
- * HTML tags that are inline-level by default. When Element renders one of
338
- * these tags, child Content wrappers use `span` instead of `div` to
339
- * preserve valid HTML nesting.
340
- */
341
- const INLINE_ELEMENTS = {
342
- span: true,
343
- a: true,
344
- button: true,
345
- input: true,
346
- label: true,
347
- select: true,
348
- textarea: true,
349
- br: true,
350
- img: true,
351
- strong: true,
352
- small: true,
353
- code: true,
354
- b: true,
355
- big: true,
356
- i: true,
357
- tt: true,
358
- abbr: true,
359
- acronym: true,
360
- cite: true,
361
- dfn: true,
362
- em: true,
363
- kbd: true,
364
- samp: true,
365
- var: true,
366
- bdo: true,
367
- map: true,
368
- object: true,
369
- q: true,
370
- script: true,
371
- sub: true,
372
- sup: true
373
- };
374
- /**
375
- * HTML void/self-closing elements that cannot have children. When Element
376
- * detects one of these tags, it skips rendering beforeContent/content/afterContent
377
- * and returns the Wrapper alone.
378
- */
379
- const EMPTY_ELEMENTS = {
380
- area: true,
381
- base: true,
382
- br: true,
383
- col: true,
384
- embed: true,
385
- hr: true,
386
- img: true,
387
- input: true,
388
- keygen: true,
389
- link: true,
390
- textarea: true,
391
- source: true,
392
- track: true,
393
- wbr: true
394
- };
395
-
396
- //#endregion
397
- //#region src/Element/utils.ts
398
- /** Checks whether the given HTML tag is an inline-level element, used to determine sub-tag nesting. */
399
- const isInlineElement = (tag) => {
400
- if (tag && tag in INLINE_ELEMENTS) return true;
401
- return false;
402
- };
403
- /** Checks whether the given HTML tag is a void element that cannot have children. */
404
- const getShouldBeEmpty = (tag) => {
405
- if (tag && tag in EMPTY_ELEMENTS) return true;
406
- return false;
407
- };
408
-
409
421
  //#endregion
410
422
  //#region src/Element/component.tsx
411
423
  /**
@@ -500,7 +512,13 @@ const Component = (props) => {
500
512
  else if (externalRef != null) externalRef.current = node;
501
513
  };
502
514
  if (own.equalBeforeAfter && own.beforeContent && own.afterContent) onMount(() => {
503
- if (equalizeRef) equalize(equalizeRef, own.direction);
515
+ const node = equalizeRef;
516
+ if (!node) return void 0;
517
+ equalize(node, own.direction);
518
+ if (typeof ResizeObserver === "undefined") return void 0;
519
+ const observer = new ResizeObserver(() => equalize(node, own.direction));
520
+ observer.observe(node);
521
+ return () => observer.disconnect();
504
522
  });
505
523
  const WRAPPER_PROPS = {
506
524
  ref: mergedRef,
@@ -522,14 +540,14 @@ const Component = (props) => {
522
540
  ...WRAPPER_DEV_PROPS,
523
541
  ref: mergedRef,
524
542
  as: own.tag,
525
- $element: {
543
+ $element: internElementBundle({
526
544
  block: own.block,
527
545
  direction: wrapperDirection,
528
546
  alignX: wrapperAlignX,
529
547
  alignY: wrapperAlignY,
530
548
  equalCols: own.equalCols,
531
549
  extraStyles: own.css
532
- },
550
+ }),
533
551
  children: render(getChildren())
534
552
  });
535
553
  if (isSimpleElement) return /* @__PURE__ */ jsx(Wrapper_default, {
@@ -731,34 +749,28 @@ const Component$7 = (props) => {
731
749
  };
732
750
  return renderItems();
733
751
  };
734
- var component_default = Object.assign(Component$7, {
752
+ const Iterator = Object.assign(Component$7, {
735
753
  isIterator: true,
736
754
  RESERVED_PROPS
737
755
  });
738
756
 
739
757
  //#endregion
740
758
  //#region src/helpers/Iterator/index.ts
741
- var Iterator_default = component_default;
759
+ var Iterator_default = Iterator;
742
760
 
743
761
  //#endregion
744
762
  //#region src/List/component.tsx
745
- /**
746
- * List component that combines Iterator (data-driven rendering) with an
747
- * optional Element root wrapper. When `rootElement` is false (default),
748
- * it renders a bare Iterator as a fragment. When true, the Iterator output
749
- * is wrapped in an Element that receives all non-iterator props (e.g.,
750
- * layout, alignment, css), allowing the list to be styled as a single block.
751
- */
752
- const Component$1 = ((allProps) => {
763
+ const LooseIterator = Iterator_default;
764
+ const Component$1 = (allProps) => {
753
765
  const [own, props] = splitProps(allProps, ["rootElement", "ref"]);
754
- const renderedList = /* @__PURE__ */ jsx(Iterator_default, { ...pick(props, Iterator_default.RESERVED_PROPS) });
766
+ const renderedList = /* @__PURE__ */ jsx(LooseIterator, { ...pick(props, Iterator_default.RESERVED_PROPS) });
755
767
  if (!own.rootElement) return renderedList;
756
768
  return /* @__PURE__ */ jsx(Component, {
757
- ...own.ref ? { ref: own.ref } : {},
769
+ ref: own.ref,
758
770
  ...omit(props, Iterator_default.RESERVED_PROPS),
759
771
  children: renderedList
760
772
  });
761
- });
773
+ };
762
774
  const name$4 = `${PKG_NAME}/List`;
763
775
  Component$1.displayName = name$4;
764
776
  Component$1.pkgName = PKG_NAME;
@@ -776,6 +788,7 @@ const Component$3 = (props) => {
776
788
  });
777
789
  return /* @__PURE__ */ jsx(Fragment$1, { children: props.children });
778
790
  };
791
+ nativeCompat(Component$3);
779
792
 
780
793
  //#endregion
781
794
  //#region src/Overlay/positioning.ts
@@ -1232,14 +1245,21 @@ const name$3 = `${PKG_NAME}/Overlay`;
1232
1245
  Component$2.displayName = name$3;
1233
1246
  Component$2.pkgName = PKG_NAME;
1234
1247
  Component$2.PYREON__COMPONENT = name$3;
1248
+ nativeCompat(Component$2);
1235
1249
 
1236
1250
  //#endregion
1237
1251
  //#region src/Portal/component.tsx
1238
1252
  const Component$4 = (props) => {
1239
- const target = props.DOMLocation ?? (typeof document !== "undefined" ? document.body : void 0);
1240
- if (!target) return null;
1253
+ if (typeof document === "undefined") return null;
1254
+ const tag = props.tag ?? "div";
1255
+ const target = props.DOMLocation ?? document.body;
1256
+ const wrapper = document.createElement(tag);
1257
+ target.appendChild(wrapper);
1258
+ onUnmount(() => {
1259
+ wrapper.remove();
1260
+ });
1241
1261
  return /* @__PURE__ */ jsx(Portal, {
1242
- target,
1262
+ target: wrapper,
1243
1263
  children: props.children
1244
1264
  });
1245
1265
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/elements",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Foundational UI components for Pyreon",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "files": [
12
12
  "lib",
13
+ "!lib/**/*.map",
13
14
  "!lib/analysis",
14
15
  "README.md",
15
16
  "LICENSE",
@@ -41,21 +42,21 @@
41
42
  "typecheck": "tsc --noEmit"
42
43
  },
43
44
  "devDependencies": {
44
- "@pyreon/core": "^0.14.0",
45
- "@pyreon/reactivity": "^0.14.0",
46
- "@pyreon/runtime-dom": "^0.14.0",
47
- "@pyreon/test-utils": "^0.13.2",
48
- "@pyreon/typescript": "^0.14.0",
45
+ "@pyreon/core": "^0.16.0",
46
+ "@pyreon/reactivity": "^0.16.0",
47
+ "@pyreon/runtime-dom": "^0.16.0",
48
+ "@pyreon/test-utils": "^0.13.3",
49
+ "@pyreon/typescript": "^0.16.0",
49
50
  "@vitest/browser-playwright": "^4.1.4",
50
- "@vitus-labs/tools-rolldown": "^1.15.3"
51
- },
52
- "peerDependencies": {
53
- "@pyreon/core": "^0.14.0",
54
- "@pyreon/reactivity": "^0.14.0",
55
- "@pyreon/ui-core": "^0.14.0",
56
- "@pyreon/unistyle": "^0.14.0"
51
+ "@vitus-labs/tools-rolldown": "^2.3.0"
57
52
  },
58
53
  "engines": {
59
54
  "node": ">= 22"
55
+ },
56
+ "dependencies": {
57
+ "@pyreon/core": "^0.16.0",
58
+ "@pyreon/reactivity": "^0.16.0",
59
+ "@pyreon/ui-core": "^0.16.0",
60
+ "@pyreon/unistyle": "^0.16.0"
60
61
  }
61
62
  }
@@ -11,6 +11,7 @@ import { onMount, splitProps } from '@pyreon/core'
11
11
  import { render } from '@pyreon/ui-core'
12
12
  import { PKG_NAME } from '../constants'
13
13
  import { Content, Wrapper } from '../helpers'
14
+ import { internElementBundle } from '../helpers/internElementBundle'
14
15
  import WrapperStyled from '../helpers/Wrapper/styled'
15
16
  import { isWebFixNeeded } from '../helpers/Wrapper/utils'
16
17
  import { IS_DEVELOPMENT } from '../utils'
@@ -136,9 +137,20 @@ const Component: PyreonElement = (props) => {
136
137
  }
137
138
 
138
139
  if (own.equalBeforeAfter && own.beforeContent && own.afterContent) {
140
+ // Run once on mount AND continue equalizing as the element resizes —
141
+ // catches async slot content (font swaps, lazy text, viewport resize)
142
+ // that a one-shot measurement would miss. Mirrors vitus-labs's Element
143
+ // useLayoutEffect + ResizeObserver pattern.
139
144
  onMount(() => {
140
- if (equalizeRef) equalize(equalizeRef, own.direction)
141
- return undefined
145
+ const node = equalizeRef
146
+ if (!node) return undefined
147
+
148
+ equalize(node, own.direction)
149
+
150
+ if (typeof ResizeObserver === 'undefined') return undefined
151
+ const observer = new ResizeObserver(() => equalize(node, own.direction))
152
+ observer.observe(node)
153
+ return () => observer.disconnect()
142
154
  })
143
155
  }
144
156
 
@@ -180,14 +192,14 @@ const Component: PyreonElement = (props) => {
180
192
  {...WRAPPER_DEV_PROPS}
181
193
  ref={mergedRef}
182
194
  as={own.tag}
183
- $element={{
195
+ $element={internElementBundle({
184
196
  block: own.block,
185
197
  direction: wrapperDirection,
186
198
  alignX: wrapperAlignX,
187
199
  alignY: wrapperAlignY,
188
200
  equalCols: own.equalCols,
189
201
  extraStyles: own.css,
190
- }}
202
+ })}
191
203
  >
192
204
  {render(getChildren())}
193
205
  </WrapperStyled>