@pyreon/attrs 0.22.0 → 0.24.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.
Files changed (2) hide show
  1. package/README.md +61 -91
  2. package/package.json +5 -5
package/README.md CHANGED
@@ -1,102 +1,81 @@
1
1
  # @pyreon/attrs
2
2
 
3
- Immutable, chainable default-props factory for Pyreon components.
3
+ Chainable HOC factory for default props, base swaps, composition, and statics.
4
4
 
5
- Think of styled-components' `.attrs()` as a standalone, type-safe composition system. Define default props, swap base components, attach HOCs, and add metadata — all through an immutable chain where every call returns a new component.
5
+ `@pyreon/attrs` wraps a Pyreon component in an immutable, chainable builder that accumulates default props (`.attrs()`), reconfigures the base component (`.config()`), composes additional HOCs (`.compose()`), and attaches static metadata (`.statics()`). Every chain method returns a new component the original is never mutated and TypeScript generics accumulate so prop types stay correct after each `.attrs<P>({...})` call. It's the foundation `@pyreon/rocketstyle` builds on; you'll also use it directly when you want default-prop composition without the dimension-styling layer.
6
6
 
7
- ## Features
8
-
9
- - **Immutable chaining** — every method returns a new component, never mutates the original
10
- - **Props merge order** — `priorityAttrs` > `attrs` > explicit props, with full control over precedence
11
- - **Prop filtering** — strip internal props before they reach the DOM
12
- - **HOC composition** — named HOCs via `.compose()` with selective removal
13
- - **Static metadata** — attach and access custom data via `.statics()` / `.meta`
14
- - **TypeScript inference** — generics accumulate through the chain
15
-
16
- ## Installation
7
+ ## Install
17
8
 
18
9
  ```bash
19
- bun add @pyreon/attrs
10
+ bun add @pyreon/attrs @pyreon/core @pyreon/ui-core
20
11
  ```
21
12
 
22
- ## Quick Start
13
+ ## Quick start
23
14
 
24
- ```ts
15
+ ```tsx
25
16
  import attrs from '@pyreon/attrs'
26
17
  import { Element } from '@pyreon/elements'
27
18
 
28
- const Button = attrs({ name: 'Button', component: Element }).attrs({
29
- tag: 'button',
30
- alignX: 'center',
31
- alignY: 'center',
32
- })
19
+ const Button = attrs({ name: 'Button', component: Element })
20
+ .attrs({ tag: 'button', alignX: 'center', alignY: 'center' })
21
+ .attrs<{ primary?: boolean }>(({ primary }) => ({
22
+ backgroundColor: primary ? 'blue' : 'gray',
23
+ }))
33
24
 
34
- // Renders Element with tag="button", alignX="center", alignY="center"
35
- Button({ label: 'Click me' })
25
+ // Renders Element with the accumulated defaults
26
+ <Button label="Click me" />
36
27
 
37
- // Explicit props override attrs defaults
38
- Button({ tag: 'a', label: 'Link button' })
28
+ // Explicit props override .attrs() defaults (unless `priority: true`)
29
+ <Button tag="a" href="/x" label="Link button" />
39
30
  ```
40
31
 
41
32
  ## API
42
33
 
43
- ### attrs(options)
44
-
45
- Creates an attrs-enhanced component.
34
+ ### `attrs({ name, component })`
46
35
 
47
- ```ts
48
- const Component = attrs({
49
- name: 'ComponentName', // required — sets displayName
50
- component: BaseComponent, // required — the Pyreon component to wrap
51
- })
52
- ```
36
+ Factory entry. Returns a Pyreon `ComponentFn` enhanced with chainable methods. Both `name` (used as `displayName` and a dev `data-attrs` attribute) and `component` (the base) are required — dev mode throws on missing values.
53
37
 
54
- ### .attrs(props | callback, options?)
38
+ ### `.attrs(props | callback, options?)`
55
39
 
56
- Add default props. Can be called multiple times — defaults stack left-to-right.
40
+ Add default props. Call multiple times — defaults stack left-to-right in the chain.
57
41
 
58
42
  ```ts
59
- // Object form — static defaults
43
+ // Object form
60
44
  Button.attrs({ tag: 'button' })
61
45
 
