@texturehq/edges-explore
v0.1.0
Published
Public Explore framework primitives for map, hybrid, list, and info-panel experiences built on Edges.
Keywords
Readme
@texturehq/edges-explore
Curated primitives for map / list / detail "Explore" experiences built on Edges.
This package is the shared interaction model that dashboard Explore and CommonGrid Explore will eventually consume. Primitives are promoted one at a time through the lab canvas at apps/edges/lab/explore-framework; only promoted primitives ship on the public surface.
Status
ExplorePanel is the first curated primitive. Other modules in src/ (layers, layout, modes, selection, states, tooltips, dashboard-compat) are reference material being refined in the canvas — they are not exposed on the package's public exports yet. Promoting a primitive means: graduate it in the canvas, add a manifest entry in src/manifest.ts, and re-add its subpath in package.json + tsup.config.ts.
Installation
{
"dependencies": {
"@texturehq/edges-explore": "workspace:*"
}
}Usage
For a full-page Explore experience with deep linking + browser back/forward, use useUrlExploreRouteStack — the in-memory stack is the source of truth, with the URL as its persisted form. The URL only feeds the stack at mount and on popstate; user-initiated mutations flow stack → URL:
import { ExplorePanel, useUrlExploreRouteStack } from "@texturehq/edges-explore";
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
type Route =
| { type: "overview"; id: "overview" }
| { type: "site"; id: string; payload: { siteId: string } };
const parse = (params: URLSearchParams): Route[] => {
const stack: Route[] = [{ type: "overview", id: "overview" }];
const siteId = params.get("siteId");
if (siteId) stack.push({ type: "site", id: `site:${siteId}`, payload: { siteId } });
return stack;
};
const serialize = (routes: Route[]): URLSearchParams => {
const params = new URLSearchParams();
for (const route of routes) {
if (route.type === "site") params.set("siteId", route.payload.siteId);
}
return params;
};
function ExploreSidebar() {
const router = useRouter();
const searchParams = useSearchParams();
const stack = useUrlExploreRouteStack<Route>({
parse: useCallback(parse, []),
serialize: useCallback(serialize, []),
initialSearch: searchParams?.toString(),
});
const [expanded, setExpanded] = useState(false);
// Sync stack changes back to the URL.
useEffect(() => {
const next = stack.serializedSearch;
const current = window.location.search.replace(/^\?/, "");
if (next !== current) router.replace(next ? `?${next}` : window.location.pathname, { scroll: false });
}, [stack.serializedSearch, router]);
const routeLabel = (route: Route) =>
route.type === "overview" ? "Overview" : `Site ${route.payload.siteId}`;
return (
<ExplorePanel
current={stack.current}
previous={stack.previous}
getRouteLabel={routeLabel}
onBack={stack.back}
onClose={() => stack.replace(null)}
isExpanded={expanded}
onToggleExpanded={() => setExpanded((v) => !v)}
>
<RouteBody route={stack.current} />
</ExplorePanel>
);
}For embedded contexts (modals, canvas demos, etc.) where there's no URL to bind to, use useExploreRouteStack instead — same controller shape, in-memory state, accepts an initialRoute.
The panel is a flex sibling — wrap it next to your map column for push behavior (map shrinks to make room), or render it absolutely-positioned over the map for overlay. That choice belongs to the parent layout, not the primitive.
Public exports
| Subpath | Exports | Purpose |
| --- | --- | --- |
| . | re-exports of ./panel and ./routes | Convenience barrel |
| ./panel | ExplorePanel, ExplorePanelProps, width constants | Right-edge info panel with route-stack header and animated transitions |
| ./routes | ExploreRouteDescriptor, useUrlExploreRouteStack (URL-backed), useExploreRouteStack (in-memory), pure reducers (pushExploreRoute, pushDeeperExploreRoute, popExploreRoute, replaceExploreRoute) | Headless route-stack primitives the panel drives off |
| ./manifest | explorePrimitivesManifest, ExplorePrimitiveEntry | Prompt/tool-friendly catalog of what's promoted |
Dependency rules
@texturehq/edges-explore may depend on @texturehq/edges, React, and small package-local helpers. It must not depend on:
apps/dashboard- CommonGrid app code
@texturehq/edges-entity-surfaces- private API clients or app data hooks
If a primitive needs to know what a "site", "device", "alert", "utility", or "power plant" is, it does not belong in this package. Pass that content through slots or keep it in an entity-surface package/app.
Scripts
yarn lint
yarn typecheck
yarn test
yarn build