Sign In

@aspicio/react

Package Overview
Dependencies
Maintainers
1
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@aspicio/react - npm Package Compare versions

Comparing version
0.1.0
to
0.2.0
+25
-5
dist/index.d.mts

@@ -23,2 +23,8 @@ import { CSSProperties, ReactElement } from "react";

onViewer?: (viewer: DxfViewer | null) => void;
/**
* When provided, the canvas hit-tests the layer under the cursor on hover,
* highlights it, and reports its name (or null). DxfEmbed uses this to
* reverse-highlight the matching layer-panel row.
*/
onHoverLayer?: (layer: string | null) => void;
}

@@ -41,2 +47,3 @@ /**

readonly panel: "#191c22";
readonly panel2: "#1f232b";
readonly hover: "rgba(255,255,255,.055)";

@@ -49,2 +56,6 @@ readonly hairline: "#282c34";

readonly crease: "#4c8dff";
readonly creasedim: "rgba(76,141,255,.16)";
readonly amber: "#e0a82e";
readonly amberdim: "rgba(224,168,46,.16)";
readonly amberborder: "rgba(224,168,46,.4)";
readonly fontSans: '"IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif';

@@ -85,2 +96,9 @@ readonly fontMono: '"IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace';

theme?: DxfTheme;
/**
* Layer currently hovered on the canvas (from DxfPreview's onHoverLayer).
* Its row is reverse-highlighted. DxfEmbed wires this automatically.
*/
reverseHighlightLayer?: string | null;
/** Show the gesture-hints footer (themed mode only). Default: true. */
hints?: boolean;
className?: string;

@@ -90,7 +108,7 @@ style?: CSSProperties;

/**
* Ready-made layer list for an embedded viewer: visibility checkboxes,
* effective-color swatches, entity counts, and hover-to-highlight. Styled
* like the Aspicio demo app by default (fonts degrade to system faces
* unless the host loads IBM Plex); pass theme="none" to inherit the host's
* look instead.
* Ready-made layer list for an embedded viewer, matching the Aspicio demo
* app: header with layer count, visibility checkboxes, effective-color
* swatches, entity counts, hover-to-highlight, double-click-to-solo (with a
* banner), canvas-hover reverse-highlight, and a gesture-hints footer. Pass
* theme="none" for a minimal list that inherits the host's styling.
*/

