From fcbc47f9ef21c08efd69bd40a9c19725ae4eb59c Mon Sep 17 00:00:00 2001 From: MANISH KUMAR <146671113+manishpatel00@users.noreply.github.com> Date: Sat, 11 Apr 2026 18:46:44 +0530 Subject: [PATCH 1/5] feat(ux): display polygon coordinates in modal on shape completion When a user finishes designing a polygon (via Close Shape, double-click, Enter, or Esc), the normalized SVG coordinates are now displayed in a focused modal overlay on the canvas instead of being shown in the textbox below where they can easily be missed. Modal features: - Inline copy button with 'Copied' feedback - 'Done' action to dismiss and keep shape - 'Clear & Start Over' action to reset canvas - Overlay click-to-close for quick dismissal - 'View Coordinates' button to re-open modal after close - Full keyboard and screen-reader accessibility - Dark/light theme support The fallback textarea is retained for accessibility and non-JS contexts. Fixes #148 Signed-off-by: MANISH KUMAR <146671113+manishpatel00@users.noreply.github.com> --- .../ShapeBuilder/CoordinatesModal.js | 238 ++++++++++++++++++ site/src/components/ShapeBuilder/index.js | 78 +++--- site/src/pages/index.js | 2 +- 3 files changed, 282 insertions(+), 36 deletions(-) create mode 100644 site/src/components/ShapeBuilder/CoordinatesModal.js diff --git a/site/src/components/ShapeBuilder/CoordinatesModal.js b/site/src/components/ShapeBuilder/CoordinatesModal.js new file mode 100644 index 0000000..33aa620 --- /dev/null +++ b/site/src/components/ShapeBuilder/CoordinatesModal.js @@ -0,0 +1,238 @@ +import React, { useState } from "react"; +import styled from "styled-components"; +import { CopyIcon } from "@sistent/sistent"; + +// ─── Overlay ──────────────────────────────────────────────────────────────── +const Overlay = styled.div` + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.65); + display: flex; + align-items: center; + justify-content: center; + z-index: 1300; + /* subtle entrance */ + animation: fadeIn 0.18s ease; + + @keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } + } +`; + +// ─── Modal Card ────────────────────────────────────────────────────────────── +const ModalCard = styled.div` + background: ${({ theme }) => theme.body || "#1e2227"}; + border: 1px solid ${({ theme }) => theme.border || "#2d3139"}; + border-radius: 12px; + padding: 2rem; + width: min(560px, 92vw); + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.5); + animation: slideUp 0.2s ease; + position: relative; + + @keyframes slideUp { + from { transform: translateY(24px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } + } +`; + +// ─── Header row ───────────────────────────────────────────────────────────── +const Header = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.25rem; +`; + +const Title = styled.h2` + font-size: 1.15rem; + font-weight: 600; + color: ${({ theme }) => theme.text || "#ffffff"}; + margin: 0; +`; + +const CloseButton = styled.button` + background: none; + border: none; + cursor: pointer; + color: ${({ theme }) => theme.text || "#ffffff"}; + font-size: 1.4rem; + line-height: 1; + padding: 2px 6px; + border-radius: 4px; + opacity: 0.7; + transition: opacity 0.15s; + + &:hover { opacity: 1; } +`; + +// ─── Sub-label ─────────────────────────────────────────────────────────────── +const SubLabel = styled.p` + font-size: 0.8rem; + color: ${({ theme }) => theme.textMuted || "#8b949e"}; + margin: 0 0 1rem 0; +`; + +// ─── Code block ────────────────────────────────────────────────────────────── +const CodeBlock = styled.div` + position: relative; + background: ${({ theme }) => theme.codeBackground || "#0d1117"}; + border: 1px solid ${({ theme }) => theme.border || "#2d3139"}; + border-radius: 8px; + padding: 1rem 3rem 1rem 1rem; + margin-bottom: 1.25rem; +`; + +const CoordText = styled.pre` + color: #00b39f; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + font-size: 0.85rem; + line-height: 1.6; + margin: 0; + white-space: pre-wrap; + word-break: break-all; +`; + +const InlineCopyButton = styled.button` + position: absolute; + top: 10px; + right: 10px; + background: none; + border: 1px solid ${({ theme }) => theme.border || "#2d3139"}; + border-radius: 6px; + cursor: pointer; + padding: 5px 8px; + display: flex; + align-items: center; + gap: 4px; + font-size: 0.72rem; + color: ${({ theme }) => theme.text || "#ffffff"}; + opacity: 0.75; + transition: opacity 0.15s, background 0.15s; + + &:hover { + opacity: 1; + background: ${({ theme }) => theme.border || "#2d3139"}; + } + + svg { + width: 14px; + height: 14px; + fill: currentColor; + } +`; + +// ─── Action row ────────────────────────────────────────────────────────────── +const ActionRow = styled.div` + display: flex; + gap: 0.75rem; + justify-content: flex-end; + flex-wrap: wrap; +`; + +const ActionButton = styled.button` + padding: 0.55rem 1.3rem; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: background 0.15s, opacity 0.15s; + + &.primary { + background: #00b39f; + color: #fff; + border: none; + &:hover { background: #009684; } + } + + &.secondary { + background: transparent; + color: ${({ theme }) => theme.text || "#ffffff"}; + border: 1px solid ${({ theme }) => theme.border || "#2d3139"}; + &:hover { opacity: 0.75; } + } +`; + +// ─── Component ─────────────────────────────────────────────────────────────── +const CoordinatesModal = ({ coordinates, onClose, onClear, theme }) => { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(coordinates); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error("Failed to copy:", err); + } + }; + + // Close on overlay click (not on card click) + const handleOverlayClick = (e) => { + if (e.target === e.currentTarget) onClose(); + }; + + return ( + + + {/* ── Header ── */} +
+ + Polygon Coordinates + + + × + +
+ + + SVG-format values normalized to the [−1, 1] range. Copy and paste + into your Meshery Component's  + polygon shape field. + + + {/* ── Code block ── */} + + {coordinates} + + {copied ? ( + "✓ Copied" + ) : ( + <> + + Copy + + )} + + + + {/* ── Actions ── */} + + + Clear & Start Over + + + Done + + +
+
+ ); +}; + +export default CoordinatesModal; diff --git a/site/src/components/ShapeBuilder/index.js b/site/src/components/ShapeBuilder/index.js index 5ab1d15..0f7b455 100644 --- a/site/src/components/ShapeBuilder/index.js +++ b/site/src/components/ShapeBuilder/index.js @@ -4,6 +4,7 @@ import { Wrapper, CanvasContainer, OutputBox, StyledSVG, CopyButton } from "./sh import { Button, Typography, Box, CopyIcon, Select, MenuItem, Slider, FormControl } from "@sistent/sistent"; import { SVG, extend as SVGextend } from "@svgdotjs/svg.js"; import draw from "@svgdotjs/svg.draw.js"; +import CoordinatesModal from "./CoordinatesModal"; SVGextend(SVG.Polygon, draw); @@ -11,7 +12,7 @@ const SCALE_PRESETS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3]; const MIN_SCALE = 0.1; const MAX_SCALE = 3; -const ShapeBuilder = () => { +const ShapeBuilder = ({ theme }) => { const boardRef = useRef(null); const polyRef = useRef(null); const keyHandlersRef = useRef({}); @@ -22,9 +23,11 @@ const ShapeBuilder = () => { const [scale, setScale] = useState(1); const [currentPreset, setCurrentPreset] = useState(1); + // ── Modal state ───────────────────────────────────────────────────────────── + const [modalOpen, setModalOpen] = useState(false); + const handleCopyToClipboard = async () => { if (!result.trim()) return; - try { await navigator.clipboard.writeText(result); setShowCopied(true); @@ -44,17 +47,17 @@ const ShapeBuilder = () => { const showCytoArray = () => { const poly = polyRef.current; if (!poly) return; - try { const points = getPlottedPoints(poly); if (!points) throw new Error("Invalid or empty polygon points"); - const normalized = points .map(([x, y]) => [(x - 260) / 260, (y - 260) / 260]) .flat() .join(" "); setResult(normalized); setError(null); + // ── Open the modal whenever coordinates are ready ── + setModalOpen(true); } catch (err) { setError("Failed to extract and normalize polygon points."); console.error("showCytoArray error:", err); @@ -64,27 +67,21 @@ const ShapeBuilder = () => { const applyScale = (newScale) => { const poly = polyRef.current; if (!poly) return; - const points = getPlottedPoints(poly); if (!points || points.length === 0) return; - if (!basePointsRef.current) { basePointsRef.current = points; } - const basePoints = basePointsRef.current; - const xs = basePoints.map(p => p[0]); const ys = basePoints.map(p => p[1]); const centerX = (Math.max(...xs) + Math.min(...xs)) / 2; const centerY = (Math.max(...ys) + Math.min(...ys)) / 2; - const scaledPoints = basePoints.map(([x, y]) => { const dx = x - centerX; const dy = y - centerY; return [centerX + dx * newScale, centerY + dy * newScale]; }); - poly.plot(scaledPoints); showCytoArray(); }; @@ -92,10 +89,8 @@ const ShapeBuilder = () => { const handleScaleChange = (newScale) => { const clampedScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, newScale)); setScale(clampedScale); - const matchingPreset = SCALE_PRESETS.find(p => Math.abs(p - clampedScale) < 0.01); setCurrentPreset(matchingPreset || clampedScale); - applyScale(clampedScale); }; @@ -113,17 +108,14 @@ const ShapeBuilder = () => { const handleKeyDown = (e) => { const poly = polyRef.current; if (!poly) return; - if (e.ctrlKey) { poly.draw("param", "snapToGrid", 0.001); } - if (e.key === "Enter" || e.key === "Escape") { poly.draw("done"); poly.fill("#00B39F"); showCytoArray(); } - if (e.ctrlKey && e.key.toLowerCase() === "z") { const points = getPlottedPoints(poly); if (!points) return; @@ -155,20 +147,17 @@ const ShapeBuilder = () => { setError("Canvas reference not found"); return; } - try { - const draw = SVG() + const drawInstance = SVG() .addTo(boardRef.current) .size("100%", "100%") .polygon() .draw() .attr({ stroke: "#00B39F", "stroke-width": 1, fill: "none" }); - - draw.draw("param", "snapToGrid", 16); - draw.on("drawstart", attachKeyListeners); - draw.on("drawdone", detachKeyListeners); - - polyRef.current = draw; + drawInstance.draw("param", "snapToGrid", 16); + drawInstance.on("drawstart", attachKeyListeners); + drawInstance.on("drawdone", detachKeyListeners); + polyRef.current = drawInstance; setError(null); } catch (err) { setError(`Failed to initialize drawing: ${err.message}`); @@ -177,23 +166,23 @@ const ShapeBuilder = () => { const clearShape = () => { const poly = polyRef.current; - if (!poly) return; - - poly.draw("cancel"); - poly.remove(); - detachKeyListeners(); - polyRef.current = null; - basePointsRef.current = null; + if (poly) { + poly.draw("cancel"); + poly.remove(); + detachKeyListeners(); + polyRef.current = null; + basePointsRef.current = null; + } setResult(""); setScale(1); setCurrentPreset(1); + setModalOpen(false); initializeDrawing(); }; const closeShape = () => { const poly = polyRef.current; if (!poly) return; - poly.draw("done"); poly.fill("#00B39F"); const points = getPlottedPoints(poly); @@ -217,6 +206,16 @@ const ShapeBuilder = () => { return ( + {/* ── Coordinates Modal ─────────────────────────────────────────────── */} + {modalOpen && result && ( + setModalOpen(false)} + onClear={clearShape} + /> + )} + { + {/* Re-open modal button — only shown when coordinates exist */} + {result && ( + + )} @@ -261,9 +270,7 @@ const ShapeBuilder = () => { aria-label="Scale preset" sx={{ color: "#fff", - "& .MuiSelect-icon": { - color: "#fff" - } + "& .MuiSelect-icon": { color: "#fff" } }} > {SCALE_PRESETS.map((preset) => ( @@ -295,12 +302,13 @@ const ShapeBuilder = () => { + {/* ── Fallback output box (kept for accessibility / non-JS contexts) ── */} Polygon Coordinates (SVG format):
-