@jini-ai/admin 0.4.7 → 0.4.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.9 — 2026-10-13
4
+
5
+ - Sitemap modal: Regenerate no longer reads `/sitemap.xml` twice. The regenerate mutation already invalidates the cached sitemap read (since 1c5d476f), so the modal's extra refetch is gone; the modal still shows the fresh XML after a successful regenerate and keeps the old XML after a failed one.
6
+
7
+ ## 0.4.8 — 2026-10-13
8
+
9
+ - Share-image warning checks decoded size against 1200×630 (width and height independently; was width ≥ 600). New copy key: `The share image is only {height} px tall; use an image at least 630 px tall.`; the width key now says 1200 px.
10
+ - SEO rules import `interpolate` from the universal `@jini-ai/ui/i18n`, so the SEO entries stay React-free. UI peer floor ^0.5.1.
11
+
3
12
  ## 0.4.7 — 2026-10-13
4
13
 
5
14
  - Accept UI ^0.5.0 for the chart contract extraction.
@@ -57,9 +57,9 @@ export interface SeoController {
57
57
  defaultOgImage: string;
58
58
  setDefaultOgImage: (value: string) => void;
59
59
  /** Resolves `true` on success, `false` on a caught failure (`error` is set either way this
60
- * already did) — `SitemapModal.tsx`'s own footer Regenerate button uses the boolean to decide
61
- * whether to refetch `/sitemap.xml`, so a failed attempt never re-fetches the same stale
62
- * content. The pre-existing caller, `react/pages/SeoPage.tsx`'s own Regenerate button, still just fires this as
60
+ * already did). Success invalidates the cached `/sitemap.xml` read, so `SitemapModal.tsx`'s
61
+ * footer Regenerate button shows fresh XML without its own refetch; a failed attempt leaves
62
+ * the viewed XML alone. The pre-existing caller, `react/pages/SeoPage.tsx`'s own Regenerate button, still just fires this as
63
63
  * a plain `onClick` and discards the return value — this is a widening, not a breaking change. */
64
64
  regenerateSitemap: () => Promise<boolean>;
65
65
  /** Whether `SitemapModal.tsx` ("View sitemap") is open. `react/pages/SeoPage.tsx` stays markup-only (this file's
@@ -136,9 +136,8 @@ export interface SitemapModalController {
136
136
  setFilter: (value: string) => void;
137
137
  view: SitemapModalView;
138
138
  setView: (view: SitemapModalView) => void;
139
- /** Re-runs the fetch — `SitemapModal.tsx`'s footer Regenerate button calls this after a
140
- * successful regenerate, so the count/table/raw view all reflect the freshly rebuilt sitemap
141
- * without the operator closing and reopening the modal. */
139
+ /** Re-runs the fetch. Retained for the hook ABI; after a successful regenerate the mutation's
140
+ * cache invalidation already refreshes the count/table/raw view, so the modal no longer calls it. */
142
141
  refetch: () => void;
143
142
  }
