@jini-ai/admin 0.4.11 → 0.4.13

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.
Files changed (165) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/agent-plugins/controllers/agent-plugin-files.controller.js +14 -9
  3. package/dist/agent-plugins/controllers/agent-plugins.controller.js +12 -9
  4. package/dist/agent-plugins/package-file-tree.js +54 -51
  5. package/dist/agent-plugins/react/components/AgentPluginRow.d.ts +1 -1
  6. package/dist/agent-plugins/react/components/AgentPluginRow.js +9 -2
  7. package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.d.ts +5 -5
  8. package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.js +14 -5
  9. package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.d.ts +15 -13
  10. package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.js +54 -40
  11. package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.d.ts +13 -13
  12. package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.js +23 -11
  13. package/dist/browser/navigation.js +44 -27
  14. package/dist/comments/adapters/memory.js +30 -12
  15. package/dist/comments/conformance/comments-api.conformance.js +55 -33
  16. package/dist/comments/controllers/comment-queue.controller.js +13 -4
  17. package/dist/comments/react/hooks/use-comment-queue.hooks.d.ts +17 -2
  18. package/dist/comments/react/hooks/use-comment-queue.hooks.js +79 -30
  19. package/dist/comments/react/hooks/use-comment-settings.hooks.js +9 -5
  20. package/dist/comments/react/index.d.ts +2 -1
  21. package/dist/comments/react/index.js +1 -1
  22. package/dist/consent/react/ConsentFields.d.ts +35 -0
  23. package/dist/consent/react/ConsentFields.js +74 -0
  24. package/dist/consent/react/ConsentPreviewFrame.d.ts +14 -0
  25. package/dist/consent/react/ConsentPreviewFrame.js +14 -0
  26. package/dist/consent/react/ConsentSettingsPanel.d.ts +27 -4
  27. package/dist/consent/react/ConsentSettingsPanel.js +6 -17
  28. package/dist/consent/react/index.d.ts +6 -1
  29. package/dist/consent/react/index.js +2 -0
  30. package/dist/consent/react/settings-panel.hooks.d.ts +28 -1
  31. package/dist/consent/react/settings-panel.hooks.js +26 -0
  32. package/dist/core/entities/rules.js +126 -81
  33. package/dist/core/manifest/rules.js +10 -10
  34. package/dist/core/module/create-admin.js +195 -117
  35. package/dist/core/ports/redirects.d.ts +8 -6
  36. package/dist/core/ports/redirects.js +8 -6
  37. package/dist/core/ports/seo.d.ts +20 -0
  38. package/dist/core/routing/rules.js +9 -4
  39. package/dist/core/transport/http.js +37 -20
  40. package/dist/forms/adapters/memory.js +22 -11
  41. package/dist/forms/conformance/forms-api.conformance.d.ts +6 -4
  42. package/dist/forms/conformance/forms-api.conformance.js +42 -46
  43. package/dist/forms/field-settings.js +150 -115
  44. package/dist/forms/html-rules.js +45 -28
  45. package/dist/forms/react/components/FieldSettingsRows.d.ts +22 -0
  46. package/dist/forms/react/components/FieldSettingsRows.js +29 -0
  47. package/dist/forms/react/components/FieldSettingsSection.d.ts +1 -1
  48. package/dist/forms/react/components/FieldSettingsSection.js +5 -6
  49. package/dist/forms/react/components/FormEditorFieldsBody.d.ts +1 -1
  50. package/dist/forms/react/components/FormEditorFieldsBody.js +4 -15
  51. package/dist/forms/react/components/FormEditorFieldsBodyParts.d.ts +18 -0
  52. package/dist/forms/react/components/FormEditorFieldsBodyParts.js +32 -0
  53. package/dist/forms/react/components/FormSubmissions.d.ts +1 -1
  54. package/dist/forms/react/components/FormSubmissions.js +4 -20
  55. package/dist/forms/react/components/FormSubmissionsTable.d.ts +19 -0
  56. package/dist/forms/react/components/FormSubmissionsTable.js +22 -0
  57. package/dist/media/adapters/memory.js +31 -26
  58. package/dist/media/controllers/edit-media.controller.js +2 -1
  59. package/dist/media/controllers/library.controller.js +48 -35
  60. package/dist/media/controllers/providers.controller.js +2 -1
  61. package/dist/media/html-attributes.js +29 -15
  62. package/dist/media/index.d.ts +1 -1
  63. package/dist/media/index.js +1 -1
  64. package/dist/media/react/components/MediaIcons.d.ts +0 -3
  65. package/dist/media/react/components/MediaIcons.js +0 -9
  66. package/dist/media/react/components/MediaLightbox.d.ts +2 -1
  67. package/dist/media/react/components/MediaLightbox.js +11 -5
  68. package/dist/media/react/hooks/LibraryTab.hooks.d.ts +55 -45
  69. package/dist/media/react/hooks/LibraryTab.hooks.js +107 -70
  70. package/dist/media/react/hooks/MediaCard.hooks.d.ts +27 -31
  71. package/dist/media/react/hooks/MediaCard.hooks.js +52 -26
  72. package/dist/media/react/hooks/MediaGrid.hooks.d.ts +1 -1
  73. package/dist/media/react/hooks/MediaLightbox.hooks.d.ts +10 -14
  74. package/dist/media/react/hooks/MediaLightbox.hooks.js +9 -56
  75. package/dist/media/react/hooks/MediaPage.hooks.d.ts +6 -0
  76. package/dist/media/react/hooks/MediaPage.hooks.js +29 -12
  77. package/dist/media/react/hooks/ProvidersTab.hooks.d.ts +33 -33
  78. package/dist/media/react/hooks/ProvidersTab.hooks.js +119 -79
  79. package/dist/media/react/hooks/UploadButton.hooks.d.ts +7 -5
  80. package/dist/media/react/hooks/UploadButton.hooks.js +36 -24
  81. package/dist/media/react/index.js +4 -0
  82. package/dist/media/react/tabs/LibraryTab.d.ts +1 -1
  83. package/dist/media/react/tabs/LibraryTab.js +2 -2
  84. package/dist/media/rules.d.ts +7 -0
  85. package/dist/media/rules.js +25 -2
  86. package/dist/react/components/ConfirmDialog/ConfirmDialog.js +5 -8
  87. package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.d.ts +20 -0
  88. package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.js +24 -0
  89. package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +16 -7
  90. package/dist/react/components/RowMenu/RowMenu.js +16 -10
  91. package/dist/react/components/Sidebar.js +32 -26
  92. package/dist/react/entities/EntityEdit.js +17 -14
  93. package/dist/react/entities/EntityField.d.ts +5 -2
  94. package/dist/react/entities/EntityField.js +38 -23
  95. package/dist/react/entities/EntityList.js +6 -5
  96. package/dist/react/entities/RelationTruncationNotice.d.ts +9 -0
  97. package/dist/react/entities/RelationTruncationNotice.js +9 -0
  98. package/dist/react/entities/use-entity-edit.d.ts +4 -2
  99. package/dist/react/entities/use-entity-edit.js +52 -33
  100. package/dist/react/shell/AdminShell.js +40 -23
  101. package/dist/react/shell/model.d.ts +4 -1
  102. package/dist/react/shell/model.js +3 -2
  103. package/dist/react/shell/use-admin-shell-session.js +25 -26
  104. package/dist/redirects/adapters/memory.js +9 -2
  105. package/dist/redirects/conformance/redirects-api.conformance.js +66 -36
  106. package/dist/redirects/react/hooks/use-import-redirects-form.hooks.d.ts +1 -1
  107. package/dist/redirects/react/hooks/use-import-redirects-form.hooks.js +10 -3
  108. package/dist/redirects/react/hooks/wired.hooks.d.ts +1 -1
  109. package/dist/security/adapters/http.js +25 -10
  110. package/dist/security/adapters/memory.js +19 -5
  111. package/dist/security/controllers/access-tokens.controller.js +27 -26
  112. package/dist/security/controllers/other-credentials.controller.js +4 -2
  113. package/dist/security/controllers/root-key.controller.js +17 -9
  114. package/dist/security/react/hooks/AccessTokensTab.hooks.d.ts +40 -38
  115. package/dist/security/react/hooks/AccessTokensTab.hooks.js +36 -9
  116. package/dist/security/react/hooks/RootKeyTab.hooks.d.ts +14 -14
  117. package/dist/security/react/hooks/RootKeyTab.hooks.js +12 -3
  118. package/dist/security/react/tabs/RootKeyTab.d.ts +1 -1
  119. package/dist/security/react/tabs/RootKeyTab.js +12 -3
  120. package/dist/security/rules.js +45 -25
  121. package/dist/seo/adapters/http.js +5 -0
  122. package/dist/seo/controllers/seo.controller.js +4 -2
  123. package/dist/seo/messages.en.d.ts +15 -10
  124. package/dist/seo/messages.en.js +15 -10
  125. package/dist/seo/models.d.ts +11 -0
  126. package/dist/seo/react/components/MediaRefField.d.ts +17 -5
  127. package/dist/seo/react/components/MediaRefField.js +31 -4
  128. package/dist/seo/react/hooks/MediaRefField.hooks.js +12 -2
  129. package/dist/seo/react/hooks/SeoPage.hooks.d.ts +1 -0
  130. package/dist/seo/react/hooks/SeoPage.hooks.js +2 -2
  131. package/dist/seo/react/hooks/SeoQuery.hooks.d.ts +1 -0
  132. package/dist/seo/react/hooks/SeoQuery.hooks.js +1 -0
  133. package/dist/seo/react/hooks/use-entry-picker.hooks.js +7 -2
  134. package/dist/seo/react/hooks/use-seo-entry-panel.hooks.js +13 -2
  135. package/dist/seo/react/hooks/use-share-image-warning.hooks.d.ts +4 -1
  136. package/dist/seo/react/hooks/use-share-image-warning.hooks.js +48 -33
  137. package/dist/seo/react/index.d.ts +45 -30
  138. package/dist/seo/react/pages/SeoPage.d.ts +6 -4
  139. package/dist/seo/react/pages/SeoPage.js +51 -29
  140. package/dist/seo/rules.d.ts +91 -0
  141. package/dist/seo/rules.js +161 -6
  142. package/dist/seo/seo.module.d.ts +15 -10
  143. package/dist/source-control/conformance/source-control-api.conformance.js +68 -38
  144. package/dist/source-control/controllers/source-control.controller.js +20 -8
  145. package/dist/source-control/react/hooks/ProvidersTab.hooks.d.ts +2 -1
  146. package/dist/source-control/react/hooks/ProvidersTab.hooks.js +18 -11
  147. package/dist/widgets/conformance/widgets-api.conformance.js +15 -1
  148. package/dist/widgets/controllers/instance-editor.controller.js +33 -24
  149. package/dist/widgets/controllers/library.controller.js +18 -12
  150. package/dist/widgets/controllers/region-editor.controller.js +6 -5
  151. package/dist/widgets/react/hooks/page-views.hooks.d.ts +1 -0
  152. package/dist/widgets/react/hooks/page-views.hooks.js +3 -2
  153. package/dist/widgets/react/hooks/use-widget-regions.hooks.d.ts +1 -1
  154. package/dist/widgets/react/hooks/use-widget-regions.hooks.js +9 -3
  155. package/dist/widgets/react/hooks/use-widgets-library.hooks.js +9 -2
  156. package/dist/widgets/react/pages/WidgetInstanceEditor.d.ts +1 -1
  157. package/dist/widgets/react/pages/WidgetInstanceEditor.js +7 -8
  158. package/dist/widgets/rules.d.ts +14 -2
  159. package/dist/widgets/rules.js +9 -6
  160. package/package.json +19 -10
  161. package/src/themes/default/admin.css +51 -0
  162. package/src/themes/default/chat.css +21 -0
  163. package/src/themes/default/manifest.json +29 -0
  164. package/dist/media/styles.d.ts +0 -4
  165. package/dist/media/styles.js +0 -5
