@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/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
+ };