@tailorkit/react 0.1.0-beta.1 → 0.1.0-beta.11

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 CHANGED
@@ -1,80 +1,209 @@
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";
1
+ // src/components/app-view.tsx
2
+ import { useEffect as useEffect2, useId as useId2, useMemo as useMemo3, useSyncExternalStore as useSyncExternalStore2 } from "react";
3
+ import { isViewAncestor as isViewAncestor2 } from "@tailorkit/core/views";
6
4
 
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.");
5
+ // src/view-registry.ts
6
+ import { getViewDepth, isViewAncestor } from "@tailorkit/core/views";
7
+ function createViewRegistry() {
8
+ const entries = /* @__PURE__ */ new Map();
9
+ const listeners = /* @__PURE__ */ new Set();
10
+ let snapshot = null;
11
+ let nextOrder = 0;
12
+ let scheduled = false;
13
+ const publish = () => {
14
+ if (scheduled) {
15
+ return;
16
+ }
17
+ scheduled = true;
18
+ queueMicrotask(() => {
19
+ scheduled = false;
20
+ const ordered = [...entries.values()].toSorted(
21
+ (a, b) => getViewDepth(b.view) - getViewDepth(a.view) || b.order - a.order
22
+ );
23
+ const selected = ordered[0];
24
+ if (selected) {
25
+ const peers = ordered.filter(
26
+ (entry) => getViewDepth(entry.view) === getViewDepth(selected.view)
27
+ );
28
+ if (peers.length > 1) {
29
+ console.warn(
30
+ `TailorKit found multiple active views at the same hierarchy depth: ${peers.toReversed().map((entry) => `"${entry.view}"`).join(
31
+ ", "
32
+ )}. TailorKit selected "${selected.view}" by mount order. Only one route at a hierarchy depth should call useView.`
33
+ );
34
+ }
35
+ snapshot = {
36
+ view: selected.view,
37
+ layers: ordered.filter((entry) => isViewAncestor(entry.view, selected.view)).toReversed().map((entry) => ({ path: entry.view, context: entry.context, status: entry.status }))
38
+ };
39
+ } else {
40
+ snapshot = null;
18
41
  }
19
- return cloneElement(render, mergeProps(render.props, props));
42
+ for (const listener of listeners) {
43
+ listener();
44
+ }
45
+ });
46
+ };
47
+ return {
48
+ getSnapshot: () => snapshot,
49
+ subscribe(listener) {
50
+ listeners.add(listener);
51
+ return () => {
52
+ listeners.delete(listener);
53
+ };
54
+ },
55
+ register(entry) {
56
+ entries.set(entry.id, { ...entry, order: entries.get(entry.id)?.order ?? nextOrder++ });
57
+ publish();
58
+ },
59
+ unregister(id) {
60
+ entries.delete(id);
61
+ publish();
20
62
  }
21
- return createElement(defaultTagName, props);
22
- }, [defaultTagName, props, render]);
63
+ };
23
64
  }
