@hansenexus/hud 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +253 -9
  2. package/dist/access-CNvJOwKM.d.ts +54 -0
  3. package/dist/capabilities-CjulXFQX.js +189 -0
  4. package/dist/define-hud-BBzsSl8b.d.ts +36 -0
  5. package/dist/env-DfxpPfs9.js +28 -0
  6. package/dist/env-vEjyJfPW.d.ts +17 -0
  7. package/dist/force-state-BaqWsmrf.js +153 -0
  8. package/dist/force-state-CCqoTfVB.d.ts +23 -0
  9. package/dist/handler-WmE4Lu7l.d.ts +39 -0
  10. package/dist/index-BYsdS-7d.d.ts +55 -0
  11. package/dist/index.d.ts +6 -39
  12. package/dist/index.js +980 -264
  13. package/dist/marker-C66LxOhJ.d.ts +9 -0
  14. package/dist/marker-CMrvxPmS.d.ts +12 -0
  15. package/dist/marker-DjeGvL-F.js +9 -0
  16. package/dist/marker-uZYPOJ4M.js +12 -0
  17. package/dist/markers.d.ts +3 -0
  18. package/dist/markers.js +3 -0
  19. package/dist/next.d.ts +33 -0
  20. package/dist/next.js +52 -0
  21. package/dist/payload-CLKDVZWl.d.ts +68 -0
  22. package/dist/plugins/access/server.d.ts +122 -0
  23. package/dist/plugins/access/server.js +339 -0
  24. package/dist/plugins/access.d.ts +41 -0
  25. package/dist/plugins/access.js +250 -0
  26. package/dist/plugins/agent/server.d.ts +129 -0
  27. package/dist/plugins/agent/server.js +429 -0
  28. package/dist/plugins/agent.d.ts +37 -0
  29. package/dist/plugins/agent.js +411 -0
  30. package/dist/plugins/design.d.ts +125 -0
  31. package/dist/plugins/design.js +619 -0
  32. package/dist/plugins/observe.d.ts +64 -0
  33. package/dist/plugins/observe.js +617 -0
  34. package/dist/plugins/ops/server.d.ts +82 -0
  35. package/dist/plugins/ops/server.js +380 -0
  36. package/dist/plugins/ops.d.ts +26 -0
  37. package/dist/plugins/ops.js +362 -0
  38. package/dist/plugins/vitals.d.ts +25 -0
  39. package/dist/plugins/vitals.js +26 -0
  40. package/dist/plugins-CQ_0w1ot.d.ts +114 -0
  41. package/dist/plugins-DVy_vaYr.js +70 -0
  42. package/dist/review.d.ts +181 -0
  43. package/dist/review.js +1017 -0
  44. package/dist/selector-NSYSe7AZ.js +28 -0
  45. package/dist/server.d.ts +3 -0
  46. package/dist/server.js +106 -0
  47. package/dist/shortcut-U6ftVY06.js +222 -0
  48. package/dist/state-boundary.d.ts +26 -0
  49. package/dist/state-boundary.js +61 -0
  50. package/dist/styles--slk86ZQ.js +332 -0
  51. package/dist/types-DADBRiGk.d.ts +50 -0
  52. package/dist/types-QJUajwWq.js +35 -0
  53. package/dist/types-f3jlNxzw.d.ts +92 -0
  54. package/dist/vitals-panel-Dvc5dnR-.js +87 -0
  55. package/package.json +36 -3
