@aglyn/plugins-mui 1.0.0-beta.221 → 1.0.0-beta.223

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/mui/src/lib/components/image.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiImage } from '@aglyn/shared-data-mdi'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport Box from '@mui/material/Box'\nimport type { SxProps } from '@mui/material/styles'\nimport { forwardRef, type ReactNode } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n// Component ids are persisted in screen documents; never rename.\nexport const ID: Aglyn.ComponentId = 'image'\n\n/**\n * The node ids of the images that load eagerly: the first image the LAYOUT\n * chain renders and the first image the SCREEN renders, each in document\n * order (AGL-2486).\n *\n * Every image used to render `loading=\"lazy\"` — the hero included. That is\n * the worst possible default for the one image that is almost always the LCP\n * element: a lazy image is not fetched until layout has run and the browser\n * has decided it is near the viewport, and it is fetched at LOW priority when\n * it finally is. Lighthouse reports that as \"LCP request discovery\", and it is\n * also why an image four sections down could finish before the one the reader\n * is looking at: with everything lazy and everything low, nothing outranked\n * anything, so the order was whatever the network felt like.\n *\n * So the lead images get `loading=\"eager\"` and the rest get\n * `fetchpriority=\"low\"`, which is the browser-level knob that stops\n * below-the-fold images competing with the ones above them.\n *\n * ONE lead image was not enough. On any site whose header carries a logo, the\n * first image in document order is that logo, so the screen's own hero — the\n * element Lighthouse names as the LCP on aglyn.com's solution pages at\n * 375x812 — stayed `lazy` + `low`: undiscovered until layout, then fetched\n * behind every script on the page (measured 2026-09-08: 1.5 s of load delay\n * and 1.5 s of load time for a 13 KB image). Composition namespaces layout\n * nodes by origin ({@link Aglyn.isLayoutComposedNodeId}), so the walk keeps\n * one lead PER ORIGIN: the layout's first image (the logo) and the screen's\n * first image (the hero). A layout image further down — a mega-menu\n * illustration, the footer mark — is still deferred, as is every later\n * screen image. In the besigner canvas nothing is layout-composed, so the\n * screen's first image is the only lead, exactly as before.\n *\n * Neither lead gets `fetchpriority=\"high\"`, and the reasoning is at the\n * `fetchPriority` prop below. Short version: \"first image of its origin\" is\n * still not \"the LCP element\", and a ranking claim needs evidence this\n * function does not have.\n *\n * Resolved from the tree rather than a render-order counter on purpose: the\n * renderer walks the tree in document order on the server AND on hydrate, but\n * a mutable counter would double-count under React's concurrent re-renders\n * and hand the priority to a different image on the client than the one the\n * HTML gave it. A pure function of the tree cannot disagree with itself — and\n * it is deliberately NOT memoized on the root object, because the canvas\n * mutates that tree in place while an author works.\n *\n * The walk STOPS once both leads are known — on a page with a header logo\n * and a hero that is O(nodes above the hero), a dozen or two on a normal\n * page — and at the first image on a screen with no layout.\n *\n * An image with no `src` renders a placeholder box and no `<img>` at all, so\n * it cannot be the LCP element and is skipped.\n */\nexport function leadImageNodeIds(\n root: Aglyn.NodeSchema | undefined,\n): readonly string[] {\n let layoutLead: string | undefined\n let screenLead: string | undefined\n const walk = (node: Aglyn.NodeSchema | undefined): boolean => {\n if (!node) return false\n if (node.componentId === ID) {\n const props = (node.resolvedProps ?? node.props ?? {}) as Record<\n string,\n unknown\n >\n if (String(props['src'] ?? '').trim()) {\n if (Aglyn.isLayoutComposedNodeId(node.$id)) {\n if (!layoutLead) layoutLead = node.$id\n } else if (!screenLead) {\n screenLead = node.$id\n }\n if (layoutLead && screenLead) return true\n }\n }\n for (const child of node.children ?? []) {\n if (walk(child)) return true\n }\n return false\n }\n walk(root)\n return [layoutLead, screenLead].filter((id): id is string => Boolean(id))\n}\n\nexport interface ImageProps {\n /**\n * Where the image comes from (AGL-72). Either a **media reference** —\n * `media:{scope}/{mediaId}`, what \"Browse media\" now stores (AGL-1215) —\n * or any URL, which covers both the legacy values already in published\n * documents and an author-typed hotlink. `resolveMediaSrc` decides.\n */\n src?: string\n alt?: string\n /**\n * Explicit decorative choice (AGL-1305): ON forces `alt=\"\"` (and drops\n * the tooltip) no matter what the alt field says, so screen readers skip\n * the image. Distinct from simply leaving alt unset, which also renders\n * `alt=\"\"` today but records no intent.\n */\n decorative?: boolean\n /** Tooltip shown on hover — the native img `title` attribute. */\n title?: string\n /** Native loading hint; unset stays lazy, exactly as before AGL-1305. */\n loading?: 'lazy' | 'eager'\n objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'\n /** CSS width (e.g. \"100%\", \"320px\"); defaults to 100%. */\n width?: string\n /** CSS height (e.g. \"240px\"); defaults to auto. */\n height?: string\n /**\n * The asset's own pixel dimensions, copied off the media document when the\n * image was picked (AGL-2486), and read from the asset again when the page\n * is composed, so a replace with a different shape reaches every page that\n * places it (AGL-2833).\n *\n * NOT author controls, which is why neither appears in the schema below:\n * they describe the file, and the CSS `width`/`height` above describe the\n * placement. The pair becomes the `<img>`'s intrinsic `width`/`height`\n * attributes, which is the only thing that lets the browser reserve the\n * right box before the bytes arrive — with `width: 100%; height: auto` the\n * element is otherwise zero-height until the image decodes, and every image\n * on the page shifts the layout as it lands.\n *\n * An attribute pair is a RATIO here, not a size: CSS wins for the used\n * dimensions either way, so a stale value costs nothing but a reservation\n * of the wrong shape. Both must be present and positive or neither is\n * emitted — one alone gives the browser no ratio and would be read as a\n * real dimension.\n *\n * Read from the node like any other prop, so this element never fetches a\n * media document: the composition has already laid the asset's current\n * pair over the pick's (`media-asset-facts.ts`), and the pick's copy is\n * what renders wherever that read cannot answer. `srcSet` still selects\n * which variant is downloaded; these only say what shape it will be.\n */\n intrinsicWidth?: number\n /** See `intrinsicWidth` — the two are only ever used as a pair. */\n intrinsicHeight?: number\n /** Border radius in px. */\n radius?: number\n /** Target screen id — resolved rename-safe like Screen Link (AGL-339). */\n screenId?: string\n /** External URL, used only when no `screenId` is set. */\n href?: string\n /**\n * Authored node styles, handed over by the renderer rather than typed into\n * an attribute — recomposed below so the author's Styles-panel values are\n * merged rather than replaced (AGL-1240).\n */\n sx?: SxProps\n /**\n * Accepted and dropped: an `<img>` is a void element and React throws on\n * ANY children value reaching one, which 500'd whole pages (AGL-579).\n * Declared because the implementation deliberately discards it (AGL-1323).\n */\n children?: ReactNode\n}\n\n/**\n * Image element (AGL-74): renders a plain img with fit/size/radius\n * controls; an empty src shows a labeled placeholder so the element stays\n * visible and selectable in the editor.\n */\nconst Image = forwardRef<HTMLElement, ImageProps>((props, ref) => {\n const {\n src: storedSrc,\n alt,\n decorative,\n title,\n loading,\n objectFit,\n width,\n height,\n intrinsicWidth,\n intrinsicHeight,\n radius,\n screenId,\n href: externalHref,\n // Never forward children to the <img> below — React throws on ANY\n // children value reaching a void element, which 500'd whole pages\n // when a renderer passed empty JSX children through (AGL-579).\n children: _children,\n // Pull `sx` out of the spread: the literals below are composed AFTER\n // `{...rest}`, so leaving it there REPLACED every style the author set\n // from the Styles panel. The hero mockups' 16px radius and drop shadow\n // were being discarded on every published page (AGL-1240).\n sx: nodeSxProp,\n ...rest\n } = props\n // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).\n const nodeSx = Array.isArray(nodeSxProp)\n ? nodeSxProp\n : nodeSxProp\n ? [nodeSxProp]\n : []\n // Optional link mode (AGL-339): screen id first (rename-safe), external\n // URL as fallback; suppressed in the besigner canvas like Screen Link.\n // Shared with every other linking element since AGL-1335, so a `Link`\n // component prop bound here behaves as it does on a Button.\n const { href: linkHref, suppressNavigation } = Aglyn.useLinkTarget(\n screenId,\n externalHref,\n )\n /**\n * Eagerness (AGL-2486). An explicit author choice always wins — including\n * an explicit `lazy`, so someone who deliberately deferred the top image\n * keeps that. Only an UNSET `loading` is decided here, and only for the\n * lead images: the layout's first and the screen's first.\n *\n * `leafIdsMatch` rather than `===`: a reusable component instance suffixes\n * its leaf ids, so the id in the tree and the id in the context are the\n * same leaf spelled two ways (the markdown block resolves the same way).\n */\n const nodeId = Aglyn.useNodeId()\n const isLeadImage =\n Boolean(nodeId) &&\n leadImageNodeIds(Aglyn.canvas.rootNode).some((leadId) =>\n Aglyn.leafIdsMatch(leadId, nodeId),\n )\n const eager = loading === 'eager' || (loading == null && isLeadImage)\n /**\n * Resolve the stored value to a URL (AGL-1215). A media reference becomes\n * a CDN URL here rather than in the document, so the route shape stays an\n * app concern; every other value — a legacy firebasestorage URL, a legacy\n * `/api/media/cdn/…` path, an author-typed hotlink — passes through.\n *\n * `useSite().hostId` is the site being rendered: present on the tenant and\n * in Preview, absent in the besigner canvas. When it is there it names the\n * asking site in the org scope, which is what lets ONE reference in a\n * layout or reusable component resolve on each site that uses it.\n */\n const { hostId } = Aglyn.useSite()\n const src = Aglyn.resolveMediaSrc(storedSrc, { hostId })\n const wrapLink = (element: JSX.Element) =>\n linkHref && !suppressNavigation ? (\n <AppLink\n componentVariant=\"naked\"\n href={linkHref}\n style={{ display: 'block' }}\n >\n {element}\n </AppLink>\n ) : (\n element\n )\n if (!src) {\n // The labeled box is for the author, so only editing surfaces draw it; a\n // published page renders the bare element.\n if (!suppressNavigation) return <Box ref={ref} {...rest} sx={nodeSx} />\n return (\n <Box\n ref={ref}\n {...rest}\n sx={[\n {\n width: width || '100%',\n height: height || 120,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n border: '1px dashed',\n borderColor: 'divider',\n borderRadius: radius != null ? `${radius}px` : undefined,\n color: 'text.secondary',\n fontSize: 12,\n fontFamily: 'system-ui, sans-serif',\n },\n ...nodeSx,\n ]}\n >\n {'Image — choose a source'}\n </Box>\n )\n }\n // CDN URLs (AGL-175) carry WebP variants selected by `?w=`; widths\n // without a variant fall back to the original server-side, so a static\n // srcSet is safe for any CDN-form URL. Asked of the RESOLVED url, so a\n // reference and a legacy stored path both keep their WebP variants.\n const isCdnUrl = Aglyn.isMediaCdnUrl(src)\n /** A CSS width that is a plain pixel value, and therefore a real `sizes`. */\n const pinnedWidth = /^\\d+(?:\\.\\d+)?px$/.test(String(width ?? '').trim())\n ? String(width).trim()\n : undefined\n /**\n * The intrinsic attribute pair, or nothing.\n *\n * Both-or-neither: the browser derives an aspect-ratio only from the pair,\n * and a lone `width` is read as a real dimension instead — which would\n * reserve a box of the wrong shape rather than no box at all. Finite and\n * positive because a media document may carry `0` or a partial capture\n * (dimensions are best-effort at upload), and `width=\"0\"` collapses the\n * element.\n */\n const usable = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0\n const intrinsicAttributes =\n usable(intrinsicWidth) && usable(intrinsicHeight)\n ? { width: intrinsicWidth, height: intrinsicHeight }\n : undefined\n return wrapLink(\n <Box\n ref={ref}\n component=\"img\"\n src={src}\n // EVERY CANDIDATE IS A `?w=` URL, and the bare one is gone (2026-08-26).\n // Why that is worth 335 KB against 4 KB, and why each candidate merges\n // its width into the url's existing query, are documented where the list\n // and the builder live — `MEDIA_CDN_VARIANT_WIDTHS` and `mediaCdnSrcSet`\n // in `media-ref.ts`.\n //\n // Called rather than restated so the Markdown and entry-body renderers\n // can ask for the same list (AGL-3149). Building it here is the reason\n // they had none: a candidate list inside a component is a candidate list\n // no other component can have.\n srcSet={Aglyn.mediaCdnSrcSet(src)}\n // `sizes` is NOT only a delivery hint, and treating it as one broke every\n // fluid image (AGL-2486). With `w` descriptors the browser derives the\n // image's density-corrected INTRINSIC size from `sizes`, so `sizes` is\n // what a CSS `width: 100%` resolves against whenever the containing block\n // is content-sized — shrink-to-fit, inline-block, a flex item sized on its\n // content. Measured in Chrome at a 1200px viewport with the author CSS\n // `width:100%;height:auto;display:block`:\n //\n // parent sizes=100vw sizes=auto\n // inline-block (shrink-to-fit) 1184px 300px\n // block / fixed-width flex 900px 900px\n //\n // 300px is the spec's default object size, used because resolving `auto`\n // against a content-sized parent is circular. So `sizes=\"auto\"` — which\n // genuinely does pick a better candidate, `?w=320` instead of a 357 KB\n // original in a 158px slot — rendered those images tiny and centred in\n // their box, on the canvas, in _preview and on published sites alike.\n //\n // A delivery win may not be paid for in layout, so this is back to\n // `100vw`: it overfetches, but it is the value every published document\n // was authored against. A pinned pixel width is still the better answer\n // where the author gave one, because it is a definite length and cannot\n // be circular. Getting image delivery right for fluid images needs the\n // media pipeline (a WebP variant at source width) or real intrinsic\n // `width`/`height` attributes from media metadata — neither of which\n // perturbs layout the way `sizes` does.\n sizes={isCdnUrl ? (pinnedWidth ?? '100vw') : undefined}\n // Unset alt keeps rendering `alt=\"\"` exactly as it always has —\n // existing documents must not change output (AGL-1305). Decorative\n // ON forces `alt=\"\"` over any alt text and suppresses the tooltip,\n // so the a11y intent is explicit rather than an accident of blank.\n alt={decorative ? '' : (alt ?? '')}\n title={decorative ? undefined : title || undefined}\n // The ordering signal, ONE-DIRECTIONAL on purpose (AGL-2486).\n //\n // `low` on every deferred image, so a footer image cannot be fetched\n // ahead of the section the reader is in. That half is safe in a way the\n // other half is not: deprioritising an image that is provably not being\n // looked at cannot starve whatever the LCP turns out to be.\n //\n // The lead image gets NO `fetchpriority` — the browser's `auto` — where\n // it used to get `high`. `high` is not a statement about this image, it\n // is a claim that this image outranks everything else in flight,\n // including the stylesheet and the webfont that a TEXT LCP is waiting\n // on. We are not in a position to make that claim: the only evidence\n // behind it was \"first `<img>` in document order\", and document order's\n // first image is the HEADER LOGO on any site whose header has one.\n //\n // Measured on aglyn.com at a 375x812 viewport, which is what sent this\n // back: the element carrying `fetchpriority=\"high\"` was the logo at\n // 145x44 = 6,380 px², while the `<h1>` under it was 343x113 =\n // 38,893 px² — six times the area, and the element Lighthouse named as\n // the LCP. So the hint was being spent to make a text LCP arrive later.\n //\n // `auto` is not a retreat to the old behaviour. The old bug was that\n // everything was `lazy`, so the lead image was not discovered until\n // after layout; it still gets `loading=\"eager\"` above, which is the\n // discovery fix and the part that actually earned the win. What goes is\n // only the RANKING claim, back to Chrome's own in-viewport heuristic —\n // which decides after layout, with the viewport and the geometry this\n // function provably does not have.\n //\n // Deliberately not replaced with a size heuristic: nothing here knows\n // the rendered size. `width`/`height` are optional author CSS strings,\n // routinely `100%`, and a logo constrained by its container measures\n // small while declaring nothing. A guess that fails the same way is not\n // an improvement on a guess.\n //\n // An author who genuinely has an image LCP should be able to SAY so —\n // but not through this control. The `loading` field is labelled\n // \"Loading\" and described in terms of lazy versus eager; an author\n // picking Eager for the top image is not asserting a priority ranking,\n // and reading one out of that choice is how the logo got `high` in the\n // first place. A real priority affordance needs its own control and its\n // own words. After September 1.\n // Decoding off the main thread for the deferred ones — they have no\n // paint deadline, and decoding them synchronously is main-thread time\n // spent on pixels nobody is looking at yet. The eager image keeps the\n // browser's default (`auto`) so it is free to decode in time to paint.\n //\n // The deferred set is `DEFERRED_IMAGE_ATTRIBUTES` rather than three\n // literals because every OTHER `<img>` a published page renders has to\n // land in the same rank to be ranked at all — a product grid, an event\n // list, a cart line. Those carried no hint whatsoever and so were\n // fetched EAGERLY, ahead of anything this component deferred. The set\n // is documented at its definition; the reasoning for each member is\n // the two paragraphs above and the two below.\n {...(eager\n ? { loading: 'eager' as const }\n : Aglyn.DEFERRED_IMAGE_ATTRIBUTES)}\n // Ahead of `{...rest}` so an author who has typed a literal width or\n // height attribute onto the node still wins, and ahead of `sx` because\n // these are ATTRIBUTES: the CSS block below sets the used size, and\n // these only supply the ratio it is laid out against. `Box` in this\n // version applies `styleFunctionSx` alone — it has no system-props\n // layer — so both forward to the `<img>` rather than becoming CSS.\n {...intrinsicAttributes}\n {...rest}\n sx={[\n {\n display: 'block',\n width: width || '100%',\n height: height || 'auto',\n objectFit: objectFit || 'cover',\n borderRadius: radius != null ? `${radius}px` : undefined,\n },\n ...nodeSx,\n ]}\n />,\n )\n})\nImage.displayName = 'Image'\n\n/** Alt text and tooltip make no sense on an explicitly decorative image. */\nconst NOT_DECORATIVE = { when: 'decorative', is: true, notMatch: true }\n\nexport const schema: Aglyn.ComponentSchema<ImageProps> = {\n $id: ID,\n pluginId: BUNDLE_ID,\n displayName: 'Image',\n description:\n 'A picture from your media library or any URL, with fit, size and an optional link.',\n category: Aglyn.ComponentCategory.MEDIA,\n icon: {\n path: mdiImage.path,\n sx: { color: '#7b1fa2' },\n },\n flags: {\n selfClosing: Aglyn.FEATURE_FLAG.ENABLED,\n },\n attributes: [\n {\n name: 'src',\n // \"Browse media\" is the path an author should take (AGL-1215) — it\n // stores a reference to the asset, which survives moves, replaces and\n // any future change to how media is delivered. Typing a URL stays\n // supported for hotlinking somebody else's image; nobody should ever\n // be pasting one of OUR paths in here.\n description:\n 'Pick from your media library with \"Browse media\", or paste the ' +\n 'URL of an image hosted somewhere else.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Image source',\n },\n {\n name: 'alt',\n // AGL-1896: \"Browse media\" now fills this in from the asset's own alt\n // text when it is empty, so the description says where the value came\n // from — otherwise a field that populates itself reads as a bug.\n description:\n 'Describes the image for screen readers and search engines. ' +\n 'Filled in from the media library when you pick a file that has ' +\n 'alt text; anything you type here wins for this placement only.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Alt text',\n // Hidden while Decorative is on — the renderer forces alt=\"\" then,\n // but the text is kept on the node so toggling back restores it.\n condition: NOT_DECORATIVE,\n },\n {\n name: 'decorative',\n description:\n 'Turn on when the image is purely decorative so screen readers ' +\n 'skip it — no alt text is needed then.',\n component: Aglyn.FieldComponentType.SWITCH,\n label: 'Decorative image',\n },\n {\n name: 'title',\n description:\n 'Optional tooltip shown when a visitor hovers over the image.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Tooltip',\n condition: NOT_DECORATIVE,\n },\n {\n name: 'objectFit',\n description: 'How the image fills its box.',\n component: Aglyn.FieldComponentType.SELECT,\n label: 'Fit',\n // Both selects on this element take real sentinels (AGL-1451):\n // `cover` and `lazy` are members of their own declared prop unions\n // and the values the render already falls back to. As `''` neither\n // could persist (AGL-1191) — an image switched to Contain or Eager\n // could not be switched back.\n options: [\n { value: 'cover', label: 'Cover (default)' },\n { value: 'contain', label: 'Contain' },\n { value: 'fill', label: 'Fill' },\n { value: 'none', label: 'None' },\n { value: 'scale-down', label: 'Scale down' },\n ],\n },\n {\n name: 'width',\n description:\n 'Width of the image — a number plus a unit, e.g. 100% or 320px.',\n component: Aglyn.FieldComponentType.CSS_DIMENSION,\n label: 'Width',\n },\n {\n name: 'height',\n description: 'Height of the image. Leave empty for auto.',\n component: Aglyn.FieldComponentType.CSS_DIMENSION,\n label: 'Height',\n },\n {\n name: 'loading',\n description:\n 'Lazy waits to load the image until a visitor scrolls near it; ' +\n 'pick Eager for the first image at the top of a page so it ' +\n 'shows immediately.',\n component: Aglyn.FieldComponentType.SELECT,\n label: 'Loading',\n options: [\n { value: 'lazy', label: 'Lazy (default)' },\n { value: 'eager', label: 'Eager' },\n ],\n },\n {\n name: 'screenId',\n description:\n 'Optional: navigate to this page when the image is clicked — ' +\n 'follows the published path like a Page Link.',\n component: Aglyn.FieldComponentType.SCREEN_SELECT,\n label: 'Link to page',\n },\n {\n name: 'href',\n description:\n 'Makes the image a link to somewhere off this site. Ignored while ' +\n 'Link to page names one. Leave both blank and the image is not ' +\n 'clickable at all.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'External URL',\n // A bare `#fragment` saves and goes nowhere (AGL-2867).\n resolveProps: Aglyn.bareFragmentLinkFieldProps,\n },\n ],\n}\n\nexport const presets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Image',\n pluginId: BUNDLE_ID,\n description: 'Image from your media library or any URL',\n category: Aglyn.ComponentCategory.MEDIA,\n icon: {\n path: mdiImage.path,\n sx: { color: '#7b1fa2' },\n },\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n]\n\nexport default Image\n"],"names":["Aglyn","mdiImage","AppLink","Box","forwardRef","BUNDLE_ID","generatePresetId","ID","leadImageNodeIds","root","layoutLead","screenLead","walk","node","componentId","props","resolvedProps","String","trim","isLayoutComposedNodeId","$id","child","children","filter","id","Boolean","Image","ref","src","storedSrc","alt","decorative","title","loading","objectFit","width","height","intrinsicWidth","intrinsicHeight","radius","screenId","href","externalHref","_children","sx","nodeSxProp","rest","nodeSx","Array","isArray","linkHref","suppressNavigation","useLinkTarget","nodeId","useNodeId","isLeadImage","canvas","rootNode","some","leadId","leafIdsMatch","eager","hostId","useSite","resolveMediaSrc","wrapLink","element","componentVariant","style","display","alignItems","justifyContent","border","borderColor","borderRadius","undefined","color","fontSize","fontFamily","isCdnUrl","isMediaCdnUrl","pinnedWidth","test","usable","value","Number","isFinite","intrinsicAttributes","component","srcSet","mediaCdnSrcSet","sizes","DEFERRED_IMAGE_ATTRIBUTES","displayName","NOT_DECORATIVE","when","is","notMatch","schema","pluginId","description","category","ComponentCategory","MEDIA","icon","path","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","name","FieldComponentType","TEXT_FIELD","label","condition","SWITCH","SELECT","options","CSS_DIMENSION","SCREEN_SELECT","resolveProps","bareFragmentLinkFieldProps","presets","type","NodeType","PRESET","data"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/mui/src/lib/components/image.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiImage } from '@aglyn/shared-data-mdi'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport Box from '@mui/material/Box'\nimport { type SxProps, type Theme, useTheme } from '@mui/material/styles'\nimport useForkRef from '@mui/utils/useForkRef'\nimport { forwardRef, type ReactNode, useEffect, useState } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { generatePresetId } from '../utils/generate-preset-id'\nimport { imageSizes, layoutBandWidths } from '../utils/image-sizes'\n\n// Component ids are persisted in screen documents; never rename.\nexport const ID: Aglyn.ComponentId = 'image'\n\n/**\n * The node ids of the images that load eagerly: the first image the LAYOUT\n * chain renders and the first image the SCREEN renders, each in document\n * order (AGL-2486).\n *\n * Every image used to render `loading=\"lazy\"` — the hero included. That is\n * the worst possible default for the one image that is almost always the LCP\n * element: a lazy image is not fetched until layout has run and the browser\n * has decided it is near the viewport, and it is fetched at LOW priority when\n * it finally is. Lighthouse reports that as \"LCP request discovery\", and it is\n * also why an image four sections down could finish before the one the reader\n * is looking at: with everything lazy and everything low, nothing outranked\n * anything, so the order was whatever the network felt like.\n *\n * So the lead images get `loading=\"eager\"` and the rest get\n * `fetchpriority=\"low\"`, which is the browser-level knob that stops\n * below-the-fold images competing with the ones above them.\n *\n * ONE lead image was not enough. On any site whose header carries a logo, the\n * first image in document order is that logo, so the screen's own hero — the\n * element Lighthouse names as the LCP on aglyn.com's solution pages at\n * 375x812 — stayed `lazy` + `low`: undiscovered until layout, then fetched\n * behind every script on the page (measured 2026-09-08: 1.5 s of load delay\n * and 1.5 s of load time for a 13 KB image). Composition namespaces layout\n * nodes by origin ({@link Aglyn.isLayoutComposedNodeId}), so the walk keeps\n * one lead PER ORIGIN: the layout's first image (the logo) and the screen's\n * first image (the hero). A layout image further down — a mega-menu\n * illustration, the footer mark — is still deferred, as is every later\n * screen image. In the besigner canvas nothing is layout-composed, so the\n * screen's first image is the only lead, exactly as before.\n *\n * Only the SCREEN's lead can get `fetchpriority=\"high\"` and a preload, and\n * only when nothing says it renders small (AGL-3485); the reasoning is at the\n * `fetchPriority` prop below. The layout's lead is the header logo on any\n * site that has one, which is why \"first image\" alone was never evidence.\n *\n * Resolved from the tree rather than a render-order counter on purpose: the\n * renderer walks the tree in document order on the server AND on hydrate, but\n * a mutable counter would double-count under React's concurrent re-renders\n * and hand the priority to a different image on the client than the one the\n * HTML gave it. A pure function of the tree cannot disagree with itself — and\n * it is deliberately NOT memoized on the root object, because the canvas\n * mutates that tree in place while an author works.\n *\n * The walk STOPS once both leads are known — on a page with a header logo\n * and a hero that is O(nodes above the hero), a dozen or two on a normal\n * page — and at the first image on a screen with no layout.\n *\n * An image with no `src` renders a placeholder box and no `<img>` at all, so\n * it cannot be the LCP element and is skipped.\n */\nexport function leadImageNodeIds(\n root: Aglyn.NodeSchema | undefined,\n): readonly string[] {\n let layoutLead: string | undefined\n let screenLead: string | undefined\n const walk = (node: Aglyn.NodeSchema | undefined): boolean => {\n if (!node) return false\n if (node.componentId === ID) {\n const props = (node.resolvedProps ?? node.props ?? {}) as Record<\n string,\n unknown\n >\n if (String(props['src'] ?? '').trim()) {\n if (Aglyn.isLayoutComposedNodeId(node.$id)) {\n if (!layoutLead) layoutLead = node.$id\n } else if (!screenLead) {\n screenLead = node.$id\n }\n if (layoutLead && screenLead) return true\n }\n }\n for (const child of node.children ?? []) {\n if (walk(child)) return true\n }\n return false\n }\n walk(root)\n return [layoutLead, screenLead].filter((id): id is string => Boolean(id))\n}\n\n/**\n * The breakpoint band the theme says is in effect. Asked of the THEME, not of\n * the window: the Besigner canvas pins its theme's breakpoint helpers to the\n * previewed device (`createDevicePinnedTheme`), so on a pinned device this is\n * the device's band, and in Fluid Responsive it is the window's — which is\n * the band the canvas's own layout is answering to either way.\n */\nfunction themeBand(theme: Theme): Aglyn.RenderedWidthBand {\n if (typeof window === 'undefined' || !window.matchMedia) return 'xs'\n for (const band of [...Aglyn.RENDERED_WIDTH_BANDS].reverse()) {\n if (band === 'xs') break\n const query = theme.breakpoints.up(band).replace(/^@media\\s*/, '')\n if (window.matchMedia(query).matches) return band\n }\n return 'xs'\n}\n\n/**\n * The width of the page an element is laid out in. On the canvas that is the\n * shadow host the page renders into — narrower than the window, and the width\n * a share of \"the viewport\" has to be taken of.\n */\nfunction pageWidthOf(element: HTMLElement): number {\n const root = element.getRootNode()\n const host =\n typeof ShadowRoot !== 'undefined' && root instanceof ShadowRoot\n ? (root.host as HTMLElement)\n : document.documentElement\n return host.clientWidth\n}\n\n/**\n * Records how wide this image renders on the Besigner canvas, per band, for\n * the save to write onto the node (AGL-3485, `rendered-widths.ts`). Inert\n * everywhere but the canvas. A `ResizeObserver` on the image and on the page\n * re-records as the author changes the layout, resizes the window or switches\n * device — a device switch also rebuilds the pinned theme, which re-runs this.\n */\nfunction useRenderedWidthRecorder(\n nodeId: string,\n enabled: boolean,\n): (element: HTMLElement | null) => void {\n const theme = useTheme()\n const [element, setElement] = useState<HTMLElement | null>(null)\n useEffect(() => {\n if (!enabled || !element || !nodeId) return undefined\n if (typeof ResizeObserver === 'undefined') return undefined\n const measure = () => {\n const percent = Aglyn.renderedWidthPercent(\n element.getBoundingClientRect().width,\n pageWidthOf(element),\n )\n if (percent !== undefined) {\n Aglyn.recordRenderedWidth(nodeId, themeBand(theme), percent)\n }\n }\n const observer = new ResizeObserver(measure)\n observer.observe(element)\n const root = element.getRootNode()\n if (typeof ShadowRoot !== 'undefined' && root instanceof ShadowRoot) {\n observer.observe(root.host)\n }\n return () => observer.disconnect()\n }, [enabled, element, nodeId, theme])\n return setElement\n}\n\nexport interface ImageProps {\n /**\n * Where the image comes from (AGL-72). Either a **media reference** —\n * `media:{scope}/{mediaId}`, what \"Browse media\" now stores (AGL-1215) —\n * or any URL, which covers both the legacy values already in published\n * documents and an author-typed hotlink. `resolveMediaSrc` decides.\n */\n src?: string\n alt?: string\n /**\n * Explicit decorative choice (AGL-1305): ON forces `alt=\"\"` (and drops\n * the tooltip) no matter what the alt field says, so screen readers skip\n * the image. Distinct from simply leaving alt unset, which also renders\n * `alt=\"\"` today but records no intent.\n */\n decorative?: boolean\n /** Tooltip shown on hover — the native img `title` attribute. */\n title?: string\n /** Native loading hint; unset stays lazy, exactly as before AGL-1305. */\n loading?: 'lazy' | 'eager'\n objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'\n /** CSS width (e.g. \"100%\", \"320px\"); defaults to 100%. */\n width?: string\n /** CSS height (e.g. \"240px\"); defaults to auto. */\n height?: string\n /**\n * The author's own `sizes` attribute, used exactly as typed (AGL-3485).\n * Empty — the normal case — lets the element work it out: see\n * `utils/image-sizes.ts`.\n */\n sizes?: string\n /**\n * How wide the image rendered on the Besigner canvas, per breakpoint band,\n * as a share of the page: written by the editor's save, never by an author\n * (AGL-3485, `rendered-widths.ts`).\n */\n renderedWidths?: Aglyn.RenderedWidths\n /**\n * The asset's current content hash, laid on by the composition from its\n * media document like the pixel pair below (AGL-3485). It makes the URL the\n * VERSIONED one, which every cache may keep for a year; a replace changes\n * it, and so the URL.\n */\n mediaVersion?: string\n /**\n * The asset's own pixel dimensions, copied off the media document when the\n * image was picked (AGL-2486), and read from the asset again when the page\n * is composed, so a replace with a different shape reaches every page that\n * places it (AGL-2833).\n *\n * NOT author controls, which is why neither appears in the schema below:\n * they describe the file, and the CSS `width`/`height` above describe the\n * placement. The pair becomes the `<img>`'s intrinsic `width`/`height`\n * attributes, which is the only thing that lets the browser reserve the\n * right box before the bytes arrive — with `width: 100%; height: auto` the\n * element is otherwise zero-height until the image decodes, and every image\n * on the page shifts the layout as it lands.\n *\n * An attribute pair is a RATIO here, not a size: CSS wins for the used\n * dimensions either way, so a stale value costs nothing but a reservation\n * of the wrong shape. Both must be present and positive or neither is\n * emitted — one alone gives the browser no ratio and would be read as a\n * real dimension.\n *\n * Read from the node like any other prop, so this element never fetches a\n * media document: the composition has already laid the asset's current\n * pair over the pick's (`media-asset-facts.ts`), and the pick's copy is\n * what renders wherever that read cannot answer. `srcSet` still selects\n * which variant is downloaded; these only say what shape it will be.\n */\n intrinsicWidth?: number\n /** See `intrinsicWidth` — the two are only ever used as a pair. */\n intrinsicHeight?: number\n /** Border radius in px. */\n radius?: number\n /** Target screen id — resolved rename-safe like Screen Link (AGL-339). */\n screenId?: string\n /** External URL, used only when no `screenId` is set. */\n href?: string\n /**\n * Authored node styles, handed over by the renderer rather than typed into\n * an attribute — recomposed below so the author's Styles-panel values are\n * merged rather than replaced (AGL-1240).\n */\n sx?: SxProps\n /**\n * Accepted and dropped: an `<img>` is a void element and React throws on\n * ANY children value reaching one, which 500'd whole pages (AGL-579).\n * Declared because the implementation deliberately discards it (AGL-1323).\n */\n children?: ReactNode\n}\n\n/**\n * Image element (AGL-74): renders a plain img with fit/size/radius\n * controls; an empty src shows a labeled placeholder so the element stays\n * visible and selectable in the editor.\n */\nconst Image = forwardRef<HTMLElement, ImageProps>((props, ref) => {\n const {\n src: storedSrc,\n alt,\n decorative,\n title,\n loading,\n objectFit,\n width,\n height,\n sizes: authoredSizes,\n renderedWidths,\n mediaVersion,\n intrinsicWidth,\n intrinsicHeight,\n radius,\n screenId,\n href: externalHref,\n // Never forward children to the <img> below — React throws on ANY\n // children value reaching a void element, which 500'd whole pages\n // when a renderer passed empty JSX children through (AGL-579).\n children: _children,\n // Pull `sx` out of the spread: the literals below are composed AFTER\n // `{...rest}`, so leaving it there REPLACED every style the author set\n // from the Styles panel. The hero mockups' 16px radius and drop shadow\n // were being discarded on every published page (AGL-1240).\n sx: nodeSxProp,\n ...rest\n } = props\n // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).\n const nodeSx = Array.isArray(nodeSxProp)\n ? nodeSxProp\n : nodeSxProp\n ? [nodeSxProp]\n : []\n // Optional link mode (AGL-339): screen id first (rename-safe), external\n // URL as fallback; suppressed in the besigner canvas like Screen Link.\n // Shared with every other linking element since AGL-1335, so a `Link`\n // component prop bound here behaves as it does on a Button.\n const { href: linkHref, suppressNavigation } = Aglyn.useLinkTarget(\n screenId,\n externalHref,\n )\n /**\n * Eagerness (AGL-2486). An explicit author choice always wins — including\n * an explicit `lazy`, so someone who deliberately deferred the top image\n * keeps that. Only an UNSET `loading` is decided here, and only for the\n * lead images: the layout's first and the screen's first.\n *\n * `leafIdsMatch` rather than `===`: a reusable component instance suffixes\n * its leaf ids, so the id in the tree and the id in the context are the\n * same leaf spelled two ways (the markdown block resolves the same way).\n */\n const nodeId = Aglyn.useNodeId()\n const leadIds = nodeId ? leadImageNodeIds(Aglyn.canvas.rootNode) : []\n const isLeadImage = leadIds.some((leadId) =>\n Aglyn.leafIdsMatch(leadId, nodeId),\n )\n const screenLeadId = leadIds.find((id) => !Aglyn.isLayoutComposedNodeId(id))\n const isScreenLead =\n screenLeadId !== undefined && Aglyn.leafIdsMatch(screenLeadId, nodeId)\n const eager = loading === 'eager' || (loading == null && isLeadImage)\n // The canvas measures; nothing else does (AGL-3485).\n const { editorInert } = Aglyn.useScreenLink(undefined)\n const recordRef = useRenderedWidthRecorder(nodeId, Boolean(editorInert))\n const imageRef = useForkRef(ref, recordRef)\n /**\n * Resolve the stored value to a URL (AGL-1215). A media reference becomes\n * a CDN URL here rather than in the document, so the route shape stays an\n * app concern; every other value — a legacy firebasestorage URL, a legacy\n * `/api/media/cdn/…` path, an author-typed hotlink — passes through.\n *\n * `useSite().hostId` is the site being rendered: present on the tenant and\n * in Preview, absent in the besigner canvas. When it is there it names the\n * asking site in the org scope, which is what lets ONE reference in a\n * layout or reusable component resolve on each site that uses it.\n */\n const { hostId } = Aglyn.useSite()\n // With the asset's version when the composition read one (AGL-3485): the\n // versioned URL is the one every cache may keep, and a replace moves it.\n const src = Aglyn.resolveMediaSrc(storedSrc, {\n hostId,\n version: mediaVersion,\n })\n const wrapLink = (element: JSX.Element) =>\n linkHref && !suppressNavigation ? (\n <AppLink\n componentVariant=\"naked\"\n href={linkHref}\n style={{ display: 'block' }}\n >\n {element}\n </AppLink>\n ) : (\n element\n )\n if (!src) {\n // The labeled box is for the author, so only editing surfaces draw it; a\n // published page renders the bare element.\n if (!suppressNavigation) return <Box ref={ref} {...rest} sx={nodeSx} />\n return (\n <Box\n ref={imageRef}\n {...rest}\n sx={[\n {\n width: width || '100%',\n height: height || 120,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n border: '1px dashed',\n borderColor: 'divider',\n borderRadius: radius != null ? `${radius}px` : undefined,\n color: 'text.secondary',\n fontSize: 12,\n fontFamily: 'system-ui, sans-serif',\n },\n ...nodeSx,\n ]}\n >\n {'Image — choose a source'}\n </Box>\n )\n }\n // CDN URLs (AGL-175) carry WebP variants selected by `?w=`; widths\n // without a variant fall back to the original server-side, so a static\n // srcSet is safe for any CDN-form URL. Asked of the RESOLVED url, so a\n // reference and a legacy stored path both keep their WebP variants. Any\n // other url has no candidates, and so no `sizes` either.\n //\n // The asset's own width caps the list (AGL-3486): the widest candidate is\n // the source, described truthfully, never a `1920w` for a 1000px photo that\n // the CDN can only answer with the same 1000 pixels.\n const srcSet = Aglyn.mediaCdnSrcSet(src, {\n sourceWidth:\n typeof intrinsicWidth === 'number' &&\n Number.isFinite(intrinsicWidth) &&\n intrinsicWidth > 0\n ? intrinsicWidth\n : undefined,\n })\n /**\n * How wide the image will render, for `sizes` (AGL-3485): the author's own\n * value, else per breakpoint a pixel width or height the element states (in\n * its attributes or its `sx`, responsive values included), what the\n * Besigner measured, and what the layout around it states. See\n * `utils/image-sizes.ts` for the order and for why none of it disturbs\n * layout the way `sizes=\"auto\"` did (AGL-2486).\n *\n * The layout is read off the canvas tree this image is rendered from, which\n * the tenant fills on the server too, so the published HTML carries it.\n */\n const resolvedSizes = imageSizes({\n sizes: authoredSizes,\n width,\n height,\n sx: nodeSxProp,\n intrinsicWidth,\n intrinsicHeight,\n renderedWidths,\n layoutWidths: nodeId\n ? layoutBandWidths(Aglyn.canvas.getNode(nodeId) as never)\n : undefined,\n })\n /**\n * The page's hero: the SCREEN's lead image, loading eagerly, that nothing\n * says renders small. It gets `fetchpriority=\"high\"`, which its preload\n * carries too (AGL-3485) — see `fetchPriority`. Only on a published page:\n * an editing surface has no first paint to win.\n */\n const hero =\n isScreenLead && eager && !resolvedSizes.small && !suppressNavigation\n /**\n * The intrinsic attribute pair, or nothing.\n *\n * Both-or-neither: the browser derives an aspect-ratio only from the pair,\n * and a lone `width` is read as a real dimension instead — which would\n * reserve a box of the wrong shape rather than no box at all. Finite and\n * positive because a media document may carry `0` or a partial capture\n * (dimensions are best-effort at upload), and `width=\"0\"` collapses the\n * element.\n */\n const usable = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0\n const intrinsicAttributes =\n usable(intrinsicWidth) && usable(intrinsicHeight)\n ? { width: intrinsicWidth, height: intrinsicHeight }\n : undefined\n const sizes = srcSet ? resolvedSizes.sizes : undefined\n return wrapLink(\n <Box\n ref={imageRef}\n component=\"img\"\n src={src}\n // EVERY CANDIDATE IS A `?w=` URL, and the bare one is gone (2026-08-26).\n // Why that is worth 335 KB against 4 KB, and why each candidate merges\n // its width into the url's existing query, are documented where the list\n // and the builder live — `MEDIA_CDN_VARIANT_WIDTHS` and `mediaCdnSrcSet`\n // in `media-ref.ts`.\n //\n // Called rather than restated so the Markdown and entry-body renderers\n // can ask for the same list (AGL-3149). Building it here is the reason\n // they had none: a candidate list inside a component is a candidate list\n // no other component can have.\n srcSet={srcSet}\n // `sizes` is NOT only a delivery hint, and treating it as one broke every\n // fluid image (AGL-2486). With `w` descriptors the browser derives the\n // image's density-corrected INTRINSIC size from `sizes`, so `sizes` is\n // what a CSS `width: 100%` resolves against whenever the containing block\n // is content-sized — shrink-to-fit, inline-block, a flex item sized on its\n // content. Measured in Chrome at a 1200px viewport with the author CSS\n // `width:100%;height:auto;display:block`:\n //\n // parent sizes=100vw sizes=auto\n // inline-block (shrink-to-fit) 1184px 300px\n // block / fixed-width flex 900px 900px\n //\n // 300px is the spec's default object size, used because resolving `auto`\n // against a content-sized parent is circular. So `sizes=\"auto\"` — which\n // genuinely does pick a better candidate, `?w=320` instead of a 357 KB\n // original in a 158px slot — rendered those images tiny and centred in\n // their box, on the canvas, in _preview and on published sites alike.\n //\n // A delivery win may not be paid for in layout, which is why every\n // answer `imageSizes` gives is a real width of the slot (AGL-3485): the\n // author's own value, a definite pixel length, a span of a parent of\n // definite width, or the width the Besigner MEASURED this image at —\n // a fixed point, since an image whose intrinsic width is its measured\n // width lays out at its measured width. `100vw`, the value every\n // published document was authored against, is now only the last resort\n // for a band nothing describes.\n sizes={sizes}\n // Unset alt keeps rendering `alt=\"\"` exactly as it always has —\n // existing documents must not change output (AGL-1305). Decorative\n // ON forces `alt=\"\"` over any alt text and suppresses the tooltip,\n // so the a11y intent is explicit rather than an accident of blank.\n alt={decorative ? '' : (alt ?? '')}\n title={decorative ? undefined : title || undefined}\n // The ordering signal, ONE-DIRECTIONAL on purpose (AGL-2486).\n //\n // `low` on every deferred image, so a footer image cannot be fetched\n // ahead of the section the reader is in. That half is safe in a way the\n // other half is not: deprioritising an image that is provably not being\n // looked at cannot starve whatever the LCP turns out to be.\n //\n // `high` is a claim that this image outranks everything else in flight,\n // including the stylesheet and the webfont a TEXT LCP is waiting on, and\n // it went wrong once (AGL-2486): spent on \"the first `<img>` in document\n // order\", which is the HEADER LOGO on any site whose header has one.\n // Measured on aglyn.com at 375x812, the logo carrying it was 145x44 =\n // 6,380 px² against an `<h1>` of 38,893 px² that Lighthouse named as the\n // LCP — the hint made the real LCP arrive later.\n //\n // So it goes only to the page's HERO (AGL-3485), on two pieces of\n // evidence that did not exist then:\n //\n // - it is the SCREEN's lead image. The layout's lead — the logo — never\n // qualifies, because composition says which origin each node has;\n // - nothing says it renders small. A pixel width of 240 or less, or under\n // 40% of the page in every band the Besigner measured or the layout\n // states, marks a logo or an icon, and those keep the browser's `auto`.\n //\n // The preload is React's own: a server render emits\n // `<link rel=\"preload\" as=\"image\">` in the head for every image that is\n // not lazy, carrying its `srcset`, its `sizes` and this `fetchpriority`\n // — so the hero's request starts with the head, for the same bytes the\n // `<img>` will ask for, and nothing here restates it.\n //\n // Every other lead image — the layout's, or a small one — keeps\n // `loading=\"eager\"` (the discovery fix that earned AGL-2486's win) and\n // the browser's own in-viewport ranking.\n // Decoding off the main thread for the deferred ones — they have no\n // paint deadline, and decoding them synchronously is main-thread time\n // spent on pixels nobody is looking at yet. The eager image keeps the\n // browser's default (`auto`) so it is free to decode in time to paint.\n //\n // The deferred set is `DEFERRED_IMAGE_ATTRIBUTES` rather than three\n // literals because every OTHER `<img>` a published page renders has to\n // land in the same rank to be ranked at all — a product grid, an event\n // list, a cart line. Those carried no hint whatsoever and so were\n // fetched EAGERLY, ahead of anything this component deferred. The set\n // is documented at its definition; the reasoning for each member is\n // the two paragraphs above and the two below.\n {...(eager\n ? {\n loading: 'eager' as const,\n ...(hero ? { fetchPriority: 'high' as const } : {}),\n }\n : Aglyn.DEFERRED_IMAGE_ATTRIBUTES)}\n // Ahead of `{...rest}` so an author who has typed a literal width or\n // height attribute onto the node still wins, and ahead of `sx` because\n // these are ATTRIBUTES: the CSS block below sets the used size, and\n // these only supply the ratio it is laid out against. `Box` in this\n // version applies `styleFunctionSx` alone — it has no system-props\n // layer — so both forward to the `<img>` rather than becoming CSS.\n {...intrinsicAttributes}\n {...rest}\n sx={[\n {\n display: 'block',\n width: width || '100%',\n height: height || 'auto',\n objectFit: objectFit || 'cover',\n borderRadius: radius != null ? `${radius}px` : undefined,\n },\n ...nodeSx,\n ]}\n />,\n )\n})\nImage.displayName = 'Image'\n\n/** Alt text and tooltip make no sense on an explicitly decorative image. */\nconst NOT_DECORATIVE = { when: 'decorative', is: true, notMatch: true }\n\nexport const schema: Aglyn.ComponentSchema<ImageProps> = {\n $id: ID,\n pluginId: BUNDLE_ID,\n displayName: 'Image',\n description:\n 'A picture from your media library or any URL, with fit, size and an optional link.',\n category: Aglyn.ComponentCategory.MEDIA,\n icon: {\n path: mdiImage.path,\n sx: { color: '#7b1fa2' },\n },\n flags: {\n selfClosing: Aglyn.FEATURE_FLAG.ENABLED,\n },\n attributes: [\n {\n name: 'src',\n // \"Browse media\" is the path an author should take (AGL-1215) — it\n // stores a reference to the asset, which survives moves, replaces and\n // any future change to how media is delivered. Typing a URL stays\n // supported for hotlinking somebody else's image; nobody should ever\n // be pasting one of OUR paths in here.\n description:\n 'Pick from your media library with \"Browse media\", or paste the ' +\n 'URL of an image hosted somewhere else.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Image source',\n },\n {\n name: 'alt',\n // AGL-1896: \"Browse media\" now fills this in from the asset's own alt\n // text when it is empty, so the description says where the value came\n // from — otherwise a field that populates itself reads as a bug.\n description:\n 'Describes the image for screen readers and search engines. ' +\n 'Filled in from the media library when you pick a file that has ' +\n 'alt text; anything you type here wins for this placement only.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Alt text',\n // Hidden while Decorative is on — the renderer forces alt=\"\" then,\n // but the text is kept on the node so toggling back restores it.\n condition: NOT_DECORATIVE,\n },\n {\n name: 'decorative',\n description:\n 'Turn on when the image is purely decorative so screen readers ' +\n 'skip it — no alt text is needed then.',\n component: Aglyn.FieldComponentType.SWITCH,\n label: 'Decorative image',\n },\n {\n name: 'title',\n description:\n 'Optional tooltip shown when a visitor hovers over the image.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Tooltip',\n condition: NOT_DECORATIVE,\n },\n {\n name: 'objectFit',\n description: 'How the image fills its box.',\n component: Aglyn.FieldComponentType.SELECT,\n label: 'Fit',\n // Both selects on this element take real sentinels (AGL-1451):\n // `cover` and `lazy` are members of their own declared prop unions\n // and the values the render already falls back to. As `''` neither\n // could persist (AGL-1191) — an image switched to Contain or Eager\n // could not be switched back.\n options: [\n { value: 'cover', label: 'Cover (default)' },\n { value: 'contain', label: 'Contain' },\n { value: 'fill', label: 'Fill' },\n { value: 'none', label: 'None' },\n { value: 'scale-down', label: 'Scale down' },\n ],\n },\n {\n name: 'width',\n description:\n 'Width of the image — a number plus a unit, e.g. 100% or 320px.',\n component: Aglyn.FieldComponentType.CSS_DIMENSION,\n label: 'Width',\n },\n {\n name: 'height',\n description: 'Height of the image. Leave empty for auto.',\n component: Aglyn.FieldComponentType.CSS_DIMENSION,\n label: 'Height',\n },\n {\n // AGL-3485. Empty is the answer for nearly everyone: the image works\n // out how big it renders from where it sits on the page, measured as\n // the page is built. This is the override for someone who knows the\n // HTML attribute and wants to say it themselves.\n name: 'sizes',\n description:\n 'Leave empty — the image works out how big it shows on every ' +\n 'screen size from where you place it, and downloads a file that ' +\n 'size. Advanced: an HTML sizes value used exactly as typed, e.g. ' +\n '(min-width: 900px) 33vw, 100vw.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Sizes',\n },\n {\n name: 'loading',\n description:\n 'Lazy waits to load the image until a visitor scrolls near it; ' +\n 'pick Eager for the first image at the top of a page so it ' +\n 'shows immediately.',\n component: Aglyn.FieldComponentType.SELECT,\n label: 'Loading',\n options: [\n { value: 'lazy', label: 'Lazy (default)' },\n { value: 'eager', label: 'Eager' },\n ],\n },\n {\n name: 'screenId',\n description:\n 'Optional: navigate to this page when the image is clicked — ' +\n 'follows the published path like a Page Link.',\n component: Aglyn.FieldComponentType.SCREEN_SELECT,\n label: 'Link to page',\n },\n {\n name: 'href',\n description:\n 'Makes the image a link to somewhere off this site. Ignored while ' +\n 'Link to page names one. Leave both blank and the image is not ' +\n 'clickable at all.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'External URL',\n // A bare `#fragment` saves and goes nowhere (AGL-2867).\n resolveProps: Aglyn.bareFragmentLinkFieldProps,\n },\n ],\n}\n\nexport const presets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Image',\n pluginId: BUNDLE_ID,\n description: 'Image from your media library or any URL',\n category: Aglyn.ComponentCategory.MEDIA,\n icon: {\n path: mdiImage.path,\n sx: { color: '#7b1fa2' },\n },\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n]\n\nexport default Image\n"],"names":["Aglyn","mdiImage","AppLink","Box","useTheme","useForkRef","forwardRef","useEffect","useState","BUNDLE_ID","generatePresetId","imageSizes","layoutBandWidths","ID","leadImageNodeIds","root","layoutLead","screenLead","walk","node","componentId","props","resolvedProps","String","trim","isLayoutComposedNodeId","$id","child","children","filter","id","Boolean","themeBand","theme","window","matchMedia","band","RENDERED_WIDTH_BANDS","reverse","query","breakpoints","up","replace","matches","pageWidthOf","element","getRootNode","host","ShadowRoot","document","documentElement","clientWidth","useRenderedWidthRecorder","nodeId","enabled","setElement","undefined","ResizeObserver","measure","percent","renderedWidthPercent","getBoundingClientRect","width","recordRenderedWidth","observer","observe","disconnect","Image","ref","src","storedSrc","alt","decorative","title","loading","objectFit","height","sizes","authoredSizes","renderedWidths","mediaVersion","intrinsicWidth","intrinsicHeight","radius","screenId","href","externalHref","_children","sx","nodeSxProp","rest","nodeSx","Array","isArray","linkHref","suppressNavigation","useLinkTarget","useNodeId","leadIds","canvas","rootNode","isLeadImage","some","leadId","leafIdsMatch","screenLeadId","find","isScreenLead","eager","editorInert","useScreenLink","recordRef","imageRef","hostId","useSite","resolveMediaSrc","version","wrapLink","componentVariant","style","display","alignItems","justifyContent","border","borderColor","borderRadius","color","fontSize","fontFamily","srcSet","mediaCdnSrcSet","sourceWidth","Number","isFinite","resolvedSizes","layoutWidths","getNode","hero","small","usable","value","intrinsicAttributes","component","fetchPriority","DEFERRED_IMAGE_ATTRIBUTES","displayName","NOT_DECORATIVE","when","is","notMatch","schema","pluginId","description","category","ComponentCategory","MEDIA","icon","path","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","name","FieldComponentType","TEXT_FIELD","label","condition","SWITCH","SELECT","options","CSS_DIMENSION","SCREEN_SELECT","resolveProps","bareFragmentLinkFieldProps","presets","type","NodeType","PRESET","data"],"mappings":"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"}
@@ -33,6 +33,7 @@ export interface MarkdownProps extends BoxProps {
33
33
  intrinsicSizes?: Record<string, {
34
34
  width: number;
35
35
  height: number;
36
+ version?: string;
36
37
  }>;
37
38
  }
