@aglyn/shared-ui-jsx 1.0.0-beta.147 → 1.0.0-beta.150

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/shared-ui-jsx",
3
- "version": "1.0.0-beta.147",
3
+ "version": "1.0.0-beta.150",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,10 +25,10 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/shared-data-mdi": "1.0.0-beta.147",
29
- "@aglyn/shared-ui-theme": "1.0.0-beta.147",
30
- "@aglyn/shared-util-tools": "1.0.0-beta.147",
31
- "@aglyn/shared-util-vendor": "1.0.0-beta.147",
28
+ "@aglyn/shared-data-mdi": "1.0.0-beta.150",
29
+ "@aglyn/shared-ui-theme": "1.0.0-beta.150",
30
+ "@aglyn/shared-util-tools": "1.0.0-beta.150",
31
+ "@aglyn/shared-util-vendor": "1.0.0-beta.150",
32
32
  "@mdi/js": "^7.4.47",
33
33
  "@swc/helpers": "0.5.23",
34
34
  "change-case": "^5.4.4",
@@ -0,0 +1,96 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { ReadOutcome } from '../utils/read-outcome';
3
+ export interface ReadGatedEmptyStateProps {
4
+ /** MDI path (e.g. `ICON_VARIANT_HOST_GROUP.path`) shown above the title. */
5
+ iconPath?: string;
6
+ /** Headline — the one-line "what this is / what to do" message. */
7
+ title: ReactNode;
8
+ /** Supporting copy under the title; keep it to a sentence or two. */
9
+ description?: ReactNode;
10
+ /** Primary call to action (usually a `<Button>`), rendered below the copy. */
11
+ action?: ReactNode;
12
+ /**
13
+ * Did the read behind this list actually succeed? REQUIRED, and
14
+ * deliberately not defaulted (AGL-1066).
15
+ *
16
+ * A list is empty for three different reasons and only one of them is
17
+ * "there is nothing here". Passing `loaded` is an assertion that the read
18
+ * reached the server and came back with zero rows; anything else renders
19
+ * the loading or degraded branch below and `title`/`description`/`action`
20
+ * are never shown. See `utils/read-outcome`.
21
+ */
22
+ read: ReadOutcome;
23
+ /**
24
+ * What could not be loaded, for the degraded copy — a lower-case noun
25
+ * phrase in the customer's terms: `'your sites'`, `'your workspaces'`.
26
+ */
27
+ subject?: string;
28
+ /** Re-runs the failed read. Omit only when nothing can retry it. */
29
+ onRetry?: () => void;
30
+ /**
31
+ * An action for the degraded branch that OUTRANKS the retry button
32
+ * (AGL-3080) — for a caller that knows retrying cannot work.
33
+ *
34
+ * The case it exists for is the console's stale session: every server read
35
+ * is being refused, so "Try again" is an invitation to fail, and what the
36
+ * reader needs is the way back into the sign-in dialog. Only the app knows
37
+ * that, because only the app holds the session store — which is exactly why
38
+ * this is a prop rather than a branch in here. A caller that passes nothing
39
+ * gets the retry button, which is the right answer for every read whose
40
+ * failure is not the session.
41
+ */
42
+ degradedAction?: ReactNode;
43
+ }
44
+ /**
45
+ * Reusable zero-state block: a centered icon, title, supporting copy and an
46
+ * optional call to action inside the standard `CardDisplay` framing. Use it
47
+ * wherever a list/grid can legitimately be empty (no sites yet, no org yet,
48
+ * empty media library) instead of leaving a blank content area.
49
+ *
50
+ * Distinct from {@link EmptyState} in this same library, which is the
51
+ * illustrated "nothing here" block for a panel that has no read behind it.
52
+ * This one is for a LIST, and the difference that matters is the gate.
53
+ *
54
+ * ## Lives here so a plugin can state the rule too (AGL-3080)
55
+ *
56
+ * It was the console's, at `apps/console/components/empty-state.component`,
57
+ * and the rule below is not an app's rule — the marketplace's own licence
58
+ * list asserts "this workspace holds no licenses" off the same three-valued
59
+ * evidence. A plugin may not import an app, so the choice was to move this or
60
+ * to let each plugin hand-roll a zero-state that forgets the gate, which is
61
+ * the defect AGL-1066 already found in every console surface that had one.
62
+ * The console keeps a wrapper at the old path that supplies
63
+ * {@link ReadGatedEmptyStateProps.degradedAction}, so its own call sites are
64
+ * unchanged.
65
+ *
66
+ * ## The zero-state is GATED, not merely offered (AGL-1066, AGL-1062)
67
+ *
68
+ * "No sites yet — Create a site to start building" is a statement of fact
69
+ * about someone's account, and it was reachable from a read that never
70
+ * reached the server: a stale session denies every server read while
71
+ * `persistentLocalCache` keeps listeners painting, so the list rendered, then
72
+ * emptied, then asserted the emptiness. On a page whose zero-state carries a
73
+ * **Create site** button that is not just wrong, it invites a customer to
74
+ * rebuild sites they still own.
75
+ *
76
+ * So the copy and the call to action live behind `read === 'loaded'`. This is
77
+ * a required prop rather than a caller-side `if` because the caller-side `if`
78
+ * is exactly what every surface forgot; putting it here means a new list
79
+ * cannot render a zero-state without answering the question first.
80
+ *
81
+ * ## The degraded branch says only what a list can know (AGL-2486)
82
+ *
83
+ * It used to end "if the banner above asks you to sign in again, that fixes
84
+ * it". There is no banner above any more — a stale session opens the re-auth
85
+ * dialog directly — and pointing at it was already the weaker half of a
86
+ * message told twice. What survives is the part only this surface knows:
87
+ * THIS list is incomplete, and nothing has been deleted. The diagnosis stays
88
+ * where the evidence is; a list that guessed at it would be the AGL-1179
89
+ * mistake at the surface instead of in a log.
90
+ */
91
+ export declare function ReadGatedEmptyState({ iconPath, title, description, action, read, subject, onRetry, degradedAction, }: ReadGatedEmptyStateProps): import("react").JSX.Element;
92
+ export declare namespace ReadGatedEmptyState {
93
+ var displayName: string;
94
+ var aglyn: boolean;
95
+ }
96
+ export default ReadGatedEmptyState;
@@ -0,0 +1,138 @@
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
+ import { Alert, Box, Button, CircularProgress, Stack, Typography } from "@mui/material";
19
+ import CardDisplay from "./card-display.js";
20
+ import MdiIcon from "./mdi-icon/mdi-icon.js";
21
+ /**
22
+ * Reusable zero-state block: a centered icon, title, supporting copy and an
23
+ * optional call to action inside the standard `CardDisplay` framing. Use it
24
+ * wherever a list/grid can legitimately be empty (no sites yet, no org yet,
25
+ * empty media library) instead of leaving a blank content area.
26
+ *
27
+ * Distinct from {@link EmptyState} in this same library, which is the
28
+ * illustrated "nothing here" block for a panel that has no read behind it.
29
+ * This one is for a LIST, and the difference that matters is the gate.
30
+ *
31
+ * ## Lives here so a plugin can state the rule too (AGL-3080)
32
+ *
33
+ * It was the console's, at `apps/console/components/empty-state.component`,
34
+ * and the rule below is not an app's rule — the marketplace's own licence
35
+ * list asserts "this workspace holds no licenses" off the same three-valued
36
+ * evidence. A plugin may not import an app, so the choice was to move this or
37
+ * to let each plugin hand-roll a zero-state that forgets the gate, which is
38
+ * the defect AGL-1066 already found in every console surface that had one.
39
+ * The console keeps a wrapper at the old path that supplies
40
+ * {@link ReadGatedEmptyStateProps.degradedAction}, so its own call sites are
41
+ * unchanged.
42
+ *
43
+ * ## The zero-state is GATED, not merely offered (AGL-1066, AGL-1062)
44
+ *
45
+ * "No sites yet — Create a site to start building" is a statement of fact
46
+ * about someone's account, and it was reachable from a read that never
47
+ * reached the server: a stale session denies every server read while
48
+ * `persistentLocalCache` keeps listeners painting, so the list rendered, then
49
+ * emptied, then asserted the emptiness. On a page whose zero-state carries a
50
+ * **Create site** button that is not just wrong, it invites a customer to
51
+ * rebuild sites they still own.
52
+ *
53
+ * So the copy and the call to action live behind `read === 'loaded'`. This is
54
+ * a required prop rather than a caller-side `if` because the caller-side `if`
55
+ * is exactly what every surface forgot; putting it here means a new list
56
+ * cannot render a zero-state without answering the question first.
57
+ *
58
+ * ## The degraded branch says only what a list can know (AGL-2486)
59
+ *
60
+ * It used to end "if the banner above asks you to sign in again, that fixes
61
+ * it". There is no banner above any more — a stale session opens the re-auth
62
+ * dialog directly — and pointing at it was already the weaker half of a
63
+ * message told twice. What survives is the part only this surface knows:
64
+ * THIS list is incomplete, and nothing has been deleted. The diagnosis stays
65
+ * where the evidence is; a list that guessed at it would be the AGL-1179
66
+ * mistake at the surface instead of in a log.
67
+ */ export function ReadGatedEmptyState({ iconPath, title, description, action, read, subject = 'this list', onRetry, degradedAction }) {
68
+ // A read still in flight is not an answer. Rendering the frame with a
69
+ // spinner keeps the page from jumping when the rows arrive, and — more to
70
+ // the point — keeps the zero-state's sentence out of the load window.
71
+ if (read === 'loading') {
72
+ return /*#__PURE__*/ _jsx(CardDisplay, {
73
+ contentGutterX: true,
74
+ contentGutterY: true,
75
+ children: /*#__PURE__*/ _jsx(Box, {
76
+ sx: {
77
+ display: 'flex',
78
+ justifyContent: 'center',
79
+ py: 6
80
+ },
81
+ children: /*#__PURE__*/ _jsx(CircularProgress, {
82
+ "aria-label": `Loading ${subject}`
83
+ })
84
+ })
85
+ });
86
+ }
87
+ if (read === 'unavailable') {
88
+ return /*#__PURE__*/ _jsx(Alert, {
89
+ severity: "warning",
90
+ action: degradedAction != null ? degradedAction : onRetry ? /*#__PURE__*/ _jsx(Button, {
91
+ color: "inherit",
92
+ size: "small",
93
+ onClick: onRetry,
94
+ children: 'Try again'
95
+ }) : null,
96
+ children: `${subject.charAt(0).toUpperCase()}${subject.slice(1)} could not be ` + 'loaded, so this list is incomplete. Nothing has been deleted.'
97
+ });
98
+ }
99
+ return /*#__PURE__*/ _jsx(CardDisplay, {
100
+ contentGutterX: true,
101
+ contentGutterY: true,
102
+ children: /*#__PURE__*/ _jsxs(Stack, {
103
+ spacing: 2,
104
+ sx: {
105
+ alignItems: 'center',
106
+ textAlign: 'center',
107
+ py: 6,
108
+ px: 2
109
+ },
110
+ children: [
111
+ iconPath ? /*#__PURE__*/ _jsx(MdiIcon, {
112
+ color: "primary",
113
+ fontSize: "large",
114
+ path: iconPath
115
+ }) : null,
116
+ /*#__PURE__*/ _jsx(Typography, {
117
+ variant: "h6",
118
+ children: title
119
+ }),
120
+ description ? /*#__PURE__*/ _jsx(Typography, {
121
+ color: "textSecondary",
122
+ sx: {
123
+ maxWidth: 440
124
+ },
125
+ children: description
126
+ }) : null,
127
+ action ? /*#__PURE__*/ _jsx("div", {
128
+ children: action
129
+ }) : null
130
+ ]
131
+ })
132
+ });
133
+ }
134
+ ReadGatedEmptyState.displayName = 'ReadGatedEmptyState';
135
+ ReadGatedEmptyState.aglyn = true;
136
+ export default ReadGatedEmptyState;
137
+
138
+ //# sourceMappingURL=read-gated-empty-state.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/read-gated-empty-state.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'use client'\n\nimport { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material'\nimport type { ReactNode } from 'react'\nimport CardDisplay from './card-display'\nimport MdiIcon from './mdi-icon/mdi-icon'\nimport type { ReadOutcome } from '../utils/read-outcome'\n\nexport interface ReadGatedEmptyStateProps {\n /** MDI path (e.g. `ICON_VARIANT_HOST_GROUP.path`) shown above the title. */\n iconPath?: string\n /** Headline — the one-line \"what this is / what to do\" message. */\n title: ReactNode\n /** Supporting copy under the title; keep it to a sentence or two. */\n description?: ReactNode\n /** Primary call to action (usually a `<Button>`), rendered below the copy. */\n action?: ReactNode\n /**\n * Did the read behind this list actually succeed? REQUIRED, and\n * deliberately not defaulted (AGL-1066).\n *\n * A list is empty for three different reasons and only one of them is\n * \"there is nothing here\". Passing `loaded` is an assertion that the read\n * reached the server and came back with zero rows; anything else renders\n * the loading or degraded branch below and `title`/`description`/`action`\n * are never shown. See `utils/read-outcome`.\n */\n read: ReadOutcome\n /**\n * What could not be loaded, for the degraded copy — a lower-case noun\n * phrase in the customer's terms: `'your sites'`, `'your workspaces'`.\n */\n subject?: string\n /** Re-runs the failed read. Omit only when nothing can retry it. */\n onRetry?: () => void\n /**\n * An action for the degraded branch that OUTRANKS the retry button\n * (AGL-3080) — for a caller that knows retrying cannot work.\n *\n * The case it exists for is the console's stale session: every server read\n * is being refused, so \"Try again\" is an invitation to fail, and what the\n * reader needs is the way back into the sign-in dialog. Only the app knows\n * that, because only the app holds the session store — which is exactly why\n * this is a prop rather than a branch in here. A caller that passes nothing\n * gets the retry button, which is the right answer for every read whose\n * failure is not the session.\n */\n degradedAction?: ReactNode\n}\n\n/**\n * Reusable zero-state block: a centered icon, title, supporting copy and an\n * optional call to action inside the standard `CardDisplay` framing. Use it\n * wherever a list/grid can legitimately be empty (no sites yet, no org yet,\n * empty media library) instead of leaving a blank content area.\n *\n * Distinct from {@link EmptyState} in this same library, which is the\n * illustrated \"nothing here\" block for a panel that has no read behind it.\n * This one is for a LIST, and the difference that matters is the gate.\n *\n * ## Lives here so a plugin can state the rule too (AGL-3080)\n *\n * It was the console's, at `apps/console/components/empty-state.component`,\n * and the rule below is not an app's rule — the marketplace's own licence\n * list asserts \"this workspace holds no licenses\" off the same three-valued\n * evidence. A plugin may not import an app, so the choice was to move this or\n * to let each plugin hand-roll a zero-state that forgets the gate, which is\n * the defect AGL-1066 already found in every console surface that had one.\n * The console keeps a wrapper at the old path that supplies\n * {@link ReadGatedEmptyStateProps.degradedAction}, so its own call sites are\n * unchanged.\n *\n * ## The zero-state is GATED, not merely offered (AGL-1066, AGL-1062)\n *\n * \"No sites yet — Create a site to start building\" is a statement of fact\n * about someone's account, and it was reachable from a read that never\n * reached the server: a stale session denies every server read while\n * `persistentLocalCache` keeps listeners painting, so the list rendered, then\n * emptied, then asserted the emptiness. On a page whose zero-state carries a\n * **Create site** button that is not just wrong, it invites a customer to\n * rebuild sites they still own.\n *\n * So the copy and the call to action live behind `read === 'loaded'`. This is\n * a required prop rather than a caller-side `if` because the caller-side `if`\n * is exactly what every surface forgot; putting it here means a new list\n * cannot render a zero-state without answering the question first.\n *\n * ## The degraded branch says only what a list can know (AGL-2486)\n *\n * It used to end \"if the banner above asks you to sign in again, that fixes\n * it\". There is no banner above any more — a stale session opens the re-auth\n * dialog directly — and pointing at it was already the weaker half of a\n * message told twice. What survives is the part only this surface knows:\n * THIS list is incomplete, and nothing has been deleted. The diagnosis stays\n * where the evidence is; a list that guessed at it would be the AGL-1179\n * mistake at the surface instead of in a log.\n */\nexport function ReadGatedEmptyState({\n iconPath,\n title,\n description,\n action,\n read,\n subject = 'this list',\n onRetry,\n degradedAction,\n}: ReadGatedEmptyStateProps) {\n // A read still in flight is not an answer. Rendering the frame with a\n // spinner keeps the page from jumping when the rows arrive, and — more to\n // the point — keeps the zero-state's sentence out of the load window.\n if (read === 'loading') {\n return (\n <CardDisplay contentGutterX contentGutterY>\n <Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>\n <CircularProgress aria-label={`Loading ${subject}`} />\n </Box>\n </CardDisplay>\n )\n }\n\n if (read === 'unavailable') {\n return (\n <Alert\n severity=\"warning\"\n action={\n degradedAction ??\n (onRetry ? (\n <Button color=\"inherit\" size=\"small\" onClick={onRetry}>\n {'Try again'}\n </Button>\n ) : null)\n }\n >\n {`${subject.charAt(0).toUpperCase()}${subject.slice(1)} could not be ` +\n 'loaded, so this list is incomplete. Nothing has been deleted.'}\n </Alert>\n )\n }\n\n return (\n <CardDisplay contentGutterX contentGutterY>\n <Stack\n spacing={2}\n sx={{ alignItems: 'center', textAlign: 'center', py: 6, px: 2 }}\n >\n {iconPath ? (\n <MdiIcon color=\"primary\" fontSize=\"large\" path={iconPath} />\n ) : null}\n <Typography variant=\"h6\">{title}</Typography>\n {description ? (\n <Typography\n color=\"textSecondary\"\n sx={{ maxWidth: 440 }}\n >\n {description}\n </Typography>\n ) : null}\n {action ? <div>{action}</div> : null}\n </Stack>\n </CardDisplay>\n )\n}\n\nReadGatedEmptyState.displayName = 'ReadGatedEmptyState'\nReadGatedEmptyState.aglyn = true\n\nexport default ReadGatedEmptyState\n"],"names":["Alert","Box","Button","CircularProgress","Stack","Typography","CardDisplay","MdiIcon","ReadGatedEmptyState","iconPath","title","description","action","read","subject","onRetry","degradedAction","contentGutterX","contentGutterY","sx","display","justifyContent","py","aria-label","severity","color","size","onClick","charAt","toUpperCase","slice","spacing","alignItems","textAlign","px","fontSize","path","variant","maxWidth","div","displayName","aglyn"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,KAAK,EAAEC,GAAG,EAAEC,MAAM,EAAEC,gBAAgB,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAEvF,OAAOC,iBAAiB,oBAAgB;AACxC,OAAOC,aAAa,yBAAqB;AA6CzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8CC,GACD,OAAO,SAASC,oBAAoB,EAClCC,QAAQ,EACRC,KAAK,EACLC,WAAW,EACXC,MAAM,EACNC,IAAI,EACJC,UAAU,WAAW,EACrBC,OAAO,EACPC,cAAc,EACW;IACzB,sEAAsE;IACtE,0EAA0E;IAC1E,sEAAsE;IACtE,IAAIH,SAAS,WAAW;QACtB,qBACE,KAACP;YAAYW,cAAc;YAACC,cAAc;sBACxC,cAAA,KAACjB;gBAAIkB,IAAI;oBAAEC,SAAS;oBAAQC,gBAAgB;oBAAUC,IAAI;gBAAE;0BAC1D,cAAA,KAACnB;oBAAiBoB,cAAY,CAAC,QAAQ,EAAET,SAAS;;;;IAI1D;IAEA,IAAID,SAAS,eAAe;QAC1B,qBACE,KAACb;YACCwB,UAAS;YACTZ,MAAM,EACJI,yBAAAA,iBACCD,wBACC,KAACb;gBAAOuB,OAAM;gBAAUC,MAAK;gBAAQC,SAASZ;0BAC3C;iBAED;sBAGL,GAAGD,QAAQc,MAAM,CAAC,GAAGC,WAAW,KAAKf,QAAQgB,KAAK,CAAC,GAAG,cAAc,CAAC,GACpE;;IAGR;IAEA,qBACE,KAACxB;QAAYW,cAAc;QAACC,cAAc;kBACxC,cAAA,MAACd;YACC2B,SAAS;YACTZ,IAAI;gBAAEa,YAAY;gBAAUC,WAAW;gBAAUX,IAAI;gBAAGY,IAAI;YAAE;;gBAE7DzB,yBACC,KAACF;oBAAQkB,OAAM;oBAAUU,UAAS;oBAAQC,MAAM3B;qBAC9C;8BACJ,KAACJ;oBAAWgC,SAAQ;8BAAM3B;;gBACzBC,4BACC,KAACN;oBACCoB,OAAM;oBACNN,IAAI;wBAAEmB,UAAU;oBAAI;8BAEnB3B;qBAED;gBACHC,uBAAS,KAAC2B;8BAAK3B;qBAAgB;;;;AAIxC;AAEAJ,oBAAoBgC,WAAW,GAAG;AAClChC,oBAAoBiC,KAAK,GAAG;AAE5B,eAAejC,oBAAmB"}
@@ -0,0 +1,81 @@
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
+ * Did the read that produced this list actually SUCCEED? (AGL-1066)
19
+ *
20
+ * A zero-state is a claim about the customer's DATA — "you have no sites",
21
+ * "your library is empty". A refused or unfinished read supports no such
22
+ * claim; all it establishes is that we could not look. `media-library` states
23
+ * the rule at its own call site (AGL-1062): *"no media" is a claim about the
24
+ * library, and a failed read is a claim about us.* This module is that rule
25
+ * made shared, because the console had it in exactly one place and every
26
+ * other list surface reached the opposite conclusion from the same evidence.
27
+ *
28
+ * It lives in `shared-ui-jsx` rather than the console (AGL-3080) because a
29
+ * PLUGIN's list surface answers the same question and cannot import an app.
30
+ * There is no UI here and there never was — the rule is about evidence, not
31
+ * about pixels — so the only thing the move changes is who may state it.
32
+ *
33
+ * ## Why a three-valued type and not a boolean
34
+ *
35
+ * Because two of the three states produce an empty array, and only one of
36
+ * them licenses the sentence. `loading` and `unavailable` both look like
37
+ * `items.length === 0` from the outside, which is precisely how a load window
38
+ * and a dead session both rendered as "No sites yet".
39
+ *
40
+ * ## Why it must be passed EXPLICITLY, and never defaulted
41
+ *
42
+ * A default answers the question for a caller who never asked it. That is the
43
+ * AGL-1380 bug shape — a loading default that reads as data — and it is the
44
+ * one this type exists to make unrepresentable. `EmptyState` therefore takes
45
+ * `read` as a REQUIRED prop: a surface cannot render a zero-state without
46
+ * first stating, in code, that its read succeeded.
47
+ *
48
+ * ## Reading a hook's state
49
+ *
50
+ * The console's list hooks converged on `{ ready, error }` (`useOrgHosts`,
51
+ * `useOrgScope`), where `error` means "this read gave up", not "this read
52
+ * returned nothing". {@link readOutcome} is the one translation from that
53
+ * pair, so the precedence — an error outranks a pending read — is decided
54
+ * once instead of at every call site.
55
+ */
56
+ /**
57
+ * `loaded` is the ONLY value that licenses a zero-state. The other two mean
58
+ * "the list is empty because we do not know", which is a different sentence
59
+ * and a different UI.
60
+ */
61
+ export type ReadOutcome = 'loading' | 'loaded' | 'unavailable';
62
+ export interface ReadState {
63
+ /** The read settled — succeeded or gave up. */
64
+ ready: boolean;
65
+ /** The read gave up. Says nothing about what exists. */
66
+ error: boolean;
67
+ }
68
+ /**
69
+ * Translate a list hook's `{ ready, error }` into a {@link ReadOutcome}.
70
+ *
71
+ * `=== true` / `=== false` rather than truthiness on purpose:
72
+ * `strictNullChecks` is off repo-wide, so an `undefined` from a hook that has
73
+ * not adopted the pair yet must fall to `loading` — the conservative value —
74
+ * instead of silently reading as "loaded, and empty".
75
+ *
76
+ * `error` is checked first: a read that gave up is not still in flight, and a
77
+ * hook that latches `ready` alongside `error` (both of ours do) would
78
+ * otherwise be ambiguous.
79
+ */
80
+ export declare function readOutcome(state: Partial<ReadState> | undefined): ReadOutcome;
81
+ export default readOutcome;
@@ -0,0 +1,76 @@
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
+ * Did the read that produced this list actually SUCCEED? (AGL-1066)
18
+ *
19
+ * A zero-state is a claim about the customer's DATA — "you have no sites",
20
+ * "your library is empty". A refused or unfinished read supports no such
21
+ * claim; all it establishes is that we could not look. `media-library` states
22
+ * the rule at its own call site (AGL-1062): *"no media" is a claim about the
23
+ * library, and a failed read is a claim about us.* This module is that rule
24
+ * made shared, because the console had it in exactly one place and every
25
+ * other list surface reached the opposite conclusion from the same evidence.
26
+ *
27
+ * It lives in `shared-ui-jsx` rather than the console (AGL-3080) because a
28
+ * PLUGIN's list surface answers the same question and cannot import an app.
29
+ * There is no UI here and there never was — the rule is about evidence, not
30
+ * about pixels — so the only thing the move changes is who may state it.
31
+ *
32
+ * ## Why a three-valued type and not a boolean
33
+ *
34
+ * Because two of the three states produce an empty array, and only one of
35
+ * them licenses the sentence. `loading` and `unavailable` both look like
36
+ * `items.length === 0` from the outside, which is precisely how a load window
37
+ * and a dead session both rendered as "No sites yet".
38
+ *
39
+ * ## Why it must be passed EXPLICITLY, and never defaulted
40
+ *
41
+ * A default answers the question for a caller who never asked it. That is the
42
+ * AGL-1380 bug shape — a loading default that reads as data — and it is the
43
+ * one this type exists to make unrepresentable. `EmptyState` therefore takes
44
+ * `read` as a REQUIRED prop: a surface cannot render a zero-state without
45
+ * first stating, in code, that its read succeeded.
46
+ *
47
+ * ## Reading a hook's state
48
+ *
49
+ * The console's list hooks converged on `{ ready, error }` (`useOrgHosts`,
50
+ * `useOrgScope`), where `error` means "this read gave up", not "this read
51
+ * returned nothing". {@link readOutcome} is the one translation from that
52
+ * pair, so the precedence — an error outranks a pending read — is decided
53
+ * once instead of at every call site.
54
+ */ /**
55
+ * `loaded` is the ONLY value that licenses a zero-state. The other two mean
56
+ * "the list is empty because we do not know", which is a different sentence
57
+ * and a different UI.
58
+ */ /**
59
+ * Translate a list hook's `{ ready, error }` into a {@link ReadOutcome}.
60
+ *
61
+ * `=== true` / `=== false` rather than truthiness on purpose:
62
+ * `strictNullChecks` is off repo-wide, so an `undefined` from a hook that has
63
+ * not adopted the pair yet must fall to `loading` — the conservative value —
64
+ * instead of silently reading as "loaded, and empty".
65
+ *
66
+ * `error` is checked first: a read that gave up is not still in flight, and a
67
+ * hook that latches `ready` alongside `error` (both of ours do) would
68
+ * otherwise be ambiguous.
69
+ */ export function readOutcome(state) {
70
+ if ((state == null ? void 0 : state.error) === true) return 'unavailable';
71
+ if ((state == null ? void 0 : state.ready) === true) return 'loaded';
72
+ return 'loading';
73
+ }
74
+ export default readOutcome;
75
+
76
+ //# sourceMappingURL=read-outcome.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/utils/read-outcome.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\n/**\n * Did the read that produced this list actually SUCCEED? (AGL-1066)\n *\n * A zero-state is a claim about the customer's DATA — \"you have no sites\",\n * \"your library is empty\". A refused or unfinished read supports no such\n * claim; all it establishes is that we could not look. `media-library` states\n * the rule at its own call site (AGL-1062): *\"no media\" is a claim about the\n * library, and a failed read is a claim about us.* This module is that rule\n * made shared, because the console had it in exactly one place and every\n * other list surface reached the opposite conclusion from the same evidence.\n *\n * It lives in `shared-ui-jsx` rather than the console (AGL-3080) because a\n * PLUGIN's list surface answers the same question and cannot import an app.\n * There is no UI here and there never was — the rule is about evidence, not\n * about pixels — so the only thing the move changes is who may state it.\n *\n * ## Why a three-valued type and not a boolean\n *\n * Because two of the three states produce an empty array, and only one of\n * them licenses the sentence. `loading` and `unavailable` both look like\n * `items.length === 0` from the outside, which is precisely how a load window\n * and a dead session both rendered as \"No sites yet\".\n *\n * ## Why it must be passed EXPLICITLY, and never defaulted\n *\n * A default answers the question for a caller who never asked it. That is the\n * AGL-1380 bug shape — a loading default that reads as data — and it is the\n * one this type exists to make unrepresentable. `EmptyState` therefore takes\n * `read` as a REQUIRED prop: a surface cannot render a zero-state without\n * first stating, in code, that its read succeeded.\n *\n * ## Reading a hook's state\n *\n * The console's list hooks converged on `{ ready, error }` (`useOrgHosts`,\n * `useOrgScope`), where `error` means \"this read gave up\", not \"this read\n * returned nothing\". {@link readOutcome} is the one translation from that\n * pair, so the precedence — an error outranks a pending read — is decided\n * once instead of at every call site.\n */\n\n/**\n * `loaded` is the ONLY value that licenses a zero-state. The other two mean\n * \"the list is empty because we do not know\", which is a different sentence\n * and a different UI.\n */\nexport type ReadOutcome = 'loading' | 'loaded' | 'unavailable'\n\nexport interface ReadState {\n /** The read settled — succeeded or gave up. */\n ready: boolean\n /** The read gave up. Says nothing about what exists. */\n error: boolean\n}\n\n/**\n * Translate a list hook's `{ ready, error }` into a {@link ReadOutcome}.\n *\n * `=== true` / `=== false` rather than truthiness on purpose:\n * `strictNullChecks` is off repo-wide, so an `undefined` from a hook that has\n * not adopted the pair yet must fall to `loading` — the conservative value —\n * instead of silently reading as \"loaded, and empty\".\n *\n * `error` is checked first: a read that gave up is not still in flight, and a\n * hook that latches `ready` alongside `error` (both of ours do) would\n * otherwise be ambiguous.\n */\nexport function readOutcome(state: Partial<ReadState> | undefined): ReadOutcome {\n if (state?.error === true) return 'unavailable'\n if (state?.ready === true) return 'loaded'\n return 'loading'\n}\n\nexport default readOutcome\n"],"names":["readOutcome","state","error","ready"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCC,GAED;;;;CAIC,GAUD;;;;;;;;;;;CAWC,GACD,OAAO,SAASA,YAAYC,KAAqC;IAC/D,IAAIA,CAAAA,yBAAAA,MAAOC,KAAK,MAAK,MAAM,OAAO;IAClC,IAAID,CAAAA,yBAAAA,MAAOE,KAAK,MAAK,MAAM,OAAO;IAClC,OAAO;AACT;AAEA,eAAeH,YAAW"}