@@ -0,0 +1,28 @@
1
+ //#region src/core/grabber/selector.ts
2
+ /**
3
+ * `nth-of-type` path from `root` down to `el`. Ported from hn-monorepo
4
+ * `review-kit/anchor`, where console's design comments and the old HUD
5
+ * grabber shared it.
6
+ */
7
+ function buildAnchorSelector(el, root) {
8
+ const segments = [];
9
+ let node = el;
10
+ while (node && node !== root) {
11
+ const tag = node.tagName.toLowerCase();
12
+ const parent = node.parentElement;
13
+ if (!parent) break;
14
+ const siblings = [...parent.children].filter((c) => c.tagName === node?.tagName);
15
+ const index = siblings.indexOf(node) + 1;
16
+ segments.unshift(siblings.length > 1 ? `${tag}:nth-of-type(${index})` : tag);
17
+ node = parent;
18
+ }
19
+ return segments.join(" > ");
20
+ }
21
+ /** Human label such as `button “Termin buchen”`. */
22
+ function anchorLabelFor(el) {
23
+ const text = (el.textContent ?? "").trim().replace(/\s+/g, " ");
24
+ const tag = el.tagName.toLowerCase();
25
+ return text ? `${tag} “${text.slice(0, 32)}${text.length > 32 ? "…" : ""}”` : tag;
26
+ }
27
+ //#endregion
28
+ export { buildAnchorSelector as n, anchorLabelFor as t };
@@ -0,0 +1,3 @@
1
+ import { a as HudHttpMethod, d as HudRoutes, l as HudRouteContext, o as HudPlugin, p as definePlugin, u as HudRouteHandler } from "./plugins-CQ_0w1ot.js";
2
+ import { a as createHudHandler, i as HudRequestHandler, n as HudHandler, o as developmentOnly, r as HudHandlerOptions, t as HudGuard } from "./handler-WmE4Lu7l.js";
3
+ export { type HudGuard, type HudHandler, type HudHandlerOptions, type HudHttpMethod, type HudPlugin, type HudRequestHandler, type HudRouteContext, type HudRouteHandler, type HudRoutes, createHudHandler, definePlugin, developmentOnly };
package/dist/server.js ADDED
@@ -0,0 +1,106 @@
1
+ import { a as resolvePlugins, i as definePlugin, t as HUD_HTTP_METHODS } from "./plugins-DVy_vaYr.js";
2
+ //#region src/server/handler.ts
3
+ function json(status, body, headers) {
4
+ const res = Response.json(body, { status });
5
+ if (headers) for (const [key, value] of new Headers(headers)) res.headers.set(key, value);
6
+ return res;
7
+ }
8
+ function notFound() {
9
+ return json(404, {
10
+ ok: false,
11
+ error: "not found"
12
+ });
13
+ }
14
+ /**
15
+ * The default guard. Core assumes development, so a deployed build that
16
+ * mounts the route without an `access` guard serves nothing and reveals
17
+ * nothing about which plugins exist.
18
+ */
19
+ const developmentOnly = () => {
20
+ return (typeof process === "undefined" ? void 0 : process.env.NODE_ENV) === "development" ? null : notFound();
21
+ };
22
+ function splitPath(path) {
23
+ return path.split("/").filter(Boolean);
24
+ }
25
+ function compile(plugin) {
26
+ return Object.entries(plugin.routes ?? {}).map(([path, methods]) => ({
27
+ segments: splitPath(path),
28
+ methods
29
+ }));
30
+ }
31
+ function match(route, segments) {
32
+ if (route.segments.length !== segments.length) return null;
33
+ const params = {};
34
+ for (const [i, pattern] of route.segments.entries()) {
35
+ const segment = segments[i] ?? "";
36
+ if (!pattern.startsWith(":")) {
37
+ if (pattern !== segment) return null;
38
+ continue;
39
+ }
40
+ try {
41
+ params[pattern.slice(1)] = decodeURIComponent(segment);
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ return params;
47
+ }
48
+ function isMethod(method) {
49
+ return HUD_HTTP_METHODS.includes(method);
50
+ }
51
+ /**
52
+ * Serves every plugin's routes from one catch-all route
53
+ * (`app/api/hud/[...hud]/route.ts` in Next). A request to
54
+ * `<basePath>/<plugin id>/<path>` reaches that plugin's `routes[path][method]`;
55
+ * unknown plugin or path answers 404, a known path with another method 405.
56
+ *
57
+ * Pass the server halves (`@hansenexus/hud/plugins/<id>/server`): they carry
58
+ * `routes` and no React.
59
+ */
60
+ function createHudHandler(plugins, options = {}) {
61
+ const basePath = splitPath(options.basePath ?? "/api/hud");
62
+ const guard = options.guard ?? developmentOnly;
63
+ const table = new Map(resolvePlugins(plugins, options.can ?? (() => true)).map((plugin) => [plugin.id, {
64
+ plugin,
65
+ routes: compile(plugin)
66
+ }]));
67
+ const handle = async (request) => {
68
+ const segments = splitPath(new URL(request.url).pathname);
69
+ if (!basePath.every((segment, i) => segments[i] === segment)) return notFound();
70
+ const [pluginId, ...rest] = segments.slice(basePath.length);
71
+ const entry = pluginId === void 0 ? void 0 : table.get(pluginId);
72
+ const refusal = await guard(request, entry?.plugin.capability);
73
+ if (refusal) return refusal;
74
+ if (!entry) return notFound();
75
+ for (const route of entry.routes) {
76
+ const params = match(route, rest);
77
+ if (!params) continue;
78
+ const method = request.method === "HEAD" ? "GET" : request.method;
79
+ const fn = isMethod(method) ? route.methods[method] : void 0;
80
+ if (!fn) return json(405, {
81
+ ok: false,
82
+ error: "method not allowed"
83
+ }, { allow: HUD_HTTP_METHODS.filter((m) => route.methods[m]).join(", ") });
84
+ try {
85
+ return await fn(request, { params });
86
+ } catch (error) {
87
+ console.error(`hud: ${entry.plugin.id} ${request.method} /${rest.join("/")} failed`, error);
88
+ return json(500, {
89
+ ok: false,
90
+ error: "plugin route failed"
91
+ });
92
+ }
93
+ }
94
+ return notFound();
95
+ };
96
+ return {
97
+ handle,
98
+ GET: handle,
99
+ POST: handle,
100
+ PUT: handle,
101
+ PATCH: handle,
102
+ DELETE: handle
103
+ };
104
+ }
105
+ //#endregion
106
+ export { createHudHandler, definePlugin, developmentOnly };
@@ -0,0 +1,222 @@
1
+ import { n as buildAnchorSelector, t as anchorLabelFor } from "./selector-NSYSe7AZ.js";
2
+ import { useSyncExternalStore } from "react";
3
+ //#region src/core/grabber/store.ts
4
+ const IDLE = {
5
+ grabbing: false,
6
+ payload: null
7
+ };
8
+ function createGrabber() {
9
+ let state = IDLE;
10
+ const listeners = /* @__PURE__ */ new Set();
11
+ const set = (next) => {
12
+ if (next.grabbing === state.grabbing && next.payload === state.payload) return;
13
+ state = next;
14
+ for (const listener of listeners) listener();
15
+ };
16
+ return {
17
+ start: () => set({
18
+ ...state,
19
+ grabbing: true
20
+ }),
21
+ cancel: () => set({
22
+ ...state,
23
+ grabbing: false
24
+ }),
25
+ clear: () => set({
26
+ ...state,
27
+ payload: null
28
+ }),
29
+ complete: (payload) => set({
30
+ grabbing: false,
31
+ payload
32
+ }),
33
+ getState: () => state,
34
+ subscribe: (listener) => {
35
+ listeners.add(listener);
36
+ return () => listeners.delete(listener);
37
+ }
38
+ };
39
+ }
40
+ /** Subscribes a component to a HUD's grab state: `useGrab(ctx.grab)`. */
41
+ function useGrab(grab) {
42
+ return useSyncExternalStore(grab.subscribe, grab.getState, grab.getState);
43
+ }
44
+ //#endregion
45
+ //#region src/core/grabber/inspect.ts
46
+ const MAX_OWNERS = 5;
47
+ /** React's internal fiber key on a DOM node: `__reactFiber$<random>`. */
48
+ function fiberOf(el) {
49
+ for (const key of Object.keys(el)) if (key.startsWith("__reactFiber$")) {
50
+ const fiber = el[key];
51
+ return fiber && typeof fiber === "object" ? fiber : null;
52
+ }
53
+ return null;
54
+ }
55
+ /** Display name of a component fiber's type; null for host elements, fragments, providers. */
56
+ function componentName(type) {
57
+ if (typeof type === "function") {
58
+ const fn = type;
59
+ const name = typeof fn.displayName === "string" ? fn.displayName : fn.name;
60
+ return typeof name === "string" && name ? name : null;
61
+ }
62
+ if (type && typeof type === "object") {
63
+ const wrapper = type;
64
+ if (typeof wrapper.displayName === "string" && wrapper.displayName) return wrapper.displayName;
65
+ return componentName(wrapper.render) ?? componentName(wrapper.type);
66
+ }
67
+ return null;
68
+ }
69
+ /** Internal frames between the caller and React: the JSX runtime, React itself, bundler glue. */
70
+ const INTERNAL_FRAME = /react-stack-top-frame|jsx-?dev|jsx-runtime|\/react(-dom)?[./@]|node_modules/;
71
+ /**
72
+ * Turns a frame location such as `http://localhost:5173/src/site.tsx?t=1:12:5`
73
+ * or `webpack-internal:///(app-pages-browser)/./src/hero.tsx:8:3` into
74
+ * `src/site.tsx:12:5`.
75
+ */
76
+ function cleanSourceLocation(location) {
77
+ const match = /^(.*?):(\d+)(?::(\d+))?$/.exec(location.trim());
78
+ if (!match) return location;
79
+ const [, rawPath = "", line, column] = match;
80
+ let path = rawPath.replace(/[?#].*$/, "");
81
+ const bundled = /^webpack-internal:\/\/\/(?:\([^)]*\)\/)?(.*)$/.exec(path);
82
+ const url = /^[a-z][a-z0-9+.-]*:\/\/[^/]*\/(.*)$/i.exec(path);
83
+ path = bundled?.[1] ?? url?.[1] ?? path;
84
+ path = path.replace(/^\.\//, "").replace(/^\/?@fs\//, "/");
85
+ if (!path) path = rawPath;
86
+ return column ? `${path}:${line}:${column}` : `${path}:${line}`;
87
+ }
88
+ /** First non-internal frame of a V8 or SpiderMonkey/JSC stack. */
89
+ function sourceFromStack(stack) {
90
+ for (const line of stack.split("\n").slice(1)) {
91
+ if (INTERNAL_FRAME.test(line)) continue;
92
+ const v8 = /^\s*at (?:.*? \()?(.+?)\)?$/.exec(line);
93
+ const gecko = /@(.+)$/.exec(line);
94
+ const location = v8?.[1] ?? gecko?.[1];
95
+ if (location && /:\d+(:\d+)?$/.test(location)) return cleanSourceLocation(location);
96
+ }
97
+ }
98
+ function sourceOf(fiber) {
99
+ const legacy = fiber._debugSource;
100
+ if (legacy?.fileName && legacy.lineNumber) return cleanSourceLocation(`${legacy.fileName}:${legacy.lineNumber}${legacy.columnNumber ? `:${legacy.columnNumber}` : ""}`);
101
+ const stack = fiber._debugStack?.stack;
102
+ return typeof stack === "string" ? sourceFromStack(stack) : void 0;
103
+ }
104
+ function inspectComponent(el) {
105
+ const host = fiberOf(el);
106
+ if (!host) return null;
107
+ const names = [];
108
+ let source = sourceOf(host);
109
+ for (let fiber = host.return; fiber && names.length <= MAX_OWNERS; fiber = fiber.return) {
110
+ const name = componentName(fiber.type);
111
+ if (!name) continue;
112
+ names.push(name);
113
+ source ??= sourceOf(fiber);
114
+ }
115
+ const [name, ...owners] = names;
116
+ if (!name) return null;
117
+ return source ? {
118
+ name,
119
+ owners,
120
+ source
121
+ } : {
122
+ name,
123
+ owners
124
+ };
125
+ }
126
+ //#endregion
127
+ //#region src/core/grabber/payload.ts
128
+ function pct(part, whole) {
129
+ return whole > 0 ? Number((part / whole * 100).toFixed(1)) : 0;
130
+ }
131
+ function buildGrabAnchor(el) {
132
+ const doc = el.ownerDocument;
133
+ const win = doc.defaultView;
134
+ const width = win?.innerWidth ?? 0;
135
+ const height = win?.innerHeight ?? 0;
136
+ const r = el.getBoundingClientRect();
137
+ return {
138
+ selector: buildAnchorSelector(el, doc.body),
139
+ label: anchorLabelFor(el),
140
+ tag: el.tagName.toLowerCase(),
141
+ rect: {
142
+ x: pct(r.left, width),
143
+ y: pct(r.top, height),
144
+ width: pct(r.width, width),
145
+ height: pct(r.height, height)
146
+ }
147
+ };
148
+ }
149
+ function buildGrabPayload(el, { app }) {
150
+ const win = el.ownerDocument.defaultView;
151
+ return {
152
+ app,
153
+ url: win?.location.href ?? "",
154
+ route: win?.location.pathname ?? "/",
155
+ anchor: buildGrabAnchor(el),
156
+ component: inspectComponent(el),
157
+ viewport: {
158
+ width: win?.innerWidth ?? 0,
159
+ height: win?.innerHeight ?? 0
160
+ },
161
+ grabbedAt: (/* @__PURE__ */ new Date()).toISOString()
162
+ };
163
+ }
164
+ /** The picker's hover label: component and source when React reports them, the tag otherwise. */
165
+ function describeElement(el) {
166
+ const tag = el.tagName.toLowerCase();
167
+ const component = inspectComponent(el);
168
+ if (!component) return tag;
169
+ const file = component.source?.split("/").pop()?.replace(/(:\d+)+$/, "");
170
+ return `<${component.name}> ${tag}${file ? ` · ${file}` : ""}`;
171
+ }
172
+ /**
173
+ * Scores manifest entries against the route and anchor label. Pure token
174
+ * overlap: a hint for the agent, not a resolution. Ported from the old HUD's
175
+ * `grabber/payload.ts`.
176
+ */
177
+ function matchComponentHints(hints, route, label, max = 5) {
178
+ if (!hints?.length) return [];
179
+ const tokens = `${route} ${label}`.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2);
180
+ if (tokens.length === 0) return hints.slice(0, max);
181
+ return hints.map((hint) => {
182
+ const haystack = `${hint.name} ${hint.path}`.toLowerCase();
183
+ return {
184
+ hint,
185
+ score: tokens.reduce((n, token) => haystack.includes(token) ? n + 1 : n, 0)
186
+ };
187
+ }).filter((h) => h.score > 0).sort((a, b) => b.score - a.score).slice(0, max).map((h) => h.hint);
188
+ }
189
+ //#endregion
190
+ //#region src/shell/commands/shortcut.ts
191
+ function parseShortcut(spec) {
192
+ const parts = spec.toLowerCase().split("+");
193
+ return {
194
+ key: parts.at(-1) ?? "",
195
+ mod: parts.includes("mod"),
196
+ shift: parts.includes("shift"),
197
+ alt: parts.includes("alt")
198
+ };
199
+ }
200
+ function isApplePlatform() {
201
+ return typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent);
202
+ }
203
+ function matchesShortcut(event, shortcut, apple) {
204
+ if ((apple ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey) !== shortcut.mod) return false;
205
+ if (event.shiftKey !== shortcut.shift || event.altKey !== shortcut.alt) return false;
206
+ if (event.key.toLowerCase() === shortcut.key) return true;
207
+ if (/^[a-z]$/.test(shortcut.key)) return event.code === `Key${shortcut.key.toUpperCase()}`;
208
+ if (/^\d$/.test(shortcut.key)) return event.code === `Digit${shortcut.key}`;
209
+ return false;
210
+ }
211
+ function formatShortcut(shortcut, apple) {
212
+ const key = shortcut.key.toUpperCase();
213
+ if (apple) return `${shortcut.mod ? "⌘" : ""}${shortcut.alt ? "⌥" : ""}${shortcut.shift ? "⇧" : ""}${key}`;
214
+ return [
215
+ shortcut.mod && "Ctrl",
216
+ shortcut.alt && "Alt",
217
+ shortcut.shift && "Shift",
218
+ key
219
+ ].filter(Boolean).join("+");
220
+ }
221
+ //#endregion
222
+ export { buildGrabPayload as a, createGrabber as c, parseShortcut as i, useGrab as l, isApplePlatform as n, describeElement as o, matchesShortcut as r, matchComponentHints as s, formatShortcut as t };
@@ -0,0 +1,26 @@
1
+ import { i as ForcedState } from "./force-state-CCqoTfVB.js";
2
+ import { ReactNode } from "react";
3
+ //#region src/plugins/design/state-boundary.d.ts
4
+ export interface StateBoundaryProps {
5
+ /** Stable, page-unique id; the HUD States panel lists boundaries by it. */
6
+ id: string;
7
+ /** Renders the boundary; `forced` is the state to show instead of the real one, or null. */
8
+ children: (forced: ForcedState | null) => ReactNode;
9
+ }
10
+ /**
11
+ * Registers a loading/empty/error boundary with the HUD States panel
12
+ * (`forceState()` from `@hansenexus/hud/plugins/design`) and `?__state=`.
13
+ * Wrap the part of a page that branches on a query result:
14
+ *
15
+ * ```tsx
16
+ * <StateBoundary id="blog-post">
17
+ * {(forced) => forced === "loading" || post === undefined ? <Skeleton /> : …}
18
+ * </StateBoundary>
19
+ * ```
20
+ *
21
+ * Production-safe to import: outside development it renders `children(null)`
22
+ * and nothing else.
23
+ */
24
+ export declare function StateBoundary({ id, children }: StateBoundaryProps): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined> | null | undefined;
25
+ //#endregion
26
+ export type { ForcedState };
@@ -0,0 +1,61 @@
1
+ import { a as getForceStateSnapshot, c as subscribeForceState, o as getForceStateStore, s as resolveForcedState } from "./force-state-BaqWsmrf.js";
2
+ import { createContext, useContext, useEffect, useSyncExternalStore } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/plugins/design/state-boundary.tsx
5
+ /**
6
+ * Build-time gate. A literal once the consumer's bundler inlines NODE_ENV, so
7
+ * a production build folds `StateBoundary` to `children(null)` and drops
8
+ * everything below, the store included: no production code path reads
9
+ * `?__state=`.
10
+ */
11
+ const FORCE_STATE_ENABLED = process.env.NODE_ENV === "development";
12
+ /**
13
+ * Registers a loading/empty/error boundary with the HUD States panel
14
+ * (`forceState()` from `@hansenexus/hud/plugins/design`) and `?__state=`.
15
+ * Wrap the part of a page that branches on a query result:
16
+ *
17
+ * ```tsx
18
+ * <StateBoundary id="blog-post">
19
+ * {(forced) => forced === "loading" || post === undefined ? <Skeleton /> : …}
20
+ * </StateBoundary>
21
+ * ```
22
+ *
23
+ * Production-safe to import: outside development it renders `children(null)`
24
+ * and nothing else.
25
+ */
26
+ function StateBoundary({ id, children }) {
27
+ if (!FORCE_STATE_ENABLED) return children(null);
28
+ return /* @__PURE__ */ jsx(DevStateBoundary, {
29
+ id,
30
+ children
31
+ });
32
+ }
33
+ /** The enclosing boundary's id, so the store knows which boundaries are top-level. */
34
+ const ParentBoundary = /* @__PURE__ */ createContext(null);
35
+ const SERVER_SNAPSHOT = {
36
+ boundaries: [],
37
+ overrides: {},
38
+ param: null
39
+ };
40
+ function getServerSnapshot() {
41
+ return SERVER_SNAPSHOT;
42
+ }
43
+ function DevStateBoundary({ id, children }) {
44
+ const parentId = useContext(ParentBoundary);
45
+ const snapshot = useSyncExternalStore(subscribeForceState, getForceStateSnapshot, getServerSnapshot);
46
+ useEffect(() => {
47
+ const store = getForceStateStore();
48
+ store.syncSearch(window.location.search);
49
+ return store.register(id, parentId);
50
+ }, [id, parentId]);
51
+ const forced = resolveForcedState(snapshot, {
52
+ id,
53
+ parentId
54
+ });
55
+ return /* @__PURE__ */ jsx(ParentBoundary.Provider, {
56
+ value: id,
57
+ children: children(forced)
58
+ });
59
+ }
60
+ //#endregion
61
+ export { StateBoundary };