38
39
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/mui/src/lib/components/markdown.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiLanguageMarkdown, mdiTableOfContents } from '@aglyn/shared-data-mdi'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport {\n scrollOverflowFadeKeyframes,\n scrollOverflowFadeTimeline,\n scrollRegionProps,\n scrollableTableSx,\n scrollableTableWrapperSx,\n} from '@aglyn/shared-ui-jsx/utils/scroll-overflow'\nimport Box, { type BoxProps } from '@mui/material/Box'\nimport MuiLink from '@mui/material/Link'\nimport type { Theme } from '@mui/material/styles'\nimport Typography from '@mui/material/Typography'\nimport { observer } from 'mobx-react-lite'\nimport type { MouseEvent, ReactNode } from 'react'\nimport { forwardRef, useContext, useMemo } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { dropClearedProps } from '../utils/drop-cleared-props'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n// Component ids are persisted in screen documents; never rename.\nexport const MARKDOWN_ID: Aglyn.ComponentId = 'markdown'\n/** A compose-time token the tenant has not substituted — never a document. */\nconst UNRESOLVED_TOKEN = /^\\{\\{[^}]+\\}\\}$/\n/**\n * What a screen reader announces on reaching a table's scroll box.\n *\n * The plain word, not the table's own heading: the name has to come from\n * something every document has, and a markdown table carries no caption.\n */\nconst TABLE_REGION_LABEL = 'Table'\nexport const TABLE_OF_CONTENTS_ID: Aglyn.ComponentId = 'tableOfContents'\n\n/**\n * A long document's body type, which is NOT MUI's `body1` (AGL-1162).\n *\n * A privacy policy is read, not skimmed: the house measure for running prose\n * is 17px on a 1.75 leading, in `text.primary` rather than the softened\n * secondary a caption uses. MUI's default 16/1.5 sets the same words tighter\n * and lighter, and a page built from Typography defaults reads visibly\n * different from one built by hand next to it — which is how the legal pages\n * ended up hand-assembled from hundreds of Typography nodes in the first\n * place.\n *\n * ⚑ `inherit`, not `text.primary` (AGL-3210). The colour is still\n * `text.primary` — {@link BODY_COLOUR_SX} puts it on the element's own root,\n * where an authored `color` can replace it. Repeating the token on every\n * block instead made the author's colour unreachable: it landed on the root\n * and every child overrode it, so a card with a fixed light background kept\n * dark mode's near-white body text and read as blank.\n */\nconst BODY_SX = {\n fontSize: 17,\n lineHeight: 1.75,\n color: 'inherit',\n} as const\n\n/**\n * The element's own colour, applied to its root BEFORE the authored `sx`\n * (AGL-3210), so it is a default the Styles panel can replace rather than a\n * floor every block re-asserts. Inside the element everything inherits it.\n */\nconst BODY_COLOUR_SX = { color: 'text.primary' } as const\n\n/**\n * Heading ramp (AGL-1162). Sizes step across `{xs, sm, md}` and the tracking\n * steps WITH them: tracking is a function of size, so a single letterSpacing\n * that looks right at the desktop size is visibly loose on a phone. Negative\n * throughout — display sizes need the letters pulled together, which is the\n * opposite of the positive tracking small uppercase labels take.\n */\nconst HEADING_SX = {\n 2: {\n fontSize: { xs: 26, sm: 30, md: 34 },\n letterSpacing: { xs: '-0.3px', sm: '-0.4px', md: '-0.5px' },\n lineHeight: 1.25,\n fontWeight: 700,\n },\n 3: {\n fontSize: { xs: 19, sm: 20, md: 22 },\n letterSpacing: { xs: '-0.1px', sm: '-0.15px', md: '-0.2px' },\n lineHeight: 1.35,\n fontWeight: 700,\n },\n} as const\n\n/**\n * Breathing room a heading keeps above it when an anchor lands on it. Sites\n * built here usually have a sticky app bar, and without this the browser\n * scrolls the heading to y=0 — underneath the bar — so a working link looks\n * like a broken one.\n */\nconst ANCHOR_SCROLL_MARGIN = Aglyn.HEADING_ANCHOR_SCROLL_MARGIN\n\n/**\n * Inline runs as elements (AGL-1162), following the entry-body renderer:\n * internal links route through AppLink for client-side navigation, external\n * ones stay plain anchors, and editing surfaces render the link look without\n * an href so clicking never leaves the canvas.\n *\n * Which of those a link is — and whether it is a link at all — is\n * `resolveMarkdownLink`'s to say (AGL-3118), so a target named by reference\n * resolves through the same routing map an element link does, and one whose\n * target is gone renders as its words rather than as `href=\"entry:…\"`.\n */\nconst renderInlines = (\n inlines: Aglyn.MarkdownInline[],\n links: { suppressNavigation?: boolean; screens?: Aglyn.ScreenRouteMap },\n): ReactNode[] =>\n inlines.map((inline, index) => {\n if (inline.type === 'bold') return <strong key={index}>{inline.text}</strong>\n if (inline.type === 'italic') return <em key={index}>{inline.text}</em>\n if (inline.type === 'link') {\n const link = Aglyn.resolveMarkdownLink(inline.href, links)\n if (link.kind === 'text') return <span key={index}>{inline.text}</span>\n if (link.kind === 'inert') {\n return (\n <MuiLink key={index} component=\"span\" sx={{ cursor: 'default' }}>\n {inline.text}\n </MuiLink>\n )\n }\n return link.kind === 'internal' ? (\n <AppLink key={index} href={link.href}>\n {inline.text}\n </AppLink>\n ) : (\n <MuiLink key={index} href={link.href}>\n {inline.text}\n </MuiLink>\n )\n }\n return <span key={index}>{inline.text}</span>\n })\n\nexport interface MarkdownProps extends BoxProps {\n /** The document, as markdown-lite source. */\n content?: string\n /**\n * The pixel pair of each library image this document names, keyed by the\n * target as the document writes it, stamped by the composition from the\n * assets' DAM records (AGL-3149) — never set by hand.\n *\n * An image with no entry here renders exactly as it did before this prop\n * existed: see `mediaBodyImageAttributes` for why a partial answer is worse\n * than none.\n */\n intrinsicSizes?: Record<string, { width: number; height: number }>\n}\n\n/**\n * A whole document authored as markdown (AGL-1162).\n *\n * The elements that made a legal or policy page before this were hundreds of\n * Typography nodes, one per paragraph — which meant the page could not be\n * pasted from, or back into, the markdown file in Drive that is its actual\n * source. Here the source IS the element's content, so updating the page is\n * one paste.\n *\n * Rendered through the shared markdown-lite parser, which emits plain data\n * blocks and never an HTML string — so there is no `dangerouslySetInnerHTML`\n * anywhere on this path and nothing to sanitize. Parsing is pure, so the full\n * document server-renders and the text is in the crawled HTML.\n *\n * Child NODES are never rendered, in either branch (AGL-1388). The document\n * is the `content` prop; there is no position in a parsed block list that a\n * dropped element could occupy, so the schema turns dropping off and\n * `nodeAcceptsChildren` now honors that — a drop lands as a sibling instead.\n * `children` is destructured away rather than left in `rest` because the\n * empty-content branch spreads `rest` onto a childless Box: without this, a\n * Markdown element rendered exactly the child nodes it had swallowed a\n * moment earlier as soon as its content was cleared.\n */\nconst Markdown = forwardRef<HTMLDivElement, MarkdownProps>((props, ref) => {\n const { content, intrinsicSizes, sx, children: _children, ...rest } = props\n // The routing map travels with the suppression flag: both decide what a\n // link in this document renders as (AGL-3118).\n const { suppressNavigation, screens } = useContext(Aglyn.ScreenLinkContext)\n const links = useMemo(\n () => ({ suppressNavigation, screens }),\n [suppressNavigation, screens],\n )\n /**\n * The site being rendered, for image blocks (AGL-1686). Read once here\n * rather than per block: hooks cannot run inside the `blocks.map` below,\n * and a document may hold any number of images.\n */\n const { hostId } = Aglyn.useSite()\n const source = (content ?? '').trim()\n const blocks = useMemo(\n () => (source ? Aglyn.parseMarkdownLite(source) : []),\n [source],\n )\n // Slugs are derived rather than stored (see collectMarkdownHeadings), and\n // keyed by block index so the ids land on the right headings even when two\n // of them carry the same words. The map is built in `markdown-lite` now,\n // because every renderer needs exactly this and five of the six were not\n // getting it (AGL-1162).\n const slugs = useMemo(() => Aglyn.markdownHeadingSlugs(blocks), [blocks])\n\n if (!source) {\n // Editing surfaces get somewhere to click; the published page renders\n // nothing rather than a placeholder nobody meant to publish.\n if (!suppressNavigation) return <Box ref={ref} sx={sx} {...rest} />\n return (\n <Box\n ref={ref}\n {...rest}\n sx={[\n {\n p: 2,\n border: '1px dashed',\n borderColor: 'divider',\n color: 'text.secondary',\n fontSize: 12,\n },\n ...(Array.isArray(sx) ? sx : sx ? [sx] : []),\n ]}\n >\n {'Markdown — paste the document into the Content attribute'}\n </Box>\n )\n }\n\n return (\n <Box\n ref={ref}\n {...rest}\n sx={[BODY_COLOUR_SX, ...(Array.isArray(sx) ? sx : sx ? [sx] : [])]}\n >\n {blocks.map((block, index) => {\n if (block.type === 'heading') {\n return (\n <Typography\n key={index}\n // The anchor a table of contents links to. `component` is set\n // explicitly because a Typography with only an `sx` fontSize\n // still renders a <p>: the page would screenshot correctly and\n // have no headings in it at all.\n id={slugs[index]}\n component={block.level === 2 ? 'h2' : 'h3'}\n sx={{\n ...HEADING_SX[block.level],\n // See BODY_SX: the token lives on the root, not on every\n // block, so an authored colour reaches the words (AGL-3210).\n color: 'inherit',\n scrollMarginTop: `${ANCHOR_SCROLL_MARGIN}px`,\n mt: block.level === 2 ? 5 : 3.5,\n mb: 1.5,\n '&:first-of-type': { mt: 0 },\n }}\n >\n {renderInlines(block.inlines, links)}\n </Typography>\n )\n }\n if (block.type === 'image') {\n return (\n <Box\n key={index}\n component=\"img\"\n // Resolved, exactly as `image.tsx` resolves its `src` prop\n // (AGL-1686). A document records WHICH ASSET; turning that into\n // a URL is the renderer's business, so the `/api/media/cdn/…`\n // route shape never has to be baked into a published document.\n // Every other value — a legacy storage URL, a legacy CDN path,\n // an author-typed hotlink — passes through untouched.\n //\n // The resolve now comes back with the WebP candidate list and\n // the intrinsic pair beside it, when the composition could read\n // the asset's record (AGL-3149). Shared with the entry body and\n // with `image.tsx`'s own srcSet so a body image cannot drift\n // away from the element again.\n {...Aglyn.mediaBodyImageAttributes({\n src: block.src,\n hostId,\n size: intrinsicSizes?.[block.src],\n })}\n alt={block.alt}\n // `height: 'auto'` is load-bearing beside the pair above, not\n // tidiness: the pair's `height` arrives as a presentational hint\n // too, and a hint that survives the `maxWidth` cap squashes the\n // picture instead of scaling it. It is what `auto` already\n // resolved to before the pair existed, so this changes nothing\n // for an image the composition could not measure.\n sx={{ maxWidth: '100%', height: 'auto', borderRadius: 1, my: 2 }}\n // `lazy` alone until AGL-2486. A markdown image is by\n // definition inside prose the reader scrolls through, so it\n // belongs in the same deferred rank as everything else that is\n // not the lead image.\n {...Aglyn.DEFERRED_IMAGE_ATTRIBUTES}\n />\n )\n }\n if (block.type === 'list') {\n return (\n <Box\n key={index}\n component=\"ul\"\n sx={{ ...BODY_SX, my: 2, pl: 3, '& li': { mb: 1 } }}\n >\n {block.items.map((item, itemIndex) => (\n <li key={itemIndex}>\n {renderInlines(item, links)}\n </li>\n ))}\n </Box>\n )\n }\n // A numbered list (AGL-1320): the same measure as the bullet list,\n // as a real `<ol>` carrying `start` — a legal document's enumerated\n // elements are the reason this block exists.\n if (block.type === 'orderedList') {\n return (\n <Box\n key={index}\n component=\"ol\"\n start={block.start}\n sx={{ ...BODY_SX, my: 2, pl: 3, '& li': { mb: 1 } }}\n >\n {block.items.map((item, itemIndex) => (\n <li key={itemIndex}>\n {renderInlines(item, links)}\n </li>\n ))}\n </Box>\n )\n }\n // Code and tables scroll rather than wrap, like every other\n // markdown-lite renderer here (AGL-974): a wrapped command line\n // reads as two commands, and a squeezed table column loses the\n // structure that made it a table.\n if (block.type === 'code') {\n return (\n <Box\n key={index}\n component=\"pre\"\n sx={{\n my: 2,\n p: 2,\n overflowX: 'auto',\n borderRadius: 1,\n bgcolor: 'action.hover',\n fontFamily: 'monospace',\n fontSize: 14,\n }}\n >\n <code>{block.text}</code>\n </Box>\n )\n }\n if (block.type === 'table') {\n return (\n <Box\n key={index}\n {...scrollRegionProps(TABLE_REGION_LABEL)}\n sx={{ my: 2, ...scrollableTableWrapperSx }}\n >\n <Box\n component=\"table\"\n sx={{\n ...BODY_SX,\n fontSize: 16,\n borderCollapse: 'collapse',\n ...scrollableTableSx,\n '& th, & td': {\n border: '1px solid',\n borderColor: 'divider',\n px: 1.5,\n py: 1,\n },\n '& th': { bgcolor: 'action.hover', fontWeight: 600 },\n }}\n >\n <thead>\n <tr>\n {block.header.map((cell, cellIndex) => (\n <th\n key={cellIndex}\n style={{ textAlign: block.align[cellIndex] ?? 'left' }}\n >\n {renderInlines(cell, links)}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {block.rows.map((row, rowIndex) => (\n <tr key={rowIndex}>\n {row.map((cell, cellIndex) => (\n <td\n key={cellIndex}\n style={{\n textAlign: block.align[cellIndex] ?? 'left',\n }}\n >\n {renderInlines(cell, links)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </Box>\n </Box>\n )\n }\n // A pull-quote (AGL-1315): the document's body measure, one size up\n // and italic, behind a left accent — palette tokens keep it right in\n // both modes.\n if (block.type === 'quote') {\n return (\n <Typography\n key={index}\n component=\"blockquote\"\n sx={{\n ...BODY_SX,\n my: 3,\n mx: 0,\n pl: 2.5,\n borderLeft: '3px solid',\n borderColor: 'primary.main',\n fontStyle: 'italic',\n fontSize: 21,\n lineHeight: 1.6,\n }}\n >\n {renderInlines(block.inlines, links)}\n </Typography>\n )\n }\n return (\n <Typography key={index} component=\"p\" sx={{ ...BODY_SX, my: 2 }}>\n {renderInlines(block.inlines, links)}\n </Typography>\n )\n })}\n </Box>\n )\n})\nMarkdown.displayName = 'AglynMarkdown'\n\n/* ── Table of contents ──────────────────────────────────────────────────── */\n\nexport interface TableOfContentsProps extends BoxProps {\n /**\n * The Markdown element this lists the headings of. Empty means \"the first\n * Markdown element on the screen\", which is the answer on every page that\n * has exactly one — see {@link resolveMarkdownSource}.\n */\n forNodeId?: string\n /** Aside heading; an empty string renders no heading at all. */\n heading?: string\n /** Deepest heading level listed: `'2'` for top level only. */\n depth?: '2' | '3'\n}\n\n/**\n * Which Markdown element a table of contents speaks for (AGL-1162).\n *\n * Resolved through the NODE TREE, never the DOM: the besigner canvas renders\n * inside a closed shadow root, so a `document.querySelector` from a component\n * finds nothing there and the aside would be empty in the one place it is\n * being authored.\n *\n * The tree is walked from the root in document order, so \"the first Markdown\n * element\" means the first one a reader meets — not the first key of a node\n * map, whose order is storage detail.\n *\n * An explicit `forNodeId` wins, compared with `leafIdsMatch` because the\n * stored id is the raw canvas id while a composed tree namespaces layout and\n * reusable-component grafts (AGL-573/1229). A pick that no longer resolves —\n * the element was deleted and re-added, which is exactly what re-pasting a\n * document looks like — falls back to the first Markdown element rather than\n * leaving a published page with an empty aside.\n */\nexport function resolveMarkdownSource(\n root: Aglyn.NodeSchema | undefined,\n forNodeId?: string,\n): string {\n const found: Array<{ $id: string; content: string }> = []\n const walk = (node: Aglyn.NodeSchema | undefined): void => {\n if (!node) return\n // Two element types hold a markdown-lite document, and an aside beside a\n // blog article is the case this issue was opened for (AGL-1162): the\n // Collection Entry Body carries the post, under `markdown` rather than\n // `content`, and used to be invisible here — so \"On this page\" on an\n // article template rendered its authoring affordance forever.\n const source =\n node.componentId === MARKDOWN_ID\n ? 'content'\n : node.componentId === Aglyn.COLLECTION_ENTRY_BODY_COMPONENT_ID\n ? 'markdown'\n : undefined\n if (source) {\n const props = (node.resolvedProps ?? node.props ?? {}) as Record<\n string,\n unknown\n >\n const content = String(props[source] ?? '')\n // An entry body on an unresolved surface is the literal\n // `{{entry.body}}` token. It is not a document, and treating it as one\n // would let it win over a real Markdown element further down the page\n // and empty the aside on the canvas.\n if (!UNRESOLVED_TOKEN.test(content.trim())) {\n found.push({ $id: node.$id, content })\n }\n }\n for (const child of node.children ?? []) walk(child)\n }\n walk(root)\n if (forNodeId) {\n const picked = found.find((entry) => Aglyn.leafIdsMatch(entry.$id, forNodeId))\n if (picked) return picked.content\n }\n return found[0]?.content ?? ''\n}\n\n/**\n * Scrolls to a heading in the cases the browser cannot reach, and ONLY those\n * (AGL-1162). Returns whether it handled the click.\n *\n * In `document` it deliberately does nothing and lets the plain `#slug`\n * anchor work: native fragment navigation updates the address bar, adds a\n * history entry the back button understands, and honours the site's CSS\n * `scroll-behavior` — none of which a hand-rolled `scrollIntoView` gives\n * back. It is also the only version that works with JavaScript off.\n *\n * (`behavior: 'smooth'` was the first attempt at the whole job, and it is\n * exactly the wrong shape: a browser that declines to animate — a background\n * tab, reduced-motion — leaves the click prevented and the page unmoved, so\n * a working link reads as a broken one.)\n *\n * The besigner canvas renders inside a CLOSED shadow root, where the heading\n * is not in `document` at all and a fragment navigation finds nothing. There\n * the link's own `getRootNode()` is the shadow root holding both ends, and\n * `ShadowRoot.getElementById` finds the heading.\n */\nexport function scrollToHeading(anchor: Element, slug: string): boolean {\n const root = anchor.getRootNode()\n if (root === anchor.ownerDocument || !(root as ShadowRoot).getElementById) {\n return false\n }\n const target = (root as ShadowRoot).getElementById(slug)\n if (!target) return false\n // No explicit `behavior`: 'auto' follows the scrolling box's CSS\n // `scroll-behavior`, so a site that asked for smooth still gets it.\n target.scrollIntoView({ block: 'start' })\n return true\n}\n\n/**\n * What the sidebar's chrome costs, above and below the list (AGL-2486).\n *\n * The list's bound is `100dvh` MINUS this, so it is a function of the window\n * rather than a height somebody picked. Measured on the live\n * `/legal/marketplace-publisher-agreement` sidebar, which is the shape every\n * legal page uses: the card is `position: sticky; top: 96px` under the app\n * bar, then 24px of card padding and 31px of \"ON THIS PAGE\" label sit above\n * the list and 31px of padding below it — 182px of chrome. 200 leaves the\n * card's bottom BORDER inside the window rather than flush against it, so\n * the card reads as a card and not as a page that ran out.\n *\n * It is an allowance, not a contract: this component renders inside whatever\n * the author built around it, and it cannot read that card's `top` offset.\n * Being a MAX height, an allowance a little too generous only means a few\n * pixels of the card sit below the fold; one a little too tight only means\n * the scroll box starts a line sooner. Both are recoverable, which is the\n * whole reason this is a `max-height` and not a `height`.\n */\nconst TOC_VIEWPORT_ALLOWANCE = '200px'\n\n/**\n * The list is its own scroll box once it outgrows the window (AGL-2486).\n *\n * The bug: nothing here was bounded, and the card the author wraps this in is\n * `position: sticky`. A sticky box taller than the viewport does not scroll\n * into view — it pins at its `top` offset and everything past the window's\n * bottom edge is simply unreachable. Measured on the Marketplace Publisher\n * Agreement (14 `h2`s) in a 1100x480 window: the card stood 502px tall from\n * `top: 96px` and overhung the fold by 118px, entry 11 was cut through the\n * middle of its own text at the window edge, and 12 to 14 could not be\n * reached by any scroll — `ol.scrollTop` stayed 0, because the list was not a\n * scroll box at all. Which row gets cut moves with the window height and the\n * browser zoom; the reported sighting was entry 8. Either way it did not read\n * as \"there is more\", it read as a document with fewer sections than it has.\n *\n * Three things this deliberately is NOT:\n *\n * - NOT a `height`. A `max-height` is inert until the content exceeds it, so\n * the four-item TOCs (`/legal/dmca`, `/legal/subprocessors`) keep their\n * exact present box: no scroll region, no reserved gutter, no dead space.\n * That is also why `overflow-y` is `auto` and never `scroll`.\n *\n * - NOT applied below `md`. The bound only makes sense for a sidebar. In the\n * stacked narrow layout the TOC is a full-width block in the page flow, and\n * bounding it there would plant a scroll trap in the middle of an article\n * to solve a clipping problem that layout does not have.\n *\n * - NOT silent, and the FADE is the part that carries that. A scroll box a\n * reader cannot see is the same bug wearing a scrollbar: measured in Chrome\n * on macOS, `offsetWidth - clientWidth` on this list is 0 even with\n * `scrollbar-color` set — the platform draws an OVERLAY scrollbar, which is\n * invisible until you have already guessed to scroll. So the thumb styling\n * is the secondary cue, for the platforms that draw one at rest, and the\n * primary cue is a bottom fade: the cut stops being a slice through the\n * middle of a word and becomes an edge that visibly continues.\n *\n * The fade rides a scroll-driven animation on purpose: a `scroll()` timeline\n * on a box with nothing to scroll is INACTIVE, so its animation applies no\n * styles at all. That is the only way to say \"fade only when overflowing\" in\n * CSS with no measuring and no JavaScript, and it degrades to plain\n * unfaded-but-scrollable in a browser that lacks it. Verified both ways at\n * 1100x480: the 14-entry list fades and scrolls, the 4-entry\n * `/legal/subprocessors` list computes `mask-image: none` and does not.\n *\n * NO new attribute goes with this, deliberately. The three the element has —\n * `forNodeId`, `heading`, `depth` — all answer \"which headings does this\n * list\", and a \"list height\" field would hand the author back the very\n * decision this bug is made of. The right answer is \"as much of the window as\n * there is\", which is a thing CSS can work out and an author cannot type.\n */\nconst TABLE_OF_CONTENTS_LIST_SX = (theme: Theme) => ({\n listStyle: 'none',\n m: 0,\n p: 0,\n [theme.breakpoints.up('md')]: {\n maxHeight: `calc(100vh - ${TOC_VIEWPORT_ALLOWANCE})`,\n // `dvh` over `vh` where it exists: `vh` is the LARGEST viewport, so a\n // window with a visible toolbar gets a bound taller than the space it\n // has — which is the bug again, in miniature.\n '@supports (height: 100dvh)': {\n maxHeight: `calc(100dvh - ${TOC_VIEWPORT_ALLOWANCE})`,\n },\n overflowY: 'auto',\n // Reaching the end of the list must not then throw the page down a\n // screen — the reader was navigating the sidebar, not the document.\n overscrollBehavior: 'contain',\n scrollbarWidth: 'thin',\n // `text.disabled`, not `divider`: a 12%-black thumb is one you have to\n // already know is there. `3px` is QUOTED because a bare 3 is a theme\n // radius STEP and lands as 12px on a 6px-wide thumb.\n scrollbarColor: `${theme.palette.text.disabled} transparent`,\n '&::-webkit-scrollbar': { width: 6 },\n '&::-webkit-scrollbar-thumb': {\n backgroundColor: theme.palette.text.disabled,\n borderRadius: '3px',\n },\n ...scrollOverflowFadeTimeline('block'),\n },\n ...scrollOverflowFadeKeyframes('block'),\n})\n\n/**\n * \"On this page\" (AGL-1162) — the headings of a Markdown element on the same\n * screen, as working anchor links.\n *\n * A mobx `observer` because it reads ANOTHER node's props: the renderer's\n * Leaf re-renders on changes to the node it is rendering, and nothing else,\n * so without this the aside in the besigner would keep listing the headings\n * of the document as it was when the aside mounted.\n */\nconst TableOfContents = observer(\n forwardRef<HTMLElement, TableOfContentsProps>((props, ref) => {\n const { forNodeId, heading, depth, sx, ...rawRest } = props\n /**\n * The cleared-prop guard, applied to the props that LEAVE this\n * component rather than to all of them (AGL-1451).\n *\n * A props-wide `dropClearedProps` would be wrong here, and this is the\n * component that shows why the wrapper is not a reflex: `heading` reads\n * an EMPTY value as a real author choice — \"clear it to render no\n * label\", which is what its help text promises and what the `heading\n * === undefined ? 'On this page' : heading` line below implements. A\n * guard over the whole props object would strip the cleared heading and\n * put the default label back, i.e. eat the choice the author made. So\n * `heading` and `depth` are read as authored, and the guard covers only\n * what is spread onto the Box.\n */\n const rest = dropClearedProps(rawRest)\n const { suppressNavigation } = useContext(Aglyn.ScreenLinkContext)\n const source = resolveMarkdownSource(Aglyn.canvas.rootNode, forNodeId)\n const entries = useMemo(() => {\n const headings = Aglyn.collectMarkdownHeadings(\n Aglyn.parseMarkdownLite(source),\n )\n return depth === '2'\n ? headings.filter((entry) => entry.level === 2)\n : headings\n }, [source, depth])\n const title = heading === undefined ? 'On this page' : heading\n\n if (!entries.length) {\n // Same split as the Markdown element: an affordance while authoring,\n // nothing at all on the published page — and no empty `nav` landmark.\n if (!suppressNavigation) return <Box ref={ref} sx={sx} {...rest} />\n return (\n <Box\n ref={ref}\n component=\"nav\"\n sx={[\n {\n p: 2,\n border: '1px dashed',\n borderColor: 'divider',\n color: 'text.secondary',\n fontSize: 12,\n },\n ...(Array.isArray(sx) ? sx : sx ? [sx] : []),\n ]}\n {...rest}\n >\n {'Table of contents — add ## headings to the Markdown element'}\n </Box>\n )\n }\n\n return (\n <Box\n ref={ref}\n component=\"nav\"\n aria-label={title || 'Table of contents'}\n sx={sx}\n {...rest}\n >\n {title ? (\n <Typography\n component=\"p\"\n sx={{\n fontSize: 12,\n fontWeight: 700,\n // Positive tracking here, unlike the document headings: this is\n // a small uppercase label, where the letters need pushing apart\n // rather than pulling together.\n letterSpacing: '0.8px',\n textTransform: 'uppercase',\n color: 'text.secondary',\n mb: 1.5,\n }}\n >\n {title}\n </Typography>\n ) : null}\n <Box component=\"ol\" sx={TABLE_OF_CONTENTS_LIST_SX}>\n {entries.map((entry) => (\n <Box component=\"li\" key={entry.slug} sx={{ mb: 0.75 }}>\n <MuiLink\n // A real href, so the entry is a link to a crawler, works\n // with no JavaScript, and can be copied out of the page.\n href={`#${entry.slug}`}\n underline=\"hover\"\n color=\"text.secondary\"\n onClick={(event: MouseEvent<HTMLAnchorElement>) => {\n // Prevented ONLY when this actually moved the page — on a\n // published page it does nothing and the anchor works the\n // way anchors do.\n if (scrollToHeading(event.currentTarget, entry.slug)) {\n event.preventDefault()\n }\n }}\n sx={{\n display: 'block',\n fontSize: 15,\n lineHeight: 1.5,\n // Sub-headings indent; nothing else distinguishes them, so\n // a flat list of both levels reads as one long list.\n pl: entry.level === 3 ? 2 : 0,\n '&:hover': { color: 'text.primary' },\n }}\n >\n {entry.text}\n </MuiLink>\n </Box>\n ))}\n </Box>\n </Box>\n )\n }),\n)\nTableOfContents.displayName = 'AglynTableOfContents'\n\nexport const markdownSchema: Aglyn.ComponentSchema<MarkdownProps> = {\n $id: MARKDOWN_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Markdown',\n description:\n 'A whole document written as markdown. It takes no child elements.',\n category: Aglyn.ComponentCategory.TEXT,\n icon: { path: mdiLanguageMarkdown.path, sx: { color: '#455a64' } },\n flags: {\n // The document is the `content` attribute; dropping elements inside a\n // rendered document would put them somewhere the markdown cannot say.\n dropping: Aglyn.FEATURE_FLAG.DISABLED,\n },\n attributes: [\n {\n name: 'content',\n label: 'Content',\n description:\n 'The document, as markdown. Headings (## and ###), paragraphs, ' +\n 'bold/italic, links, bullet and 1. numbered lists, images, fenced ' +\n 'code, pipe tables and > quotes. Paste the whole source file — a ' +\n 'Table of contents element on the same page lists the ## and ### ' +\n 'headings automatically.',\n // The WYSIWYG rather than a raw textarea (AGL-1616): this attribute is\n // a whole document — the published Privacy Policy body is one of them —\n // and editing it as raw markdown-lite meant a 13 KB paste (AGL-1594).\n // The stored value is unchanged, so every renderer is untouched.\n component: Aglyn.FieldComponentType.MARKDOWN,\n },\n ],\n}\n\nexport const tableOfContentsSchema: Aglyn.ComponentSchema<TableOfContentsProps> =\n {\n $id: TABLE_OF_CONTENTS_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Table of Contents',\n description:\n \"An on-this-page list linking to a Markdown element's headings.\",\n category: Aglyn.ComponentCategory.NAVIGATION,\n icon: { path: mdiTableOfContents.path, sx: { color: '#455a64' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'forNodeId',\n label: 'Markdown element',\n description:\n 'The Markdown element to list the headings of. Leave empty on a ' +\n 'page with one Markdown element — the first one on the page is ' +\n 'used, and a pick that no longer exists falls back to it too.',\n component: Aglyn.FieldComponentType.NODE_SELECT,\n },\n {\n name: 'heading',\n label: 'Heading',\n description: 'Label above the list. Clear it to render no label.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'depth',\n label: 'Levels',\n description: 'How deep the list goes.',\n component: Aglyn.FieldComponentType.SELECT,\n // `depth` never reaches MUI — it is read here, as `depth === '2'`.\n // So the `''` was not a rendering defect; it was an option that\n // could not survive a save (AGL-1191), which meant an author who\n // set \"## headings only\" and then changed their mind could not put\n // it back. `'3'` states the same thing as a real, persistable\n // value — and is what `TableOfContentsProps.depth` has declared\n // all along, so the option list was the half that had drifted.\n // Both levels remain what an unset field means (AGL-1451).\n options: [\n { value: '3', label: '## and ### headings' },\n { value: '2', label: '## headings only' },\n ],\n },\n ],\n }\n\nexport const markdownPresets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(MARKDOWN_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Markdown',\n pluginId: BUNDLE_ID,\n description: 'A whole document, authored as markdown',\n category: Aglyn.ComponentCategory.TEXT,\n icon: { path: mdiLanguageMarkdown.path, sx: { color: '#455a64' } },\n data: {\n $id: null,\n componentId: MARKDOWN_ID,\n pluginId: BUNDLE_ID,\n props: {\n content:\n '## First section\\n\\nReplace this with your document. Paste ' +\n 'markdown straight from the source file.\\n\\n' +\n '### A sub-section\\n\\nHeadings become anchors, and a Table of ' +\n 'contents element on the same page links to them.',\n },\n },\n },\n {\n $id: generatePresetId(TABLE_OF_CONTENTS_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Table of Contents',\n pluginId: BUNDLE_ID,\n description: 'On this page — links to a Markdown element’s headings',\n category: Aglyn.ComponentCategory.NAVIGATION,\n icon: { path: mdiTableOfContents.path, sx: { color: '#455a64' } },\n data: {\n $id: null,\n componentId: TABLE_OF_CONTENTS_ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n]\n\nexport { Markdown, TableOfContents }\nexport default Markdown\n"],"names":["Aglyn","mdiLanguageMarkdown","mdiTableOfContents","AppLink","scrollOverflowFadeKeyframes","scrollOverflowFadeTimeline","scrollRegionProps","scrollableTableSx","scrollableTableWrapperSx","Box","MuiLink","Typography","observer","forwardRef","useContext","useMemo","BUNDLE_ID","dropClearedProps","generatePresetId","MARKDOWN_ID","UNRESOLVED_TOKEN","TABLE_REGION_LABEL","TABLE_OF_CONTENTS_ID","BODY_SX","fontSize","lineHeight","color","BODY_COLOUR_SX","HEADING_SX","xs","sm","md","letterSpacing","fontWeight","ANCHOR_SCROLL_MARGIN","HEADING_ANCHOR_SCROLL_MARGIN","renderInlines","inlines","links","map","inline","index","type","strong","text","em","link","resolveMarkdownLink","href","kind","span","component","sx","cursor","Markdown","props","ref","content","intrinsicSizes","children","_children","rest","suppressNavigation","screens","ScreenLinkContext","hostId","useSite","source","trim","blocks","parseMarkdownLite","slugs","markdownHeadingSlugs","p","border","borderColor","Array","isArray","block","id","level","scrollMarginTop","mt","mb","mediaBodyImageAttributes","src","size","alt","maxWidth","height","borderRadius","my","DEFERRED_IMAGE_ATTRIBUTES","pl","items","item","itemIndex","li","start","overflowX","bgcolor","fontFamily","code","borderCollapse","px","py","thead","tr","header","cell","cellIndex","th","style","textAlign","align","tbody","rows","row","rowIndex","td","mx","borderLeft","fontStyle","displayName","resolveMarkdownSource","root","forNodeId","found","walk","node","componentId","COLLECTION_ENTRY_BODY_COMPONENT_ID","undefined","resolvedProps","String","test","push","$id","child","picked","find","entry","leafIdsMatch","scrollToHeading","anchor","slug","getRootNode","ownerDocument","getElementById","target","scrollIntoView","TOC_VIEWPORT_ALLOWANCE","TABLE_OF_CONTENTS_LIST_SX","theme","listStyle","m","breakpoints","up","maxHeight","overflowY","overscrollBehavior","scrollbarWidth","scrollbarColor","palette","disabled","width","backgroundColor","TableOfContents","heading","depth","rawRest","canvas","rootNode","entries","headings","collectMarkdownHeadings","filter","title","length","aria-label","textTransform","underline","onClick","event","currentTarget","preventDefault","display","markdownSchema","pluginId","description","category","ComponentCategory","TEXT","icon","path","flags","dropping","FEATURE_FLAG","DISABLED","attributes","name","label","FieldComponentType","MARKDOWN","tableOfContentsSchema","NAVIGATION","selfClosing","ENABLED","NODE_SELECT","TEXT_FIELD","SELECT","options","value","markdownPresets","NodeType","PRESET","data"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/mui/src/lib/components/markdown.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiLanguageMarkdown, mdiTableOfContents } from '@aglyn/shared-data-mdi'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport {\n scrollOverflowFadeKeyframes,\n scrollOverflowFadeTimeline,\n scrollRegionProps,\n scrollableTableSx,\n scrollableTableWrapperSx,\n} from '@aglyn/shared-ui-jsx/utils/scroll-overflow'\nimport Box, { type BoxProps } from '@mui/material/Box'\nimport MuiLink from '@mui/material/Link'\nimport type { Theme } from '@mui/material/styles'\nimport Typography from '@mui/material/Typography'\nimport { observer } from 'mobx-react-lite'\nimport type { MouseEvent, ReactNode } from 'react'\nimport { forwardRef, useContext, useMemo } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { dropClearedProps } from '../utils/drop-cleared-props'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n// Component ids are persisted in screen documents; never rename.\nexport const MARKDOWN_ID: Aglyn.ComponentId = 'markdown'\n/** A compose-time token the tenant has not substituted — never a document. */\nconst UNRESOLVED_TOKEN = /^\\{\\{[^}]+\\}\\}$/\n/**\n * What a screen reader announces on reaching a table's scroll box.\n *\n * The plain word, not the table's own heading: the name has to come from\n * something every document has, and a markdown table carries no caption.\n */\nconst TABLE_REGION_LABEL = 'Table'\nexport const TABLE_OF_CONTENTS_ID: Aglyn.ComponentId = 'tableOfContents'\n\n/**\n * A long document's body type, which is NOT MUI's `body1` (AGL-1162).\n *\n * A privacy policy is read, not skimmed: the house measure for running prose\n * is 17px on a 1.75 leading, in `text.primary` rather than the softened\n * secondary a caption uses. MUI's default 16/1.5 sets the same words tighter\n * and lighter, and a page built from Typography defaults reads visibly\n * different from one built by hand next to it — which is how the legal pages\n * ended up hand-assembled from hundreds of Typography nodes in the first\n * place.\n *\n * ⚑ `inherit`, not `text.primary` (AGL-3210). The colour is still\n * `text.primary` — {@link BODY_COLOUR_SX} puts it on the element's own root,\n * where an authored `color` can replace it. Repeating the token on every\n * block instead made the author's colour unreachable: it landed on the root\n * and every child overrode it, so a card with a fixed light background kept\n * dark mode's near-white body text and read as blank.\n */\nconst BODY_SX = {\n fontSize: 17,\n lineHeight: 1.75,\n color: 'inherit',\n} as const\n\n/**\n * The element's own colour, applied to its root BEFORE the authored `sx`\n * (AGL-3210), so it is a default the Styles panel can replace rather than a\n * floor every block re-asserts. Inside the element everything inherits it.\n */\nconst BODY_COLOUR_SX = { color: 'text.primary' } as const\n\n/**\n * Heading ramp (AGL-1162). Sizes step across `{xs, sm, md}` and the tracking\n * steps WITH them: tracking is a function of size, so a single letterSpacing\n * that looks right at the desktop size is visibly loose on a phone. Negative\n * throughout — display sizes need the letters pulled together, which is the\n * opposite of the positive tracking small uppercase labels take.\n */\nconst HEADING_SX = {\n 2: {\n fontSize: { xs: 26, sm: 30, md: 34 },\n letterSpacing: { xs: '-0.3px', sm: '-0.4px', md: '-0.5px' },\n lineHeight: 1.25,\n fontWeight: 700,\n },\n 3: {\n fontSize: { xs: 19, sm: 20, md: 22 },\n letterSpacing: { xs: '-0.1px', sm: '-0.15px', md: '-0.2px' },\n lineHeight: 1.35,\n fontWeight: 700,\n },\n} as const\n\n/**\n * Breathing room a heading keeps above it when an anchor lands on it. Sites\n * built here usually have a sticky app bar, and without this the browser\n * scrolls the heading to y=0 — underneath the bar — so a working link looks\n * like a broken one.\n */\nconst ANCHOR_SCROLL_MARGIN = Aglyn.HEADING_ANCHOR_SCROLL_MARGIN\n\n/**\n * Inline runs as elements (AGL-1162), following the entry-body renderer:\n * internal links route through AppLink for client-side navigation, external\n * ones stay plain anchors, and editing surfaces render the link look without\n * an href so clicking never leaves the canvas.\n *\n * Which of those a link is — and whether it is a link at all — is\n * `resolveMarkdownLink`'s to say (AGL-3118), so a target named by reference\n * resolves through the same routing map an element link does, and one whose\n * target is gone renders as its words rather than as `href=\"entry:…\"`.\n */\nconst renderInlines = (\n inlines: Aglyn.MarkdownInline[],\n links: { suppressNavigation?: boolean; screens?: Aglyn.ScreenRouteMap },\n): ReactNode[] =>\n inlines.map((inline, index) => {\n if (inline.type === 'bold') return <strong key={index}>{inline.text}</strong>\n if (inline.type === 'italic') return <em key={index}>{inline.text}</em>\n if (inline.type === 'link') {\n const link = Aglyn.resolveMarkdownLink(inline.href, links)\n if (link.kind === 'text') return <span key={index}>{inline.text}</span>\n if (link.kind === 'inert') {\n return (\n <MuiLink key={index} component=\"span\" sx={{ cursor: 'default' }}>\n {inline.text}\n </MuiLink>\n )\n }\n return link.kind === 'internal' ? (\n <AppLink key={index} href={link.href}>\n {inline.text}\n </AppLink>\n ) : (\n <MuiLink key={index} href={link.href}>\n {inline.text}\n </MuiLink>\n )\n }\n return <span key={index}>{inline.text}</span>\n })\n\nexport interface MarkdownProps extends BoxProps {\n /** The document, as markdown-lite source. */\n content?: string\n /**\n * The pixel pair of each library image this document names, keyed by the\n * target as the document writes it, stamped by the composition from the\n * assets' DAM records (AGL-3149) — never set by hand.\n *\n * An image with no entry here renders exactly as it did before this prop\n * existed: see `mediaBodyImageAttributes` for why a partial answer is worse\n * than none.\n */\n intrinsicSizes?: Record<\n string,\n { width: number; height: number; version?: string }\n >\n}\n\n/**\n * A whole document authored as markdown (AGL-1162).\n *\n * The elements that made a legal or policy page before this were hundreds of\n * Typography nodes, one per paragraph — which meant the page could not be\n * pasted from, or back into, the markdown file in Drive that is its actual\n * source. Here the source IS the element's content, so updating the page is\n * one paste.\n *\n * Rendered through the shared markdown-lite parser, which emits plain data\n * blocks and never an HTML string — so there is no `dangerouslySetInnerHTML`\n * anywhere on this path and nothing to sanitize. Parsing is pure, so the full\n * document server-renders and the text is in the crawled HTML.\n *\n * Child NODES are never rendered, in either branch (AGL-1388). The document\n * is the `content` prop; there is no position in a parsed block list that a\n * dropped element could occupy, so the schema turns dropping off and\n * `nodeAcceptsChildren` now honors that — a drop lands as a sibling instead.\n * `children` is destructured away rather than left in `rest` because the\n * empty-content branch spreads `rest` onto a childless Box: without this, a\n * Markdown element rendered exactly the child nodes it had swallowed a\n * moment earlier as soon as its content was cleared.\n */\nconst Markdown = forwardRef<HTMLDivElement, MarkdownProps>((props, ref) => {\n const { content, intrinsicSizes, sx, children: _children, ...rest } = props\n // The routing map travels with the suppression flag: both decide what a\n // link in this document renders as (AGL-3118).\n const { suppressNavigation, screens } = useContext(Aglyn.ScreenLinkContext)\n const links = useMemo(\n () => ({ suppressNavigation, screens }),\n [suppressNavigation, screens],\n )\n /**\n * The site being rendered, for image blocks (AGL-1686). Read once here\n * rather than per block: hooks cannot run inside the `blocks.map` below,\n * and a document may hold any number of images.\n */\n const { hostId } = Aglyn.useSite()\n const source = (content ?? '').trim()\n const blocks = useMemo(\n () => (source ? Aglyn.parseMarkdownLite(source) : []),\n [source],\n )\n // Slugs are derived rather than stored (see collectMarkdownHeadings), and\n // keyed by block index so the ids land on the right headings even when two\n // of them carry the same words. The map is built in `markdown-lite` now,\n // because every renderer needs exactly this and five of the six were not\n // getting it (AGL-1162).\n const slugs = useMemo(() => Aglyn.markdownHeadingSlugs(blocks), [blocks])\n\n if (!source) {\n // Editing surfaces get somewhere to click; the published page renders\n // nothing rather than a placeholder nobody meant to publish.\n if (!suppressNavigation) return <Box ref={ref} sx={sx} {...rest} />\n return (\n <Box\n ref={ref}\n {...rest}\n sx={[\n {\n p: 2,\n border: '1px dashed',\n borderColor: 'divider',\n color: 'text.secondary',\n fontSize: 12,\n },\n ...(Array.isArray(sx) ? sx : sx ? [sx] : []),\n ]}\n >\n {'Markdown — paste the document into the Content attribute'}\n </Box>\n )\n }\n\n return (\n <Box\n ref={ref}\n {...rest}\n sx={[BODY_COLOUR_SX, ...(Array.isArray(sx) ? sx : sx ? [sx] : [])]}\n >\n {blocks.map((block, index) => {\n if (block.type === 'heading') {\n return (\n <Typography\n key={index}\n // The anchor a table of contents links to. `component` is set\n // explicitly because a Typography with only an `sx` fontSize\n // still renders a <p>: the page would screenshot correctly and\n // have no headings in it at all.\n id={slugs[index]}\n component={block.level === 2 ? 'h2' : 'h3'}\n sx={{\n ...HEADING_SX[block.level],\n // See BODY_SX: the token lives on the root, not on every\n // block, so an authored colour reaches the words (AGL-3210).\n color: 'inherit',\n scrollMarginTop: `${ANCHOR_SCROLL_MARGIN}px`,\n mt: block.level === 2 ? 5 : 3.5,\n mb: 1.5,\n '&:first-of-type': { mt: 0 },\n }}\n >\n {renderInlines(block.inlines, links)}\n </Typography>\n )\n }\n if (block.type === 'image') {\n return (\n <Box\n key={index}\n component=\"img\"\n // Resolved, exactly as `image.tsx` resolves its `src` prop\n // (AGL-1686). A document records WHICH ASSET; turning that into\n // a URL is the renderer's business, so the `/api/media/cdn/…`\n // route shape never has to be baked into a published document.\n // Every other value — a legacy storage URL, a legacy CDN path,\n // an author-typed hotlink — passes through untouched.\n //\n // The resolve now comes back with the WebP candidate list and\n // the intrinsic pair beside it, when the composition could read\n // the asset's record (AGL-3149). Shared with the entry body and\n // with `image.tsx`'s own srcSet so a body image cannot drift\n // away from the element again.\n {...Aglyn.mediaBodyImageAttributes({\n src: block.src,\n hostId,\n size: intrinsicSizes?.[block.src],\n })}\n alt={block.alt}\n // `height: 'auto'` is load-bearing beside the pair above, not\n // tidiness: the pair's `height` arrives as a presentational hint\n // too, and a hint that survives the `maxWidth` cap squashes the\n // picture instead of scaling it. It is what `auto` already\n // resolved to before the pair existed, so this changes nothing\n // for an image the composition could not measure.\n sx={{ maxWidth: '100%', height: 'auto', borderRadius: 1, my: 2 }}\n // `lazy` alone until AGL-2486. A markdown image is by\n // definition inside prose the reader scrolls through, so it\n // belongs in the same deferred rank as everything else that is\n // not the lead image.\n {...Aglyn.DEFERRED_IMAGE_ATTRIBUTES}\n />\n )\n }\n if (block.type === 'list') {\n return (\n <Box\n key={index}\n component=\"ul\"\n sx={{ ...BODY_SX, my: 2, pl: 3, '& li': { mb: 1 } }}\n >\n {block.items.map((item, itemIndex) => (\n <li key={itemIndex}>\n {renderInlines(item, links)}\n </li>\n ))}\n </Box>\n )\n }\n // A numbered list (AGL-1320): the same measure as the bullet list,\n // as a real `<ol>` carrying `start` — a legal document's enumerated\n // elements are the reason this block exists.\n if (block.type === 'orderedList') {\n return (\n <Box\n key={index}\n component=\"ol\"\n start={block.start}\n sx={{ ...BODY_SX, my: 2, pl: 3, '& li': { mb: 1 } }}\n >\n {block.items.map((item, itemIndex) => (\n <li key={itemIndex}>\n {renderInlines(item, links)}\n </li>\n ))}\n </Box>\n )\n }\n // Code and tables scroll rather than wrap, like every other\n // markdown-lite renderer here (AGL-974): a wrapped command line\n // reads as two commands, and a squeezed table column loses the\n // structure that made it a table.\n if (block.type === 'code') {\n return (\n <Box\n key={index}\n component=\"pre\"\n sx={{\n my: 2,\n p: 2,\n overflowX: 'auto',\n borderRadius: 1,\n bgcolor: 'action.hover',\n fontFamily: 'monospace',\n fontSize: 14,\n }}\n >\n <code>{block.text}</code>\n </Box>\n )\n }\n if (block.type === 'table') {\n return (\n <Box\n key={index}\n {...scrollRegionProps(TABLE_REGION_LABEL)}\n sx={{ my: 2, ...scrollableTableWrapperSx }}\n >\n <Box\n component=\"table\"\n sx={{\n ...BODY_SX,\n fontSize: 16,\n borderCollapse: 'collapse',\n ...scrollableTableSx,\n '& th, & td': {\n border: '1px solid',\n borderColor: 'divider',\n px: 1.5,\n py: 1,\n },\n '& th': { bgcolor: 'action.hover', fontWeight: 600 },\n }}\n >\n <thead>\n <tr>\n {block.header.map((cell, cellIndex) => (\n <th\n key={cellIndex}\n style={{ textAlign: block.align[cellIndex] ?? 'left' }}\n >\n {renderInlines(cell, links)}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {block.rows.map((row, rowIndex) => (\n <tr key={rowIndex}>\n {row.map((cell, cellIndex) => (\n <td\n key={cellIndex}\n style={{\n textAlign: block.align[cellIndex] ?? 'left',\n }}\n >\n {renderInlines(cell, links)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </Box>\n </Box>\n )\n }\n // A pull-quote (AGL-1315): the document's body measure, one size up\n // and italic, behind a left accent — palette tokens keep it right in\n // both modes.\n if (block.type === 'quote') {\n return (\n <Typography\n key={index}\n component=\"blockquote\"\n sx={{\n ...BODY_SX,\n my: 3,\n mx: 0,\n pl: 2.5,\n borderLeft: '3px solid',\n borderColor: 'primary.main',\n fontStyle: 'italic',\n fontSize: 21,\n lineHeight: 1.6,\n }}\n >\n {renderInlines(block.inlines, links)}\n </Typography>\n )\n }\n return (\n <Typography key={index} component=\"p\" sx={{ ...BODY_SX, my: 2 }}>\n {renderInlines(block.inlines, links)}\n </Typography>\n )\n })}\n </Box>\n )\n})\nMarkdown.displayName = 'AglynMarkdown'\n\n/* ── Table of contents ──────────────────────────────────────────────────── */\n\nexport interface TableOfContentsProps extends BoxProps {\n /**\n * The Markdown element this lists the headings of. Empty means \"the first\n * Markdown element on the screen\", which is the answer on every page that\n * has exactly one — see {@link resolveMarkdownSource}.\n */\n forNodeId?: string\n /** Aside heading; an empty string renders no heading at all. */\n heading?: string\n /** Deepest heading level listed: `'2'` for top level only. */\n depth?: '2' | '3'\n}\n\n/**\n * Which Markdown element a table of contents speaks for (AGL-1162).\n *\n * Resolved through the NODE TREE, never the DOM: the besigner canvas renders\n * inside a closed shadow root, so a `document.querySelector` from a component\n * finds nothing there and the aside would be empty in the one place it is\n * being authored.\n *\n * The tree is walked from the root in document order, so \"the first Markdown\n * element\" means the first one a reader meets — not the first key of a node\n * map, whose order is storage detail.\n *\n * An explicit `forNodeId` wins, compared with `leafIdsMatch` because the\n * stored id is the raw canvas id while a composed tree namespaces layout and\n * reusable-component grafts (AGL-573/1229). A pick that no longer resolves —\n * the element was deleted and re-added, which is exactly what re-pasting a\n * document looks like — falls back to the first Markdown element rather than\n * leaving a published page with an empty aside.\n */\nexport function resolveMarkdownSource(\n root: Aglyn.NodeSchema | undefined,\n forNodeId?: string,\n): string {\n const found: Array<{ $id: string; content: string }> = []\n const walk = (node: Aglyn.NodeSchema | undefined): void => {\n if (!node) return\n // Two element types hold a markdown-lite document, and an aside beside a\n // blog article is the case this issue was opened for (AGL-1162): the\n // Collection Entry Body carries the post, under `markdown` rather than\n // `content`, and used to be invisible here — so \"On this page\" on an\n // article template rendered its authoring affordance forever.\n const source =\n node.componentId === MARKDOWN_ID\n ? 'content'\n : node.componentId === Aglyn.COLLECTION_ENTRY_BODY_COMPONENT_ID\n ? 'markdown'\n : undefined\n if (source) {\n const props = (node.resolvedProps ?? node.props ?? {}) as Record<\n string,\n unknown\n >\n const content = String(props[source] ?? '')\n // An entry body on an unresolved surface is the literal\n // `{{entry.body}}` token. It is not a document, and treating it as one\n // would let it win over a real Markdown element further down the page\n // and empty the aside on the canvas.\n if (!UNRESOLVED_TOKEN.test(content.trim())) {\n found.push({ $id: node.$id, content })\n }\n }\n for (const child of node.children ?? []) walk(child)\n }\n walk(root)\n if (forNodeId) {\n const picked = found.find((entry) => Aglyn.leafIdsMatch(entry.$id, forNodeId))\n if (picked) return picked.content\n }\n return found[0]?.content ?? ''\n}\n\n/**\n * Scrolls to a heading in the cases the browser cannot reach, and ONLY those\n * (AGL-1162). Returns whether it handled the click.\n *\n * In `document` it deliberately does nothing and lets the plain `#slug`\n * anchor work: native fragment navigation updates the address bar, adds a\n * history entry the back button understands, and honours the site's CSS\n * `scroll-behavior` — none of which a hand-rolled `scrollIntoView` gives\n * back. It is also the only version that works with JavaScript off.\n *\n * (`behavior: 'smooth'` was the first attempt at the whole job, and it is\n * exactly the wrong shape: a browser that declines to animate — a background\n * tab, reduced-motion — leaves the click prevented and the page unmoved, so\n * a working link reads as a broken one.)\n *\n * The besigner canvas renders inside a CLOSED shadow root, where the heading\n * is not in `document` at all and a fragment navigation finds nothing. There\n * the link's own `getRootNode()` is the shadow root holding both ends, and\n * `ShadowRoot.getElementById` finds the heading.\n */\nexport function scrollToHeading(anchor: Element, slug: string): boolean {\n const root = anchor.getRootNode()\n if (root === anchor.ownerDocument || !(root as ShadowRoot).getElementById) {\n return false\n }\n const target = (root as ShadowRoot).getElementById(slug)\n if (!target) return false\n // No explicit `behavior`: 'auto' follows the scrolling box's CSS\n // `scroll-behavior`, so a site that asked for smooth still gets it.\n target.scrollIntoView({ block: 'start' })\n return true\n}\n\n/**\n * What the sidebar's chrome costs, above and below the list (AGL-2486).\n *\n * The list's bound is `100dvh` MINUS this, so it is a function of the window\n * rather than a height somebody picked. Measured on the live\n * `/legal/marketplace-publisher-agreement` sidebar, which is the shape every\n * legal page uses: the card is `position: sticky; top: 96px` under the app\n * bar, then 24px of card padding and 31px of \"ON THIS PAGE\" label sit above\n * the list and 31px of padding below it — 182px of chrome. 200 leaves the\n * card's bottom BORDER inside the window rather than flush against it, so\n * the card reads as a card and not as a page that ran out.\n *\n * It is an allowance, not a contract: this component renders inside whatever\n * the author built around it, and it cannot read that card's `top` offset.\n * Being a MAX height, an allowance a little too generous only means a few\n * pixels of the card sit below the fold; one a little too tight only means\n * the scroll box starts a line sooner. Both are recoverable, which is the\n * whole reason this is a `max-height` and not a `height`.\n */\nconst TOC_VIEWPORT_ALLOWANCE = '200px'\n\n/**\n * The list is its own scroll box once it outgrows the window (AGL-2486).\n *\n * The bug: nothing here was bounded, and the card the author wraps this in is\n * `position: sticky`. A sticky box taller than the viewport does not scroll\n * into view — it pins at its `top` offset and everything past the window's\n * bottom edge is simply unreachable. Measured on the Marketplace Publisher\n * Agreement (14 `h2`s) in a 1100x480 window: the card stood 502px tall from\n * `top: 96px` and overhung the fold by 118px, entry 11 was cut through the\n * middle of its own text at the window edge, and 12 to 14 could not be\n * reached by any scroll — `ol.scrollTop` stayed 0, because the list was not a\n * scroll box at all. Which row gets cut moves with the window height and the\n * browser zoom; the reported sighting was entry 8. Either way it did not read\n * as \"there is more\", it read as a document with fewer sections than it has.\n *\n * Three things this deliberately is NOT:\n *\n * - NOT a `height`. A `max-height` is inert until the content exceeds it, so\n * the four-item TOCs (`/legal/dmca`, `/legal/subprocessors`) keep their\n * exact present box: no scroll region, no reserved gutter, no dead space.\n * That is also why `overflow-y` is `auto` and never `scroll`.\n *\n * - NOT applied below `md`. The bound only makes sense for a sidebar. In the\n * stacked narrow layout the TOC is a full-width block in the page flow, and\n * bounding it there would plant a scroll trap in the middle of an article\n * to solve a clipping problem that layout does not have.\n *\n * - NOT silent, and the FADE is the part that carries that. A scroll box a\n * reader cannot see is the same bug wearing a scrollbar: measured in Chrome\n * on macOS, `offsetWidth - clientWidth` on this list is 0 even with\n * `scrollbar-color` set — the platform draws an OVERLAY scrollbar, which is\n * invisible until you have already guessed to scroll. So the thumb styling\n * is the secondary cue, for the platforms that draw one at rest, and the\n * primary cue is a bottom fade: the cut stops being a slice through the\n * middle of a word and becomes an edge that visibly continues.\n *\n * The fade rides a scroll-driven animation on purpose: a `scroll()` timeline\n * on a box with nothing to scroll is INACTIVE, so its animation applies no\n * styles at all. That is the only way to say \"fade only when overflowing\" in\n * CSS with no measuring and no JavaScript, and it degrades to plain\n * unfaded-but-scrollable in a browser that lacks it. Verified both ways at\n * 1100x480: the 14-entry list fades and scrolls, the 4-entry\n * `/legal/subprocessors` list computes `mask-image: none` and does not.\n *\n * NO new attribute goes with this, deliberately. The three the element has —\n * `forNodeId`, `heading`, `depth` — all answer \"which headings does this\n * list\", and a \"list height\" field would hand the author back the very\n * decision this bug is made of. The right answer is \"as much of the window as\n * there is\", which is a thing CSS can work out and an author cannot type.\n */\nconst TABLE_OF_CONTENTS_LIST_SX = (theme: Theme) => ({\n listStyle: 'none',\n m: 0,\n p: 0,\n [theme.breakpoints.up('md')]: {\n maxHeight: `calc(100vh - ${TOC_VIEWPORT_ALLOWANCE})`,\n // `dvh` over `vh` where it exists: `vh` is the LARGEST viewport, so a\n // window with a visible toolbar gets a bound taller than the space it\n // has — which is the bug again, in miniature.\n '@supports (height: 100dvh)': {\n maxHeight: `calc(100dvh - ${TOC_VIEWPORT_ALLOWANCE})`,\n },\n overflowY: 'auto',\n // Reaching the end of the list must not then throw the page down a\n // screen — the reader was navigating the sidebar, not the document.\n overscrollBehavior: 'contain',\n scrollbarWidth: 'thin',\n // `text.disabled`, not `divider`: a 12%-black thumb is one you have to\n // already know is there. `3px` is QUOTED because a bare 3 is a theme\n // radius STEP and lands as 12px on a 6px-wide thumb.\n scrollbarColor: `${theme.palette.text.disabled} transparent`,\n '&::-webkit-scrollbar': { width: 6 },\n '&::-webkit-scrollbar-thumb': {\n backgroundColor: theme.palette.text.disabled,\n borderRadius: '3px',\n },\n ...scrollOverflowFadeTimeline('block'),\n },\n ...scrollOverflowFadeKeyframes('block'),\n})\n\n/**\n * \"On this page\" (AGL-1162) — the headings of a Markdown element on the same\n * screen, as working anchor links.\n *\n * A mobx `observer` because it reads ANOTHER node's props: the renderer's\n * Leaf re-renders on changes to the node it is rendering, and nothing else,\n * so without this the aside in the besigner would keep listing the headings\n * of the document as it was when the aside mounted.\n */\nconst TableOfContents = observer(\n forwardRef<HTMLElement, TableOfContentsProps>((props, ref) => {\n const { forNodeId, heading, depth, sx, ...rawRest } = props\n /**\n * The cleared-prop guard, applied to the props that LEAVE this\n * component rather than to all of them (AGL-1451).\n *\n * A props-wide `dropClearedProps` would be wrong here, and this is the\n * component that shows why the wrapper is not a reflex: `heading` reads\n * an EMPTY value as a real author choice — \"clear it to render no\n * label\", which is what its help text promises and what the `heading\n * === undefined ? 'On this page' : heading` line below implements. A\n * guard over the whole props object would strip the cleared heading and\n * put the default label back, i.e. eat the choice the author made. So\n * `heading` and `depth` are read as authored, and the guard covers only\n * what is spread onto the Box.\n */\n const rest = dropClearedProps(rawRest)\n const { suppressNavigation } = useContext(Aglyn.ScreenLinkContext)\n const source = resolveMarkdownSource(Aglyn.canvas.rootNode, forNodeId)\n const entries = useMemo(() => {\n const headings = Aglyn.collectMarkdownHeadings(\n Aglyn.parseMarkdownLite(source),\n )\n return depth === '2'\n ? headings.filter((entry) => entry.level === 2)\n : headings\n }, [source, depth])\n const title = heading === undefined ? 'On this page' : heading\n\n if (!entries.length) {\n // Same split as the Markdown element: an affordance while authoring,\n // nothing at all on the published page — and no empty `nav` landmark.\n if (!suppressNavigation) return <Box ref={ref} sx={sx} {...rest} />\n return (\n <Box\n ref={ref}\n component=\"nav\"\n sx={[\n {\n p: 2,\n border: '1px dashed',\n borderColor: 'divider',\n color: 'text.secondary',\n fontSize: 12,\n },\n ...(Array.isArray(sx) ? sx : sx ? [sx] : []),\n ]}\n {...rest}\n >\n {'Table of contents — add ## headings to the Markdown element'}\n </Box>\n )\n }\n\n return (\n <Box\n ref={ref}\n component=\"nav\"\n aria-label={title || 'Table of contents'}\n sx={sx}\n {...rest}\n >\n {title ? (\n <Typography\n component=\"p\"\n sx={{\n fontSize: 12,\n fontWeight: 700,\n // Positive tracking here, unlike the document headings: this is\n // a small uppercase label, where the letters need pushing apart\n // rather than pulling together.\n letterSpacing: '0.8px',\n textTransform: 'uppercase',\n color: 'text.secondary',\n mb: 1.5,\n }}\n >\n {title}\n </Typography>\n ) : null}\n <Box component=\"ol\" sx={TABLE_OF_CONTENTS_LIST_SX}>\n {entries.map((entry) => (\n <Box component=\"li\" key={entry.slug} sx={{ mb: 0.75 }}>\n <MuiLink\n // A real href, so the entry is a link to a crawler, works\n // with no JavaScript, and can be copied out of the page.\n href={`#${entry.slug}`}\n underline=\"hover\"\n color=\"text.secondary\"\n onClick={(event: MouseEvent<HTMLAnchorElement>) => {\n // Prevented ONLY when this actually moved the page — on a\n // published page it does nothing and the anchor works the\n // way anchors do.\n if (scrollToHeading(event.currentTarget, entry.slug)) {\n event.preventDefault()\n }\n }}\n sx={{\n display: 'block',\n fontSize: 15,\n lineHeight: 1.5,\n // Sub-headings indent; nothing else distinguishes them, so\n // a flat list of both levels reads as one long list.\n pl: entry.level === 3 ? 2 : 0,\n '&:hover': { color: 'text.primary' },\n }}\n >\n {entry.text}\n </MuiLink>\n </Box>\n ))}\n </Box>\n </Box>\n )\n }),\n)\nTableOfContents.displayName = 'AglynTableOfContents'\n\nexport const markdownSchema: Aglyn.ComponentSchema<MarkdownProps> = {\n $id: MARKDOWN_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Markdown',\n description:\n 'A whole document written as markdown. It takes no child elements.',\n category: Aglyn.ComponentCategory.TEXT,\n icon: { path: mdiLanguageMarkdown.path, sx: { color: '#455a64' } },\n flags: {\n // The document is the `content` attribute; dropping elements inside a\n // rendered document would put them somewhere the markdown cannot say.\n dropping: Aglyn.FEATURE_FLAG.DISABLED,\n },\n attributes: [\n {\n name: 'content',\n label: 'Content',\n description:\n 'The document, as markdown. Headings (## and ###), paragraphs, ' +\n 'bold/italic, links, bullet and 1. numbered lists, images, fenced ' +\n 'code, pipe tables and > quotes. Paste the whole source file — a ' +\n 'Table of contents element on the same page lists the ## and ### ' +\n 'headings automatically.',\n // The WYSIWYG rather than a raw textarea (AGL-1616): this attribute is\n // a whole document — the published Privacy Policy body is one of them —\n // and editing it as raw markdown-lite meant a 13 KB paste (AGL-1594).\n // The stored value is unchanged, so every renderer is untouched.\n component: Aglyn.FieldComponentType.MARKDOWN,\n },\n ],\n}\n\nexport const tableOfContentsSchema: Aglyn.ComponentSchema<TableOfContentsProps> =\n {\n $id: TABLE_OF_CONTENTS_ID,\n pluginId: BUNDLE_ID,\n displayName: 'Table of Contents',\n description:\n \"An on-this-page list linking to a Markdown element's headings.\",\n category: Aglyn.ComponentCategory.NAVIGATION,\n icon: { path: mdiTableOfContents.path, sx: { color: '#455a64' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'forNodeId',\n label: 'Markdown element',\n description:\n 'The Markdown element to list the headings of. Leave empty on a ' +\n 'page with one Markdown element — the first one on the page is ' +\n 'used, and a pick that no longer exists falls back to it too.',\n component: Aglyn.FieldComponentType.NODE_SELECT,\n },\n {\n name: 'heading',\n label: 'Heading',\n description: 'Label above the list. Clear it to render no label.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'depth',\n label: 'Levels',\n description: 'How deep the list goes.',\n component: Aglyn.FieldComponentType.SELECT,\n // `depth` never reaches MUI — it is read here, as `depth === '2'`.\n // So the `''` was not a rendering defect; it was an option that\n // could not survive a save (AGL-1191), which meant an author who\n // set \"## headings only\" and then changed their mind could not put\n // it back. `'3'` states the same thing as a real, persistable\n // value — and is what `TableOfContentsProps.depth` has declared\n // all along, so the option list was the half that had drifted.\n // Both levels remain what an unset field means (AGL-1451).\n options: [\n { value: '3', label: '## and ### headings' },\n { value: '2', label: '## headings only' },\n ],\n },\n ],\n }\n\nexport const markdownPresets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(MARKDOWN_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Markdown',\n pluginId: BUNDLE_ID,\n description: 'A whole document, authored as markdown',\n category: Aglyn.ComponentCategory.TEXT,\n icon: { path: mdiLanguageMarkdown.path, sx: { color: '#455a64' } },\n data: {\n $id: null,\n componentId: MARKDOWN_ID,\n pluginId: BUNDLE_ID,\n props: {\n content:\n '## First section\\n\\nReplace this with your document. Paste ' +\n 'markdown straight from the source file.\\n\\n' +\n '### A sub-section\\n\\nHeadings become anchors, and a Table of ' +\n 'contents element on the same page links to them.',\n },\n },\n },\n {\n $id: generatePresetId(TABLE_OF_CONTENTS_ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Table of Contents',\n pluginId: BUNDLE_ID,\n description: 'On this page — links to a Markdown element’s headings',\n category: Aglyn.ComponentCategory.NAVIGATION,\n icon: { path: mdiTableOfContents.path, sx: { color: '#455a64' } },\n data: {\n $id: null,\n componentId: TABLE_OF_CONTENTS_ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n]\n\nexport { Markdown, TableOfContents }\nexport default Markdown\n"],"names":["Aglyn","mdiLanguageMarkdown","mdiTableOfContents","AppLink","scrollOverflowFadeKeyframes","scrollOverflowFadeTimeline","scrollRegionProps","scrollableTableSx","scrollableTableWrapperSx","Box","MuiLink","Typography","observer","forwardRef","useContext","useMemo","BUNDLE_ID","dropClearedProps","generatePresetId","MARKDOWN_ID","UNRESOLVED_TOKEN","TABLE_REGION_LABEL","TABLE_OF_CONTENTS_ID","BODY_SX","fontSize","lineHeight","color","BODY_COLOUR_SX","HEADING_SX","xs","sm","md","letterSpacing","fontWeight","ANCHOR_SCROLL_MARGIN","HEADING_ANCHOR_SCROLL_MARGIN","renderInlines","inlines","links","map","inline","index","type","strong","text","em","link","resolveMarkdownLink","href","kind","span","component","sx","cursor","Markdown","props","ref","content","intrinsicSizes","children","_children","rest","suppressNavigation","screens","ScreenLinkContext","hostId","useSite","source","trim","blocks","parseMarkdownLite","slugs","markdownHeadingSlugs","p","border","borderColor","Array","isArray","block","id","level","scrollMarginTop","mt","mb","mediaBodyImageAttributes","src","size","alt","maxWidth","height","borderRadius","my","DEFERRED_IMAGE_ATTRIBUTES","pl","items","item","itemIndex","li","start","overflowX","bgcolor","fontFamily","code","borderCollapse","px","py","thead","tr","header","cell","cellIndex","th","style","textAlign","align","tbody","rows","row","rowIndex","td","mx","borderLeft","fontStyle","displayName","resolveMarkdownSource","root","forNodeId","found","walk","node","componentId","COLLECTION_ENTRY_BODY_COMPONENT_ID","undefined","resolvedProps","String","test","push","$id","child","picked","find","entry","leafIdsMatch","scrollToHeading","anchor","slug","getRootNode","ownerDocument","getElementById","target","scrollIntoView","TOC_VIEWPORT_ALLOWANCE","TABLE_OF_CONTENTS_LIST_SX","theme","listStyle","m","breakpoints","up","maxHeight","overflowY","overscrollBehavior","scrollbarWidth","scrollbarColor","palette","disabled","width","backgroundColor","TableOfContents","heading","depth","rawRest","canvas","rootNode","entries","headings","collectMarkdownHeadings","filter","title","length","aria-label","textTransform","underline","onClick","event","currentTarget","preventDefault","display","markdownSchema","pluginId","description","category","ComponentCategory","TEXT","icon","path","flags","dropping","FEATURE_FLAG","DISABLED","attributes","name","label","FieldComponentType","MARKDOWN","tableOfContentsSchema","NAVIGATION","selfClosing","ENABLED","NODE_SELECT","TEXT_FIELD","SELECT","options","value","markdownPresets","NodeType","PRESET","data"],"mappings":"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"}
@@ -33,7 +33,9 @@ export interface SearchBoxProps {
33
33
  * Site search box (AGL-88): a plain GET form to the tenant's /search page,
34
34
  * so it works with zero JavaScript and is inert in the besigner (the
35
35
  * console has no /search route to submit to; navigation there is a no-op
36
- * form post the editor never triggers).
36
+ * form post the editor never triggers). Once hydrated the submit goes
37
+ * through the client router, so the site's chrome stays mounted rather than
38
+ * the whole document reloading for a results page.
37
39
  */
