@diskette/use-render 0.9.0 → 0.10.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/README.md CHANGED
@@ -8,11 +8,35 @@ pnpm add @diskette/use-render
8
8
 
9
9
  ## Overview
10
10
 
11
- When building component libraries, you often need to let consumers customize rendering—swap the underlying element, access internal state for styling, or compose refs and event handlers. These hooks handle that plumbing:
11
+ When building component libraries, you often need to let consumers customize rendering to:
12
+
13
+ - swap the underlying element
14
+ - access internal state for styling
15
+ - compose refs and event handlers.
16
+
17
+ These hooks handle that plumbing:
12
18
 
13
19
  - **Component authors** get prop merging, ref composition, and render prop support out of the box
14
20
  - **Consumers** get type-safe APIs where `className`, `style`, and `children` can be functions of component state
15
21
 
22
+ ## `useRender` — Stateful Components
23
+
24
+ For components that expose internal state to consumers. The state flows through `className`, `style`, `children`, and `render` as callback parameters.
25
+
26
+ **Component author:**
27
+
28
+ ```tsx
29
+ import { useRender, ComponentProps } from '@diskette/use-render'
30
+
31
+ type State = { disabled: boolean; loading: boolean }
32
+ type ButtonProps = ComponentProps<'button', State>
33
+
34
+ function Button(props: ButtonProps) {
35
+ const state: State = { disabled: props.disabled ?? false, loading: false }
36
+ return useRender('button', state, { props })
37
+ }
38
+ ```
39
+
16
40
  ## `useRenderSlot` — Stateless Slots
17
41
 
18
42
  For wrapper components that don't expose internal state. Consumers can still swap the element or pass props—they just won't get state callbacks.