@@ -100,2 +118,4 @@ declare function DxfLayerPanel({

theme,
reverseHighlightLayer,
hints,
className,

@@ -102,0 +122,0 @@ style

@@ -10,2 +10,3 @@ import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";

panel: "#191c22",
panel2: "#1f232b",
hover: "rgba(255,255,255,.055)",

@@ -18,2 +19,6 @@ hairline: "#282c34",

crease: "#4c8dff",
creasedim: "rgba(76,141,255,.16)",
amber: "#e0a82e",
amberdim: "rgba(224,168,46,.16)",
amberborder: "rgba(224,168,46,.4)",
fontSans: "\"IBM Plex Sans\", system-ui, -apple-system, \"Segoe UI\", sans-serif",

@@ -35,22 +40,59 @@ fontMono: "\"IBM Plex Mono\", ui-monospace, \"SF Mono\", Menlo, monospace"

//#region src/DxfLayerPanel.tsx
const baseRow = {
display: "flex",
alignItems: "center",
gap: "0.6em",
padding: "0.35em 0.5em",
borderRadius: 3,
cursor: "pointer",
userSelect: "none"
};
const DESKTOP_HINTS = [
["DRAG", "pan"],
["SCROLL", "zoom"],
["⇧+DRAG", "rotate"],
["2×CLICK", "fit to view"],
["HOVER", "highlight layer"],
["2×CLICK ROW", "solo layer"]
];
const CheckIcon = () => /* @__PURE__ */ jsxs("svg", {
width: 16,
height: 16,
viewBox: "0 0 16 16",
"aria-hidden": true,
children: [/* @__PURE__ */ jsx("rect", {
x: 1.5,
y: 1.5,
width: 13,
height: 13,
rx: 2.5,
fill: aspicioTokens.crease
}), /* @__PURE__ */ jsx("path", {
d: "M4.4 8.2 L6.9 10.6 L11.6 5.3",
fill: "none",
stroke: "#fff",
strokeWidth: 1.7,
strokeLinecap: "round",
strokeLinejoin: "round"
})]
});
const UncheckIcon = () => /* @__PURE__ */ jsx("svg", {
width: 16,
height: 16,
viewBox: "0 0 16 16",
"aria-hidden": true,
children: /* @__PURE__ */ jsx("rect", {
x: 1.5,
y: 1.5,
width: 13,
height: 13,
rx: 2.5,
fill: "none",
stroke: aspicioTokens.hairline2,
strokeWidth: 1.4
})
});
/**
* Ready-made layer list for an embedded viewer: visibility checkboxes,
* effective-color swatches, entity counts, and hover-to-highlight. Styled
* like the Aspicio demo app by default (fonts degrade to system faces
* unless the host loads IBM Plex); pass theme="none" to inherit the host's
* look instead.
* Ready-made layer list for an embedded viewer, matching the Aspicio demo
* app: header with layer count, visibility checkboxes, effective-color
* swatches, entity counts, hover-to-highlight, double-click-to-solo (with a
* banner), canvas-hover reverse-highlight, and a gesture-hints footer. Pass
* theme="none" for a minimal list that inherits the host's styling.
*/
function DxfLayerPanel({ viewer, theme = "aspicio", className, style }) {
function DxfLayerPanel({ viewer, theme = "aspicio", reverseHighlightLayer = null, hints = true, className, style }) {
const [layers, setLayers] = useState([]);
const [soloLayer, setSoloLayer] = useState(null);
const [hoveredRow, setHoveredRow] = useState(null);
const [, bump] = useState(0);
const [, forceRender] = useState(0);
const themed = theme === "aspicio";

@@ -62,3 +104,6 @@ useEffect(() => {

}
const sync = () => setLayers(viewer.getLayers());
const sync = () => {
setSoloLayer(null);
setLayers(viewer.getLayers());
};
sync();

@@ -68,20 +113,34 @@ viewer.on("loaded", sync);

}, [viewer]);
const toggle = (layer) => {
viewer?.setLayerVisible(layer.name, !layer.visible);
bump((n) => n + 1);
const tick = () => forceRender((n) => n + 1);
const isVisible = (layer) => soloLayer ? layer.name === soloLayer : layer.visible !== false;
const toggleLayer = (layer) => {
if (!viewer) return;
if (soloLayer) {
for (const l of layers) viewer.setLayerVisible(l.name, l.name === layer.name);
setSoloLayer(null);
} else viewer.setLayerVisible(layer.name, layer.visible === false);
tick();
};
return /* @__PURE__ */ jsx("ul", {
const toggleSolo = (layer) => {
if (!viewer) return;
const next = soloLayer === layer.name ? null : layer.name;
for (const l of layers) viewer.setLayerVisible(l.name, next ? l.name === next : true);
setSoloLayer(next);
tick();
};
const exitSolo = () => {
if (!viewer) return;
for (const l of layers) viewer.setLayerVisible(l.name, true);
setSoloLayer(null);
tick();
};
const hover = (name) => {
setHoveredRow(name);
viewer?.setLayerHighlight(name);
};
if (!themed) return /* @__PURE__ */ jsx("ul", {
className,
style: themed ? {
style: {
listStyle: "none",
margin: 0,
padding: "8px",
background: aspicioTokens.panel,
color: aspicioTokens.text,
fontFamily: aspicioTokens.fontSans,
fontSize: 13,
...style
} : {
listStyle: "none",
margin: 0,
padding: 0,

@@ -91,24 +150,23 @@ ...style

children: layers.map((layer) => {
const visible = isVisible(layer);
const color = `#${(layer.effectiveColors?.[0] ?? layer.color).toString(16).padStart(6, "0")}`;
return /* @__PURE__ */ jsxs("li", {
style: {
...baseRow,
opacity: layer.visible ? 1 : .5,
background: themed && hoveredRow === layer.name ? aspicioTokens.hover : void 0
display: "flex",
alignItems: "center",
gap: "0.6em",
padding: "0.35em 0.5em",
cursor: "pointer",
userSelect: "none",
opacity: visible ? 1 : .5
},
onMouseEnter: () => {
setHoveredRow(layer.name);
viewer?.setLayerHighlight(layer.name);
},
onMouseLeave: () => {
setHoveredRow(null);
viewer?.setLayerHighlight(null);
},
onMouseEnter: () => hover(layer.name),
onMouseLeave: () => hover(null),
onDoubleClick: () => toggleSolo(layer),
children: [
/* @__PURE__ */ jsx("input", {
type: "checkbox",
checked: layer.visible,
onChange: () => toggle(layer),
"aria-label": layer.name,
style: themed ? { accentColor: aspicioTokens.crease } : void 0
checked: visible,
onChange: () => toggleLayer(layer),
"aria-label": layer.name
}),

@@ -122,4 +180,4 @@ /* @__PURE__ */ jsx("span", {

flexShrink: 0,
background: layer.visible ? color : "transparent",
border: layer.visible ? "1px solid rgba(255,255,255,.25)" : `1px solid ${color}`
background: visible ? color : "transparent",
border: visible ? "1px solid rgba(128,128,128,.5)" : `1px solid ${color}`
}

@@ -133,4 +191,3 @@ }),

textOverflow: "ellipsis",
whiteSpace: "nowrap",
color: themed && !layer.visible ? aspicioTokens.text3 : void 0
whiteSpace: "nowrap"
},

@@ -140,7 +197,3 @@ children: layer.name

/* @__PURE__ */ jsx("span", {
style: themed ? {
color: aspicioTokens.text3,
fontFamily: aspicioTokens.fontMono,
fontSize: 11
} : {
style: {
opacity: .55,

@@ -155,2 +208,257 @@ fontSize: "0.85em"

});
const mono = aspicioTokens.fontMono;
return /* @__PURE__ */ jsxs("div", {
className,
style: {
display: "flex",
flexDirection: "column",
minHeight: 0,
height: "100%",
background: aspicioTokens.panel,
color: aspicioTokens.text,
fontFamily: aspicioTokens.fontSans,
fontSize: 13,
...style
},
children: [
/* @__PURE__ */ jsxs("div", {
style: {
display: "flex",
alignItems: "center",
gap: 8,
padding: "13px 14px 10px",
flexShrink: 0,
fontFamily: mono,
fontSize: 11,
letterSpacing: "0.14em",
color: aspicioTokens.text3
},
children: ["LAYERS", /* @__PURE__ */ jsx("span", {
style: {
fontSize: 10.5,
background: aspicioTokens.panel2,
border: `1px solid ${aspicioTokens.hairline}`,
borderRadius: 3,
padding: "1px 6px",
fontFeatureSettings: "\"tnum\" 1"
},
children: layers.length
})]
}),
soloLayer && /* @__PURE__ */ jsxs("div", {
style: {
margin: "0 12px 8px",
display: "flex",
alignItems: "center",
gap: 8,
padding: "7px 9px",
background: aspicioTokens.amberdim,
border: `1px solid ${aspicioTokens.amberborder}`,
borderRadius: 3
},
children: [
/* @__PURE__ */ jsx("span", {
style: {
fontFamily: mono,
fontSize: 10,
letterSpacing: "0.1em",
fontWeight: 600,
color: aspicioTokens.amber
},
children: "SOLO"
}),
/* @__PURE__ */ jsx("span", {
style: {
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
fontFamily: mono,
fontSize: 11.5,
color: aspicioTokens.text
},
children: soloLayer
}),
/* @__PURE__ */ jsx("button", {
type: "button",
onClick: exitSolo,
style: {
border: "none",
background: "transparent",
color: aspicioTokens.amber,
cursor: "pointer",
fontFamily: mono,
fontSize: 10,
letterSpacing: "0.08em",
padding: "2px 4px"
},
children: "EXIT"
})
]
}),
/* @__PURE__ */ jsxs("ul", {
style: {
listStyle: "none",
margin: 0,
padding: "0 8px",
flex: 1,
minHeight: 0,
overflowY: "auto"
},
children: [layers.length === 0 && /* @__PURE__ */ jsx("li", {
style: {
padding: "24px 10px",
textAlign: "center",
color: aspicioTokens.text3,
fontSize: 12.5,
lineHeight: 1.6
},
children: "No layers yet."
}), layers.map((layer) => {
const visible = isVisible(layer);
const isSolo = soloLayer === layer.name;
const dimmed = !!soloLayer && !isSolo;
const reverse = reverseHighlightLayer === layer.name;
const rowHover = hoveredRow === layer.name;
const color = `#${(layer.effectiveColors?.[0] ?? layer.color).toString(16).padStart(6, "0")}`;
return /* @__PURE__ */ jsxs("li", {
title: `${layer.name} · ${layer.entityCount} entities`,
onMouseEnter: () => hover(layer.name),
onMouseLeave: () => hover(null),
onDoubleClick: (e) => {
e.preventDefault();
toggleSolo(layer);
},
style: {
display: "flex",
alignItems: "center",
gap: 10,
padding: "8px 8px",
paddingLeft: isSolo ? 12 : 8,
borderRadius: 3,
cursor: "pointer",
userSelect: "none",
position: "relative",
minWidth: 0,
border: `1px solid ${reverse ? aspicioTokens.crease : isSolo ? aspicioTokens.amberborder : "transparent"}`,
background: reverse ? aspicioTokens.creasedim : isSolo ? aspicioTokens.amberdim : rowHover ? aspicioTokens.hover : "transparent",
opacity: dimmed ? .34 : visible || isSolo ? 1 : .55,
transition: "background 140ms, border-color 140ms, opacity 140ms"
},
children: [
isSolo && /* @__PURE__ */ jsx("span", {
"aria-hidden": true,
style: {
position: "absolute",
left: 0,
top: 3,
bottom: 3,
width: 2,
background: aspicioTokens.amber,
borderRadius: 2
}
}),
/* @__PURE__ */ jsx("span", {
role: "checkbox",
"aria-checked": visible,
"aria-label": layer.name,
tabIndex: 0,
onClick: (e) => {
e.stopPropagation();
toggleLayer(layer);
},
style: {
flexShrink: 0,
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: 20,
height: 20,
cursor: "pointer"
},
children: visible ? /* @__PURE__ */ jsx(CheckIcon, {}) : /* @__PURE__ */ jsx(UncheckIcon, {})
}),
/* @__PURE__ */ jsx("span", {
"aria-hidden": true,
style: {
width: 13,
height: 13,
borderRadius: 2,
flexShrink: 0,
background: visible ? color : "transparent",
border: visible ? "1px solid rgba(255,255,255,.25)" : `1px solid ${color}`
}
}),
/* @__PURE__ */ jsx("span", {
style: {
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
fontSize: 13,
color: visible || isSolo ? aspicioTokens.text : aspicioTokens.text3
},
children: layer.name
}),
isSolo && /* @__PURE__ */ jsx("span", {
style: {
fontFamily: mono,
fontSize: 8.5,
letterSpacing: "0.1em",
color: aspicioTokens.amber,
border: `1px solid ${aspicioTokens.amberborder}`,
borderRadius: 2,
padding: "1px 4px",
flexShrink: 0
},
children: "SOLO"
}),
/* @__PURE__ */ jsx("span", {
style: {
fontFamily: mono,
fontSize: 11,
color: aspicioTokens.text3,
flexShrink: 0,
fontFeatureSettings: "\"tnum\" 1"
},
children: layer.entityCount
})
]
}, layer.name);
})]
}),
hints && /* @__PURE__ */ jsx("div", {
style: {
flexShrink: 0,
padding: "11px 14px 13px",
borderTop: `1px solid ${aspicioTokens.hairline}`,
display: "grid",
gridTemplateColumns: "auto 1fr",
gap: "5px 10px",
alignItems: "baseline"
},
children: DESKTOP_HINTS.map(([k, v]) => /* @__PURE__ */ jsxs("div", {
style: { display: "contents" },
children: [/* @__PURE__ */ jsx("span", {
style: {
fontFamily: mono,
fontSize: 9.5,
letterSpacing: "0.06em",
color: aspicioTokens.text2,
whiteSpace: "nowrap"
},
children: k
}), /* @__PURE__ */ jsx("span", {
style: {
fontSize: 11,
color: aspicioTokens.text3
},
children: v
})]
}, k))
})
]
});
}

@@ -165,3 +473,3 @@ //#endregion

*/
const DxfPreview = forwardRef(function DxfPreview({ src, srcUrl, options, className, style, onLoaded, onError, onViewer }, ref) {
const DxfPreview = forwardRef(function DxfPreview({ src, srcUrl, options, className, style, onLoaded, onError, onViewer, onHoverLayer }, ref) {
const containerRef = useRef(null);

@@ -173,3 +481,4 @@ const [viewer, setViewer] = useState(null);

onError,
onViewer
onViewer,
onHoverLayer
});

@@ -179,3 +488,4 @@ callbacks.current = {

onError,
onViewer
onViewer,
onHoverLayer
};

@@ -213,2 +523,33 @@ const optionsKey = JSON.stringify(options ?? {});

]);
const hoverEnabled = onHoverLayer != null;
useEffect(() => {
const container = containerRef.current;
if (!viewer || !container || !hoverEnabled) return;
let queued = false;
let last = null;
const report = (layer) => {
if (layer === last) return;
last = layer;
viewer.setLayerHighlight(layer);
callbacks.current.onHoverLayer?.(layer);
};
const onMove = (e) => {
if (e.pointerType !== "mouse" || e.buttons !== 0 || queued) return;
queued = true;
const { clientX, clientY } = e;
requestAnimationFrame(() => {
queued = false;
const rect = container.getBoundingClientRect();
report(viewer.pickLayer(clientX - rect.left, clientY - rect.top));
});
};
const onLeave = () => report(null);
container.addEventListener("pointermove", onMove);
container.addEventListener("pointerleave", onLeave);
return () => {
container.removeEventListener("pointermove", onMove);
container.removeEventListener("pointerleave", onLeave);
report(null);
};
}, [viewer, hoverEnabled]);
useImperativeHandle(ref, () => viewer, [viewer]);

@@ -240,4 +581,5 @@ return /* @__PURE__ */ jsx("div", {

*/
const DxfEmbed = forwardRef(function DxfEmbed({ panel = "left", panelClassName, panelStyle, theme = "aspicio", className, style, options, onViewer, ...previewProps }, ref) {
const DxfEmbed = forwardRef(function DxfEmbed({ panel = "left", panelClassName, panelStyle, theme = "aspicio", className, style, options, onViewer, onHoverLayer, ...previewProps }, ref) {
const [viewer, setViewer] = useState(null);
const [hoveredLayer, setHoveredLayer] = useState(null);
useImperativeHandle(ref, () => viewer, [viewer]);

@@ -248,2 +590,3 @@ const themed = theme === "aspicio";

theme,
reverseHighlightLayer: hoveredLayer,
className: panelClassName,

@@ -253,4 +596,3 @@ style: {

flexShrink: 0,
overflowY: "auto",
...themed && { [panel === "left" ? "borderRight" : "borderLeft"]: `1px solid ${aspicioTokens.hairline}` },
...themed ? { [panel === "left" ? "borderRight" : "borderLeft"]: `1px solid ${aspicioTokens.hairline}` } : { overflowY: "auto" },
...panelStyle

@@ -290,2 +632,6 @@ }

},
onHoverLayer: (layer) => {
if (panel !== "none") setHoveredLayer(layer);
onHoverLayer?.(layer);
},
onViewer: (instance) => {

@@ -292,0 +638,0 @@ setViewer(instance);

+4
-3
{
"name": "@aspicio/react",
"version": "0.1.0",
"version": "0.2.0",
"description": "React bindings for the Aspicio DXF viewer.",

@@ -33,3 +33,3 @@ "homepage": "https://github.com/frontsail-ai/aspicio/tree/main/packages/react#readme",

"dependencies": {
"@aspicio/core": "^0.0.0"
"@aspicio/core": "^0.2.0"
},

@@ -48,4 +48,5 @@ "devDependencies": {

"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
"react": "^18.0.0 || ^19.0.0",
"three": ">=0.184.0 <1.0.0"
}
}

@@ -7,3 +7,3 @@ # @aspicio/react

```bash
npm install @aspicio/react # @aspicio/core comes along; react 18/19 is a peer
npm install @aspicio/react react three # @aspicio/core comes along; react 18/19 and three (>=0.184) are peers
```

@@ -14,5 +14,8 @@

- `<DxfPreview>` — the embeddable canvas alone: pan/zoom/rotate (mouse and
multi-touch), animated fit, batched WebGL rendering. No chrome.
- `<DxfLayerPanel>` — the ready-made layer list alone: visibility toggles,
effective-color swatches, entity counts, hover-to-highlight.
multi-touch), animated fit, batched WebGL rendering. No chrome. Pass
`onHoverLayer` to hit-test the layer under the cursor.
- `<DxfLayerPanel>` — the ready-made layer list, identical to the demo app:
header with layer count, visibility checkboxes, effective-color swatches,
entity counts, hover-to-highlight, double-click-to-solo (with a banner),
and a gesture-hints footer. `theme="none"` renders a minimal unstyled list.

@@ -19,0 +22,0 @@ ## One component