UNPKG

@activecollab/components

Version:

ActiveCollab Components

357 lines (338 loc) 18.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.shapePreviewGlyph = exports.resolveMockLink = exports.linkDomain = exports.linkColor = exports.isValidLinkUrl = exports.isUnfurlable = exports.clampCardWidth = exports.StyledShapePreview = exports.StyledLinkInput = exports.StyledLinkHint = exports.StyledLinkEditArea = exports.StyledLinkDraft = exports.StyledLinkCard = exports.PRIVATE_HOST = exports.LinkPreviewCard = exports.LinkFormattingToolbar = exports.LinkElement = exports.LINK_PALETTE = exports.LINK_DISPLAY = exports.LINK_DEFAULT_WIDTH = exports.KNOWN_LINKS = exports.CARD_MIN_WIDTH = exports.CARD_MAX_WIDTH = exports.CARD_DRAW_THRESHOLD = void 0; var _react = _interopRequireWildcard(require("react")); var _styledComponents = _interopRequireDefault(require("styled-components")); var _elements = require("./elements"); var _Icons = require("../../components/Icons"); var _StackedCard = require("../../components/StackedCard"); var _Tooltip = require("../../components/Tooltip"); var _Typography = require("../../components/Typography"); var _content = require("../stackedCard/content"); var _useInlineEdit = require("../stackedCard/useInlineEdit"); var _stackedCardCover = _interopRequireDefault(require("../static/stacked-card-cover.png")); var _stackedCardHero = _interopRequireDefault(require("../static/stacked-card-hero.png")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); } function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /* ------------------------------------------------------------------ */ /* Link element — StackedCard + the URL unfurl lifecycle */ /* ------------------------------------------------------------------ */ /** * The only canvas element here built on the shipped design-system * `StackedCard`. The shell is hollow, so the board supplies the visual state * (`selected`) and the width, and the content comes from `LinkCard` — which * renders the unfurl lifecycle, not a static layout. * * Link cards are AUTO-HEIGHT. The host sets the width; the height follows the * content. That is why drawing a box commits only its width, and why resizing * is width-only. */ /* Width bounds shared by every drawn card (link, file). */ var CARD_MIN_WIDTH = exports.CARD_MIN_WIDTH = 180; var CARD_MAX_WIDTH = exports.CARD_MAX_WIDTH = 420; /* Below this the drag reads as a click, not a drawn box. */ var CARD_DRAW_THRESHOLD = exports.CARD_DRAW_THRESHOLD = 8; var clampCardWidth = exports.clampCardWidth = function clampCardWidth(width) { return Math.min(CARD_MAX_WIDTH, Math.max(CARD_MIN_WIDTH, Math.round(width))); }; var LINK_DEFAULT_WIDTH = exports.LINK_DEFAULT_WIDTH = 260; /* A shape check, nothing more. The real validation is server-side: the backend normalizes the URL, refuses anything but http(s), blocks private address space and re-checks every redirect hop. */ var isValidLinkUrl = exports.isValidLinkUrl = function isValidLinkUrl(value) { return /^https?:\/\/[^\s/?#]+\.[^\s]*$/i.test(value.trim()); }; /* https://www.example.com/a?b → example.com */ var linkDomain = exports.linkDomain = function linkDomain(url) { return url.trim().replace(/^https?:\/\//i, "").split(/[/?#]/)[0].replace(/^www\./i, ""); }; /* Stand-in favicon / hero colours. A real unfurl stores the fetched favicon and OpenGraph image as attachments; here the colour is derived from the domain so every pasted host keeps a stable identity instead of sharing one colour. */ var LINK_PALETTE = exports.LINK_PALETTE = ["#0F6E56", "#8C5A2B", "#2F5DA8", "#8A3E6B", "#3F6B25", "#A8452F"]; var linkColor = exports.linkColor = function linkColor(domain, offset) { var hash = 0; for (var index = 0; index < domain.length; index += 1) { hash = (hash * 31 + domain.charCodeAt(index)) % 997; } return LINK_PALETTE[(hash + offset) % LINK_PALETTE.length]; }; /* Hand-written seeds for the hosts worth demoing. Vimeo resolves to a playable video with a real cover image; the history link deliberately resolves without one, so the colour-cover fallback (a link whose page carries no OpenGraph image) is on the board too. */ var KNOWN_LINKS = exports.KNOWN_LINKS = [_objectSpread(_objectSpread({}, _content.MOCK_LINKS.vimeo), {}, { heroSrc: _stackedCardHero.default }), _objectSpread({}, _content.MOCK_LINKS.history)]; /** * The fake "server" response for a URL — what `GET /link-previews/...` would * eventually hold. Known hosts get their seed; anything else resolves to a * generic article keyed off its own domain, so pasting an arbitrary link still * produces a believable card instead of an empty one. */ var resolveMockLink = exports.resolveMockLink = function resolveMockLink(url) { var domain = linkDomain(url); var known = KNOWN_LINKS.find(function (link) { return linkDomain(link.url) === domain; }); if (known) { return _objectSpread(_objectSpread({}, known), {}, { url }); } return { url, domain, faviconColor: linkColor(domain, 0), heroColor: linkColor(domain, 3), heroSrc: _stackedCardCover.default, title: "".concat(domain, " \u2014 the title a real unfurl reads from og:title"), description: "The summary an unfurl takes from og:description, falling back to the page's meta description when the site carries no OpenGraph tags.", kind: "Article", playable: false }; }; /* Private address space — the one class of URL that passes a shape check and still can never resolve, because the server refuses to fetch it (and re-checks every redirect hop). Pasting one of these is how the error rung of the ladder is reachable from the board. */ var PRIVATE_HOST = exports.PRIVATE_HOST = /^(?:10|127|192\.168|169\.254|172\.(?:1[6-9]|2\d|3[01]))\./; var isUnfurlable = exports.isUnfurlable = function isUnfurlable(domain) { return !PRIVATE_HOST.test(domain); }; /** A link element on the board. Position lives in the shared positions map. */ /* On the canvas a link is always the expanded card — hero, then the tinted footer band with the domain, title and description. Compact is a column-view density, so the board pins the level and shows no display switch. */ var LINK_DISPLAY = exports.LINK_DISPLAY = "expanded"; /* The card fills the element box the board drew; the shell's reference 260px width gives way to the host. Height stays auto. */ var StyledLinkCard = exports.StyledLinkCard = (0, _styledComponents.default)(_StackedCard.StackedCard).withConfig({ displayName: "linkElement__StyledLinkCard", componentId: "sc-1e6iu71-0" })(["&&{width:100%;}user-select:none;"]); /* The inline URL editor — the placeholder's interactive half. */ var StyledLinkEditArea = exports.StyledLinkEditArea = _styledComponents.default.div.withConfig({ displayName: "linkElement__StyledLinkEditArea", componentId: "sc-1e6iu71-1" })(["display:flex;flex-direction:column;gap:6px;padding:16px 12px;"]); var StyledLinkInput = exports.StyledLinkInput = _styledComponents.default.input.withConfig({ displayName: "linkElement__StyledLinkInput", componentId: "sc-1e6iu71-2" })(["width:100%;box-sizing:border-box;padding:8px 10px;border:1.5px solid ", ";border-radius:6px;background:var(--page-paper-main);color:var(--color-theme-900);font-family:-apple-system,BlinkMacSystemFont,\"Roboto\",\"Helvetica Neue\",Arial,sans-serif;font-size:13px;text-align:center;outline:none;&::placeholder{color:var(--color-theme-500);}"], function (_ref) { var $invalid = _ref.$invalid; return $invalid ? "var(--red-alert)" : "var(--sc-selection-ring)"; }); var StyledLinkHint = exports.StyledLinkHint = (0, _styledComponents.default)(_Typography.Caption2).withConfig({ displayName: "linkElement__StyledLinkHint", componentId: "sc-1e6iu71-3" })(["display:block;text-align:center;color:var(--red-alert);"]); /* The dashed rubber band drawn while the Link tool is armed. The width readout is the honest signal: the box sets the card's width, the height is the card's own business. */ /* The outline the app's ShapePreview draws — stroke only, the selection blue, at 70% — used both for the shape rubber band and the hover preview. */ var StyledShapePreview = exports.StyledShapePreview = _styledComponents.default.div.withConfig({ displayName: "linkElement__StyledShapePreview", componentId: "sc-1e6iu71-4" })(["position:absolute;box-sizing:border-box;pointer-events:none;svg{display:block;overflow:visible;fill:none;stroke:", ";stroke-width:2;opacity:0.7;}"], _elements.SELECTION_BLUE); var shapePreviewGlyph = exports.shapePreviewGlyph = function shapePreviewGlyph(tool, width, height) { return /*#__PURE__*/_react.default.createElement("svg", { width: "100%", height: "100%" }, tool === "circle" ? /*#__PURE__*/_react.default.createElement("ellipse", { cx: "50%", cy: "50%", rx: "50%", ry: "50%" }) : null, tool === "roundedRectangle" ? /*#__PURE__*/_react.default.createElement("rect", { x: "0", y: "0", width: "100%", height: "100%", rx: 8, ry: 8 }) : null, tool === "triangle" ? /*#__PURE__*/_react.default.createElement("polygon", { points: "".concat(width / 2, ",0 ").concat(width, ",").concat(height, " 0,").concat(height) }) : null); }; var StyledLinkDraft = exports.StyledLinkDraft = _styledComponents.default.div.withConfig({ displayName: "linkElement__StyledLinkDraft", componentId: "sc-1e6iu71-5" })(["position:absolute;box-sizing:border-box;border:1.5px dashed ", ";border-radius:8px;background-color:rgba(59,130,246,0.08);pointer-events:none;.draft-width{position:absolute;top:-22px;left:0;padding:1px 6px;border-radius:4px;background-color:", ";color:#ffffff;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;white-space:nowrap;}"], _elements.SELECTION_BLUE, _elements.SELECTION_BLUE); /* The link element's floating toolbar. It acts on the ENTITY: go to the page, change which page, or bin the card. */ var LinkFormattingToolbar = exports.LinkFormattingToolbar = function LinkFormattingToolbar(_ref2) { var url = _ref2.url, onEditUrl = _ref2.onEditUrl, onDelete = _ref2.onDelete; return /*#__PURE__*/_react.default.createElement(_elements.StyledFormattingToolbar, { onMouseDown: _elements.stopClick, onClick: _elements.stopClick }, /*#__PURE__*/_react.default.createElement(_elements.StyledToolbarLink, { href: url, target: "_blank", rel: "noopener noreferrer" }, /*#__PURE__*/_react.default.createElement(_Icons.OpenExpandedIcon, { className: "i16" }), "Visit"), /*#__PURE__*/_react.default.createElement(_elements.StyledToolbarButton, { type: "button", onClick: onEditUrl }, /*#__PURE__*/_react.default.createElement(_Icons.InsertLinkIcon, { className: "i16" }), "Edit"), /*#__PURE__*/_react.default.createElement("span", { className: "divider" }), /*#__PURE__*/_react.default.createElement(_Tooltip.Tooltip, { title: "Move to Trash" }, /*#__PURE__*/_react.default.createElement(_elements.StyledToolbarButton, { type: "button", className: "icon-only", onClick: onDelete }, /*#__PURE__*/_react.default.createElement(_Icons.TrashIcon, { className: "i16" })))); }; /** * The card's three resize callbacks, handed down untouched from the board. The * grip reports deltas measured from the gesture START and brackets the stream * with a start and a commit, so a card is only a conduit — the board resolves * what a delta means. */ /** * A link element with a committed URL: the card runs the unfurl lifecycle. * * One controller per card, and the seed is memoized on the URL so the board's * own re-renders (drag, selection, a grid change) never restart the ladder. */ var LinkPreviewCard = exports.LinkPreviewCard = function LinkPreviewCard(_ref3) { var _element$url; var element = _ref3.element, selected = _ref3.selected, resize = _ref3.resize, onEditUrl = _ref3.onEditUrl, onDelete = _ref3.onDelete; var url = (_element$url = element.url) !== null && _element$url !== void 0 ? _element$url : ""; var link = (0, _react.useMemo)(function () { return resolveMockLink(url); }, [url]); var unfurl = (0, _content.useUnfurl)({ link, delayMs: 700, shouldFail: !isUnfurlable(link.domain) }); return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(StyledLinkCard, _extends({ selected: selected, resizable: selected, resizeAxis: "width", resizeLabel: "Resize link card" }, resize), /*#__PURE__*/_react.default.createElement(_content.LinkCard, { unfurl: unfurl, display: LINK_DISPLAY })), selected ? /*#__PURE__*/_react.default.createElement(LinkFormattingToolbar, { url: url, onEditUrl: onEditUrl, onDelete: onDelete }) : null); }; /** * The three phases of a link element: * * placeholder drawn but empty — an interactive invitation, distinct from the * loading skeleton (spec §5) * editing the inline URL field, with the shell's `entered` ring and drag * switched off * preview a URL is committed; the unfurl lifecycle takes over */ var LinkElement = exports.LinkElement = function LinkElement(_ref4) { var element = _ref4.element, selected = _ref4.selected, autoEdit = _ref4.autoEdit, onCommitUrl = _ref4.onCommitUrl, resize = _ref4.resize, onDelete = _ref4.onDelete; var edit = (0, _useInlineEdit.useInlineEdit)({ validate: function validate(value) { return isValidLinkUrl(value) ? true : "Enter a valid link, e.g. https://…"; }, onCommit: function onCommit(value) { return onCommitUrl(value.trim()); } }); var begin = edit.begin; // A box drawn with the Link tool is a request to add a link, so the editor // opens straight away; the placeholder is what a cancelled edit falls back to. (0, _react.useEffect)(function () { if (autoEdit) { begin(); } }, [autoEdit, begin]); /* Editing wins over the preview, so the same editor serves the first fill and every later correction: "Edit link" seeds the field with the current URL and Esc drops back to the card that was already there. */ if (edit.editing) { return /*#__PURE__*/_react.default.createElement(StyledLinkCard, { entered: true, draggable: false, "aria-label": "Add a link" }, /*#__PURE__*/_react.default.createElement(StyledLinkEditArea, null, /*#__PURE__*/_react.default.createElement(StyledLinkInput, _extends({}, edit.bind, { type: "text", inputMode: "url", placeholder: "https://\u2026", $invalid: edit.error != null })), edit.error ? /*#__PURE__*/_react.default.createElement(StyledLinkHint, { role: "alert" }, edit.error) : /*#__PURE__*/_react.default.createElement(_Typography.Caption2, { color: "tertiary", style: { textAlign: "center" } }, "Enter to add \xB7 Esc to cancel"))); } if (element.url != null) { return /*#__PURE__*/_react.default.createElement(LinkPreviewCard, { element: element, selected: selected, resize: resize, onEditUrl: function onEditUrl() { var _element$url2; edit.reset((_element$url2 = element.url) !== null && _element$url2 !== void 0 ? _element$url2 : ""); edit.begin(); }, onDelete: onDelete }); } return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(StyledLinkCard, { selected: selected, "aria-label": "Empty link card" }, /*#__PURE__*/_react.default.createElement(_content.LinkPlaceholder, { "data-drag-through": "", onClick: edit.begin })), selected ? /*#__PURE__*/_react.default.createElement(_elements.StyledFormattingToolbar, { onMouseDown: _elements.stopClick, onClick: _elements.stopClick }, /*#__PURE__*/_react.default.createElement(_elements.StyledToolbarButton, { type: "button", onClick: edit.begin }, /*#__PURE__*/_react.default.createElement(_Icons.InsertLinkIcon, { className: "i16" }), "Edit"), /*#__PURE__*/_react.default.createElement("span", { className: "divider" }), /*#__PURE__*/_react.default.createElement(_Tooltip.Tooltip, { title: "Move to Trash" }, /*#__PURE__*/_react.default.createElement(_elements.StyledToolbarButton, { type: "button", className: "icon-only", onClick: onDelete }, /*#__PURE__*/_react.default.createElement(_Icons.TrashIcon, { className: "i16" })))) : null); }; //# sourceMappingURL=linkElement.js.map