@tailorkit/react 0.1.0-beta.8 → 0.1.0-beta.9

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