@activecollab/components
Version:
ActiveCollab Components
736 lines (698 loc) • 30.9 kB
JavaScript
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