UNPKG

@carbon/ibm-products

Version:

Carbon for IBM Products

745 lines (718 loc) 25.9 kB
/** * Copyright IBM Corp. 2020, 2025 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; var _rollupPluginBabelHelpers = require('../../../_virtual/_rollupPluginBabelHelpers.js'); var React = require('react'); var index = require('../../../_virtual/index.js'); var cx = require('classnames'); var react = require('@carbon/react'); var layout = require('@carbon/layout'); var PageHeaderUtils = require('../PageHeaderUtils.js'); var overflowHandler = require('./overflowHandler.js'); var utilities = require('@carbon/utilities'); var useOverflowItems = require('../../../global/js/hooks/useOverflowItems/useOverflowItems.js'); var useId = require('../../../global/js/utils/useId.js'); var icons = require('@carbon/react/icons'); var context = require('./context.js'); var utils = require('./utils.js'); var settings = require('../../../settings.js'); var useResizeObserver = require('../../../global/js/hooks/useResizeObserver.js'); /** * ---------- * PageHeader * ---------- */ const PageHeader = /*#__PURE__*/React.forwardRef(function PageHeader(_ref, ref) { let { className, children, ...other } = _ref; const [refs, setRefs] = React.useState({}); const tempRef = React.useRef(null); const componentRef = ref ?? tempRef; const classNames = cx({ [`${PageHeaderUtils.blockClass}`]: true, [`${PageHeaderUtils.blockClass}__next`]: true }, className); // Used to set CSS custom property with PageHeaderContent height to be used // for sticky positioning useResizeObserver.useResizeObserver(componentRef, () => { if (componentRef?.current && refs?.contentRef?.current) { const pageHeaderContentHeight = refs?.contentRef?.current?.offsetHeight; const totalHeaderOffset = utils.getHeaderOffset(componentRef?.current); componentRef?.current.style.setProperty(`--${settings.pkg.prefix}-page-header-header-top`, `${(Math.round(pageHeaderContentHeight) - totalHeaderOffset) * -1}px`); componentRef?.current.style.setProperty(`--${settings.pkg.prefix}-page-header-breadcrumb-top`, `${totalHeaderOffset}px`); } }); const [fullyCollapsed, setFullyCollapsed] = React.useState(false); const [titleClipped, setTitleClipped] = React.useState(false); // Intersection Observer setup, tracks if the PageHeaderContent is visible on page. // If it is not visible, we should set fully collapsed to true so that the // scroller button will know if it is clicked to expand rather than // collapse the header. React.useEffect(() => { if (!refs?.contentRef || !componentRef?.current) { return; } const totalHeaderOffset = utils.getHeaderOffset(componentRef?.current); const predefinedContentPadding = 24; const contentObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.target === refs?.contentRef.current) { setFullyCollapsed(!entry.isIntersecting); } }); }, { root: null, rootMargin: `${(predefinedContentPadding + totalHeaderOffset + 40) * -1}px 0px 0px 0px`, threshold: 0.1 }); if (!refs?.titleRef?.current) { return; } const totalTitleHeight = refs?.titleRef.current.offsetHeight; const titleObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.target === refs?.titleRef.current) { setTitleClipped(!entry.isIntersecting); } }); }, { root: null, rootMargin: `${(predefinedContentPadding + totalTitleHeight + totalHeaderOffset + 24) * -1}px 0px 0px 0px`, threshold: 0.1 }); if (refs?.contentRef.current) { contentObserver.observe(refs?.contentRef.current); } if (refs?.titleRef.current) { titleObserver.observe(refs?.titleRef.current); } return () => { if (!refs?.contentRef?.current) { return; } contentObserver.unobserve(refs?.contentRef.current); if (!refs?.titleRef?.current) { return; } contentObserver.unobserve(refs?.titleRef.current); }; }, [refs, componentRef]); return /*#__PURE__*/React.createElement(context.PageHeaderContext.Provider, { value: { refs, setRefs, fullyCollapsed, titleClipped } }, /*#__PURE__*/React.createElement("div", _rollupPluginBabelHelpers.extends({ className: classNames, ref: componentRef }, other), children)); }); PageHeader.displayName = 'PageHeader'; /** * ----------------------- * PageHeaderBreadcrumbBar * ----------------------- */ const PageHeaderBreadcrumbBar = /*#__PURE__*/React.forwardRef(function PageHeaderBreadcrumbBar(_ref2, ref) { let { border = true, className, children, renderIcon: IconElement, contentActions, contentActionsFlush, pageActions, pageActionsFlush, ...other } = _ref2; const classNames = cx({ [`${PageHeaderUtils.blockClass}__breadcrumb-bar`]: true, [`${PageHeaderUtils.blockClass}__breadcrumb-bar-border`]: border, [`${PageHeaderUtils.blockClass}__breadcrumb__actions-flush`]: pageActionsFlush }, className); const contentActionsClasses = cx({ [`${PageHeaderUtils.blockClass}__breadcrumb__content-actions`]: !contentActionsFlush }); return /*#__PURE__*/React.createElement("div", _rollupPluginBabelHelpers.extends({ className: classNames, ref: ref }, other), /*#__PURE__*/React.createElement(react.Grid, null, /*#__PURE__*/React.createElement(react.Column, { lg: 16, md: 8, sm: 4 }, /*#__PURE__*/React.createElement("div", { className: `${PageHeaderUtils.blockClass}__breadcrumb-container` }, /*#__PURE__*/React.createElement("div", { className: `${PageHeaderUtils.blockClass}__breadcrumb-wrapper` }, IconElement && /*#__PURE__*/React.createElement("div", { className: `${PageHeaderUtils.blockClass}__breadcrumb__icon` }, /*#__PURE__*/React.createElement(IconElement, null)), children), /*#__PURE__*/React.createElement("div", { className: `${PageHeaderUtils.blockClass}__breadcrumb__actions` }, /*#__PURE__*/React.createElement("div", { className: contentActionsClasses }, contentActions), pageActions))))); }); PageHeaderBreadcrumbBar.displayName = 'PageHeaderBreadcrumbBar'; /** * ----------------- * PageHeaderContent * ----------------- */ const PageHeaderContent = /*#__PURE__*/React.forwardRef(function PageHeaderContent(_ref3, ref) { let { className, children, title, renderIcon: IconElement, contextualActions, pageActions, ...other } = _ref3; const contentRef = React.useRef(null); const componentRef = ref ?? contentRef; const { setRefs } = context.usePageHeader(); const classNames = cx({ [`${PageHeaderUtils.blockClass}__content`]: true }, className); const titleRef = React.useRef(null); React.useEffect(() => { if (componentRef?.current) { setRefs(prev => ({ ...prev, contentRef: componentRef, titleRef })); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const [isEllipsisApplied, setIsEllipsisApplied] = React.useState(false); const isEllipsisActive = element => { setIsEllipsisApplied(element.offsetHeight < element.scrollHeight); return element.offsetHeight < element.scrollHeight; }; React.useLayoutEffect(() => { titleRef.current && isEllipsisActive(titleRef.current); }, [title]); return /*#__PURE__*/React.createElement("div", _rollupPluginBabelHelpers.extends({ className: classNames, ref: componentRef }, other), /*#__PURE__*/React.createElement(react.Grid, null, /*#__PURE__*/React.createElement(react.Column, { lg: 16, md: 8, sm: 4 }, /*#__PURE__*/React.createElement("div", { className: `${PageHeaderUtils.blockClass}__content__title-wrapper` }, /*#__PURE__*/React.createElement("div", { className: `${PageHeaderUtils.blockClass}__content__start` }, /*#__PURE__*/React.createElement("div", { className: `${PageHeaderUtils.blockClass}__content__title-container` }, IconElement && /*#__PURE__*/React.createElement("div", { className: `${PageHeaderUtils.blockClass}__content__icon` }, /*#__PURE__*/React.createElement(IconElement, null)), isEllipsisApplied ? /*#__PURE__*/React.createElement(react.DefinitionTooltip, { definition: title }, /*#__PURE__*/React.createElement(react.unstable_Text, { ref: titleRef, as: "h4", className: `${PageHeaderUtils.blockClass}__content__title` }, title)) : /*#__PURE__*/React.createElement(react.unstable_Text, { ref: titleRef, as: "h4", className: `${PageHeaderUtils.blockClass}__content__title` }, title)), contextualActions && /*#__PURE__*/React.createElement("div", { className: `${PageHeaderUtils.blockClass}__content__contextual-actions` }, contextualActions)), pageActions), children))); }); PageHeaderContent.displayName = 'PageHeaderContent'; PageHeaderContent.propTypes = { /** * Provide child elements to be rendered inside PageHeaderContent. */ children: index.default.node, /** * Specify an optional className to be added to your PageHeaderContent */ className: index.default.string, /** * The PageHeaderContent's contextual actions */ contextualActions: index.default.node, /** * The PageHeaderContent's page actions */ pageActions: index.default.node, /** * Provide an optional icon to render in front of the PageHeaderContent's title. */ renderIcon: index.default.oneOfType([index.default.func, index.default.object]), /** * The PageHeaderContent's subtitle */ subtitle: index.default.string, /** * The PageHeaderContent's title */ title: index.default.string.isRequired }; /** * ---------------- * PageHeaderContentPageActions * ---------------- */ const PageHeaderContentPageActions = _ref4 => { let { className, children, menuButtonLabel = 'Actions', actions, ...other } = _ref4; const classNames = cx({ [`${PageHeaderUtils.blockClass}__content__page-actions`]: true }, className); const containerRef = React.useRef(null); const offsetRef = React.useRef(null); const [menuButtonVisibility, setMenuButtonVisibility] = React.useState(false); const [hiddenItems, setHiddenItems] = React.useState([]); // need to set the grid columns width based on the menu button's width // to avoid overlapping when resizing React.useLayoutEffect(() => { if (menuButtonVisibility && offsetRef.current) { const width = offsetRef.current.offsetWidth; document.documentElement.style.setProperty('--page-header-title-grid-width', `${width}px`); } }, [menuButtonVisibility]); React.useEffect(() => { if (!containerRef.current || !Array.isArray(actions)) { return; } utilities.createOverflowHandler({ container: containerRef.current, // exclude the hidden menu button from children maxVisibleItems: containerRef.current.children.length - 1, onChange: (visible, hidden) => { setHiddenItems(actions?.slice(visible.length)); if (hidden.length > 0) { setMenuButtonVisibility(true); } } }); }, [actions]); return /*#__PURE__*/React.createElement("div", _rollupPluginBabelHelpers.extends({ className: classNames, ref: containerRef }, other), Array.isArray(actions) && /*#__PURE__*/React.createElement(React.Fragment, null, actions.map(action => /*#__PURE__*/React.createElement("div", { key: action.id }, /*#__PURE__*/React.cloneElement(action.body, { ...action.body.props, onClick: action.onClick }))), /*#__PURE__*/React.createElement("span", { "data-offset": true, "data-hidden": true, ref: offsetRef }, /*#__PURE__*/React.createElement(react.MenuButton, { menuAlignment: "bottom-end", label: menuButtonLabel, size: "md" }, [...hiddenItems].reverse().map(item => /*#__PURE__*/React.createElement(react.MenuItem, _rollupPluginBabelHelpers.extends({ key: item.id, onClick: item.onClick }, item.menuItem))))))); }; PageHeaderContentPageActions.displayName = 'PageHeaderContentPageActions'; PageHeaderContentPageActions.propTypes = { /** * The PageHeaderContent's page actions */ actions: index.default.oneOfType([index.default.node, index.default.array]), /** * Provide child elements to be rendered inside PageHeaderContentPageActions. */ children: index.default.node, /** * Specify an optional className to be added to your PageHeaderContentPageActions */ className: index.default.string, /** * The PageHeaderContent's collapsible Menu button label */ menuButtonLabel: index.default.string }; /** * ---------------- * PageHeaderContentText * ---------------- */ const PageHeaderContentText = _ref5 => { let { className, children, subtitle, ...other } = _ref5; const classNames = cx({ [`${PageHeaderUtils.blockClass}__content__body`]: true }, className); return /*#__PURE__*/React.createElement("div", _rollupPluginBabelHelpers.extends({ className: classNames }, other), subtitle && /*#__PURE__*/React.createElement(react.unstable_Text, { as: "h3", className: `${PageHeaderUtils.blockClass}__content__subtitle` }, subtitle), children); }; PageHeaderContentText.displayName = 'PageHeaderContentText'; PageHeaderContentText.propTypes = { /** * Provide child elements to be rendered inside PageHeaderContentText. */ children: index.default.node, /** * Specify an optional className to be added to your PageHeaderContentText */ className: index.default.string, /** * The PageHeaderContent's subtitle */ subtitle: index.default.string }; /** * ---------------- * PageHeaderHeroImage * ---------------- */ const PageHeaderHeroImage = _ref6 => { let { className, children, ...other } = _ref6; const [lgBreakpoint, setLgBreakpoint] = React.useState(false); const classNames = cx({ [`${PageHeaderUtils.blockClass}__hero-image`]: true }, className); const lgMediaQuery = `(min-width: ${layout.breakpoints.lg.width})`; React.useEffect(() => { const listener = event => { setLgBreakpoint(event.matches); }; const mediaQueryList = window.matchMedia(lgMediaQuery); mediaQueryList.addEventListener('change', listener); setLgBreakpoint(mediaQueryList.matches); return () => { mediaQueryList.removeEventListener('change', listener); }; }, [lgMediaQuery]); return /*#__PURE__*/React.createElement(react.AspectRatio, _rollupPluginBabelHelpers.extends({ className: classNames }, other, { ratio: lgBreakpoint ? '2x1' : '3x2' }), children); }; PageHeaderHeroImage.displayName = 'PageHeaderHeroImage'; PageHeaderHeroImage.propTypes = { /** * Provide child elements to be rendered inside PageHeaderHeroImage. */ children: index.default.node, /** * Specify an optional className to be added to your PageHeaderHeroImage */ className: index.default.string }; /** * ---------------- * PageHeaderTabBar * ---------------- */ const PageHeaderTabBar = /*#__PURE__*/React.forwardRef(function PageHeaderTabBar(_ref7, ref) { let { className, children, tags = [], scroller, ...other } = _ref7; const classNames = cx({ [`${PageHeaderUtils.blockClass}__tab-bar`]: true }, className); const renderScroller = () => scroller && /*#__PURE__*/React.createElement("div", { className: `${settings.pkg.prefix}--page-header--scroller-button-container` }, scroller); // Early return if no tags are provided if (!tags.length) { return /*#__PURE__*/React.createElement("div", _rollupPluginBabelHelpers.extends({ className: classNames, ref: ref }, other), /*#__PURE__*/React.createElement(react.Grid, null, /*#__PURE__*/React.createElement(react.Column, { lg: 16, md: 8, sm: 4 }, children, renderScroller()))); } const [openPopover, setOpenPopover] = React.useState(false); const tagSize = tags[0]?.size || 'md'; const instanceId = useId.useId('PageHeaderTabBar'); const tagsWithIds = React.useMemo(() => { return tags.map((tag, index) => ({ ...tag, id: tag.id || `tag-${index}-${instanceId}` })); }, [instanceId, tags]); const tagsContainerRef = React.useRef(null); const offsetRef = React.useRef(null); // To close popover when window resizes React.useEffect(() => { const handleResize = () => { // Close the popover when window resizes to prevent unwanted opens setOpenPopover(false); }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }, []); // overflow items hook const { visibleItems = [], hiddenItems = [], itemRefHandler = () => {} } = useOverflowItems.useOverflowItems(tagsWithIds, tagsContainerRef, offsetRef) || { visibleItems: [], hiddenItems: [], itemRefHandler: () => {} }; const handleOverflowClick = React.useCallback(event => { event.stopPropagation(); setOpenPopover(prev => !prev); }, []); // Function to render tags const renderTags = () => /*#__PURE__*/React.createElement("div", { className: `${PageHeaderUtils.blockClass}__tags`, ref: tagsContainerRef }, visibleItems.map(tag => /*#__PURE__*/React.createElement(react.Tag, { key: tag.id, ref: node => itemRefHandler(tag.id, node), type: tag.type, size: tag.size, className: `${PageHeaderUtils.blockClass}__tag-item` }, tag.text)), hiddenItems.length > 0 && /*#__PURE__*/React.createElement(react.Popover, { open: openPopover, onRequestClose: () => setOpenPopover(false) }, /*#__PURE__*/React.createElement(react.OperationalTag, { onClick: handleOverflowClick, "aria-expanded": openPopover, text: `+${hiddenItems.length}`, size: tagSize }), /*#__PURE__*/React.createElement(react.PopoverContent, { className: "tag-popover-content" }, /*#__PURE__*/React.createElement("div", { className: `${PageHeaderUtils.blockClass}__tags-popover-list` }, hiddenItems.map(tag => /*#__PURE__*/React.createElement(react.Tag, { key: tag.id, type: tag.type, size: tag.size }, tag.text)))))); return /*#__PURE__*/React.createElement("div", _rollupPluginBabelHelpers.extends({ className: classNames, ref: ref }, other), /*#__PURE__*/React.createElement(react.Grid, null, /*#__PURE__*/React.createElement(react.Column, { lg: 16, md: 8, sm: 4 }, /*#__PURE__*/React.createElement("div", { className: cx(`${PageHeaderUtils.blockClass}__tab-bar--tablist`, { [`${settings.pkg.prefix}--page-header__tab-bar--with-scroller`]: !!scroller }) }, children, tags.length > 0 && renderTags(), renderScroller())))); }); PageHeaderTabBar.displayName = 'PageHeaderTabBar'; const PageHeaderScrollButton = /*#__PURE__*/React.forwardRef(function PageHeaderExpander(_ref8, ref) { let { className, children, label, onClick, collapseText = 'Collapse', expandText = 'Expand', ...other } = _ref8; const { refs, fullyCollapsed } = context.usePageHeader(); const handleScroller = isFullyCollapsed => { if (!refs?.contentRef?.current) { return; } const scrollableTarget = utils.scrollableAncestor(refs?.contentRef.current); // Page header content is not fully collapsed if (!isFullyCollapsed) { const pageHeaderContentHeight = refs?.contentRef.current.offsetHeight; scrollableTarget?.scrollTo({ top: pageHeaderContentHeight, // headerTopValue, check if breadcrumb bar is included behavior: 'smooth' }); } else { // Page header content is fully collapsed scrollableTarget?.scrollTo({ top: 0, behavior: 'smooth' }); } }; return /*#__PURE__*/React.createElement(react.IconButton, _rollupPluginBabelHelpers.extends({ ref: ref, label: fullyCollapsed ? expandText : collapseText, size: "md", kind: "ghost", autoAlign: true }, other, { onClick: event => { onClick?.(event); handleScroller(!!fullyCollapsed); }, className: cx(className, `${settings.pkg.prefix}--page-header--scroller-button`) }), /*#__PURE__*/React.createElement(icons.ChevronUp, { className: cx(`${settings.pkg.prefix}--page-header--scroller-button-icon`, { [`${settings.pkg.prefix}--page-header--scroller-button-icon-collapsed`]: fullyCollapsed }) })); }); const PageHeaderTitleBreadcrumb = /*#__PURE__*/React.forwardRef((_ref9, ref) => { let { className, children, ...other } = _ref9; const { titleClipped, refs } = context.usePageHeader(); return /*#__PURE__*/React.createElement(react.BreadcrumbItem, _rollupPluginBabelHelpers.extends({ ref: ref, isCurrentPage: true }, other, { className: cx(className, `${settings.pkg.prefix}--page-header-title-breadcrumb`, { [`${settings.pkg.prefix}--page-header-title-breadcrumb-show`]: titleClipped && refs?.titleRef }) }), children); }); // This component is a wrapper for the Breadcrumb, and renders breadcrumb items as children // including the overflow breadcrumb item. The overflowHandler determines what elements // are visible and hidden and passes the hidden elements back to the render prop used // to display the overflow breadcrumb const PageHeaderBreadcrumbOverflow = /*#__PURE__*/React.forwardRef((_ref10, ref) => { let { renderOverflowBreadcrumb, className, children, ...other } = _ref10; const [hiddenBreadcrumbs, setHiddenBreadcrumbs] = React.useState([]); const fallbackRef = React.useRef(null); const componentRef = ref ?? fallbackRef; // Initialize overflow resize handler const carbonPrefix = react.usePrefix(); React.useEffect(() => { if (!componentRef) { return; } const breadcrumbList = componentRef?.current.querySelector(`.${carbonPrefix}--breadcrumb`); overflowHandler.createOverflowHandler({ container: breadcrumbList, onChange: (_, hidden) => { setHiddenBreadcrumbs(hidden); } }); // Don't want ref or carbon prefix in dependency array // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const renderChildren = () => { // Only BreadcrumbItems and TitleBreadcrumbs are valid children const filteredBreadcrumbs = React.Children.toArray(children).filter(child => { if (/*#__PURE__*/React.isValidElement(child)) { return child.type === react.BreadcrumbItem || PageHeaderTitleBreadcrumb; } }); // We need to clone the renderProp for the overflow breadcrumb item // to place it before the title breadcrumb according to the design if (filteredBreadcrumbs) { const overflowBreadcrumb = renderOverflowBreadcrumb?.(hiddenBreadcrumbs); // If no overflow breadcrumb provided, return here with the rest of the children if (!overflowBreadcrumb) { return children; } const clonedTitleBreadcrumb = /*#__PURE__*/React.cloneElement(overflowBreadcrumb, { key: 'cloned overflow breadcrumb item', 'data-fixed': true, className: cx(`${settings.pkg.prefix}--page-header-breadcrumb-overflow-item`, { [`${settings.pkg.prefix}--page-header-overflow-breadcrumb-item-with-items`]: hiddenBreadcrumbs.length }) }); const clonedChildren = [...filteredBreadcrumbs]; clonedChildren.splice(filteredBreadcrumbs.length - 1, 0, clonedTitleBreadcrumb); // second to last position return clonedChildren; } return children; }; return /*#__PURE__*/React.createElement(react.Breadcrumb, _rollupPluginBabelHelpers.extends({ className: cx(className, `${settings.pkg.prefix}--page-header-breadcrumb-overflow`), ref: componentRef }, other), renderChildren()); }); /** * ------- * Exports * ------- */ const Root = PageHeader; Root.displayName = 'PageHeader.Root'; const BreadcrumbBar = PageHeaderBreadcrumbBar; BreadcrumbBar.displayName = 'PageHeaderBreadcrumbBar'; const Content = PageHeaderContent; Content.displayName = 'PageHeaderContent'; const ContentPageActions = PageHeaderContentPageActions; ContentPageActions.displayName = 'PageHeaderContentPageActions'; const ContentText = PageHeaderContentText; ContentText.displayName = 'PageHeaderContentText'; const HeroImage = PageHeaderHeroImage; HeroImage.displayName = 'PageHeaderHeroImage'; const TabBar = PageHeaderTabBar; TabBar.displayName = 'PageHeaderTabBar'; const ScrollButton = PageHeaderScrollButton; ScrollButton.displayName = 'PageHeaderScrollButton'; const TitleBreadcrumb = PageHeaderTitleBreadcrumb; TitleBreadcrumb.displayName = 'PageHeaderTitleBreadcrumb'; const BreadcrumbOverflow = PageHeaderBreadcrumbOverflow; BreadcrumbOverflow.displayName = 'PageHeaderBreadcrumbOverflow'; exports.BreadcrumbBar = BreadcrumbBar; exports.BreadcrumbOverflow = BreadcrumbOverflow; exports.Content = Content; exports.ContentPageActions = ContentPageActions; exports.ContentText = ContentText; exports.HeroImage = HeroImage; exports.PageHeader = PageHeader; exports.PageHeaderBreadcrumbBar = PageHeaderBreadcrumbBar; exports.PageHeaderBreadcrumbOverflow = PageHeaderBreadcrumbOverflow; exports.PageHeaderContent = PageHeaderContent; exports.PageHeaderContentPageActions = PageHeaderContentPageActions; exports.PageHeaderContentText = PageHeaderContentText; exports.PageHeaderHeroImage = PageHeaderHeroImage; exports.PageHeaderScrollButton = PageHeaderScrollButton; exports.PageHeaderTabBar = PageHeaderTabBar; exports.PageHeaderTitleBreadcrumb = PageHeaderTitleBreadcrumb; exports.Root = Root; exports.ScrollButton = ScrollButton; exports.TabBar = TabBar; exports.TitleBreadcrumb = TitleBreadcrumb;