@blocksdiy/blocks-client-sdk 1.24.2 → 1.25.1
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/README.md +32 -0
- package/dist/Navigation.d.ts +40 -0
- package/dist/Navigation.d.ts.map +1 -0
- package/dist/Navigation.js +66 -0
- package/dist/ReactClientSdk.d.ts +1 -0
- package/dist/ReactClientSdk.d.ts.map +1 -1
- package/dist/ReactClientSdk.jsx +1 -0
- package/dist/app/AppRoutes.d.ts +8 -0
- package/dist/app/AppRoutes.d.ts.map +1 -0
- package/dist/app/AppRoutes.jsx +222 -0
- package/dist/app/BlocksApp.d.ts +22 -0
- package/dist/app/BlocksApp.d.ts.map +1 -0
- package/dist/app/BlocksApp.jsx +73 -0
- package/dist/app/SdkProvider.d.ts +6 -0
- package/dist/app/SdkProvider.d.ts.map +1 -0
- package/dist/app/SdkProvider.jsx +45 -0
- package/dist/app/app-shape/bundle-shape.d.ts +54 -0
- package/dist/app/app-shape/bundle-shape.d.ts.map +1 -0
- package/dist/app/app-shape/bundle-shape.js +118 -0
- package/dist/app/app-shape/route-tree.d.ts +33 -0
- package/dist/app/app-shape/route-tree.d.ts.map +1 -0
- package/dist/app/app-shape/route-tree.jsx +137 -0
- package/dist/app/app-shape/use-app-shape.d.ts +13 -0
- package/dist/app/app-shape/use-app-shape.d.ts.map +1 -0
- package/dist/app/app-shape/use-app-shape.js +13 -0
- package/dist/app/dev-server/bootstrap.d.ts +8 -0
- package/dist/app/dev-server/bootstrap.d.ts.map +1 -0
- package/dist/app/dev-server/bootstrap.js +43 -0
- package/dist/app/dev-server/fonts.d.ts +8 -0
- package/dist/app/dev-server/fonts.d.ts.map +1 -0
- package/dist/app/dev-server/fonts.js +67 -0
- package/dist/app/hooks/use-scroll-to-top-on-page-change.d.ts +17 -0
- package/dist/app/hooks/use-scroll-to-top-on-page-change.d.ts.map +1 -0
- package/dist/app/hooks/use-scroll-to-top-on-page-change.js +55 -0
- package/dist/app/host.d.ts +106 -0
- package/dist/app/host.d.ts.map +1 -0
- package/dist/app/host.js +67 -0
- package/dist/app/index.d.ts +14 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +14 -0
- package/dist/app/sign-in/Login.d.ts +8 -0
- package/dist/app/sign-in/Login.d.ts.map +1 -0
- package/dist/app/sign-in/Login.jsx +194 -0
- package/dist/app/sign-in/api.d.ts +13 -0
- package/dist/app/sign-in/api.d.ts.map +1 -0
- package/dist/app/sign-in/api.js +47 -0
- package/dist/app/sign-in/auth-url.d.ts +7 -0
- package/dist/app/sign-in/auth-url.d.ts.map +1 -0
- package/dist/app/sign-in/auth-url.js +6 -0
- package/dist/app/types.d.ts +36 -0
- package/dist/app/types.d.ts.map +1 -0
- package/dist/app/types.js +1 -0
- package/dist/app/ui/icons.d.ts +12 -0
- package/dist/app/ui/icons.d.ts.map +1 -0
- package/dist/app/ui/icons.jsx +32 -0
- package/dist/app/ui/ui-styles.d.ts +15 -0
- package/dist/app/ui/ui-styles.d.ts.map +1 -0
- package/dist/app/ui/ui-styles.js +458 -0
- package/dist/dev-config.d.ts +15 -0
- package/dist/dev-config.d.ts.map +1 -0
- package/dist/dev-config.js +9 -0
- package/package.json +22 -6
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle-shape.d.ts","sourceRoot":"","sources":["../../../src/app/app-shape/bundle-shape.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,KAAK,EAAmB,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGlE,2DAA2D;AAC3D,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,aAAa,CAAC;CAC1B;AAED,+DAA+D;AAC/D,MAAM,WAAW,QAAQ;IACvB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,0FAA0F;IAC1F,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,qCAAqC;IACrC,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,8EAA8E;IAC9E,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;IAC5B,kGAAkG;IAClG,OAAO,EAAE,OAAO,CAAC;IACjB,kFAAkF;IAClF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAwCD,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC7B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,uFAAuF;IACvF,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAKD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,QAAQ,cAAc,GAAG,SAAS,EAAE,OAAO,MAAM,GAAG,SAAS,KAAG,QAoB/F,CAAC;AAmBF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,QA+BnE,CAAC"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { buildBlocksRoutes, srcRelativePath } from "./route-tree";
|
|
2
|
+
/**
|
|
3
|
+
* Flatten the routes-mode tree into legacy-shaped page entries so missing-page reporting and the
|
|
4
|
+
* "no pages" check work unchanged. Leaf nodes are pages (nodes with children are layouts); the
|
|
5
|
+
* route id doubles as the name.
|
|
6
|
+
*/
|
|
7
|
+
const flattenRoutePages = (nodes) => {
|
|
8
|
+
const pages = [];
|
|
9
|
+
const visit = (node) => {
|
|
10
|
+
if (!node.children && (node.index || node.path)) {
|
|
11
|
+
pages.push({ id: node.id, name: node.id, component: node.Component });
|
|
12
|
+
}
|
|
13
|
+
node.children?.forEach(visit);
|
|
14
|
+
};
|
|
15
|
+
nodes.forEach(visit);
|
|
16
|
+
return pages;
|
|
17
|
+
};
|
|
18
|
+
/** A pages-mode app's routes for error reports: each page at its name and its id, as the tree routes it. */
|
|
19
|
+
const pageRoutes = (pages) => pages.flatMap(({ id, name }) => [{ id, path: name }, ...(name === id ? [] : [{ id, path: id }])]);
|
|
20
|
+
const routesShape = (routes, chatComponentRenderer) => ({
|
|
21
|
+
routes,
|
|
22
|
+
pages: flattenRoutePages(routes.routes),
|
|
23
|
+
chatComponentRenderer,
|
|
24
|
+
// The tree IS a RouteObject tree; the extra `id`/`name` metadata is ignored.
|
|
25
|
+
errorRoutes: routes.routes,
|
|
26
|
+
isEmpty: false,
|
|
27
|
+
});
|
|
28
|
+
const pagesShape = (pages, layout, chatComponentRenderer) => ({
|
|
29
|
+
pages,
|
|
30
|
+
layout,
|
|
31
|
+
chatComponentRenderer,
|
|
32
|
+
errorRoutes: pageRoutes(pages),
|
|
33
|
+
isEmpty: pages.length === 0,
|
|
34
|
+
});
|
|
35
|
+
/** The bundle's exports that are not pages. */
|
|
36
|
+
const BUNDLE_META_EXPORTS = new Set(["__blocksRoutes", "__agentChatCodeComponent", "AppLayout"]);
|
|
37
|
+
/**
|
|
38
|
+
* The shape of a compiled bundle the host imported, in any of its three forms: a routes-mode
|
|
39
|
+
* module (`__blocksRoutes`), a legacy module (`AppLayout` plus one named export per page), or the
|
|
40
|
+
* oldest, a `window["compiler_<appId>"]` map of one-character-prefixed page ids. An EMPTY routes tree (every route module failed to derive, e.g. no default exports)
|
|
41
|
+
* takes the legacy branch, so the "Could not load the components" fatal still fires.
|
|
42
|
+
*/
|
|
43
|
+
export const shapeFromBundle = (bundle, appId) => {
|
|
44
|
+
const chatComponentRenderer = bundle?.__agentChatCodeComponent;
|
|
45
|
+
if (bundle?.__blocksRoutes?.routes?.length) {
|
|
46
|
+
return routesShape(bundle.__blocksRoutes, chatComponentRenderer);
|
|
47
|
+
}
|
|
48
|
+
if (bundle) {
|
|
49
|
+
const pages = Object.entries(bundle)
|
|
50
|
+
.filter(([name]) => !BUNDLE_META_EXPORTS.has(name))
|
|
51
|
+
.map(([, page]) => page);
|
|
52
|
+
return pagesShape(pages, bundle.AppLayout, chatComponentRenderer);
|
|
53
|
+
}
|
|
54
|
+
const windowPages = window[`compiler_${appId}`];
|
|
55
|
+
const pages = Object.entries(windowPages ?? {}).map(([blockId, component]) => ({
|
|
56
|
+
id: blockId.substring(1),
|
|
57
|
+
name: blockId.substring(1),
|
|
58
|
+
component,
|
|
59
|
+
}));
|
|
60
|
+
return pagesShape(pages, undefined, chatComponentRenderer);
|
|
61
|
+
};
|
|
62
|
+
/** The chat-component renderer's module, at the path the app's `src/main.tsx` globs it. */
|
|
63
|
+
const CHAT_COMPONENT_RENDERER_FILE = "components/ui/agent-chat-code-component.tsx";
|
|
64
|
+
/** A pages-mode app's `layout.tsx`, as the compiler exports it. */
|
|
65
|
+
const LAYOUT_ID = "AppLayout";
|
|
66
|
+
/** The landing page of a pages-mode app, by file name, as the compiler picks it; else its first page. */
|
|
67
|
+
const LANDING_PAGE_NAMES = ["main", "home", "index", "dashboard"];
|
|
68
|
+
const landingPageId = (pages) => {
|
|
69
|
+
for (const preferred of LANDING_PAGE_NAMES) {
|
|
70
|
+
const page = pages.find(({ name }) => name.slice(name.lastIndexOf("/") + 1).toLowerCase() === preferred);
|
|
71
|
+
if (page) {
|
|
72
|
+
return page.id;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return pages[0]?.id;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* The shape of a self-mounting app from the modules its `src/main.tsx` globbed, by the rule the
|
|
79
|
+
* compiler builds a bundle with: an app with a module under `routes/` is routes mode (`routes/**`
|
|
80
|
+
* and `root.tsx` make the tree), else `pages/**` are its pages, each at its file name inside
|
|
81
|
+
* `layout.tsx`. `components/ui/agent-chat-code-component.tsx`, when the app has the agent-chat
|
|
82
|
+
* item, is its chat-component renderer.
|
|
83
|
+
*/
|
|
84
|
+
export const shapeFromModules = (modules) => {
|
|
85
|
+
let chatComponentRenderer;
|
|
86
|
+
let layout;
|
|
87
|
+
let hasRouteModules = false;
|
|
88
|
+
const routeModules = {};
|
|
89
|
+
const pages = [];
|
|
90
|
+
for (const modulePath of Object.keys(modules).sort()) {
|
|
91
|
+
const module = modules[modulePath];
|
|
92
|
+
const file = srcRelativePath(modulePath);
|
|
93
|
+
if (file === CHAT_COMPONENT_RENDERER_FILE) {
|
|
94
|
+
chatComponentRenderer = module;
|
|
95
|
+
}
|
|
96
|
+
else if (file === "layout.tsx") {
|
|
97
|
+
layout = { id: LAYOUT_ID, name: LAYOUT_ID, component: module?.default };
|
|
98
|
+
}
|
|
99
|
+
else if (file.startsWith("pages/") && file.endsWith(".tsx")) {
|
|
100
|
+
// As the compiler exports it, default export or not: a page without one crashes in its own
|
|
101
|
+
// boundary, which names it to the builder.
|
|
102
|
+
const name = file.slice("pages/".length, -".tsx".length);
|
|
103
|
+
pages.push({ id: name, name, component: module?.default });
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
hasRouteModules ||= file.startsWith("routes/");
|
|
107
|
+
routeModules[modulePath] = module;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (hasRouteModules) {
|
|
111
|
+
const routes = buildBlocksRoutes(routeModules);
|
|
112
|
+
// An empty tree (every route module failed to derive) is an app with no page, as for a bundle.
|
|
113
|
+
return routes.routes.length
|
|
114
|
+
? routesShape(routes, chatComponentRenderer)
|
|
115
|
+
: pagesShape([], undefined, chatComponentRenderer);
|
|
116
|
+
}
|
|
117
|
+
return { ...pagesShape(pages, layout, chatComponentRenderer), defaultPageId: landingPageId(pages) };
|
|
118
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-system routing: the folder structure under `src/routes/` IS the URL structure (conventions:
|
|
3
|
+
* the boilerplate's pages-and-routing skill). The app's `src/main.tsx` imports every route module
|
|
4
|
+
* with an eager `import.meta.glob` and hands the flat module map here, which folds it into the
|
|
5
|
+
* nested tree `BlocksApp` gives React Router's `useRoutes`:
|
|
6
|
+
*
|
|
7
|
+
* - a file → a route node (`[x]` → `:x`, `index` → index route,
|
|
8
|
+
* `not-found` → the folder's catch-all 404)
|
|
9
|
+
* - folder WITH layout.tsx → parent node; children render into its <Outlet/>
|
|
10
|
+
* - folder WITHOUT layout → no node; children get compound paths ("admin/Users")
|
|
11
|
+
*
|
|
12
|
+
* The compiler mirrors this derivation when route files sync to blocks
|
|
13
|
+
* (apps/compiler/src/modules/git-sync/fs-routes-path.ts) — keep identical.
|
|
14
|
+
*/
|
|
15
|
+
import { type ComponentType } from "react";
|
|
16
|
+
import type { BlocksRouteNode, BlocksRoutesTree } from "../types.js";
|
|
17
|
+
/** A module map as `import.meta.glob(..., { eager: true })` produces it. */
|
|
18
|
+
export type RouteModules = Record<string, {
|
|
19
|
+
default?: ComponentType;
|
|
20
|
+
} | undefined>;
|
|
21
|
+
/**
|
|
22
|
+
* The module's path from `src/`, whichever form the glob gave it: absolute from the project root
|
|
23
|
+
* (`/src/routes/x.tsx`) or relative to `src/main.tsx` (`./routes/x.tsx`).
|
|
24
|
+
*/
|
|
25
|
+
export declare const srcRelativePath: (modulePath: string) => string;
|
|
26
|
+
/** Fold the route module map (the `src/routes/**` entries) into route nodes. */
|
|
27
|
+
export declare function deriveRouteNodes(modules: RouteModules): BlocksRouteNode[];
|
|
28
|
+
/**
|
|
29
|
+
* The app's route tree from the modules its `src/main.tsx` globbed: every `routes/**` module and
|
|
30
|
+
* `root.tsx` (the root layout; absent, a bare outlet).
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildBlocksRoutes(modules: RouteModules): BlocksRoutesTree;
|
|
33
|
+
//# sourceMappingURL=route-tree.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-tree.d.ts","sourceRoot":"","sources":["../../../src/app/app-shape/route-tree.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAG3C,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAElE,4EAA4E;AAC5E,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,CAAC,EAAE,aAAa,CAAA;CAAE,GAAG,SAAS,CAAC,CAAC;AAenF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,YAAY,MAAM,KAAG,MAAiE,CAAC;AAyFvH,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,eAAe,EAAE,CAiBzE;AAID;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,gBAAgB,CAezE"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Outlet } from "react-router";
|
|
2
|
+
/** `[city]` → `:city`, `[...rest]` → `*`, anything else stays literal. */
|
|
3
|
+
function mapSegment(segment) {
|
|
4
|
+
const splat = segment.match(/^\[\.\.\..*\]$/);
|
|
5
|
+
if (splat) {
|
|
6
|
+
return "*";
|
|
7
|
+
}
|
|
8
|
+
const param = segment.match(/^\[(.+)\]$/);
|
|
9
|
+
if (param) {
|
|
10
|
+
return `:${param[1]}`;
|
|
11
|
+
}
|
|
12
|
+
return segment;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The module's path from `src/`, whichever form the glob gave it: absolute from the project root
|
|
16
|
+
* (`/src/routes/x.tsx`) or relative to `src/main.tsx` (`./routes/x.tsx`).
|
|
17
|
+
*/
|
|
18
|
+
export const srcRelativePath = (modulePath) => modulePath.replace(/^\/src\//, "").replace(/^\.\//, "");
|
|
19
|
+
const newFolder = () => ({ files: new Map(), folders: new Map() });
|
|
20
|
+
function buildFolderTree(modules) {
|
|
21
|
+
const root = newFolder();
|
|
22
|
+
for (const modulePath of Object.keys(modules).sort()) {
|
|
23
|
+
const Component = modules[modulePath]?.default;
|
|
24
|
+
if (!Component) {
|
|
25
|
+
console.error(`[blocks] Route module has no default export: ${modulePath}`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
// "routes/concerts/[city].tsx" → ["concerts", "[city].tsx"]
|
|
29
|
+
const file = srcRelativePath(modulePath);
|
|
30
|
+
const segments = file.replace(/^routes\//, "").split("/");
|
|
31
|
+
if (segments.some((segment) => segment.startsWith("."))) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
let folder = root;
|
|
35
|
+
for (const dir of segments.slice(0, -1)) {
|
|
36
|
+
if (!folder.folders.has(dir)) {
|
|
37
|
+
folder.folders.set(dir, newFolder());
|
|
38
|
+
}
|
|
39
|
+
folder = folder.folders.get(dir);
|
|
40
|
+
}
|
|
41
|
+
const base = segments[segments.length - 1].replace(/\.tsx$/, "");
|
|
42
|
+
folder.files.set(base, { file, Component });
|
|
43
|
+
}
|
|
44
|
+
return root;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Convert a folder into route nodes. `pathPrefix` accumulates the segments of
|
|
48
|
+
* layout-less ancestor folders.
|
|
49
|
+
*/
|
|
50
|
+
function folderToNodes(folder, pathPrefix) {
|
|
51
|
+
const nodes = [];
|
|
52
|
+
const joinPath = (...parts) => parts.filter(Boolean).join("/");
|
|
53
|
+
for (const [base, entry] of folder.files) {
|
|
54
|
+
if (base === "layout") {
|
|
55
|
+
continue; // consumed by the parent folder conversion
|
|
56
|
+
}
|
|
57
|
+
const id = entry.file.replace(/\.tsx$/, "");
|
|
58
|
+
if (base === "index") {
|
|
59
|
+
nodes.push({
|
|
60
|
+
id,
|
|
61
|
+
// An index route matches the parent path. With a layout-less prefix it
|
|
62
|
+
// still needs the accumulated segments as its own path.
|
|
63
|
+
...(pathPrefix ? { path: pathPrefix } : { index: true }),
|
|
64
|
+
Component: entry.Component,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
nodes.push({
|
|
69
|
+
id,
|
|
70
|
+
// `not-found.tsx` is the folder's 404 (Next.js-style): a catch-all
|
|
71
|
+
// matching any URL nothing else in the folder matches. Splats rank
|
|
72
|
+
// lowest in React Router, so it never shadows real routes.
|
|
73
|
+
path: joinPath(pathPrefix, base === "not-found" ? "*" : mapSegment(base)),
|
|
74
|
+
Component: entry.Component,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const [dirName, sub] of folder.folders) {
|
|
79
|
+
const segment = mapSegment(dirName);
|
|
80
|
+
const layout = sub.files.get("layout");
|
|
81
|
+
if (layout) {
|
|
82
|
+
nodes.push({
|
|
83
|
+
id: layout.file.replace(/\.tsx$/, ""),
|
|
84
|
+
path: joinPath(pathPrefix, segment),
|
|
85
|
+
Component: layout.Component,
|
|
86
|
+
children: folderToNodes(sub, ""),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// No layout — the folder contributes URL segments only; children are
|
|
91
|
+
// hoisted to this level with compound paths.
|
|
92
|
+
nodes.push(...folderToNodes(sub, joinPath(pathPrefix, segment)));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return nodes;
|
|
96
|
+
}
|
|
97
|
+
/** Fold the route module map (the `src/routes/**` entries) into route nodes. */
|
|
98
|
+
export function deriveRouteNodes(modules) {
|
|
99
|
+
const root = buildFolderTree(modules);
|
|
100
|
+
const nodes = folderToNodes(root, "");
|
|
101
|
+
// A top-level routes/layout.tsx wraps ALL routes (inside src/root.tsx) —
|
|
102
|
+
// same folder rule as everywhere else, with src/routes/ being the folder.
|
|
103
|
+
// It becomes a pathless layout route so every URL renders through it.
|
|
104
|
+
const rootLayout = root.files.get("layout");
|
|
105
|
+
if (rootLayout) {
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
id: rootLayout.file.replace(/\.tsx$/, ""),
|
|
109
|
+
Component: rootLayout.Component,
|
|
110
|
+
children: nodes,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
return nodes;
|
|
115
|
+
}
|
|
116
|
+
const FallbackRoot = () => <Outlet />;
|
|
117
|
+
/**
|
|
118
|
+
* The app's route tree from the modules its `src/main.tsx` globbed: every `routes/**` module and
|
|
119
|
+
* `root.tsx` (the root layout; absent, a bare outlet).
|
|
120
|
+
*/
|
|
121
|
+
export function buildBlocksRoutes(modules) {
|
|
122
|
+
const routeModules = {};
|
|
123
|
+
let rootModule;
|
|
124
|
+
for (const [modulePath, module] of Object.entries(modules)) {
|
|
125
|
+
const file = srcRelativePath(modulePath);
|
|
126
|
+
if (file === "root.tsx") {
|
|
127
|
+
rootModule = module;
|
|
128
|
+
}
|
|
129
|
+
else if (file.startsWith("routes/") && file.endsWith(".tsx")) {
|
|
130
|
+
routeModules[modulePath] = module;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
root: { id: "root", Component: rootModule?.default ?? FallbackRoot },
|
|
135
|
+
routes: deriveRouteNodes(routeModules),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AppShape } from "./bundle-shape.js";
|
|
2
|
+
/** The app's code as `BlocksApp` shaped it, with when it arrived. */
|
|
3
|
+
export type LoadedAppShape = AppShape & {
|
|
4
|
+
/** Stamps the code's arrival: the tree remounts on it after a recompile, so new code takes over cleanly. */
|
|
5
|
+
componentsLoadedAt: number | undefined;
|
|
6
|
+
};
|
|
7
|
+
export declare const AppShapeContext: import("react").Context<LoadedAppShape | undefined>;
|
|
8
|
+
/**
|
|
9
|
+
* The app's code as shaped (its routes or pages, layout and chat renderer) with its load stamp, for
|
|
10
|
+
* the tree and for the host's layers in it.
|
|
11
|
+
*/
|
|
12
|
+
export declare const useAppShape: () => LoadedAppShape;
|
|
13
|
+
//# sourceMappingURL=use-app-shape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-app-shape.d.ts","sourceRoot":"","sources":["../../../src/app/app-shape/use-app-shape.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,qEAAqE;AACrE,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG;IACtC,4GAA4G;IAC5G,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC,CAAC;AAEF,eAAO,MAAM,eAAe,qDAAuD,CAAC;AAEpF;;;GAGG;AACH,eAAO,MAAM,WAAW,QAAO,cAM9B,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
export const AppShapeContext = createContext(undefined);
|
|
3
|
+
/**
|
|
4
|
+
* The app's code as shaped (its routes or pages, layout and chat renderer) with its load stamp, for
|
|
5
|
+
* the tree and for the host's layers in it.
|
|
6
|
+
*/
|
|
7
|
+
export const useAppShape = () => {
|
|
8
|
+
const shape = useContext(AppShapeContext);
|
|
9
|
+
if (!shape) {
|
|
10
|
+
throw new Error("useAppShape must be used within BlocksApp");
|
|
11
|
+
}
|
|
12
|
+
return shape;
|
|
13
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dev server has no host page: `pnpm dev` serves the boilerplate's own `index.html`, so the
|
|
3
|
+
* app hosts itself. Once, before the first render, this stands in for app-render's boot — the app
|
|
4
|
+
* is the one the workspace is for, the token is whatever the developer (or the builder's sandbox)
|
|
5
|
+
* stored, and the theme comes from the app's own stylesheet.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ensureSelfHost: () => void;
|
|
8
|
+
//# sourceMappingURL=bootstrap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../../../src/app/dev-server/bootstrap.ts"],"names":[],"mappings":"AAOA;;;;;GAKG;AACH,eAAO,MAAM,cAAc,QAAO,IAiCjC,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getGoogleBaseUrl, getHost } from "@blocksdiy/blocks-client-api/envService";
|
|
2
|
+
import { devConfig, tokenStorageKey } from "../../dev-config.js";
|
|
3
|
+
import { getBlocksHostState, setBlocksHost, THEME_MODE_STORAGE_KEY } from "../host.js";
|
|
4
|
+
import { loadFontsFromTheme } from "./fonts.js";
|
|
5
|
+
/**
|
|
6
|
+
* The dev server has no host page: `pnpm dev` serves the boilerplate's own `index.html`, so the
|
|
7
|
+
* app hosts itself. Once, before the first render, this stands in for app-render's boot — the app
|
|
8
|
+
* is the one the workspace is for, the token is whatever the developer (or the builder's sandbox)
|
|
9
|
+
* stored, and the theme comes from the app's own stylesheet.
|
|
10
|
+
*/
|
|
11
|
+
export const ensureSelfHost = () => {
|
|
12
|
+
if (getBlocksHostState()) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
// Only the dev server declares itself: anywhere else, an empty store means the tree has no host
|
|
16
|
+
// (or the page loaded a second copy of the SDK), which the tree fails on rather than host the app
|
|
17
|
+
// on its own.
|
|
18
|
+
const dev = devConfig();
|
|
19
|
+
if (!dev) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const appId = dev.appId;
|
|
23
|
+
if (!appId) {
|
|
24
|
+
throw new Error("BlocksApp has no host: set window.__BLOCKS_DEV__.appId (VITE_APP_ID) for the dev server");
|
|
25
|
+
}
|
|
26
|
+
// The SDK's URL helpers read it (`getLoginUrl`, `getPageUrl`).
|
|
27
|
+
window.appId = appId;
|
|
28
|
+
const token = window.localStorage.getItem(tokenStorageKey()) ?? undefined;
|
|
29
|
+
const themeMode = window.localStorage.getItem(THEME_MODE_STORAGE_KEY) || "system";
|
|
30
|
+
// The platform the workspace points at takes the login's requests and its Google sign-in alike.
|
|
31
|
+
const apiHost = dev?.apiHost?.replace(/\/$/, "");
|
|
32
|
+
setBlocksHost({
|
|
33
|
+
config: {
|
|
34
|
+
baseUrl: apiHost || getHost().replace(/\/$/, ""),
|
|
35
|
+
googleBaseUrl: apiHost || getGoogleBaseUrl(),
|
|
36
|
+
},
|
|
37
|
+
app: { id: appId, name: document.title || "App", compiledAt: 0 },
|
|
38
|
+
token,
|
|
39
|
+
requiresSignIn: !token,
|
|
40
|
+
themeMode,
|
|
41
|
+
});
|
|
42
|
+
loadFontsFromTheme();
|
|
43
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load Google Fonts from the theme's CSS variables (--font-sans, --font-serif, --font-mono) on the
|
|
3
|
+
* dev server, where no host injects the theme: mirrors app-render's font loading so the app looks
|
|
4
|
+
* the same as it does live.
|
|
5
|
+
*/
|
|
6
|
+
/** Read the font variables off the document and load any non-system Google Font through a <link>. */
|
|
7
|
+
export declare function loadFontsFromTheme(): void;
|
|
8
|
+
//# sourceMappingURL=fonts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fonts.d.ts","sourceRoot":"","sources":["../../../src/app/dev-server/fonts.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAiDH,qGAAqG;AACrG,wBAAgB,kBAAkB,IAAI,IAAI,CAoBzC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load Google Fonts from the theme's CSS variables (--font-sans, --font-serif, --font-mono) on the
|
|
3
|
+
* dev server, where no host injects the theme: mirrors app-render's font loading so the app looks
|
|
4
|
+
* the same as it does live.
|
|
5
|
+
*/
|
|
6
|
+
const buildFontCssUrl = (family, weights = ["400"]) => {
|
|
7
|
+
const encodedFamily = encodeURIComponent(family);
|
|
8
|
+
const weightsParam = weights.join(";");
|
|
9
|
+
return `https://fonts.googleapis.com/css2?family=${encodedFamily}:wght@${weightsParam}&display=swap`;
|
|
10
|
+
};
|
|
11
|
+
const loadGoogleFont = (family, weights = ["400", "700"]) => {
|
|
12
|
+
const href = buildFontCssUrl(family, weights);
|
|
13
|
+
if (document.querySelector(`link[href="${href}"]`)) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const link = document.createElement("link");
|
|
17
|
+
link.rel = "stylesheet";
|
|
18
|
+
link.href = href;
|
|
19
|
+
document.head.appendChild(link);
|
|
20
|
+
};
|
|
21
|
+
const SYSTEM_FONTS = [
|
|
22
|
+
"ui-sans-serif",
|
|
23
|
+
"ui-serif",
|
|
24
|
+
"ui-monospace",
|
|
25
|
+
"system-ui",
|
|
26
|
+
"sans-serif",
|
|
27
|
+
"serif",
|
|
28
|
+
"monospace",
|
|
29
|
+
"cursive",
|
|
30
|
+
"fantasy",
|
|
31
|
+
];
|
|
32
|
+
function extractFontFamily(fontFamilyValue) {
|
|
33
|
+
if (!fontFamilyValue) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const firstFont = fontFamilyValue.split(",")[0]?.trim();
|
|
37
|
+
if (!firstFont) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const cleanFont = firstFont.replace(/['"]/g, "");
|
|
41
|
+
if (SYSTEM_FONTS.includes(cleanFont.toLowerCase())) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return cleanFont;
|
|
45
|
+
}
|
|
46
|
+
const FONT_VARS = ["--font-sans", "--font-serif", "--font-mono", "--font-inter", "--font-inter-tight"];
|
|
47
|
+
/** Read the font variables off the document and load any non-system Google Font through a <link>. */
|
|
48
|
+
export function loadFontsFromTheme() {
|
|
49
|
+
if (typeof document === "undefined") {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const computed = getComputedStyle(document.documentElement);
|
|
53
|
+
const weights = ["400", "500", "600", "700"];
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
for (const varName of FONT_VARS) {
|
|
56
|
+
const family = extractFontFamily(computed.getPropertyValue(varName).trim());
|
|
57
|
+
if (family && !seen.has(family)) {
|
|
58
|
+
seen.add(family);
|
|
59
|
+
try {
|
|
60
|
+
loadGoogleFont(family, weights);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
console.warn("Failed to load Google font:", family, e);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side navigation keeps the document's scroll offset: pushState never resets it, and the
|
|
3
|
+
* declarative <BrowserRouter> has no <ScrollRestoration>, so a new page landed on from the bottom
|
|
4
|
+
* of the previous one would open already scrolled to its end. This applies the rule
|
|
5
|
+
* ScrollRestoration applies to new locations, narrowed on purpose:
|
|
6
|
+
*
|
|
7
|
+
* - Only a PATH change counts. Search/hash-only changes are left alone — tabs, filters and
|
|
8
|
+
* pagination update `?params` in place and must not jump the page.
|
|
9
|
+
* - Back/forward (POP) is left to the browser, which restores its own saved offset.
|
|
10
|
+
* - The boot location is never touched; a reload keeps whatever the browser restores.
|
|
11
|
+
* - A visible fragment target on the new path wins over the top of the page.
|
|
12
|
+
*
|
|
13
|
+
* Layout effect so the reset lands before paint (no one-frame flash of the old offset), and
|
|
14
|
+
* `behavior: "instant"` so an app-level `scroll-behavior: smooth` can't animate it.
|
|
15
|
+
*/
|
|
16
|
+
export declare const useScrollToTopOnPageChange: () => void;
|
|
17
|
+
//# sourceMappingURL=use-scroll-to-top-on-page-change.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-scroll-to-top-on-page-change.d.ts","sourceRoot":"","sources":["../../../src/app/hooks/use-scroll-to-top-on-page-change.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,0BAA0B,YAuBtC,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from "react";
|
|
2
|
+
import { NavigationType, useLocation, useNavigationType } from "react-router";
|
|
3
|
+
/**
|
|
4
|
+
* The element a `#fragment` points at, if it can be scrolled to. Null for no fragment, no such
|
|
5
|
+
* element, or one without a layout box (hidden): `scrollIntoView` would silently do nothing
|
|
6
|
+
* there, and the page must still end up somewhere deterministic. Malformed percent-encoding in
|
|
7
|
+
* a hand-typed URL must not throw out of a layout effect that sits above the app's error boundary.
|
|
8
|
+
*/
|
|
9
|
+
const getHashTarget = (hash) => {
|
|
10
|
+
if (!hash) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const target = document.getElementById(decodeURIComponent(hash.slice(1)));
|
|
15
|
+
return target?.getClientRects().length ? target : null;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Client-side navigation keeps the document's scroll offset: pushState never resets it, and the
|
|
23
|
+
* declarative <BrowserRouter> has no <ScrollRestoration>, so a new page landed on from the bottom
|
|
24
|
+
* of the previous one would open already scrolled to its end. This applies the rule
|
|
25
|
+
* ScrollRestoration applies to new locations, narrowed on purpose:
|
|
26
|
+
*
|
|
27
|
+
* - Only a PATH change counts. Search/hash-only changes are left alone — tabs, filters and
|
|
28
|
+
* pagination update `?params` in place and must not jump the page.
|
|
29
|
+
* - Back/forward (POP) is left to the browser, which restores its own saved offset.
|
|
30
|
+
* - The boot location is never touched; a reload keeps whatever the browser restores.
|
|
31
|
+
* - A visible fragment target on the new path wins over the top of the page.
|
|
32
|
+
*
|
|
33
|
+
* Layout effect so the reset lands before paint (no one-frame flash of the old offset), and
|
|
34
|
+
* `behavior: "instant"` so an app-level `scroll-behavior: smooth` can't animate it.
|
|
35
|
+
*/
|
|
36
|
+
export const useScrollToTopOnPageChange = () => {
|
|
37
|
+
const { pathname, hash } = useLocation();
|
|
38
|
+
const navigationType = useNavigationType();
|
|
39
|
+
const previousPathnameRef = useRef(pathname);
|
|
40
|
+
useLayoutEffect(() => {
|
|
41
|
+
if (pathname === previousPathnameRef.current) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
previousPathnameRef.current = pathname;
|
|
45
|
+
if (navigationType === NavigationType.Pop) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const hashTarget = getHashTarget(hash);
|
|
49
|
+
if (hashTarget) {
|
|
50
|
+
hashTarget.scrollIntoView({ behavior: "instant", block: "start" });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
54
|
+
}, [pathname, hash, navigationType]);
|
|
55
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type ComponentType, type ReactNode } from "react";
|
|
2
|
+
import type { RouteObject } from "react-router";
|
|
3
|
+
import type { User } from "../ClientSdk.js";
|
|
4
|
+
import type { AppType, ThemeMode } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* The host contract: what the page hosting a Blocks app tells the app's tree, how it extends the
|
|
7
|
+
* tree, and what the tree reports back. Blocks' own hosting (app-render: a browser tab, the
|
|
8
|
+
* builder's canvas, the mobile app) implements it; so can a page of your own. `BlocksApp` reads one
|
|
9
|
+
* module-level store, so the state reaches the tree whichever root renders it, and survives a bundle
|
|
10
|
+
* swap: the host sets it before importing a bundle and never tears it down. The dev server has no
|
|
11
|
+
* host page; there the tree hosts itself (`dev-server/bootstrap.ts`).
|
|
12
|
+
*/
|
|
13
|
+
/** What the host knows, kept current as it changes; a flag left out is off. */
|
|
14
|
+
export interface BlocksHostState {
|
|
15
|
+
/** The platform's origins, for the login and the app lookups. */
|
|
16
|
+
config: {
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
googleBaseUrl: string;
|
|
19
|
+
};
|
|
20
|
+
app?: AppType;
|
|
21
|
+
appVersion?: number;
|
|
22
|
+
token?: string;
|
|
23
|
+
/**
|
|
24
|
+
* The host hands the token over after boot (it mints one for its own signed-in user): the client
|
|
25
|
+
* waits for it, rather than signing in anonymously first and again once it lands.
|
|
26
|
+
*/
|
|
27
|
+
awaitsToken?: boolean;
|
|
28
|
+
/** The current user as the host fetched it for this boot, taken instead of `authenticate()`. */
|
|
29
|
+
bootUser?: Omit<User, "isAuthenticated"> | null;
|
|
30
|
+
/** The host's own app lookup failed with no token to drop: the viewer must sign in. */
|
|
31
|
+
hasAppLoadFailed?: boolean;
|
|
32
|
+
/** The dev server with no token stored: sign in, where a live app would render signed out. */
|
|
33
|
+
requiresSignIn?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The host holds the app's routes until it has navigated to the page the app opens on (the
|
|
36
|
+
* builder's canvas boots on the app's root while it may still be loading its page list). Nothing
|
|
37
|
+
* renders meanwhile, so no page flashes and no page's data loads.
|
|
38
|
+
*/
|
|
39
|
+
routesHeld?: boolean;
|
|
40
|
+
themeMode: ThemeMode;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* How the host extends the tree, and what the tree tells it. Set once, before the tree renders. The
|
|
44
|
+
* layers render inside the app's own root, so they must use the same react-router, SDK and
|
|
45
|
+
* `@blocksdiy/blocks-client-sdk/app` modules the app does: a host that provides those modules to
|
|
46
|
+
* its bundles (as app-render does, on `window`) gets that for free.
|
|
47
|
+
*/
|
|
48
|
+
export interface BlocksHostExtensions {
|
|
49
|
+
/** Rendered inside the app's router, before the app: the host's own use of the router. */
|
|
50
|
+
RouterLayer?: ComponentType;
|
|
51
|
+
/** Wraps the app's routes, inside the SDK client: the host's own layer over the app. */
|
|
52
|
+
Shell?: ComponentType<{
|
|
53
|
+
children: ReactNode;
|
|
54
|
+
}>;
|
|
55
|
+
/**
|
|
56
|
+
* Shown where the viewer must sign in (an app the host could not load, an auth URL, the dev
|
|
57
|
+
* server without a token), in place of the tree's own login. `null`: the host keeps the viewer
|
|
58
|
+
* signed in, so the tree shows no sign-in screen, and an auth URL returns to the app's start.
|
|
59
|
+
*/
|
|
60
|
+
SignIn?: ComponentType | null;
|
|
61
|
+
/** The routes the tree resolved, for naming the page in error reports. */
|
|
62
|
+
onRoutesResolved?: (routes: RouteObject[] | undefined) => void;
|
|
63
|
+
/** The app's code is in and the app is on its page: once per load of the code. */
|
|
64
|
+
onReady?: () => void;
|
|
65
|
+
/** The app is on a page: the page's id (a route's id, a legacy page's) and its query. */
|
|
66
|
+
onPageChange?: (page: {
|
|
67
|
+
pageId: string;
|
|
68
|
+
searchParams: string;
|
|
69
|
+
}) => void;
|
|
70
|
+
/** A URL the app has no page for. */
|
|
71
|
+
onMissingPage?: (missing: {
|
|
72
|
+
url: string;
|
|
73
|
+
pages: {
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
}[];
|
|
77
|
+
}) => void;
|
|
78
|
+
/** A render error the tree caught (it renders nothing in its place), or code with no page to show. */
|
|
79
|
+
onAppError?: (report: {
|
|
80
|
+
error: Error;
|
|
81
|
+
componentStack?: string;
|
|
82
|
+
blockId?: string;
|
|
83
|
+
}) => void;
|
|
84
|
+
/** The app asked to sign out (`/auth/logout`) where the host keeps the viewer signed in. */
|
|
85
|
+
onSignOut?: () => void;
|
|
86
|
+
}
|
|
87
|
+
export declare const THEME_MODE_STORAGE_KEY = "app-theme-mode";
|
|
88
|
+
/** The theme mode to start from: the viewer's stored choice, else the app's default. */
|
|
89
|
+
export declare const initialThemeMode: (app: AppType | undefined) => ThemeMode;
|
|
90
|
+
export declare const setBlocksHost: (next: BlocksHostState) => void;
|
|
91
|
+
export declare const updateBlocksHost: (patch: Partial<BlocksHostState>) => void;
|
|
92
|
+
export declare const getBlocksHostState: () => BlocksHostState | null;
|
|
93
|
+
export declare const setBlocksHostExtensions: (next: BlocksHostExtensions) => void;
|
|
94
|
+
export declare const getBlocksHostExtensions: () => BlocksHostExtensions;
|
|
95
|
+
/** The viewer's choice, or the host's: stored, applied to the document, and pushed to the tree. */
|
|
96
|
+
export declare const setThemeMode: (themeMode: ThemeMode) => void;
|
|
97
|
+
/**
|
|
98
|
+
* A value derived from the host state: the component re-renders only when that value changes. The
|
|
99
|
+
* value must stay the same between updates that don't change it: a field, or a primitive.
|
|
100
|
+
*/
|
|
101
|
+
export declare const useBlocksHostSelector: <T>(select: (state: BlocksHostState) => T) => T;
|
|
102
|
+
/** One field of the host state, re-rendering only when that field changes. */
|
|
103
|
+
export declare const useBlocksHostField: <K extends keyof BlocksHostState>(key: K) => BlocksHostState[K];
|
|
104
|
+
/** Tests only: forget the host between cases. */
|
|
105
|
+
export declare const resetBlocksHostForTests: () => void;
|
|
106
|
+
//# sourceMappingURL=host.d.ts.map
|