@activecollab/components
Version:
ActiveCollab Components
366 lines (346 loc) • 17.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.LinkSkeleton = exports.LinkPlaceholder = exports.LinkCard = void 0;
var _react = _interopRequireDefault(require("react"));
var _styledComponents = _interopRequireDefault(require("styled-components"));
var _shared = require("./shared");
var _unfurl = require("./unfurl");
var _Icons = require("../../../components/Icons");
var _Loaders = require("../../../components/Loaders");
var _StackedCard = require("../../../components/StackedCard");
var _Typography = require("../../../components/Typography");
var _Typography2 = require("../../../components/Typography/Typography");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
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); }
/* There is no global CSS reset in this Storybook, so a raw <button> falls back
to the UA font (Arial) instead of the DS stack. Every button below is text, so
they all need it. */
var CONTROL_FONT = "-apple-system, BlinkMacSystemFont, \"Roboto\",\n \"Helvetica Neue\", Arial, sans-serif";
/**
* LinkCard — a link preview as a LIFECYCLE, not two static layouts.
*
* Data climbs a progressive ladder (url → basic → full); the anatomy is
* CONSTANT at every tier, so each slot swaps a skeleton for real content in
* place and the card never reflows on upgrade. Four statuses layer on top:
* resolving — climbing the ladder, aspect + rows reserved by skeletons
* resolved — the full card
* stale — resolved, but past its cache window: shows `fetchedAt` and a
* re-unfurl affordance
* error — the unfurl failed: favicon + raw URL + retry, NO media
*
* Compact ↔ expanded is a user-switchable DISPLAY LEVEL on the same link
* entity (the data lives in the controller, so switching never refetches or
* loses it) — not two separate card types.
*/
/* ------------------------------------------------------------------ */
/* Small building blocks */
/* ------------------------------------------------------------------ */
var StyledDomainRow = _styledComponents.default.div.withConfig({
displayName: "LinkCard__StyledDomainRow",
componentId: "sc-qyea73-0"
})(["display:flex;align-items:center;gap:8px;min-height:16px;"]);
var StyledChips = _styledComponents.default.div.withConfig({
displayName: "LinkCard__StyledChips",
componentId: "sc-qyea73-1"
})(["display:flex;align-items:center;gap:6px;min-height:18px;"]);
/* The stale strip — a re-unfurl affordance with the fetch time. */
var StyledStaleStrip = _styledComponents.default.div.withConfig({
displayName: "LinkCard__StyledStaleStrip",
componentId: "sc-qyea73-2"
})(["display:flex;align-items:center;gap:8px;color:var(--color-theme-600);> span{flex:1 1 auto;min-width:0;}"]);
var StyledReunfurl = _styledComponents.default.button.withConfig({
displayName: "LinkCard__StyledReunfurl",
componentId: "sc-qyea73-3"
})(["display:inline-flex;align-items:center;gap:4px;flex:0 0 auto;padding:3px 8px;border:1px solid var(--border-primary);border-radius:6px;background:var(--page-paper-main);color:var(--color-theme-800);font-family:", ";font-size:11px;cursor:pointer;svg{width:12px;height:12px;}&:hover{background:var(--color-theme-200);border-color:var(--sc-selection-ring);}"], CONTROL_FONT);
/* The error body — favicon + raw URL + retry, no media (per spec §5). It is a
column row like any other: the padding comes from the role, so the error
state lines up with the states it replaces instead of setting its own inset.
Only the gap is widened — three stacked blocks, not four text lines. */
var StyledErrorRow = (0, _styledComponents.default)(_StackedCard.StackedCardRow).withConfig({
displayName: "LinkCard__StyledErrorRow",
componentId: "sc-qyea73-4"
})(["gap:10px;.err-head{display:flex;align-items:center;gap:8px;color:var(--red-alert);}.err-head svg{width:16px;height:16px;flex:0 0 auto;}.err-url{display:flex;align-items:center;gap:8px;word-break:break-all;}"]);
var StyledRetry = _styledComponents.default.button.withConfig({
displayName: "LinkCard__StyledRetry",
componentId: "sc-qyea73-5"
})(["align-self:flex-start;display:inline-flex;align-items:center;gap:6px;padding:5px 10px;border:1px solid var(--border-primary);border-radius:6px;background:var(--page-paper-main);color:var(--color-theme-800);font-family:", ";cursor:pointer;svg{width:14px;height:14px;}&:hover{background:var(--color-theme-200);border-color:var(--sc-selection-ring);}"], CONTROL_FONT);
/* The resolved cover. A production unfurl stores the OpenGraph image, so when
the ladder returns one we render a real <img> (the Cover fill already gives
it `object-fit: cover`); the fill colour is the fallback for links that carry
no image at all. */
var resolvedCover = function resolvedCover(data) {
var _data$heroColor;
return data.heroSrc ? /*#__PURE__*/_react.default.createElement("img", {
src: data.heroSrc,
alt: ""
}) : /*#__PURE__*/_react.default.createElement(_shared.StyledColorCover, {
$color: (_data$heroColor = data.heroColor) !== null && _data$heroColor !== void 0 ? _data$heroColor : "#1D9E75"
});
};
/* The expanded footer band — tinted, holds the title + domain under the hero. */
var StyledFooterTitle = (0, _styledComponents.default)(_Typography.Body2).withConfig({
displayName: "LinkCard__StyledFooterTitle",
componentId: "sc-qyea73-6"
})(["display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;"]);
/* ------------------------------------------------------------------ */
/* The display-level switch — lives in the entity "…" slot */
/* ------------------------------------------------------------------ */
var StyledDisplayToggle = _styledComponents.default.button.withConfig({
displayName: "LinkCard__StyledDisplayToggle",
componentId: "sc-qyea73-7"
})(["display:inline-flex;align-items:center;padding:2px 8px;border:1px solid var(--border-primary);border-radius:10px;background:var(--color-theme-200);color:var(--color-theme-700);font-family:", ";font-size:11px;cursor:pointer;white-space:nowrap;&:hover{background:var(--color-theme-300);}"], CONTROL_FONT);
var DisplayToggle = function DisplayToggle(_ref) {
var display = _ref.display,
onToggle = _ref.onToggle;
return /*#__PURE__*/_react.default.createElement(StyledDisplayToggle, {
type: "button",
"data-fixed": true,
className: _StackedCard.STACKED_CARD_CONTROL_CLASS,
"aria-label": "Switch display level",
onClick: onToggle
}, display === "compact" ? "Expand ▾" : "Compact ▴");
};
/* ------------------------------------------------------------------ */
/* LinkCard */
/* ------------------------------------------------------------------ */
var LinkCard = exports.LinkCard = function LinkCard(_ref2) {
var _data$faviconColor2;
var unfurl = _ref2.unfurl,
_ref2$display = _ref2.display,
display = _ref2$display === void 0 ? "compact" : _ref2$display,
onToggleDisplay = _ref2.onToggleDisplay;
var status = unfurl.status,
tier = unfurl.tier,
data = unfurl.data,
fetchedAt = unfurl.fetchedAt,
retry = unfurl.retry,
reunfurl = unfurl.reunfurl;
// ── Error: favicon + raw URL + retry, no media ──────────────────
if (status === "error") {
return /*#__PURE__*/_react.default.createElement(StyledErrorRow, {
role: "custom",
direction: "column"
}, /*#__PURE__*/_react.default.createElement("div", {
className: "err-head"
}, /*#__PURE__*/_react.default.createElement(_Icons.WarningTriangleIcon, null), /*#__PURE__*/_react.default.createElement(_Typography.Caption1, {
weight: "bold",
color: "tertiary"
}, "Couldn\u2019t load a preview")), /*#__PURE__*/_react.default.createElement("div", {
className: "err-url"
}, /*#__PURE__*/_react.default.createElement(_shared.StyledFavicon, {
$bg: "var(--color-theme-400)",
"data-fixed": true
}), /*#__PURE__*/_react.default.createElement(_Typography.Caption1, {
color: "secondary"
}, data.url)), /*#__PURE__*/_react.default.createElement(StyledRetry, {
type: "button",
onClick: retry
}, /*#__PURE__*/_react.default.createElement(_Icons.ArrowRefreshIcon, null), /*#__PURE__*/_react.default.createElement(_Typography2.Typography, {
as: "span",
variant: "Caption 1"
}, "Retry")));
}
var hasBasic = tier === "basic" || tier === "full";
var hasFull = tier === "full";
/* The display level belongs to the host, so the switch only appears when a
host actually owns one. A surface that fixes the card at one level (the
whiteboard pins it to expanded) gets no dead control. */
var toggle = onToggleDisplay ? /*#__PURE__*/_react.default.createElement(DisplayToggle, {
display: display,
onToggle: onToggleDisplay
}) : null;
// ── Expanded: hero + tinted footer band (title + domain) ────────
if (display === "expanded") {
var _data$faviconColor;
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_StackedCard.StackedCardCover, {
aspectRatio: _shared.COVER_RATIO_HERO,
centerSlot: hasFull && data.playable ? _shared.playControl : undefined,
topRightSlot: (0, _shared.coverManagement)()
}, hasFull ? resolvedCover(data) : /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
style: {
width: "100%",
height: "100%"
}
})), /*#__PURE__*/_react.default.createElement(_StackedCard.StackedCardRow, {
role: "footer",
direction: "column",
background: "var(--color-theme-200)"
}, /*#__PURE__*/_react.default.createElement(StyledDomainRow, null, hasBasic ? /*#__PURE__*/_react.default.createElement(_shared.StyledFavicon, {
$bg: (_data$faviconColor = data.faviconColor) !== null && _data$faviconColor !== void 0 ? _data$faviconColor : "#0F6E56",
"data-fixed": true
}) : /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
"data-fixed": true,
style: {
width: 16,
height: 16,
borderRadius: 4
}
}), hasBasic ? /*#__PURE__*/_react.default.createElement(_Typography.Caption1, {
color: "tertiary"
}, data.domain) : /*#__PURE__*/_react.default.createElement(_Typography.Caption1, {
color: "tertiary"
}, data.url), /*#__PURE__*/_react.default.createElement("span", {
style: {
flex: "1 1 auto"
}
}), toggle), hasBasic ? /*#__PURE__*/_react.default.createElement(StyledFooterTitle, {
weight: "bold"
}, data.title) : /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
style: {
height: 14,
width: "90%"
}
}), hasFull ? /*#__PURE__*/_react.default.createElement(_Typography.Caption1, {
color: "tertiary"
}, data.description) : /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
style: {
height: 10,
width: "70%"
}
})), status === "stale" ? /*#__PURE__*/_react.default.createElement(_StackedCard.StackedCardRow, {
role: "meta"
}, /*#__PURE__*/_react.default.createElement(StaleStrip, {
fetchedAt: fetchedAt,
onReunfurl: reunfurl
})) : null);
}
// ── Compact: cover + favicon/domain + title + chips ─────────────
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_StackedCard.StackedCardCover, {
aspectRatio: _StackedCard.STACKED_CARD_COVER_RATIO,
centerSlot: hasFull && data.playable ? _shared.playControl : undefined
}, hasFull ? resolvedCover(data) : /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
style: {
width: "100%",
height: "100%"
}
})), /*#__PURE__*/_react.default.createElement(_StackedCard.StackedCardRow, {
role: "custom"
}, /*#__PURE__*/_react.default.createElement(StyledDomainRow, null, hasBasic ? /*#__PURE__*/_react.default.createElement(_shared.StyledFavicon, {
$bg: (_data$faviconColor2 = data.faviconColor) !== null && _data$faviconColor2 !== void 0 ? _data$faviconColor2 : "#0F6E56",
"data-fixed": true
}) : /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
"data-fixed": true,
style: {
width: 16,
height: 16,
borderRadius: 4
}
}), hasBasic ? /*#__PURE__*/_react.default.createElement(_Typography.Caption1, {
color: "tertiary"
}, data.domain) : /*#__PURE__*/_react.default.createElement(_Typography.Caption1, {
color: "tertiary"
}, data.url), /*#__PURE__*/_react.default.createElement("span", {
style: {
flex: "1 1 auto"
}
}), toggle)), /*#__PURE__*/_react.default.createElement(_StackedCard.StackedCardRow, {
role: "title"
}, hasBasic ? /*#__PURE__*/_react.default.createElement(_shared.StyledClamp, null, data.title) : /*#__PURE__*/_react.default.createElement("div", {
style: {
width: "100%"
}
}, /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
style: {
height: 14,
width: "92%"
}
}), /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
style: {
height: 14,
width: "60%",
marginTop: 6
}
}))), /*#__PURE__*/_react.default.createElement(_StackedCard.StackedCardRow, {
role: "custom"
}, /*#__PURE__*/_react.default.createElement(StyledChips, null, hasFull ? /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, data.kind ? /*#__PURE__*/_react.default.createElement(_shared.StyledChip, {
"data-fixed": true
}, data.kind) : null, data.duration ? /*#__PURE__*/_react.default.createElement(_shared.StyledChip, {
"data-fixed": true
}, data.duration) : null) : /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
style: {
height: 18,
width: 96,
borderRadius: 10
}
}))), status === "stale" ? /*#__PURE__*/_react.default.createElement(_StackedCard.StackedCardRow, {
role: "meta"
}, /*#__PURE__*/_react.default.createElement(StaleStrip, {
fetchedAt: fetchedAt,
onReunfurl: reunfurl
})) : null);
};
/* The stale affordance — fetch time + a manual re-unfurl button. */
var StaleStrip = function StaleStrip(_ref3) {
var fetchedAt = _ref3.fetchedAt,
onReunfurl = _ref3.onReunfurl;
return /*#__PURE__*/_react.default.createElement(StyledStaleStrip, null, /*#__PURE__*/_react.default.createElement(_Typography2.Typography, {
as: "span",
variant: "Caption 1",
color: "tertiary"
}, "Preview may be out of date \xB7 fetched ", (0, _unfurl.formatFetchedAt)(fetchedAt)), /*#__PURE__*/_react.default.createElement(StyledReunfurl, {
type: "button",
onClick: onReunfurl
}, /*#__PURE__*/_react.default.createElement(_Icons.ArrowRefreshIcon, null), "Re-unfurl"));
};
/* ---- skeleton ---- */
/* Mirrors the compact anatomy row for row (this is the `resolving` render at
tier `url`), so a resolving link never reflows as it climbs the ladder. */
var LinkSkeleton = exports.LinkSkeleton = function LinkSkeleton() {
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_StackedCard.StackedCardCover, {
aspectRatio: _StackedCard.STACKED_CARD_COVER_RATIO
}, /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
style: {
width: "100%",
height: "100%"
}
})), /*#__PURE__*/_react.default.createElement(_StackedCard.StackedCardRow, {
role: "custom"
}, /*#__PURE__*/_react.default.createElement(StyledDomainRow, null, /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
"data-fixed": true,
style: {
width: 16,
height: 16,
borderRadius: 4
}
}), /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
style: {
height: 10,
width: "40%"
}
}))), /*#__PURE__*/_react.default.createElement(_StackedCard.StackedCardRow, {
role: "title"
}, /*#__PURE__*/_react.default.createElement("div", {
style: {
width: "100%"
}
}, /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
style: {
height: 14,
width: "92%"
}
}), /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
style: {
height: 14,
width: "60%",
marginTop: 6
}
}))), /*#__PURE__*/_react.default.createElement(_StackedCard.StackedCardRow, {
role: "custom"
}, /*#__PURE__*/_react.default.createElement(StyledChips, null, /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
style: {
height: 18,
width: 96,
borderRadius: 10
}
}))));
};
/* ---- placeholder ---- */
var LinkPlaceholder = exports.LinkPlaceholder = function LinkPlaceholder(props) {
return /*#__PURE__*/_react.default.createElement(_shared.Placeholder, _extends({
icon: /*#__PURE__*/_react.default.createElement(_Icons.InsertLinkIcon, null),
label: "Add a link"
}, props));
};
//# sourceMappingURL=LinkCard.js.map