38
40
  declare const SearchBox: import("react").ForwardRefExoticComponent<SearchBoxProps & import("react").RefAttributes<HTMLFormElement>>;
39
41
  export declare const schema: Aglyn.ComponentSchema<SearchBoxProps>;
@@ -22,6 +22,7 @@ import IconButton from "@mui/material/IconButton";
22
22
  import InputAdornment from "@mui/material/InputAdornment";
23
23
  import TextField from "@mui/material/TextField";
24
24
  import { MdiIcon } from "@aglyn/shared-ui-jsx";
25
+ import { useSoftGetSubmit } from "@aglyn/shared-ui-jsx/hooks/use-soft-get-submit";
25
26
  import { forwardRef } from "react";
26
27
  import { BUNDLE_ID } from "../constants/bundle-common.js";
27
28
  import { generatePresetId } from "../utils/generate-preset-id.js";
@@ -31,17 +32,21 @@ export const ID = 'searchBox';
31
32
  * Site search box (AGL-88): a plain GET form to the tenant's /search page,
32
33
  * so it works with zero JavaScript and is inert in the besigner (the
33
34
  * console has no /search route to submit to; navigation there is a no-op
34
- * form post the editor never triggers).
35
+ * form post the editor never triggers). Once hydrated the submit goes
36
+ * through the client router, so the site's chrome stays mounted rather than
37
+ * the whole document reloading for a results page.
35
38
  */ const SearchBox = /*#__PURE__*/ forwardRef((props, ref)=>{
36
39
  const { placeholder, defaultValue } = props, rest = _object_without_properties_loose(props, [
37
40
  "placeholder",
38
41
  "defaultValue"
39
42
  ]);
43
+ const onSubmit = useSoftGetSubmit();
40
44
  return /*#__PURE__*/ _jsx("form", _extends({
41
45
  ref: ref,
42
46
  action: "/search",
43
47
  method: "get",
44
- role: "search"
48
+ role: "search",
49
+ onSubmit: onSubmit
45
50
  }, rest, {
46
51
  children: /*#__PURE__*/ _jsx(TextField, {
47
52
  name: "q",
@@ -64,7 +69,7 @@ export const ID = 'searchBox';
64
69
  })
65
70
  }
66
71
  }
67
- })
72
+ }, defaultValue || '')
68
73
  }));
