@activecollab/components
Version:
ActiveCollab Components
357 lines (338 loc) • 18.8 kB
JavaScript
"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