@tailorkit/react 0.1.0-beta.7 → 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, 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 })
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
  }
@@ -515,8 +560,7 @@ function RemoteViewHost({
515
560
  appUrl,
516
561
  components: components2,
517
562
  createIframe,
518
- props,
519
- runtimeUrl
563
+ props
520
564
  }) {
521
565
  const appKey = appUrl.toString();
522
566
  const storeRef = useRef2(null);
@@ -529,6 +573,12 @@ function RemoteViewHost({
529
573
  appKeyRef.current = appKey;
530
574
  store.clear();
531
575
  }
576
+ const hostRef = useRef2(null);
577
+ const propsRef = useRef2(props);
578
+ useEffect(() => {
579
+ propsRef.current = props;
580
+ hostRef.current?.setProps(props);
581
+ }, [props]);
532
582
  const dispatchRef = useRef2(null);
533
583
  const [error, setError] = useState(null);
534
584
  const [status, setStatus] = useState("starting");
@@ -537,22 +587,22 @@ function RemoteViewHost({
537
587
  dispatch: (payload) => dispatchRef.current?.(payload),
538
588
  store
539
589
  });
540
- useEffect3(() => {
590
+ useEffect(() => {
541
591
  ctxRef.current.components = components2;
542
592
  }, [components2]);
543
- const ui = useMemo5(() => {
593
+ const ui = useMemo2(() => {
544
594
  const ctx = ctxRef.current;
545
- return createElement2(
595
+ return createElement(
546
596
  RemoteUIContext.Provider,
547
597
  { value: ctx },
548
- createElement2(
598
+ createElement(
549
599
  RemoteErrorBoundary,
550
600
  { key: appKey },
551
- createElement2(RemoteRoot, { store: ctx.store })
601
+ createElement(RemoteRoot, { store: ctx.store })
552
602
  )
553
603
  );
554
604
  }, [appKey]);
555
- useEffect3(() => {
605
+ useEffect(() => {
556
606
  const host = createIframeUiHost(appUrl, {
557
607
  createIframe,
558
608
  onError: (error2) => {
@@ -560,9 +610,9 @@ function RemoteViewHost({
560
610
  setError(error2);
561
611
  setStatus("error");
562
612
  },
563
- props,
564
- runtimeUrl
613
+ props: propsRef.current
565
614
  });
615
+ hostRef.current = host;
566
616
  dispatchRef.current = (payload) => host.dispatch(payload);
567
617
  const unsubscribe = host.subscribe(() => {
568
618
  const tree = host.getSnapshot();
@@ -577,26 +627,27 @@ function RemoteViewHost({
577
627
  return () => {
578
628
  unsubscribe();
579
629
  dispatchRef.current = null;
630
+ hostRef.current = null;
580
631
  host.destroy();
581
632
  };
582
- }, [appUrl, createIframe, props, runtimeUrl, store]);
633
+ }, [appUrl, createIframe, store]);
583
634
  if (status === "error" && error) {
584
- return createElement2("div", null, formatError(error));
635
+ return createElement("div", null, formatError(error));
585
636
  }
586
637
  return ui;
587
638
  }
588
639
  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);
640
+ const subscribe = useCallback((listener) => store.subscribeRoot(listener), [store]);
641
+ const getSnapshot = useCallback(() => store.getRootId(), [store]);
642
+ const rootId = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
592
643
  if (rootId === null) {
593
644
  return null;
594
645
  }
595
- return /* @__PURE__ */ jsx3(RemoteView, { nodeId: rootId });
646
+ return /* @__PURE__ */ jsx2(RemoteView, { nodeId: rootId });
596
647
  }
597
648
  var RemoteElementView = memo(({ ctx, node }) => {
598
649
  const { dispatch, components: components2 } = ctx;
599
- 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));
600
651
  const component = components2[node.type];
601
652
  if (component === void 0) {
602
653
  throw new Error(`TailorKit component "${node.type}" is not registered.`);
@@ -614,7 +665,7 @@ var RemoteElementView = memo(({ ctx, node }) => {
614
665
  });
615
666
  };
616
667
  }
617
- return createElement2(component, props, ...children);
668
+ return createElement(component, props, ...children);
618
669
  });
619
670
  var RemoteView = memo(function RemoteView2({ nodeId }) {
620
671
  const ctx = useContext3(RemoteUIContext);
@@ -622,27 +673,109 @@ var RemoteView = memo(function RemoteView2({ nodeId }) {
622
673
  return null;
623
674
  }
624
675
  const { store } = ctx;
625
- const subscribe = useCallback2(
676
+ const subscribe = useCallback(
626
677
  (listener) => store.subscribe(nodeId, listener),
627
678
  [store, nodeId]
628
679
  );
629
- const getSnapshot = useCallback2(() => store.getNode(nodeId), [store, nodeId]);
630
- const node = useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
680
+ const getSnapshot = useCallback(() => store.getNode(nodeId), [store, nodeId]);
681
+ const node = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
631
682
  if (node === null) {
632
683
  return null;
633
684
  }
634
685
  if (node.kind === "text") {
635
686
  return node.text;
636
687
  }
637
- 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));
638
689
  if (node.kind === "fragment") {
639
- return createElement2(Fragment, { key: node.id }, ...childElements);
690
+ return createElement(Fragment, { key: node.id }, ...childElements);
640
691
  }
641
- return /* @__PURE__ */ jsx3(RemoteElementView, { node, ctx });
692
+ return /* @__PURE__ */ jsx2(RemoteElementView, { node, ctx });
642
693
  });
643
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
+
644
778
  // src/tailor-kit.tsx
645
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
646
779
  var componentTagPrefix = "tailorkit-";
647
780
  var toComponentTagName = (name) => `${componentTagPrefix}${name.replaceAll(/([a-z0-9])([A-Z])/gu, "$1-$2").replaceAll(/[\s_]+/gu, "-").toLowerCase()}`;
648
781
  function components(_schema, customComponents) {
@@ -653,7 +786,6 @@ function createTailorKitClient(options) {
653
786
  }
654
787
  function createReactTailorKitClient(options) {
655
788
  const wrappedComponents = {};
656
- const store = createTailorKitStore(options.baseUrl);
657
789
  const theme = options.theme ?? {};
658
790
  for (const [name, renderer] of Object.entries(options.components ?? {})) {
659
791
  if (renderer) {
@@ -663,258 +795,179 @@ function createReactTailorKitClient(options) {
663
795
  }) {
664
796
  return renderer({
665
797
  props,
666
- slots: { default: children }
798
+ children
667
799
  });
668
800
  };
669
801
  wrappedComponents[name] = TailorKitComponent;
670
802
  wrappedComponents[toComponentTagName(name)] = TailorKitComponent;
671
803
  }
672
804
  }
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
- };
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.");
700
822
  }
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;
823
+ return cloneElement(render, mergeProps(render.props, props));
724
824
  }
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
- };
825
+ return createElement2(defaultTagName, props);
826
+ }, [defaultTagName, props, render]);
751
827
  }
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();
828
+ function mergeProps(...propSets) {
829
+ const merged = {};
830
+ for (const props of propSets) {
831
+ if (!props) {
832
+ continue;
775
833
  }
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;
834
+ for (const [key, value] of Object.entries(props)) {
835
+ if (value === void 0) {
836
+ continue;
787
837
  }
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;
838
+ const existing = merged[key];
839
+ if (key === "className" && existing && value) {
840
+ merged[key] = `${existing} ${value}`;
841
+ continue;
805
842
  }
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"
843
+ if (key === "style" && existing && value) {
844
+ merged[key] = {
845
+ ...existing,
846
+ ...value
831
847
  };
832
- emit();
833
- });
834
- return fetchAppsPromise;
835
- },
836
- fetchMeta: () => {
837
- if (fetchMetaPromise) {
838
- return fetchMetaPromise;
848
+ continue;
839
849
  }
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"
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
+ }
859
861
  };
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;
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;
877
876
  }
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
877
  }
892
878
  };
893
879
  }
894
- function getScreenDepth(screen) {
895
- return screen === "/" ? 0 : screen.split("/").filter(Boolean).length;
880
+ function isEventHandler(key) {
881
+ return /^on[A-Z]/u.test(key);
896
882
  }
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;
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);
903
893
  }
904
- return new URL(
905
- `projects/${app.projectId}/apps/${app.id}/deployments/${app.currentDeployment.id}/files/client.js`,
906
- 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
907
922
  );
923
+ useEffect4(() => {
924
+ void store.fetchApps();
925
+ }, [store]);
926
+ const refetch = useCallback2(() => store.fetchApps({ force: true }), [store]);
927
+ return toUseAppsResult(snapshot, refetch);
908
928
  }
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;
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]);
915
964
  }
916
965
  export {
966
+ AppView,
967
+ Root,
917
968
  components,
918
969
  createTailorKitClient,
919
- primitives
970
+ primitives,
971
+ useApps,
972
+ useView
920
973
  };