@activecollab/components
Version:
ActiveCollab Components
335 lines (309 loc) • 13.3 kB
JavaScript
import React, { useCallback, useContext, useState } from "react";
import styled from "styled-components";
import { screenBelow } from "../../components/BreakPoints";
import { Button } from "../../components/Button";
import { Columns } from "../../components/Columns";
import { IconButton } from "../../components/IconButton";
import { CancelCrossIcon, ThumbUpOutlineIcon } from "../../components/Icons";
import { InfoBox } from "../../components/InfoBox";
import { Media } from "../../components/Media";
import { Modal } from "../../components/Modal";
import { StyledLink as InlineLink } from "../../components/PortableText/Styles";
import { Row } from "../../components/Row";
import { Stack } from "../../components/Stack";
import { Tiles } from "../../components/Tiles";
import { TitledText } from "../../components/TitledText";
import { Tooltip } from "../../components/Tooltip";
import { Body2 } from "../../components/Typography";
import { Typography } from "../../components/Typography/Typography";
/**
* The in-app message dialog, as a Presentation mock.
*
* Two pieces, mirroring how the product is split:
*
* - {@link MessageDialog} — the hollow dialog shell (width, closing,
* feedback), built only from design-system parts. In the product this shell
* lives in the application as `InAppMessageDialog`.
* - {@link messageComponents} — a demo message element catalog injected into
* the design-system Portable Text engine, exactly like the typed cards
* injected into the hollow StackedCard shell. In the product the catalog is
* application code; the presentation ships this demo copy so the stories
* need no app imports.
*/
/* ---- the dialog shell ---------------------------------------------- */
const DIALOG_WIDTH = {
sm: "520px",
md: "720px",
lg: "880px"
};
/* Feedback — the thumbs up / down slot. Down-vote reuses the up icon,
rotated (the DS set only ships ThumbUpOutlineIcon). */
const StyledFeedback = styled.div.withConfig({
displayName: "MessageDialog__StyledFeedback",
componentId: "sc-1kt5lil-0"
})(["display:flex;align-items:center;justify-content:center;gap:8px;padding:8px 24px 28px;", "{padding:4px 24px 24px;}.fb-question{margin-right:4px;}.fb-btn svg{width:18px;height:18px;}.fb-down svg{transform:rotate(180deg);}"], screenBelow.sm);
const DialogFeedback = _ref => {
let onVote = _ref.onVote;
const _useState = useState(null),
voted = _useState[0],
setVoted = _useState[1];
const vote = useCallback(value => {
setVoted(value);
onVote == null || onVote(value);
}, [onVote]);
return /*#__PURE__*/React.createElement(StyledFeedback, null, voted ? /*#__PURE__*/React.createElement(Body2, {
color: "secondary"
}, "Thanks for your feedback!") : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Body2, {
color: "secondary",
className: "fb-question"
}, "Was this helpful?"), /*#__PURE__*/React.createElement(Tooltip, {
title: "Yes, helpful"
}, /*#__PURE__*/React.createElement(IconButton, {
type: "button",
variant: "text gray",
className: "fb-btn",
"aria-label": "Yes, helpful",
onClick: () => vote("up")
}, /*#__PURE__*/React.createElement(ThumbUpOutlineIcon, null))), /*#__PURE__*/React.createElement(Tooltip, {
title: "No, not helpful"
}, /*#__PURE__*/React.createElement(IconButton, {
type: "button",
variant: "text gray",
className: "fb-btn fb-down",
"aria-label": "No, not helpful",
onClick: () => vote("down")
}, /*#__PURE__*/React.createElement(ThumbUpOutlineIcon, null)))));
};
/* The shell is behaviour only: it sizes itself, closes on click-outside /
Esc (DS Modal) / the X, and shows the feedback slot when an onVote handler
is supplied. Content is whatever the caller renders as children — usually
the Portable Text engine with the demo catalog injected. */
const StyledDialog = styled.div.withConfig({
displayName: "MessageDialog__StyledDialog",
componentId: "sc-1kt5lil-1"
})(["position:relative;box-sizing:border-box;display:flex;flex-direction:column;width:calc(100% - 32px);max-width:", ";max-height:calc(100vh - 96px);margin:56px auto 40px;overflow-y:auto;background-color:var(--page-paper-main);color:var(--color-theme-900);border-radius:12px;box-shadow:var(--shadow-primary);", "{margin:16px auto;}.dlg-close{position:absolute;top:12px;right:12px;z-index:2;}.dlg-content{padding:40px;}", "{.dlg-content{padding:32px 20px 24px;}}"], _ref2 => {
let $size = _ref2.$size;
return DIALOG_WIDTH[$size];
}, screenBelow.sm, screenBelow.sm);
export const MessageDialog = _ref3 => {
let open = _ref3.open,
onClose = _ref3.onClose,
_ref3$size = _ref3.size,
size = _ref3$size === void 0 ? "md" : _ref3$size,
onVote = _ref3.onVote,
children = _ref3.children;
return /*#__PURE__*/React.createElement(Modal, {
open: open,
onClose: onClose
}, /*#__PURE__*/React.createElement(StyledDialog, {
$size: size
}, /*#__PURE__*/React.createElement("div", {
className: "dlg-close"
}, /*#__PURE__*/React.createElement(Tooltip, {
title: "Close"
}, /*#__PURE__*/React.createElement(IconButton, {
type: "button",
variant: "text gray",
"aria-label": "Close",
onClick: onClose
}, /*#__PURE__*/React.createElement(CancelCrossIcon, null)))), /*#__PURE__*/React.createElement("div", {
className: "dlg-content"
}, children), onVote ? /*#__PURE__*/React.createElement(DialogFeedback, {
onVote: onVote
}) : null));
};
/* ---- doc-link wiring ------------------------------------------------ */
/** How a doc-link mark opens its article (by a slug-like reference). The
surface that renders the message supplies the handler — in the product it
closes the dialog and opens the article Sheet. */
export const DocLinkContext = /*#__PURE__*/React.createContext(() => undefined);
/* ---- the demo message element catalog ------------------------------- */
/* Inline marks ------------------------------------------------------- */
/* interface-name — the name of an interface element, e.g. the Security page. */
const StyledInterfaceName = styled.span.withConfig({
displayName: "MessageDialog__StyledInterfaceName",
componentId: "sc-1kt5lil-2"
})(["padding:0 4px;border-radius:4px;background-color:var(--color-theme-300);color:var(--color-theme-900);font-weight:600;white-space:nowrap;"]);
const InterfaceNameMark = _ref4 => {
let children = _ref4.children;
return /*#__PURE__*/React.createElement(StyledInterfaceName, null, children);
};
/* keyboard-key — a keyboard key cap. */
const StyledKeyboardKey = styled.kbd.withConfig({
displayName: "MessageDialog__StyledKeyboardKey",
componentId: "sc-1kt5lil-3"
})(["display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;border:1px solid var(--border-primary);border-bottom-width:2px;border-radius:5px;background-color:var(--color-theme-200);color:var(--color-theme-900);font-family:ui-monospace,\"SFMono-Regular\",Menlo,Consolas,monospace;font-size:11px;font-weight:600;line-height:1;vertical-align:baseline;"]);
const KeyboardKeyMark = _ref5 => {
let children = _ref5.children;
return /*#__PURE__*/React.createElement(StyledKeyboardKey, null, children);
};
/* doc-link — an inline link that opens a full help article. The mark carries
an article reference; the open behaviour comes from DocLinkContext, so the
authored content stays declarative. */
/* Built on the engine's inline anchor so a doc-link sits in the sentence
exactly like a default link mark — same size, weight and line box as the
surrounding copy — just recoloured. */
const StyledDocLink = styled(InlineLink).withConfig({
displayName: "MessageDialog__StyledDocLink",
componentId: "sc-1kt5lil-4"
})(["color:var(--color-secondary);text-decoration-color:var(--color-secondary);"]);
const DocLinkMark = _ref6 => {
let mark = _ref6.mark,
children = _ref6.children;
const openArticle = useContext(DocLinkContext);
return /*#__PURE__*/React.createElement(StyledDocLink, {
href: "#",
onClick: event => {
event.preventDefault();
openArticle(mark == null ? void 0 : mark.article);
}
}, children);
};
/* Component objects --------------------------------------------------- */
/* callout — rendered with the DS InfoBox; the catalog keeps its own tone
vocabulary and maps it to InfoBox types. */
const CALLOUT_TYPE = {
info: "default",
note: "note",
success: "success"
};
const CalloutObject = _ref7 => {
var _ref8;
let node = _ref7.node,
children = _ref7.children;
return /*#__PURE__*/React.createElement(InfoBox, {
type: CALLOUT_TYPE[(_ref8 = node.tone) != null ? _ref8 : "info"],
title: node.title,
showIcon: true
}, children);
};
/* button-reference — a real DS Button shown for reference as its own block,
so a message can point at a button instead of screenshotting it. It is
intentionally non-interactive. */
const StyledButtonReference = styled.div.withConfig({
displayName: "MessageDialog__StyledButtonReference",
componentId: "sc-1kt5lil-5"
})(["display:flex;.c-button{pointer-events:none;}"]);
const ButtonReferenceObject = _ref9 => {
var _ref0;
let node = _ref9.node;
if (typeof node.label !== "string") {
return null;
}
return /*#__PURE__*/React.createElement(StyledButtonReference, {
"aria-hidden": "true"
}, /*#__PURE__*/React.createElement(Button, {
variant: (_ref0 = node.variant) != null ? _ref0 : "primary",
size: "small",
type: "button",
tabIndex: -1
}, node.label));
};
/* row — a horizontal run of blocks (e.g. two button references). */
const RowObject = _ref1 => {
var _ref10;
let node = _ref1.node,
children = _ref1.children;
return /*#__PURE__*/React.createElement(Row, {
space: (_ref10 = node.space) != null ? _ref10 : "sm"
}, children);
};
/* tiles — an equal-size mosaic of blocks. */
const TilesObject = _ref11 => {
var _ref12;
let node = _ref11.node,
children = _ref11.children;
return /*#__PURE__*/React.createElement(Tiles, {
columns: (_ref12 = node.columns) != null ? _ref12 : 3,
space: "md",
collapseBelow: "sm"
}, children);
};
/* split — text beside media, with the one layout rule the messages follow:
screenshots sit on the right, flat illustrations on the left. `widths` is
authored as [text, media] and reordered with the columns. */
const SplitObject = _ref13 => {
var _ref14;
let node = _ref13.node,
children = _ref13.children;
const media = node.media;
const widths = (_ref14 = node.widths) != null ? _ref14 : [1, 1];
const textColumn = /*#__PURE__*/React.createElement(Stack, {
space: "lg"
}, children);
// A split without media degrades to its text column instead of crashing.
if (!(media != null && media.src)) {
return textColumn;
}
const screenshot = media.variant === "screenshot";
const mediaColumn = /*#__PURE__*/React.createElement(Media, {
src: media.src,
alt: media.alt,
variant: media.variant,
caption: media.caption
});
return /*#__PURE__*/React.createElement(Columns, {
widths: screenshot ? widths : [...widths].reverse(),
space: "xl",
alignY: "center",
collapseBelow: "md"
}, screenshot ? textColumn : mediaColumn, screenshot ? mediaColumn : textColumn);
};
/* Default-block overrides --------------------------------------------- */
/* The engine's defaults use Body 1 primary; the messages are designed on the
quieter Body 2 secondary voice, so the catalog overrides the text blocks —
demonstrating the `blocks` half of the injection contract. */
const ParagraphBlock = _ref15 => {
let children = _ref15.children;
return /*#__PURE__*/React.createElement(Body2, {
color: "secondary",
lineHeight: "loose"
}, children);
};
const ListItemBlock = _ref16 => {
let children = _ref16.children;
return /*#__PURE__*/React.createElement(Typography, {
variant: "Body 2",
as: "li",
color: "secondary",
lineHeight: "loose"
}, children);
};
/* Titled sections speak in the quiet Header2 subhead voice rather than the
engine's default Title2 for level 2, and keep a roomier rhythm between a
titled section's content nodes. */
const TitledTextBlock = _ref17 => {
let block = _ref17.block,
children = _ref17.children;
const _ref18 = block,
title = _ref18.title,
level = _ref18.level;
return /*#__PURE__*/React.createElement(TitledText, {
title: title,
variant: level === 1 ? "title1" : "header2"
}, children ? /*#__PURE__*/React.createElement(Stack, {
space: "sm"
}, children) : undefined);
};
/** The demo catalog the presentations inject into the Portable Text engine —
the stand-in for the application's `messageComponents` map. */
export const messageComponents = {
marks: {
"interface-name": InterfaceNameMark,
"keyboard-key": KeyboardKeyMark,
"doc-link": DocLinkMark
},
objects: {
callout: CalloutObject,
"button-reference": ButtonReferenceObject,
row: RowObject,
tiles: TilesObject,
split: SplitObject
},
blocks: {
paragraph: ParagraphBlock,
"list-item": ListItemBlock,
"titled-text": TitledTextBlock
}
};
//# sourceMappingURL=MessageDialog.js.map