@@ -1,7 +1,7 @@
1
1
  import { useWiredMediaRefField } from "../hooks/MediaRefField.hooks.js";
2
2
  /**
3
- * @file `MediaRefField` — a text input for an `{assetId}:{transformName}` media reference (or a
4
- * pasted absolute URL), plus a "Choose image" affordance that opens the existing
3
+ * @file `MediaRefField` — an `{assetId}:{transformName}` media reference (or an absolute URL),
4
+ * shown as the chosen file's thumbnail and name, plus a "Choose image" affordance that opens the existing
5
5
  * `MediaPickerDialog`, a thumbnail preview of the current value, and a Remove control.
6
6
  *
7
7
  * Built for `react/pages/SeoPage.tsx`'s `defaultOgImage`/`ogImage`/`twitterImage` fields (SPEC intent: "make the OG
@@ -16,11 +16,15 @@ import { useWiredMediaRefField } from "../hooks/MediaRefField.hooks.js";
16
16
  * 2026-10-08 polish: the original editable-input rationale above still applies to pasted URLs
17
17
  * and empty fields. A selected library reference now shows plain copy with its preview; Remove
18
18
  * returns to manual entry. A hidden input preserves the exact FormData value on defaults saves.
19
+ * 2026-10-15: superseded — the input is removed entirely (owner: the read-only box "looks
20
+ * useless"). An existing pasted URL still displays (by its file name) and still saves unchanged;
21
+ * a new URL can no longer be typed here, only chosen from the library.
19
22
  */
