@pyreon/elements 0.11.1 → 0.11.3

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.
Files changed (52) hide show
  1. package/package.json +8 -7
  2. package/src/Element/component.tsx +211 -0
  3. package/src/Element/constants.ts +96 -0
  4. package/src/Element/index.ts +6 -0
  5. package/src/Element/types.ts +168 -0
  6. package/src/Element/utils.ts +15 -0
  7. package/src/List/component.tsx +57 -0
  8. package/src/List/index.ts +5 -0
  9. package/src/Overlay/component.tsx +131 -0
  10. package/src/Overlay/context.tsx +37 -0
  11. package/src/Overlay/index.ts +7 -0
  12. package/src/Overlay/useOverlay.tsx +616 -0
  13. package/src/Portal/component.tsx +41 -0
  14. package/src/Portal/index.ts +5 -0
  15. package/src/Text/component.tsx +65 -0
  16. package/src/Text/index.ts +5 -0
  17. package/src/Text/styled.ts +30 -0
  18. package/src/Util/component.tsx +43 -0
  19. package/src/Util/index.ts +5 -0
  20. package/src/__tests__/Content.test.tsx +115 -0
  21. package/src/__tests__/Element.test.ts +604 -0
  22. package/src/__tests__/Iterator.test.ts +483 -0
  23. package/src/__tests__/List.test.ts +199 -0
  24. package/src/__tests__/Overlay.test.ts +485 -0
  25. package/src/__tests__/Portal.test.ts +82 -0
  26. package/src/__tests__/Text.test.ts +274 -0
  27. package/src/__tests__/Util.test.ts +63 -0
  28. package/src/__tests__/Wrapper.test.tsx +152 -0
  29. package/src/__tests__/equalBeforeAfter.test.ts +122 -0
  30. package/src/__tests__/helpers.test.ts +65 -0
  31. package/src/__tests__/overlayContext.test.tsx +78 -0
  32. package/src/__tests__/responsiveProps.test.ts +298 -0
  33. package/src/__tests__/useOverlay.test.ts +1330 -0
  34. package/src/__tests__/utils.test.ts +69 -0
  35. package/src/constants.ts +1 -0
  36. package/src/helpers/Content/component.tsx +51 -0
  37. package/src/helpers/Content/index.ts +3 -0
  38. package/src/helpers/Content/styled.ts +105 -0
  39. package/src/helpers/Content/types.ts +49 -0
  40. package/src/helpers/Iterator/component.tsx +252 -0
  41. package/src/helpers/Iterator/index.ts +13 -0
  42. package/src/helpers/Iterator/types.ts +79 -0
  43. package/src/helpers/Wrapper/component.tsx +78 -0
  44. package/src/helpers/Wrapper/constants.ts +10 -0
  45. package/src/helpers/Wrapper/index.ts +3 -0
  46. package/src/helpers/Wrapper/styled.ts +69 -0
  47. package/src/helpers/Wrapper/types.ts +56 -0
  48. package/src/helpers/Wrapper/utils.ts +7 -0
  49. package/src/helpers/index.ts +4 -0
  50. package/src/index.ts +37 -0
  51. package/src/types.ts +81 -0
  52. package/src/utils.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/elements",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/pyreon/pyreon",
@@ -24,7 +24,8 @@
24
24
  "!lib/**/*.map",
25
25
  "!lib/analysis",
26
26
  "README.md",
27
- "LICENSE"
27
+ "LICENSE",
28
+ "src"
28
29
  ],
29
30
  "engines": {
30
31
  "node": ">= 22"
@@ -43,13 +44,13 @@
43
44
  "typecheck": "tsc --noEmit"
44
45
  },
45
46
  "peerDependencies": {
46
- "@pyreon/core": "^0.11.1",
47
- "@pyreon/reactivity": "^0.11.1",
48
- "@pyreon/ui-core": "^0.11.1",
49
- "@pyreon/unistyle": "^0.11.1"
47
+ "@pyreon/core": "^0.11.3",
48
+ "@pyreon/reactivity": "^0.11.3",
49
+ "@pyreon/ui-core": "^0.11.3",
50
+ "@pyreon/unistyle": "^0.11.3"
50
51
  },
