UNPKG

@activecollab/components

Version:

ActiveCollab Components

855 lines (826 loc) 39 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import React, { useCallback, useState } from "react"; import styled from "styled-components"; import { BORDER_STYLE_OPTIONS, CANVAS_ELEMENTS, COLOR_OPTIONS, ELEMENT_BODIES, FONT_SIZE_OPTIONS, OPACITY_OPTIONS, RADIUS_OPTIONS, SHAPE_TYPE_ICON, THICKNESS_OPTIONS, ALIGN_OPTIONS, STYLE_OPTIONS } from "./elements"; import { formatFileSize } from "./fileElement"; import { OUTLINE_MAX_WIDTH, OUTLINE_MIN_WIDTH, OUTLINE_WIDTH, StyledChooseRow, StyledPanelChooseSlot } from "./outline"; import { projectElementTitle } from "./projectElements"; import { Checkbox } from "../../components/Checkbox"; import { ChooseV2 } from "../../components/ChooseV2"; import { Dot } from "../../components/Dot"; import { EditableText } from "../../components/EditableText"; import { IconButton } from "../../components/IconButton"; import { AttachmentIcon, CircleSlashIcon, InsertLinkIcon, LockIcon, NoteIcon, ProjectIcon, TaskIcon } from "../../components/Icons"; import { Input, InputAdornment, InputNumber } from "../../components/Input"; import { Label } from "../../components/Label"; import { Select } from "../../components/Select"; import { SelectTrigger } from "../../components/SelectTrigger"; import { Tooltip } from "../../components/Tooltip"; import { Body2, Caption1, Header3 } from "../../components/Typography"; /* ------------------------------------------------------------------ */ /* Right Inspector — selected-element properties panel */ /* ------------------------------------------------------------------ */ /* Which right-docked panel is open. Only "inspector" is built here; "comments" is reserved for the next iteration. */ /* ── Inspector capability traits ────────────────────────────────────────── The Inspector is TRAIT-DRIVEN: a selected element declares which property groups it supports, and only those groups render. This is the "interface with implementation traits" model — the interface is the full set of groups, and each element kind overrides the flags it does not support (a sticky has no stroke, and its height is auto because the note grows with its content). New element kinds slot in by returning their own trait set. */ export const elementTraits = def => def.kind === "sticky" ? { geometry: true, fill: true, fillTransparent: false, stroke: false, text: true, autoHeight: true } : { geometry: true, fill: true, fillTransparent: true, stroke: true, text: true }; /** * A project / task card's traits. It is the most constrained element on the * board, and every "false" here is a fact about the card rather than a feature * that has not been built yet: * * fill / stroke / text the card's appearance is the design system's, not * the board's — recolouring it would make a task look * like something it is not on another person's screen. * autoHeight the card is content-sized; only its width is given. * renamable the title belongs to the task / project record. * * Rotation is absent for the same reason it is absent on link and file cards: * the Geometry group for a card simply does not render one. */ export const PROJECT_ELEMENT_TRAITS = { geometry: true, fill: false, stroke: false, text: false, autoHeight: true, renamable: false }; /** * A LIMITED-ACCESS card's traits: the project card's, with everything the * viewer could have changed taken away. * * The panel is not empty, and that is deliberate — geometry still renders, so * the viewer can see that the thing has a position and a size, which is * precisely the amount that Workload's limited cell exposes (its data shape is * a quantity and an offset). It simply cannot be edited. */ export const LIMITED_ELEMENT_TRAITS = _extends({}, PROJECT_ELEMENT_TRAITS, { readOnly: true }); export const TYPE_LABEL = { rectangle: "Rectangle", roundedRectangle: "Rounded Rectangle", circle: "Circle", triangle: "Triangle" }; export const sameColor = (a, b) => (a != null ? a : "").toLowerCase() === b.toLowerCase(); /* Right dock — mirrors StyledOutlineSidebar but docked on the RIGHT: border on the left edge, resizer on the left edge. Same 260–780 width band. */ export const StyledInspectorSidebar = styled.aside.withConfig({ displayName: "inspector__StyledInspectorSidebar", componentId: "sc-do334k-0" })(["flex-shrink:0;position:relative;box-sizing:border-box;display:flex;flex-direction:column;border-left:1px solid var(--selectable);background-color:var(--page-paper-main);font-family:-apple-system,BlinkMacSystemFont,\"Roboto\",\"Helvetica Neue\",Arial,sans-serif;"]); /* Grab strip on the LEFT edge, because the panel is docked right — dragging it left widens. Same token pair as the outline resizer: selectable grey at rest, the hover token while grabbed. */ export const StyledInspectorResizer = styled.div.withConfig({ displayName: "inspector__StyledInspectorResizer", componentId: "sc-do334k-1" })(["position:absolute;top:0;bottom:0;left:-2px;width:5px;cursor:col-resize;z-index:2;&::after{content:\"\";position:absolute;top:0;bottom:0;left:2px;width:1px;background-color:var(--selectable-active);opacity:0;transition:opacity 150ms ease;}&:hover::after,&.resizing::after{opacity:1;}"]); export const StyledInspectorBody = styled.div.withConfig({ displayName: "inspector__StyledInspectorBody", componentId: "sc-do334k-2" })(["flex:1;overflow-y:auto;display:flex;flex-direction:column;"]); /* Wraps the property groups. The Properties/Comments switch above does the separating, so the first group drops its top hairline — no divider directly under the header, matching the left sidebar. Inner groups keep separators. */ export const StyledPropGroups = styled.div.withConfig({ displayName: "inspector__StyledPropGroups", componentId: "sc-do334k-3" })(["display:flex;flex-direction:column;> div:first-child{border-top:none;}"]); export const StyledInspectorHeader = styled.div.withConfig({ displayName: "inspector__StyledInspectorHeader", componentId: "sc-do334k-4" })(["display:flex;align-items:center;gap:8px;height:56px;padding:0 16px;box-sizing:border-box;flex-shrink:0;.inspector-type-icon{display:flex;align-items:center;justify-content:center;width:20px;height:20px;flex-shrink:0;color:var(--color-theme-700);}.inspector-type-icon svg{width:20px;height:20px;}.inspector-name{flex:1;min-width:0;}.inspector-name-static{padding:0 4px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}"]); /* Empty state — same muted treatment the outline uses for unnamed rows. */ export const StyledInspectorEmpty = styled.div.withConfig({ displayName: "inspector__StyledInspectorEmpty", componentId: "sc-do334k-5" })(["flex:1;display:flex;align-items:center;justify-content:center;padding:24px;text-align:center;color:var(--color-theme-600);"]); /* Non-collapsible section: bold title, no chevron / drag handle, separated from the section above by a hairline. */ export const StyledPropGroup = styled.div.withConfig({ displayName: "inspector__StyledPropGroup", componentId: "sc-do334k-6" })(["display:flex;flex-direction:column;gap:12px;padding:16px;border-top:1px solid var(--border-primary);.group-title{color:var(--color-theme-800);}"]); export const StyledPropRow = styled.div.withConfig({ displayName: "inspector__StyledPropRow", componentId: "sc-do334k-7" })(["display:flex;flex-direction:column;gap:4px;"]); export const StyledFieldGrid = styled.div.withConfig({ displayName: "inspector__StyledFieldGrid", componentId: "sc-do334k-8" })(["display:grid;grid-template-columns:1fr 1fr;gap:12px;"]); /* InputNumber's wrapper is a fixed 360px and SelectTrigger a fixed 300px — both overflow the narrow dock, so force every inspector control to fill its cell. */ export const StyledControl = styled.div.withConfig({ displayName: "inspector__StyledControl", componentId: "sc-do334k-9" })(["width:100%;&.disabled{opacity:0.4;pointer-events:none;}.c-input-wrapper{width:100%;}.wb-inspector-trigger{width:100%;}.wb-color-value{display:flex;align-items:center;gap:8px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.wb-color-value .wb-transparent-swatch{flex:none;color:var(--color-theme-500);}"]); /* Locked checkbox row — the DS Checkbox renders only the box, so it is paired with a Label the way the design system expects. */ export const StyledCheckboxRow = styled.div.withConfig({ displayName: "inspector__StyledCheckboxRow", componentId: "sc-do334k-10" })(["display:flex;align-items:center;gap:8px;"]); /* The element body is HTML (the API's `body` field). The Inspector renders it formatted and read-only for now — editing comes later. Raw text needs an explicit font-family (no global reset), and the child tags get light prose styling. */ export const StyledBody = styled.div.withConfig({ displayName: "inspector__StyledBody", componentId: "sc-do334k-11" })(["width:100%;max-height:140px;overflow-y:auto;font-family:-apple-system,BlinkMacSystemFont,\"Roboto\",\"Helvetica Neue\",Arial,sans-serif;font-size:13px;line-height:1.5;color:var(--color-theme-800);p{margin:0 0 8px;}p:last-child{margin-bottom:0;}ul,ol{margin:0 0 8px;padding-left:18px;}li{margin:2px 0;}strong{font-weight:600;}em{font-style:italic;}a{color:var(--color-primary);}"]); /* Read-only "Auto" field for an auto-sized dimension (a sticky's height). */ export const StyledAutoField = styled.div.withConfig({ displayName: "inspector__StyledAutoField", componentId: "sc-do334k-12" })(["height:32px;display:flex;align-items:center;padding:0 8px;box-sizing:border-box;border:1px solid var(--color-theme-400);border-radius:var(--ac-br-8);background-color:var(--color-theme-200);color:var(--color-theme-600);font-size:13px;"]); export const StyledSegmented = styled.div.withConfig({ displayName: "inspector__StyledSegmented", componentId: "sc-do334k-13" })(["display:flex;gap:4px;svg{display:block;width:16px;height:16px;}"]); export const NumberField = _ref => { let label = _ref.label, value = _ref.value, min = _ref.min, unit = _ref.unit, onCommit = _ref.onCommit; return /*#__PURE__*/React.createElement(StyledPropRow, null, /*#__PURE__*/React.createElement(Label, { size: "small", weight: "regular" }, label), /*#__PURE__*/React.createElement(StyledControl, null, /*#__PURE__*/React.createElement(InputNumber, { className: "wb-inspector-num", value: value, decimalLength: 0, min: min, update: true, endAdornment: unit ? /*#__PURE__*/React.createElement(InputAdornment, { disablePointerEvents: true }, /*#__PURE__*/React.createElement(Caption1, { color: "secondary" }, unit)) : undefined, onChange: next => { if (next !== undefined && !Number.isNaN(next)) { onCommit(Math.round(next)); } } }))); }; export const AutoField = _ref2 => { let label = _ref2.label; return /*#__PURE__*/React.createElement(StyledPropRow, null, /*#__PURE__*/React.createElement(Label, { size: "small", weight: "regular" }, label), /*#__PURE__*/React.createElement(StyledAutoField, null, "Auto")); }; /* A geometry value the viewer may READ but not change — the same field `AutoField` and the file card's Size already use, with a real value in it. Showing the number greyed out is the point: a limited card still has a position and a size, and those are the only facts about it there are. */ export const ReadOnlyField = _ref3 => { let label = _ref3.label, value = _ref3.value; return /*#__PURE__*/React.createElement(StyledPropRow, null, /*#__PURE__*/React.createElement(Label, { size: "small", weight: "regular" }, label), /*#__PURE__*/React.createElement(StyledAutoField, null, value)); }; export const OptionSelect = _ref4 => { let options = _ref4.options, value = _ref4.value, triggerLabel = _ref4.triggerLabel, onChange = _ref4.onChange; return /*#__PURE__*/React.createElement(StyledControl, null, /*#__PURE__*/React.createElement(Select, { type: "single", disableSearch: true, forceCloseMenu: true, options: options, selected: value, onChange: next => onChange(Array.isArray(next) ? next[0] : next), target: /*#__PURE__*/React.createElement(SelectTrigger, { className: "wb-inspector-trigger" }, triggerLabel) })); }; export const TRANSPARENT_OPTION = { id: "transparent", name: "Transparent", color: "transparent", textColor: "transparent" }; /* The name shown on the color picker's trigger, resolved from the current hex. */ export const colorName = hex => { if (!hex || sameColor(hex, "transparent")) { return "Transparent"; } const match = COLOR_OPTIONS.find(option => sameColor(hex, option.color)); return match ? match.name : "Custom"; }; /* The regular design-system named-colour picker (the one in the label dialogs): a Select whose trigger is a Dot + colour name, and whose menu is the standard dot-and-radio list. Custom colours are deferred. */ export const ColorPickerField = _ref5 => { let value = _ref5.value, disabled = _ref5.disabled, includeTransparent = _ref5.includeTransparent, onSelect = _ref5.onSelect; const options = includeTransparent ? [TRANSPARENT_OPTION, ...COLOR_OPTIONS] : COLOR_OPTIONS; const isTransparent = !value || sameColor(value, "transparent"); return /*#__PURE__*/React.createElement(StyledControl, { className: disabled ? "disabled" : undefined }, /*#__PURE__*/React.createElement(Select, { type: "single", disableSearch: true, forceCloseMenu: true, options: options, selected: isTransparent ? "transparent" : value, onChange: next => onSelect(String(Array.isArray(next) ? next[0] : next)), target: /*#__PURE__*/React.createElement(SelectTrigger, { className: "wb-inspector-trigger" }, /*#__PURE__*/React.createElement("span", { className: "wb-color-value" }, isTransparent ? /*#__PURE__*/React.createElement(CircleSlashIcon, { width: 14, height: 14, fill: "currentColor", className: "wb-transparent-swatch" }) : /*#__PURE__*/React.createElement(Dot, { color: value, size: 14 }), colorName(value))) })); }; export const RightInspector = _ref6 => { var _sizes$def$id, _positions$def$id, _rotations$def$id, _names$def$id, _shape$align, _BORDER_STYLE_OPTIONS, _BORDER_STYLE_OPTIONS2; let selectedId = _ref6.selectedId, positions = _ref6.positions, setPositions = _ref6.setPositions, sizes = _ref6.sizes, setSizes = _ref6.setSizes, rotations = _ref6.rotations, setRotations = _ref6.setRotations, shapes = _ref6.shapes, setShapes = _ref6.setShapes, stickyStyle = _ref6.stickyStyle, setStickyStyle = _ref6.setStickyStyle, links = _ref6.links, setLinks = _ref6.setLinks, files = _ref6.files, setFiles = _ref6.setFiles, createdShapes = _ref6.createdShapes, projectElements = _ref6.projectElements, setProjectElements = _ref6.setProjectElements, locks = _ref6.locks, setLocks = _ref6.setLocks; const _useState = useState(OUTLINE_WIDTH), width = _useState[0], setWidth = _useState[1]; const _useState2 = useState(false), resizing = _useState2[0], setResizing = _useState2[1]; /* Mock only — element display names are not persisted anywhere; they default to the type label until renamed here. */ const _useState3 = useState({}), names = _useState3[0], setNames = _useState3[1]; /* Which inspector tab is showing: the property groups we design, or the upcoming comments stream (details TBD). */ const _useState4 = useState("properties"), tab = _useState4[0], setTab = _useState4[1]; /* Docked right, so the grab strip is on the LEFT edge and dragging left widens: the delta is subtracted rather than added (see LeftSidebar). */ const startResize = useCallback(event => { var _event$currentTarget$, _event$currentTarget$2; event.preventDefault(); const startX = event.clientX; const startWidth = (_event$currentTarget$ = (_event$currentTarget$2 = event.currentTarget.parentElement) == null ? void 0 : _event$currentTarget$2.offsetWidth) != null ? _event$currentTarget$ : 0; const previousUserSelect = document.body.style.userSelect; setResizing(true); document.body.style.userSelect = "none"; document.body.style.cursor = "col-resize"; const handleMove = moveEvent => { const next = startWidth - (moveEvent.clientX - startX); setWidth(Math.min(OUTLINE_MAX_WIDTH, Math.max(OUTLINE_MIN_WIDTH, next))); }; const handleUp = () => { setResizing(false); document.body.style.userSelect = previousUserSelect; document.body.style.cursor = ""; window.removeEventListener("mousemove", handleMove); window.removeEventListener("mouseup", handleUp); }; window.addEventListener("mousemove", handleMove); window.addEventListener("mouseup", handleUp); }, []); const def = selectedId ? [...CANVAS_ELEMENTS, ...createdShapes].find(element => element.id === selectedId) : undefined; const linkDef = selectedId ? links.find(link => link.id === selectedId) : undefined; const fileDef = selectedId ? files.find(file => file.id === selectedId) : undefined; const projectDef = selectedId ? projectElements.find(entry => entry.id === selectedId) : undefined; const shell = content => /*#__PURE__*/React.createElement(StyledInspectorSidebar, { style: { width } }, /*#__PURE__*/React.createElement(StyledInspectorResizer, { className: resizing ? "resizing" : "", onMouseDown: startResize, role: "separator", "aria-orientation": "vertical", "aria-label": "Resize inspector" }), content); /* Description — inline, multi-line editable text, shown first for every element kind. */ /* Read-only formatted body (HTML from the API's `body` field). No label; when the body is empty the field is not rendered at all. No edit yet. */ const renderBody = id => { var _ELEMENT_BODIES$id; const html = (_ELEMENT_BODIES$id = ELEMENT_BODIES[id]) != null ? _ELEMENT_BODIES$id : ""; if (!html) { return null; } // eslint-disable-next-line react/no-danger return /*#__PURE__*/React.createElement(StyledBody, { dangerouslySetInnerHTML: { __html: html } }); }; /* The Locked checkbox — shown for every element kind, toggling a design lock flag keyed by element id. */ /* `forced` is the limited-access case: the element is locked FOR THIS VIEWER and the checkbox is the honest place to say so — ticked, and disabled so it reads as a statement about the element rather than a switch that was left on. */ const renderLocked = function (id, forced) { var _locks$id; if (forced === void 0) { forced = false; } return /*#__PURE__*/React.createElement(StyledCheckboxRow, null, /*#__PURE__*/React.createElement(Checkbox, { id: "wb-lock-" + id, checked: forced || ((_locks$id = locks[id]) != null ? _locks$id : false), disabled: forced, onChange: () => setLocks(current => { var _current$id; return _extends({}, current, { [id]: !((_current$id = current[id]) != null ? _current$id : false) }); }) }), /*#__PURE__*/React.createElement(Label, { htmlFor: "wb-lock-" + id, size: "small", weight: "regular" }, "Locked")); }; /* Geometry for a card (link / file / project / task): X/Y from the shared positions map, an editable width and an auto height (cards are content-sized). Cards cannot be rotated, so there is no Rotation field. `readOnly` keeps the same three values in the same three places and swaps the inputs for read-only fields — a limited card's geometry is information, not a control. */ const renderCardGeometry = function (id, cardWidth, onWidth, readOnly) { var _positions$id; if (readOnly === void 0) { readOnly = false; } const cardPosition = (_positions$id = positions[id]) != null ? _positions$id : { x: 0, y: 0 }; return /*#__PURE__*/React.createElement(StyledPropGroup, null, /*#__PURE__*/React.createElement(Body2, { weight: "bold", className: "group-title" }, "Geometry"), /*#__PURE__*/React.createElement(StyledFieldGrid, null, readOnly ? /*#__PURE__*/React.createElement(ReadOnlyField, { label: "X", value: cardPosition.x + " px" }) : /*#__PURE__*/React.createElement(NumberField, { label: "X", unit: "px", value: cardPosition.x, onCommit: next => setPositions(current => { var _current$id2; return _extends({}, current, { [id]: _extends({}, (_current$id2 = current[id]) != null ? _current$id2 : { x: 0, y: 0 }, { x: next }) }); }) }), readOnly ? /*#__PURE__*/React.createElement(ReadOnlyField, { label: "Y", value: cardPosition.y + " px" }) : /*#__PURE__*/React.createElement(NumberField, { label: "Y", unit: "px", value: cardPosition.y, onCommit: next => setPositions(current => { var _current$id3; return _extends({}, current, { [id]: _extends({}, (_current$id3 = current[id]) != null ? _current$id3 : { x: 0, y: 0 }, { y: next }) }); }) })), /*#__PURE__*/React.createElement(StyledFieldGrid, null, readOnly ? /*#__PURE__*/React.createElement(ReadOnlyField, { label: "Width", value: cardWidth + " px" }) : /*#__PURE__*/React.createElement(NumberField, { label: "Width", unit: "px", value: cardWidth, min: 1, onCommit: onWidth }), /*#__PURE__*/React.createElement(AutoField, { label: "Height" }))); }; /* Header + panel switch + body, shared by every element kind — only the groups inside differ. `renamable` false swaps the live EditableText for read-only text, for elements whose name is not the board's to change. */ const renderInspectorBody = function (icon, headerName, id, groups, renamable) { if (renamable === void 0) { renamable = true; } return /*#__PURE__*/React.createElement(StyledInspectorBody, null, /*#__PURE__*/React.createElement(StyledInspectorHeader, null, /*#__PURE__*/React.createElement("span", { className: "inspector-type-icon" }, icon), /*#__PURE__*/React.createElement("span", { className: "inspector-name" }, renamable ? /*#__PURE__*/React.createElement(EditableText, { value: headerName, variant: "Header 3", weight: "bold", onSave: event => setNames(current => _extends({}, current, { [id]: event.target.value })), inputProps: { maxLength: 120 } }) : /*#__PURE__*/React.createElement(Header3, { className: "inspector-name-static", title: headerName }, headerName))), /*#__PURE__*/React.createElement(StyledChooseRow, null, /*#__PURE__*/React.createElement(StyledPanelChooseSlot, null, /*#__PURE__*/React.createElement(ChooseV2, { required: true, options: [{ id: "properties", name: "Properties" }, { id: "comments", name: "Comments" }], selected: [tab], onChange: selected => setTab(selected[0]) }))), tab === "comments" ? /*#__PURE__*/React.createElement(StyledInspectorEmpty, null, /*#__PURE__*/React.createElement(Body2, null, "The comments stream is coming soon.")) : /*#__PURE__*/React.createElement(StyledPropGroups, null, groups)); }; /* Nothing selected. */ if (!def && !linkDef && !fileDef && !projectDef) { return shell(/*#__PURE__*/React.createElement(StyledInspectorEmpty, null, /*#__PURE__*/React.createElement(Body2, null, "Select an element to see its properties."))); } /* Project / task card — the most constrained element here. Its look is not the board's to change (no Fill, Stroke or Text), and its title belongs to the record it shows, so the header is read-only. What is left is what the board really does own: where the card sits, how wide it is, and whether it is locked. */ if (projectDef) { const limited = projectDef.limited === true; const cardTraits = limited ? LIMITED_ELEMENT_TRAITS : PROJECT_ELEMENT_TRAITS; return shell(renderInspectorBody( /* A limited card gets a NEUTRAL icon. The task and project glyphs would each say which kind of record is behind the card, and the kind is part of what the viewer is not entitled to. */ limited ? /*#__PURE__*/React.createElement(LockIcon, null) : projectDef.kind === "project" ? /*#__PURE__*/React.createElement(ProjectIcon, null) : /*#__PURE__*/React.createElement(TaskIcon, null), projectElementTitle(projectDef), projectDef.id, /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(StyledPropGroup, null, renderLocked(projectDef.id, cardTraits.readOnly)), cardTraits.geometry ? renderCardGeometry(projectDef.id, projectDef.width, width => setProjectElements(current => current.map(entry => entry.id === projectDef.id ? _extends({}, entry, { width }) : entry)), cardTraits.readOnly) : null), cardTraits.renamable)); } /* Link card — its URL plus the shared Locked flag. */ if (linkDef) { var _names$linkDef$id, _linkDef$url; return shell(renderInspectorBody(/*#__PURE__*/React.createElement(InsertLinkIcon, null), (_names$linkDef$id = names[linkDef.id]) != null ? _names$linkDef$id : linkDef.url || "Link", linkDef.id, /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(StyledPropGroup, null, renderBody(linkDef.id), /*#__PURE__*/React.createElement(StyledPropRow, null, /*#__PURE__*/React.createElement(Label, { size: "small", weight: "regular" }, "URL"), /*#__PURE__*/React.createElement(StyledControl, null, /*#__PURE__*/React.createElement(Input, { value: (_linkDef$url = linkDef.url) != null ? _linkDef$url : "", placeholder: "https://\u2026", onChange: event => setLinks(current => current.map(link => link.id === linkDef.id ? _extends({}, link, { url: event.target.value }) : link)) }))), renderLocked(linkDef.id)), renderCardGeometry(linkDef.id, linkDef.width, width => setLinks(current => current.map(link => link.id === linkDef.id ? _extends({}, link, { width }) : link)))))); } /* File card — read-only size plus the shared Locked flag. */ if (fileDef) { var _names$fileDef$id; return shell(renderInspectorBody(/*#__PURE__*/React.createElement(AttachmentIcon, null), (_names$fileDef$id = names[fileDef.id]) != null ? _names$fileDef$id : fileDef.file ? fileDef.file.base + "." + fileDef.file.extension : "File", fileDef.id, /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(StyledPropGroup, null, renderBody(fileDef.id), /*#__PURE__*/React.createElement(StyledPropRow, null, /*#__PURE__*/React.createElement(Label, { size: "small", weight: "regular" }, "Size"), /*#__PURE__*/React.createElement(StyledAutoField, null, fileDef.file ? formatFileSize(fileDef.file.size) : "—")), renderLocked(fileDef.id)), renderCardGeometry(fileDef.id, fileDef.width, width => setFiles(current => current.map(file => file.id === fileDef.id ? _extends({}, file, { width }) : file)))))); } /* Shape / sticky from here on. */ if (!def) { return shell(/*#__PURE__*/React.createElement(StyledInspectorEmpty, null, /*#__PURE__*/React.createElement(Body2, null, "Select an element to see its properties."))); } const traits = elementTraits(def); const shape = def.kind === "shape" ? shapes[def.id] : null; const size = (_sizes$def$id = sizes[def.id]) != null ? _sizes$def$id : { width: def.width, height: def.height }; const position = (_positions$def$id = positions[def.id]) != null ? _positions$def$id : { x: def.x, y: def.y }; const rotation = (_rotations$def$id = rotations[def.id]) != null ? _rotations$def$id : 0; const typeLabel = shape ? TYPE_LABEL[shape.type] : "Sticky note"; const name = (_names$def$id = names[def.id]) != null ? _names$def$id : typeLabel; const updateShape = partial => setShapes(current => _extends({}, current, { [def.id]: _extends({}, current[def.id], partial) })); /* Fill is unified across kinds: a shape writes shape.fill, a sticky its note colour. A transparent colour means no fill. */ const currentFill = shape ? shape.fill : stickyStyle.color; const currentFillOpacity = shape ? shape.fillOpacity : stickyStyle.fillOpacity; const setFill = color => { if (shape) { updateShape({ fill: color }); } else { setStickyStyle(_extends({}, stickyStyle, { color })); } }; const setFillOpacity = next => { if (shape) { updateShape({ fillOpacity: next }); } else { setStickyStyle(_extends({}, stickyStyle, { fillOpacity: next })); } }; const currentFontSize = shape ? shape.fontSize : stickyStyle.fontSize; const currentAlign = shape ? (_shape$align = shape.align) != null ? _shape$align : "left" : stickyStyle.align; const setFontSize = next => { if (shape) { updateShape({ fontSize: next }); } else { setStickyStyle(_extends({}, stickyStyle, { fontSize: next })); } }; const setAlign = next => { if (shape) { updateShape({ align: next }); } else { setStickyStyle(_extends({}, stickyStyle, { align: next })); } }; const styleFlags = shape != null ? shape : stickyStyle; const toggleStyle = key => { if (shape) { updateShape({ [key]: !shape[key] }); } else { setStickyStyle(_extends({}, stickyStyle, { [key]: !stickyStyle[key] })); } }; return shell(renderInspectorBody(shape ? SHAPE_TYPE_ICON[shape.type] : /*#__PURE__*/React.createElement(NoteIcon, null), name, def.id, /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(StyledPropGroup, null, renderBody(def.id), shape ? /*#__PURE__*/React.createElement(StyledPropRow, null, /*#__PURE__*/React.createElement(Label, { size: "small", weight: "regular" }, "Shape"), /*#__PURE__*/React.createElement(OptionSelect, { options: ["rectangle", "roundedRectangle", "circle", "triangle"].map(shapeType => ({ id: shapeType, name: TYPE_LABEL[shapeType] })), value: shape.type, triggerLabel: TYPE_LABEL[shape.type], onChange: next => updateShape({ type: next }) })) : null, renderLocked(def.id)), traits.geometry ? /*#__PURE__*/React.createElement(StyledPropGroup, null, /*#__PURE__*/React.createElement(Body2, { weight: "bold", className: "group-title" }, "Geometry"), /*#__PURE__*/React.createElement(StyledFieldGrid, null, /*#__PURE__*/React.createElement(NumberField, { label: "X", unit: "px", value: position.x, onCommit: next => setPositions(current => _extends({}, current, { [def.id]: _extends({}, current[def.id], { x: next }) })) }), /*#__PURE__*/React.createElement(NumberField, { label: "Y", unit: "px", value: position.y, onCommit: next => setPositions(current => _extends({}, current, { [def.id]: _extends({}, current[def.id], { y: next }) })) })), /*#__PURE__*/React.createElement(StyledFieldGrid, null, traits.autoWidth ? /*#__PURE__*/React.createElement(AutoField, { label: "Width" }) : /*#__PURE__*/React.createElement(NumberField, { label: "Width", unit: "px", value: size.width, min: 1, onCommit: next => setSizes(current => { var _current$def$id$heigh, _current$def$id; return _extends({}, current, { [def.id]: { width: next, height: (_current$def$id$heigh = (_current$def$id = current[def.id]) == null ? void 0 : _current$def$id.height) != null ? _current$def$id$heigh : def.height } }); }) }), traits.autoHeight ? /*#__PURE__*/React.createElement(AutoField, { label: "Height" }) : /*#__PURE__*/React.createElement(NumberField, { label: "Height", unit: "px", value: size.height, min: 1, onCommit: next => setSizes(current => { var _current$def$id$width, _current$def$id2; return _extends({}, current, { [def.id]: { width: (_current$def$id$width = (_current$def$id2 = current[def.id]) == null ? void 0 : _current$def$id2.width) != null ? _current$def$id$width : def.width, height: next } }); }) })), /*#__PURE__*/React.createElement(StyledFieldGrid, null, /*#__PURE__*/React.createElement(NumberField, { label: "Rotation", unit: "\xB0", value: rotation, onCommit: next => setRotations(current => _extends({}, current, { [def.id]: next })) }))) : null, traits.fill ? /*#__PURE__*/React.createElement(StyledPropGroup, null, /*#__PURE__*/React.createElement(Body2, { weight: "bold", className: "group-title" }, "Fill"), /*#__PURE__*/React.createElement(StyledFieldGrid, null, /*#__PURE__*/React.createElement(StyledPropRow, null, /*#__PURE__*/React.createElement(Label, { size: "small", weight: "regular" }, "Color"), /*#__PURE__*/React.createElement(ColorPickerField, { includeTransparent: traits.fillTransparent, value: currentFill, onSelect: setFill })), /*#__PURE__*/React.createElement(StyledPropRow, null, /*#__PURE__*/React.createElement(Label, { size: "small", weight: "regular" }, "Opacity"), /*#__PURE__*/React.createElement(OptionSelect, { options: OPACITY_OPTIONS.map(opacity => ({ id: opacity, name: opacity + "%" })), value: currentFillOpacity, triggerLabel: currentFillOpacity + "%", onChange: next => setFillOpacity(Number(next)) })))) : null, traits.stroke && shape ? /*#__PURE__*/React.createElement(StyledPropGroup, null, /*#__PURE__*/React.createElement(Body2, { weight: "bold", className: "group-title" }, "Stroke"), /*#__PURE__*/React.createElement(StyledPropRow, null, /*#__PURE__*/React.createElement(Label, { size: "small", weight: "regular" }, "Pattern"), /*#__PURE__*/React.createElement(StyledSegmented, null, /*#__PURE__*/React.createElement(ChooseV2, { required: true, options: BORDER_STYLE_OPTIONS.map(option => ({ id: option.label, name: option.label })), selected: [(_BORDER_STYLE_OPTIONS = (_BORDER_STYLE_OPTIONS2 = BORDER_STYLE_OPTIONS.find(option => option.value === shape.strokeDash)) == null ? void 0 : _BORDER_STYLE_OPTIONS2.label) != null ? _BORDER_STYLE_OPTIONS : "Solid"], onChange: selected => { var _BORDER_STYLE_OPTIONS3; return updateShape({ strokeDash: (_BORDER_STYLE_OPTIONS3 = BORDER_STYLE_OPTIONS.find(option => option.label === selected[0])) == null ? void 0 : _BORDER_STYLE_OPTIONS3.value }); } }))), /*#__PURE__*/React.createElement(StyledFieldGrid, null, /*#__PURE__*/React.createElement(StyledPropRow, null, /*#__PURE__*/React.createElement(Label, { size: "small", weight: "regular" }, "Thickness"), /*#__PURE__*/React.createElement(OptionSelect, { options: THICKNESS_OPTIONS.map(thickness => ({ id: thickness, name: thickness + " pt" })), value: shape.strokeWidth, triggerLabel: shape.strokeWidth + " pt", onChange: next => updateShape({ strokeWidth: Number(next) }) })), /*#__PURE__*/React.createElement(StyledPropRow, null, /*#__PURE__*/React.createElement(Label, { size: "small", weight: "regular" }, "Color"), /*#__PURE__*/React.createElement(ColorPickerField, { value: shape.stroke, onSelect: color => updateShape({ stroke: color }) }))), /*#__PURE__*/React.createElement(StyledFieldGrid, null, /*#__PURE__*/React.createElement(StyledPropRow, null, /*#__PURE__*/React.createElement(Label, { size: "small", weight: "regular" }, "Radius"), /*#__PURE__*/React.createElement(OptionSelect, { options: RADIUS_OPTIONS.map(radius => ({ id: radius, name: radius + " px" })), value: shape.borderRadius, triggerLabel: shape.borderRadius + " px", onChange: next => updateShape({ borderRadius: Number(next) }) })), /*#__PURE__*/React.createElement(StyledPropRow, null, /*#__PURE__*/React.createElement(Label, { size: "small", weight: "regular" }, "Opacity"), /*#__PURE__*/React.createElement(OptionSelect, { options: OPACITY_OPTIONS.map(opacity => ({ id: opacity, name: opacity + "%" })), value: shape.strokeOpacity, triggerLabel: shape.strokeOpacity + "%", onChange: next => updateShape({ strokeOpacity: Number(next) }) })))) : null, traits.text ? /*#__PURE__*/React.createElement(StyledPropGroup, null, /*#__PURE__*/React.createElement(Body2, { weight: "bold", className: "group-title" }, "Text"), /*#__PURE__*/React.createElement(StyledFieldGrid, null, /*#__PURE__*/React.createElement(StyledPropRow, null, /*#__PURE__*/React.createElement(Label, { size: "small", weight: "regular" }, "Style"), /*#__PURE__*/React.createElement(StyledSegmented, null, STYLE_OPTIONS.map(option => /*#__PURE__*/React.createElement(Tooltip, { key: option.key, title: option.label }, /*#__PURE__*/React.createElement(IconButton, { variant: "text gray", active: styleFlags[option.key], "aria-label": option.label, onClick: () => toggleStyle(option.key) }, option.icon))))), /*#__PURE__*/React.createElement(StyledPropRow, null, /*#__PURE__*/React.createElement(Label, { size: "small", weight: "regular" }, "Size"), /*#__PURE__*/React.createElement(OptionSelect, { options: FONT_SIZE_OPTIONS.map(fontSize => ({ id: fontSize, name: fontSize + " pt" })), value: currentFontSize, triggerLabel: currentFontSize + " pt", onChange: next => setFontSize(Number(next)) }))), /*#__PURE__*/React.createElement(StyledPropRow, null, /*#__PURE__*/React.createElement(Label, { size: "small", weight: "regular" }, "Alignment"), /*#__PURE__*/React.createElement(StyledSegmented, null, ALIGN_OPTIONS.map(option => /*#__PURE__*/React.createElement(Tooltip, { key: option.value, title: option.label }, /*#__PURE__*/React.createElement(IconButton, { variant: "text gray", active: currentAlign === option.value, "aria-label": option.label, onClick: () => setAlign(option.value) }, option.icon)))))) : null))); }; //# sourceMappingURL=inspector.js.map