20
23
  export interface MediaRefFieldProps {
21
24
  locale: string;
22
- /** `<input id>`/`<label htmlFor>` pairing — same `getByLabelText`-friendly shape `react/pages/SeoPage.tsx`'s
23
- * other explicit-`htmlFor` fields already use. */
25
+ /** Base id: the caption is `{id}-label` and names the row's `role="group"`; the hidden input (when
26
+ * `name` is set) carries the id itself. Was an `<input id>`/`<label htmlFor>` pair until the text
27
+ * box was removed (2026-10-15). */
24
28
  id: string;
25
29
  /** Forwarded to the `<input name>` only when set — `defaultOgImage` needs this (its `<form>` reads
26
30
  * `FormData` by name on submit); the per-entry `ogImage`/`twitterImage` fields do not (they save
@@ -32,9 +36,17 @@ export interface MediaRefFieldProps {
32
36
  /** This field's own base handle — appended `-choose`/`-clear`/`-dialog` for its own controls, and
33
37
  * forwarded as-is to the `<input>` itself. Omit to leave every element here untagged. */
34
38
  agentHandle?: string;
39
+ /** Shown in the file slot while no image is set — for a field that inherits another field's
40
+ * image when empty (the X image falls back to the social preview image). */
41
+ emptyHint?: string;
42
+ /** The value comes from the site default or page content, with no SEO override of its own: the file
43
+ * shows muted as "Using site default: …" or "Using image from page content: …"; size and Remove
44
+ * are hidden — there is no SEO override to remove. "Choose image" is how an override starts
45
+ * (owner, 2026-10-15). */
46
+ inherited?: false | 'default' | 'content';
35
47
  /** Injectable seam for the picker's own state — same convention as `MediaPickerDialog`'s own
36
48
  * `useDialog` prop. Defaults to the real {@link useWiredMediaRefField}. */
37
49
  useField?: typeof useWiredMediaRefField;
38
50
  }
39
- export declare function MediaRefField({ locale, id, name, label, value, onChange, agentHandle: base, useField, }: MediaRefFieldProps, _optional?: Record<string, never>): import("react").JSX.Element;
51
+ export declare function MediaRefField({ locale, id, name, label, value, onChange, agentHandle: base, emptyHint, inherited, useField, }: MediaRefFieldProps): import("react").JSX.Element;
40
52
  //# sourceMappingURL=MediaRefField.d.ts.map
@@ -1,12 +1,39 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { agentHandle } from "@jini-ai/agentic";
3
+ import { interpolate } from "@jini-ai/ui/i18n";
3
4
  import { MediaPickerBridge } from "./MediaPickerBridge.js";
4
5
  import { useSeoCopy } from "../hooks/SeoPorts.hooks.js";
5
6
  import { useWiredMediaRefField, useMediaRefPresentation, handleSpread } from "../hooks/MediaRefField.hooks.js";
