UNPKG

@activecollab/components

Version:

ActiveCollab Components

736 lines (698 loc) 30.9 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import React, { useState } from "react"; import styled from "styled-components"; import { MinusGlyph, PlusGlyph, SquareGlyph } from "./glyphs"; import { Dot } from "../../components/Dot"; import { IconButton } from "../../components/IconButton"; import { ArrowRefreshIcon, CircleIcon, CircleSlashIcon, LockIcon, RoundedRectangleIcon, TextAlignCenterIcon, TextAlignJustifyIcon, TextAlignLeftIcon, TextAlignRightIcon, TextStyleBoldIcon, TextStyleItalicIcon, TextStyleStrikeIcon, TextStyleUnderlineIcon, TrashIcon, TriangleIcon } from "../../components/Icons"; import { List, ListItem } from "../../components/List"; import { Menu } from "../../components/Menu"; import { Tooltip } from "../../components/Tooltip"; import { colors } from "../../utils/colors"; /* ------------------------------------------------------------------ */ /* Canvas elements (1:1 with new-activecollab-front */ /* src/components/Whiteboards/ — constants.js, ShapeElement.jsx, */ /* PostitElement.jsx, ResizableWrapper.jsx, ResizeHandles.jsx, */ /* FormattingToolbarContainer.jsx + the two formatting toolbars) */ /* ------------------------------------------------------------------ */ /* element colors are canvas DATA (app constants), not theme — hardcoded */ export const ELEMENT_FILL = "#ffffff"; export const ELEMENT_STROKE = "#374151"; export const ELEMENT_TEXT = "#1f2937"; export const STICKY_FILL = "#fbbf24"; export const SELECTION_BLUE = "#3b82f6"; /* COLOR_PALETTE from toolbar/colorPalette.js (submenu swatch rows) */ export const COLOR_PALETTE = ["#ffffff", "#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", "#f97316", "#06b6d4", "#84cc16", "#ec4899", "#6b7280"]; /* Discrete option sets for the right Inspector — mirror the app's fixed font-size ladder and the border-thickness steps offered in the toolbar. */ export const FONT_SIZE_OPTIONS = [10, 13, 16, 20, 24, 30, 36, 48, 72, 96, 144, 216, 288]; export const THICKNESS_OPTIONS = [1, 2, 3, 4, 5]; export const OPACITY_OPTIONS = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]; /* Corner radii scaled to the board: shapes are drawn at ~100–300px, so the jump from a crisp corner to a pill reads across 0–64px. */ export const RADIUS_OPTIONS = [0, 4, 8, 12, 16, 24, 32, 48, 64]; /* Element bodies are HTML, as the API returns them (the `body` field). The Inspector renders them read-only; when a body is empty the field is omitted. Mock the three cases across the canvas: a short body (shape-rounded, the default selection), a long one that scrolls (shape-circle), and none at all (shape-triangle / sticky). */ export const ELEMENT_BODIES = { "shape-rounded": "<p>Marks the <strong>Kickoff</strong> phase.</p>", "shape-circle": "<p>The <strong>discovery</strong> loop for this initiative.</p><p>We collect signals from support, sales and analytics, then cluster them into themes before committing to a direction.</p><ul><li>Owner: research</li><li>Inputs: interviews, tickets, funnel data</li><li>Output: a prioritised opportunity list</li></ul><p>See the <em>retro notes</em> and last quarter's readout for the full context and prior decisions.</p>" }; /* Named-colour options for the Inspector fill/stroke pickers, built from the design system's shared data colours (utils/colors). Shaped for the DS Select (id + name + color + textColor) so it renders the standard dot-and-radio menu used by the label dialogs. */ export const COLOR_OPTIONS = Object.values(colors).map(entry => ({ id: entry.color, name: entry.name, color: entry.color, textColor: entry.color })); /* per-shape editable state (the app bakes opacity into rgba via hexToRgba/parseOpacity; the mock keeps opacity separate and rebuilds the color when rendering) */ export const defaultShapeState = type => ({ type, fill: ELEMENT_FILL, fillOpacity: 100, stroke: ELEMENT_STROKE, strokeOpacity: 100, strokeWidth: 2, strokeDash: undefined, borderRadius: type === "roundedRectangle" ? 12 : 0, fontSize: 16, bold: false, italic: false, underline: false, strikethrough: false, textColor: ELEMENT_TEXT, align: "left" }); /* swatches are 6-digit hex, the color wheel returns hsl(...) */ export const withOpacity = (color, opacityPct) => { if (opacityPct >= 100) { return color; } const alpha = opacityPct / 100; if (color.startsWith("#")) { const r = parseInt(color.slice(1, 3), 16); const g = parseInt(color.slice(3, 5), 16); const b = parseInt(color.slice(5, 7), 16); return "rgba(" + r + ", " + g + ", " + b + ", " + alpha + ")"; } if (color.startsWith("hsl(")) { return color.replace("hsl(", "hsla(").replace(")", ", " + alpha + ")"); } return color; }; /* getShapeBorder from ShapeElement.jsx */ export const shapeBorder = state => { if (state.strokeWidth === 0) { return "none"; } const lineStyle = state.strokeDash === "5,5" ? "dashed" : state.strokeDash === "2,2" ? "dotted" : "solid"; return state.strokeWidth + "px " + lineStyle + " " + withOpacity(state.stroke, state.strokeOpacity); }; /* PostitElement gradient: color → same color at ~87% alpha */ export const stickyGradient = function (color, opacity) { if (opacity === void 0) { opacity = 100; } const main = withOpacity(color, opacity); const faded = withOpacity(color, Math.max(0, opacity - 13)); return "linear-gradient(135deg, " + main + " 0%, " + faded + " 100%)"; }; /* Grid config, aligned to the 8px design-system grid. dotSpacing drives the dot layer; snapStep drives snapping — decoupled, so each level snaps finer than its dots (Small: 16px dots / 8px snap; Large: 32px dots / 16px snap, i.e. dots and half-dots). */ export const GRID_LEVELS = [{ value: "off", label: "Off", dotSpacing: null, snapStep: null }, { value: "small", label: "Small", dotSpacing: 16, snapStep: 8 }, { value: "large", label: "Large", dotSpacing: 32, snapStep: 16 }]; export const gridLevel = value => { var _GRID_LEVELS$find; return (_GRID_LEVELS$find = GRID_LEVELS.find(level => level.value === value)) != null ? _GRID_LEVELS$find : GRID_LEVELS[0]; }; /* default sizes from DEFAULT_SHAPE_SIZES / DEFAULT_STICKY_SIZE; positions are board-space pixels (top-left) so dragging can snap to the pixel grid */ export const CANVAS_ELEMENTS = [{ id: "shape-circle", kind: "shape", initialType: "circle", x: 320, y: 180, width: 80, height: 80 }, { id: "shape-rounded", kind: "shape", initialType: "roundedRectangle", x: 560, y: 300, width: 100, height: 60 }, { id: "shape-triangle", kind: "shape", initialType: "triangle", x: 820, y: 170, width: 80, height: 80 }, { id: "sticky", kind: "sticky", x: 430, y: 430, width: 120, height: 120 }]; export const INITIAL_SHAPES = CANVAS_ELEMENTS.reduce((acc, element) => { if (element.kind === "shape" && element.initialType) { acc[element.id] = defaultShapeState(element.initialType); } return acc; }, {}); export const INITIAL_POSITIONS = CANVAS_ELEMENTS.reduce((acc, element) => { acc[element.id] = { x: element.x, y: element.y }; return acc; }, {}); export const snapValue = (value, step) => step ? Math.round(value / step) * step : value; /* deselect happens on canvas-background click (layer under the bars) */ export const StyledCanvasLayer = styled.div.withConfig({ displayName: "elements__StyledCanvasLayer", componentId: "sc-ohnju4-0" })(["position:absolute;inset:0;&.drawing{cursor:crosshair;}"]); /* ResizableWrapper: selected = 2px blue-500 ring @50%, hover = 4px blue-400 ring @70% (Tailwind rings are box-shadows) */ export const StyledCanvasElement = styled.div.withConfig({ displayName: "elements__StyledCanvasElement", componentId: "sc-ohnju4-1" })(["position:absolute;cursor:move;&:hover:not(.selected){box-shadow:0 0 0 4px rgba(96,165,250,0.7);}&.selected{box-shadow:0 0 0 2px rgba(59,130,246,0.5);}&.is-card{cursor:grab;height:auto;&:hover:not(.selected),&.selected{box-shadow:none;}}.shape-fill{width:100%;height:100%;box-sizing:border-box;}"]); /* PostitElement: 12px padding, shadow-lg, 1deg tilt that straightens on hover, diagonal gradient to ~87% alpha, Comic Sans */ export const StyledSticky = styled.div.withConfig({ displayName: "elements__StyledSticky", componentId: "sc-ohnju4-2" })(["width:100%;height:100%;box-sizing:border-box;padding:12px;box-shadow:0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -4px rgba(0,0,0,0.1);transform:rotate(1deg);transition:transform 200ms;font-family:\"Comic Sans MS\",cursive,system-ui,sans-serif;line-height:1.4;word-break:break-word;white-space:pre-wrap;", ":hover &{transform:rotate(0deg);}"], StyledCanvasElement); /* ResizeHandles: 8 12px blue squares w/ 2px white border on the box, plus the 24px rotate handle outside the bottom-left corner */ export const StyledHandles = styled.div.withConfig({ displayName: "elements__StyledHandles", componentId: "sc-ohnju4-3" })([".rh{position:absolute;width:12px;height:12px;box-sizing:border-box;background-color:", ";border:2px solid #ffffff;border-radius:2px;z-index:10;&:hover{background-color:#2563eb;}}.rh-nw{top:-6px;left:-6px;cursor:nw-resize;}.rh-n{top:-6px;left:50%;margin-left:-6px;cursor:n-resize;}.rh-ne{top:-6px;right:-6px;cursor:ne-resize;}.rh-w{top:50%;left:-6px;margin-top:-6px;cursor:w-resize;}.rh-e{top:50%;right:-6px;margin-top:-6px;cursor:e-resize;}.rh-sw{bottom:-6px;left:-6px;cursor:sw-resize;}.rh-s{bottom:-6px;left:50%;margin-left:-6px;cursor:s-resize;}.rh-se{bottom:-6px;right:-6px;cursor:se-resize;}.rotate-handle{position:absolute;bottom:-35px;left:-35px;width:24px;height:24px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;background-color:#ffffff;border:2px solid ", ";border-radius:50%;cursor:grab;z-index:11;svg{width:16px;height:16px;}}"], SELECTION_BLUE, SELECTION_BLUE); export const RESIZE_DIRECTIONS = ["nw", "n", "ne", "w", "e", "sw", "s", "se"]; export const ResizeHandles = () => /*#__PURE__*/React.createElement(StyledHandles, { onMouseDown: event => event.stopPropagation() }, RESIZE_DIRECTIONS.map(direction => /*#__PURE__*/React.createElement("span", { key: direction, className: "rh rh-" + direction })), /*#__PURE__*/React.createElement("span", { className: "rotate-handle" }, /*#__PURE__*/React.createElement(ArrowRefreshIcon, { fill: SELECTION_BLUE }))); /* FormattingToolbarContainer: Paper paper-2 (bg + 8px radius + shadow, no border), 8px padding, 4px gap, centered 10px above the element */ export const StyledFormattingToolbar = styled.div.withConfig({ displayName: "elements__StyledFormattingToolbar", componentId: "sc-ohnju4-4" })(["position:absolute;bottom:calc(100% + 10px);left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:4px;padding:8px;border-radius:8px;background-color:var(--page-paper-main);box-shadow:var(--shadow-tertiary);white-space:nowrap;cursor:auto;z-index:20;.tb-group{position:relative;display:flex;align-items:center;gap:4px;padding-right:8px;margin-right:8px;border-right:1px solid var(--border-primary);}.tb-rel{position:relative;display:inline-flex;}.divider{width:1px;height:20px;background-color:var(--border-primary);margin:0 4px;flex-shrink:0;}"]); /* Button variant="option" (idle) / "tertiary" (active), h-8 px-2 gap-1 */ export const StyledToolbarButton = styled.button.withConfig({ displayName: "elements__StyledToolbarButton", componentId: "sc-ohnju4-5" })(["display:inline-flex;align-items:center;justify-content:center;gap:4px;height:32px;box-sizing:border-box;padding:0 8px;border:0;border-radius:8px;background-color:transparent;color:var(--color-secondary);font-family:-apple-system,BlinkMacSystemFont,\"Roboto\",\"Helvetica Neue\",Arial,sans-serif;font-size:14px;cursor:pointer;&:hover{background-color:var(--color-theme-200);}&.active{background-color:var(--color-theme-300);}&.icon-only{width:32px;padding:0;}&:disabled{opacity:0.45;cursor:default;&:hover{background-color:transparent;}}svg{fill:currentColor;flex-shrink:0;}svg.i16{width:16px;height:16px;}svg.i12{width:12px;height:12px;}"]); /* Same chrome as a toolbar button, but an <a> — used where the action is a real navigation (Visit). `withComponent` keeps the styles and swaps the element, so the props type becomes the anchor's. */ export const StyledToolbarLink = styled(StyledToolbarButton.withComponent("a")).withConfig({ displayName: "elements__StyledToolbarLink", componentId: "sc-ohnju4-6" })(["text-decoration:none;"]); /* font-mono min-w-[24px] text-xs readout between the stepper buttons */ export const StyledFontSizeValue = styled.span.withConfig({ displayName: "elements__StyledFontSizeValue", componentId: "sc-ohnju4-7" })(["min-width:24px;text-align:center;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--color-secondary);"]); export const stopClick = event => event.stopPropagation(); /* Shared drop shadow for the colour swatch popover / colour wheel that the compact toolbar swatch trigger opens. */ export const SUBMENU_SHADOW = "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)"; /* ColorWheelPopover: beside the submenu (SHAPE_POPOVER_CLASS) or below a toolbar trigger (sticky default class) */ export const StyledColorPopover = styled.div.withConfig({ displayName: "elements__StyledColorPopover", componentId: "sc-ohnju4-8" })(["position:absolute;box-sizing:border-box;padding:12px;background-color:var(--page-paper-main);border:1px solid var(--border-primary);border-radius:8px;box-shadow:", ";z-index:70;cursor:auto;&.beside{top:0;left:100%;margin-left:8px;}&.below{top:40px;left:0;}"], SUBMENU_SHADOW); /* ImprovedColorWheel: hue = angle from east (atan2), saturation = distance, lightness 50 — the canvas wheel replicated with CSS gradients */ export const WHEEL_SIZE = 200; export const WHEEL_RADIUS = WHEEL_SIZE / 2 - 5; export const StyledColorWheel = styled.button.withConfig({ displayName: "elements__StyledColorWheel", componentId: "sc-ohnju4-9" })(["display:block;width:", "px;height:", "px;padding:0;border:0;border-radius:50%;cursor:crosshair;box-shadow:", ";background:radial-gradient( circle closest-side,#ffffff,rgba(255,255,255,0) ),conic-gradient( from 90deg,#ff0000,#ffff00,#00ff00,#00ffff,#0000ff,#ff00ff,#ff0000 );"], WHEEL_SIZE, WHEEL_SIZE, SUBMENU_SHADOW); export const ColorWheel = _ref => { let onColorChange = _ref.onColorChange; return /*#__PURE__*/React.createElement(StyledColorWheel, { onClick: event => { const rect = event.currentTarget.getBoundingClientRect(); const dx = event.clientX - rect.left - WHEEL_SIZE / 2; const dy = event.clientY - rect.top - WHEEL_SIZE / 2; const distance = Math.sqrt(dx * dx + dy * dy); if (distance > WHEEL_RADIUS) { return; } let angle = Math.atan2(dy, dx); if (angle < 0) { angle += 2 * Math.PI; } const hue = Math.round(angle * 180 / Math.PI); const saturation = Math.round(Math.min(100, distance / WHEEL_RADIUS * 100)); onColorChange("hsl(" + hue + ", " + saturation + "%, 50%)"); } }); }; /* ColorSwatchRow.jsx: 32px swatches + a "+" that opens the color wheel */ export const StyledSwatchRow = styled.div.withConfig({ displayName: "elements__StyledSwatchRow", componentId: "sc-ohnju4-10" })(["display:flex;flex-wrap:wrap;gap:8px;.swatch{width:32px;height:32px;box-sizing:border-box;padding:0;border:2px solid var(--border-primary);border-radius:4px;cursor:pointer;&:hover{border-color:var(--color-theme-500);}&.selected{box-shadow:0 0 0 2px ", ";}}.swatch-add{display:flex;align-items:center;justify-content:center;background-color:var(--color-theme-200);color:var(--color-secondary);}"], SELECTION_BLUE); export const ColorSwatchRow = _ref2 => { let selectedColor = _ref2.selectedColor, includeTransparent = _ref2.includeTransparent, onSelect = _ref2.onSelect, onAddClick = _ref2.onAddClick; return /*#__PURE__*/React.createElement(StyledSwatchRow, null, includeTransparent ? /*#__PURE__*/React.createElement("button", { type: "button", className: selectedColor === "transparent" ? "swatch swatch-add selected" : "swatch swatch-add", "aria-label": "Transparent", onClick: () => onSelect("transparent") }, /*#__PURE__*/React.createElement(CircleSlashIcon, { width: 20, height: 20, fill: "currentColor" })) : null, COLOR_PALETTE.map(color => /*#__PURE__*/React.createElement("button", { key: color, type: "button", className: selectedColor === color ? "swatch selected" : "swatch", style: { backgroundColor: color }, onClick: () => onSelect(color) })), /*#__PURE__*/React.createElement("button", { type: "button", className: "swatch swatch-add", onClick: onAddClick }, /*#__PURE__*/React.createElement(PlusGlyph, null))); }; export const SHAPE_TYPE_ICON = { rectangle: /*#__PURE__*/React.createElement(SquareGlyph, null), roundedRectangle: /*#__PURE__*/React.createElement(RoundedRectangleIcon, { className: "i16" }), circle: /*#__PURE__*/React.createElement(CircleIcon, { className: "i16" }), triangle: /*#__PURE__*/React.createElement(TriangleIcon, { className: "i16" }) }; /* The four shape types, in the order the shape-type menu lists them. */ export const SHAPE_TYPE_OPTIONS = [{ type: "rectangle", label: "Rectangle" }, { type: "roundedRectangle", label: "Rounded Rectangle" }, { type: "circle", label: "Circle" }, { type: "triangle", label: "Triangle" }]; /* Stroke pattern options — now only the Inspector's Stroke group renders these (the shape floating toolbar no longer edits stroke). Mirrors the app's STROKE_PATTERN_OPTIONS dash enum: solid | dashed | dotted. */ export const BORDER_STYLE_OPTIONS = [{ value: undefined, label: "Solid" }, { value: "5,5", label: "Dashed" }, { value: "2,2", label: "Dotted" }]; /* ── Shared floating-toolbar controls ─────────────────────────────────────── The floating toolbars were flattened this session to match the app: compact, collapsed controls (single-button dropdowns) with uniform 4px spacing and a single hairline separator only before Lock / Delete. These mirror the shared controls in the app's Inspector/Controls (ShapeTypeMenu, AlignmentMenu, ColorPicker compact, ToolbarActions). */ /* ToolbarActions — the trailing block shared by EVERY floating toolbar: separator → Lock → Delete, in the standard control colour (not red). Mirrors app `toolbar/ToolbarActions.jsx`. */ export const ToolbarActions = _ref3 => { let locked = _ref3.locked, onToggleLock = _ref3.onToggleLock, onDelete = _ref3.onDelete; return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", { className: "divider" }), /*#__PURE__*/React.createElement(Tooltip, { title: locked ? "Unlock" : "Lock" }, /*#__PURE__*/React.createElement(StyledToolbarButton, { className: locked ? "icon-only active" : "icon-only", "aria-pressed": locked, onClick: onToggleLock }, /*#__PURE__*/React.createElement(LockIcon, { className: "i16" }))), /*#__PURE__*/React.createElement(Tooltip, { title: "Delete" }, /*#__PURE__*/React.createElement(StyledToolbarButton, { className: "icon-only", onClick: onDelete }, /*#__PURE__*/React.createElement(TrashIcon, { className: "i16" })))); }; /* ShapeTypeMenu — collapsed shape-type picker: one icon button showing the current shape, opening a dropdown of the four types. Modeled on the app's ShapeTypeMenu (DS Menu + List). */ export const ShapeTypeMenu = _ref4 => { let value = _ref4.value, onChange = _ref4.onChange; const _useState = useState(false), open = _useState[0], setOpen = _useState[1]; return /*#__PURE__*/React.createElement(Menu, { open: open, onOpen: () => setOpen(true), onClose: () => setOpen(false), position: "bottom-start", target: /*#__PURE__*/React.createElement(IconButton, { type: "button", variant: "text gray", active: open, "aria-label": "Shape" }, SHAPE_TYPE_ICON[value]) }, /*#__PURE__*/React.createElement(List, { style: { padding: "8px 0" }, tabIndex: -1 }, SHAPE_TYPE_OPTIONS.map(option => /*#__PURE__*/React.createElement(ListItem, { key: option.type, "aria-label": option.label, onClick: () => { onChange(option.type); setOpen(false); } }, SHAPE_TYPE_ICON[option.type], option.label)))); }; /* AlignmentMenu — collapsed alignment picker (single button + dropdown), the compact-toolbar counterpart to the Inspector's expanded four-button row. Mirrors the app's AlignmentMenu. */ export const AlignmentMenu = _ref5 => { var _ALIGN_OPTIONS$find; let value = _ref5.value, onChange = _ref5.onChange, disabled = _ref5.disabled; const _useState2 = useState(false), open = _useState2[0], setOpen = _useState2[1]; const selected = (_ALIGN_OPTIONS$find = ALIGN_OPTIONS.find(option => option.value === value)) != null ? _ALIGN_OPTIONS$find : ALIGN_OPTIONS[0]; return /*#__PURE__*/React.createElement(Menu, { open: open, onOpen: () => setOpen(true), onClose: () => setOpen(false), position: "bottom-start", target: /*#__PURE__*/React.createElement(IconButton, { type: "button", variant: "text gray", active: open, disabled: disabled, "aria-label": "Alignment" }, selected.icon) }, /*#__PURE__*/React.createElement(List, { style: { padding: "8px 0" }, tabIndex: -1 }, ALIGN_OPTIONS.map(option => /*#__PURE__*/React.createElement(ListItem, { key: option.value, "aria-label": option.label, onClick: () => { onChange(option.value); setOpen(false); } }, option.icon, option.label)))); }; /* Compact swatch-only colour trigger: a single dot (or a CircleSlash chip when transparent) that opens a swatch popover. The Inspector's ColorPickerField keeps the swatch + name; the floating toolbars use this swatch-only variant. `includeTransparent` is passed through to the swatch row — shape fill opts in, sticky fill and text colour do not. */ export const ToolbarColorSwatch = _ref6 => { let value = _ref6.value, includeTransparent = _ref6.includeTransparent, label = _ref6.label, onChange = _ref6.onChange; const _useState3 = useState(false), open = _useState3[0], setOpen = _useState3[1]; const _useState4 = useState(false), wheelOpen = _useState4[0], setWheelOpen = _useState4[1]; const isTransparent = value === "transparent"; return /*#__PURE__*/React.createElement("span", { className: "tb-rel" }, /*#__PURE__*/React.createElement(Tooltip, { title: label }, /*#__PURE__*/React.createElement(StyledToolbarButton, { className: open ? "icon-only active" : "icon-only", "aria-label": label, onClick: () => { setOpen(current => !current); setWheelOpen(false); } }, isTransparent ? /*#__PURE__*/React.createElement(CircleSlashIcon, { width: 16, height: 16, fill: "currentColor" }) : /*#__PURE__*/React.createElement(Dot, { color: value, size: 16 }))), open ? /*#__PURE__*/React.createElement(StyledColorPopover, { className: "below" }, /*#__PURE__*/React.createElement(ColorSwatchRow, { selectedColor: value, includeTransparent: includeTransparent, onSelect: color => { onChange(color); setOpen(false); }, onAddClick: () => setWheelOpen(current => !current) }), wheelOpen ? /*#__PURE__*/React.createElement(StyledColorPopover, { className: "beside" }, /*#__PURE__*/React.createElement(ColorWheel, { onColorChange: color => { onChange(color); setOpen(false); } })) : null) : null); }; /* The text controls shared by the shape and sticky toolbars: font-size stepper, B / I / U / S, and the collapsed alignment menu. Same set the app's sticky and shape toolbars share (font-size picker, TextStyleButtons, AlignmentMenu). Kept generic so both element shapes can drive it. */ export const ToolbarTextControls = _ref7 => { let fontSize = _ref7.fontSize, styleFlags = _ref7.styleFlags, align = _ref7.align, onFontSize = _ref7.onFontSize, onToggleStyle = _ref7.onToggleStyle, onAlign = _ref7.onAlign; return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Tooltip, { title: "Decrease font size" }, /*#__PURE__*/React.createElement(StyledToolbarButton, { className: "icon-only", "aria-label": "Decrease font size", onClick: () => onFontSize(Math.max(8, fontSize - 2)) }, /*#__PURE__*/React.createElement(MinusGlyph, { className: "i16" }))), /*#__PURE__*/React.createElement(StyledFontSizeValue, null, fontSize), /*#__PURE__*/React.createElement(Tooltip, { title: "Increase font size" }, /*#__PURE__*/React.createElement(StyledToolbarButton, { className: "icon-only", "aria-label": "Increase font size", onClick: () => onFontSize(Math.min(96, fontSize + 2)) }, /*#__PURE__*/React.createElement(PlusGlyph, { className: "i16" }))), STYLE_OPTIONS.map(option => /*#__PURE__*/React.createElement(Tooltip, { key: option.key, title: option.label }, /*#__PURE__*/React.createElement(StyledToolbarButton, { className: styleFlags[option.key] ? "icon-only active" : "icon-only", "aria-label": option.label, "aria-pressed": styleFlags[option.key], onClick: () => onToggleStyle(option.key) }, option.icon))), /*#__PURE__*/React.createElement(AlignmentMenu, { value: align, onChange: onAlign })); }; /* ShapeFormattingToolbar — flat layout (was submenu-based): shape-type menu → fill colour → the same text controls as the sticky toolbar → Lock / Delete. Stroke is no longer edited here — it lives only in the Inspector's Stroke group now. Mirrors the app's ShapeFormattingToolbar. */ export const ShapeFormattingToolbar = _ref8 => { var _element$align; let element = _ref8.element, onUpdate = _ref8.onUpdate, onDelete = _ref8.onDelete, locked = _ref8.locked, onToggleLock = _ref8.onToggleLock; return /*#__PURE__*/React.createElement(StyledFormattingToolbar, { onClick: stopClick, onMouseDown: stopClick }, /*#__PURE__*/React.createElement(ShapeTypeMenu, { value: element.type, onChange: type => onUpdate({ type }) }), /*#__PURE__*/React.createElement(ToolbarColorSwatch, { value: element.fill, includeTransparent: true, label: "Fill color", onChange: fill => onUpdate({ fill }) }), /*#__PURE__*/React.createElement(ToolbarTextControls, { fontSize: element.fontSize, styleFlags: element, align: (_element$align = element.align) != null ? _element$align : "left", onFontSize: fontSize => onUpdate({ fontSize }), onToggleStyle: key => onUpdate({ [key]: !element[key] }), onAlign: align => onUpdate({ align }) }), /*#__PURE__*/React.createElement(ToolbarActions, { locked: locked, onToggleLock: onToggleLock, onDelete: onDelete })); }; /* PostitFormattingToolbar — flat layout: note (fill) colour → the shared text controls (font-size, B/I/U/S, alignment menu) → Lock / Delete. The note's colour is its whole point, so its fill swatch does NOT offer Transparent. Mirrors the app's PostitFormattingToolbar. */ export const PostitFormattingToolbar = _ref9 => { let value = _ref9.value, onChange = _ref9.onChange, onDelete = _ref9.onDelete, locked = _ref9.locked, onToggleLock = _ref9.onToggleLock; return /*#__PURE__*/React.createElement(StyledFormattingToolbar, { onClick: stopClick, onMouseDown: stopClick }, /*#__PURE__*/React.createElement(ToolbarColorSwatch, { value: value.color, label: "Note color", onChange: color => onChange(_extends({}, value, { color })) }), /*#__PURE__*/React.createElement(ToolbarTextControls, { fontSize: value.fontSize, styleFlags: value, align: value.align, onFontSize: fontSize => onChange(_extends({}, value, { fontSize })), onToggleStyle: key => onChange(_extends({}, value, { [key]: !value[key] })), onAlign: align => onChange(_extends({}, value, { align })) }), /*#__PURE__*/React.createElement(ToolbarActions, { locked: locked, onToggleLock: onToggleLock, onDelete: onDelete })); }; /* shape body per current (editable) type */ export const renderShapeBody = function (def, state, /* Effective size — the Inspector can override the element's default width / height, so the triangle polygon is drawn from the live size, not the def. */ width, height) { if (width === void 0) { width = def.width; } if (height === void 0) { height = def.height; } return state.type === "triangle" ? /*#__PURE__*/ /* TriangleElement: SVG polygon, apex top-center */ React.createElement("svg", { width: "100%", height: "100%", style: { display: "block", overflow: "visible" } }, /*#__PURE__*/React.createElement("polygon", { points: width / 2 + ",0 " + width + "," + height + " 0," + height, fill: withOpacity(state.fill, state.fillOpacity), stroke: withOpacity(state.stroke, state.strokeOpacity), strokeWidth: state.strokeWidth, strokeDasharray: state.strokeDash })) : /*#__PURE__*/React.createElement("div", { className: "shape-fill", style: { backgroundColor: withOpacity(state.fill, state.fillOpacity), border: shapeBorder(state), borderRadius: state.type === "circle" ? "50%" : state.borderRadius } }); }; export const ALIGN_OPTIONS = [{ value: "left", label: "Align Left", icon: /*#__PURE__*/React.createElement(TextAlignLeftIcon, null) }, { value: "center", label: "Align Center", icon: /*#__PURE__*/React.createElement(TextAlignCenterIcon, null) }, { value: "right", label: "Align Right", icon: /*#__PURE__*/React.createElement(TextAlignRightIcon, null) }, { value: "justify", label: "Justify", icon: /*#__PURE__*/React.createElement(TextAlignJustifyIcon, null) }]; export const STYLE_OPTIONS = [{ key: "bold", label: "Bold", icon: /*#__PURE__*/React.createElement(TextStyleBoldIcon, null) }, { key: "italic", label: "Italic", icon: /*#__PURE__*/React.createElement(TextStyleItalicIcon, null) }, { key: "underline", label: "Underline", icon: /*#__PURE__*/React.createElement(TextStyleUnderlineIcon, null) }, { key: "strikethrough", label: "Strikethrough", icon: /*#__PURE__*/React.createElement(TextStyleStrikeIcon, null) }]; //# sourceMappingURL=elements.js.map