@tailorkit/react 0.1.0-beta.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/LICENSE +674 -0
- package/dist/components/context.d.ts +11 -0
- package/dist/components/context.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/render.d.ts +20 -0
- package/dist/components/render.d.ts.map +1 -0
- package/dist/components/root.d.ts +11 -0
- package/dist/components/root.d.ts.map +1 -0
- package/dist/hooks/use-apps.d.ts +13 -0
- package/dist/hooks/use-apps.d.ts.map +1 -0
- package/dist/hooks/use-current-screen.d.ts +24 -0
- package/dist/hooks/use-current-screen.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +920 -0
- package/dist/node-store.d.ts +17 -0
- package/dist/node-store.d.ts.map +1 -0
- package/dist/primitives.d.ts +38 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/remote-context.d.ts +9 -0
- package/dist/remote-context.d.ts.map +1 -0
- package/dist/remote-view.d.ts +15 -0
- package/dist/remote-view.d.ts.map +1 -0
- package/dist/tailor-kit.d.ts +117 -0
- package/dist/tailor-kit.d.ts.map +1 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
// src/tailor-kit.tsx
|
|
2
|
+
import { useEffect as useEffect4, useId as useId2, useMemo as useMemo6, useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
3
|
+
|
|
4
|
+
// src/components/root.tsx
|
|
5
|
+
import { useMemo as useMemo2 } from "react";
|
|
6
|
+
|
|
7
|
+
// src/components/render.tsx
|
|
8
|
+
import { cloneElement, createElement, isValidElement, useMemo } from "react";
|
|
9
|
+
function useRender({
|
|
10
|
+
defaultTagName,
|
|
11
|
+
props,
|
|
12
|
+
render
|
|
13
|
+
}) {
|
|
14
|
+
return useMemo(() => {
|
|
15
|
+
if (render) {
|
|
16
|
+
if (!isValidElement(render)) {
|
|
17
|
+
throw new Error("The render prop must be a valid React element.");
|
|
18
|
+
}
|
|
19
|
+
return cloneElement(render, mergeProps(render.props, props));
|
|
20
|
+
}
|
|
21
|
+
return createElement(defaultTagName, props);
|
|
22
|
+
}, [defaultTagName, props, render]);
|
|
23
|
+
}
|
|
24
|
+
function mergeProps(...propSets) {
|
|
25
|
+
const merged = {};
|
|
26
|
+
for (const props of propSets) {
|
|
27
|
+
if (!props) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
for (const [key, value] of Object.entries(props)) {
|
|
31
|
+
if (value === void 0) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const existing = merged[key];
|
|
35
|
+
if (key === "className" && existing && value) {
|
|
36
|
+
merged[key] = `${existing} ${value}`;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (key === "style" && existing && value) {
|
|
40
|
+
merged[key] = {
|
|
41
|
+
...existing,
|
|
42
|
+
...value
|
|
43
|
+
};
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (key === "ref" && existing && value) {
|
|
47
|
+
merged[key] = composeRefs(existing, value);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (isEventHandler(key) && typeof existing === "function" && typeof value === "function") {
|
|
51
|
+
merged[key] = (...args) => {
|
|
52
|
+
existing(...args);
|
|
53
|
+
const event = args[0];
|
|
54
|
+
if (!event?.defaultPrevented) {
|
|
55
|
+
value(...args);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
merged[key] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return merged;
|
|
64
|
+
}
|
|
65
|
+
function composeRefs(...refs) {
|
|
66
|
+
return (node) => {
|
|
67
|
+
for (const ref of refs) {
|
|
68
|
+
if (typeof ref === "function") {
|
|
69
|
+
ref(node);
|
|
70
|
+
} else if (ref) {
|
|
71
|
+
ref.current = node;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function isEventHandler(key) {
|
|
77
|
+
return /^on[A-Z]/u.test(key);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/components/context.ts
|
|
81
|
+
import { createContext, useContext } from "react";
|
|
82
|
+
var TailorRootContext = createContext(null);
|
|
83
|
+
function useTailorRootContext(component) {
|
|
84
|
+
const context = useContext(TailorRootContext);
|
|
85
|
+
if (!context) {
|
|
86
|
+
throw new Error(`${component} must be rendered inside tailor.Root.`);
|
|
87
|
+
}
|
|
88
|
+
return context;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/components/root.tsx
|
|
92
|
+
import { jsx } from "react/jsx-runtime";
|
|
93
|
+
var EMPTY_APPS = [];
|
|
94
|
+
function Root({ apps: appsProp, children, render, tailor, ...props }) {
|
|
95
|
+
const appsResult = tailor.useApps();
|
|
96
|
+
const apps = appsProp ?? appsResult.data ?? EMPTY_APPS;
|
|
97
|
+
const context = useMemo2(
|
|
98
|
+
() => ({
|
|
99
|
+
apps
|
|
100
|
+
}),
|
|
101
|
+
[apps]
|
|
102
|
+
);
|
|
103
|
+
const element = render ? useRender({
|
|
104
|
+
defaultTagName: "div",
|
|
105
|
+
props: mergeProps({ children }, props),
|
|
106
|
+
render
|
|
107
|
+
}) : children;
|
|
108
|
+
return /* @__PURE__ */ jsx(TailorRootContext.Provider, { value: context, children: element });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/hooks/use-apps.ts
|
|
112
|
+
import { useCallback, useEffect, useSyncExternalStore } from "react";
|
|
113
|
+
function createUseApps(store) {
|
|
114
|
+
return function useApps() {
|
|
115
|
+
const snapshot = useSyncExternalStore(
|
|
116
|
+
store.subscribe,
|
|
117
|
+
store.getAppsSnapshot,
|
|
118
|
+
store.getAppsSnapshot
|
|
119
|
+
);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
void store.fetchApps();
|
|
122
|
+
}, []);
|
|
123
|
+
const refetch = useCallback(() => store.fetchApps({ force: true }), []);
|
|
124
|
+
return toUseAppsResult(snapshot, refetch);
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function toUseAppsResult(snapshot, refetch) {
|
|
128
|
+
return {
|
|
129
|
+
data: snapshot.status === "ready" ? snapshot.apps : void 0,
|
|
130
|
+
error: snapshot.error,
|
|
131
|
+
isError: snapshot.status === "error",
|
|
132
|
+
isLoading: snapshot.status === "loading",
|
|
133
|
+
isPending: snapshot.status === "idle" || snapshot.status === "loading",
|
|
134
|
+
isSuccess: snapshot.status === "ready",
|
|
135
|
+
refetch,
|
|
136
|
+
status: snapshot.status
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/hooks/use-current-screen.ts
|
|
141
|
+
import { useEffect as useEffect2, useMemo as useMemo3, useRef } from "react";
|
|
142
|
+
function createUseCurrentScreen(store) {
|
|
143
|
+
return function useCurrentScreen(options) {
|
|
144
|
+
useTailorRootContext("tailor.useCurrentScreen");
|
|
145
|
+
const id = useMemo3(() => /* @__PURE__ */ Symbol("tailorkit-current-screen"), []);
|
|
146
|
+
const status = options.status ?? "ready";
|
|
147
|
+
const context = "context" in options ? options.context : void 0;
|
|
148
|
+
const contextKey = JSON.stringify(context);
|
|
149
|
+
const contextRef = useRef({ key: contextKey, value: context });
|
|
150
|
+
if (contextRef.current.key !== contextKey) {
|
|
151
|
+
contextRef.current = { key: contextKey, value: context };
|
|
152
|
+
}
|
|
153
|
+
const contextSnapshot = contextRef.current.value;
|
|
154
|
+
useEffect2(
|
|
155
|
+
() => () => {
|
|
156
|
+
store.unregisterScreen(id);
|
|
157
|
+
},
|
|
158
|
+
[id]
|
|
159
|
+
);
|
|
160
|
+
useEffect2(() => {
|
|
161
|
+
store.registerScreen({
|
|
162
|
+
context: contextSnapshot,
|
|
163
|
+
id,
|
|
164
|
+
screen: options.screen,
|
|
165
|
+
status
|
|
166
|
+
});
|
|
167
|
+
}, [contextSnapshot, id, options.screen, status]);
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/primitives.tsx
|
|
172
|
+
import { createContext as createContext2, useContext as useContext2, useId, useMemo as useMemo4 } from "react";
|
|
173
|
+
import { resolveTheme } from "@tailorkit/core/primitives/theme";
|
|
174
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
175
|
+
var PrimitiveThemeContext = createContext2(null);
|
|
176
|
+
var tokenGroups = [
|
|
177
|
+
"space",
|
|
178
|
+
"radius",
|
|
179
|
+
"background",
|
|
180
|
+
"border",
|
|
181
|
+
"borderColor",
|
|
182
|
+
"textColor"
|
|
183
|
+
];
|
|
184
|
+
var cssEscape = (value) => {
|
|
185
|
+
const css = globalThis.CSS;
|
|
186
|
+
if (typeof css?.escape === "function") {
|
|
187
|
+
return css.escape(value);
|
|
188
|
+
}
|
|
189
|
+
return value.replaceAll(/[^A-Za-z0-9_-]/gu, "\\$&");
|
|
190
|
+
};
|
|
191
|
+
var toCssValue = (group, value) => {
|
|
192
|
+
if (typeof value === "number") {
|
|
193
|
+
return `${value}px`;
|
|
194
|
+
}
|
|
195
|
+
if (group === "size" || group === "raw") {
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
return `var(--tailorkit-${group}-${cssEscape(value)})`;
|
|
199
|
+
};
|
|
200
|
+
var toResponsiveEntries = (value) => {
|
|
201
|
+
if (value === void 0) {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
if (!(value && typeof value === "object") || Array.isArray(value)) {
|
|
205
|
+
return [["base", value]];
|
|
206
|
+
}
|
|
207
|
+
return Object.entries(value).filter(
|
|
208
|
+
(entry) => entry[1] !== void 0
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
var declarationsForProp = (prop, value) => {
|
|
212
|
+
if (prop === "columns") {
|
|
213
|
+
return [`grid-template-columns: repeat(${String(value)}, minmax(0, 1fr));`];
|
|
214
|
+
}
|
|
215
|
+
const tokenDeclarations = {
|
|
216
|
+
background: ["background", "background"],
|
|
217
|
+
border: ["border", "border"],
|
|
218
|
+
borderColor: ["borderColor", "border-color"],
|
|
219
|
+
textColor: ["textColor", "color"]
|
|
220
|
+
};
|
|
221
|
+
const tokenDeclaration = tokenDeclarations[prop];
|
|
222
|
+
if (tokenDeclaration) {
|
|
223
|
+
const [group, property] = tokenDeclaration;
|
|
224
|
+
if (prop === "border") {
|
|
225
|
+
return [`border-style: ${toCssValue(group, value)};`, "border-width: 1px;"];
|
|
226
|
+
}
|
|
227
|
+
return [`${property}: ${toCssValue(group, value)};`];
|
|
228
|
+
}
|
|
229
|
+
if (prop === "gap" || prop === "margin" || prop === "padding") {
|
|
230
|
+
return [`${prop}: ${toCssValue("space", value)};`];
|
|
231
|
+
}
|
|
232
|
+
if (prop === "height" || prop === "width") {
|
|
233
|
+
return [`${prop}: ${toCssValue("size", value)};`];
|
|
234
|
+
}
|
|
235
|
+
if (prop === "radius") {
|
|
236
|
+
return [`border-radius: ${toCssValue("radius", value)};`];
|
|
237
|
+
}
|
|
238
|
+
if (prop === "align") {
|
|
239
|
+
return [`align-items: ${toFlexAlignment(value)};`];
|
|
240
|
+
}
|
|
241
|
+
if (prop === "justify") {
|
|
242
|
+
return [`justify-content: ${toFlexJustification(value)};`];
|
|
243
|
+
}
|
|
244
|
+
if (prop === "direction") {
|
|
245
|
+
return [`flex-direction: ${toCssValue("raw", value)};`];
|
|
246
|
+
}
|
|
247
|
+
if (prop === "overflow") {
|
|
248
|
+
return [`overflow: ${toCssValue("raw", value)};`];
|
|
249
|
+
}
|
|
250
|
+
if (prop === "grow") {
|
|
251
|
+
return [`flex-grow: ${String(value)};`];
|
|
252
|
+
}
|
|
253
|
+
if (prop === "shrink") {
|
|
254
|
+
return [`flex-shrink: ${String(value)};`];
|
|
255
|
+
}
|
|
256
|
+
if (prop === "minHeight") {
|
|
257
|
+
return [`min-height: ${toCssValue("size", value)};`];
|
|
258
|
+
}
|
|
259
|
+
if (prop === "minWidth") {
|
|
260
|
+
return [`min-width: ${toCssValue("size", value)};`];
|
|
261
|
+
}
|
|
262
|
+
if (prop === "basis") {
|
|
263
|
+
return [`flex-basis: ${toCssValue("size", value)};`];
|
|
264
|
+
}
|
|
265
|
+
if (prop === "wrap") {
|
|
266
|
+
return [`flex-wrap: ${toCssValue("raw", value)};`];
|
|
267
|
+
}
|
|
268
|
+
return [];
|
|
269
|
+
};
|
|
270
|
+
var toFlexAlignment = (value) => {
|
|
271
|
+
if (value === "start") {
|
|
272
|
+
return "flex-start";
|
|
273
|
+
}
|
|
274
|
+
if (value === "end") {
|
|
275
|
+
return "flex-end";
|
|
276
|
+
}
|
|
277
|
+
return String(value);
|
|
278
|
+
};
|
|
279
|
+
var toFlexJustification = (value) => {
|
|
280
|
+
if (value === "start") {
|
|
281
|
+
return "flex-start";
|
|
282
|
+
}
|
|
283
|
+
if (value === "end") {
|
|
284
|
+
return "flex-end";
|
|
285
|
+
}
|
|
286
|
+
if (value === "between") {
|
|
287
|
+
return "space-between";
|
|
288
|
+
}
|
|
289
|
+
return String(value);
|
|
290
|
+
};
|
|
291
|
+
var buildPrimitiveCss = ({
|
|
292
|
+
display,
|
|
293
|
+
nodeId,
|
|
294
|
+
props,
|
|
295
|
+
screenId,
|
|
296
|
+
theme
|
|
297
|
+
}) => {
|
|
298
|
+
const selector = `[data-tailorkit-screen="${cssEscape(screenId)}"] [data-tailorkit-node="${cssEscape(
|
|
299
|
+
nodeId
|
|
300
|
+
)}"]`;
|
|
301
|
+
const base = [`display: ${display};`];
|
|
302
|
+
const byBreakpoint = /* @__PURE__ */ new Map();
|
|
303
|
+
for (const [prop, rawValue] of Object.entries(props)) {
|
|
304
|
+
for (const [breakpoint, value] of toResponsiveEntries(rawValue)) {
|
|
305
|
+
const declarations = declarationsForProp(prop, value);
|
|
306
|
+
if (declarations.length === 0) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const target = breakpoint === "base" ? base : byBreakpoint.get(breakpoint) ?? [];
|
|
310
|
+
target.push(...declarations);
|
|
311
|
+
if (breakpoint !== "base") {
|
|
312
|
+
byBreakpoint.set(breakpoint, target);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const rules = [`${selector} { ${base.join(" ")} }`];
|
|
317
|
+
for (const [breakpoint, declarations] of byBreakpoint) {
|
|
318
|
+
const minWidth = theme.breakpoints?.[breakpoint];
|
|
319
|
+
if (!minWidth) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
rules.push(`@media (min-width: ${minWidth}) { ${selector} { ${declarations.join(" ")} } }`);
|
|
323
|
+
}
|
|
324
|
+
return rules.join("\n");
|
|
325
|
+
};
|
|
326
|
+
var buildThemeCss = (screenId, theme) => {
|
|
327
|
+
const selector = `[data-tailorkit-screen="${cssEscape(screenId)}"]`;
|
|
328
|
+
const declarations = [];
|
|
329
|
+
const resolvedTheme = resolveTheme(theme);
|
|
330
|
+
for (const group of tokenGroups) {
|
|
331
|
+
for (const [name, value] of Object.entries(resolvedTheme.tokens?.[group] ?? {})) {
|
|
332
|
+
declarations.push(`--tailorkit-${group}-${cssEscape(name)}: ${value};`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return `${selector} { ${declarations.join(" ")} }`;
|
|
336
|
+
};
|
|
337
|
+
var usePrimitiveNodeId = () => {
|
|
338
|
+
const id = useId();
|
|
339
|
+
return useMemo4(() => `tailorkit-${id.replaceAll(":", "")}`, [id]);
|
|
340
|
+
};
|
|
341
|
+
var Primitive = ({
|
|
342
|
+
children,
|
|
343
|
+
display,
|
|
344
|
+
element,
|
|
345
|
+
props
|
|
346
|
+
}) => {
|
|
347
|
+
const context = useContext2(PrimitiveThemeContext);
|
|
348
|
+
const nodeId = usePrimitiveNodeId();
|
|
349
|
+
const Element = element;
|
|
350
|
+
const css = context ? buildPrimitiveCss({
|
|
351
|
+
display,
|
|
352
|
+
nodeId,
|
|
353
|
+
props,
|
|
354
|
+
screenId: context.screenId,
|
|
355
|
+
theme: context.theme
|
|
356
|
+
}) : "";
|
|
357
|
+
return /* @__PURE__ */ jsxs(Element, { "data-tailorkit-node": nodeId, children: [
|
|
358
|
+
css === "" ? null : /* @__PURE__ */ jsx2("style", { "data-tailorkit-node-style": nodeId, children: css }),
|
|
359
|
+
children
|
|
360
|
+
] });
|
|
361
|
+
};
|
|
362
|
+
var primitives = {
|
|
363
|
+
Box: ({ props, slots }) => /* @__PURE__ */ jsx2(Primitive, { display: "block", element: "div", props, children: slots.default }),
|
|
364
|
+
Flex: ({ props, slots }) => /* @__PURE__ */ jsx2(Primitive, { display: "flex", element: "div", props, children: slots.default }),
|
|
365
|
+
Grid: ({ props, slots }) => /* @__PURE__ */ jsx2(Primitive, { display: "grid", element: "div", props, children: slots.default }),
|
|
366
|
+
Inline: ({ props, slots }) => /* @__PURE__ */ jsx2(Primitive, { display: "inline", element: "span", props, children: slots.default })
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// src/remote-view.tsx
|
|
370
|
+
import {
|
|
371
|
+
Component,
|
|
372
|
+
Fragment,
|
|
373
|
+
createElement as createElement2,
|
|
374
|
+
memo,
|
|
375
|
+
useCallback as useCallback2,
|
|
376
|
+
useContext as useContext3,
|
|
377
|
+
useEffect as useEffect3,
|
|
378
|
+
useMemo as useMemo5,
|
|
379
|
+
useRef as useRef2,
|
|
380
|
+
useState,
|
|
381
|
+
useSyncExternalStore as useSyncExternalStore2
|
|
382
|
+
} from "react";
|
|
383
|
+
import { createIframeUiHost } from "@tailorkit/sandbox/host";
|
|
384
|
+
|
|
385
|
+
// src/node-store.ts
|
|
386
|
+
var childIds = (children) => children.map((c) => c.id).join(",");
|
|
387
|
+
var nodeSignature = (node) => {
|
|
388
|
+
if (node.kind === "text") {
|
|
389
|
+
return `text:${node.text}`;
|
|
390
|
+
}
|
|
391
|
+
if (node.kind === "fragment") {
|
|
392
|
+
return `frag:${childIds(node.children)}`;
|
|
393
|
+
}
|
|
394
|
+
const propsStr = JSON.stringify(node.props);
|
|
395
|
+
const callbacksStr = (node.callbacks ?? []).map((binding) => `${binding.callback}:${binding.event}`).join(",");
|
|
396
|
+
return `elem:${node.type}|${childIds(node.children)}|${propsStr}|${callbacksStr}`;
|
|
397
|
+
};
|
|
398
|
+
var flattenTree = (node, out) => {
|
|
399
|
+
out.set(node.id, node);
|
|
400
|
+
if (node.kind === "text") {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
for (const child of node.children) {
|
|
404
|
+
flattenTree(child, out);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
var NodeStore = class {
|
|
408
|
+
nodes = /* @__PURE__ */ new Map();
|
|
409
|
+
signatures = /* @__PURE__ */ new Map();
|
|
410
|
+
nodeListeners = /* @__PURE__ */ new Map();
|
|
411
|
+
rootListeners = /* @__PURE__ */ new Set();
|
|
412
|
+
rootId = null;
|
|
413
|
+
clear() {
|
|
414
|
+
const nodeIds = [...this.nodeListeners.keys()];
|
|
415
|
+
const hadRoot = this.rootId !== null;
|
|
416
|
+
this.nodes = /* @__PURE__ */ new Map();
|
|
417
|
+
this.signatures = /* @__PURE__ */ new Map();
|
|
418
|
+
this.rootId = null;
|
|
419
|
+
for (const id of nodeIds) {
|
|
420
|
+
const subs = this.nodeListeners.get(id);
|
|
421
|
+
if (subs) {
|
|
422
|
+
for (const l of subs) {
|
|
423
|
+
l();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (hadRoot) {
|
|
428
|
+
for (const l of this.rootListeners) {
|
|
429
|
+
l();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
setSnapshot(tree) {
|
|
434
|
+
const next = /* @__PURE__ */ new Map();
|
|
435
|
+
flattenTree(tree, next);
|
|
436
|
+
const changed = /* @__PURE__ */ new Set();
|
|
437
|
+
for (const [id, node] of next) {
|
|
438
|
+
const sig = nodeSignature(node);
|
|
439
|
+
if (this.signatures.get(id) !== sig) {
|
|
440
|
+
changed.add(id);
|
|
441
|
+
this.signatures.set(id, sig);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (const id of this.signatures.keys()) {
|
|
445
|
+
if (!next.has(id)) {
|
|
446
|
+
this.signatures.delete(id);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
this.nodes = next;
|
|
450
|
+
const prevRootId = this.rootId;
|
|
451
|
+
this.rootId = tree.id;
|
|
452
|
+
for (const id of changed) {
|
|
453
|
+
const subs = this.nodeListeners.get(id);
|
|
454
|
+
if (subs) {
|
|
455
|
+
for (const l of subs) {
|
|
456
|
+
l();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (prevRootId !== this.rootId) {
|
|
461
|
+
for (const l of this.rootListeners) {
|
|
462
|
+
l();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
getRootId() {
|
|
467
|
+
return this.rootId;
|
|
468
|
+
}
|
|
469
|
+
getNode(id) {
|
|
470
|
+
return this.nodes.get(id) ?? null;
|
|
471
|
+
}
|
|
472
|
+
subscribe(id, listener) {
|
|
473
|
+
let subs = this.nodeListeners.get(id);
|
|
474
|
+
if (!subs) {
|
|
475
|
+
subs = /* @__PURE__ */ new Set();
|
|
476
|
+
this.nodeListeners.set(id, subs);
|
|
477
|
+
}
|
|
478
|
+
subs.add(listener);
|
|
479
|
+
return () => {
|
|
480
|
+
subs.delete(listener);
|
|
481
|
+
if (subs.size === 0) {
|
|
482
|
+
this.nodeListeners.delete(id);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
subscribeRoot(listener) {
|
|
487
|
+
this.rootListeners.add(listener);
|
|
488
|
+
return () => this.rootListeners.delete(listener);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// src/remote-context.ts
|
|
493
|
+
import { createContext as createContext3 } from "react";
|
|
494
|
+
var RemoteUIContext = createContext3(null);
|
|
495
|
+
|
|
496
|
+
// src/remote-view.tsx
|
|
497
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
498
|
+
var formatError = (error) => error.message;
|
|
499
|
+
var RemoteErrorBoundary = class extends Component {
|
|
500
|
+
state = { error: null };
|
|
501
|
+
static getDerivedStateFromError(error) {
|
|
502
|
+
return { error };
|
|
503
|
+
}
|
|
504
|
+
static componentDidCatch(error) {
|
|
505
|
+
console.error("TailorKit remote app failed", error);
|
|
506
|
+
}
|
|
507
|
+
render() {
|
|
508
|
+
if (this.state.error) {
|
|
509
|
+
return createElement2("div", null, formatError(this.state.error));
|
|
510
|
+
}
|
|
511
|
+
return this.props.children;
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
function RemoteViewHost({
|
|
515
|
+
appUrl,
|
|
516
|
+
components: components2,
|
|
517
|
+
createIframe,
|
|
518
|
+
props,
|
|
519
|
+
runtimeUrl
|
|
520
|
+
}) {
|
|
521
|
+
const appKey = appUrl.toString();
|
|
522
|
+
const storeRef = useRef2(null);
|
|
523
|
+
if (storeRef.current === null) {
|
|
524
|
+
storeRef.current = new NodeStore();
|
|
525
|
+
}
|
|
526
|
+
const store = storeRef.current;
|
|
527
|
+
const appKeyRef = useRef2(appKey);
|
|
528
|
+
if (appKeyRef.current !== appKey) {
|
|
529
|
+
appKeyRef.current = appKey;
|
|
530
|
+
store.clear();
|
|
531
|
+
}
|
|
532
|
+
const dispatchRef = useRef2(null);
|
|
533
|
+
const [error, setError] = useState(null);
|
|
534
|
+
const [status, setStatus] = useState("starting");
|
|
535
|
+
const ctxRef = useRef2({
|
|
536
|
+
components: components2,
|
|
537
|
+
dispatch: (payload) => dispatchRef.current?.(payload),
|
|
538
|
+
store
|
|
539
|
+
});
|
|
540
|
+
useEffect3(() => {
|
|
541
|
+
ctxRef.current.components = components2;
|
|
542
|
+
}, [components2]);
|
|
543
|
+
const ui = useMemo5(() => {
|
|
544
|
+
const ctx = ctxRef.current;
|
|
545
|
+
return createElement2(
|
|
546
|
+
RemoteUIContext.Provider,
|
|
547
|
+
{ value: ctx },
|
|
548
|
+
createElement2(
|
|
549
|
+
RemoteErrorBoundary,
|
|
550
|
+
{ key: appKey },
|
|
551
|
+
createElement2(RemoteRoot, { store: ctx.store })
|
|
552
|
+
)
|
|
553
|
+
);
|
|
554
|
+
}, [appKey]);
|
|
555
|
+
useEffect3(() => {
|
|
556
|
+
const host = createIframeUiHost(appUrl, {
|
|
557
|
+
createIframe,
|
|
558
|
+
onError: (error2) => {
|
|
559
|
+
console.error("TailorKit remote app failed", error2);
|
|
560
|
+
setError(error2);
|
|
561
|
+
setStatus("error");
|
|
562
|
+
},
|
|
563
|
+
props,
|
|
564
|
+
runtimeUrl
|
|
565
|
+
});
|
|
566
|
+
dispatchRef.current = (payload) => host.dispatch(payload);
|
|
567
|
+
const unsubscribe = host.subscribe(() => {
|
|
568
|
+
const tree = host.getSnapshot();
|
|
569
|
+
if (tree !== null) {
|
|
570
|
+
store.setSnapshot(tree);
|
|
571
|
+
setStatus("ready");
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
setStatus("starting");
|
|
575
|
+
setError(null);
|
|
576
|
+
host.mount();
|
|
577
|
+
return () => {
|
|
578
|
+
unsubscribe();
|
|
579
|
+
dispatchRef.current = null;
|
|
580
|
+
host.destroy();
|
|
581
|
+
};
|
|
582
|
+
}, [appUrl, createIframe, props, runtimeUrl, store]);
|
|
583
|
+
if (status === "error" && error) {
|
|
584
|
+
return createElement2("div", null, formatError(error));
|
|
585
|
+
}
|
|
586
|
+
return ui;
|
|
587
|
+
}
|
|
588
|
+
function RemoteRoot({ store }) {
|
|
589
|
+
const subscribe = useCallback2((listener) => store.subscribeRoot(listener), [store]);
|
|
590
|
+
const getSnapshot = useCallback2(() => store.getRootId(), [store]);
|
|
591
|
+
const rootId = useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
|
|
592
|
+
if (rootId === null) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
return /* @__PURE__ */ jsx3(RemoteView, { nodeId: rootId });
|
|
596
|
+
}
|
|
597
|
+
var RemoteElementView = memo(({ ctx, node }) => {
|
|
598
|
+
const { dispatch, components: components2 } = ctx;
|
|
599
|
+
const children = node.children.map((child) => /* @__PURE__ */ jsx3(RemoteView, { nodeId: child.id }, child.id));
|
|
600
|
+
const component = components2[node.type];
|
|
601
|
+
if (component === void 0) {
|
|
602
|
+
throw new Error(`TailorKit component "${node.type}" is not registered.`);
|
|
603
|
+
}
|
|
604
|
+
const props = { ...node.props };
|
|
605
|
+
for (const binding of node.callbacks ?? []) {
|
|
606
|
+
props[binding.callback] = (...args) => {
|
|
607
|
+
dispatch({
|
|
608
|
+
data: {
|
|
609
|
+
args: args.slice(0, binding.inputCount),
|
|
610
|
+
event: binding.event,
|
|
611
|
+
nodeId: node.id
|
|
612
|
+
},
|
|
613
|
+
type: "dispatchCallback"
|
|
614
|
+
});
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
return createElement2(component, props, ...children);
|
|
618
|
+
});
|
|
619
|
+
var RemoteView = memo(function RemoteView2({ nodeId }) {
|
|
620
|
+
const ctx = useContext3(RemoteUIContext);
|
|
621
|
+
if (ctx === null) {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
const { store } = ctx;
|
|
625
|
+
const subscribe = useCallback2(
|
|
626
|
+
(listener) => store.subscribe(nodeId, listener),
|
|
627
|
+
[store, nodeId]
|
|
628
|
+
);
|
|
629
|
+
const getSnapshot = useCallback2(() => store.getNode(nodeId), [store, nodeId]);
|
|
630
|
+
const node = useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
|
|
631
|
+
if (node === null) {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
if (node.kind === "text") {
|
|
635
|
+
return node.text;
|
|
636
|
+
}
|
|
637
|
+
const childElements = node.children.map((child) => /* @__PURE__ */ jsx3(RemoteView2, { nodeId: child.id }, child.id));
|
|
638
|
+
if (node.kind === "fragment") {
|
|
639
|
+
return createElement2(Fragment, { key: node.id }, ...childElements);
|
|
640
|
+
}
|
|
641
|
+
return /* @__PURE__ */ jsx3(RemoteElementView, { node, ctx });
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// src/tailor-kit.tsx
|
|
645
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
646
|
+
var componentTagPrefix = "tailorkit-";
|
|
647
|
+
var toComponentTagName = (name) => `${componentTagPrefix}${name.replaceAll(/([a-z0-9])([A-Z])/gu, "$1-$2").replaceAll(/[\s_]+/gu, "-").toLowerCase()}`;
|
|
648
|
+
function components(_schema, customComponents) {
|
|
649
|
+
return customComponents;
|
|
650
|
+
}
|
|
651
|
+
function createTailorKitClient(options) {
|
|
652
|
+
return createReactTailorKitClient(options);
|
|
653
|
+
}
|
|
654
|
+
function createReactTailorKitClient(options) {
|
|
655
|
+
const wrappedComponents = {};
|
|
656
|
+
const store = createTailorKitStore(options.baseUrl);
|
|
657
|
+
const theme = options.theme ?? {};
|
|
658
|
+
for (const [name, renderer] of Object.entries(options.components ?? {})) {
|
|
659
|
+
if (renderer) {
|
|
660
|
+
const TailorKitComponent = function TailorKitComponent2({
|
|
661
|
+
children,
|
|
662
|
+
...props
|
|
663
|
+
}) {
|
|
664
|
+
return renderer({
|
|
665
|
+
props,
|
|
666
|
+
slots: { default: children }
|
|
667
|
+
});
|
|
668
|
+
};
|
|
669
|
+
wrappedComponents[name] = TailorKitComponent;
|
|
670
|
+
wrappedComponents[toComponentTagName(name)] = TailorKitComponent;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const useApps = createUseApps(store);
|
|
674
|
+
const useCurrentScreen = createUseCurrentScreen(store);
|
|
675
|
+
const AppView = ({
|
|
676
|
+
app,
|
|
677
|
+
createIframe,
|
|
678
|
+
fallback = null,
|
|
679
|
+
runtimeUrl,
|
|
680
|
+
...screenProps
|
|
681
|
+
}) => {
|
|
682
|
+
const reactId = useId2();
|
|
683
|
+
const currentScreen = useSyncExternalStore3(
|
|
684
|
+
store.subscribe,
|
|
685
|
+
store.getCurrentScreen,
|
|
686
|
+
store.getCurrentScreen
|
|
687
|
+
);
|
|
688
|
+
const screen = screenProps.screen;
|
|
689
|
+
const context = screenProps.context;
|
|
690
|
+
const status = screenProps.status ?? "ready";
|
|
691
|
+
const props = useMemo6(() => {
|
|
692
|
+
if (screen !== void 0) {
|
|
693
|
+
return {
|
|
694
|
+
screen: {
|
|
695
|
+
context,
|
|
696
|
+
path: screen,
|
|
697
|
+
status
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
return currentScreen === null ? void 0 : {
|
|
702
|
+
screen: {
|
|
703
|
+
context: currentScreen.context,
|
|
704
|
+
path: currentScreen.screen,
|
|
705
|
+
status: currentScreen.status
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
}, [context, currentScreen, screen, status]);
|
|
709
|
+
const meta = useSyncExternalStore3(
|
|
710
|
+
store.subscribe,
|
|
711
|
+
store.getMetaSnapshot,
|
|
712
|
+
store.getMetaSnapshot
|
|
713
|
+
);
|
|
714
|
+
const assetsBaseUrl = app.clientPath ? null : meta.assetsBaseUrl;
|
|
715
|
+
const appUrl = useMemo6(
|
|
716
|
+
() => resolveAppUrl(app, store.baseUrl, assetsBaseUrl),
|
|
717
|
+
[app, assetsBaseUrl]
|
|
718
|
+
);
|
|
719
|
+
useEffect4(() => {
|
|
720
|
+
void store.fetchMeta();
|
|
721
|
+
}, []);
|
|
722
|
+
if (props === void 0 || appUrl === null) {
|
|
723
|
+
return fallback;
|
|
724
|
+
}
|
|
725
|
+
const screenId = `tailorkit-screen-${reactId.replaceAll(":", "")}`;
|
|
726
|
+
return /* @__PURE__ */ jsx4(PrimitiveThemeContext.Provider, { value: { screenId, theme }, children: /* @__PURE__ */ jsxs2("div", { "data-tailorkit-screen": screenId, children: [
|
|
727
|
+
/* @__PURE__ */ jsx4("style", { "data-tailorkit-theme-style": screenId, children: buildThemeCss(screenId, theme) }),
|
|
728
|
+
/* @__PURE__ */ jsx4(
|
|
729
|
+
RemoteViewHost,
|
|
730
|
+
{
|
|
731
|
+
appUrl,
|
|
732
|
+
components: wrappedComponents,
|
|
733
|
+
createIframe,
|
|
734
|
+
props,
|
|
735
|
+
runtimeUrl
|
|
736
|
+
}
|
|
737
|
+
)
|
|
738
|
+
] }) });
|
|
739
|
+
};
|
|
740
|
+
const client = {
|
|
741
|
+
AppView,
|
|
742
|
+
Root: (props) => /* @__PURE__ */ jsx4(Root, { ...props, tailor: client }),
|
|
743
|
+
getApp: store.getApp,
|
|
744
|
+
getApps: store.getApps,
|
|
745
|
+
useApps,
|
|
746
|
+
useCurrentScreen
|
|
747
|
+
};
|
|
748
|
+
return {
|
|
749
|
+
...client
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function createTailorKitStore(baseUrlInput) {
|
|
753
|
+
const baseUrl = toBaseUrl(baseUrlInput);
|
|
754
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
755
|
+
const screens = /* @__PURE__ */ new Map();
|
|
756
|
+
let appsSnapshot = {
|
|
757
|
+
apps: [],
|
|
758
|
+
error: null,
|
|
759
|
+
status: "idle"
|
|
760
|
+
};
|
|
761
|
+
let metaSnapshot = {
|
|
762
|
+
assetsBaseUrl: null,
|
|
763
|
+
error: null,
|
|
764
|
+
schema: null,
|
|
765
|
+
status: "idle"
|
|
766
|
+
};
|
|
767
|
+
let currentScreen = null;
|
|
768
|
+
let fetchAppsPromise = null;
|
|
769
|
+
let fetchMetaPromise = null;
|
|
770
|
+
let fetchAppsRequestId = 0;
|
|
771
|
+
let nextOrder = 0;
|
|
772
|
+
const emit = () => {
|
|
773
|
+
for (const listener of listeners) {
|
|
774
|
+
listener();
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
const selectCurrentScreen = () => {
|
|
778
|
+
let selected = null;
|
|
779
|
+
for (const screen of screens.values()) {
|
|
780
|
+
const depth = getScreenDepth(screen.screen);
|
|
781
|
+
const selectedDepth = selected === null ? -1 : getScreenDepth(selected.screen);
|
|
782
|
+
if (selected === null || depth > selectedDepth) {
|
|
783
|
+
selected = screen;
|
|
784
|
+
}
|
|
785
|
+
if (selected !== null && depth === selectedDepth && screen.order > selected.order) {
|
|
786
|
+
selected = screen;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
const deepestScreens = selected === null ? [] : [...screens.values()].filter(
|
|
790
|
+
(screen) => getScreenDepth(screen.screen) === getScreenDepth(selected.screen)
|
|
791
|
+
);
|
|
792
|
+
if (typeof console !== "undefined" && selected !== null && deepestScreens.length > 1) {
|
|
793
|
+
const screenNames = deepestScreens.map((screen) => `"${screen.screen}"`).join(", ");
|
|
794
|
+
console.warn(
|
|
795
|
+
`TailorKit found multiple active screens at the same hierarchy depth: ${screenNames}. TailorKit selected "${selected.screen}" by mount order. Only one route at a hierarchy depth should call useCurrentScreen.`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
currentScreen = selected;
|
|
799
|
+
};
|
|
800
|
+
return {
|
|
801
|
+
baseUrl,
|
|
802
|
+
fetchApps: (options = {}) => {
|
|
803
|
+
if (fetchAppsPromise && !options.force) {
|
|
804
|
+
return fetchAppsPromise;
|
|
805
|
+
}
|
|
806
|
+
appsSnapshot = { ...appsSnapshot, status: "loading" };
|
|
807
|
+
emit();
|
|
808
|
+
const requestId = ++fetchAppsRequestId;
|
|
809
|
+
fetchAppsPromise = fetch(new URL("apps", baseUrl)).then(async (response) => {
|
|
810
|
+
if (!response.ok) {
|
|
811
|
+
throw new Error(`Unable to fetch TailorKit apps from ${baseUrl.toString()}.`);
|
|
812
|
+
}
|
|
813
|
+
const apps = await response.json();
|
|
814
|
+
if (requestId !== fetchAppsRequestId) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
appsSnapshot = {
|
|
818
|
+
apps,
|
|
819
|
+
error: null,
|
|
820
|
+
status: "ready"
|
|
821
|
+
};
|
|
822
|
+
emit();
|
|
823
|
+
}).catch((error) => {
|
|
824
|
+
if (requestId !== fetchAppsRequestId) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
appsSnapshot = {
|
|
828
|
+
...appsSnapshot,
|
|
829
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
830
|
+
status: "error"
|
|
831
|
+
};
|
|
832
|
+
emit();
|
|
833
|
+
});
|
|
834
|
+
return fetchAppsPromise;
|
|
835
|
+
},
|
|
836
|
+
fetchMeta: () => {
|
|
837
|
+
if (fetchMetaPromise) {
|
|
838
|
+
return fetchMetaPromise;
|
|
839
|
+
}
|
|
840
|
+
metaSnapshot = { ...metaSnapshot, status: "loading" };
|
|
841
|
+
emit();
|
|
842
|
+
fetchMetaPromise = fetch(new URL("meta", baseUrl)).then(async (response) => {
|
|
843
|
+
if (!response.ok) {
|
|
844
|
+
throw new Error(`Unable to fetch TailorKit metadata from ${baseUrl.toString()}.`);
|
|
845
|
+
}
|
|
846
|
+
const meta = await response.json();
|
|
847
|
+
metaSnapshot = {
|
|
848
|
+
assetsBaseUrl: meta.assetsBaseUrl ?? null,
|
|
849
|
+
error: null,
|
|
850
|
+
schema: meta.schema,
|
|
851
|
+
status: "ready"
|
|
852
|
+
};
|
|
853
|
+
emit();
|
|
854
|
+
}).catch((error) => {
|
|
855
|
+
metaSnapshot = {
|
|
856
|
+
...metaSnapshot,
|
|
857
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
858
|
+
status: "error"
|
|
859
|
+
};
|
|
860
|
+
emit();
|
|
861
|
+
});
|
|
862
|
+
return fetchMetaPromise;
|
|
863
|
+
},
|
|
864
|
+
getApp: (id) => appsSnapshot.apps.find((app) => app.id === id),
|
|
865
|
+
getApps: () => appsSnapshot.apps,
|
|
866
|
+
getAppsSnapshot: () => appsSnapshot,
|
|
867
|
+
getCurrentScreen: () => currentScreen,
|
|
868
|
+
getMetaSnapshot: () => metaSnapshot,
|
|
869
|
+
registerScreen: (entry) => {
|
|
870
|
+
const existing = screens.get(entry.id);
|
|
871
|
+
screens.set(entry.id, {
|
|
872
|
+
...entry,
|
|
873
|
+
order: existing?.order ?? nextOrder
|
|
874
|
+
});
|
|
875
|
+
if (!existing) {
|
|
876
|
+
nextOrder += 1;
|
|
877
|
+
}
|
|
878
|
+
selectCurrentScreen();
|
|
879
|
+
emit();
|
|
880
|
+
},
|
|
881
|
+
subscribe: (listener) => {
|
|
882
|
+
listeners.add(listener);
|
|
883
|
+
return () => {
|
|
884
|
+
listeners.delete(listener);
|
|
885
|
+
};
|
|
886
|
+
},
|
|
887
|
+
unregisterScreen: (id) => {
|
|
888
|
+
screens.delete(id);
|
|
889
|
+
selectCurrentScreen();
|
|
890
|
+
emit();
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
function getScreenDepth(screen) {
|
|
895
|
+
return screen === "/" ? 0 : screen.split("/").filter(Boolean).length;
|
|
896
|
+
}
|
|
897
|
+
function resolveAppUrl(app, baseUrl, assetsBaseUrl) {
|
|
898
|
+
if (app.clientPath) {
|
|
899
|
+
return new URL(app.clientPath, baseUrl);
|
|
900
|
+
}
|
|
901
|
+
if (!assetsBaseUrl || !app.projectId || !app.currentDeployment?.id) {
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
return new URL(
|
|
905
|
+
`projects/${app.projectId}/apps/${app.id}/deployments/${app.currentDeployment.id}/files/client.js`,
|
|
906
|
+
toBaseUrl(assetsBaseUrl)
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
function toBaseUrl(value) {
|
|
910
|
+
const url = value instanceof URL ? new URL(value) : new URL(value);
|
|
911
|
+
if (!url.pathname.endsWith("/")) {
|
|
912
|
+
url.pathname = `${url.pathname}/`;
|
|
913
|
+
}
|
|
914
|
+
return url;
|
|
915
|
+
}
|
|
916
|
+
export {
|
|
917
|
+
components,
|
|
918
|
+
createTailorKitClient,
|
|
919
|
+
primitives
|
|
920
|
+
};
|