@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.
- package/package.json +7 -7
- package/src/lib/components/accordion.js +21 -1
- package/src/lib/components/accordion.js.map +1 -1
- package/src/lib/components/collection-entries-search.d.ts +29 -0
- package/src/lib/components/collection-entries-search.js +228 -0
- package/src/lib/components/collection-entries-search.js.map +1 -0
- package/src/lib/components/collection-entries.d.ts +45 -10
- package/src/lib/components/collection-entries.js +67 -183
- package/src/lib/components/collection-entries.js.map +1 -1
- package/src/lib/components/collection-entry-body.d.ts +1 -0
- package/src/lib/components/collection-entry-body.js.map +1 -1
- package/src/lib/components/collection-search-box.js +9 -0
- package/src/lib/components/collection-search-box.js.map +1 -1
- package/src/lib/components/image.d.ts +24 -5
- package/src/lib/components/image.js +145 -25
- package/src/lib/components/image.js.map +1 -1
- package/src/lib/components/markdown.d.ts +1 -0
- package/src/lib/components/markdown.js.map +1 -1
- package/src/lib/components/search-box.d.ts +3 -1
- package/src/lib/components/search-box.js +8 -3
- package/src/lib/components/search-box.js.map +1 -1
- package/src/lib/plugin.d.ts +11 -0
- package/src/lib/plugin.js +16 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/utils/image-sizes.d.ts +146 -0
- package/src/lib/utils/image-sizes.js +423 -0
- package/src/lib/utils/image-sizes.js.map +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/mui/src/lib/components/collection-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 { AppLink, MdiIcon } from '@aglyn/shared-ui-jsx'\n// The icon picker's fuzzy matcher (use-mdi-icons-fuzzy), not a re-implementation\n// (AGL-1516): search here has to feel like search does everywhere else in the\n// product, and two matchers is how they drift. A TYPE here: the matcher itself\n// is fetched on a reader's first keystroke — see `loadCollectionFuse`.\nimport type { Fuse } from '@aglyn/shared-util-vendor/fuse'\nimport Box from '@mui/material/Box'\nimport Chip from '@mui/material/Chip'\nimport InputBase from '@mui/material/InputBase'\nimport MuiStack from '@mui/material/Stack'\nimport Typography from '@mui/material/Typography'\nimport { useMemo, useState } from 'react'\n\n/**\n * The search box the Collection Entries block and the standalone Collection\n * Search block both draw (AGL-1516/AGL-1525), and the fuzzy matcher behind\n * it. Its own module so the other collection elements carry neither the MUI\n * input nor the panel (AGL-3401); the matcher itself is fetched only when a\n * reader types.\n */\n\n/** The toolbar search field, from the frame (Figma 494:1220). */\nconst SEARCH_FIELD_WIDTH = 240\n\n/** Suggestion panel (Figma 496:1218). */\nconst SUGGESTION_PANEL_WIDTH = 420\n\n/**\n * Rows in the panel before it stops. The frame shows five; the sixth row is\n * always the \"View all results\" link, which is the honest overflow answer —\n * a scrolling suggestion panel is a worse results page.\n */\nconst SUGGESTION_LIMIT = 5\n\n/** Where \"View all results\" goes — the reserved site-search route (AGL-88). */\nconst searchResultsHref = (query: string) =>\n `/search?q=${encodeURIComponent(query)}`\n\n/**\n * The frame's compact filled field: magnify glyph + hint on the quiet\n * surface token, right-aligned so it sits where the toolbar row puts it.\n * Palette tokens only, so it follows the site theme in both modes.\n */\nconst searchFieldSx = {\n alignSelf: 'flex-end',\n display: 'flex',\n alignItems: 'center',\n gap: 1,\n px: 1.5,\n py: 0.75,\n borderRadius: 2,\n bgcolor: 'action.hover',\n color: 'text.secondary',\n width: SEARCH_FIELD_WIDTH,\n maxWidth: '100%',\n}\n\n/**\n * The positioned parent of the suggestion panel (AGL-1525). It takes over\n * `alignSelf` from the field so the toolbar row is unchanged — the panel\n * hangs off the field, it does not move it.\n */\nconst suggestionAnchorSx = {\n alignSelf: 'flex-end',\n position: 'relative',\n width: SEARCH_FIELD_WIDTH,\n maxWidth: '100%',\n}\n\n/**\n * The floating panel itself. Right-aligned to the field it drops from, and\n * wider than it, as the frame draws it — a suggestion row carries a title, a\n * chip, a date and a line of excerpt, none of which fit in 240px.\n *\n * Palette and shadow tokens only, so it follows the site theme in both\n * modes rather than pinning a light-mode card onto a dark page.\n */\nconst suggestionPanelSx = {\n position: 'absolute',\n top: 'calc(100% + 8px)',\n right: 0,\n zIndex: 10,\n width: SUGGESTION_PANEL_WIDTH,\n // Never wider than the viewport on a phone, where the anchor is narrow\n // and `right: 0` would otherwise hang the panel off the left edge.\n maxWidth: 'calc(100vw - 32px)',\n bgcolor: 'background.paper',\n color: 'text.primary',\n border: 1,\n borderColor: 'divider',\n borderRadius: 2,\n boxShadow: 6,\n overflow: 'hidden',\n textAlign: 'left',\n}\n\nconst suggestionRowLinkSx = {\n display: 'block',\n color: 'inherit',\n '&:hover': { bgcolor: 'action.hover' },\n}\n\nconst suggestionFooterSx = {\n display: 'block',\n px: 2,\n py: 1.25,\n borderTop: 1,\n borderColor: 'divider',\n bgcolor: 'action.hover',\n fontSize: 14,\n fontWeight: 500,\n}\n\n/** What a built index can be searched by — Fuse over the stamped rows. */\ntype EntryFuse = InstanceType<typeof Fuse<Aglyn.CollectionEntrySearchItem>>\n\n/** The matcher's constructor, once something has asked for it. */\nlet loadedFuse: typeof Fuse | undefined\nlet fuseLoad: Promise<typeof Fuse> | undefined\n\n/**\n * The fuzzy matcher, fetched the first time a reader types into a collection\n * search box (AGL-3401) — one fetch per visit, however many boxes ask.\n *\n * It was a static import, and `fuse.js` is 26 KB of the 68 KB this module\n * weighed: every page that placed ANY collection element downloaded and\n * parsed it, including the many whose entries block has no search at all and\n * the rest, where most visitors never type. The box itself still renders on\n * the server; only the matcher waits for a query.\n *\n * A failed fetch (offline, a deploy mid-visit) is forgotten rather than\n * cached, so the next keystroke tries again instead of the box going dead.\n */\nexport const loadCollectionFuse = (): Promise<typeof Fuse> =>\n (fuseLoad ??= import('./collection-search-fuse').then(\n (module) => (loadedFuse = module.Fuse),\n (error: unknown) => {\n fuseLoad = undefined\n throw error\n },\n ))\n\n/**\n * The Fuse index over `items`, and the call that asks for the matcher.\n *\n * `fuzzy` is null until the first `requestFuzzy()` has resolved — a box\n * nobody typed into never builds an index. A box mounted after another one\n * loaded the matcher starts with it, so a second search on the page answers\n * its first keystroke synchronously.\n */\nexport const useEntryFuse = (\n items: readonly Aglyn.CollectionEntrySearchItem[] | undefined,\n): { fuzzy: EntryFuse | null; requestFuzzy: () => void } => {\n const [FuseConstructor, setFuseConstructor] = useState<\n typeof Fuse | undefined\n >(() => loadedFuse)\n const fuzzy = useMemo(\n () =>\n FuseConstructor && items?.length\n ? // The ONE matcher config (AGL-1525), shared with the site-wide\n // results page the suggestion panel links to — a query the panel\n // forgave must not come back empty from \"View all results\".\n new FuseConstructor(items, { ...Aglyn.COLLECTION_SEARCH_FUSE_OPTIONS })\n : null,\n [FuseConstructor, items],\n )\n const requestFuzzy = () => {\n if (FuseConstructor) return\n loadCollectionFuse().then(\n (loaded) => setFuseConstructor(() => loaded),\n // The box keeps working as a form that submits to `/search`; the next\n // keystroke asks again.\n () => undefined,\n )\n }\n return { fuzzy, requestFuzzy }\n}\n\n/**\n * The frame's search input (Figma 494:1220) — the magnify glyph and a bare\n * `InputBase` on the quiet surface token.\n *\n * Presentational and stateless, so the entries block's in-place filter and\n * the standalone toolbar box (AGL-1516) are the SAME field rather than two\n * that merely look alike. `inert` is the editing-surface rendering: read-only\n * and handler-free, so a click or stray keystroke in the besigner never edits\n * state the canvas cannot use — how Category Pills go inert there too.\n */\nexport const SearchField = ({\n value,\n placeholder,\n inert,\n landmark,\n onQuery,\n onEscape,\n}: {\n value: string\n placeholder?: string\n inert?: boolean\n /**\n * Put `role=\"search\"` on the FIELD. False when a wrapping form carries it\n * instead — one search landmark per box, or a screen reader announces the\n * same field twice.\n */\n landmark?: boolean\n onQuery?: (next: string) => void\n onEscape?: () => void\n}) => (\n <Box {...(landmark ? { role: 'search' } : {})} sx={searchFieldSx}>\n <MdiIcon path={mdiMagnify.path} />\n <InputBase\n value={value}\n name=\"q\"\n placeholder={(placeholder ?? '').trim() || 'Search posts…'}\n inputProps={{ 'aria-label': 'Search entries' }}\n sx={{ flex: 1, fontSize: 14, color: 'text.primary' }}\n {...(inert\n ? { readOnly: true }\n : {\n onChange: (event) => onQuery?.(event.target.value),\n onKeyDown: (event) => {\n if (event.key === 'Escape') onEscape?.()\n },\n })}\n />\n </Box>\n)\n\n/**\n * The floating suggestion panel (AGL-1525, Figma 496:1218) — rows straight\n * off the server-stamped index, and a \"View all results\" link that is there\n * hits or no hits.\n *\n * `emptyText` is the caller's, not this component's, and that is the whole\n * point of it being a parameter: a page-windowed entries block and a\n * whole-collection toolbar box searched different sets, and a miss has to say\n * WHICH set came back empty. A shared \"No matches.\" would let one of them\n * claim a reach it never had.\n */\nconst SuggestionPanel = ({\n suggestions,\n trimmed,\n emptyText,\n}: {\n suggestions: readonly Aglyn.CollectionEntrySearchItem[]\n trimmed: string\n emptyText: string\n}) => (\n <Box sx={suggestionPanelSx}>\n {suggestions.length ? (\n suggestions.map((item, index) => {\n const row = (\n <MuiStack spacing={0.5} sx={{ px: 2, py: 1.25 }}>\n <MuiStack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"subtitle2\" sx={{ color: 'text.primary' }}>\n {item.title}\n </Typography>\n {item.category ? (\n <Chip label={item.category} size=\"small\" />\n ) : null}\n {item.date ? (\n <Typography variant=\"caption\" sx={{ color: 'text.secondary' }}>\n {item.date}\n </Typography>\n ) : null}\n </MuiStack>\n {item.excerpt ? (\n <Typography\n variant=\"body2\"\n sx={{\n color: 'text.secondary',\n // One line of excerpt, as the frame draws it. A row that\n // grows with the prose turns the panel into a page.\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n }}\n >\n {item.excerpt}\n </Typography>\n ) : null}\n </MuiStack>\n )\n // A row without a `url` is a page cached before AGL-1525 stamped one,\n // or a slugless entry. It renders as TEXT rather than as a link to\n // nowhere: a suggestion that navigates to the wrong page is worse\n // than one the reader has to read.\n return item.url ? (\n <AppLink\n key={index}\n href={item.url}\n sx={suggestionRowLinkSx}\n underline=\"none\"\n >\n {row}\n </AppLink>\n ) : (\n <Box key={index}>{row}</Box>\n )\n })\n ) : (\n <Typography\n variant=\"body2\"\n sx={{ color: 'text.secondary', px: 2, py: 1.25 }}\n >\n {emptyText}\n </Typography>\n )}\n {/*\n Always present, hits or no hits (Figma 496:1218). A box that searches\n one page, or one bounded read of a collection, cannot answer \"then\n where is it?\" on its own — the site-wide results page is the only\n honest answer, and it matters most in exactly the case where the panel\n came back empty.\n */}\n <AppLink\n href={searchResultsHref(trimmed)}\n sx={suggestionFooterSx}\n underline=\"none\"\n >\n {`View all results for “${trimmed}” →`}\n </AppLink>\n </Box>\n)\n\n/**\n * Field plus dropdown, in a REAL form (AGL-1525) — so Enter reaches the\n * results page and the box still works with the panel's JS doing nothing at\n * all. The panel is an enhancement over a working search box, not the search\n * box.\n *\n * Owns the query, because nothing outside it needs one: `suggest` never\n * touches the cards underneath. That is what lets the standalone toolbar\n * block exist at all (AGL-1516) — a box with no clones to hide is a complete\n * feature, not a crippled one.\n */\nexport const SuggestSearchBox = ({\n fuzzy,\n requestFuzzy,\n items,\n placeholder,\n inert,\n emptyText,\n}: {\n /** Null until the matcher has loaded; see {@link useEntryFuse}. */\n fuzzy: EntryFuse | null\n /** Asks for the matcher; called on every query, a no-op once it is here. */\n requestFuzzy: () => void\n items?: readonly Aglyn.CollectionEntrySearchItem[]\n placeholder?: string\n inert?: boolean\n /** The honest miss for the set THIS box searched. */\n emptyText: (query: string) => string\n}) => {\n const [query, setQuery] = useState('')\n /** Escape dismissed THIS answer (AGL-1525); typing brings it back. */\n const [closed, setClosed] = useState(false)\n const trimmed = query.trim()\n const suggestions =\n !inert && trimmed && fuzzy && items\n ? fuzzy\n .search(trimmed)\n .slice(0, SUGGESTION_LIMIT)\n .map((result) => items[result.refIndex])\n .filter(Boolean)\n : []\n const field = (\n <SearchField\n value={query}\n {...(placeholder === undefined ? {} : { placeholder })}\n // No form wraps the inert field, so the landmark belongs on it.\n {...(inert ? { inert: true, landmark: true } : {})}\n onQuery={(next) => {\n requestFuzzy()\n setQuery(next)\n // A new query reopens a panel the reader dismissed — Escape closes\n // THIS answer, it does not turn the feature off for the visit.\n setClosed(false)\n }}\n onEscape={() => setClosed(true)}\n />\n )\n if (inert) return field\n return (\n <Box\n component=\"form\"\n role=\"search\"\n action=\"/search\"\n method=\"get\"\n sx={suggestionAnchorSx}\n >\n {field}\n {/*\n Not until the matcher is here: a panel drawn in the moment it takes\n to arrive would say \"No matches\" about a query nothing has searched.\n */}\n {trimmed && !closed && fuzzy ? (\n <SuggestionPanel\n suggestions={suggestions}\n trimmed={trimmed}\n emptyText={emptyText(trimmed)}\n />\n ) : null}\n </Box>\n )\n}\n"],"names":["Aglyn","mdiMagnify","AppLink","MdiIcon","Box","Chip","InputBase","MuiStack","Typography","useMemo","useState","SEARCH_FIELD_WIDTH","SUGGESTION_PANEL_WIDTH","SUGGESTION_LIMIT","searchResultsHref","query","encodeURIComponent","searchFieldSx","alignSelf","display","alignItems","gap","px","py","borderRadius","bgcolor","color","width","maxWidth","suggestionAnchorSx","position","suggestionPanelSx","top","right","zIndex","border","borderColor","boxShadow","overflow","textAlign","suggestionRowLinkSx","suggestionFooterSx","borderTop","fontSize","fontWeight","loadedFuse","fuseLoad","loadCollectionFuse","then","module","Fuse","error","undefined","useEntryFuse","items","FuseConstructor","setFuseConstructor","fuzzy","length","COLLECTION_SEARCH_FUSE_OPTIONS","requestFuzzy","loaded","SearchField","value","placeholder","inert","landmark","onQuery","onEscape","role","sx","path","name","trim","inputProps","flex","readOnly","onChange","event","target","onKeyDown","key","SuggestionPanel","suggestions","trimmed","emptyText","map","item","index","row","spacing","direction","variant","title","category","label","size","date","excerpt","textOverflow","whiteSpace","url","href","underline","SuggestSearchBox","setQuery","closed","setClosed","search","slice","result","refIndex","filter","Boolean","field","next","component","action","method"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/mui/src/lib/components/collection-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 { AppLink, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { useSoftGetSubmit } from '@aglyn/shared-ui-jsx/hooks/use-soft-get-submit'\n// The icon picker's fuzzy matcher (use-mdi-icons-fuzzy), not a re-implementation\n// (AGL-1516): search here has to feel like search does everywhere else in the\n// product, and two matchers is how they drift. A TYPE here: the matcher itself\n// is fetched on a reader's first keystroke — see `loadCollectionFuse`.\nimport type { Fuse } from '@aglyn/shared-util-vendor/fuse'\nimport Box from '@mui/material/Box'\nimport Chip from '@mui/material/Chip'\nimport InputBase from '@mui/material/InputBase'\nimport MuiStack from '@mui/material/Stack'\nimport Typography from '@mui/material/Typography'\nimport { type FormEvent, useMemo, useState } from 'react'\n\n/**\n * The search box the Collection Entries block and the standalone Collection\n * Search block both draw (AGL-1516/AGL-1525), and the fuzzy matcher behind\n * it. Its own module so the other collection elements carry neither the MUI\n * input nor the panel (AGL-3401); the matcher itself is fetched only when a\n * reader types.\n */\n\n/** The toolbar search field, from the frame (Figma 494:1220). */\nconst SEARCH_FIELD_WIDTH = 240\n\n/** Suggestion panel (Figma 496:1218). */\nconst SUGGESTION_PANEL_WIDTH = 420\n\n/**\n * Rows in the panel before it stops. The frame shows five; the sixth row is\n * always the \"View all results\" link, which is the honest overflow answer —\n * a scrolling suggestion panel is a worse results page.\n */\nconst SUGGESTION_LIMIT = 5\n\n/** Where \"View all results\" goes — the reserved site-search route (AGL-88). */\nconst searchResultsHref = (query: string) =>\n `/search?q=${encodeURIComponent(query)}`\n\n/**\n * The frame's compact filled field: magnify glyph + hint on the quiet\n * surface token, right-aligned so it sits where the toolbar row puts it.\n * Palette tokens only, so it follows the site theme in both modes.\n */\nconst searchFieldSx = {\n alignSelf: 'flex-end',\n display: 'flex',\n alignItems: 'center',\n gap: 1,\n px: 1.5,\n py: 0.75,\n borderRadius: 2,\n bgcolor: 'action.hover',\n color: 'text.secondary',\n width: SEARCH_FIELD_WIDTH,\n maxWidth: '100%',\n}\n\n/**\n * The positioned parent of the suggestion panel (AGL-1525). It takes over\n * `alignSelf` from the field so the toolbar row is unchanged — the panel\n * hangs off the field, it does not move it.\n */\nconst suggestionAnchorSx = {\n alignSelf: 'flex-end',\n position: 'relative',\n width: SEARCH_FIELD_WIDTH,\n maxWidth: '100%',\n}\n\n/**\n * The floating panel itself. Right-aligned to the field it drops from, and\n * wider than it, as the frame draws it — a suggestion row carries a title, a\n * chip, a date and a line of excerpt, none of which fit in 240px.\n *\n * Palette and shadow tokens only, so it follows the site theme in both\n * modes rather than pinning a light-mode card onto a dark page.\n */\nconst suggestionPanelSx = {\n position: 'absolute',\n top: 'calc(100% + 8px)',\n right: 0,\n zIndex: 10,\n width: SUGGESTION_PANEL_WIDTH,\n // Never wider than the viewport on a phone, where the anchor is narrow\n // and `right: 0` would otherwise hang the panel off the left edge.\n maxWidth: 'calc(100vw - 32px)',\n bgcolor: 'background.paper',\n color: 'text.primary',\n border: 1,\n borderColor: 'divider',\n borderRadius: 2,\n boxShadow: 6,\n overflow: 'hidden',\n textAlign: 'left',\n}\n\nconst suggestionRowLinkSx = {\n display: 'block',\n color: 'inherit',\n '&:hover': { bgcolor: 'action.hover' },\n}\n\nconst suggestionFooterSx = {\n display: 'block',\n px: 2,\n py: 1.25,\n borderTop: 1,\n borderColor: 'divider',\n bgcolor: 'action.hover',\n fontSize: 14,\n fontWeight: 500,\n}\n\n/** What a built index can be searched by — Fuse over the stamped rows. */\ntype EntryFuse = InstanceType<typeof Fuse<Aglyn.CollectionEntrySearchItem>>\n\n/** The matcher's constructor, once something has asked for it. */\nlet loadedFuse: typeof Fuse | undefined\nlet fuseLoad: Promise<typeof Fuse> | undefined\n\n/**\n * The fuzzy matcher, fetched the first time a reader types into a collection\n * search box (AGL-3401) — one fetch per visit, however many boxes ask.\n *\n * It was a static import, and `fuse.js` is 26 KB of the 68 KB this module\n * weighed: every page that placed ANY collection element downloaded and\n * parsed it, including the many whose entries block has no search at all and\n * the rest, where most visitors never type. The box itself still renders on\n * the server; only the matcher waits for a query.\n *\n * A failed fetch (offline, a deploy mid-visit) is forgotten rather than\n * cached, so the next keystroke tries again instead of the box going dead.\n */\nexport const loadCollectionFuse = (): Promise<typeof Fuse> =>\n (fuseLoad ??= import('./collection-search-fuse').then(\n (module) => (loadedFuse = module.Fuse),\n (error: unknown) => {\n fuseLoad = undefined\n throw error\n },\n ))\n\n/**\n * The Fuse index over `items`, and the call that asks for the matcher.\n *\n * `fuzzy` is null until the first `requestFuzzy()` has resolved — a box\n * nobody typed into never builds an index. A box mounted after another one\n * loaded the matcher starts with it, so a second search on the page answers\n * its first keystroke synchronously.\n */\nexport const useEntryFuse = (\n items: readonly Aglyn.CollectionEntrySearchItem[] | undefined,\n): { fuzzy: EntryFuse | null; requestFuzzy: () => void } => {\n const [FuseConstructor, setFuseConstructor] = useState<\n typeof Fuse | undefined\n >(() => loadedFuse)\n const fuzzy = useMemo(\n () =>\n FuseConstructor && items?.length\n ? // The ONE matcher config (AGL-1525), shared with the site-wide\n // results page the suggestion panel links to — a query the panel\n // forgave must not come back empty from \"View all results\".\n new FuseConstructor(items, { ...Aglyn.COLLECTION_SEARCH_FUSE_OPTIONS })\n : null,\n [FuseConstructor, items],\n )\n const requestFuzzy = () => {\n if (FuseConstructor) return\n loadCollectionFuse().then(\n (loaded) => setFuseConstructor(() => loaded),\n // The box keeps working as a form that submits to `/search`; the next\n // keystroke asks again.\n () => undefined,\n )\n }\n return { fuzzy, requestFuzzy }\n}\n\n/**\n * The frame's search input (Figma 494:1220) — the magnify glyph and a bare\n * `InputBase` on the quiet surface token.\n *\n * Presentational and stateless, so the entries block's in-place filter and\n * the standalone toolbar box (AGL-1516) are the SAME field rather than two\n * that merely look alike. `inert` is the editing-surface rendering: read-only\n * and handler-free, so a click or stray keystroke in the besigner never edits\n * state the canvas cannot use — how Category Pills go inert there too.\n */\nexport const SearchField = ({\n value,\n placeholder,\n inert,\n landmark,\n onQuery,\n onEscape,\n}: {\n value: string\n placeholder?: string\n inert?: boolean\n /**\n * Put `role=\"search\"` on the FIELD. False when a wrapping form carries it\n * instead — one search landmark per box, or a screen reader announces the\n * same field twice.\n */\n landmark?: boolean\n onQuery?: (next: string) => void\n onEscape?: () => void\n}) => (\n <Box {...(landmark ? { role: 'search' } : {})} sx={searchFieldSx}>\n <MdiIcon path={mdiMagnify.path} />\n <InputBase\n value={value}\n name=\"q\"\n placeholder={(placeholder ?? '').trim() || 'Search posts…'}\n inputProps={{ 'aria-label': 'Search entries' }}\n sx={{ flex: 1, fontSize: 14, color: 'text.primary' }}\n {...(inert\n ? { readOnly: true }\n : {\n onChange: (event) => onQuery?.(event.target.value),\n onKeyDown: (event) => {\n if (event.key === 'Escape') onEscape?.()\n },\n })}\n />\n </Box>\n)\n\n/**\n * The floating suggestion panel (AGL-1525, Figma 496:1218) — rows straight\n * off the server-stamped index, and a \"View all results\" link that is there\n * hits or no hits.\n *\n * `emptyText` is the caller's, not this component's, and that is the whole\n * point of it being a parameter: a page-windowed entries block and a\n * whole-collection toolbar box searched different sets, and a miss has to say\n * WHICH set came back empty. A shared \"No matches.\" would let one of them\n * claim a reach it never had.\n */\nconst SuggestionPanel = ({\n suggestions,\n trimmed,\n emptyText,\n}: {\n suggestions: readonly Aglyn.CollectionEntrySearchItem[]\n trimmed: string\n emptyText: string\n}) => (\n <Box sx={suggestionPanelSx}>\n {suggestions.length ? (\n suggestions.map((item, index) => {\n const row = (\n <MuiStack spacing={0.5} sx={{ px: 2, py: 1.25 }}>\n <MuiStack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"subtitle2\" sx={{ color: 'text.primary' }}>\n {item.title}\n </Typography>\n {item.category ? (\n <Chip label={item.category} size=\"small\" />\n ) : null}\n {item.date ? (\n <Typography variant=\"caption\" sx={{ color: 'text.secondary' }}>\n {item.date}\n </Typography>\n ) : null}\n </MuiStack>\n {item.excerpt ? (\n <Typography\n variant=\"body2\"\n sx={{\n color: 'text.secondary',\n // One line of excerpt, as the frame draws it. A row that\n // grows with the prose turns the panel into a page.\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n }}\n >\n {item.excerpt}\n </Typography>\n ) : null}\n </MuiStack>\n )\n // A row without a `url` is a page cached before AGL-1525 stamped one,\n // or a slugless entry. It renders as TEXT rather than as a link to\n // nowhere: a suggestion that navigates to the wrong page is worse\n // than one the reader has to read.\n return item.url ? (\n <AppLink\n key={index}\n href={item.url}\n sx={suggestionRowLinkSx}\n underline=\"none\"\n >\n {row}\n </AppLink>\n ) : (\n <Box key={index}>{row}</Box>\n )\n })\n ) : (\n <Typography\n variant=\"body2\"\n sx={{ color: 'text.secondary', px: 2, py: 1.25 }}\n >\n {emptyText}\n </Typography>\n )}\n {/*\n Always present, hits or no hits (Figma 496:1218). A box that searches\n one page, or one bounded read of a collection, cannot answer \"then\n where is it?\" on its own — the site-wide results page is the only\n honest answer, and it matters most in exactly the case where the panel\n came back empty.\n */}\n <AppLink\n href={searchResultsHref(trimmed)}\n sx={suggestionFooterSx}\n underline=\"none\"\n >\n {`View all results for “${trimmed}” →`}\n </AppLink>\n </Box>\n)\n\n/**\n * Field plus dropdown, in a REAL form (AGL-1525) — so Enter reaches the\n * results page and the box still works with the panel's JS doing nothing at\n * all. The panel is an enhancement over a working search box, not the search\n * box.\n *\n * Owns the query, because nothing outside it needs one: `suggest` never\n * touches the cards underneath. That is what lets the standalone toolbar\n * block exist at all (AGL-1516) — a box with no clones to hide is a complete\n * feature, not a crippled one.\n */\nexport const SuggestSearchBox = ({\n fuzzy,\n requestFuzzy,\n items,\n placeholder,\n inert,\n emptyText,\n}: {\n /** Null until the matcher has loaded; see {@link useEntryFuse}. */\n fuzzy: EntryFuse | null\n /** Asks for the matcher; called on every query, a no-op once it is here. */\n requestFuzzy: () => void\n items?: readonly Aglyn.CollectionEntrySearchItem[]\n placeholder?: string\n inert?: boolean\n /** The honest miss for the set THIS box searched. */\n emptyText: (query: string) => string\n}) => {\n const [query, setQuery] = useState('')\n /** Escape dismissed THIS answer (AGL-1525); typing brings it back. */\n const [closed, setClosed] = useState(false)\n const softSubmit = useSoftGetSubmit()\n const trimmed = query.trim()\n const suggestions =\n !inert && trimmed && fuzzy && items\n ? fuzzy\n .search(trimmed)\n .slice(0, SUGGESTION_LIMIT)\n .map((result) => items[result.refIndex])\n .filter(Boolean)\n : []\n const field = (\n <SearchField\n value={query}\n {...(placeholder === undefined ? {} : { placeholder })}\n // No form wraps the inert field, so the landmark belongs on it.\n {...(inert ? { inert: true, landmark: true } : {})}\n onQuery={(next) => {\n requestFuzzy()\n setQuery(next)\n // A new query reopens a panel the reader dismissed — Escape closes\n // THIS answer, it does not turn the feature off for the visit.\n setClosed(false)\n }}\n onEscape={() => setClosed(true)}\n />\n )\n if (inert) return field\n return (\n <Box\n component=\"form\"\n role=\"search\"\n action=\"/search\"\n method=\"get\"\n onSubmit={(event: FormEvent<HTMLFormElement>) => {\n // The client router keeps this box mounted when it sits in the\n // site's chrome, so the panel is shut here rather than left open\n // over the results page it just opened.\n setClosed(true)\n softSubmit(event)\n }}\n sx={suggestionAnchorSx}\n >\n {field}\n {/*\n Not until the matcher is here: a panel drawn in the moment it takes\n to arrive would say \"No matches\" about a query nothing has searched.\n */}\n {trimmed && !closed && fuzzy ? (\n <SuggestionPanel\n suggestions={suggestions}\n trimmed={trimmed}\n emptyText={emptyText(trimmed)}\n />\n ) : null}\n </Box>\n )\n}\n"],"names":["Aglyn","mdiMagnify","AppLink","MdiIcon","useSoftGetSubmit","Box","Chip","InputBase","MuiStack","Typography","useMemo","useState","SEARCH_FIELD_WIDTH","SUGGESTION_PANEL_WIDTH","SUGGESTION_LIMIT","searchResultsHref","query","encodeURIComponent","searchFieldSx","alignSelf","display","alignItems","gap","px","py","borderRadius","bgcolor","color","width","maxWidth","suggestionAnchorSx","position","suggestionPanelSx","top","right","zIndex","border","borderColor","boxShadow","overflow","textAlign","suggestionRowLinkSx","suggestionFooterSx","borderTop","fontSize","fontWeight","loadedFuse","fuseLoad","loadCollectionFuse","then","module","Fuse","error","undefined","useEntryFuse","items","FuseConstructor","setFuseConstructor","fuzzy","length","COLLECTION_SEARCH_FUSE_OPTIONS","requestFuzzy","loaded","SearchField","value","placeholder","inert","landmark","onQuery","onEscape","role","sx","path","name","trim","inputProps","flex","readOnly","onChange","event","target","onKeyDown","key","SuggestionPanel","suggestions","trimmed","emptyText","map","item","index","row","spacing","direction","variant","title","category","label","size","date","excerpt","textOverflow","whiteSpace","url","href","underline","SuggestSearchBox","setQuery","closed","setClosed","softSubmit","search","slice","result","refIndex","filter","Boolean","field","next","component","action","method","onSubmit"],"mappings":"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"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import * as Aglyn from '@aglyn/aglyn';
|
|
18
|
-
import type
|
|
18
|
+
import { type SxProps } from '@mui/material/styles';
|
|
19
19
|
import { type ReactNode } from 'react';
|
|
20
20
|
export declare const ID: Aglyn.ComponentId;
|
|
21
21
|
/**
|
|
@@ -49,10 +49,10 @@ export declare const ID: Aglyn.ComponentId;
|
|
|
49
49
|
* screen image. In the besigner canvas nothing is layout-composed, so the
|
|
50
50
|
* screen's first image is the only lead, exactly as before.
|
|
51
51
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
52
|
+
* Only the SCREEN's lead can get `fetchpriority="high"` and a preload, and
|
|
53
|
+
* only when nothing says it renders small (AGL-3485); the reasoning is at the
|
|
54
|
+
* `fetchPriority` prop below. The layout's lead is the header logo on any
|
|
55
|
+
* site that has one, which is why "first image" alone was never evidence.
|
|
56
56
|
*
|
|
57
57
|
* Resolved from the tree rather than a render-order counter on purpose: the
|
|
58
58
|
* renderer walks the tree in document order on the server AND on hydrate, but
|
|
@@ -95,6 +95,25 @@ export interface ImageProps {
|
|
|
95
95
|
width?: string;
|
|
96
96
|
/** CSS height (e.g. "240px"); defaults to auto. */
|
|
97
97
|
height?: string;
|
|
98
|
+
/**
|
|
99
|
+
* The author's own `sizes` attribute, used exactly as typed (AGL-3485).
|
|
100
|
+
* Empty — the normal case — lets the element work it out: see
|
|
101
|
+
* `utils/image-sizes.ts`.
|
|
102
|
+
*/
|
|
103
|
+
sizes?: string;
|
|
104
|
+
/**
|
|
105
|
+
* How wide the image rendered on the Besigner canvas, per breakpoint band,
|
|
106
|
+
* as a share of the page: written by the editor's save, never by an author
|
|
107
|
+
* (AGL-3485, `rendered-widths.ts`).
|
|
108
|
+
*/
|
|
109
|
+
renderedWidths?: Aglyn.RenderedWidths;
|
|
110
|
+
/**
|
|
111
|
+
* The asset's current content hash, laid on by the composition from its
|
|
112
|
+
* media document like the pixel pair below (AGL-3485). It makes the URL the
|
|
113
|
+
* VERSIONED one, which every cache may keep for a year; a replace changes
|
|
114
|
+
* it, and so the URL.
|
|
115
|
+
*/
|
|
116
|
+
mediaVersion?: string;
|
|
98
117
|
/**
|
|
99
118
|
* The asset's own pixel dimensions, copied off the media document when the
|
|
100
119
|
* image was picked (AGL-2486), and read from the asset again when the page
|
|
@@ -20,9 +20,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
20
20
|
import { mdiImage } from "@aglyn/shared-data-mdi";
|
|
21
21
|
import { AppLink } from "@aglyn/shared-ui-jsx";
|
|
22
22
|
import Box from "@mui/material/Box";
|
|
23
|
-
import {
|
|
23
|
+
import { useTheme } from "@mui/material/styles";
|
|
24
|
+
import useForkRef from "@mui/utils/useForkRef";
|
|
25
|
+
import { forwardRef, useEffect, useState } from "react";
|
|
24
26
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
25
27
|
import { generatePresetId } from "../utils/generate-preset-id.js";
|
|
28
|
+
import { imageSizes, layoutBandWidths } from "../utils/image-sizes.js";
|
|
26
29
|
// Component ids are persisted in screen documents; never rename.
|
|
27
30
|
export const ID = 'image';
|
|
28
31
|
/**
|
|
@@ -56,10 +59,10 @@ export const ID = 'image';
|
|
|
56
59
|
* screen image. In the besigner canvas nothing is layout-composed, so the
|
|
57
60
|
* screen's first image is the only lead, exactly as before.
|
|
58
61
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
62
|
+
* Only the SCREEN's lead can get `fetchpriority="high"` and a preload, and
|
|
63
|
+
* only when nothing says it renders small (AGL-3485); the reasoning is at the
|
|
64
|
+
* `fetchPriority` prop below. The layout's lead is the header logo on any
|
|
65
|
+
* site that has one, which is why "first image" alone was never evidence.
|
|
63
66
|
*
|
|
64
67
|
* Resolved from the tree rather than a render-order counter on purpose: the
|
|
65
68
|
* renderer walks the tree in document order on the server AND on hydrate, but
|
|
@@ -104,12 +107,71 @@ export const ID = 'image';
|
|
|
104
107
|
screenLead
|
|
105
108
|
].filter((id)=>Boolean(id));
|
|
106
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* The breakpoint band the theme says is in effect. Asked of the THEME, not of
|
|
112
|
+
* the window: the Besigner canvas pins its theme's breakpoint helpers to the
|
|
113
|
+
* previewed device (`createDevicePinnedTheme`), so on a pinned device this is
|
|
114
|
+
* the device's band, and in Fluid Responsive it is the window's — which is
|
|
115
|
+
* the band the canvas's own layout is answering to either way.
|
|
116
|
+
*/ function themeBand(theme) {
|
|
117
|
+
if (typeof window === 'undefined' || !window.matchMedia) return 'xs';
|
|
118
|
+
for (const band of [
|
|
119
|
+
...Aglyn.RENDERED_WIDTH_BANDS
|
|
120
|
+
].reverse()){
|
|
121
|
+
if (band === 'xs') break;
|
|
122
|
+
const query = theme.breakpoints.up(band).replace(/^@media\s*/, '');
|
|
123
|
+
if (window.matchMedia(query).matches) return band;
|
|
124
|
+
}
|
|
125
|
+
return 'xs';
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The width of the page an element is laid out in. On the canvas that is the
|
|
129
|
+
* shadow host the page renders into — narrower than the window, and the width
|
|
130
|
+
* a share of "the viewport" has to be taken of.
|
|
131
|
+
*/ function pageWidthOf(element) {
|
|
132
|
+
const root = element.getRootNode();
|
|
133
|
+
const host = typeof ShadowRoot !== 'undefined' && root instanceof ShadowRoot ? root.host : document.documentElement;
|
|
134
|
+
return host.clientWidth;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Records how wide this image renders on the Besigner canvas, per band, for
|
|
138
|
+
* the save to write onto the node (AGL-3485, `rendered-widths.ts`). Inert
|
|
139
|
+
* everywhere but the canvas. A `ResizeObserver` on the image and on the page
|
|
140
|
+
* re-records as the author changes the layout, resizes the window or switches
|
|
141
|
+
* device — a device switch also rebuilds the pinned theme, which re-runs this.
|
|
142
|
+
*/ function useRenderedWidthRecorder(nodeId, enabled) {
|
|
143
|
+
const theme = useTheme();
|
|
144
|
+
const [element, setElement] = useState(null);
|
|
145
|
+
useEffect(()=>{
|
|
146
|
+
if (!enabled || !element || !nodeId) return undefined;
|
|
147
|
+
if (typeof ResizeObserver === 'undefined') return undefined;
|
|
148
|
+
const measure = ()=>{
|
|
149
|
+
const percent = Aglyn.renderedWidthPercent(element.getBoundingClientRect().width, pageWidthOf(element));
|
|
150
|
+
if (percent !== undefined) {
|
|
151
|
+
Aglyn.recordRenderedWidth(nodeId, themeBand(theme), percent);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const observer = new ResizeObserver(measure);
|
|
155
|
+
observer.observe(element);
|
|
156
|
+
const root = element.getRootNode();
|
|
157
|
+
if (typeof ShadowRoot !== 'undefined' && root instanceof ShadowRoot) {
|
|
158
|
+
observer.observe(root.host);
|
|
159
|
+
}
|
|
160
|
+
return ()=>observer.disconnect();
|
|
161
|
+
}, [
|
|
162
|
+
enabled,
|
|
163
|
+
element,
|
|
164
|
+
nodeId,
|
|
165
|
+
theme
|
|
166
|
+
]);
|
|
167
|
+
return setElement;
|
|
168
|
+
}
|
|
107
169
|
/**
|
|
108
170
|
* Image element (AGL-74): renders a plain img with fit/size/radius
|
|
109
171
|
* controls; an empty src shows a labeled placeholder so the element stays
|
|
110
172
|
* visible and selectable in the editor.
|
|
111
173
|
*/ const Image = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
112
|
-
const { src: storedSrc, alt, decorative, title, loading, objectFit, width, height, intrinsicWidth, intrinsicHeight, radius, screenId, href: externalHref, // Never forward children to the <img> below — React throws on ANY
|
|
174
|
+
const { src: storedSrc, alt, decorative, title, loading, objectFit, width, height, sizes: authoredSizes, renderedWidths, mediaVersion, intrinsicWidth, intrinsicHeight, radius, screenId, href: externalHref, // Never forward children to the <img> below — React throws on ANY
|
|
113
175
|
// children value reaching a void element, which 500'd whole pages
|
|
114
176
|
// when a renderer passed empty JSX children through (AGL-579).
|
|
115
177
|
children: _children, // Pull `sx` out of the spread: the literals below are composed AFTER
|
|
@@ -125,6 +187,9 @@ export const ID = 'image';
|
|
|
125
187
|
"objectFit",
|
|
126
188
|
"width",
|
|
127
189
|
"height",
|
|
190
|
+
"sizes",
|
|
191
|
+
"renderedWidths",
|
|
192
|
+
"mediaVersion",
|
|
128
193
|
"intrinsicWidth",
|
|
129
194
|
"intrinsicHeight",
|
|
130
195
|
"radius",
|
|
@@ -152,8 +217,15 @@ export const ID = 'image';
|
|
|
152
217
|
* its leaf ids, so the id in the tree and the id in the context are the
|
|
153
218
|
* same leaf spelled two ways (the markdown block resolves the same way).
|
|
154
219
|
*/ const nodeId = Aglyn.useNodeId();
|
|
155
|
-
const
|
|
220
|
+
const leadIds = nodeId ? leadImageNodeIds(Aglyn.canvas.rootNode) : [];
|
|
221
|
+
const isLeadImage = leadIds.some((leadId)=>Aglyn.leafIdsMatch(leadId, nodeId));
|
|
222
|
+
const screenLeadId = leadIds.find((id)=>!Aglyn.isLayoutComposedNodeId(id));
|
|
223
|
+
const isScreenLead = screenLeadId !== undefined && Aglyn.leafIdsMatch(screenLeadId, nodeId);
|
|
156
224
|
const eager = loading === 'eager' || loading == null && isLeadImage;
|
|
225
|
+
// The canvas measures; nothing else does (AGL-3485).
|
|
226
|
+
const { editorInert } = Aglyn.useScreenLink(undefined);
|
|
227
|
+
const recordRef = useRenderedWidthRecorder(nodeId, Boolean(editorInert));
|
|
228
|
+
const imageRef = useForkRef(ref, recordRef);
|
|
157
229
|
/**
|
|
158
230
|
* Resolve the stored value to a URL (AGL-1215). A media reference becomes
|
|
159
231
|
* a CDN URL here rather than in the document, so the route shape stays an
|
|
@@ -165,8 +237,11 @@ export const ID = 'image';
|
|
|
165
237
|
* asking site in the org scope, which is what lets ONE reference in a
|
|
166
238
|
* layout or reusable component resolve on each site that uses it.
|
|
167
239
|
*/ const { hostId } = Aglyn.useSite();
|
|
240
|
+
// With the asset's version when the composition read one (AGL-3485): the
|
|
241
|
+
// versioned URL is the one every cache may keep, and a replace moves it.
|
|
168
242
|
const src = Aglyn.resolveMediaSrc(storedSrc, {
|
|
169
|
-
hostId
|
|
243
|
+
hostId,
|
|
244
|
+
version: mediaVersion
|
|
170
245
|
});
|
|
171
246
|
const wrapLink = (element)=>linkHref && !suppressNavigation ? /*#__PURE__*/ _jsx(AppLink, {
|
|
172
247
|
componentVariant: "naked",
|
|
@@ -185,7 +260,7 @@ export const ID = 'image';
|
|
|
185
260
|
sx: nodeSx
|
|
186
261
|
}));
|
|
187
262
|
return /*#__PURE__*/ _jsx(Box, _extends({
|
|
188
|
-
ref:
|
|
263
|
+
ref: imageRef
|
|
189
264
|
}, rest, {
|
|
190
265
|
sx: [
|
|
191
266
|
{
|
|
@@ -209,9 +284,41 @@ export const ID = 'image';
|
|
|
209
284
|
// CDN URLs (AGL-175) carry WebP variants selected by `?w=`; widths
|
|
210
285
|
// without a variant fall back to the original server-side, so a static
|
|
211
286
|
// srcSet is safe for any CDN-form URL. Asked of the RESOLVED url, so a
|
|
212
|
-
// reference and a legacy stored path both keep their WebP variants.
|
|
213
|
-
|
|
214
|
-
|
|
287
|
+
// reference and a legacy stored path both keep their WebP variants. Any
|
|
288
|
+
// other url has no candidates, and so no `sizes` either.
|
|
289
|
+
//
|
|
290
|
+
// The asset's own width caps the list (AGL-3486): the widest candidate is
|
|
291
|
+
// the source, described truthfully, never a `1920w` for a 1000px photo that
|
|
292
|
+
// the CDN can only answer with the same 1000 pixels.
|
|
293
|
+
const srcSet = Aglyn.mediaCdnSrcSet(src, {
|
|
294
|
+
sourceWidth: typeof intrinsicWidth === 'number' && Number.isFinite(intrinsicWidth) && intrinsicWidth > 0 ? intrinsicWidth : undefined
|
|
295
|
+
});
|
|
296
|
+
/**
|
|
297
|
+
* How wide the image will render, for `sizes` (AGL-3485): the author's own
|
|
298
|
+
* value, else per breakpoint a pixel width or height the element states (in
|
|
299
|
+
* its attributes or its `sx`, responsive values included), what the
|
|
300
|
+
* Besigner measured, and what the layout around it states. See
|
|
301
|
+
* `utils/image-sizes.ts` for the order and for why none of it disturbs
|
|
302
|
+
* layout the way `sizes="auto"` did (AGL-2486).
|
|
303
|
+
*
|
|
304
|
+
* The layout is read off the canvas tree this image is rendered from, which
|
|
305
|
+
* the tenant fills on the server too, so the published HTML carries it.
|
|
306
|
+
*/ const resolvedSizes = imageSizes({
|
|
307
|
+
sizes: authoredSizes,
|
|
308
|
+
width,
|
|
309
|
+
height,
|
|
310
|
+
sx: nodeSxProp,
|
|
311
|
+
intrinsicWidth,
|
|
312
|
+
intrinsicHeight,
|
|
313
|
+
renderedWidths,
|
|
314
|
+
layoutWidths: nodeId ? layoutBandWidths(Aglyn.canvas.getNode(nodeId)) : undefined
|
|
315
|
+
});
|
|
316
|
+
/**
|
|
317
|
+
* The page's hero: the SCREEN's lead image, loading eagerly, that nothing
|
|
318
|
+
* says renders small. It gets `fetchpriority="high"`, which its preload
|
|
319
|
+
* carries too (AGL-3485) — see `fetchPriority`. Only on a published page:
|
|
320
|
+
* an editing surface has no first paint to win.
|
|
321
|
+
*/ const hero = isScreenLead && eager && !resolvedSizes.small && !suppressNavigation;
|
|
215
322
|
/**
|
|
216
323
|
* The intrinsic attribute pair, or nothing.
|
|
217
324
|
*
|
|
@@ -226,8 +333,9 @@ export const ID = 'image';
|
|
|
226
333
|
width: intrinsicWidth,
|
|
227
334
|
height: intrinsicHeight
|
|
228
335
|
} : undefined;
|
|
336
|
+
const sizes = srcSet ? resolvedSizes.sizes : undefined;
|
|
229
337
|
return wrapLink(/*#__PURE__*/ _jsx(Box, _extends({
|
|
230
|
-
ref:
|
|
338
|
+
ref: imageRef,
|
|
231
339
|
component: "img",
|
|
232
340
|
src: src,
|
|
233
341
|
// EVERY CANDIDATE IS A `?w=` URL, and the bare one is gone (2026-08-26).
|
|
@@ -240,7 +348,7 @@ export const ID = 'image';
|
|
|
240
348
|
// can ask for the same list (AGL-3149). Building it here is the reason
|
|
241
349
|
// they had none: a candidate list inside a component is a candidate list
|
|
242
350
|
// no other component can have.
|
|
243
|
-
srcSet:
|
|
351
|
+
srcSet: srcSet,
|
|
244
352
|
// `sizes` is NOT only a delivery hint, and treating it as one broke every
|
|
245
353
|
// fluid image (AGL-2486). With `w` descriptors the browser derives the
|
|
246
354
|
// image's density-corrected INTRINSIC size from `sizes`, so `sizes` is
|
|
@@ -259,24 +367,26 @@ export const ID = 'image';
|
|
|
259
367
|
// original in a 158px slot — rendered those images tiny and centred in
|
|
260
368
|
// their box, on the canvas, in _preview and on published sites alike.
|
|
261
369
|
//
|
|
262
|
-
// A delivery win may not be paid for in layout,
|
|
263
|
-
// `
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
sizes:
|
|
370
|
+
// A delivery win may not be paid for in layout, which is why every
|
|
371
|
+
// answer `imageSizes` gives is a real width of the slot (AGL-3485): the
|
|
372
|
+
// author's own value, a definite pixel length, a span of a parent of
|
|
373
|
+
// definite width, or the width the Besigner MEASURED this image at —
|
|
374
|
+
// a fixed point, since an image whose intrinsic width is its measured
|
|
375
|
+
// width lays out at its measured width. `100vw`, the value every
|
|
376
|
+
// published document was authored against, is now only the last resort
|
|
377
|
+
// for a band nothing describes.
|
|
378
|
+
sizes: sizes,
|
|
271
379
|
// Unset alt keeps rendering `alt=""` exactly as it always has —
|
|
272
380
|
// existing documents must not change output (AGL-1305). Decorative
|
|
273
381
|
// ON forces `alt=""` over any alt text and suppresses the tooltip,
|
|
274
382
|
// so the a11y intent is explicit rather than an accident of blank.
|
|
275
383
|
alt: decorative ? '' : alt != null ? alt : '',
|
|
276
384
|
title: decorative ? undefined : title || undefined
|
|
277
|
-
}, eager ? {
|
|
385
|
+
}, eager ? _extends({
|
|
278
386
|
loading: 'eager'
|
|
279
|
-
}
|
|
387
|
+
}, hero ? {
|
|
388
|
+
fetchPriority: 'high'
|
|
389
|
+
} : {}) : Aglyn.DEFERRED_IMAGE_ATTRIBUTES, intrinsicAttributes, rest, {
|
|
280
390
|
sx: [
|
|
281
391
|
{
|
|
282
392
|
display: 'block',
|
|
@@ -392,6 +502,16 @@ export const schema = {
|
|
|
392
502
|
component: Aglyn.FieldComponentType.CSS_DIMENSION,
|
|
393
503
|
label: 'Height'
|
|
394
504
|
},
|
|
505
|
+
{
|
|
506
|
+
// AGL-3485. Empty is the answer for nearly everyone: the image works
|
|
507
|
+
// out how big it renders from where it sits on the page, measured as
|
|
508
|
+
// the page is built. This is the override for someone who knows the
|
|
509
|
+
// HTML attribute and wants to say it themselves.
|
|
510
|
+
name: 'sizes',
|
|
511
|
+
description: 'Leave empty — the image works out how big it shows on every ' + 'screen size from where you place it, and downloads a file that ' + 'size. Advanced: an HTML sizes value used exactly as typed, e.g. ' + '(min-width: 900px) 33vw, 100vw.',
|
|
512
|
+
component: Aglyn.FieldComponentType.TEXT_FIELD,
|
|
513
|
+
label: 'Sizes'
|
|
514
|
+
},
|
|
395
515
|
{
|
|
396
516
|
name: 'loading',
|
|
397
517
|
description: 'Lazy waits to load the image until a visitor scrolls near it; ' + 'pick Eager for the first image at the top of a page so it ' + 'shows immediately.',
|