51
52
  "devDependencies": {
52
53
  "@vitus-labs/tools-rolldown": "^1.15.3",
53
- "@pyreon/typescript": "^0.11.1"
54
+ "@pyreon/typescript": "^0.11.3"
54
55
  }
55
56
  }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Core building block of the elements package. Renders a three-section layout
3
+ * (beforeContent / content / afterContent) inside a flex Wrapper. When only
4
+ * content is present, the Wrapper inherits content-level alignment directly
5
+ * to avoid an unnecessary nesting layer. Handles HTML-specific edge cases
6
+ * like void elements (input, img) and inline elements (span, a) by
7
+ * skipping children or switching sub-tags accordingly.
8
+ */
9
+
10
+ import { onMount } from "@pyreon/core"
11
+ import { render } from "@pyreon/ui-core"
12
+ import { PKG_NAME } from "../constants"
13
+ import { Content, Wrapper } from "../helpers"
14
+ import type { PyreonElement } from "./types"
15
+ import { getShouldBeEmpty, isInlineElement } from "./utils"
16
+
17
+ const equalize = (el: HTMLElement, direction: unknown) => {
18
+ const beforeEl = el.firstElementChild as HTMLElement | null
19
+ const afterEl = el.lastElementChild as HTMLElement | null
20
+
21
+ if (beforeEl && afterEl && beforeEl !== afterEl) {
22
+ const type: "height" | "width" = direction === "rows" ? "height" : "width"
23
+ const prop = type === "height" ? "offsetHeight" : "offsetWidth"
24
+ const beforeSize = beforeEl[prop]
25
+ const afterSize = afterEl[prop]
26
+
27
+ if (Number.isInteger(beforeSize) && Number.isInteger(afterSize)) {
28
+ const maxSize = `${Math.max(beforeSize, afterSize)}px`
29
+ beforeEl.style[type] = maxSize
30
+ afterEl.style[type] = maxSize
31
+ }
32
+ }
33
+ }
34
+
35
+ const defaultDirection = "inline"
36
+ const defaultContentDirection = "rows"
37
+ const defaultAlignX = "left"
38
+ const defaultAlignY = "center"
39
+
40
+ const Component: PyreonElement = ({
41
+ innerRef,
42
+ tag,
43
+ label,
44
+ content,
45
+ children,
46
+ beforeContent,
47
+ afterContent,
48
+ equalBeforeAfter,
49
+
50
+ block,
51
+ equalCols,
52
+ gap,
53
+
54
+ direction,
55
+ alignX = defaultAlignX,
56
+ alignY = defaultAlignY,
57
+
58
+ css,
59
+ contentCss,
60
+ beforeContentCss,
61
+ afterContentCss,
62
+
63
+ contentDirection = defaultContentDirection,
64
+ contentAlignX = defaultAlignX,
65
+ contentAlignY = defaultAlignY,
66
+
67
+ beforeContentDirection = defaultDirection,
68
+ beforeContentAlignX = defaultAlignX,
69
+ beforeContentAlignY = defaultAlignY,
70
+
71
+ afterContentDirection = defaultDirection,
72
+ afterContentAlignX = defaultAlignX,
73
+ afterContentAlignY = defaultAlignY,
74
+
75
+ ref,
76
+ ...props
77
+ }) => {
78
+ // --------------------------------------------------------
79
+ // check if should render only single element
80
+ // --------------------------------------------------------
81
+ const shouldBeEmpty = !!props.dangerouslySetInnerHTML || getShouldBeEmpty(tag)
82
+
83
+ // --------------------------------------------------------
84
+ // if not single element, calculate values
85
+ // --------------------------------------------------------
86
+ const isSimpleElement = !beforeContent && !afterContent
87
+ const CHILDREN = children ?? content ?? label
88
+
89
+ const isInline = isInlineElement(tag)
90
+ const SUB_TAG = isInline ? "span" : undefined
91
+
92
+ // --------------------------------------------------------
93
+ // direction & alignX & alignY calculations
94
+ // --------------------------------------------------------
95
+ let wrapperDirection: typeof direction = direction
96
+ let wrapperAlignX: typeof alignX = alignX
97
+ let wrapperAlignY: typeof alignY = alignY
98
+
99
+ if (isSimpleElement) {
100
+ if (contentDirection) wrapperDirection = contentDirection
101
+ if (contentAlignX) wrapperAlignX = contentAlignX
102
+ if (contentAlignY) wrapperAlignY = contentAlignY
103
+ } else if (direction) {
104
+ wrapperDirection = direction
105
+ } else {
106
+ wrapperDirection = defaultDirection
107
+ }
108
+
109
+ // --------------------------------------------------------
110
+ // equalBeforeAfter: measure & equalize slot dimensions
111
+ // --------------------------------------------------------
112
+ let equalizeRef: HTMLElement | null = null
113
+ const externalRef = ref ?? innerRef
114
+
115
+ const mergedRef = (node: HTMLElement | null) => {
116
+ equalizeRef = node
117
+ if (typeof externalRef === "function") externalRef(node)
118
+ else if (externalRef != null) {
119
+ ;(externalRef as unknown as { current: HTMLElement | null }).current = node
120
+ }
121
+ }
122
+
123
+ if (equalBeforeAfter && beforeContent && afterContent) {
124
+ onMount(() => {
125
+ if (equalizeRef) equalize(equalizeRef, direction)
126
+ return undefined
127
+ })
128
+ }
129
+
130
+ // --------------------------------------------------------
131
+ // common wrapper props
132
+ // --------------------------------------------------------
133
+ const WRAPPER_PROPS = {
134
+ ref: mergedRef,
135
+ extendCss: css,
136
+ tag,
137
+ block,
138
+ direction: wrapperDirection,
139
+ alignX: wrapperAlignX,
140
+ alignY: wrapperAlignY,
141
+ as: undefined, // reset styled-components `as` prop
142
+ }
143
+
144
+ // --------------------------------------------------------
145
+ // return simple/empty element like input or image etc.
146
+ // --------------------------------------------------------
147
+ if (shouldBeEmpty) {
148
+ return <Wrapper {...props} {...WRAPPER_PROPS} />
149
+ }
150
+
151
+ return (
152
+ <Wrapper {...props} {...WRAPPER_PROPS} isInline={isInline}>
153
+ {beforeContent && (
154
+ <Content
155
+ tag={SUB_TAG}
156
+ contentType="before"
157
+ parentDirection={wrapperDirection}
158
+ extendCss={beforeContentCss}
159
+ direction={beforeContentDirection}
160
+ alignX={beforeContentAlignX}
161
+ alignY={beforeContentAlignY}
162
+ equalCols={equalCols}
163
+ gap={gap}
164
+ >
165
+ {beforeContent}
166
+ </Content>
167
+ )}
168
+
169
+ {isSimpleElement ? (
170
+ render(CHILDREN)
171
+ ) : (
172
+ <Content
173
+ tag={SUB_TAG}
174
+ contentType="content"
175
+ parentDirection={wrapperDirection}
176
+ extendCss={contentCss}
177
+ direction={contentDirection}
178
+ alignX={contentAlignX}
179
+ alignY={contentAlignY}
180
+ equalCols={equalCols}
181
+ >
182
+ {CHILDREN}
183
+ </Content>
184
+ )}
185
+
186
+ {afterContent && (
187
+ <Content
188
+ tag={SUB_TAG}
189
+ contentType="after"
190
+ parentDirection={wrapperDirection}
191
+ extendCss={afterContentCss}
192
+ direction={afterContentDirection}
193
+ alignX={afterContentAlignX}
194
+ alignY={afterContentAlignY}
195
+ equalCols={equalCols}
196
+ gap={gap}
197
+ >
198
+ {afterContent}
199
+ </Content>
200
+ )}
201
+ </Wrapper>
202
+ )
203
+ }
204
+
205
+ const name = `${PKG_NAME}/Element` as const
206
+
207
+ Component.displayName = name
208
+ Component.pkgName = PKG_NAME
209
+ Component.PYREON__COMPONENT = name
210
+
211
+ export default Component
@@ -0,0 +1,96 @@
1
+ /** Props consumed by Element that should not be forwarded to the underlying DOM node. */
2
+ export const RESERVED_PROPS = [
3
+ "innerRef",
4
+ "tag",
5
+ "block",
6
+ "label",
7
+ "children",
8
+ "beforeContent",
9
+ "afterContent",
10
+
11
+ "equalCols",
12
+ "vertical",
13
+ "direction",
14
+ "alignX",
15
+ "alignY",
16
+
17
+ "css",
18
+ "contentCss",
19
+ "beforeContentCss",
20
+ "afterContentCss",
21
+
22
+ "contentDirection",
23
+ "contentAlignX",
24
+ "contentAlignY",
25
+
26
+ "beforeContentDirection",
27
+ "beforeContentAlignX",
28
+ "beforeContentAlignY",
29
+
30
+ "afterContentDirection",
31
+ "afterContentAlignX",
32
+ "afterContentAlignY",
33
+ ] as const
34
+
35
+ /**
36
+ * HTML tags that are inline-level by default. When Element renders one of
37
+ * these tags, child Content wrappers use `span` instead of `div` to
38
+ * preserve valid HTML nesting.
39
+ */
40
+ export const INLINE_ELEMENTS = {
41
+ span: true,
42
+ a: true,
43
+ button: true,
44
+ input: true,
45
+ label: true,
46
+ select: true,
47
+ textarea: true,
48
+ br: true,
49
+ img: true,
50
+ strong: true,
51
+ small: true,
52
+ code: true,
53
+ b: true,
54
+ big: true,
55
+ i: true,
56
+ tt: true,
57
+ abbr: true,
58
+ acronym: true,
59
+ cite: true,
60
+ dfn: true,
61
+ em: true,
62
+ kbd: true,
63
+ samp: true,
64
+ var: true,
65
+ bdo: true,
66
+ map: true,
67
+ object: true,
68
+ q: true,
69
+ script: true,
70
+ sub: true,
71
+ sup: true,
72
+ }
73
+
74
+ /**
75
+ * HTML void/self-closing elements that cannot have children. When Element
76
+ * detects one of these tags, it skips rendering beforeContent/content/afterContent
77
+ * and returns the Wrapper alone.
78
+ */
79
+ export const EMPTY_ELEMENTS = {
80
+ area: true,
81
+ base: true,
82
+ br: true,
83
+ col: true,
84
+ embed: true,
85
+ hr: true,
86
+ img: true,
87
+ input: true,
88
+ keygen: true,
89
+ link: true,
90
+ textarea: true,
91
+ // 'meta': true,
92
+ // 'param': true,
93
+ source: true,
94
+ track: true,
95
+ wbr: true,
96
+ }
@@ -0,0 +1,6 @@
1
+ import component from "./component"
2
+ import type { Props, PyreonElement } from "./types"
3
+
4
+ export type { Props as ElementProps, PyreonElement }
5
+
6
+ export { component as Element }
@@ -0,0 +1,168 @@
1
+ import type { ComponentFn, PyreonHTMLAttributes } from "@pyreon/core"
2
+ import type { HTMLTags } from "@pyreon/ui-core"
3
+ import type {
4
+ AlignX,
5
+ AlignY,
6
+ Content,
7
+ Direction,
8
+ ExtendCss,
9
+ InnerRef,
10
+ PyreonStatic,
11
+ Responsive,
12
+ ResponsiveBoolType,
13
+ } from "../types"
14
+
15
+ export type Props = Partial<{
16
+ /**
17
+ * Valid HTML Tag
18
+ */
19
+ tag: HTMLTags
20
+
21
+ /**
22
+ * Ref prop, alternative to `ref`
23
+ */
24
+ innerRef: InnerRef
25
+
26
+ /**
27
+ * Valid `children`
28
+ */
29
+ children: Content
30
+
31
+ /**
32
+ * Alternative prop to `children`
33
+ * It is recommended to pass only one of `children`, `content` or `label` props
34
+ *
35
+ * The prioritization of rendering is following: `children` → `content` → `label`
36
+ */
37
+ content: Content
38
+
39
+ /**
40
+ * Alternative prop to `children`
41
+ * It is recommended to pass only one of `children`, `content` or `label` props
42
+ *
43
+ * The prioritization of rendering is following: `children` → `content` → `label`
44
+ */
45
+ label: Content
46
+
47
+ /**
48
+ * Valid `children` to be rendered inside _beforeContent_ wrapper
49
+ */
50
+ beforeContent: Content
51
+
52
+ /**
53
+ * Valid `children` to be rendered inside _afterContent_ wrapper
54
+ */
55
+ afterContent: Content
56
+
57
+ /**
58
+ * A boolean type to define whether **Element** should behave
59
+ * as an inline or block element (`flex` vs. `inline-flex`)
60
+ */
61
+ block: ResponsiveBoolType
62
+
63
+ /**
64
+ * A boolean type to define whether inner wrappers should be equal
65
+ * (have the same width or height)
66
+ */
67
+ equalCols: ResponsiveBoolType
68
+
69
+ /**
70
+ * When true, measures the `beforeContent` and `afterContent` slot wrappers
71
+ * after render and sets both to the larger dimension so they match.
72
+ */
73
+ equalBeforeAfter: boolean
74
+
75
+ /**
76
+ * Defines a `gap` spacing between inner wrappers
77
+ */
78
+ gap: Responsive
79
+
80
+ /**
81
+ * Defines direction of inner wrappers
82
+ */
83
+ direction: Direction
84
+
85
+ /**
86
+ * Defines flow of `children` elements within its inner wrapper.
87
+ */
88
+ contentDirection: Direction
89
+
90
+ /**
91
+ * Defines flow of `beforeContent` elements within its inner wrapper.
92
+ */
93
+ beforeContentDirection: Direction
94
+
95
+ /**
96
+ * Defines flow of `afterContent` elements within its inner wrapper.
97
+ */
98
+ afterContentDirection: Direction
99
+
100
+ /**
101
+ * Define alignment horizontally.
102
+ */
103
+ alignX: AlignX
104
+
105
+ /**
106
+ * Defines how `content` children are aligned horizontally.
107
+ */
108
+ contentAlignX: AlignX
109
+
110
+ /**
111
+ * Defines how `beforeContent` children are aligned horizontally.
112
+ */
113
+ beforeContentAlignX: AlignX
114
+
115
+ /**
116
+ * Defines how `afterContent` children are aligned horizontally.
117
+ */
118
+ afterContentAlignX: AlignX
119
+
120
+ /**
121
+ * Define alignment vertically.
122
+ */
123
+ alignY: AlignY
124
+
125
+ /**
126
+ * Defines how `content` children are aligned vertically.
127
+ */
128
+ contentAlignY: AlignY
129
+
130
+ /**
131
+ * Defines how `beforeContent` children are aligned vertically.
132
+ */
133
+ beforeContentAlignY: AlignY
134
+
135
+ /**
136
+ * Defines how `afterContent` children are aligned vertically.
137
+ */
138
+ afterContentAlignY: AlignY
139
+
140
+ /**
141
+ * `dangerouslySetInnerHTML` prop
142
+ */
143
+ dangerouslySetInnerHTML: { __html: string }
144
+
145
+ /**
146
+ * An additional prop for extending styling of the **root** wrapper element
147
+ */
148
+ css: ExtendCss
149
+
150
+ /**
151
+ * An additional prop for extending styling of the **content** wrapper element.
152
+ */
153
+ contentCss: ExtendCss
154
+
155
+ /**
156
+ * An additional prop for extending styling of the **beforeContent** wrapper element.
157
+ */
158
+ beforeContentCss: ExtendCss
159
+
160
+ /**
161
+ * An additional prop for extending styling of the **afterContent** wrapper element.
162
+ */
163
+ afterContentCss: ExtendCss
164
+ }> &
165
+ PyreonHTMLAttributes
166
+
167
+ export type PyreonElement<P extends Record<string, unknown> = {}> = ComponentFn<Props & P> &
168
+ PyreonStatic
@@ -0,0 +1,15 @@
1
+ import { EMPTY_ELEMENTS, INLINE_ELEMENTS } from "./constants"
2
+
3
+ type GetValue = (tag?: string) => boolean
4
+
5
+ /** Checks whether the given HTML tag is an inline-level element, used to determine sub-tag nesting. */
6
+ export const isInlineElement: GetValue = (tag) => {
7
+ if (tag && tag in INLINE_ELEMENTS) return true
8
+ return false
9
+ }
10
+
11
+ /** Checks whether the given HTML tag is a void element that cannot have children. */
12
+ export const getShouldBeEmpty: GetValue = (tag) => {
13
+ if (tag && tag in EMPTY_ELEMENTS) return true
14
+ return false
15
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * List component that combines Iterator (data-driven rendering) with an
3
+ * optional Element root wrapper. When `rootElement` is false (default),
4
+ * it renders a bare Iterator as a fragment. When true, the Iterator output
5
+ * is wrapped in an Element that receives all non-iterator props (e.g.,
6
+ * layout, alignment, css), allowing the list to be styled as a single block.
7
+ */
8
+ import { omit, pick } from "@pyreon/ui-core"
9
+ import { PKG_NAME } from "../constants"
10
+ import type { ElementProps, PyreonElement } from "../Element"
11
+ import { Element } from "../Element"
12
+ import type { Props as IteratorProps } from "../helpers/Iterator"
13
+ import Iterator from "../helpers/Iterator"
14
+ import type { MergeTypes } from "../types"
15
+
16
+ type ListProps = {
17
+ /**
18
+ * A boolean value. When set to `false`, component returns fragment.
19
+ * When set to `true`, component returns as the **root** element `Element`
20
+ * component.
21
+ */
22
+ rootElement?: boolean
23
+ /**
24
+ * Label prop from `Element` component is being ignored.
25
+ */
26
+ label: never
27
+ /**
28
+ * Content prop from `Element` component is being ignored.
29
+ */
30
+ content: never
31
+ }
32
+
33
+ export type Props = MergeTypes<[IteratorProps, ListProps]>
34
+
35
+ const Component: PyreonElement<Props> = (({
36
+ rootElement = false,
37
+ ref,
38
+ ...props
39
+ }: Partial<Props & ElementProps>) => {
40
+ const renderedList = <Iterator {...pick(props, Iterator.RESERVED_PROPS)} />
41
+
42
+ if (!rootElement) return renderedList
43
+
44
+ return (
45
+ <Element {...(ref ? { ref } : {})} {...omit(props, Iterator.RESERVED_PROPS)}>
46
+ {renderedList}
47
+ </Element>
48
+ )
49
+ }) as PyreonElement<Props>
50
+
51
+ const name = `${PKG_NAME}/List` as const
52
+
53
+ Component.displayName = name
54
+ Component.pkgName = PKG_NAME
55
+ Component.PYREON__COMPONENT = name
56
+
57
+ export default Component
@@ -0,0 +1,5 @@
1
+ import type { Props } from "./component"
2
+ import component from "./component"
3
+
4
+ export type { Props as ListProps }
5
+ export { component as List }