6
- export function MediaRefField({ locale, id, name, label, value, onChange, agentHandle: base, useField = useWiredMediaRefField, }, _optional = {}) {
7
+ export function MediaRefField({ locale, id, name, label, value, onChange, agentHandle: base, emptyHint, inherited = false, useField = useWiredMediaRefField, }) {
7
8
  const t = useSeoCopy();
8
- const { Picker = MediaPickerBridge, storedReference } = useMediaRefPresentation({ value });
9
- const { pickerOpen, openPicker, closePicker, handleSelect, clear, previewUrl, accept, warning, onPreviewLoad, onPreviewError } = useField({ value, onChange });
10
- return (_jsxs("div", { className: "jini-field jini-media-ref-field", children: [_jsx("label", { className: "jini-field-label", htmlFor: id, children: label }), _jsxs("div", { className: "jini-media-ref-field-row", children: [_jsx("input", { className: "jini-input", id: id, name: storedReference ? undefined : name, value: storedReference ? t({ locale, key: "Selected image" }) : value, readOnly: storedReference, "aria-describedby": warning ? `${id}-warning` : undefined, onChange: (e) => onChange(e.target.value), ...(base ? agentHandle({ handle: base }, { role: "field", label }) : {}) }), storedReference && name ? _jsx("input", { type: "hidden", name: name, value: value }) : null, _jsx("button", { type: "button", className: "jini-btn jini-btn-secondary", onClick: openPicker, ...handleSpread({ ...(base === undefined ? {} : { base }), suffix: "choose", label: `Choose an image for: ${label}` }), children: t({ locale: locale, key: "Choose image" }) }), value ? (_jsx("button", { type: "button", className: "jini-btn jini-btn-secondary", onClick: clear, ...handleSpread({ ...(base === undefined ? {} : { base }), suffix: "clear", label: `Remove the selected image for: ${label}` }), children: t({ locale: locale, key: "Remove" }) })) : null] }), previewUrl ? _jsx("img", { className: "jini-media-ref-field-preview", src: previewUrl, alt: `${label} preview`, onLoad: onPreviewLoad, onError: onPreviewError }, previewUrl) : null, warning ? _jsx("p", { id: `${id}-warning`, className: "jini-notice jini-notice-warning", role: "status", children: warning }) : null, pickerOpen ? (_jsx(Picker, { onSelect: handleSelect, onCancel: closePicker, accept: accept, agentHandle: base ? `${base}-dialog` : undefined })) : null] }));
9
+ const { Picker = MediaPickerBridge } = useMediaRefPresentation({ value });
10
+ const { pickerOpen, openPicker, closePicker, handleSelect, clear, previewUrl, file, accept, onPreviewLoad, onPreviewError } = useField({ value, onChange });
11
+ const labelId = `${id}-label`;
12
+ /* One compact row: thumbnail, the file's name and size, then Choose/Remove kept together in their
13
+ own group so a narrow column wraps the pair as one unit instead of stranding Remove on a line of
14
+ its own (owner, 2026-10-15). The thumbnail used to sit under the row at 4rem, repeated per field.
15
+ `warning` (share-image size/byte/HTTP diagnostics) is still computed by the hook but no longer
16
+ painted: the owner called the yellow callout under every image "completely useless".
17
+ 2026-10-15 (round 3): the text box is gone — for a library pick it only ever said a read-only
18
+ "Selected image" ("i can't change it and looks useless"), so the row names the file instead and
19
+ the picker is the one way in. The exact value still rides a hidden input for FormData saves. */
20
+ return (_jsxs("div", { className: "jini-field jini-media-ref-field", children: [_jsx("span", { className: "jini-field-label", id: labelId, children: label }), _jsxs("div", { className: "jini-media-ref-field-row", role: "group", "aria-labelledby": labelId, ...(base ? agentHandle({ handle: base }, { role: "region", label }) : {}), children: [_jsx(MediaRefFile, { file: file, emptyHint: emptyHint, label: label, locale: locale, t: t, inherited: inherited, previewUrl: previewUrl, onPreviewLoad: onPreviewLoad, onPreviewError: onPreviewError }), name ? _jsx("input", { type: "hidden", id: id, name: name, value: value }) : null, _jsx(MediaRefActions, { label: label, locale: locale, t: t, base: base, value: value, inherited: inherited, openPicker: openPicker, clear: clear })] }), pickerOpen ? (_jsx(Picker, { onSelect: handleSelect, onCancel: closePicker, accept: accept, agentHandle: base ? `${base}-dialog` : undefined })) : null] }));
21
+ }
22
+ /** The chosen file: thumbnail, name (prefixed with its source when inherited) and, for an own
23
+ * override, its pixel size. With no file, the field's empty hint (if any) takes the slot. */
24
+ function MediaRefFile({ file, emptyHint, label, locale, t, inherited, previewUrl, onPreviewLoad, onPreviewError }) {
25
+ if (!file)
26
+ return emptyHint ? _jsx("span", { className: "jini-media-ref-field-empty", children: emptyHint }) : null;
27
+ return (_jsxs("span", { className: inherited ? "jini-media-ref-field-file jini-media-ref-field-file-inherited" : "jini-media-ref-field-file", children: [previewUrl ? _jsx("img", { className: "jini-media-ref-field-preview", src: previewUrl, alt: `${label} preview`, onLoad: onPreviewLoad, onError: onPreviewError }, previewUrl) : null, _jsx("span", { className: "jini-media-ref-field-name", title: file.name, children: inherited ? inheritedFileName({ name: file.name, inherited, locale, t }) : file.name }), file.size && !inherited ? _jsx("span", { className: "jini-media-ref-field-size", children: file.size }) : null] }));
28
+ }
29
+ /** "Using site default: {name}" or "Using image from page content: {name}", translated. */
30
+ function inheritedFileName({ name, inherited, locale, t }, _optional = {}) {
31
+ const key = inherited === "content" ? "Using image from page content: {name}" : "Using site default: {name}";
32
+ return interpolate({ template: t({ locale, key, vars: { name } }), vars: { name } });
33
+ }
34
+ /** Choose (always) and Remove (only for an own value), grouped so they wrap as one unit. */
35
+ function MediaRefActions({ label, locale, t, base, value, inherited, openPicker, clear }) {
36
+ const handleBase = base === undefined ? {} : { base };
37
+ return (_jsxs("span", { className: "jini-media-ref-field-actions", children: [_jsx("button", { type: "button", className: "jini-btn jini-btn-secondary", onClick: openPicker, ...handleSpread({ ...handleBase, suffix: "choose", label: `Choose an image for: ${label}` }), children: t({ locale, key: "Choose image" }) }), value && !inherited ? (_jsx("button", { type: "button", className: "jini-btn jini-btn-secondary", onClick: clear, ...handleSpread({ ...handleBase, suffix: "clear", label: `Remove the selected image for: ${label}` }), children: t({ locale, key: "Remove" }) })) : null] }));
11
38
  }
12
39
  //# sourceMappingURL=MediaRefField.js.map
@@ -3,16 +3,26 @@ import { useShareImageWarning } from './use-share-image-warning.hooks.js';
3
3
  import { agentHandle } from '@jini-ai/agentic';
4
4
  import { useController } from '../../../core/react/use-controller.js';
5
5
  import { createMediaRefController } from '../../controllers/media-ref.controller.js';
6
- import { isStoredMediaRef, resolveMediaRefPreviewUrl } from '../../rules.js';
6
+ import { describeMediaRef, isStoredMediaRef, resolveMediaRefPreviewUrl } from '../../rules.js';
7
+ import { useFetchQuery } from '@jini-ai/ui/fetch-query';
8
+ import { extensionForMimeType } from '@jini-ai/ui';
9
+ import { SEO_KEYS } from './SeoQuery.hooks.js';
7
10
  import { useSeoPorts, useSeoOptions } from './SeoPorts.hooks.js';
8
11
  export function useMediaRefField({ value, onChange, api }, { t } = {}) {
9
12
  const change = useRef(onChange);
10
13
  change.current = onChange;
11
14
  const { controller, snapshot } = useController({ create: () => createMediaRefController({ onChange: value => change.current(value) }), dependencies: [] });
12
15
  const previewUrl = api.mediaPreviewUrl ? api.mediaPreviewUrl({ value }) : resolveMediaRefPreviewUrl({ value, mediaOriginalUrl: id => api.mediaOriginalUrl({ id }) });
13
- const diagnostics = useShareImageWarning({ value, previewUrl, api }, { t });
16
+ const { naturalWidth, naturalHeight, ...diagnostics } = useShareImageWarning({ value, previewUrl, api }, { t });
17
+ // One cached library read shared by every image field on the screen; only a library reference
18
+ // needs it (a URL names itself), and a host without `listSeoMedia` falls back to the slug.
19
+ const list = api.listSeoMedia;
20
+ // `fetch` runs only while the query is enabled, which requires `list`: the fetch-query adapters
21
+ // never call a disabled read's fetch (not on mount, focus or prefix invalidation).
22
+ const media = useFetchQuery({ key: SEO_KEYS.media, fetch: () => list.call(api, {}) }, { enabled: Boolean(list) && isStoredMediaRef({ value }) });
14
23
  return {
15
24
  ...diagnostics,
25
+ file: describeMediaRef({ value, media: media.data }, { width: naturalWidth, height: naturalHeight, t, extensionFor: extensionForMimeType }),
16
26
  pickerOpen: snapshot?.pickerOpen ?? false,
17
27
  openPicker: () => controller?.openPicker({}), closePicker: () => controller?.closePicker({}),
18
28
  handleSelect: item => controller?.handleSelect({ item }), clear: () => controller?.clear({}),
@@ -29,6 +29,7 @@ export declare function useAnalyzePanel({ analysis }: {
29
29
  analysis: SeoEntryAnalysis;
30
30
  }, _optional?: Record<string, never>): {
31
31
  sortedIssues: import("../../../core/index.js").AdminSeoIssue[];
32
+ tone: "failure" | "warning" | "success";
32
33
  };
33
34
  /** All seven defaults remain in one FormData submission; tabs never split this form. */
34
35
  export declare function useSeoDefaults({ save }: {
@@ -1,6 +1,6 @@
1
1
  import { createElement } from 'react';
2
2
  import { SeoDefaultsIcon, SeoSitemapIcon, SeoEntriesIcon } from '../components/SeoIcons.js';
3
- import { buildSeoSettingsPatch, resolveSeoTabId, sortIssuesBySeverity } from '../../rules.js';
3
+ import { buildSeoSettingsPatch, resolveSeoTabId, sortIssuesBySeverity, scoreTone } from '../../rules.js';
4
4
  import { useSeoOptions } from './SeoPorts.hooks.js';
5
5
  export { buildSeoSettingsPatch, resolveSeoTabId } from '../../rules.js';
6
6
  export function resolveSeoTabs(_required = {}, { t = key => key } = {}) {
@@ -20,7 +20,7 @@ export function useSeoTabs({ tabId }, _optional = {}) {
20
20
  return { activeTabId: resolveSeoTabId({ tabId }), tabs: resolveSeoTabs({}, { t }) };
21
21
  }
22
22
  export function useAnalyzePanel({ analysis }, _optional = {}) {
23
- return { sortedIssues: sortIssuesBySeverity({ issues: analysis.issues }) };
23
+ return { sortedIssues: sortIssuesBySeverity({ issues: analysis.issues }), tone: scoreTone({ score: analysis.score }) };
24
24
  }
25
25
  /** All seven defaults remain in one FormData submission; tabs never split this form. */
26
26
  export function useSeoDefaults({ save }, _optional = {}) {
@@ -8,6 +8,7 @@ export declare const SEO_KEYS: {
8
8
  }, _optional?: Record<string, never>) => readonly ["seo", "entry", string];
9
9
  choices: readonly ["seo", "choices"];
10
10
  sitemap: readonly ["seo", "sitemap"];
11
+ media: readonly ["seo", "media"];
11
12
  };
12
13
  /** Controllers keep their draft/error contracts; these injected ports share the provider cache.
13
14
  * Reads omit a controller's abort signal because another mounted consumer can own the same read.
@@ -7,6 +7,7 @@ export const SEO_KEYS = {
7
7
  entry: ({ entryId }, _optional = {}) => ['seo', 'entry', entryId],
8
8
  choices: ['seo', 'choices'],
9
9
  sitemap: ['seo', 'sitemap'],
10
+ media: ['seo', 'media'],
10
11
  };
11
12
  /** Controllers keep their draft/error contracts; these injected ports share the provider cache.
12
13
  * Reads omit a controller's abort signal because another mounted consumer can own the same read.
@@ -2,13 +2,18 @@ import { describeSeoError } from '../../models.js';
2
2
  import { useFetchQuery } from '@jini-ai/ui/fetch-query';
3
3
  import { SEO_KEYS } from './SeoQuery.hooks.js';
4
4
  import { initialEntryPickerState } from '../../models.js';
5
+ import { entryPickerOptions } from '../../rules.js';
5
6
  import { useSeoOptions, useSeoPorts } from './SeoPorts.hooks.js';
6
7
  export function useEntryPicker({ api }, { t, describeError } = {}) {
7
8
  const query = useFetchQuery({ key: SEO_KEYS.choices, fetch: async () => {
8
9
  const [posts, pages] = await Promise.all([api.listSeoPosts({}), api.listSeoPages({})]);
9
- return [...posts, ...pages];
10
+ return { posts, pages };
10
11
  } });
11
- return { ...initialEntryPickerState, entries: query.data ?? null, error: query.error ? (describeError ?? describeSeoError)({ error: query.error, fallback: t?.('failed to load entries') ?? 'failed to load entries' }) : null };
12
+ const data = query.data;
13
+ return { ...initialEntryPickerState,
14
+ entries: data ? [...data.posts, ...data.pages] : null,
15
+ options: data ? entryPickerOptions(data, { t }) : null,
16
+ error: query.error ? (describeError ?? describeSeoError)({ error: query.error, fallback: t?.('failed to load entries') ?? 'failed to load entries' }) : null };
12
17
  }
13
18
  export function useWiredEntryPicker(_required = {}, _optional = {}) {
14
19
  const { seoApi: api } = useSeoPorts();
@@ -1,9 +1,11 @@
1
1
  import { describeSeoError } from '../../models.js';
2
2
  import { useEffect, useRef } from 'react';
3
- import { useSeoEntryQuery } from './SeoQuery.hooks.js';
3
+ import { useSeoEntryQuery, SEO_KEYS } from './SeoQuery.hooks.js';
4
+ import { useFetchQuery } from '@jini-ai/ui/fetch-query';
4
5
  import { useController } from '../../../core/react/use-controller.js';
5
6
  import { createSeoEntryController } from '../../controllers/seo-entry.controller.js';
6
7
  import { initialSeoEntryState } from '../../models.js';
8
+ import { inheritedXFor, ogImageInherited } from '../../rules.js';
7
9
  import { useSeoOptions, useSeoPorts } from './SeoPorts.hooks.js';
8
10
  export function useSeoEntryPanel({ api, entryId }, { t, describeError } = {}) {
9
11
  const cached = useSeoEntryQuery({ api, entryId });
@@ -17,11 +19,20 @@ export function useSeoEntryPanel({ api, entryId }, { t, describeError } = {}) {
17
19
  if (controller && cached.query.data)
18
20
  controller.set({ patch: cached.query.data });
19
21
  }, [controller, cached.query.data]);
22
+ // The site defaults read the Site defaults tab already caches; only the default image is used here.
23
+ const settings = useFetchQuery({ key: SEO_KEYS.settings, fetch: () => api.getSeoSettings({}) });
20
24
  const state = snapshot ?? initialSeoEntryState;
21
25
  return {
22
26
  ...state,
27
+ ogImageInherited: state.resolved ? ogImageInherited({ meta: state.resolved, defaultOgImage: settings.data?.defaultOgImage, touched: state.touched }) : false,
23
28
  fieldValue: (key, resolvedValue) => controller ? controller.fieldValue({ key, resolvedValue }) : resolvedValue,
24
- setField: (key, value) => controller?.setField({ key, value }),
29
+ setField: (key, value) => {
30
+ // An edit to the social preview also clears an X field that was following it (rules.ts).
31
+ const xKey = state.resolved ? inheritedXFor({ key, meta: state.resolved, touched: state.touched }) : null;
32
+ controller?.setField({ key, value });
33
+ if (xKey)
34
+ controller?.setField({ key: xKey, value: '' });
35
+ },
25
36
  save: async () => { await controller?.save({}); },
26
37
  };
27
38
  }
@@ -15,5 +15,8 @@ export declare function useShareImageWarning({ value, previewUrl, api }: {
15
15
  api: AdminSeoPort;
16
16
  }, { t }?: {
17
17
  t?: SeoTranslator | undefined;
18
- }): Pick<MediaRefFieldController, 'warning' | 'onPreviewLoad' | 'onPreviewError'>;
18
+ }): Pick<MediaRefFieldController, 'warning' | 'onPreviewLoad' | 'onPreviewError'> & {
19
+ naturalWidth?: number | undefined;
20
+ naturalHeight?: number | undefined;
21
+ };
19
22
  //# sourceMappingURL=use-share-image-warning.hooks.d.ts.map
@@ -17,20 +17,8 @@ export function useShareImageWarning({ value, previewUrl, api }, { t = key => ke
17
17
  const [inspection, setInspection] = useState(null);
18
18
  useEffect(() => {
19
19
  const inspect = api.inspectShareImage;
20
- if (!previewUrl || !inspect)
20
+ if (!previewUrl || !inspect || !isBrowserSameOrigin({ url: previewUrl }))
21
21
  return;
22
- // Browser origin, rather than the public site's alias: a cross-origin HEAD cannot establish
23
- // that an otherwise loadable external image is missing when the server omits CORS headers.
24
- const location = globalThis.location;
25
- if (!location)
26
- return;
27
- try {
28
- if (new URL(previewUrl, location.href).origin !== location.origin)
29
- return;
30
- }
31
- catch {
32
- return;
33
- }
34
22
  const controller = new AbortController();
35
23
  let active = true;
36
24
  const timeout = setTimeout(() => {
@@ -39,17 +27,8 @@ export function useShareImageWarning({ value, previewUrl, api }, { t = key => ke
39
27
  setInspection({ url: previewUrl, error: { key: 'The share image check timed out after 10 seconds.' } });
40
28
  }, 10_000);
41
29
  inspect.call(api, { url: previewUrl }, { signal: controller.signal }).then(({ status, byteSize }) => {
42
- if (!active)
43
- return;
44
- if (status === 404 || status === 410) {
45
- setInspection({ url: previewUrl, error: { key: 'The share image is missing (HTTP {status}).', vars: { status } } });
46
- }
47
- else if (status < 200 || status >= 300) {
48
- setInspection({ url: previewUrl, error: { key: 'The share image could not be checked (HTTP {status}).', vars: { status } } });
49
- }
50
- else {
51
- setInspection({ url: previewUrl, byteSize });
52
- }
30
+ if (active)
31
+ setInspection(inspectionFromResponse({ url: previewUrl, status, byteSize }));
53
32
  }).catch((error) => {
54
33
  if (!active)
55
34
  return;
@@ -58,22 +37,58 @@ export function useShareImageWarning({ value, previewUrl, api }, { t = key => ke
58
37
  }).finally(() => { clearTimeout(timeout); });
59
38
  return () => { active = false; clearTimeout(timeout); controller.abort(); };
60
39
  }, [previewUrl, api]);
61
- const source = decoded?.url === previewUrl ? decoded : null;
62
- const checked = inspection?.url === previewUrl ? inspection : null;
63
- const error = checked?.error;
40
+ const source = stateForUrl({ state: decoded, url: previewUrl });
41
+ const checked = stateForUrl({ state: inspection, url: previewUrl });
64
42
  return {
65
- warning: shareImageWarning({ value, previewUrl, width: source?.width, height: source?.height, previewFailed: source?.failed, byteSize: checked?.byteSize,
66
- error: error ? interpolate({ template: t(error.key, error.vars), vars: error.vars ?? {} }) : undefined }, { t }),
43
+ naturalWidth: source.width, naturalHeight: source.height,
44
+ warning: shareImageWarning({ value, previewUrl, width: source.width, height: source.height, previewFailed: source.failed, byteSize: checked.byteSize,
45
+ error: translatedInspectionError({ error: checked.error }, { t }) }, { t }),
67
46
  onPreviewLoad: ({ currentTarget }) => {
68
- const url = currentTarget.getAttribute('src');
69
- if (url && url === currentUrl.current)
47
+ const url = currentPreviewSrc({ image: currentTarget, currentUrl: currentUrl.current });
48
+ if (url)
70
49
  setDecoded({ url, width: currentTarget.naturalWidth, height: currentTarget.naturalHeight });
71
50
  },
72
51
  onPreviewError: ({ currentTarget }) => {
73
- const url = currentTarget.getAttribute('src');
74
- if (url && url === currentUrl.current)
52
+ const url = currentPreviewSrc({ image: currentTarget, currentUrl: currentUrl.current });
53
+ if (url)
75
54
  setDecoded({ url, failed: true });
76
55
  },
77
56
  };
78
57
  }
58
+ /** URL-keyed state for the current preview only; a prior choice's state reads as nothing known. */
59
+ function stateForUrl({ state, url }, _optional = {}) {
60
+ return state?.url === url ? state : {};
61
+ }
62
+ /** Whether `url` resolves to the browser's own origin; `false` outside a browser or for an unparseable URL. */
63
+ function isBrowserSameOrigin({ url }, _optional = {}) {
64
+ // Browser origin, rather than the public site's alias: a cross-origin HEAD cannot establish
65
+ // that an otherwise loadable external image is missing when the server omits CORS headers.
66
+ const location = globalThis.location;
67
+ if (!location)
68
+ return false;
69
+ try {
70
+ return new URL(url, location.href).origin === location.origin;
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ }
76
+ /** A HEAD response as inspection state: 404/410 is a missing file, any other non-2xx an unchecked
77
+ * one, and a 2xx carries the reported byte size. */
78
+ function inspectionFromResponse({ url, status, byteSize }, _optional = {}) {
79
+ if (status === 404 || status === 410)
80
+ return { url, error: { key: 'The share image is missing (HTTP {status}).', vars: { status } } };
81
+ if (status < 200 || status >= 300)
82
+ return { url, error: { key: 'The share image could not be checked (HTTP {status}).', vars: { status } } };
83
+ return { url, byteSize };
84
+ }
85
+ /** The inspection error's copy, translated at render so a language switch re-renders it. */
86
+ function translatedInspectionError({ error }, { t }) {
87
+ return error ? interpolate({ template: t(error.key, error.vars), vars: error.vars ?? {} }) : undefined;
88
+ }
89
+ /** The preview image's `src` when it is still the current selection's; `null` for a stale event. */
90
+ function currentPreviewSrc({ image, currentUrl }, _optional = {}) {
91
+ const url = image.getAttribute('src');
92
+ return url && url === currentUrl ? url : null;
93
+ }
79
94
  //# sourceMappingURL=use-share-image-warning.hooks.js.map
@@ -36,21 +36,24 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
36
36
  readonly "Canonical URL": "Canonical URL";
37
37
  readonly Noindex: "Noindex";
38
38
  readonly Nofollow: "Nofollow";
39
- readonly "OG title": "OG title";
40
- readonly "OG description": "OG description";
41
- readonly "OG image": "OG image";
42
- readonly "Twitter title": "Twitter title";
43
- readonly "Twitter description": "Twitter description";
44
- readonly "Twitter image": "Twitter image";
39
+ readonly "Preview title": "Preview title";
40
+ readonly "Preview description": "Preview description";
41
+ readonly "Preview image": "Preview image";
42
+ readonly "X/Twitter title": "X/Twitter title";
43
+ readonly "X/Twitter description": "X/Twitter description";
44
+ readonly "X/Twitter image": "X/Twitter image";
45
+ readonly "Customize for X/Twitter (optional)": "Customize for X/Twitter (optional)";
46
+ readonly "Same as the preview image": "Same as the preview image";
47
+ readonly "Used by Facebook, LinkedIn, Slack, Discord, iMessage, WhatsApp and X/Twitter": "Used by Facebook, LinkedIn, Slack, Discord, iMessage, WhatsApp and X/Twitter";
45
48
  readonly "Saving\u2026": "Saving…";
46
49
  readonly "Save overrides": "Save overrides";
47
50
  readonly "Per-entry SEO": "Per-entry SEO";
48
51
  readonly "Search appearance": "Search appearance";
49
52
  readonly "Title template (must contain %s)": "Title template (must contain %s)";
50
53
  readonly "Default meta description": "Default meta description";
51
- readonly "Social sharing": "Social sharing";
52
- readonly "Default Open Graph / Twitter image": "Default Open Graph / Twitter image";
53
- readonly "Twitter @site handle": "Twitter @site handle";
54
+ readonly "Social preview": "Social preview";
55
+ readonly "Default preview image": "Default preview image";
56
+ readonly "X/Twitter @handle": "X/Twitter @handle";
54
57
  readonly Crawling: "Crawling";
55
58
  readonly "Default noindex": "Default noindex";
56
59
  readonly "Default nofollow": "Default nofollow";
@@ -63,7 +66,7 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
63
66
  readonly "View sitemap": "View sitemap";
64
67
  readonly "Loading SEO settings\u2026": "Loading SEO settings…";
65
68
  readonly Marketing: "Marketing";
66
- readonly "Site-wide defaults for meta titles, descriptions, Open Graph/Twitter cards, and robots directives.": "Site-wide defaults for meta titles, descriptions, Open Graph/Twitter cards, and robots directives.";
69
+ readonly "Site-wide defaults for meta titles, descriptions, social previews, and robots directives.": "Site-wide defaults for meta titles, descriptions, social previews, and robots directives.";
67
70
  readonly "Choose image": "Choose image";
68
71
  readonly Remove: "Remove";
69
72
  readonly "Sitemap \u00B7 {count} URLs": "Sitemap · {count} URLs";
@@ -91,6 +94,8 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
91
94
  readonly "failed to load the sitemap": "failed to load the sitemap";
92
95
  readonly "Could not load the sitemap (HTTP {status}).": "Could not load the sitemap (HTTP {status}).";
93
96
  readonly "Selected image": "Selected image";
97
+ readonly "Using site default: {name}": "Using site default: {name}";
98
+ readonly "Using image from page content: {name}": "Using image from page content: {name}";
94
99
  };
95
100
  pages: {
96
101
  readonly settings: {
@@ -158,21 +163,24 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
158
163
  readonly "Canonical URL": "Canonical URL";
159
164
  readonly Noindex: "Noindex";
160
165
  readonly Nofollow: "Nofollow";
161
- readonly "OG title": "OG title";
162
- readonly "OG description": "OG description";
163
- readonly "OG image": "OG image";
164
- readonly "Twitter title": "Twitter title";
165
- readonly "Twitter description": "Twitter description";
166
- readonly "Twitter image": "Twitter image";
166
+ readonly "Preview title": "Preview title";
167
+ readonly "Preview description": "Preview description";
168
+ readonly "Preview image": "Preview image";
169
+ readonly "X/Twitter title": "X/Twitter title";
170
+ readonly "X/Twitter description": "X/Twitter description";
171
+ readonly "X/Twitter image": "X/Twitter image";
172
+ readonly "Customize for X/Twitter (optional)": "Customize for X/Twitter (optional)";
173
+ readonly "Same as the preview image": "Same as the preview image";
174
+ readonly "Used by Facebook, LinkedIn, Slack, Discord, iMessage, WhatsApp and X/Twitter": "Used by Facebook, LinkedIn, Slack, Discord, iMessage, WhatsApp and X/Twitter";
167
175
  readonly "Saving\u2026": "Saving…";
168
176
  readonly "Save overrides": "Save overrides";
169
177
  readonly "Per-entry SEO": "Per-entry SEO";
170
178
  readonly "Search appearance": "Search appearance";
171
179
  readonly "Title template (must contain %s)": "Title template (must contain %s)";
172
180
  readonly "Default meta description": "Default meta description";
173
- readonly "Social sharing": "Social sharing";
174
- readonly "Default Open Graph / Twitter image": "Default Open Graph / Twitter image";
175
- readonly "Twitter @site handle": "Twitter @site handle";
181
+ readonly "Social preview": "Social preview";
182
+ readonly "Default preview image": "Default preview image";
183
+ readonly "X/Twitter @handle": "X/Twitter @handle";
176
184
  readonly Crawling: "Crawling";
177
185
  readonly "Default noindex": "Default noindex";
178
186
  readonly "Default nofollow": "Default nofollow";
@@ -185,7 +193,7 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
185
193
  readonly "View sitemap": "View sitemap";
186
194
  readonly "Loading SEO settings\u2026": "Loading SEO settings…";
187
195
  readonly Marketing: "Marketing";
188
- readonly "Site-wide defaults for meta titles, descriptions, Open Graph/Twitter cards, and robots directives.": "Site-wide defaults for meta titles, descriptions, Open Graph/Twitter cards, and robots directives.";
196
+ readonly "Site-wide defaults for meta titles, descriptions, social previews, and robots directives.": "Site-wide defaults for meta titles, descriptions, social previews, and robots directives.";
189
197
  readonly "Choose image": "Choose image";
190
198
  readonly Remove: "Remove";
191
199
  readonly "Sitemap \u00B7 {count} URLs": "Sitemap · {count} URLs";
@@ -213,6 +221,8 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
213
221
  readonly "failed to load the sitemap": "failed to load the sitemap";
214
222
  readonly "Could not load the sitemap (HTTP {status}).": "Could not load the sitemap (HTTP {status}).";
215
223
  readonly "Selected image": "Selected image";
224
+ readonly "Using site default: {name}": "Using site default: {name}";
225
+ readonly "Using image from page content: {name}": "Using image from page content: {name}";
216
226
  };
217
227
  readonly pages: {
218
228
  readonly settings: {
@@ -281,21 +291,24 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
281
291
  readonly "Canonical URL": "Canonical URL";
282
292
  readonly Noindex: "Noindex";
283
293
  readonly Nofollow: "Nofollow";
284
- readonly "OG title": "OG title";
285
- readonly "OG description": "OG description";
286
- readonly "OG image": "OG image";
287
- readonly "Twitter title": "Twitter title";
288
- readonly "Twitter description": "Twitter description";
289
- readonly "Twitter image": "Twitter image";
294
+ readonly "Preview title": "Preview title";
295
+ readonly "Preview description": "Preview description";
296
+ readonly "Preview image": "Preview image";
297
+ readonly "X/Twitter title": "X/Twitter title";
298
+ readonly "X/Twitter description": "X/Twitter description";
299
+ readonly "X/Twitter image": "X/Twitter image";
300
+ readonly "Customize for X/Twitter (optional)": "Customize for X/Twitter (optional)";
301
+ readonly "Same as the preview image": "Same as the preview image";
302
+ readonly "Used by Facebook, LinkedIn, Slack, Discord, iMessage, WhatsApp and X/Twitter": "Used by Facebook, LinkedIn, Slack, Discord, iMessage, WhatsApp and X/Twitter";
290
303
  readonly "Saving\u2026": "Saving…";
291
304
  readonly "Save overrides": "Save overrides";
292
305
  readonly "Per-entry SEO": "Per-entry SEO";
293
306
  readonly "Search appearance": "Search appearance";
294
307
  readonly "Title template (must contain %s)": "Title template (must contain %s)";
295
308
  readonly "Default meta description": "Default meta description";
296
- readonly "Social sharing": "Social sharing";
297
- readonly "Default Open Graph / Twitter image": "Default Open Graph / Twitter image";
298
- readonly "Twitter @site handle": "Twitter @site handle";
309
+ readonly "Social preview": "Social preview";
310
+ readonly "Default preview image": "Default preview image";
311
+ readonly "X/Twitter @handle": "X/Twitter @handle";
299
312
  readonly Crawling: "Crawling";
300
313
  readonly "Default noindex": "Default noindex";
301
314
  readonly "Default nofollow": "Default nofollow";
@@ -308,7 +321,7 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
308
321
  readonly "View sitemap": "View sitemap";
309
322
  readonly "Loading SEO settings\u2026": "Loading SEO settings…";
310
323
  readonly Marketing: "Marketing";
311
- readonly "Site-wide defaults for meta titles, descriptions, Open Graph/Twitter cards, and robots directives.": "Site-wide defaults for meta titles, descriptions, Open Graph/Twitter cards, and robots directives.";
324
+ readonly "Site-wide defaults for meta titles, descriptions, social previews, and robots directives.": "Site-wide defaults for meta titles, descriptions, social previews, and robots directives.";
312
325
  readonly "Choose image": "Choose image";
313
326
  readonly Remove: "Remove";
314
327
  readonly "Sitemap \u00B7 {count} URLs": "Sitemap · {count} URLs";
@@ -336,6 +349,8 @@ export declare function seo({ siteUrl }: Pick<SeoReactOptions, 'siteUrl'>, optio
336
349
  readonly "failed to load the sitemap": "failed to load the sitemap";
337
350
  readonly "Could not load the sitemap (HTTP {status}).": "Could not load the sitemap (HTTP {status}).";
338
351
  readonly "Selected image": "Selected image";
352
+ readonly "Using site default: {name}": "Using site default: {name}";
353
+ readonly "Using image from page content: {name}": "Using image from page content: {name}";
339
354
  };
340
355
  readonly pages: {
341
356
  readonly settings: {
@@ -65,6 +65,8 @@ import { useWiredSeo } from "../hooks/use-seo.hooks.js";
65
65
  */
66
66
  export interface SeoSectionProps {
67
67
  title: string;
68
+ /** Optional ⓘ explanation beside the title (explanations are tips, never loose paragraphs). */
69
+ tip?: string;
68
70
  children: ReactNode;
69
71
  }
70
72
  export interface EntryPickerProps {
@@ -94,7 +96,7 @@ export interface SeoEntrySectionProps {
94
96
  * and nothing else — the exact regression `deployment/HistoryTab.tsx`'s own comment records
95
97
  * measuring live on its tab. It is also not a duplicate: the tab reads "Pages & posts" (what you
96
98
  * pick from), the heading reads "Per-entry SEO" (what you are editing). */
97
- export declare function SeoEntrySection({ locale, useSeoEntrySectionHook }: SeoEntrySectionProps, _optional?: Record<string, never>): import("react").JSX.Element;
99
+ export declare function SeoEntrySection({ locale, useSeoEntrySectionHook }: SeoEntrySectionProps): import("react").JSX.Element;
98
100
  /** What the two site-wide tab panels need from `useWiredSeo()`. Named as one object rather than
99
101
  * passed as six loose props for the same reason `Roles.tsx`'s `RoleCreateFormController` documents:
100
102
  * none of these is meaningful without the others, so naming the group is what lets a test hand a
@@ -116,7 +118,7 @@ export declare function SeoEntrySection({ locale, useSeoEntrySectionHook }: SeoE
116
118
  */
117
119
  export declare function SeoDefaultsTab({ controller }: {
118
120
  controller: SeoTabController;
119
- }, _optional?: Record<string, never>): import("react").JSX.Element;
121
+ }): import("react").JSX.Element;
120
122
  /**
121
123
  * The "Sitemap" tab — the two sitemap actions, moved here verbatim, plus one line this screen never
122
124
  * had: whether a sitemap is being published at all.
@@ -136,7 +138,7 @@ export declare function SeoDefaultsTab({ controller }: {
136
138
  */
137
139
  export declare function SeoSitemapTab({ controller }: {
138
140
  controller: SeoTabController;
139
- }, _optional?: Record<string, never>): import("react").JSX.Element;
141
+ }): import("react").JSX.Element;
140
142
  /** Dispatches the one active tab's panel as a flat if-chain — a plain function rather than a
141
143
  * ternary written directly in `Seo`'s JSX, which would be counted against that component's own
142
144
  * complexity. Same split `Deployment.tsx`'s `deploymentTabPanel` and `Sites.tsx`'s `sitesTabPanel`
@@ -155,7 +157,7 @@ export interface SeoProps {
155
157
  tab: string;
156
158
  }, optional?: Record<string, never>) => void) | undefined;
157
159
  }
158
- export declare function Seo({ tabId, onTabChange, useSeoHook }?: SeoProps, _optional?: Record<string, never>): import("react").JSX.Element;
160
+ export declare function Seo({ tabId, onTabChange, useSeoHook }?: SeoProps): import("react").JSX.Element;
159
161
  export { Seo as SeoPage };
160
162
  export default Seo;
161
163
  //# sourceMappingURL=SeoPage.d.ts.map