24
- function mergeProps(...propSets) {
25
- const merged = {};
26
- for (const props of propSets) {
27
- if (!props) {
28
- continue;
65
+
66
+ // src/store.ts
67
+ function createTailorKitStore(baseUrlInput, initialApps, previewSessionId) {
68
+ const baseUrl = toBaseUrl(baseUrlInput);
69
+ const listeners = /* @__PURE__ */ new Set();
70
+ let providedApps = initialApps;
71
+ let appsSnapshot = {
72
+ apps: initialApps ?? [],
73
+ error: null,
74
+ status: initialApps === void 0 ? "idle" : "ready"
75
+ };
76
+ let metaSnapshot = {
77
+ assetsBaseUrl: null,
78
+ error: null,
79
+ schema: null,
80
+ status: "idle"
81
+ };
82
+ let fetchAppsPromise = null;
83
+ let fetchMetaPromise = null;
84
+ let fetchAppsRequestId = 0;
85
+ const emit = () => {
86
+ for (const listener of listeners) {
87
+ listener();
29
88
  }
30
- for (const [key, value] of Object.entries(props)) {
31
- if (value === void 0) {
32
- continue;
89
+ };
90
+ const store = {
91
+ baseUrl,
92
+ views: createViewRegistry(),
93
+ setProvidedApps(apps) {
94
+ if (providedApps === apps) {
95
+ return;
33
96
  }
34
- const existing = merged[key];
35
- if (key === "className" && existing && value) {
36
- merged[key] = `${existing} ${value}`;
37
- continue;
97
+ providedApps = apps;
98
+ fetchAppsRequestId += 1;
99
+ fetchAppsPromise = null;
100
+ appsSnapshot = {
101
+ apps: apps ?? [],
102
+ error: null,
103
+ status: apps === void 0 ? "idle" : "ready"
104
+ };
105
+ emit();
106
+ if (apps === void 0) {
107
+ void store.fetchApps();
38
108
  }
39
- if (key === "style" && existing && value) {
40
- merged[key] = {
41
- ...existing,
42
- ...value
43
- };
44
- continue;
109
+ },
110
+ fetchApps: (options = {}) => {
111
+ if (providedApps !== void 0) {
112
+ return Promise.resolve();
45
113
  }
46
- if (key === "ref" && existing && value) {
47
- merged[key] = composeRefs(existing, value);
48
- continue;
114
+ if (fetchAppsPromise && !options.force) {
115
+ return fetchAppsPromise;
49
116
  }
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;
117
+ appsSnapshot = { ...appsSnapshot, status: "loading" };
118
+ emit();
119
+ const requestId = ++fetchAppsRequestId;
120
+ const appsUrl = new URL("apps", baseUrl);
121
+ if (previewSessionId) {
122
+ appsUrl.searchParams.set("previewSessionId", previewSessionId);
59
123
  }
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;
124
+ fetchAppsPromise = fetch(appsUrl).then(async (response) => {
125
+ if (!response.ok) {
126
+ throw new Error(`Unable to fetch TailorKit apps from ${baseUrl.toString()}.`);
127
+ }
128
+ const apps = await response.json();
129
+ if (requestId !== fetchAppsRequestId) {
130
+ return;
131
+ }
132
+ appsSnapshot = {
133
+ apps,
134
+ error: null,
135
+ status: "ready"
136
+ };
137
+ emit();
138
+ }).catch((error) => {
139
+ if (requestId !== fetchAppsRequestId) {
140
+ return;
141
+ }
142
+ appsSnapshot = {
143
+ ...appsSnapshot,
144
+ error: error instanceof Error ? error : new Error(String(error)),
145
+ status: "error"
146
+ };
147
+ emit();
148
+ });
149
+ return fetchAppsPromise;
150
+ },
151
+ fetchMeta: () => {
152
+ if (fetchMetaPromise) {
153
+ return fetchMetaPromise;
72
154
  }
155
+ metaSnapshot = { ...metaSnapshot, status: "loading" };
156
+ emit();
157
+ fetchMetaPromise = fetch(new URL("meta", baseUrl)).then(async (response) => {
158
+ if (!response.ok) {
159
+ throw new Error(`Unable to fetch TailorKit metadata from ${baseUrl.toString()}.`);
160
+ }
161
+ const meta = await response.json();
162
+ metaSnapshot = {
163
+ assetsBaseUrl: meta.assetsBaseUrl ?? null,
164
+ error: null,
165
+ schema: meta.schema,
166
+ status: "ready"
167
+ };
168
+ emit();
169
+ }).catch((error) => {
170
+ metaSnapshot = {
171
+ ...metaSnapshot,
172
+ error: error instanceof Error ? error : new Error(String(error)),
173
+ status: "error"
174
+ };
175
+ emit();
176
+ });
177
+ return fetchMetaPromise;
178
+ },
179
+ getAppsSnapshot: () => appsSnapshot,
180
+ getMetaSnapshot: () => metaSnapshot,
181
+ subscribe: (listener) => {
182
+ listeners.add(listener);
183
+ return () => {
184
+ listeners.delete(listener);
185
+ };
73
186
  }
74
187
  };
188
+ return store;
75
189
  }
76
- function isEventHandler(key) {
77
- return /^on[A-Z]/u.test(key);
190
+ function toBaseUrl(value) {
191
+ const url = value instanceof URL ? new URL(value) : new URL(value);
192
+ if (!url.pathname.endsWith("/")) {
193
+ url.pathname = `${url.pathname}/`;
194
+ }
195
+ return url;
196
+ }
197
+
198
+ // src/hooks/use-stable-context.ts
199
+ import { useRef } from "react";
200
+ function useStableContext(value) {
201
+ const key = JSON.stringify(value);
202
+ const ref = useRef({ key, value });
203
+ if (ref.current.key !== key) {
204
+ ref.current = { key, value };
205
+ }
206
+ return ref.current.value;
78
207
  }
79
208
 
80
209
  // src/components/context.ts
@@ -83,95 +212,15 @@ var TailorRootContext = createContext(null);
83
212
  function useTailorRootContext(component) {
84
213
  const context = useContext(TailorRootContext);
85
214
  if (!context) {
86
- throw new Error(`${component} must be rendered inside tailor.Root.`);
215
+ throw new Error(`${component} must be rendered inside Root.`);
87
216
  }
88
217
  return context;
89
218
  }
90
219
 
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
220
  // src/primitives.tsx
172
- import { createContext as createContext2, useContext as useContext2, useId, useMemo as useMemo4 } from "react";
221
+ import { createContext as createContext2, useContext as useContext2, useId, useMemo } from "react";
173
222
  import { resolveTheme } from "@tailorkit/core/primitives/theme";
174
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
223
+ import { jsx, jsxs } from "react/jsx-runtime";
175
224
  var PrimitiveThemeContext = createContext2(null);
176
225
  var tokenGroups = [
177
226
  "space",
@@ -292,10 +341,10 @@ var buildPrimitiveCss = ({
292
341
  display,
293
342
  nodeId,
294
343
  props,
295
- screenId,
344
+ viewId,
296
345
  theme
297
346
  }) => {
298
- const selector = `[data-tailorkit-screen="${cssEscape(screenId)}"] [data-tailorkit-node="${cssEscape(
347
+ const selector = `[data-tailorkit-view="${cssEscape(viewId)}"] [data-tailorkit-node="${cssEscape(
299
348
  nodeId
300
349
  )}"]`;
301
350
  const base = [`display: ${display};`];
@@ -323,8 +372,8 @@ var buildPrimitiveCss = ({
323
372
  }
324
373
  return rules.join("\n");
325
374
  };
326
- var buildThemeCss = (screenId, theme) => {
327
- const selector = `[data-tailorkit-screen="${cssEscape(screenId)}"]`;
375
+ var buildThemeCss = (viewId, theme) => {
376
+ const selector = `[data-tailorkit-view="${cssEscape(viewId)}"]`;
328
377
  const declarations = [];
329
378
  const resolvedTheme = resolveTheme(theme);
330
379
  for (const group of tokenGroups) {
@@ -336,7 +385,7 @@ var buildThemeCss = (screenId, theme) => {
336
385
  };
337
386
  var usePrimitiveNodeId = () => {
338
387
  const id = useId();
339
- return useMemo4(() => `tailorkit-${id.replaceAll(":", "")}`, [id]);
388
+ return useMemo(() => `tailorkit-${id.replaceAll(":", "")}`, [id]);
340
389
  };
341
390
  var Primitive = ({
342
391
  children,
@@ -351,34 +400,34 @@ var Primitive = ({
351
400
  display,
352
401
  nodeId,
353
402
  props,
354
- screenId: context.screenId,
403
+ viewId: context.viewId,
355
404
  theme: context.theme
356
405
  }) : "";
357
406
  return /* @__PURE__ */ jsxs(Element, { "data-tailorkit-node": nodeId, children: [
358
- css === "" ? null : /* @__PURE__ */ jsx2("style", { "data-tailorkit-node-style": nodeId, children: css }),
407
+ css === "" ? null : /* @__PURE__ */ jsx("style", { "data-tailorkit-node-style": nodeId, children: css }),
359
408
  children
360
409
  ] });
361
410
  };
362
411
  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 })
412
+ Box: ({ props, children }) => /* @__PURE__ */ jsx(Primitive, { display: "block", element: "div", props, children }),
413
+ Flex: ({ props, children }) => /* @__PURE__ */ jsx(Primitive, { display: "flex", element: "div", props, children }),
414
+ Grid: ({ props, children }) => /* @__PURE__ */ jsx(Primitive, { display: "grid", element: "div", props, children }),
415
+ Inline: ({ props, children }) => /* @__PURE__ */ jsx(Primitive, { display: "inline", element: "span", props, children })
367
416
  };
368
417
 
369
418
  // src/remote-view.tsx
370
419
  import {
371
420
  Component,
372
421
  Fragment,
373
- createElement as createElement2,
422
+ createElement,
374
423
  memo,
375
- useCallback as useCallback2,
424
+ useCallback,
376
425
  useContext as useContext3,
377
- useEffect as useEffect3,
378
- useMemo as useMemo5,
426
+ useEffect,
427
+ useMemo as useMemo2,
379
428
  useRef as useRef2,
380
429
  useState,
381
- useSyncExternalStore as useSyncExternalStore2
430
+ useSyncExternalStore
382
431
  } from "react";
383
432
  import { createIframeUiHost } from "@tailorkit/sandbox/host";
384
433
 
@@ -494,7 +543,7 @@ import { createContext as createContext3 } from "react";
494
543
  var RemoteUIContext = createContext3(null);
495
544
 
496
545
  // src/remote-view.tsx
497
- import { jsx as jsx3 } from "react/jsx-runtime";
546
+ import { jsx as jsx2 } from "react/jsx-runtime";
498
547
  var formatError = (error) => error.message;
499
548
  var RemoteErrorBoundary = class extends Component {
500
549
  state = { error: null };
@@ -506,7 +555,7 @@ var RemoteErrorBoundary = class extends Component {
506
555
  }
507
556
  render() {
508
557
  if (this.state.error) {
509
- return createElement2("div", null, formatError(this.state.error));
558
+ return createElement("div", null, formatError(this.state.error));
510
559
  }
511
560
  return this.props.children;
512
561
  }
@@ -515,8 +564,7 @@ function RemoteViewHost({
515
564
  appUrl,
516
565
  components: components2,
517
566
  createIframe,
518
- props,
519
- runtimeUrl
567
+ props
520
568
  }) {
521
569
  const appKey = appUrl.toString();
522
570
  const storeRef = useRef2(null);
@@ -529,6 +577,12 @@ function RemoteViewHost({
529
577
  appKeyRef.current = appKey;
530
578
  store.clear();
531
579
  }
580
+ const hostRef = useRef2(null);
581
+ const propsRef = useRef2(props);
582
+ useEffect(() => {
583
+ propsRef.current = props;
584
+ hostRef.current?.setProps(props);
585
+ }, [props]);
532
586
  const dispatchRef = useRef2(null);
533
587
  const [error, setError] = useState(null);
534
588
  const [status, setStatus] = useState("starting");
@@ -537,22 +591,22 @@ function RemoteViewHost({
537
591
  dispatch: (payload) => dispatchRef.current?.(payload),
538
592
  store
539
593
  });
540
- useEffect3(() => {
594
+ useEffect(() => {
541
595
  ctxRef.current.components = components2;
542
596
  }, [components2]);
543
- const ui = useMemo5(() => {
597
+ const ui = useMemo2(() => {
544
598
  const ctx = ctxRef.current;
545
- return createElement2(
599
+ return createElement(
546
600
  RemoteUIContext.Provider,
547
601
  { value: ctx },
548
- createElement2(
602
+ createElement(
549
603
  RemoteErrorBoundary,
550
604
  { key: appKey },
551
- createElement2(RemoteRoot, { store: ctx.store })
605
+ createElement(RemoteRoot, { store: ctx.store })
552
606
  )
553
607
  );
554
608
  }, [appKey]);
555
- useEffect3(() => {
609
+ useEffect(() => {
556
610
  const host = createIframeUiHost(appUrl, {
557
611
  createIframe,
558
612
  onError: (error2) => {
@@ -560,9 +614,9 @@ function RemoteViewHost({
560
614
  setError(error2);
561
615
  setStatus("error");
562
616
  },
563
- props,
564
- runtimeUrl
617
+ props: propsRef.current
565
618
  });
619
+ hostRef.current = host;
566
620
  dispatchRef.current = (payload) => host.dispatch(payload);
567
621
  const unsubscribe = host.subscribe(() => {
568
622
  const tree = host.getSnapshot();
@@ -577,26 +631,27 @@ function RemoteViewHost({
577
631
  return () => {
578
632
  unsubscribe();
579
633
  dispatchRef.current = null;
634
+ hostRef.current = null;
580
635
  host.destroy();
581
636
  };
582
- }, [appUrl, createIframe, props, runtimeUrl, store]);
637
+ }, [appUrl, createIframe, store]);
583
638
  if (status === "error" && error) {
584
- return createElement2("div", null, formatError(error));
639
+ return createElement("div", null, formatError(error));
585
640
  }
586
641
  return ui;
587
642
  }
588
643
  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);
644
+ const subscribe = useCallback((listener) => store.subscribeRoot(listener), [store]);
645
+ const getSnapshot = useCallback(() => store.getRootId(), [store]);
646
+ const rootId = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
592
647
  if (rootId === null) {
593
648
  return null;
594
649
  }
595
- return /* @__PURE__ */ jsx3(RemoteView, { nodeId: rootId });
650
+ return /* @__PURE__ */ jsx2(RemoteView, { nodeId: rootId });
596
651
  }
597
652
  var RemoteElementView = memo(({ ctx, node }) => {
598
653
  const { dispatch, components: components2 } = ctx;
599
- const children = node.children.map((child) => /* @__PURE__ */ jsx3(RemoteView, { nodeId: child.id }, child.id));
654
+ const children = node.children.map((child) => /* @__PURE__ */ jsx2(RemoteView, { nodeId: child.id }, child.id));
600
655
  const component = components2[node.type];
601
656
  if (component === void 0) {
602
657
  throw new Error(`TailorKit component "${node.type}" is not registered.`);
@@ -614,35 +669,118 @@ var RemoteElementView = memo(({ ctx, node }) => {
614
669
  });
615
670
  };
616
671
  }
617
- return createElement2(component, props, ...children);
672
+ return createElement(component, props, ...children);
618
673
  });
619
- var RemoteView = memo(function RemoteView2({ nodeId }) {
674
+ function RemoteViewComponent({ nodeId }) {
620
675
  const ctx = useContext3(RemoteUIContext);
621
676
  if (ctx === null) {
622
677
  return null;
623
678
  }
624
679
  const { store } = ctx;
625
- const subscribe = useCallback2(
680
+ const subscribe = useCallback(
626
681
  (listener) => store.subscribe(nodeId, listener),
627
682
  [store, nodeId]
628
683
  );
629
- const getSnapshot = useCallback2(() => store.getNode(nodeId), [store, nodeId]);
630
- const node = useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
684
+ const getSnapshot = useCallback(() => store.getNode(nodeId), [store, nodeId]);
685
+ const node = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
631
686
  if (node === null) {
632
687
  return null;
633
688
  }
634
689
  if (node.kind === "text") {
635
690
  return node.text;
636
691
  }
637
- const childElements = node.children.map((child) => /* @__PURE__ */ jsx3(RemoteView2, { nodeId: child.id }, child.id));
692
+ const childElements = node.children.map((child) => /* @__PURE__ */ jsx2(RemoteView, { nodeId: child.id }, child.id));
638
693
  if (node.kind === "fragment") {
639
- return createElement2(Fragment, { key: node.id }, ...childElements);
694
+ return createElement(Fragment, { key: node.id }, ...childElements);
640
695
  }
641
- return /* @__PURE__ */ jsx3(RemoteElementView, { node, ctx });
642
- });
696
+ return /* @__PURE__ */ jsx2(RemoteElementView, { node, ctx });
697
+ }
698
+ var RemoteView = memo(RemoteViewComponent);
699
+
700
+ // src/components/app-view.tsx
701
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
702
+ var AppView = ({
703
+ app,
704
+ slot,
705
+ createIframe,
706
+ fallback = null,
707
+ ...viewProps
708
+ }) => {
709
+ const { store, client } = useTailorRootContext("AppView");
710
+ const { theme, components: wrappedComponents } = client;
711
+ const reactId = useId2();
712
+ const currentView = useSyncExternalStore2(
713
+ store.views.subscribe,
714
+ store.views.getSnapshot,
715
+ store.views.getSnapshot
716
+ );
717
+ const view = viewProps.view;
718
+ const suppliedContext = viewProps.context;
719
+ const context = useStableContext(suppliedContext);
720
+ const status = viewProps.status ?? "ready";
721
+ const props = useMemo3(() => {
722
+ if (view !== void 0) {
723
+ return {
724
+ slot,
725
+ view,
726
+ layers: [
727
+ ...(currentView?.layers ?? []).filter(
728
+ (layer) => layer.path !== view && isViewAncestor2(layer.path, view)
729
+ ),
730
+ { path: view, context, status }
731
+ ]
732
+ };
733
+ }
734
+ return currentView === null ? void 0 : { slot, ...currentView };
735
+ }, [context, currentView, view, status, slot]);
736
+ const meta = useSyncExternalStore2(store.subscribe, store.getMetaSnapshot, store.getMetaSnapshot);
737
+ const runtimeProps = useMemo3(
738
+ () => props === void 0 ? void 0 : {
739
+ ...props,
740
+ declaredViews: Object.keys(meta.schema?.views ?? {}),
741
+ supportedViews: meta.schema?.slots[slot]?.views ?? []
742
+ },
743
+ [props, meta.schema, slot]
744
+ );
745
+ const assetsBaseUrl = app.clientPath ? null : meta.assetsBaseUrl;
746
+ const appUrl = useMemo3(
747
+ () => resolveAppUrl(app, store.baseUrl, assetsBaseUrl),
748
+ [app, assetsBaseUrl, store.baseUrl]
749
+ );
750
+ useEffect2(() => {
751
+ void store.fetchMeta();
752
+ }, [store]);
753
+ if (props === void 0 || appUrl === null || meta.schema === null) {
754
+ return fallback;
755
+ }
756
+ const viewId = `tailorkit-view-${reactId.replaceAll(":", "")}`;
757
+ return /* @__PURE__ */ jsx3(PrimitiveThemeContext.Provider, { value: { viewId, theme }, children: /* @__PURE__ */ jsxs2("div", { "data-tailorkit-view": viewId, children: [
758
+ /* @__PURE__ */ jsx3("style", { "data-tailorkit-theme-style": viewId, children: buildThemeCss(viewId, theme) }),
759
+ /* @__PURE__ */ jsx3(
760
+ RemoteViewHost,
761
+ {
762
+ appUrl: appUrl.toString(),
763
+ components: wrappedComponents,
764
+ createIframe,
765
+ props: runtimeProps
766
+ }
767
+ )
768
+ ] }) });
769
+ };
770
+ function resolveAppUrl(app, baseUrl, assetsBaseUrl) {
771
+ if (app.clientPath) {
772
+ return new URL(app.clientPath, baseUrl);
773
+ }
774
+ if (!assetsBaseUrl || !app.projectId || !app.currentDeployment?.id) {
775
+ return null;
776
+ }
777
+ return new URL(
778
+ `projects/${app.projectId}/apps/${app.id}/deployments/${app.currentDeployment.id}/files/client.js`,
779
+ toBaseUrl(assetsBaseUrl)
780
+ );
781
+ }
643
782
 
644
783
  // src/tailor-kit.tsx
645
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
646
784
  var componentTagPrefix = "tailorkit-";
647
785
  var toComponentTagName = (name) => `${componentTagPrefix}${name.replaceAll(/([a-z0-9])([A-Z])/gu, "$1-$2").replaceAll(/[\s_]+/gu, "-").toLowerCase()}`;
648
786
  function components(_schema, customComponents) {
@@ -653,7 +791,6 @@ function createTailorKitClient(options) {
653
791
  }
654
792
  function createReactTailorKitClient(options) {
655
793
  const wrappedComponents = {};
656
- const store = createTailorKitStore(options.baseUrl);
657
794
  const theme = options.theme ?? {};
658
795
  for (const [name, renderer] of Object.entries(options.components ?? {})) {
659
796
  if (renderer) {
@@ -663,258 +800,186 @@ function createReactTailorKitClient(options) {
663
800
  }) {
664
801
  return renderer({
665
802
  props,
666
- slots: { default: children }
803
+ children
667
804
  });
668
805
  };
669
806
  wrappedComponents[name] = TailorKitComponent;
670
807
  wrappedComponents[toComponentTagName(name)] = TailorKitComponent;
671
808
  }
672
809
  }
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
810
  return {
749
- ...client
811
+ baseUrl: options.baseUrl,
812
+ components: wrappedComponents,
813
+ previewSessionId: options.previewSessionId,
814
+ theme
750
815
  };
751
816
  }
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;
817
+
818
+ // src/components/root.tsx
819
+ import { useEffect as useEffect3, useMemo as useMemo5, useState as useState2 } from "react";
820
+
821
+ // src/components/render.tsx
822
+ import { cloneElement, createElement as createElement2, isValidElement, useMemo as useMemo4 } from "react";
823
+ function useRender({
824
+ defaultTagName,
825
+ props,
826
+ render
827
+ }) {
828
+ return useMemo4(() => {
829
+ if (render) {
830
+ if (!isValidElement(render)) {
831
+ throw new Error("The render prop must be a valid React element.");
787
832
  }
833
+ return cloneElement(render, mergeProps(render.props, props));
788
834
  }
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
- );
835
+ return createElement2(defaultTagName, props);
836
+ }, [defaultTagName, props, render]);
837
+ }
838
+ function mergeProps(...propSets) {
839
+ const merged = {};
840
+ for (const props of propSets) {
841
+ if (!props) {
842
+ continue;
797
843
  }
798
- currentScreen = selected;
799
- };
800
- return {
801
- baseUrl,
802
- fetchApps: (options = {}) => {
803
- if (fetchAppsPromise && !options.force) {
804
- return fetchAppsPromise;
844
+ for (const [key, value] of Object.entries(props)) {
845
+ if (value === void 0) {
846
+ continue;
805
847
  }
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;
848
+ const existing = merged[key];
849
+ if (key === "className" && existing && value) {
850
+ merged[key] = `${existing} ${value}`;
851
+ continue;
839
852
  }
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"
853
+ if (key === "style" && existing && value) {
854
+ merged[key] = {
855
+ ...existing,
856
+ ...value
852
857
  };
853
- emit();
854
- }).catch((error) => {
855
- metaSnapshot = {
856
- ...metaSnapshot,
857
- error: error instanceof Error ? error : new Error(String(error)),
858
- status: "error"
858
+ continue;
859
+ }
860
+ if (key === "ref" && existing && value) {
861
+ merged[key] = composeRefs(existing, value);
862
+ continue;
863
+ }
864
+ if (isEventHandler(key) && typeof existing === "function" && typeof value === "function") {
865
+ merged[key] = (...args) => {
866
+ existing(...args);
867
+ const event = args[0];
868
+ if (!event?.defaultPrevented) {
869
+ value(...args);
870
+ }
859
871
  };
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;
872
+ continue;
873
+ }
874
+ merged[key] = value;
875
+ }
876
+ }
877
+ return merged;
878
+ }
879
+ function composeRefs(...refs) {
880
+ return (node) => {
881
+ for (const ref of refs) {
882
+ if (typeof ref === "function") {
883
+ ref(node);
884
+ } else if (ref) {
885
+ ref.current = node;
877
886
  }
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
887
  }
892
888
  };
893
889
  }
894
- function getScreenDepth(screen) {
895
- return screen === "/" ? 0 : screen.split("/").filter(Boolean).length;
890
+ function isEventHandler(key) {
891
+ return /^on[A-Z]/u.test(key);
896
892
  }
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;
893
+
894
+ // src/components/root.tsx
895
+ import { jsx as jsx4 } from "react/jsx-runtime";
896
+ function Root({ apps: appsProp, children, render, client, ...props }) {
897
+ const baseUrl = toBaseUrl(client.baseUrl).toString();
898
+ const [previousStore, setStore] = useState2(
899
+ () => createTailorKitStore(baseUrl, appsProp, client.previewSessionId)
900
+ );
901
+ let store = previousStore;
902
+ if (previousStore.baseUrl.toString() !== baseUrl) {
903
+ store = createTailorKitStore(baseUrl, appsProp, client.previewSessionId);
904
+ setStore(store);
903
905
  }
904
- return new URL(
905
- `projects/${app.projectId}/apps/${app.id}/deployments/${app.currentDeployment.id}/files/client.js`,
906
- toBaseUrl(assetsBaseUrl)
906
+ useEffect3(() => {
907
+ store.setProvidedApps(appsProp);
908
+ }, [store, appsProp]);
909
+ const context = useMemo5(
910
+ () => ({
911
+ store,
912
+ client
913
+ }),
914
+ [store, client]
915
+ );
916
+ const element = render ? useRender({
917
+ defaultTagName: "div",
918
+ props: mergeProps({ children }, props),
919
+ render
920
+ }) : children;
921
+ return /* @__PURE__ */ jsx4(TailorRootContext.Provider, { value: context, children: element });
922
+ }
923
+
924
+ // src/hooks/use-apps.ts
925
+ import { useCallback as useCallback2, useEffect as useEffect4, useSyncExternalStore as useSyncExternalStore3 } from "react";
926
+ function useApps() {
927
+ return useAppsStore(useTailorRootContext("useApps").store);
928
+ }
929
+ function useAppsStore(store) {
930
+ const snapshot = useSyncExternalStore3(
931
+ store.subscribe,
932
+ store.getAppsSnapshot,
933
+ store.getAppsSnapshot
907
934
  );
935
+ useEffect4(() => {
936
+ void store.fetchApps();
937
+ }, [store]);
938
+ const refetch = useCallback2(() => store.fetchApps({ force: true }), [store]);
939
+ return toUseAppsResult(snapshot, refetch);
908
940
  }
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;
941
+ function toUseAppsResult(snapshot, refetch) {
942
+ return {
943
+ data: snapshot.status === "ready" ? snapshot.apps : void 0,
944
+ error: snapshot.error,
945
+ isError: snapshot.status === "error",
946
+ isLoading: snapshot.status === "loading",
947
+ isPending: snapshot.status === "idle" || snapshot.status === "loading",
948
+ isSuccess: snapshot.status === "ready",
949
+ refetch,
950
+ status: snapshot.status
951
+ };
952
+ }
953
+
954
+ // src/hooks/use-view.ts
955
+ import { useEffect as useEffect5, useMemo as useMemo6 } from "react";
956
+ function useView(view, options) {
957
+ const { store } = useTailorRootContext("useView");
958
+ const id = useMemo6(() => /* @__PURE__ */ Symbol("tailorkit-current-view"), []);
959
+ const status = options.status ?? "ready";
960
+ const context = "context" in options ? options.context : void 0;
961
+ const contextSnapshot = useStableContext(context);
962
+ useEffect5(
963
+ () => () => {
964
+ store.views.unregister(id);
965
+ },
966
+ [id, store]
967
+ );
968
+ useEffect5(() => {
969
+ store.views.register({
970
+ context: contextSnapshot,
971
+ id,
972
+ view,
973
+ status
974
+ });
975
+ }, [contextSnapshot, id, view, status, store]);
915
976
  }
916
977
  export {
978
+ AppView,
979
+ Root,
917
980
  components,
918
981
  createTailorKitClient,
919
- primitives
982
+ primitives,
983
+ useApps,
984
+ useView
920
985
  };