@aglyn/shared-ui-jsx 1.0.0-beta.277 → 1.0.0-beta.283

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/stock-photo-search.component.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\n'use client'\n\nimport { mdiArrowLeft, mdiImageOutline, mdiMagnify } from '@mdi/js'\nimport {\n Alert,\n Box,\n Button,\n ButtonBase,\n InputAdornment,\n LinearProgress,\n Link,\n Skeleton,\n Stack,\n TextField,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n type Theme,\n} from '@mui/material'\nimport { type FormEvent, useCallback, useEffect, useRef, useState } from 'react'\nimport EmptyStateComponent from './empty-state.component'\nimport { MdiIcon } from './mdi-icon'\n\n/**\n * SEARCH A STOCK PHOTO LIBRARY AND CHOOSE ONE (AGL-4066): the media picker's\n * Stock photos tab, and the details of the photo chosen.\n *\n * Presentation only. What a search asks and where a photo goes are the\n * caller's: it hands in a {@link StockPhotoSearchClient} (the console binds\n * its route and the member's credentials) and receives the photo clicked,\n * which it places the way it places a library file. Every result shows its\n * photographer and the library it comes from, since a library's terms ask\n * that its results say where they are from.\n *\n * Thumbnails arrive as image bytes from the caller's server and are shown\n * through object URLs this component owns, so no address at the library is\n * ever loaded by the browser; it fetches them a few at a time, only for the\n * results on screen.\n */\n\n/** The shapes the search offers. */\nexport type StockPhotoSearchOrientation = 'any' | 'horizontal' | 'vertical'\n\n/** One result, as much of it as this shows. */\nexport interface StockPhotoSearchHit {\n /** What the caller names the photo by when it asks for its preview. */\n ref: unknown\n /** Unique among the results. */\n sourceKey: string\n width: number\n height: number\n photographer: string\n tags: string[]\n alt?: string\n}\n\n/** The details panel's further facts about a result. */\nexport interface StockPhotoDetailsHit extends StockPhotoSearchHit {\n photographerUrl?: string\n pageUrl: string\n /** \"Photo by … on …\". */\n credit: string\n license: string\n licenseUrl: string\n}\n\ntype Refusal = { status: 'busy' | 'unconfigured' | 'refused' | 'gone'; message: string }\n\n/** What the caller's client answers. */\nexport interface StockPhotoSearchClient<Hit extends StockPhotoSearchHit> {\n status(): Promise<{ status: 'ready'; library: { label: string } } | Refusal>\n search(\n query: string,\n orientation: StockPhotoSearchOrientation,\n ): Promise<{ status: 'ok'; library: { label: string }; hits: Hit[] } | Refusal>\n preview(ref: Hit['ref']): Promise<Blob | null>\n}\n\nexport interface StockPhotoSearchPanelProps<Hit extends StockPhotoSearchHit> {\n client: StockPhotoSearchClient<Hit>\n /** Where a used photo is saved, as a sentence ends: \"this site's library\". */\n libraryName: string\n /** The longest search the library takes. */\n maxQueryLength: number\n /** The result currently chosen. */\n selectedKey?: string | null\n /** A result was clicked; `previewUrl` is its thumbnail, when it has loaded. */\n onSelect: (hit: Hit, context: { previewUrl: string | null; libraryLabel: string }) => void\n}\n\n/** Results shown at first, and added by each Show more. */\nexport const STOCK_PHOTO_SEARCH_PAGE = 20\n/** Previews fetched at once. */\nconst PREVIEWS_AT_ONCE = 4\n/** Skeleton tiles while a search runs. */\nconst SKELETON_TILES = 12\n\nconst ORIENTATION_LABELS: ReadonlyArray<[StockPhotoSearchOrientation, string]> = [\n ['any', 'Any'],\n ['horizontal', 'Landscape'],\n ['vertical', 'Portrait'],\n]\n\n/** The grid every state of the results lays out in. */\nconst gridSx = (theme: Theme) => ({\n display: 'grid',\n gap: theme.spacing(2),\n gridTemplateColumns: `repeat(auto-fill, minmax(${theme.spacing(20)}, 1fr))`,\n})\n\n/** A result's tile: the thumbnail, the photographer and the library. */\nfunction StockPhotoTile(props: {\n hit: StockPhotoSearchHit\n previewUrl: string | null | undefined\n libraryLabel: string\n selected: boolean\n onClick: () => void\n}) {\n const { hit, previewUrl, libraryLabel, selected, onClick } = props\n const described = hit.alt || hit.tags.slice(0, 3).join(', ')\n return (\n <ButtonBase\n onClick={onClick}\n aria-pressed={selected}\n aria-label={`Photo by ${hit.photographer} on ${libraryLabel}${described ? `: ${described}` : ''}`}\n data-testid=\"stock-photo-tile\"\n sx={(theme) => ({\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'stretch',\n textAlign: 'left',\n borderRadius: 1,\n overflow: 'hidden',\n border: 1,\n borderColor: selected ? 'primary.main' : 'divider',\n boxShadow: selected ? `0 0 0 1px ${theme.palette.primary.main}` : 'none',\n bgcolor: 'background.paper',\n transition: theme.transitions.create(['border-color', 'box-shadow']),\n '&:hover': { borderColor: selected ? 'primary.main' : 'text.primary' },\n '&.Mui-focusVisible': { outline: `${theme.spacing(0.25)} solid ${theme.palette.primary.main}`, outlineOffset: theme.spacing(0.25) },\n })}\n >\n <Box sx={{ aspectRatio: '4 / 3', bgcolor: 'action.hover', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'text.disabled' }}>\n {previewUrl ? (\n <Box component=\"img\" src={previewUrl} alt=\"\" sx={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />\n ) : previewUrl === null ? (\n <MdiIcon path={mdiImageOutline} fontSize=\"large\" />\n ) : (\n <Skeleton variant=\"rectangular\" sx={{ width: '100%', height: '100%' }} />\n )}\n </Box>\n <Box sx={{ px: 1, py: 0.75, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {hit.photographer}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap component=\"div\">\n {libraryLabel}\n </Typography>\n </Box>\n </ButtonBase>\n )\n}\n\ntype SearchState<Hit> =\n | { kind: 'idle' }\n | { kind: 'searching'; query: string }\n | { kind: 'results'; query: string; hits: Hit[] }\n | { kind: 'refused'; refusal: Refusal; query: string }\n\n/**\n * The Stock photos tab: a search box, the shape, and the results. Asks the\n * client for its status once, on mount, so a deployment with no library, or\n * a member who cannot add files here, is told so before typing anything.\n */\nexport function StockPhotoSearchPanel<Hit extends StockPhotoSearchHit>(props: StockPhotoSearchPanelProps<Hit>) {\n const { client, libraryName, maxQueryLength, selectedKey, onSelect } = props\n const [status, setStatus] = useState<{ status: 'loading' } | Awaited<ReturnType<StockPhotoSearchClient<Hit>['status']>>>({ status: 'loading' })\n const [query, setQuery] = useState('')\n const [orientation, setOrientation] = useState<StockPhotoSearchOrientation>('any')\n const [search, setSearch] = useState<SearchState<Hit>>({ kind: 'idle' })\n const [shown, setShown] = useState(STOCK_PHOTO_SEARCH_PAGE)\n const [previews, setPreviews] = useState<Record<string, string | null>>({})\n const owned = useRef<Map<string, string | null>>(new Map())\n const asked = useRef<Set<string>>(new Set())\n const latest = useRef(0)\n\n useEffect(() => {\n let live = true\n setStatus({ status: 'loading' })\n client.status().then((answer) => {\n if (live) setStatus(answer)\n })\n return () => {\n live = false\n }\n }, [client])\n\n // The object URLs are this component's: let them go with it.\n useEffect(() => {\n const urls = owned.current\n return () => {\n for (const url of urls.values()) if (url) URL.revokeObjectURL(url)\n urls.clear()\n }\n }, [])\n\n const libraryLabel = status.status === 'ready' ? status.library.label : ''\n\n const run = useCallback(\n async (words: string, shape: StockPhotoSearchOrientation) => {\n const trimmed = words.replace(/\\s+/g, ' ').trim()\n if (!trimmed) return\n const ticket = ++latest.current\n setSearch({ kind: 'searching', query: trimmed })\n setShown(STOCK_PHOTO_SEARCH_PAGE)\n const answer = await client.search(trimmed, shape)\n if (ticket !== latest.current) return\n setSearch(answer.status === 'ok' ? { kind: 'results', query: trimmed, hits: answer.hits } : { kind: 'refused', refusal: answer, query: trimmed })\n },\n [client],\n )\n\n const visible = search.kind === 'results' ? search.hits.slice(0, shown) : []\n\n // Previews for the results on screen, a few at a time, each asked once.\n useEffect(() => {\n let live = true\n const queue = visible.filter((hit) => !asked.current.has(hit.sourceKey))\n const next = async (): Promise<void> => {\n const hit = queue.shift()\n if (!hit || !live) return\n asked.current.add(hit.sourceKey)\n const blob = await client.preview(hit.ref)\n if (!live) {\n // Superseded: the next run asks for it again, for the results then on screen.\n asked.current.delete(hit.sourceKey)\n return\n }\n const url = blob ? URL.createObjectURL(blob) : null\n owned.current.set(hit.sourceKey, url)\n setPreviews((held) => ({ ...held, [hit.sourceKey]: url }))\n return next()\n }\n void Promise.all(Array.from({ length: PREVIEWS_AT_ONCE }, next))\n return () => {\n live = false\n }\n // `visible` is derived from these.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client, search, shown])\n\n const submit = (event: FormEvent) => {\n event.preventDefault()\n void run(query, orientation)\n }\n\n if (status.status === 'loading') {\n return <LinearProgress aria-label=\"Loading stock photos\" />\n }\n if (status.status !== 'ready') {\n return (\n <Alert severity={status.status === 'refused' ? 'warning' : 'info'} data-testid=\"stock-photos-unavailable\">\n {status.message}\n </Alert>\n )\n }\n\n return (\n <Stack spacing={2} data-testid=\"stock-photo-search\">\n <Box component=\"form\" role=\"search\" onSubmit={submit} sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center' }}>\n <TextField\n size=\"small\"\n value={query}\n onChange={(event) => setQuery(event.target.value.slice(0, maxQueryLength))}\n placeholder={`Search ${libraryLabel} photos`}\n slotProps={{\n htmlInput: { maxLength: maxQueryLength, 'aria-label': `Search ${libraryLabel} photos` },\n input: {\n startAdornment: (\n <InputAdornment position=\"start\">\n <MdiIcon path={mdiMagnify} />\n </InputAdornment>\n ),\n },\n }}\n sx={{ flex: 1, minWidth: (theme) => theme.spacing(25) }}\n />\n <ToggleButtonGroup\n size=\"small\"\n exclusive\n value={orientation}\n aria-label=\"Photo shape\"\n onChange={(_event, value: StockPhotoSearchOrientation | null) => {\n if (!value) return\n setOrientation(value)\n if (search.kind !== 'idle') void run(search.query, value)\n }}\n >\n {ORIENTATION_LABELS.map(([value, label]) => (\n <ToggleButton key={value} value={value}>\n {label}\n </ToggleButton>\n ))}\n </ToggleButtonGroup>\n <Button type=\"submit\" variant=\"contained\" disabled={!query.trim() || search.kind === 'searching'}>\n {'Search'}\n </Button>\n </Box>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Photos come from ${libraryLabel}. The one you use is saved to ${libraryName} with the photographer's credit.`}\n </Typography>\n {search.kind === 'idle' ? (\n <EmptyStateComponent\n label=\"Find a photo for your page\"\n description=\"Describe what the photo should show, such as a coffee shop counter or a mountain lake, then choose Search.\"\n />\n ) : search.kind === 'searching' ? (\n <Box sx={gridSx} aria-busy=\"true\" aria-label=\"Searching\">\n {Array.from({ length: SKELETON_TILES }, (_, index) => (\n <Skeleton key={index} variant=\"rectangular\" sx={{ aspectRatio: '4 / 3', height: 'auto', borderRadius: 1 }} />\n ))}\n </Box>\n ) : search.kind === 'refused' ? (\n <Alert\n severity={search.refusal.status === 'refused' ? 'warning' : 'info'}\n action={\n search.refusal.status === 'busy' ? (\n <Button color=\"inherit\" size=\"small\" onClick={() => void run(search.query, orientation)}>\n {'Try again'}\n </Button>\n ) : undefined\n }\n >\n {search.refusal.message}\n </Alert>\n ) : search.hits.length === 0 ? (\n <EmptyStateComponent label={`No photos found for “${search.query}”`} description=\"Try fewer words, or describe it differently.\" />\n ) : (\n <>\n <Box sx={gridSx}>\n {visible.map((hit) => (\n <StockPhotoTile\n key={hit.sourceKey}\n hit={hit}\n previewUrl={previews[hit.sourceKey]}\n libraryLabel={libraryLabel}\n selected={hit.sourceKey === selectedKey}\n onClick={() => onSelect(hit, { previewUrl: previews[hit.sourceKey] ?? null, libraryLabel })}\n />\n ))}\n </Box>\n {search.hits.length > shown ? (\n <Button onClick={() => setShown((count) => count + STOCK_PHOTO_SEARCH_PAGE)} sx={{ alignSelf: 'center' }}>\n {'Show more photos'}\n </Button>\n ) : null}\n </>\n )}\n </Stack>\n )\n}\n\nexport interface StockPhotoDetailsProps {\n hit: StockPhotoDetailsHit\n previewUrl: string | null\n /** The library's name: \"Pixabay\". */\n libraryLabel: string\n /** Where using the photo saves it, as a sentence ends: \"this site's library\". */\n libraryName: string\n /** Why using it failed, said above the facts. */\n error?: string | null\n /** Small screens, where the details replace the results: back to them. */\n onBack?: () => void\n}\n\n/** A caption over one fact. */\nconst Label = ({ children }: { children: string }) => (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"div\" sx={{ mb: 0.5 }}>\n {children}\n </Typography>\n)\n\n/** An outside page, opened in a new tab. */\nconst OutLink = ({ href, children }: { href: string; children: string }) => (\n <Link href={href} target=\"_blank\" rel=\"noopener noreferrer\">\n {children}\n </Link>\n)\n\n/**\n * The stock photo clicked, before it is used: a larger look, who took it,\n * the license and where it will be saved. Nothing is copied until the\n * caller's confirm button is pressed.\n */\nexport function StockPhotoDetails(props: StockPhotoDetailsProps) {\n const { hit, previewUrl, libraryLabel, libraryName, error, onBack } = props\n return (\n <Stack spacing={2} data-testid=\"stock-photo-details\">\n {onBack ? (\n <Button size=\"small\" startIcon={<MdiIcon path={mdiArrowLeft} />} onClick={onBack} sx={{ alignSelf: 'flex-start' }}>\n {'Back to photos'}\n </Button>\n ) : null}\n <Box\n sx={{\n aspectRatio: `${hit.width} / ${hit.height}`,\n maxHeight: (theme) => theme.spacing(30),\n borderRadius: 1,\n overflow: 'hidden',\n bgcolor: 'action.hover',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n color: 'text.disabled',\n }}\n >\n {previewUrl ? (\n <Box component=\"img\" src={previewUrl} alt={hit.alt || `Photo by ${hit.photographer}`} sx={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block' }} />\n ) : (\n <MdiIcon path={mdiImageOutline} fontSize=\"large\" />\n )}\n </Box>\n <Typography variant=\"subtitle1\" component=\"h3\">\n {hit.photographerUrl ? (\n <>\n {'Photo by '}\n <OutLink href={hit.photographerUrl}>{hit.photographer}</OutLink>\n </>\n ) : (\n `Photo by ${hit.photographer}`\n )}\n </Typography>\n {error ? <Alert severity=\"warning\">{error}</Alert> : null}\n <Box>\n <Label>{'Credit saved with the photo'}</Label>\n <Typography variant=\"body2\">{hit.credit}</Typography>\n </Box>\n <Box>\n <Label>{'License'}</Label>\n <Typography variant=\"body2\">\n {hit.licenseUrl ? <OutLink href={hit.licenseUrl}>{hit.license}</OutLink> : hit.license}\n </Typography>\n </Box>\n <Box>\n <Label>{'Size'}</Label>\n <Typography variant=\"body2\">{`${hit.width} × ${hit.height} pixels`}</Typography>\n </Box>\n <Typography variant=\"body2\">\n <OutLink href={hit.pageUrl}>{`See it on ${libraryLabel}`}</OutLink>\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`When you use it, a copy is saved to ${libraryName} and placed from there. If the library already has this photo, that copy is used.`}\n </Typography>\n </Stack>\n )\n}\n"],"names":["mdiArrowLeft","mdiImageOutline","mdiMagnify","Alert","Box","Button","ButtonBase","InputAdornment","LinearProgress","Link","Skeleton","Stack","TextField","ToggleButton","ToggleButtonGroup","Typography","useCallback","useEffect","useRef","useState","EmptyStateComponent","MdiIcon","STOCK_PHOTO_SEARCH_PAGE","PREVIEWS_AT_ONCE","SKELETON_TILES","ORIENTATION_LABELS","gridSx","theme","display","gap","spacing","gridTemplateColumns","StockPhotoTile","props","hit","previewUrl","libraryLabel","selected","onClick","described","alt","tags","slice","join","aria-pressed","aria-label","photographer","data-testid","sx","flexDirection","alignItems","textAlign","borderRadius","overflow","border","borderColor","boxShadow","palette","primary","main","bgcolor","transition","transitions","create","outline","outlineOffset","aspectRatio","justifyContent","color","component","src","width","height","objectFit","path","fontSize","variant","px","py","minWidth","noWrap","StockPhotoSearchPanel","client","libraryName","maxQueryLength","selectedKey","onSelect","status","setStatus","query","setQuery","orientation","setOrientation","search","setSearch","kind","shown","setShown","previews","setPreviews","owned","Map","asked","Set","latest","live","then","answer","urls","current","url","values","URL","revokeObjectURL","clear","library","label","run","words","shape","trimmed","replace","trim","ticket","hits","refusal","visible","queue","filter","has","sourceKey","next","shift","add","blob","preview","ref","delete","createObjectURL","set","held","Promise","all","Array","from","length","submit","event","preventDefault","severity","message","role","onSubmit","flexWrap","size","value","onChange","target","placeholder","slotProps","htmlInput","maxLength","input","startAdornment","position","flex","exclusive","_event","map","type","disabled","description","aria-busy","_","index","action","undefined","count","alignSelf","Label","children","mb","OutLink","href","rel","StockPhotoDetails","error","onBack","startIcon","maxHeight","photographerUrl","credit","licenseUrl","license","pageUrl"],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * File ▸ Discard changes… for every besigner editor (AGL-3723).
19
+ *
20
+ * Puts the canvas back to the stored document — the published tree on the
21
+ * live version, the last save on any other — and drops the local and shared
22
+ * drafts, after a confirm because the reset cannot be undone.
23
+ * `useBesignerDocument().discardChanges` does the document half; the editor
24
+ * passes what only it owns: the co-editing mirror and its own "draft saved"
25
+ * flag.
26
+ */
27
+ export declare function useDiscardChangesMenuItem(options: {
28
+ noun: string;
29
+ /** Editing the version the sites serve, where the stored tree is live. */
30
+ live: boolean;
31
+ /** `useBesignerDocument().canDiscard`, plus any draft state the editor keeps. */
32
+ canDiscard: boolean;
33
+ discardChanges: () => boolean;
34
+ /** Clears what the editor owns: the mirror, a draft-saved flag. */
35
+ onDiscarded?: () => void;
36
+ }): {
37
+ onClick: () => Promise<void>;
38
+ ListItemTextProps: {
39
+ inset: boolean;
40
+ secondary?: undefined;
41
+ };
42
+ id: string;
43
+ children: string;
44
+ disabled: boolean;
45
+ } | {
46
+ onClick: () => Promise<void>;
47
+ ListItemTextProps: {
48
+ inset: boolean;
49
+ secondary: string;
50
+ };
51
+ id: string;
52
+ children: string;
53
+ disabled: boolean;
54
+ };
55
+ export default useDiscardChangesMenuItem;
@@ -0,0 +1,83 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { useConfirmationContext } from "../contexts/confirmation.context.js";
18
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
19
+ import { useCallback } from "react";
20
+ /**
21
+ * File ▸ Discard changes… for every besigner editor (AGL-3723).
22
+ *
23
+ * Puts the canvas back to the stored document — the published tree on the
24
+ * live version, the last save on any other — and drops the local and shared
25
+ * drafts, after a confirm because the reset cannot be undone.
26
+ * `useBesignerDocument().discardChanges` does the document half; the editor
27
+ * passes what only it owns: the co-editing mirror and its own "draft saved"
28
+ * flag.
29
+ */ export function useDiscardChangesMenuItem(options) {
30
+ const { noun, live, canDiscard, discardChanges, onDiscarded } = options;
31
+ const { confirm } = useConfirmationContext();
32
+ const { enqueueSnackbar } = useSnackbar();
33
+ const target = live ? 'the published version' : 'its last save';
34
+ const onClick = useCallback(async ()=>{
35
+ const confirmed = await confirm({
36
+ title: 'Discard changes?',
37
+ description: `This puts the ${noun} back to ${target} and deletes its unsaved ` + 'changes and saved draft. This cannot be undone.',
38
+ confirmationText: 'Discard',
39
+ confirmationButtonProps: {
40
+ color: 'error'
41
+ }
42
+ }).then(()=>true).catch(()=>false);
43
+ if (!confirmed) return;
44
+ if (!discardChanges()) {
45
+ enqueueSnackbar(`The ${noun} has not loaded yet — try again.`, {
46
+ variant: 'warning',
47
+ persist: false
48
+ });
49
+ return;
50
+ }
51
+ onDiscarded == null ? void 0 : onDiscarded();
52
+ enqueueSnackbar(`Changes discarded — back to ${target}.`, {
53
+ variant: 'success',
54
+ persist: false
55
+ });
56
+ }, [
57
+ confirm,
58
+ noun,
59
+ target,
60
+ discardChanges,
61
+ onDiscarded,
62
+ enqueueSnackbar
63
+ ]);
64
+ return _extends({
65
+ id: 'center-nav-file-discard',
66
+ children: 'Discard changes…',
67
+ disabled: !canDiscard
68
+ }, canDiscard ? {
69
+ ListItemTextProps: {
70
+ inset: true
71
+ }
72
+ } : {
73
+ ListItemTextProps: {
74
+ inset: true,
75
+ secondary: 'No changes to discard'
76
+ }
77
+ }, {
78
+ onClick
79
+ });
80
+ }
81
+ export default useDiscardChangesMenuItem;
82
+
83
+ //# sourceMappingURL=use-discard-changes-menu-item.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/hooks/use-discard-changes-menu-item.ts"],"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 { useConfirmationContext } from '../contexts/confirmation.context'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useCallback } from 'react'\n\n/**\n * File ▸ Discard changes… for every besigner editor (AGL-3723).\n *\n * Puts the canvas back to the stored document — the published tree on the\n * live version, the last save on any other — and drops the local and shared\n * drafts, after a confirm because the reset cannot be undone.\n * `useBesignerDocument().discardChanges` does the document half; the editor\n * passes what only it owns: the co-editing mirror and its own \"draft saved\"\n * flag.\n */\nexport function useDiscardChangesMenuItem(options: {\n noun: string\n /** Editing the version the sites serve, where the stored tree is live. */\n live: boolean\n /** `useBesignerDocument().canDiscard`, plus any draft state the editor keeps. */\n canDiscard: boolean\n discardChanges: () => boolean\n /** Clears what the editor owns: the mirror, a draft-saved flag. */\n onDiscarded?: () => void\n}) {\n const { noun, live, canDiscard, discardChanges, onDiscarded } = options\n const { confirm } = useConfirmationContext()\n const { enqueueSnackbar } = useSnackbar()\n const target = live ? 'the published version' : 'its last save'\n\n const onClick = useCallback(async () => {\n const confirmed = await confirm({\n title: 'Discard changes?',\n description:\n `This puts the ${noun} back to ${target} and deletes its unsaved ` +\n 'changes and saved draft. This cannot be undone.',\n confirmationText: 'Discard',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n if (!discardChanges()) {\n enqueueSnackbar(`The ${noun} has not loaded yet — try again.`, {\n variant: 'warning',\n persist: false,\n })\n return\n }\n onDiscarded?.()\n enqueueSnackbar(`Changes discarded — back to ${target}.`, {\n variant: 'success',\n persist: false,\n })\n }, [confirm, noun, target, discardChanges, onDiscarded, enqueueSnackbar])\n\n return {\n id: 'center-nav-file-discard',\n children: 'Discard changes…',\n disabled: !canDiscard,\n // A disabled entry says why, like Save & publish does.\n ...(canDiscard\n ? { ListItemTextProps: { inset: true } }\n : {\n ListItemTextProps: { inset: true, secondary: 'No changes to discard' },\n }),\n onClick,\n }\n}\n\nexport default useDiscardChangesMenuItem\n"],"names":["useConfirmationContext","useSnackbar","useCallback","useDiscardChangesMenuItem","options","noun","live","canDiscard","discardChanges","onDiscarded","confirm","enqueueSnackbar","target","onClick","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","variant","persist","id","children","disabled","ListItemTextProps","inset","secondary"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,sBAAsB,QAAQ,sCAAkC;AACzE,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SAASC,WAAW,QAAQ,QAAO;AAEnC;;;;;;;;;CASC,GACD,OAAO,SAASC,0BAA0BC,OASzC;IACC,MAAM,EAAEC,IAAI,EAAEC,IAAI,EAAEC,UAAU,EAAEC,cAAc,EAAEC,WAAW,EAAE,GAAGL;IAChE,MAAM,EAAEM,OAAO,EAAE,GAAGV;IACpB,MAAM,EAAEW,eAAe,EAAE,GAAGV;IAC5B,MAAMW,SAASN,OAAO,0BAA0B;IAEhD,MAAMO,UAAUX,YAAY;QAC1B,MAAMY,YAAY,MAAMJ,QAAQ;YAC9BK,OAAO;YACPC,aACE,CAAC,cAAc,EAAEX,KAAK,SAAS,EAAEO,OAAO,yBAAyB,CAAC,GAClE;YACFK,kBAAkB;YAClBC,yBAAyB;gBAAEC,OAAO;YAAQ;QAC5C,GACGC,IAAI,CAAC,IAAM,MACXC,KAAK,CAAC,IAAM;QACf,IAAI,CAACP,WAAW;QAChB,IAAI,CAACN,kBAAkB;YACrBG,gBAAgB,CAAC,IAAI,EAAEN,KAAK,gCAAgC,CAAC,EAAE;gBAC7DiB,SAAS;gBACTC,SAAS;YACX;YACA;QACF;QACAd,+BAAAA;QACAE,gBAAgB,CAAC,4BAA4B,EAAEC,OAAO,CAAC,CAAC,EAAE;YACxDU,SAAS;YACTC,SAAS;QACX;IACF,GAAG;QAACb;QAASL;QAAMO;QAAQJ;QAAgBC;QAAaE;KAAgB;IAExE,OAAO;QACLa,IAAI;QACJC,UAAU;QACVC,UAAU,CAACnB;OAEPA,aACA;QAAEoB,mBAAmB;YAAEC,OAAO;QAAK;IAAE,IACrC;QACED,mBAAmB;YAAEC,OAAO;YAAMC,WAAW;QAAwB;IACvE;QACJhB;;AAEJ;AAEA,eAAeV,0BAAyB"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Whether the browser currently believes it has a network connection
3
+ * (AGL-1056).
4
+ *
5
+ * The console is an authoring tool, and the failure it has to avoid is the
6
+ * silent one: edits that appear to apply, a save that never lands, and no
7
+ * indication that the network — rather than the app — is the reason. This is
8
+ * the signal the chrome renders from.
9
+ *
10
+ * `navigator.onLine` is a floor, not a guarantee. It answers "is there a
11
+ * network interface up", which is why it can read `true` on a captive portal
12
+ * or a dead uplink. It cannot produce a false OFFLINE, though, and that is the
13
+ * direction this is used in: when it says offline, the console is offline.
14
+ */
15
+ export declare function useOnlineStatus(): boolean;
16
+ export default useOnlineStatus;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { useSyncExternalStore } from "react";
18
+ /**
19
+ * Subscribe to every event that can change the answer.
20
+ *
21
+ * Module scope, not a closure built per render: `useSyncExternalStore`
22
+ * resubscribes whenever this identity changes, so an inline function would
23
+ * tear down and re-add the listeners on every render of every consumer.
24
+ *
25
+ * Symmetric by construction — the same three names with the same handler
26
+ * reference are removed as were added. That matters under StrictMode, which
27
+ * deliberately mounts, unmounts and remounts each effect: an asymmetric
28
+ * cleanup leaks a listener per mount and, worse, leaves a listener bound to a
29
+ * callback from a torn-down render.
30
+ *
31
+ * `visibilitychange` is in here because `online`/`offline` are not guaranteed
32
+ * to be delivered to a backgrounded or discarded tab — a laptop that slept on
33
+ * hotel wifi and woke on another network can miss the pair entirely. The
34
+ * snapshot below is a pure read, so an extra notification costs one
35
+ * `Object.is` comparison and React bails out; a missed one leaves the chrome
36
+ * asserting a connection state that stopped being true hours ago.
37
+ */ function subscribe(onStoreChange) {
38
+ window.addEventListener('online', onStoreChange);
39
+ window.addEventListener('offline', onStoreChange);
40
+ document.addEventListener('visibilitychange', onStoreChange);
41
+ return ()=>{
42
+ window.removeEventListener('online', onStoreChange);
43
+ window.removeEventListener('offline', onStoreChange);
44
+ document.removeEventListener('visibilitychange', onStoreChange);
45
+ };
46
+ }
47
+ /**
48
+ * The live client answer, read fresh on every mount rather than seeded from a
49
+ * default. A boolean is its own snapshot identity, so this never trips the
50
+ * "getSnapshot should be cached" loop that catches object-returning stores.
51
+ *
52
+ * Anything other than an explicit `false` counts as online. `navigator.onLine`
53
+ * is a positive assertion of disconnection only: a browser that does not
54
+ * implement it leaves it `undefined`, and reading that as "offline" would have
55
+ * the console announce a network failure it has no evidence for.
56
+ */ function getSnapshot() {
57
+ return typeof navigator === 'undefined' || navigator.onLine !== false;
58
+ }
59
+ /**
60
+ * There is no `navigator` on the server, and no way to know a client's
61
+ * connection before one exists — so the server renders the online branch, and
62
+ * React uses this same value for the hydration render. That is what keeps the
63
+ * markup identical on both sides: the offline state can only appear AFTER
64
+ * hydration, from a real reading, never as a flash of a state nobody measured.
65
+ */ function getServerSnapshot() {
66
+ return true;
67
+ }
68
+ /**
69
+ * Whether the browser currently believes it has a network connection
70
+ * (AGL-1056).
71
+ *
72
+ * The console is an authoring tool, and the failure it has to avoid is the
73
+ * silent one: edits that appear to apply, a save that never lands, and no
74
+ * indication that the network — rather than the app — is the reason. This is
75
+ * the signal the chrome renders from.
76
+ *
77
+ * `navigator.onLine` is a floor, not a guarantee. It answers "is there a
78
+ * network interface up", which is why it can read `true` on a captive portal
79
+ * or a dead uplink. It cannot produce a false OFFLINE, though, and that is the
80
+ * direction this is used in: when it says offline, the console is offline.
81
+ */ export function useOnlineStatus() {
82
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
83
+ }
84
+ export default useOnlineStatus;
85
+
86
+ //# sourceMappingURL=use-online-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/hooks/use-online-status.ts"],"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'use client'\n\nimport { useSyncExternalStore } from 'react'\n\n/**\n * Subscribe to every event that can change the answer.\n *\n * Module scope, not a closure built per render: `useSyncExternalStore`\n * resubscribes whenever this identity changes, so an inline function would\n * tear down and re-add the listeners on every render of every consumer.\n *\n * Symmetric by construction — the same three names with the same handler\n * reference are removed as were added. That matters under StrictMode, which\n * deliberately mounts, unmounts and remounts each effect: an asymmetric\n * cleanup leaks a listener per mount and, worse, leaves a listener bound to a\n * callback from a torn-down render.\n *\n * `visibilitychange` is in here because `online`/`offline` are not guaranteed\n * to be delivered to a backgrounded or discarded tab — a laptop that slept on\n * hotel wifi and woke on another network can miss the pair entirely. The\n * snapshot below is a pure read, so an extra notification costs one\n * `Object.is` comparison and React bails out; a missed one leaves the chrome\n * asserting a connection state that stopped being true hours ago.\n */\nfunction subscribe(onStoreChange: () => void): () => void {\n window.addEventListener('online', onStoreChange)\n window.addEventListener('offline', onStoreChange)\n document.addEventListener('visibilitychange', onStoreChange)\n return () => {\n window.removeEventListener('online', onStoreChange)\n window.removeEventListener('offline', onStoreChange)\n document.removeEventListener('visibilitychange', onStoreChange)\n }\n}\n\n/**\n * The live client answer, read fresh on every mount rather than seeded from a\n * default. A boolean is its own snapshot identity, so this never trips the\n * \"getSnapshot should be cached\" loop that catches object-returning stores.\n *\n * Anything other than an explicit `false` counts as online. `navigator.onLine`\n * is a positive assertion of disconnection only: a browser that does not\n * implement it leaves it `undefined`, and reading that as \"offline\" would have\n * the console announce a network failure it has no evidence for.\n */\nfunction getSnapshot(): boolean {\n return typeof navigator === 'undefined' || navigator.onLine !== false\n}\n\n/**\n * There is no `navigator` on the server, and no way to know a client's\n * connection before one exists — so the server renders the online branch, and\n * React uses this same value for the hydration render. That is what keeps the\n * markup identical on both sides: the offline state can only appear AFTER\n * hydration, from a real reading, never as a flash of a state nobody measured.\n */\nfunction getServerSnapshot(): boolean {\n return true\n}\n\n/**\n * Whether the browser currently believes it has a network connection\n * (AGL-1056).\n *\n * The console is an authoring tool, and the failure it has to avoid is the\n * silent one: edits that appear to apply, a save that never lands, and no\n * indication that the network — rather than the app — is the reason. This is\n * the signal the chrome renders from.\n *\n * `navigator.onLine` is a floor, not a guarantee. It answers \"is there a\n * network interface up\", which is why it can read `true` on a captive portal\n * or a dead uplink. It cannot produce a false OFFLINE, though, and that is the\n * direction this is used in: when it says offline, the console is offline.\n */\nexport function useOnlineStatus(): boolean {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)\n}\n\nexport default useOnlineStatus\n"],"names":["useSyncExternalStore","subscribe","onStoreChange","window","addEventListener","document","removeEventListener","getSnapshot","navigator","onLine","getServerSnapshot","useOnlineStatus"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAASA,oBAAoB,QAAQ,QAAO;AAE5C;;;;;;;;;;;;;;;;;;;CAmBC,GACD,SAASC,UAAUC,aAAyB;IAC1CC,OAAOC,gBAAgB,CAAC,UAAUF;IAClCC,OAAOC,gBAAgB,CAAC,WAAWF;IACnCG,SAASD,gBAAgB,CAAC,oBAAoBF;IAC9C,OAAO;QACLC,OAAOG,mBAAmB,CAAC,UAAUJ;QACrCC,OAAOG,mBAAmB,CAAC,WAAWJ;QACtCG,SAASC,mBAAmB,CAAC,oBAAoBJ;IACnD;AACF;AAEA;;;;;;;;;CASC,GACD,SAASK;IACP,OAAO,OAAOC,cAAc,eAAeA,UAAUC,MAAM,KAAK;AAClE;AAEA;;;;;;CAMC,GACD,SAASC;IACP,OAAO;AACT;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC;IACd,OAAOX,qBAAqBC,WAAWM,aAAaG;AACtD;AAEA,eAAeC,gBAAe"}