62
- // Callback form — computed defaults based on current props
63
- Button.attrs((props) => ({
46
+ // Callback form — receives the current resolved props
47
+ Button.attrs<{ label: string }>((props) => ({
64
48
  'aria-label': props.label,
65
49
  }))
66
50
 
67
- // Priority — resolved before regular attrs, cannot be overridden by explicit props
51
+ // Priority — wins over EXPLICIT props at the call site
68
52
  Button.attrs({ tag: 'button' }, { priority: true })
69
53
 
70
- // Filter — remove props before passing to the underlying component
54
+ // Filter — strip these prop names before forwarding to the base
71
55
  Button.attrs({}, { filter: ['internalFlag', 'variant'] })
72
56
  ```
73
57
 
74
- **Props merge order:**
58
+ **Merge order at render time:**
75
59
 
76
60
  ```text
77
- priorityAttrs (highest) attrs explicit props (lowest for priority, highest for regular)
61
+ priorityAttrs attrs explicit props → filterAttrs strips base component
78
62
  ```
79
63
 
80
- For regular attrs, explicit props win. For priority attrs, the priority value wins.
64
+ For regular `attrs`, explicit props win. For `priorityAttrs`, the priority value wins (used by `rocketstyle` to lock structural props like `tag`).
81
65
 
82
- ### .config(options)
66
+ ### `.config({ name?, component?, DEBUG? })`
83
67
 
84
- Reconfigure the component. Returns a new component instance.
68
+ Swap the underlying component, rename, or toggle dev debugging. Returns a new instance.
85
69
 
86
70
  ```ts
87
- // Rename
88
- Button.config({ name: 'PrimaryButton' })
89
-
90
- // Swap the base component
91
- Button.config({ component: AnotherComponent })
92
-
93
- // Enable debug mode — adds data-attrs attribute in development
94
- Button.config({ DEBUG: true })
71
+ const Anchor = Button.config({ component: 'a', name: 'Anchor' })
95
72
  ```
96
73
 
97
- ### .compose(hocs)
74
+ **Gotcha**: swapping `component` resets the `attrs` / `priorityAttrs` / `filterAttrs` / `compose` chains because they were tailored to the previous component's prop shape (applying them blindly leaks invalid attrs to the DOM). `theme` / `styles` / dimension chains are preserved. Re-chain shared attrs explicitly if you need them.
98
75
 
99
- Attach named Higher-Order Components. Applied in declaration order.
76
+ ### `.compose({ hocName: hocFn })`
77
+
78
+ Attach named HOCs to the chain. Applied in registration order — outermost wraps first. Pass `null` to remove a previously composed HOC.
100
79
 
101
80
  ```ts
102
81
  const Enhanced = Button.compose({
@@ -104,72 +83,63 @@ const Enhanced = Button.compose({
104
83
  withTracking: trackingHoc,
105
84
  })
106
85
 
107
- // Remove a specific HOC from the chain
108
- const WithoutTracking = Enhanced.compose({ withTracking: null })
86
+ const NoTracking = Enhanced.compose({ withTracking: null })
109
87
  ```
110
88
 
111
- ### .statics(metadata)
89
+ ### `.statics({ key: value })`
112
90
 
113
- Attach metadata accessible via the `.meta` property.
91
+ Attach arbitrary metadata on `.meta`. Used by `@pyreon/document-primitives` (`_documentType`) and other systems that need post-construction component introspection.
114
92
 
115
93
  ```ts
116
- const Button = attrs({ name: 'Button', component: Element }).statics({
94
+ const Btn = attrs({ name: 'Btn', component: Element }).statics({
117
95
  category: 'action',
118
96
  sizes: ['sm', 'md', 'lg'],
119
97
  })
120
98
 
121
- Button.meta.category // => 'action'
122
- Button.meta.sizes // => ['sm', 'md', 'lg']
99
+ Btn.meta.category // 'action'
123
100
  ```
124
101
 
125
- ### isAttrsComponent(value)
102
+ ### `.getDefaultAttrs()`
126
103
 
127
- Runtime type guard.
104
+ Resolve the accumulated default props (calls every `.attrs()` callback with `{}`).
128
105
 
129
106
  ```ts
130
- import { isAttrsComponent } from '@pyreon/attrs'
131
-
132
- isAttrsComponent(Button) // => true
133
- isAttrsComponent('div') // => false
107
+ Button.getDefaultAttrs() // { tag: 'button', alignX: 'center', alignY: 'center' }
134
108
  ```
135
109
 
136
- ### getDefaultAttrs()
110
+ ### `isAttrsComponent(value)`
137
111
 
138
- Retrieve the computed default props for a component.
112
+ Runtime guard returns `true` for components produced by `attrs()`.
139
113
 
140
114
  ```ts
141
- Button.getDefaultAttrs() // => { tag: 'button', alignX: 'center', ... }
115
+ import { isAttrsComponent } from '@pyreon/attrs'
116
+ isAttrsComponent(Button) // true
117
+ isAttrsComponent('div') // false
142
118
  ```
143
119
 
144
120
  ## TypeScript
145
121
 
146
- Each `.attrs<P>()` call adds `P` to the component's prop types through `MergeTypes`:
122
+ Each `.attrs<P>()` generic accumulates into the component's prop type. Three type-only properties expose the accumulated shapes:
147
123
 
148
124
  ```ts
149
- const Base = attrs({ name: 'Base', component: Element })
150
-
151
- const Typed = Base.attrs<{ variant: 'primary' | 'secondary' }>({ variant: 'primary' }).attrs<{
152
- size?: 'sm' | 'md' | 'lg'
153
- }>({})
154
-
155
- // Typed accepts: Element props + { variant, size? }
156
- Typed({ variant: 'secondary', size: 'lg', label: 'Hello' })
125
+ type AllProps = typeof Button.$$types // origin + extended
126
+ type OriginProps = typeof Button.$$originTypes // base component's props
127
+ type ExtendedProps = typeof Button.$$extendedTypes // everything added through .attrs<P>()
157
128
  ```
158
129
 
159
- Access the accumulated types via type-only properties:
130
+ Use `ExtractProps<typeof Button>` from `@pyreon/core` to recover the union when forwarding through another HOC.
160
131
 
161
- ```ts
162
- type AllProps = typeof Typed.$$types
163
- type OriginalProps = typeof Typed.$$originTypes
164
- type ExtendedProps = typeof Typed.$$extendedTypes
165
- ```
132
+ ## Gotchas
133
+
134
+ - **`.config({ component })` resets the prop chains.** Re-chain shared attrs explicitly if you swap the base. `theme` / `styles` / dimension chains survive.
135
+ - **Defaults are merged, not deep-merged.** Object-valued props (e.g. `style={{ color: 'red' }}`) get replaced, not combined.
136
+ - **The dev `data-attrs` attribute is added in dev builds** to aid debugging. Tree-shaken in production (gated on `process.env.NODE_ENV !== 'production'`).
137
+ - **`hoistNonReactStatics`** copies non-React statics from the base onto the wrapper, so `MyComponent.someStaticMethod` survives the HOC chain.
138
+ - **Generic accumulation has a depth limit** — TypeScript's recursive conditional-type inference caps at ~24-50 levels depending on the host environment. If you stack `.attrs<P>()` calls past that, narrow generics or split the component.
166
139
 
167
- ## Peer Dependencies
140
+ ## Documentation
168
141
 
169
- | Package | Version |
170
- | --------------- | -------- |
171
- | @pyreon/core | >= 0.0.1 |
172
- | @pyreon/ui-core | >= 0.0.1 |
142
+ Full docs: [docs.pyreon.dev/docs/attrs](https://docs.pyreon.dev/docs/attrs) (or `docs/docs/attrs.md` in this repo).
173
143
 
174
144
  ## License
175
145
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/attrs",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "Attrs HOC chaining for Pyreon components",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,14 +41,14 @@
41
41
  "typecheck": "tsc --noEmit"
42
42
  },
43
43
  "devDependencies": {
44
- "@pyreon/typescript": "^0.22.0",
45
- "@vitus-labs/tools-rolldown": "^2.3.0"
44
+ "@pyreon/typescript": "^0.24.0",
45
+ "@vitus-labs/tools-rolldown": "^2.4.0"
46
46
  },
47
47
  "engines": {
48
48
  "node": ">= 22"
49
49
  },
50
50
  "dependencies": {
51
- "@pyreon/core": "^0.22.0",
52
- "@pyreon/ui-core": "^0.22.0"
51
+ "@pyreon/core": "^0.24.0",
52
+ "@pyreon/ui-core": "^0.24.0"
53
53
  }
54
54
  }