69
74
  });
70
75
  SearchBox.displayName = 'AglynSearchBox';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/mui/src/lib/components/search-box.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiMagnify } from '@aglyn/shared-data-mdi'\nimport IconButton from '@mui/material/IconButton'\nimport InputAdornment from '@mui/material/InputAdornment'\nimport TextField from '@mui/material/TextField'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { forwardRef } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n// Component ids are persisted in screen documents; never rename.\nexport const ID: Aglyn.ComponentId = 'searchBox'\n\nexport interface SearchBoxProps {\n placeholder?: string\n /**\n * What the field starts with (AGL-2513). The results page prefills the\n * query it is showing, so a reader can refine it where they are rather\n * than going back for the box they typed into.\n *\n * Uncontrolled on purpose: this component holds no state and issues no\n * request, and a `value` would freeze the field the moment the reader\n * typed into it.\n */\n defaultValue?: string\n}\n\n/**\n * Site search box (AGL-88): a plain GET form to the tenant's /search page,\n * so it works with zero JavaScript and is inert in the besigner (the\n * console has no /search route to submit to; navigation there is a no-op\n * form post the editor never triggers).\n */\nconst SearchBox = forwardRef<HTMLFormElement, SearchBoxProps>(\n (props, ref) => {\n const { placeholder, defaultValue, ...rest } = props\n return (\n <form ref={ref} action=\"/search\" method=\"get\" role=\"search\" {...rest}>\n <TextField\n name=\"q\"\n size=\"small\"\n placeholder={placeholder || 'Search…'}\n defaultValue={defaultValue || ''}\n slotProps={{\n input: {\n endAdornment: (\n <InputAdornment position=\"end\">\n <IconButton type=\"submit\" size=\"small\" aria-label=\"search\">\n <MdiIcon path={mdiMagnify.path} fontSize=\"small\" />\n </IconButton>\n </InputAdornment>\n ),\n },\n }}\n />\n </form>\n )\n },\n)\nSearchBox.displayName = 'AglynSearchBox'\n\nexport const schema: Aglyn.ComponentSchema<SearchBoxProps> = {\n $id: ID,\n pluginId: BUNDLE_ID,\n displayName: 'Search Box',\n description:\n \"A search field that opens the site's search page. It works without JavaScript.\",\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiMagnify.path, sx: { color: '#455a64' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'placeholder',\n description: 'Placeholder text inside the search field.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Placeholder',\n },\n {\n name: 'defaultValue',\n description:\n 'Text the field starts with. The search results page fills in the ' +\n 'query it is showing.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Starting text',\n },\n ],\n}\n\nexport const presets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Search Box',\n pluginId: BUNDLE_ID,\n description: 'Search field that opens the site search page',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiMagnify.path, sx: { color: '#455a64' } },\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n]\n\nexport default SearchBox\n"],"names":["Aglyn","mdiMagnify","IconButton","InputAdornment","TextField","MdiIcon","forwardRef","BUNDLE_ID","generatePresetId","ID","SearchBox","props","ref","placeholder","defaultValue","rest","form","action","method","role","name","size","slotProps","input","endAdornment","position","type","aria-label","path","fontSize","displayName","schema","$id","pluginId","description","category","ComponentCategory","FORMS","icon","sx","color","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","component","FieldComponentType","TEXT_FIELD","label","presets","NodeType","PRESET","data","componentId"],"mappings":";;;AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,UAAU,QAAQ,yBAAwB;AACnD,OAAOC,gBAAgB,2BAA0B;AACjD,OAAOC,oBAAoB,+BAA8B;AACzD,OAAOC,eAAe,0BAAyB;AAC/C,SAASC,OAAO,QAAQ,uBAAsB;AAC9C,SAASC,UAAU,QAAQ,QAAO;AAClC,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,gBAAgB,QAAQ,iCAA6B;AAE9D,iEAAiE;AACjE,OAAO,MAAMC,KAAwB,YAAW;AAgBhD;;;;;CAKC,GACD,MAAMC,0BAAYJ,WAChB,CAACK,OAAOC;IACN,MAAM,EAAEC,WAAW,EAAEC,YAAY,EAAW,GAAGH,OAATI,wCAASJ;;;;IAC/C,qBACE,KAACK;QAAKJ,KAAKA;QAAKK,QAAO;QAAUC,QAAO;QAAMC,MAAK;OAAaJ;kBAC9D,cAAA,KAACX;YACCgB,MAAK;YACLC,MAAK;YACLR,aAAaA,eAAe;YAC5BC,cAAcA,gBAAgB;YAC9BQ,WAAW;gBACTC,OAAO;oBACLC,4BACE,KAACrB;wBAAesB,UAAS;kCACvB,cAAA,KAACvB;4BAAWwB,MAAK;4BAASL,MAAK;4BAAQM,cAAW;sCAChD,cAAA,KAACtB;gCAAQuB,MAAM3B,WAAW2B,IAAI;gCAAEC,UAAS;;;;gBAIjD;YACF;;;AAIR;AAEFnB,UAAUoB,WAAW,GAAG;AAExB,OAAO,MAAMC,SAAgD;IAC3DC,KAAKvB;IACLwB,UAAU1B;IACVuB,aAAa;IACbI,aACE;IACFC,UAAUnC,MAAMoC,iBAAiB,CAACC,KAAK;IACvCC,MAAM;QAAEV,MAAM3B,WAAW2B,IAAI;QAAEW,IAAI;YAAEC,OAAO;QAAU;IAAE;IACxDC,OAAO;QAAEC,aAAa1C,MAAM2C,YAAY,CAACC,OAAO;IAAC;IACjDC,YAAY;QACV;YACEzB,MAAM;YACNc,aAAa;YACbY,WAAW9C,MAAM+C,kBAAkB,CAACC,UAAU;YAC9CC,OAAO;QACT;QACA;YACE7B,MAAM;YACNc,aACE,sEACA;YACFY,WAAW9C,MAAM+C,kBAAkB,CAACC,UAAU;YAC9CC,OAAO;QACT;KACD;AACH,EAAC;AAED,OAAO,MAAMC,UAAgC;IAC3C;QACElB,KAAKxB,iBAAiBC;QACtBiB,MAAM1B,MAAMmD,QAAQ,CAACC,MAAM;QAC3BtB,aAAa;QACbG,UAAU1B;QACV2B,aAAa;QACbC,UAAUnC,MAAMoC,iBAAiB,CAACC,KAAK;QACvCC,MAAM;YAAEV,MAAM3B,WAAW2B,IAAI;YAAEW,IAAI;gBAAEC,OAAO;YAAU;QAAE;QACxDa,MAAM;YACJrB,KAAK;YACLsB,aAAa7C;YACbwB,UAAU1B;YACVI,OAAO,CAAC;QACV;IACF;CACD,CAAA;AAED,eAAeD,UAAS"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/mui/src/lib/components/search-box.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiMagnify } from '@aglyn/shared-data-mdi'\nimport IconButton from '@mui/material/IconButton'\nimport InputAdornment from '@mui/material/InputAdornment'\nimport TextField from '@mui/material/TextField'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { useSoftGetSubmit } from '@aglyn/shared-ui-jsx/hooks/use-soft-get-submit'\nimport { forwardRef } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n// Component ids are persisted in screen documents; never rename.\nexport const ID: Aglyn.ComponentId = 'searchBox'\n\nexport interface SearchBoxProps {\n placeholder?: string\n /**\n * What the field starts with (AGL-2513). The results page prefills the\n * query it is showing, so a reader can refine it where they are rather\n * than going back for the box they typed into.\n *\n * Uncontrolled on purpose: this component holds no state and issues no\n * request, and a `value` would freeze the field the moment the reader\n * typed into it.\n */\n defaultValue?: string\n}\n\n/**\n * Site search box (AGL-88): a plain GET form to the tenant's /search page,\n * so it works with zero JavaScript and is inert in the besigner (the\n * console has no /search route to submit to; navigation there is a no-op\n * form post the editor never triggers). Once hydrated the submit goes\n * through the client router, so the site's chrome stays mounted rather than\n * the whole document reloading for a results page.\n */\nconst SearchBox = forwardRef<HTMLFormElement, SearchBoxProps>(\n (props, ref) => {\n const { placeholder, defaultValue, ...rest } = props\n const onSubmit = useSoftGetSubmit()\n return (\n <form\n ref={ref}\n action=\"/search\"\n method=\"get\"\n role=\"search\"\n onSubmit={onSubmit}\n {...rest}\n >\n <TextField\n // Keyed by the starting text. A submit is a client navigation, so\n // this uncontrolled field can outlive the query it was mounted\n // with; a new key is what makes it re-read `defaultValue`.\n key={defaultValue || ''}\n name=\"q\"\n size=\"small\"\n placeholder={placeholder || 'Search…'}\n defaultValue={defaultValue || ''}\n slotProps={{\n input: {\n endAdornment: (\n <InputAdornment position=\"end\">\n <IconButton type=\"submit\" size=\"small\" aria-label=\"search\">\n <MdiIcon path={mdiMagnify.path} fontSize=\"small\" />\n </IconButton>\n </InputAdornment>\n ),\n },\n }}\n />\n </form>\n )\n },\n)\nSearchBox.displayName = 'AglynSearchBox'\n\nexport const schema: Aglyn.ComponentSchema<SearchBoxProps> = {\n $id: ID,\n pluginId: BUNDLE_ID,\n displayName: 'Search Box',\n description:\n \"A search field that opens the site's search page. It works without JavaScript.\",\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiMagnify.path, sx: { color: '#455a64' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'placeholder',\n description: 'Placeholder text inside the search field.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Placeholder',\n },\n {\n name: 'defaultValue',\n description:\n 'Text the field starts with. The search results page fills in the ' +\n 'query it is showing.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Starting text',\n },\n ],\n}\n\nexport const presets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Search Box',\n pluginId: BUNDLE_ID,\n description: 'Search field that opens the site search page',\n category: Aglyn.ComponentCategory.FORMS,\n icon: { path: mdiMagnify.path, sx: { color: '#455a64' } },\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n]\n\nexport default SearchBox\n"],"names":["Aglyn","mdiMagnify","IconButton","InputAdornment","TextField","MdiIcon","useSoftGetSubmit","forwardRef","BUNDLE_ID","generatePresetId","ID","SearchBox","props","ref","placeholder","defaultValue","rest","onSubmit","form","action","method","role","name","size","slotProps","input","endAdornment","position","type","aria-label","path","fontSize","displayName","schema","$id","pluginId","description","category","ComponentCategory","FORMS","icon","sx","color","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","component","FieldComponentType","TEXT_FIELD","label","presets","NodeType","PRESET","data","componentId"],"mappings":";;;AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,UAAU,QAAQ,yBAAwB;AACnD,OAAOC,gBAAgB,2BAA0B;AACjD,OAAOC,oBAAoB,+BAA8B;AACzD,OAAOC,eAAe,0BAAyB;AAC/C,SAASC,OAAO,QAAQ,uBAAsB;AAC9C,SAASC,gBAAgB,QAAQ,iDAAgD;AACjF,SAASC,UAAU,QAAQ,QAAO;AAClC,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,gBAAgB,QAAQ,iCAA6B;AAE9D,iEAAiE;AACjE,OAAO,MAAMC,KAAwB,YAAW;AAgBhD;;;;;;;CAOC,GACD,MAAMC,0BAAYJ,WAChB,CAACK,OAAOC;IACN,MAAM,EAAEC,WAAW,EAAEC,YAAY,EAAW,GAAGH,OAATI,wCAASJ;;;;IAC/C,MAAMK,WAAWX;IACjB,qBACE,KAACY;QACCL,KAAKA;QACLM,QAAO;QACPC,QAAO;QACPC,MAAK;QACLJ,UAAUA;OACND;kBAEJ,cAAA,KAACZ;YAKCkB,MAAK;YACLC,MAAK;YACLT,aAAaA,eAAe;YAC5BC,cAAcA,gBAAgB;YAC9BS,WAAW;gBACTC,OAAO;oBACLC,4BACE,KAACvB;wBAAewB,UAAS;kCACvB,cAAA,KAACzB;4BAAW0B,MAAK;4BAASL,MAAK;4BAAQM,cAAW;sCAChD,cAAA,KAACxB;gCAAQyB,MAAM7B,WAAW6B,IAAI;gCAAEC,UAAS;;;;gBAIjD;YACF;WAfKhB,gBAAgB;;AAmB7B;AAEFJ,UAAUqB,WAAW,GAAG;AAExB,OAAO,MAAMC,SAAgD;IAC3DC,KAAKxB;IACLyB,UAAU3B;IACVwB,aAAa;IACbI,aACE;IACFC,UAAUrC,MAAMsC,iBAAiB,CAACC,KAAK;IACvCC,MAAM;QAAEV,MAAM7B,WAAW6B,IAAI;QAAEW,IAAI;YAAEC,OAAO;QAAU;IAAE;IACxDC,OAAO;QAAEC,aAAa5C,MAAM6C,YAAY,CAACC,OAAO;IAAC;IACjDC,YAAY;QACV;YACEzB,MAAM;YACNc,aAAa;YACbY,WAAWhD,MAAMiD,kBAAkB,CAACC,UAAU;YAC9CC,OAAO;QACT;QACA;YACE7B,MAAM;YACNc,aACE,sEACA;YACFY,WAAWhD,MAAMiD,kBAAkB,CAACC,UAAU;YAC9CC,OAAO;QACT;KACD;AACH,EAAC;AAED,OAAO,MAAMC,UAAgC;IAC3C;QACElB,KAAKzB,iBAAiBC;QACtBkB,MAAM5B,MAAMqD,QAAQ,CAACC,MAAM;QAC3BtB,aAAa;QACbG,UAAU3B;QACV4B,aAAa;QACbC,UAAUrC,MAAMsC,iBAAiB,CAACC,KAAK;QACvCC,MAAM;YAAEV,MAAM7B,WAAW6B,IAAI;YAAEW,IAAI;gBAAEC,OAAO;YAAU;QAAE;QACxDa,MAAM;YACJrB,KAAK;YACLsB,aAAa9C;YACbyB,UAAU3B;YACVI,OAAO,CAAC;QACV;IACF;CACD,CAAA;AAED,eAAeD,UAAS"}