@velinstyle/react 1.1.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 +78 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +225 -0
- package/package.json +54 -0
- package/src/apply-props.js +111 -0
- package/src/create-component.jsx +46 -0
- package/src/index.d.ts +86 -0
- package/src/index.jsx +54 -0
- package/src/tags.js +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @velinstyle/react
|
|
2
|
+
|
|
3
|
+
Official React wrappers around the VelinStyle custom elements. Every canonical
|
|
4
|
+
`velin-*` element has a wrapper — the list is generated from the framework's
|
|
5
|
+
component registry, so the adapter cannot fall behind the elements that ship.
|
|
6
|
+
|
|
7
|
+
There is no re-implementation: each wrapper renders the real custom element and
|
|
8
|
+
forwards refs to it, so accessibility behaviour, keyboard handling and styling
|
|
9
|
+
are identical to the plain HTML usage.
|
|
10
|
+
|
|
11
|
+
```jsx
|
|
12
|
+
import { useRef } from 'react';
|
|
13
|
+
import { VelinDialog, VelinThemeToggle } from '@velinstyle/react';
|
|
14
|
+
import '@birdapi/velinstyle/css';
|
|
15
|
+
import '@birdapi/velinstyle/bundle';
|
|
16
|
+
|
|
17
|
+
export function App() {
|
|
18
|
+
const dialogRef = useRef(null);
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
<VelinThemeToggle target="html" />
|
|
22
|
+
<VelinDialog ref={dialogRef} />
|
|
23
|
+
<button type="button" onClick={() => dialogRef.current?.confirm('Continue?')}>
|
|
24
|
+
Open dialog
|
|
25
|
+
</button>
|
|
26
|
+
</>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## How props are mapped
|
|
32
|
+
|
|
33
|
+
React 18 renders unknown props as string attributes, which turns `open={false}`
|
|
34
|
+
into `open="false"` and stringifies objects. The wrappers avoid both:
|
|
35
|
+
|
|
36
|
+
| Prop value | Applied as |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| string, number | attribute |
|
|
39
|
+
| boolean | presence attribute (`open` set or removed) |
|
|
40
|
+
| object, array, function | element property |
|
|
41
|
+
| `onVelin*` function | `addEventListener` for the matching custom event |
|
|
42
|
+
| `null` / `undefined` | omitted |
|
|
43
|
+
| `onClick` and other React events | passed to React unchanged |
|
|
44
|
+
|
|
45
|
+
Custom event names are derived from the prop name: `onVelinSearchSelect` binds
|
|
46
|
+
`velin-search-select`.
|
|
47
|
+
|
|
48
|
+
```jsx
|
|
49
|
+
<VelinSearch
|
|
50
|
+
entries={entries} // element property, not stringified
|
|
51
|
+
open={isOpen} // presence attribute
|
|
52
|
+
onVelinSearchSelect={handleSelect} // custom event listener
|
|
53
|
+
onClick={handleClick} // regular React handler
|
|
54
|
+
/>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Wrapping additional elements
|
|
58
|
+
|
|
59
|
+
`createVelinComponent` builds a wrapper for any tag, which is useful for custom
|
|
60
|
+
elements built on top of VelinStyle:
|
|
61
|
+
|
|
62
|
+
```jsx
|
|
63
|
+
import { createVelinComponent } from '@velinstyle/react';
|
|
64
|
+
|
|
65
|
+
const MyWidget = createVelinComponent('my-widget');
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Build
|
|
69
|
+
|
|
70
|
+
From the repository root:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm run build:react
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
That regenerates the wrappers from the component registry and bundles the
|
|
77
|
+
package. `react >= 18` is a peer dependency. Framework CI fails if the generated
|
|
78
|
+
wrappers are out of date, which keeps new components from shipping without one.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-GENERATED — run: node scripts/generate-react-wrappers.mjs
|
|
3
|
+
*/
|
|
4
|
+
import type { ComponentType, CSSProperties, ReactNode, Ref } from 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Props accepted by every wrapper. Scalars become attributes, booleans become
|
|
8
|
+
* presence attributes, objects and arrays are assigned as element properties,
|
|
9
|
+
* and `onVelin*` handlers are bound as custom-event listeners.
|
|
10
|
+
*/
|
|
11
|
+
export interface VelinComponentProps {
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
style?: CSSProperties;
|
|
15
|
+
ref?: Ref<HTMLElement>;
|
|
16
|
+
[prop: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface VelinComponent extends ComponentType<VelinComponentProps> {
|
|
20
|
+
/** Custom element tag rendered by this wrapper. */
|
|
21
|
+
tagName: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export declare function createVelinComponent(tag: string): VelinComponent;
|
|
25
|
+
export declare const VELIN_TAGS: readonly string[];
|
|
26
|
+
|
|
27
|
+
export declare function isVelinEventProp(name: string): boolean;
|
|
28
|
+
export declare function velinEventName(propName: string): string;
|
|
29
|
+
export declare function componentNameForTag(tag: string): string;
|
|
30
|
+
export declare function splitProps(props: Record<string, unknown>): {
|
|
31
|
+
children?: ReactNode;
|
|
32
|
+
className?: string;
|
|
33
|
+
style?: CSSProperties;
|
|
34
|
+
attributes: Record<string, string | number>;
|
|
35
|
+
properties: Record<string, unknown>;
|
|
36
|
+
booleans: Record<string, boolean>;
|
|
37
|
+
listeners: Record<string, EventListener>;
|
|
38
|
+
reactProps: Record<string, unknown>;
|
|
39
|
+
};
|
|
40
|
+
export declare function applyVelinProps(
|
|
41
|
+
el: Element,
|
|
42
|
+
parts: { properties?: Record<string, unknown>; booleans?: Record<string, boolean> },
|
|
43
|
+
): void;
|
|
44
|
+
export declare function bindVelinListeners(
|
|
45
|
+
el: Element,
|
|
46
|
+
listeners: Record<string, EventListener>,
|
|
47
|
+
): () => void;
|
|
48
|
+
|
|
49
|
+
export declare const VelinAccordion: VelinComponent;
|
|
50
|
+
export declare const VelinAnnouncer: VelinComponent;
|
|
51
|
+
export declare const VelinBottomNav: VelinComponent;
|
|
52
|
+
export declare const VelinCarousel: VelinComponent;
|
|
53
|
+
export declare const VelinCodeBlock: VelinComponent;
|
|
54
|
+
export declare const VelinCollapse: VelinComponent;
|
|
55
|
+
export declare const VelinCombobox: VelinComponent;
|
|
56
|
+
export declare const VelinCommand: VelinComponent;
|
|
57
|
+
export declare const VelinCopy: VelinComponent;
|
|
58
|
+
export declare const VelinCountdown: VelinComponent;
|
|
59
|
+
export declare const VelinCounter: VelinComponent;
|
|
60
|
+
export declare const VelinDataTable: VelinComponent;
|
|
61
|
+
export declare const VelinDialog: VelinComponent;
|
|
62
|
+
export declare const VelinDrawer: VelinComponent;
|
|
63
|
+
export declare const VelinDropdown: VelinComponent;
|
|
64
|
+
export declare const VelinEmail: VelinComponent;
|
|
65
|
+
export declare const VelinFormSummary: VelinComponent;
|
|
66
|
+
export declare const VelinIcon: VelinComponent;
|
|
67
|
+
export declare const VelinLightbox: VelinComponent;
|
|
68
|
+
export declare const VelinLiveDot: VelinComponent;
|
|
69
|
+
export declare const VelinMenubar: VelinComponent;
|
|
70
|
+
export declare const VelinModal: VelinComponent;
|
|
71
|
+
export declare const VelinPersist: VelinComponent;
|
|
72
|
+
export declare const VelinPopover: VelinComponent;
|
|
73
|
+
export declare const VelinProgressRing: VelinComponent;
|
|
74
|
+
export declare const VelinRating: VelinComponent;
|
|
75
|
+
export declare const VelinScrollTop: VelinComponent;
|
|
76
|
+
export declare const VelinScrollspy: VelinComponent;
|
|
77
|
+
export declare const VelinSearch: VelinComponent;
|
|
78
|
+
export declare const VelinSecureField: VelinComponent;
|
|
79
|
+
export declare const VelinSegmentedControl: VelinComponent;
|
|
80
|
+
export declare const VelinSheet: VelinComponent;
|
|
81
|
+
export declare const VelinSparkline: VelinComponent;
|
|
82
|
+
export declare const VelinStepper: VelinComponent;
|
|
83
|
+
export declare const VelinTabs: VelinComponent;
|
|
84
|
+
export declare const VelinThemeToggle: VelinComponent;
|
|
85
|
+
export declare const VelinToast: VelinComponent;
|
|
86
|
+
export declare const VelinTooltip: VelinComponent;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// src/create-component.jsx
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
// src/apply-props.js
|
|
5
|
+
var VELIN_EVENT_RE = /^onVelin[A-Z]/;
|
|
6
|
+
function isVelinEventProp(name) {
|
|
7
|
+
return VELIN_EVENT_RE.test(name);
|
|
8
|
+
}
|
|
9
|
+
function velinEventName(propName) {
|
|
10
|
+
return propName.slice(2).replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
function splitProps(props) {
|
|
13
|
+
const attributes = {};
|
|
14
|
+
const properties = {};
|
|
15
|
+
const booleans = {};
|
|
16
|
+
const listeners = {};
|
|
17
|
+
const reactProps = {};
|
|
18
|
+
let children;
|
|
19
|
+
let className;
|
|
20
|
+
let style;
|
|
21
|
+
for (const [key, value] of Object.entries(props)) {
|
|
22
|
+
if (key === "children") {
|
|
23
|
+
children = value;
|
|
24
|
+
} else if (key === "className") {
|
|
25
|
+
className = /** @type {string} */
|
|
26
|
+
value;
|
|
27
|
+
} else if (key === "style") {
|
|
28
|
+
style = value;
|
|
29
|
+
} else if (isVelinEventProp(key)) {
|
|
30
|
+
if (typeof value === "function") listeners[velinEventName(key)] = /** @type {EventListener} */
|
|
31
|
+
value;
|
|
32
|
+
} else if (/^on[A-Z]/.test(key)) {
|
|
33
|
+
reactProps[key] = value;
|
|
34
|
+
} else if (value === null || value === void 0) {
|
|
35
|
+
} else if (typeof value === "boolean") {
|
|
36
|
+
booleans[key] = value;
|
|
37
|
+
} else if (typeof value === "object" || typeof value === "function") {
|
|
38
|
+
properties[key] = value;
|
|
39
|
+
} else {
|
|
40
|
+
attributes[key] = /** @type {string | number} */
|
|
41
|
+
value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { children, className, style, attributes, properties, booleans, listeners, reactProps };
|
|
45
|
+
}
|
|
46
|
+
function applyVelinProps(el, { properties = {}, booleans = {} } = {}) {
|
|
47
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
48
|
+
el[key] = value;
|
|
49
|
+
}
|
|
50
|
+
for (const [key, value] of Object.entries(booleans)) {
|
|
51
|
+
if (value) el.setAttribute(key, "");
|
|
52
|
+
else el.removeAttribute(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function bindVelinListeners(el, listeners) {
|
|
56
|
+
const entries = Object.entries(listeners);
|
|
57
|
+
for (const [name, handler] of entries) el.addEventListener(name, handler);
|
|
58
|
+
return () => {
|
|
59
|
+
for (const [name, handler] of entries) el.removeEventListener(name, handler);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function componentNameForTag(tag) {
|
|
63
|
+
return tag.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/create-component.jsx
|
|
67
|
+
function createVelinComponent(tag) {
|
|
68
|
+
const Component = React.forwardRef(function VelinComponent(props, forwardedRef) {
|
|
69
|
+
const elementRef = React.useRef(null);
|
|
70
|
+
const { children, className, style, attributes, properties, booleans, listeners, reactProps } = splitProps(props);
|
|
71
|
+
const setRef = React.useCallback(
|
|
72
|
+
(node) => {
|
|
73
|
+
elementRef.current = node;
|
|
74
|
+
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
75
|
+
else if (forwardedRef) forwardedRef.current = node;
|
|
76
|
+
},
|
|
77
|
+
[forwardedRef]
|
|
78
|
+
);
|
|
79
|
+
React.useLayoutEffect(() => {
|
|
80
|
+
if (elementRef.current) applyVelinProps(elementRef.current, { properties, booleans });
|
|
81
|
+
});
|
|
82
|
+
React.useLayoutEffect(() => {
|
|
83
|
+
if (!elementRef.current) return void 0;
|
|
84
|
+
return bindVelinListeners(elementRef.current, listeners);
|
|
85
|
+
});
|
|
86
|
+
return React.createElement(
|
|
87
|
+
tag,
|
|
88
|
+
{ ref: setRef, className, style, ...attributes, ...reactProps },
|
|
89
|
+
children
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
Component.displayName = componentNameForTag(tag);
|
|
93
|
+
Component.tagName = tag;
|
|
94
|
+
return Component;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/tags.js
|
|
98
|
+
var VELIN_TAGS = [
|
|
99
|
+
"velin-accordion",
|
|
100
|
+
"velin-announcer",
|
|
101
|
+
"velin-bottom-nav",
|
|
102
|
+
"velin-carousel",
|
|
103
|
+
"velin-code-block",
|
|
104
|
+
"velin-collapse",
|
|
105
|
+
"velin-combobox",
|
|
106
|
+
"velin-command",
|
|
107
|
+
"velin-copy",
|
|
108
|
+
"velin-countdown",
|
|
109
|
+
"velin-counter",
|
|
110
|
+
"velin-data-table",
|
|
111
|
+
"velin-dialog",
|
|
112
|
+
"velin-drawer",
|
|
113
|
+
"velin-dropdown",
|
|
114
|
+
"velin-email",
|
|
115
|
+
"velin-form-summary",
|
|
116
|
+
"velin-icon",
|
|
117
|
+
"velin-lightbox",
|
|
118
|
+
"velin-live-dot",
|
|
119
|
+
"velin-menubar",
|
|
120
|
+
"velin-modal",
|
|
121
|
+
"velin-persist",
|
|
122
|
+
"velin-popover",
|
|
123
|
+
"velin-progress-ring",
|
|
124
|
+
"velin-rating",
|
|
125
|
+
"velin-scroll-top",
|
|
126
|
+
"velin-scrollspy",
|
|
127
|
+
"velin-search",
|
|
128
|
+
"velin-secure-field",
|
|
129
|
+
"velin-segmented-control",
|
|
130
|
+
"velin-sheet",
|
|
131
|
+
"velin-sparkline",
|
|
132
|
+
"velin-stepper",
|
|
133
|
+
"velin-tabs",
|
|
134
|
+
"velin-theme-toggle",
|
|
135
|
+
"velin-toast",
|
|
136
|
+
"velin-tooltip"
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
// src/index.jsx
|
|
140
|
+
var VelinAccordion = createVelinComponent("velin-accordion");
|
|
141
|
+
var VelinAnnouncer = createVelinComponent("velin-announcer");
|
|
142
|
+
var VelinBottomNav = createVelinComponent("velin-bottom-nav");
|
|
143
|
+
var VelinCarousel = createVelinComponent("velin-carousel");
|
|
144
|
+
var VelinCodeBlock = createVelinComponent("velin-code-block");
|
|
145
|
+
var VelinCollapse = createVelinComponent("velin-collapse");
|
|
146
|
+
var VelinCombobox = createVelinComponent("velin-combobox");
|
|
147
|
+
var VelinCommand = createVelinComponent("velin-command");
|
|
148
|
+
var VelinCopy = createVelinComponent("velin-copy");
|
|
149
|
+
var VelinCountdown = createVelinComponent("velin-countdown");
|
|
150
|
+
var VelinCounter = createVelinComponent("velin-counter");
|
|
151
|
+
var VelinDataTable = createVelinComponent("velin-data-table");
|
|
152
|
+
var VelinDialog = createVelinComponent("velin-dialog");
|
|
153
|
+
var VelinDrawer = createVelinComponent("velin-drawer");
|
|
154
|
+
var VelinDropdown = createVelinComponent("velin-dropdown");
|
|
155
|
+
var VelinEmail = createVelinComponent("velin-email");
|
|
156
|
+
var VelinFormSummary = createVelinComponent("velin-form-summary");
|
|
157
|
+
var VelinIcon = createVelinComponent("velin-icon");
|
|
158
|
+
var VelinLightbox = createVelinComponent("velin-lightbox");
|
|
159
|
+
var VelinLiveDot = createVelinComponent("velin-live-dot");
|
|
160
|
+
var VelinMenubar = createVelinComponent("velin-menubar");
|
|
161
|
+
var VelinModal = createVelinComponent("velin-modal");
|
|
162
|
+
var VelinPersist = createVelinComponent("velin-persist");
|
|
163
|
+
var VelinPopover = createVelinComponent("velin-popover");
|
|
164
|
+
var VelinProgressRing = createVelinComponent("velin-progress-ring");
|
|
165
|
+
var VelinRating = createVelinComponent("velin-rating");
|
|
166
|
+
var VelinScrollTop = createVelinComponent("velin-scroll-top");
|
|
167
|
+
var VelinScrollspy = createVelinComponent("velin-scrollspy");
|
|
168
|
+
var VelinSearch = createVelinComponent("velin-search");
|
|
169
|
+
var VelinSecureField = createVelinComponent("velin-secure-field");
|
|
170
|
+
var VelinSegmentedControl = createVelinComponent("velin-segmented-control");
|
|
171
|
+
var VelinSheet = createVelinComponent("velin-sheet");
|
|
172
|
+
var VelinSparkline = createVelinComponent("velin-sparkline");
|
|
173
|
+
var VelinStepper = createVelinComponent("velin-stepper");
|
|
174
|
+
var VelinTabs = createVelinComponent("velin-tabs");
|
|
175
|
+
var VelinThemeToggle = createVelinComponent("velin-theme-toggle");
|
|
176
|
+
var VelinToast = createVelinComponent("velin-toast");
|
|
177
|
+
var VelinTooltip = createVelinComponent("velin-tooltip");
|
|
178
|
+
export {
|
|
179
|
+
VELIN_TAGS,
|
|
180
|
+
VelinAccordion,
|
|
181
|
+
VelinAnnouncer,
|
|
182
|
+
VelinBottomNav,
|
|
183
|
+
VelinCarousel,
|
|
184
|
+
VelinCodeBlock,
|
|
185
|
+
VelinCollapse,
|
|
186
|
+
VelinCombobox,
|
|
187
|
+
VelinCommand,
|
|
188
|
+
VelinCopy,
|
|
189
|
+
VelinCountdown,
|
|
190
|
+
VelinCounter,
|
|
191
|
+
VelinDataTable,
|
|
192
|
+
VelinDialog,
|
|
193
|
+
VelinDrawer,
|
|
194
|
+
VelinDropdown,
|
|
195
|
+
VelinEmail,
|
|
196
|
+
VelinFormSummary,
|
|
197
|
+
VelinIcon,
|
|
198
|
+
VelinLightbox,
|
|
199
|
+
VelinLiveDot,
|
|
200
|
+
VelinMenubar,
|
|
201
|
+
VelinModal,
|
|
202
|
+
VelinPersist,
|
|
203
|
+
VelinPopover,
|
|
204
|
+
VelinProgressRing,
|
|
205
|
+
VelinRating,
|
|
206
|
+
VelinScrollTop,
|
|
207
|
+
VelinScrollspy,
|
|
208
|
+
VelinSearch,
|
|
209
|
+
VelinSecureField,
|
|
210
|
+
VelinSegmentedControl,
|
|
211
|
+
VelinSheet,
|
|
212
|
+
VelinSparkline,
|
|
213
|
+
VelinStepper,
|
|
214
|
+
VelinTabs,
|
|
215
|
+
VelinThemeToggle,
|
|
216
|
+
VelinToast,
|
|
217
|
+
VelinTooltip,
|
|
218
|
+
applyVelinProps,
|
|
219
|
+
bindVelinListeners,
|
|
220
|
+
componentNameForTag,
|
|
221
|
+
createVelinComponent,
|
|
222
|
+
isVelinEventProp,
|
|
223
|
+
splitProps,
|
|
224
|
+
velinEventName
|
|
225
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@velinstyle/react",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Official React wrappers for VelinStyle web components",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/",
|
|
17
|
+
"src/"
|
|
18
|
+
],
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/SkyliteDesign/velinstyle.git",
|
|
22
|
+
"directory": "packages/react"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://velinstyle.info/docs/guides/react-vite-starter.html",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/SkyliteDesign/velinstyle/issues"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"react",
|
|
30
|
+
"web-components",
|
|
31
|
+
"velinstyle",
|
|
32
|
+
"accessibility",
|
|
33
|
+
"wcag"
|
|
34
|
+
],
|
|
35
|
+
"author": "SkyliteDesign",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"registry": "https://registry.npmjs.org"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"generate": "node ../../scripts/generate-react-wrappers.mjs",
|
|
46
|
+
"build": "npm run generate && esbuild src/index.jsx --bundle --format=esm --packages=external --loader:.jsx=jsx --outfile=dist/index.js && node -e \"require('fs').copyFileSync('src/index.d.ts','dist/index.d.ts')\"",
|
|
47
|
+
"prepublishOnly": "npm run build",
|
|
48
|
+
"pack:check": "npm pack --dry-run"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"esbuild": "^0.24.0",
|
|
52
|
+
"react": "^18.3.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prop mapping between React and VelinStyle custom elements.
|
|
3
|
+
*
|
|
4
|
+
* React 18 renders every unknown prop as a string attribute, which breaks two
|
|
5
|
+
* things for custom elements: `false` becomes the attribute `"false"` (still
|
|
6
|
+
* truthy to `hasAttribute`), and objects/arrays are stringified. Both are
|
|
7
|
+
* handled imperatively here so the wrappers behave the same on React 18 and 19.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Custom-event props use the `onVelin…` prefix; plain React events pass through. */
|
|
11
|
+
const VELIN_EVENT_RE = /^onVelin[A-Z]/;
|
|
12
|
+
|
|
13
|
+
/** @param {string} name */
|
|
14
|
+
export function isVelinEventProp(name) {
|
|
15
|
+
return VELIN_EVENT_RE.test(name);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `onVelinSearchSelect` → `velin-search-select`
|
|
20
|
+
* @param {string} propName
|
|
21
|
+
*/
|
|
22
|
+
export function velinEventName(propName) {
|
|
23
|
+
return propName
|
|
24
|
+
.slice(2)
|
|
25
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
26
|
+
.toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Split React props into the buckets each target can handle.
|
|
31
|
+
* @param {Record<string, unknown>} props
|
|
32
|
+
*/
|
|
33
|
+
export function splitProps(props) {
|
|
34
|
+
/** @type {Record<string, string | number>} */
|
|
35
|
+
const attributes = {};
|
|
36
|
+
/** @type {Record<string, unknown>} */
|
|
37
|
+
const properties = {};
|
|
38
|
+
/** @type {Record<string, boolean>} */
|
|
39
|
+
const booleans = {};
|
|
40
|
+
/** @type {Record<string, EventListener>} */
|
|
41
|
+
const listeners = {};
|
|
42
|
+
/** @type {Record<string, unknown>} */
|
|
43
|
+
const reactProps = {};
|
|
44
|
+
|
|
45
|
+
let children;
|
|
46
|
+
let className;
|
|
47
|
+
let style;
|
|
48
|
+
|
|
49
|
+
for (const [key, value] of Object.entries(props)) {
|
|
50
|
+
if (key === 'children') {
|
|
51
|
+
children = value;
|
|
52
|
+
} else if (key === 'className') {
|
|
53
|
+
className = /** @type {string} */ (value);
|
|
54
|
+
} else if (key === 'style') {
|
|
55
|
+
style = value;
|
|
56
|
+
} else if (isVelinEventProp(key)) {
|
|
57
|
+
if (typeof value === 'function') listeners[velinEventName(key)] = /** @type {EventListener} */ (value);
|
|
58
|
+
} else if (/^on[A-Z]/.test(key)) {
|
|
59
|
+
reactProps[key] = value;
|
|
60
|
+
} else if (value === null || value === undefined) {
|
|
61
|
+
// Omitted entirely so the attribute is absent rather than "null".
|
|
62
|
+
} else if (typeof value === 'boolean') {
|
|
63
|
+
booleans[key] = value;
|
|
64
|
+
} else if (typeof value === 'object' || typeof value === 'function') {
|
|
65
|
+
properties[key] = value;
|
|
66
|
+
} else {
|
|
67
|
+
attributes[key] = /** @type {string | number} */ (value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { children, className, style, attributes, properties, booleans, listeners, reactProps };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Apply the buckets React cannot express directly onto the element.
|
|
76
|
+
* @param {Element} el
|
|
77
|
+
* @param {{ properties?: Record<string, unknown>, booleans?: Record<string, boolean> }} parts
|
|
78
|
+
*/
|
|
79
|
+
export function applyVelinProps(el, { properties = {}, booleans = {} } = {}) {
|
|
80
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
81
|
+
el[key] = value;
|
|
82
|
+
}
|
|
83
|
+
for (const [key, value] of Object.entries(booleans)) {
|
|
84
|
+
if (value) el.setAttribute(key, '');
|
|
85
|
+
else el.removeAttribute(key);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Attach custom-event listeners and return a cleanup function.
|
|
91
|
+
* @param {Element} el
|
|
92
|
+
* @param {Record<string, EventListener>} listeners
|
|
93
|
+
*/
|
|
94
|
+
export function bindVelinListeners(el, listeners) {
|
|
95
|
+
const entries = Object.entries(listeners);
|
|
96
|
+
for (const [name, handler] of entries) el.addEventListener(name, handler);
|
|
97
|
+
return () => {
|
|
98
|
+
for (const [name, handler] of entries) el.removeEventListener(name, handler);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* `velin-code-block` → `VelinCodeBlock`
|
|
104
|
+
* @param {string} tag
|
|
105
|
+
*/
|
|
106
|
+
export function componentNameForTag(tag) {
|
|
107
|
+
return tag
|
|
108
|
+
.split('-')
|
|
109
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
110
|
+
.join('');
|
|
111
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { splitProps, applyVelinProps, bindVelinListeners, componentNameForTag } from './apply-props.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a React wrapper for a VelinStyle custom element.
|
|
6
|
+
*
|
|
7
|
+
* Scalar props become attributes, booleans become presence attributes, objects
|
|
8
|
+
* and arrays are assigned as element properties, and `onVelin*` props are bound
|
|
9
|
+
* as custom-event listeners.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} tag
|
|
12
|
+
*/
|
|
13
|
+
export function createVelinComponent(tag) {
|
|
14
|
+
const Component = React.forwardRef(function VelinComponent(props, forwardedRef) {
|
|
15
|
+
const elementRef = React.useRef(null);
|
|
16
|
+
const { children, className, style, attributes, properties, booleans, listeners, reactProps } = splitProps(props);
|
|
17
|
+
|
|
18
|
+
const setRef = React.useCallback(
|
|
19
|
+
(node) => {
|
|
20
|
+
elementRef.current = node;
|
|
21
|
+
if (typeof forwardedRef === 'function') forwardedRef(node);
|
|
22
|
+
else if (forwardedRef) forwardedRef.current = node;
|
|
23
|
+
},
|
|
24
|
+
[forwardedRef],
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
React.useLayoutEffect(() => {
|
|
28
|
+
if (elementRef.current) applyVelinProps(elementRef.current, { properties, booleans });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
React.useLayoutEffect(() => {
|
|
32
|
+
if (!elementRef.current) return undefined;
|
|
33
|
+
return bindVelinListeners(elementRef.current, listeners);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return React.createElement(
|
|
37
|
+
tag,
|
|
38
|
+
{ ref: setRef, className, style, ...attributes, ...reactProps },
|
|
39
|
+
children,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
Component.displayName = componentNameForTag(tag);
|
|
44
|
+
Component.tagName = tag;
|
|
45
|
+
return Component;
|
|
46
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-GENERATED — run: node scripts/generate-react-wrappers.mjs
|
|
3
|
+
*/
|
|
4
|
+
import type { ComponentType, CSSProperties, ReactNode, Ref } from 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Props accepted by every wrapper. Scalars become attributes, booleans become
|
|
8
|
+
* presence attributes, objects and arrays are assigned as element properties,
|
|
9
|
+
* and `onVelin*` handlers are bound as custom-event listeners.
|
|
10
|
+
*/
|
|
11
|
+
export interface VelinComponentProps {
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
style?: CSSProperties;
|
|
15
|
+
ref?: Ref<HTMLElement>;
|
|
16
|
+
[prop: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface VelinComponent extends ComponentType<VelinComponentProps> {
|
|
20
|
+
/** Custom element tag rendered by this wrapper. */
|
|
21
|
+
tagName: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export declare function createVelinComponent(tag: string): VelinComponent;
|
|
25
|
+
export declare const VELIN_TAGS: readonly string[];
|
|
26
|
+
|
|
27
|
+
export declare function isVelinEventProp(name: string): boolean;
|
|
28
|
+
export declare function velinEventName(propName: string): string;
|
|
29
|
+
export declare function componentNameForTag(tag: string): string;
|
|
30
|
+
export declare function splitProps(props: Record<string, unknown>): {
|
|
31
|
+
children?: ReactNode;
|
|
32
|
+
className?: string;
|
|
33
|
+
style?: CSSProperties;
|
|
34
|
+
attributes: Record<string, string | number>;
|
|
35
|
+
properties: Record<string, unknown>;
|
|
36
|
+
booleans: Record<string, boolean>;
|
|
37
|
+
listeners: Record<string, EventListener>;
|
|
38
|
+
reactProps: Record<string, unknown>;
|
|
39
|
+
};
|
|
40
|
+
export declare function applyVelinProps(
|
|
41
|
+
el: Element,
|
|
42
|
+
parts: { properties?: Record<string, unknown>; booleans?: Record<string, boolean> },
|
|
43
|
+
): void;
|
|
44
|
+
export declare function bindVelinListeners(
|
|
45
|
+
el: Element,
|
|
46
|
+
listeners: Record<string, EventListener>,
|
|
47
|
+
): () => void;
|
|
48
|
+
|
|
49
|
+
export declare const VelinAccordion: VelinComponent;
|
|
50
|
+
export declare const VelinAnnouncer: VelinComponent;
|
|
51
|
+
export declare const VelinBottomNav: VelinComponent;
|
|
52
|
+
export declare const VelinCarousel: VelinComponent;
|
|
53
|
+
export declare const VelinCodeBlock: VelinComponent;
|
|
54
|
+
export declare const VelinCollapse: VelinComponent;
|
|
55
|
+
export declare const VelinCombobox: VelinComponent;
|
|
56
|
+
export declare const VelinCommand: VelinComponent;
|
|
57
|
+
export declare const VelinCopy: VelinComponent;
|
|
58
|
+
export declare const VelinCountdown: VelinComponent;
|
|
59
|
+
export declare const VelinCounter: VelinComponent;
|
|
60
|
+
export declare const VelinDataTable: VelinComponent;
|
|
61
|
+
export declare const VelinDialog: VelinComponent;
|
|
62
|
+
export declare const VelinDrawer: VelinComponent;
|
|
63
|
+
export declare const VelinDropdown: VelinComponent;
|
|
64
|
+
export declare const VelinEmail: VelinComponent;
|
|
65
|
+
export declare const VelinFormSummary: VelinComponent;
|
|
66
|
+
export declare const VelinIcon: VelinComponent;
|
|
67
|
+
export declare const VelinLightbox: VelinComponent;
|
|
68
|
+
export declare const VelinLiveDot: VelinComponent;
|
|
69
|
+
export declare const VelinMenubar: VelinComponent;
|
|
70
|
+
export declare const VelinModal: VelinComponent;
|
|
71
|
+
export declare const VelinPersist: VelinComponent;
|
|
72
|
+
export declare const VelinPopover: VelinComponent;
|
|
73
|
+
export declare const VelinProgressRing: VelinComponent;
|
|
74
|
+
export declare const VelinRating: VelinComponent;
|
|
75
|
+
export declare const VelinScrollTop: VelinComponent;
|
|
76
|
+
export declare const VelinScrollspy: VelinComponent;
|
|
77
|
+
export declare const VelinSearch: VelinComponent;
|
|
78
|
+
export declare const VelinSecureField: VelinComponent;
|
|
79
|
+
export declare const VelinSegmentedControl: VelinComponent;
|
|
80
|
+
export declare const VelinSheet: VelinComponent;
|
|
81
|
+
export declare const VelinSparkline: VelinComponent;
|
|
82
|
+
export declare const VelinStepper: VelinComponent;
|
|
83
|
+
export declare const VelinTabs: VelinComponent;
|
|
84
|
+
export declare const VelinThemeToggle: VelinComponent;
|
|
85
|
+
export declare const VelinToast: VelinComponent;
|
|
86
|
+
export declare const VelinTooltip: VelinComponent;
|
package/src/index.jsx
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-GENERATED — run: node scripts/generate-react-wrappers.mjs
|
|
3
|
+
*/
|
|
4
|
+
import { createVelinComponent } from './create-component.jsx';
|
|
5
|
+
|
|
6
|
+
export { createVelinComponent } from './create-component.jsx';
|
|
7
|
+
export {
|
|
8
|
+
splitProps,
|
|
9
|
+
applyVelinProps,
|
|
10
|
+
bindVelinListeners,
|
|
11
|
+
isVelinEventProp,
|
|
12
|
+
velinEventName,
|
|
13
|
+
componentNameForTag,
|
|
14
|
+
} from './apply-props.js';
|
|
15
|
+
export { VELIN_TAGS } from './tags.js';
|
|
16
|
+
|
|
17
|
+
export const VelinAccordion = createVelinComponent('velin-accordion');
|
|
18
|
+
export const VelinAnnouncer = createVelinComponent('velin-announcer');
|
|
19
|
+
export const VelinBottomNav = createVelinComponent('velin-bottom-nav');
|
|
20
|
+
export const VelinCarousel = createVelinComponent('velin-carousel');
|
|
21
|
+
export const VelinCodeBlock = createVelinComponent('velin-code-block');
|
|
22
|
+
export const VelinCollapse = createVelinComponent('velin-collapse');
|
|
23
|
+
export const VelinCombobox = createVelinComponent('velin-combobox');
|
|
24
|
+
export const VelinCommand = createVelinComponent('velin-command');
|
|
25
|
+
export const VelinCopy = createVelinComponent('velin-copy');
|
|
26
|
+
export const VelinCountdown = createVelinComponent('velin-countdown');
|
|
27
|
+
export const VelinCounter = createVelinComponent('velin-counter');
|
|
28
|
+
export const VelinDataTable = createVelinComponent('velin-data-table');
|
|
29
|
+
export const VelinDialog = createVelinComponent('velin-dialog');
|
|
30
|
+
export const VelinDrawer = createVelinComponent('velin-drawer');
|
|
31
|
+
export const VelinDropdown = createVelinComponent('velin-dropdown');
|
|
32
|
+
export const VelinEmail = createVelinComponent('velin-email');
|
|
33
|
+
export const VelinFormSummary = createVelinComponent('velin-form-summary');
|
|
34
|
+
export const VelinIcon = createVelinComponent('velin-icon');
|
|
35
|
+
export const VelinLightbox = createVelinComponent('velin-lightbox');
|
|
36
|
+
export const VelinLiveDot = createVelinComponent('velin-live-dot');
|
|
37
|
+
export const VelinMenubar = createVelinComponent('velin-menubar');
|
|
38
|
+
export const VelinModal = createVelinComponent('velin-modal');
|
|
39
|
+
export const VelinPersist = createVelinComponent('velin-persist');
|
|
40
|
+
export const VelinPopover = createVelinComponent('velin-popover');
|
|
41
|
+
export const VelinProgressRing = createVelinComponent('velin-progress-ring');
|
|
42
|
+
export const VelinRating = createVelinComponent('velin-rating');
|
|
43
|
+
export const VelinScrollTop = createVelinComponent('velin-scroll-top');
|
|
44
|
+
export const VelinScrollspy = createVelinComponent('velin-scrollspy');
|
|
45
|
+
export const VelinSearch = createVelinComponent('velin-search');
|
|
46
|
+
export const VelinSecureField = createVelinComponent('velin-secure-field');
|
|
47
|
+
export const VelinSegmentedControl = createVelinComponent('velin-segmented-control');
|
|
48
|
+
export const VelinSheet = createVelinComponent('velin-sheet');
|
|
49
|
+
export const VelinSparkline = createVelinComponent('velin-sparkline');
|
|
50
|
+
export const VelinStepper = createVelinComponent('velin-stepper');
|
|
51
|
+
export const VelinTabs = createVelinComponent('velin-tabs');
|
|
52
|
+
export const VelinThemeToggle = createVelinComponent('velin-theme-toggle');
|
|
53
|
+
export const VelinToast = createVelinComponent('velin-toast');
|
|
54
|
+
export const VelinTooltip = createVelinComponent('velin-tooltip');
|
package/src/tags.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-GENERATED — run: node scripts/generate-react-wrappers.mjs
|
|
3
|
+
*/
|
|
4
|
+
export const VELIN_TAGS = [
|
|
5
|
+
'velin-accordion',
|
|
6
|
+
'velin-announcer',
|
|
7
|
+
'velin-bottom-nav',
|
|
8
|
+
'velin-carousel',
|
|
9
|
+
'velin-code-block',
|
|
10
|
+
'velin-collapse',
|
|
11
|
+
'velin-combobox',
|
|
12
|
+
'velin-command',
|
|
13
|
+
'velin-copy',
|
|
14
|
+
'velin-countdown',
|
|
15
|
+
'velin-counter',
|
|
16
|
+
'velin-data-table',
|
|
17
|
+
'velin-dialog',
|
|
18
|
+
'velin-drawer',
|
|
19
|
+
'velin-dropdown',
|
|
20
|
+
'velin-email',
|
|
21
|
+
'velin-form-summary',
|
|
22
|
+
'velin-icon',
|
|
23
|
+
'velin-lightbox',
|
|
24
|
+
'velin-live-dot',
|
|
25
|
+
'velin-menubar',
|
|
26
|
+
'velin-modal',
|
|
27
|
+
'velin-persist',
|
|
28
|
+
'velin-popover',
|
|
29
|
+
'velin-progress-ring',
|
|
30
|
+
'velin-rating',
|
|
31
|
+
'velin-scroll-top',
|
|
32
|
+
'velin-scrollspy',
|
|
33
|
+
'velin-search',
|
|
34
|
+
'velin-secure-field',
|
|
35
|
+
'velin-segmented-control',
|
|
36
|
+
'velin-sheet',
|
|
37
|
+
'velin-sparkline',
|
|
38
|
+
'velin-stepper',
|
|
39
|
+
'velin-tabs',
|
|
40
|
+
'velin-theme-toggle',
|
|
41
|
+
'velin-toast',
|
|
42
|
+
'velin-tooltip',
|
|
43
|
+
];
|