@pyreon/elements 0.15.0 → 0.18.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.
@@ -23,6 +23,80 @@ export type ExtendedProps = {
23
23
  position: number
24
24
  }
25
25
 
26
+ // ---------------------------------------------------------------------------
27
+ // Per-mode prop shapes — narrowed via the `T` data-element type
28
+ // ---------------------------------------------------------------------------
29
+
30
+ /**
31
+ * Iterator over an array of strings/numbers. Each item is wrapped in
32
+ * `{ [valueName]: item }` and that object is what callbacks see + what's
33
+ * spread onto the rendered component.
34
+ */
35
+ export type SimpleProps<T extends SimpleValue> = {
36
+ data: Array<T | MaybeNull>
37
+ /** A component to be rendered per item. */
38
+ component: ElementType
39
+ /**
40
+ * Key under which each primitive value is exposed to `component` and
41
+ * callbacks. Defaults to `'children'` at runtime — i.e. the value is
42
+ * passed to the component as its children.
43
+ */
44
+ valueName?: string
45
+ /** Optional wrapper around each item. */
46
+ wrapComponent?: ElementType
47
+ /** Stable key per item (defaults to index). */
48
+ itemKey?: (item: T, index: number) => SimpleValue
49
+ /** Extra props merged onto the rendered component, optionally per-item. */
50
+ itemProps?: TObj | ((item: { [k: string]: T }, ext: ExtendedProps) => TObj)
51
+ /** Extra props merged onto the wrapper, optionally per-item. */
52
+ wrapProps?: TObj | ((item: { [k: string]: T }, ext: ExtendedProps) => TObj)
53
+ children?: never
54
+ }
55
+
56
+ /**
57
+ * Iterator over an array of objects. Each item is spread onto the rendered
58
+ * component as props. Per-item `component` overrides also work — when an
59
+ * item carries its own `component` field, the wrapper is bypassed.
60
+ */
61
+ export type ObjectProps<T extends ObjectValue> = {
62
+ data: Array<T | MaybeNull>
63
+ /** Default component to be rendered per item (item-level `component` overrides). */
64
+ component: ElementType
65
+ /** `valueName` is meaningless when iterating objects — TS forbids it. */
66
+ valueName?: never
67
+ /** Optional wrapper around each item. */
68
+ wrapComponent?: ElementType
69
+ /** Stable key per item — pick a key from the item, or compute it. */
70
+ itemKey?: keyof T | ((item: T, index: number) => SimpleValue)
71
+ /** Extra props merged onto the rendered component, optionally per-item. */
72
+ itemProps?: TObj | ((item: T, ext: ExtendedProps) => TObj)
73
+ /** Extra props merged onto the wrapper, optionally per-item. */
74
+ wrapProps?: TObj | ((item: T, ext: ExtendedProps) => TObj)
75
+ children?: never
76
+ }
77
+
78
+ /**
79
+ * Iterator over `children` — no `data`/`component`. Each child gets
80
+ * positional metadata via `itemProps` and an optional `wrapComponent`.
81
+ */
82
+ export type ChildrenProps = {
83
+ children: VNodeChild
84
+ data?: never
85
+ component?: never
86
+ valueName?: never
87
+ itemKey?: never
88
+ wrapComponent?: ElementType
89
+ itemProps?: TObj | ((_: Record<string, never>, ext: ExtendedProps) => TObj)
90
+ wrapProps?: TObj | ((_: Record<string, never>, ext: ExtendedProps) => TObj)
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Loose backward-compatible fallback shape (today's behavior)
95
+ //
96
+ // Used when callers don't (or can't) parameterize `Props<T>` — keeps the
97
+ // existing call surface intact so this refactor lands non-breaking.
98
+ // ---------------------------------------------------------------------------
99
+
26
100
  export type PropsCallback =
27
101
  | TObj
28
102
  | ((
@@ -30,50 +104,35 @@ export type PropsCallback =
30
104
  extendedProps: ExtendedProps,
31
105
  ) => TObj)
32
106
 
33
- export type Props = Partial<{
34
- /**
35
- * Valid children
36
- */
107
+ export type LooseProps = Partial<{
37
108
  children: VNodeChild
38
-
39
- /**
40
- * Array of data passed to `component` prop
41
- */
42
109
  data: Array<SimpleValue | ObjectValue | MaybeNull>
43
-
44
- /**
45
- * A component to be rendered within list
46
- */
47
110
  component: ElementType
48
-
49
- /**
50
- * Defines name of the prop to be passed to the iteration component
51
- * when **data** prop is type of `string[]`, `number[]` or combination
52
- * of both. Otherwise ignored.
53
- */
54
111
  valueName: string
55
-
56
- /**
57
- * A component to be rendered within list. `wrapComponent`
58
- * wraps `component`. Therefore it can be used to enhance the behavior
59
- * of the list component
60
- */
61
112
  wrapComponent: ElementType
62
-
63
- /**
64
- * Extension of **item** `component` props to be passed
65
- */
66
113
  itemProps: PropsCallback
67
-
68
- /**
69
- * Extension of **item** `wrapComponent` props to be passed
70
- */
71
- wrapProps?: PropsCallback
72
-
73
- /**
74
- * Extension of **item** `wrapComponent` props to be passed
75
- */
76
- itemKey?:
114
+ wrapProps: PropsCallback
115
+ itemKey:
77
116
  | keyof ObjectValue
78
117
  | ((item: SimpleValue | Omit<ObjectValue, 'component'>, index: number) => SimpleValue)
79
118
  }>
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Public, generic-aware Props<T>
122
+ //
123
+ // Props<string> → SimpleProps<string> (valueName REQUIRED)
124
+ // Props<{ id; name }> → ObjectProps<{...}> (valueName FORBIDDEN)
125
+ // Props<unknown> / Props → LooseProps (today's behavior)
126
+ //
127
+ // `unknown extends T` is the canonical "did the caller actually narrow T?"
128
+ // check — true only when T is left as `unknown`, false for any concrete
129
+ // narrowing. Fallback to LooseProps preserves existing call sites.
130
+ // ---------------------------------------------------------------------------
131
+
132
+ export type Props<T = unknown> = unknown extends T
133
+ ? LooseProps
134
+ : T extends SimpleValue
135
+ ? SimpleProps<T>
136
+ : T extends ObjectValue
137
+ ? ObjectProps<T>
138
+ : ChildrenProps
@@ -4,7 +4,7 @@
4
4
  * fix (parent + child Styled) because these HTML elements do not natively
5
5
  * support `display: flex` consistently across browsers.
6
6
  */
7
- import { splitProps } from '@pyreon/core'
7
+ import { h, splitProps } from '@pyreon/core'
8
8
  import { getShouldBeEmpty } from '../../Element/utils'
9
9
  import { IS_DEVELOPMENT } from '../../utils'
10
10
  import { internElementBundle } from '../internElementBundle'
@@ -14,6 +14,46 @@ import { isWebFixNeeded } from './utils'
14
14
 
15
15
  const DEV_PROPS: Record<string, string> = IS_DEVELOPMENT ? { 'data-pyr-element': 'Element' } : {}
16
16
 
17
+ /**
18
+ * Build a props object for `h(Styled, ...)` by copying own property
19
+ * DESCRIPTORS from `rest`, then layering the additional fields. Compiler-
20
+ * emitted reactive props (`_rp(() => signal())` converted to getters by
21
+ * `makeReactiveProps`) survive end-to-end with their getter intact.
22
+ *
23
+ * Why we bypass JSX spread here: the standard JSX automatic-runtime
24
+ * compilation lowers `<Styled {...rest} foo={x}>` to roughly
25
+ * `jsx(Styled, { ...rest, foo: x })`. That `{...rest, foo: x}` object
26
+ * literal is evaluated at JS level — it fires every getter on `rest` and
27
+ * stores the resolved value before `jsx()` ever sees the object. No
28
+ * amount of in-runtime descriptor preservation can recover the getters
29
+ * once they've been collapsed by the surface-level spread. The fix is
30
+ * structural: don't use JSX spread for reactive-prop forwarding. Build
31
+ * the props object with descriptor preservation and pass it to `h()`
32
+ * directly — `h()` stores props as-is on the vnode, no copy, getters
33
+ * survive into mount.
34
+ */
35
+ const buildStyledProps = (
36
+ rest: Record<string, unknown>,
37
+ refValue: unknown,
38
+ asTag: unknown,
39
+ extras: Record<string, unknown>,
40
+ ): Record<string, unknown> => {
41
+ const result: Record<string, unknown> = {}
42
+ const descriptors = Object.getOwnPropertyDescriptors(rest)
43
+ for (const key in descriptors) {
44
+ Object.defineProperty(result, key, descriptors[key]!)
45
+ }
46
+ for (const key in DEV_PROPS) {
47
+ result[key] = DEV_PROPS[key]
48
+ }
49
+ result.ref = refValue
50
+ result.as = asTag
51
+ for (const key in extras) {
52
+ result[key] = extras[key]
53
+ }
54
+ return result
55
+ }
56
+
17
57
  // Layout / ref keys consumed by Wrapper itself. Everything else is forwarded
18
58
  // onto the underlying DOM node. Listed as a tuple so `splitProps` narrows
19
59
  // `own` correctly while preserving reactive prop tracking on both halves.
@@ -34,13 +74,6 @@ const OWN_KEYS: Array<keyof Props | 'ref'> = [
34
74
  const Component = (props: Partial<Props> & { ref?: unknown }) => {
35
75
  const [own, rest] = splitProps(props, OWN_KEYS)
36
76
 
37
- const commonProps = {
38
- ...rest,
39
- ...DEV_PROPS,
40
- ref: own.ref,
41
- as: own.tag,
42
- }
43
-
44
77
  const needsFix = !own.dangerouslySetInnerHTML && isWebFixNeeded(own.tag)
45
78
 
46
79
  // Void HTML elements (hr, input, img, br, …) cannot have children. Even
@@ -50,6 +83,15 @@ const Component = (props: Partial<Props> & { ref?: unknown }) => {
50
83
  // slot is dropped here too instead of leaking into the JSX.
51
84
  const isVoidTag = !own.dangerouslySetInnerHTML && getShouldBeEmpty(own.tag)
52
85
 
86
+ // dangerouslySetInnerHTML and children are mutually exclusive — both
87
+ // become inner content (per `runtime-server/src/index.ts:228` and
88
+ // `runtime-dom/src/props.ts:289`). Pre-fix the prop was in OWN_KEYS,
89
+ // moved into `own` by splitProps, and never re-attached to the rendered
90
+ // vnode — so `<Logo dangerouslySetInnerHTML={...} />` rendered an empty
91
+ // <div></div>. Forward the prop to the styled vnode and drop the
92
+ // children slot when innerHTML is set.
93
+ const innerHTML = own.dangerouslySetInnerHTML
94
+
53
95
  if (!needsFix) {
54
96
  const bundle = internElementBundle({
55
97
  block: own.block,
@@ -60,12 +102,28 @@ const Component = (props: Partial<Props> & { ref?: unknown }) => {
60
102
  extraStyles: own.extendCss,
61
103
  })
62
104
  if (isVoidTag) {
63
- return <Styled {...commonProps} $element={bundle} />
105
+ return h(
106
+ Styled,
107
+ buildStyledProps(rest as unknown as Record<string, unknown>, own.ref, own.tag, {
108
+ $element: bundle,
109
+ }),
110
+ )
111
+ }
112
+ if (innerHTML) {
113
+ return h(
114
+ Styled,
115
+ buildStyledProps(rest as unknown as Record<string, unknown>, own.ref, own.tag, {
116
+ $element: bundle,
117
+ dangerouslySetInnerHTML: innerHTML,
118
+ }),
119
+ )
64
120
  }
65
- return (
66
- <Styled {...commonProps} $element={bundle}>
67
- {own.children}
68
- </Styled>
121
+ return h(
122
+ Styled,
123
+ buildStyledProps(rest as unknown as Record<string, unknown>, own.ref, own.tag, {
124
+ $element: bundle,
125
+ children: own.children,
126
+ }),
69
127
  )
70
128
  }
71
129
 
@@ -83,12 +141,39 @@ const Component = (props: Partial<Props> & { ref?: unknown }) => {
83
141
  equalCols: own.equalCols,
84
142
  })
85
143
 
86
- return (
87
- <Styled {...commonProps} $element={parentBundle}>
88
- <Styled as={asTag} $childFix $element={childBundle}>
89
- {own.children}
90
- </Styled>
91
- </Styled>
144
+ // needsFix path: innerHTML belongs on the INNER styled node (where the
145
+ // actual content lives), NOT on the outer flex-fix wrapper. The
146
+ // `needsFix` computation already excludes the innerHTML case
147
+ // (`!own.dangerouslySetInnerHTML && isWebFixNeeded(own.tag)`), so this
148
+ // branch normally won't execute when innerHTML is set — but we keep
149
+ // the defensive forwarding so the contract is robust against future
150
+ // refactors of the needsFix gate.
151
+ if (innerHTML) {
152
+ return h(
153
+ Styled,
154
+ buildStyledProps(rest as unknown as Record<string, unknown>, own.ref, own.tag, {
155
+ $element: parentBundle,
156
+ children: h(Styled, {
157
+ as: asTag,
158
+ $childFix: true,
159
+ $element: childBundle,
160
+ dangerouslySetInnerHTML: innerHTML,
161
+ }),
162
+ }),
163
+ )
164
+ }
165
+
166
+ return h(
167
+ Styled,
168
+ buildStyledProps(rest as unknown as Record<string, unknown>, own.ref, own.tag, {
169
+ $element: parentBundle,
170
+ children: h(Styled, {
171
+ as: asTag,
172
+ $childFix: true,
173
+ $element: childBundle,
174
+ children: own.children,
175
+ }),
176
+ }),
92
177
  )
93
178
  }
94
179
 
@@ -23,31 +23,25 @@ const parentFixCSS = `
23
23
  flex-direction: column;
24
24
  `
25
25
 
26
- const fullHeightCSS = `
27
- height: 100%;
28
- `
29
-
30
- const blockCSS = `
31
- align-self: stretch;
32
- flex: 1;
33
- min-width: 0;
34
- `
35
-
36
- const childFixPosition = (isBlock?: boolean) => `display: ${isBlock ? 'flex' : 'inline-flex'};`
37
-
38
- const styles: ResponsiveStylesCallback = ({ theme: t, css: cssFn }) => cssFn`
39
- ${t.alignY === 'block' && fullHeightCSS};
40
-
26
+ export const styles: ResponsiveStylesCallback = ({ theme: t, css: cssFn }) => cssFn`
41
27
  ${alignContent({
42
28
  direction: t.direction,
43
29
  alignX: t.alignX,
44
30
  alignY: t.alignY,
45
31
  })};
46
32
 
47
- ${t.block && blockCSS};
48
- ${t.alignY === 'block' && t.block && fullHeightCSS};
33
+ /*
34
+ * Always emit a value for the block-related properties so a responsive
35
+ * theme that flips from \`block: true\` at one breakpoint to \`block: false\`
36
+ * at another resets cleanly. Previously \`align-self\` / \`width\` / \`height\`
37
+ * were only set when the truthy branch matched, which left the prior
38
+ * breakpoint's values cascading through.
39
+ */
40
+ ${`align-self: ${t.block ? 'stretch' : 'auto'};
41
+ width: ${t.block ? '100%' : 'auto'};
42
+ height: ${t.alignY === 'block' ? '100%' : 'auto'};`};
49
43
 
50
- ${!t.childFix && childFixPosition(t.block)};
44
+ ${!t.childFix && `display: ${t.block ? 'flex' : 'inline-flex'};`};
51
45
  ${t.parentFix && parentFixCSS};
52
46
 
53
47
  ${t.extraStyles && extendCss(t.extraStyles as Parameters<typeof extendCss>[0])};
package/src/index.ts CHANGED
@@ -3,12 +3,16 @@ import { Provider } from '@pyreon/unistyle'
3
3
  export type { ElementProps, PyreonElement } from './Element'
4
4
  export { Element } from './Element'
5
5
  export type {
6
+ ChildrenProps as IteratorChildrenProps,
6
7
  ElementType,
7
8
  ExtendedProps,
9
+ LooseProps as IteratorLooseProps,
8
10
  MaybeNull,
11
+ ObjectProps as IteratorObjectProps,
9
12
  ObjectValue,
10
13
  Props as IteratorProps,
11
14
  PropsCallback,
15
+ SimpleProps as IteratorSimpleProps,
12
16
  SimpleValue,
13
17
  } from './helpers/Iterator'
14
18
  export { default as Iterator } from './helpers/Iterator'
package/src/types.ts CHANGED
@@ -69,8 +69,39 @@ export type Responsive =
69
69
 
70
70
  export type ExtendCss = Css | Css[] | Partial<Record<BreakpointKeys, Css>>
71
71
 
72
- export type ExtractProps<TComponentOrTProps> =
73
- TComponentOrTProps extends ComponentFn<infer TProps> ? TProps : TComponentOrTProps
72
+ /**
73
+ * Extracts the props type from a Pyreon component function — multi-overload
74
+ * aware. Matches up to 4 call signatures and produces the UNION of their
75
+ * first-argument types. Iterator / List ship 3-overload primitives whose
76
+ * LAST overload is `ChildrenProps` (the loosest); without overload-aware
77
+ * extraction, `ExtractProps<Iterator>` returned just `ChildrenProps` and
78
+ * lost `SimpleProps<T>` / `ObjectProps<T>` shapes. Mirrors vitus-labs
79
+ * PR #222.
80
+ *
81
+ * Kept in sync with the copies in `@pyreon/core` / `@pyreon/attrs` /
82
+ * `@pyreon/rocketstyle`.
83
+ */
84
+ export type ExtractProps<TComponentOrTProps> = TComponentOrTProps extends {
85
+ (props: infer P1, ...args: any): any
86
+ (props: infer P2, ...args: any): any
87
+ (props: infer P3, ...args: any): any
88
+ (props: infer P4, ...args: any): any
89
+ }
90
+ ? P1 | P2 | P3 | P4
91
+ : TComponentOrTProps extends {
92
+ (props: infer P1, ...args: any): any
93
+ (props: infer P2, ...args: any): any
94
+ (props: infer P3, ...args: any): any
95
+ }
96
+ ? P1 | P2 | P3
97
+ : TComponentOrTProps extends {
98
+ (props: infer P1, ...args: any): any
99
+ (props: infer P2, ...args: any): any
100
+ }
101
+ ? P1 | P2
102
+ : TComponentOrTProps extends ComponentFn<infer TProps>
103
+ ? TProps
104
+ : TComponentOrTProps
74
105
 
75
106
  export type PyreonComponent<P extends Record<string, any> = {}> = ComponentFn<P> & PyreonStatic
76
107