144
143
  export interface MediaRefFieldController {
@@ -12,9 +12,8 @@ import { useWiredSitemapModal } from "../hooks/use-sitemap-modal.hooks.js";
12
12
  * single scrolling region between fixed header and footer.
13
13
  *
14
14
  * Domain state (fetch, parse, filter, view toggle) lives in `hooks/use-sitemap-modal
15
- * .hooks.ts`; this file is markup only — the one bit of sequencing that belongs to neither that
16
- * hook nor the outer `useSeo()` (refetching after a successful regenerate) lives in its own
17
- * colocated `react/hooks/SitemapModal.hooks.ts`, see that file's header for why it isn't folded into either.
15
+ * .hooks.ts`; this file is markup only. The outer `useSeo()` mutation owns regeneration and
16
+ * cache invalidation; `react/hooks/SitemapModal.hooks.ts` forwards that action without another read.
18
17
  *
19
18
  * ## Agent handles
20
19
  * Every interactive element carries its own `agentHandle` (grep `agentHandle(` in `react/pages/SeoPage.tsx` for
@@ -34,9 +33,8 @@ export interface SitemapModalProps {
34
33
  /** `saving` from `useSeo()` — shared with the outer page's own Save/Regenerate buttons, same as
35
34
  * today's single-flag convention (`react/pages/SeoPage.tsx`'s existing `disabled={saving}` on both). */
36
35
  regenerating: boolean;
37
- /** `regenerateSitemap` from `useSeo()`. Resolves `true` on success, `false` on a caught failure —
38
- * `handleRegenerate` below only refetches on `true`, matching REQ 7 ("after a SUCCESSFUL
39
- * regenerate it refetches"). */
36
+ /** `regenerateSitemap` from `useSeo()`. Its mutation invalidates the sitemap cache on
37
+ * success; a caught failure returns `false` and leaves the viewed XML alone. */
40
38
  onRegenerate: () => Promise<boolean>;
41
39
  onClose: () => void;
42
40
  /** Injectable seam for the modal's fetch/parse/filter/view state. Defaults to the real
@@ -62,7 +62,7 @@ export function SitemapModal({ locale, sitemapEnabled, regenerating, onRegenerat
62
62
  const t = useSeoCopy();
63
63
  const modal = useModal({ enabled: sitemapEnabled, onClose });
64
64
  const { title } = useSitemapPresentation({ locale, modal });
65
- const { handleRegenerate } = useSitemapModalRegenerate({ onRegenerate, refetch: modal.refetch });
65
+ const { handleRegenerate } = useSitemapModalRegenerate({ onRegenerate });
66
66
  // Native modality keeps Tab from reaching the background, as MediaPickerDialog now does too.
67
67
  return (_jsxs(Dialog, { open: true, title: title, onClose: () => onClose(), className: "settings-dialog tovu-domain-dialog sitemap-modal", children: [_jsx("div", { className: "sitemap-modal-header", children: _jsx(SitemapModalHeaderActions, { locale: locale, modal: modal }) }), _jsx("div", { className: "sitemap-modal-body", children: _jsx(SitemapModalBody, { locale: locale, sitemapEnabled: sitemapEnabled, modal: modal }) }), _jsx("div", { className: "widget-picker-footer", children: _jsxs("span", { className: "editor-actions", children: [_jsx("button", { type: "button", className: "btn-secondary", onClick: handleRegenerate, disabled: regenerating, ...agentHandle({ handle: "seo-sitemap-modal-regenerate" }, {
68
68
  role: "button",
@@ -1,10 +1,13 @@
1
1
  import type { SitemapModalController } from '../../models.js';
2
- /** Refetch only after accepted regeneration; a failed write must leave the viewed XML alone. */
3
- export declare function useSitemapModalRegenerate({ onRegenerate, refetch }: {
2
+ /** The regeneration mutation owns cache invalidation; forwarding it must not start a second
3
+ * XML read. Failed writes leave the viewed XML alone.
4
+ * @returns The mutation callback as the modal's click handler; no additional I/O.
5
+ * @complexity O(1).
6
+ */
7
+ export declare function useSitemapModalRegenerate({ onRegenerate }: {
4
8
  onRegenerate: () => Promise<boolean>;
5
- refetch: () => void;
6
9
  }, _optional?: Record<string, never>): {
7
- handleRegenerate: () => Promise<void>;
10
+ handleRegenerate: () => Promise<boolean>;
8
11
  };
9
12
  export declare function useSitemapTable({ modal }: {
10
13
  modal: SitemapModalController;
@@ -1,9 +1,12 @@
1
1
  import { useSeoCopy, useSeoOptions } from './SeoPorts.hooks.js';
2
2
  import { buildAgentListHandles } from '@jini-ai/agentic';
3
- /** Refetch only after accepted regeneration; a failed write must leave the viewed XML alone. */
4
- export function useSitemapModalRegenerate({ onRegenerate, refetch }, _optional = {}) {
5
- return { handleRegenerate: async () => { if (await onRegenerate())
6
- refetch(); } };
3
+ /** The regeneration mutation owns cache invalidation; forwarding it must not start a second
4
+ * XML read. Failed writes leave the viewed XML alone.
5
+ * @returns The mutation callback as the modal's click handler; no additional I/O.
6
+ * @complexity O(1).
7
+ */
8
+ export function useSitemapModalRegenerate({ onRegenerate }, _optional = {}) {
9
+ return { handleRegenerate: onRegenerate };
7
10
  }
8
11
  export function useSitemapTable({ modal }, _optional = {}) {
9
12
  return { rowHandles: buildAgentListHandles({ prefix: 'seo-sitemap-row', ids: modal.filteredEntries.map(entry => entry.loc) }) };
@@ -1,10 +1,10 @@
1
1
  import type { AdminSeoPort } from '../../ports.js';
2
2
  import type { MediaRefFieldController, SeoTranslator } from '../../models.js';
3
- /** Probe original file size through the host port; decoded width comes from the existing preview.
3
+ /** Probe original file size through the host port; decoded dimensions come from the existing preview.
4
4
  * Unknown size is not zero. URL-keyed state and cancellation prevent a prior choice's response
5
5
  * or image event from warning against the current selection. The probe has a 10-second bound.
6
6
  * External HEAD responses need CORS, so only same-origin files are probed; decoded previews
7
- * still report external loading failures and known widths. HTTP copy is translated at render.
7
+ * still report external loading failures and known dimensions. HTTP copy is translated at render.
8
8
  * @param required Controlled reference, source preview URL and injected SEO API.
9
9
  * @returns Non-blocking warning and the preview's native load/error handlers.
10
10
  * @complexity O(1) state and one HEAD per selection; no image bytes are downloaded by the probe.
@@ -1,11 +1,11 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
- import { interpolate } from '@jini-ai/ui/panel-kit';
2
+ import { interpolate } from '@jini-ai/ui/i18n';
3
3
  import { shareImageWarning } from '../../rules.js';
4
- /** Probe original file size through the host port; decoded width comes from the existing preview.
4
+ /** Probe original file size through the host port; decoded dimensions come from the existing preview.
5
5
  * Unknown size is not zero. URL-keyed state and cancellation prevent a prior choice's response
6
6
  * or image event from warning against the current selection. The probe has a 10-second bound.
7
7
  * External HEAD responses need CORS, so only same-origin files are probed; decoded previews
8
- * still report external loading failures and known widths. HTTP copy is translated at render.
8
+ * still report external loading failures and known dimensions. HTTP copy is translated at render.
9
9
  * @param required Controlled reference, source preview URL and injected SEO API.
10
10
  * @returns Non-blocking warning and the preview's native load/error handlers.
11
11
  * @complexity O(1) state and one HEAD per selection; no image bytes are downloaded by the probe.
@@ -62,12 +62,12 @@ export function useShareImageWarning({ value, previewUrl, api }, { t = key => ke
62
62
  const checked = inspection?.url === previewUrl ? inspection : null;
63
63
  const error = checked?.error;
64
64
  return {
65
- warning: shareImageWarning({ value, previewUrl, width: source?.width, previewFailed: source?.failed, byteSize: checked?.byteSize,
65
+ warning: shareImageWarning({ value, previewUrl, width: source?.width, height: source?.height, previewFailed: source?.failed, byteSize: checked?.byteSize,
66
66
  error: error ? interpolate({ template: t(error.key, error.vars), vars: error.vars ?? {} }) : undefined }, { t }),
67
67
  onPreviewLoad: ({ currentTarget }) => {
68
68
  const url = currentTarget.getAttribute('src');
69
69
  if (url && url === currentUrl.current)
70
- setDecoded({ url, width: currentTarget.naturalWidth });
70
+ setDecoded({ url, width: currentTarget.naturalWidth, height: currentTarget.naturalHeight });
71
71
  },
72
72
  onPreviewError: ({ currentTarget }) => {
73
73
  const url = currentTarget.getAttribute('src');
@@ -121,14 +121,15 @@ export declare function resolveMediaRefPreviewUrl({ value, mediaOriginalUrl }: {
121
121
  siteOrigin?: string | undefined;
122
122
  }): string | null;
123
123
  /** Diagnostic policy: unknown measurements stay unknown; measured failures name their reason.
124
- * @param required Controlled field, decoded preview width and original HTTP metadata.
124
+ * @param required Controlled field, decoded preview dimensions and original HTTP metadata.
125
125
  * @returns A non-blocking explanation, or null for an image without a measured problem.
126
126
  * @complexity O(n) in value/error length; fixed-count checks, no I/O.
127
127
  */
128
- export declare function shareImageWarning({ value, previewUrl, width, byteSize, error, previewFailed }: {
128
+ export declare function shareImageWarning({ value, previewUrl, width, height, byteSize, error, previewFailed }: {
129
129
  value: string;
130
130
  previewUrl: string | null;
131
131
  width?: number | undefined;
132
+ height?: number | undefined;
132
133
  byteSize?: number | null | undefined;
133
134
  error?: string | null | undefined;
134
135
  previewFailed?: boolean | undefined;
package/dist/seo/rules.js CHANGED
@@ -1,4 +1,4 @@
1
- import { interpolate } from '@jini-ai/ui/panel-kit';
1
+ import { interpolate } from '@jini-ai/ui/i18n';
2
2
  /**
3
3
  * @file Pure logic for the `seo` feature — everything that computes a value rather than rendering
4
4
  * one.
@@ -180,11 +180,11 @@ function localRenditionAssetKey({ value, siteOrigin }, _optional = {}) {
180
180
  }
181
181
  }
182
182
  /** Diagnostic policy: unknown measurements stay unknown; measured failures name their reason.
183
- * @param required Controlled field, decoded preview width and original HTTP metadata.
183
+ * @param required Controlled field, decoded preview dimensions and original HTTP metadata.
184
184
  * @returns A non-blocking explanation, or null for an image without a measured problem.
185
185
  * @complexity O(n) in value/error length; fixed-count checks, no I/O.
186
186
  */
187
- export function shareImageWarning({ value, previewUrl, width, byteSize, error, previewFailed }, { t = key => key } = {}) {
187
+ export function shareImageWarning({ value, previewUrl, width, height, byteSize, error, previewFailed }, { t = key => key } = {}) {
188
188
  if (typeof value !== 'string' || !value.trim())
189
189
  return t('No share image is selected. Link previews may show no picture.');
190
190
  if (!previewUrl)
@@ -194,8 +194,10 @@ export function shareImageWarning({ value, previewUrl, width, byteSize, error, p
194
194
  if (previewFailed)
195
195
  return t('The share image could not be loaded. Choose an available image.');
196
196
  const reasons = [];
197
- if (width !== undefined && width < 600)
198
- reasons.push(interpolate({ template: t('The share image is only {width} px wide; use an image at least 600 px wide.', { width }), vars: { width } }));
197
+ if (width !== undefined && width < 1200)
198
+ reasons.push(interpolate({ template: t('The share image is only {width} px wide; use an image at least 1200 px wide.', { width }), vars: { width } }));
199
+ if (height !== undefined && height < 630)
200
+ reasons.push(interpolate({ template: t('The share image is only {height} px tall; use an image at least 630 px tall.', { height }), vars: { height } }));
199
201
  if (byteSize !== undefined && byteSize !== null && byteSize < 5120)
200
202
  reasons.push(interpolate({ template: t('The share image is only {byteSize} bytes; use an image of at least 5 KB (5120 bytes).', { byteSize }), vars: { byteSize } }));
201
203
  return reasons.length ? reasons.join(' ') : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/admin",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Composable admin surface for Jini-hosted products: framework-free contracts, panel registry, and route model in `/core`; React shell and panels in `/react`. Import only the layers you need.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -434,7 +434,7 @@
434
434
  "react": "^18.3.0 || ^19.0.0",
435
435
  "react-dom": "^18.3.0 || ^19.0.0",
436
436
  "@jini-ai/agentic": "^0.4.4",
437
- "@jini-ai/ui": "^0.4.8 || ^0.5.0",
437
+ "@jini-ai/ui": "^0.5.1",
438
438
  "@jini-ai/ui-kit": "^0.1.3",
439
439
  "@jini-ai/agent-plugins": "^0.4.3",
440
440
  "@jini-ai/cms": "^0.5.6",
@@ -476,12 +476,12 @@
476
476
  "react": "^19.2.0",
477
477
  "react-dom": "^19.2.0",
478
478
  "@testing-library/user-event": "^14.6.1",
479
- "@jini-ai/ui-kit": "^0.1.3",
480
- "@jini-ai/core": "^0.4.4",
479
+ "@jini-ai/ui": "^0.5.1",
481
480
  "@jini-ai/agentic": "^0.4.4",
481
+ "@jini-ai/ui-kit": "^0.1.4",
482
+ "@jini-ai/agent-plugins": "^0.4.6",
482
483
  "@jini-ai/cms": "^0.5.10",
483
- "@jini-ai/ui": "^0.5.0",
484
- "@jini-ai/agent-plugins": "^0.4.6"
484
+ "@jini-ai/core": "^0.4.5"
485
485
  },
486
486
  "typesVersions": {
487
487
  "*": {