@@ -39,24 +63,6 @@ function Card(props: CardProps) {
39
63
 
40
64
  Props merge automatically—`className` and `style` combine, refs compose, event handlers chain (consumer runs first). The `render` prop receives only `props` since there's no state to pass.
41
65
 
42
- ## `useRender` — Stateful Components
43
-
44
- For components that expose internal state to consumers. The state flows through `className`, `style`, `children`, and `render` as callback parameters.
45
-
46
- **Component author:**
47
-
48
- ```tsx
49
- import { useRender, ComponentProps } from '@diskette/use-render'
50
-
51
- type State = { disabled: boolean; loading: boolean }
52
- type ButtonProps = ComponentProps<'button', State>
53
-
54
- function Button(props: ButtonProps) {
55
- const state: State = { disabled: props.disabled ?? false, loading: false }
56
- return useRender('button', state, { props })
57
- }
58
- ```
59
-
60
66
  **Consumer:**
61
67
 
62
68
  ```tsx
@@ -101,7 +107,11 @@ function List({ items, ...props }: ListProps & { items: string[] }) {
101
107
  { props, baseProps: { children: (item) => <li>{item.value}</li> } },
102
108
  )
103
109
  // containerProps provides direct access to resolved props if needed
104
- return <Container>{items.map((v, i) => renderItem({ index: i, value: v }))}</Container>
110
+ return (
111
+ <Container>
112
+ {items.map((v, i) => renderItem({ index: i, value: v }))}
113
+ </Container>
114
+ )
105
115
  }
106
116
  ```
107
117
 
@@ -122,10 +132,10 @@ function List({ items, ...props }: ListProps & { items: string[] }) {
122
132
 
123
133
  Each hook has a corresponding type for your component's public API:
124
134
 
125
- | Hook | Props Type | State |
126
- |------|-----------|-------|
127
- | `useRenderSlot` | `SlotProps<T>` | None |
128
- | `useRender` | `ComponentProps<T, S>` | Single state |
135
+ | Hook | Props Type | State |
136
+ | -------------------- | --------------------------- | ---------------- |
137
+ | `useRenderSlot` | `SlotProps<T>` | None |
138
+ | `useRender` | `ComponentProps<T, S>` | Single state |
129
139
  | `useRenderContainer` | `ContainerProps<T, CS, IS>` | Container + Item |
130
140
 
131
141
  These extend the element's native props, adding `render` and (for stateful hooks) function forms of `className`, `style`, and `children`.
@@ -155,7 +165,10 @@ export interface ComboboxRef {
155
165
  clear: () => void
156
166
  }
157
167
 
158
- function Combobox({ ref, ...props }: ComboboxProps & { ref?: React.Ref<ComboboxRef> }) {
168
+ function Combobox({
169
+ ref,
170
+ ...props
171
+ }: ComboboxProps & { ref?: React.Ref<ComboboxRef> }) {
159
172
  const inputRef = useRef<HTMLInputElement>(null)
160
173
  const state: State = { open: false }
161
174
 
@@ -0,0 +1,2 @@
1
+ export { renderSlot } from './render-slot.ts';
2
+ export type { RenderSlotOptions, SlotProps, SlotRenderer, } from './render-slot.ts';
@@ -0,0 +1 @@
1
+ export { renderSlot } from "./render-slot.js";
@@ -0,0 +1,36 @@
1
+ import type { CSSProperties, ElementType, HTMLAttributes, JSX, ReactNode } from 'react';
2
+ import type { DataAttributes } from '../types.ts';
3
+ export type SlotRenderer = (props: HTMLAttributes<any>) => ReactNode;
4
+ export type SlotProps<T extends ElementType> = Omit<React.ComponentProps<T>, 'className' | 'style' | 'ref'> & {
5
+ className?: string | undefined;
6
+ style?: CSSProperties | undefined;
7
+ render?: SlotRenderer | JSX.Element;
8
+ };
9
+ export interface RenderSlotOptions<T extends ElementType> {
10
+ baseProps?: Omit<React.ComponentProps<T>, 'ref'> & DataAttributes;
11
+ props?: SlotProps<T> & DataAttributes;
12
+ }
13
+ /**
14
+ * Pure function for rendering slot elements with render prop support and prop merging.
15
+ * RSC-compatible version of useRenderSlot without ref handling.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * import { renderSlot, SlotProps } from '@diskette/use-render/fns'
20
+ *
21
+ * type CardProps = SlotProps<'div'>
22
+ *
23
+ * function Card(props: CardProps) {
24
+ * return renderSlot('div', {
25
+ * props,
26
+ * baseProps: { className: 'card' },
27
+ * })
28
+ * }
29
+ *
30
+ * // Usage:
31
+ * <Card className="card-primary" />
32
+ * <Card render={<section />} />
33
+ * <Card render={(props) => <article {...props} />} />
34
+ * ```
35
+ */
36
+ export declare function renderSlot<T extends ElementType>(tag: T, options?: RenderSlotOptions<T>): ReactNode;
@@ -0,0 +1,52 @@
1
+ import { cloneElement, createElement, isValidElement } from 'react';
2
+ import { cx, isFunction, isString, mergeProps } from "../utils.js";
3
+ /**
4
+ * Pure function for rendering slot elements with render prop support and prop merging.
5
+ * RSC-compatible version of useRenderSlot without ref handling.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { renderSlot, SlotProps } from '@diskette/use-render/fns'
10
+ *
11
+ * type CardProps = SlotProps<'div'>
12
+ *
13
+ * function Card(props: CardProps) {
14
+ * return renderSlot('div', {
15
+ * props,
16
+ * baseProps: { className: 'card' },
17
+ * })
18
+ * }
19
+ *
20
+ * // Usage:
21
+ * <Card className="card-primary" />
22
+ * <Card render={<section />} />
23
+ * <Card render={(props) => <article {...props} />} />
24
+ * ```
25
+ */
26
+ export function renderSlot(tag, options = {}) {
27
+ const baseProps = (options.baseProps ?? {});
28
+ const props = (options.props ?? {});
29
+ const { className: baseClassName, style: baseStyle, children: baseChildren, ...base } = baseProps;
30
+ const { className, style, children, render, ...rest } = props;
31
+ const resolvedClassName = cx(baseClassName, className);
32
+ const resolvedStyle = baseStyle || style ? { ...baseStyle, ...style } : undefined;
33
+ const resolvedProps = {
34
+ ...mergeProps(base, rest),
35
+ };
36
+ if (isString(resolvedClassName)) {
37
+ resolvedProps.className = resolvedClassName;
38
+ }
39
+ if (typeof resolvedStyle === 'object') {
40
+ resolvedProps.style = resolvedStyle;
41
+ }
42
+ const resolvedChildren = children ?? baseChildren;
43
+ // For `<Component render={<a />} />`
44
+ if (isValidElement(render)) {
45
+ return cloneElement(render, resolvedProps, resolvedChildren);
46
+ }
47
+ // For `<Component render={(props) => <a {...props} />)} />`
48
+ if (isFunction(render)) {
49
+ return render({ ...resolvedProps, children: resolvedChildren });
50
+ }
51
+ return createElement(tag, resolvedProps, resolvedChildren);
52
+ }
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@diskette/use-render",
3
3
  "type": "module",
4
- "version": "0.9.0",
5
- "exports": "./dist/index.js",
4
+ "version": "0.10.0",
5
+ "exports": {
6
+ ".": "./dist/index.js",
7
+ "./fns": "./dist/fns/index.js"
8
+ },
6
9
  "files": [
7
10
  "dist"
8
11
  ],