@jini-ai/admin 0.4.10 → 0.4.11

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.11 — 2026-10-14
4
+
5
+ - `FieldAttributesDialog` and `SitemapModal` use the neutral `jini-domain-dialog` class instead of a host-named one; hosts style `.jini-domain-dialog` (47b9fa4f).
6
+ - Rides along, no API change: forms option row keys come from the editor's own row list instead of a module-level counter (6281ddc8); redirects imports its shared widgets directly instead of the React barrel (9b45c742); comment scrubs (8e508fb6, 6281ddc8).
7
+
3
8
  ## 0.4.10 — 2026-10-13
4
9
 
5
10
  - Forms HTML starter: URL fields emit `type="text"` plus the registry's URL hints (`inputmode`, `autocomplete`, `spellcheck`, `autocapitalize`), so bare domains such as `tovu.dev` are no longer refused by the browser. CMS peer floor ^0.5.11.
@@ -31,11 +31,11 @@ export declare const KEYS: {
31
31
  /** The moderation queue's name on `lib/content-refresh-bus.ts` — see `taxonomy/rules.ts`'s
32
32
  * `TAXONOMY_RESOURCE` for why this is a plain colocated constant rather than a shared registry.
33
33
  * Agent-writable via `comments_approve_comment`/`comments_mark_comment_spam`/
34
- * `comments_trash_comment`/`comments_restore_comment` (`apps/website/src/features/comments/
35
- * agent-tools.ts`), all of which move a comment between statuses this queue lists.
34
+ * `comments_trash_comment`/`comments_restore_comment` (the host's comment agent tools),
35
+ * all of which move a comment between statuses this queue lists.
36
36
  *
37
37
  * There is deliberately NO sibling `COMMENTS_SETTINGS_RESOURCE`: `react/hooks/use-comment-settings.hooks.ts`'s
38
- * own header records a real, already-fixed lost-update bug (TM-TOVU-2026-08-12-A) whose fix is a
38
+ * own header records a real, already-fixed lost-update bug (2026-08-12) whose fix is a
39
39
  * ONE-SHOT seed guard that refuses to re-seed `settings` from any later successful load, including
40
40
  * this bus's own out-of-band notification. Wiring that hook to the bus would either do nothing
41
41
  * (the guard blocks it) or, if the guard were bypassed instead, reintroduce the exact silent
@@ -35,11 +35,11 @@ export const KEYS = {
35
35
  /** The moderation queue's name on `lib/content-refresh-bus.ts` — see `taxonomy/rules.ts`'s
36
36
  * `TAXONOMY_RESOURCE` for why this is a plain colocated constant rather than a shared registry.
37
37
  * Agent-writable via `comments_approve_comment`/`comments_mark_comment_spam`/
38
- * `comments_trash_comment`/`comments_restore_comment` (`apps/website/src/features/comments/
39
- * agent-tools.ts`), all of which move a comment between statuses this queue lists.
38
+ * `comments_trash_comment`/`comments_restore_comment` (the host's comment agent tools),
39
+ * all of which move a comment between statuses this queue lists.
40
40
  *
41
41
  * There is deliberately NO sibling `COMMENTS_SETTINGS_RESOURCE`: `react/hooks/use-comment-settings.hooks.ts`'s
42
- * own header records a real, already-fixed lost-update bug (TM-TOVU-2026-08-12-A) whose fix is a
42
+ * own header records a real, already-fixed lost-update bug (2026-08-12) whose fix is a
43
43
  * ONE-SHOT seed guard that refuses to re-seed `settings` from any later successful load, including
44
44
  * this bus's own out-of-band notification. Wiring that hook to the bus would either do nothing
45
45
  * (the guard blocks it) or, if the guard were bypassed instead, reintroduce the exact silent
@@ -65,6 +65,7 @@ export declare function retypeField({ field, type }: {
65
65
  field: AdminFormField;
66
66
  type: AdminFormField["type"];
67
67
  }, _optional?: Record<string, never>): AdminFormField;
68
+ /** Local React keys for option rows; identity is scoped to the editor's own row list. */
68
69
  export interface OptionRow {
69
70
  _rowId: number;
70
71
  value: string;
@@ -134,8 +134,6 @@ export function retypeField({ field, type }, _optional = {}) {
134
134
  next.default = field.default;
135
135
  return next;
136
136
  }
137
- /** Local React keys for option rows; same module-counter pattern as `rules.ts`'s attribute rows. */
138
- let nextOptionRowId = 0;
139
137
  const text = (value) => (value === undefined || value === null ? "" : String(value));
140
138
  /** Seed the draft from a stored field. @complexity O(o) in options. */
141
139
  export function settingsFormFromField({ field }, _optional = {}) {
@@ -147,7 +145,7 @@ export function settingsFormFromField({ field }, _optional = {}) {
147
145
  defaultChoices: Array.isArray(defaults) ? [...defaults] : typeof defaults === "string" && fieldSettingsFor({ type: field.type }).options ? [defaults] : [],
148
146
  minLength: text(field.minLength), maxLength: text(field.maxLength), pattern: text(field.pattern),
149
147
  min: text(field.min), max: text(field.max), step: text(field.step),
150
- options: (field.options ?? []).map((option) => ({ _rowId: nextOptionRowId++, value: option.value, label: option.label })),
148
+ options: (field.options ?? []).map((option, index) => ({ _rowId: index, value: option.value, label: option.label })),
151
149
  accept: [...(field.accept ?? [])],
152
150
  maxFileSizeMb: field.maxFileSize === undefined ? "" : String(Math.round((field.maxFileSize / 1024 / 1024) * 100) / 100),
153
151
  };
@@ -167,7 +165,9 @@ export function updateOptionRow({ rows, rowId, patch }, _optional = {}) {
167
165
  }
168
166
  /** @complexity O(o). */
169
167
  export function addOptionRow({ rows }, _optional = {}) {
170
- return [...rows, { _rowId: nextOptionRowId++, value: "", label: "" }];
168
+ // Use the highest current key so deleting or reordering rows cannot create a duplicate.
169
+ const rowId = rows.reduce((max, row) => Math.max(max, row._rowId), -1) + 1;
170
+ return [...rows, { _rowId: rowId, value: "", label: "" }];
171
171
  }
172
172
  /** @complexity O(o). */
173
173
  export function removeOptionRow({ rows, rowId }, _optional = {}) {
@@ -19,7 +19,7 @@ export function FieldAttributesDialog({ field, fieldIndex, onSave, onCancel, use
19
19
  // it was not judged worth the added indirection.
20
20
  const { className, addRow, error, submit } = controller;
21
21
  const { rows, displayName, onClose, onClassName, hasStyling } = useFieldAttributesView({ field, fieldIndex, controller, onCancel });
22
- return (_jsx(Dialog, { open: true, title: `${t("Field settings —")} ${displayName}`, onClose: onClose, className: "settings-dialog tovu-domain-dialog field-attrs-dialog", children: _jsxs("form", { onSubmit: submit, ...agentHandle({ handle: "form-field-attrs-dialog" }, {
22
+ return (_jsx(Dialog, { open: true, title: `${t("Field settings —")} ${displayName}`, onClose: onClose, className: "settings-dialog jini-domain-dialog field-attrs-dialog", children: _jsxs("form", { onSubmit: submit, ...agentHandle({ handle: "form-field-attrs-dialog" }, {
23
23
  role: "region",
24
24
  label: "Field settings dialog — type-specific settings, CSS classes and HTML attributes for one form field",
25
25
  }), children: [_jsx(FieldSettingsSection, { field: field, controller: controller, t: t }), _jsxs("details", { className: "field-settings-advanced", open: hasStyling, children: [_jsx("summary", { children: t("CSS classes and HTML attributes") }), _jsxs("div", { className: "field", children: [_jsxs("span", { className: "field-settings-label", children: [_jsx("label", { className: "field-label", htmlFor: "field-attrs-classname", children: t("CSS classes") }), _jsx(InfoTip, { label: t("Classes are unrestricted, e.g. Tailwind md:col-span-2. Attribute names must be aria-*, data-* or a safe layout attribute; event handlers like onclick are rejected.") })] }), _jsx("input", { id: "field-attrs-classname", value: className, placeholder: "e.g. md:col-span-2 w-1/2 focus:ring-2", onChange: onClassName, ...agentHandle({ handle: "form-field-attrs-classname" }, {
@@ -37,7 +37,7 @@ import { type FormDateLine } from "../../rules.js";
37
37
  *
38
38
  * the scoped formsEvents subscription (staleness-bug generalization pass, see that hook's own header):
39
39
  * `forms_create_definition`/`forms_update_definition`/`forms_set_definition_status`
40
- * (`apps/website/src/features/forms/agent-tools.ts`) are agent-callable, so this list re-invalidates
40
+ * (the host's forms agent tools) are agent-callable, so this list re-invalidates
41
41
  * `KEYS.list` on an out-of-band content-refresh notification the same way `use-taxonomy.hooks.ts`
42
42
  * does for its own resource.
43
43
  */
@@ -31,8 +31,8 @@ import { type FieldType } from "@jini-ai/cms/forms";
31
31
  /**
32
32
  * This screen's name on `lib/content-refresh-bus.ts` — see `taxonomy/rules.ts`'s `TAXONOMY_RESOURCE`
33
33
  * for why this is a plain colocated constant rather than a shared registry. `forms_create_definition`/
34
- * `forms_update_definition`/`forms_set_definition_status` (`apps/website/src/features/forms/agent-
35
- * tools.ts`) are agent-callable, so `FormsList.tsx` needs the same "an assistant write shows up
34
+ * `forms_update_definition`/`forms_set_definition_status` (the host's forms agent tools)
35
+ * are agent-callable, so `FormsList.tsx` needs the same "an assistant write shows up
36
36
  * without a reload" fix `use-taxonomy.hooks.ts` shipped first.
37
37
  */
38
38
  export declare const FORMS_LIST_RESOURCE = "forms";
@@ -30,8 +30,8 @@ import { FIELD_TYPE_REGISTRY } from "@jini-ai/cms/forms";
30
30
  /**
31
31
  * This screen's name on `lib/content-refresh-bus.ts` — see `taxonomy/rules.ts`'s `TAXONOMY_RESOURCE`
32
32
  * for why this is a plain colocated constant rather than a shared registry. `forms_create_definition`/
33
- * `forms_update_definition`/`forms_set_definition_status` (`apps/website/src/features/forms/agent-
34
- * tools.ts`) are agent-callable, so `FormsList.tsx` needs the same "an assistant write shows up
33
+ * `forms_update_definition`/`forms_set_definition_status` (the host's forms agent tools)
34
+ * are agent-callable, so `FormsList.tsx` needs the same "an assistant write shows up
35
35
  * without a reload" fix `use-taxonomy.hooks.ts` shipped first.
36
36
  */
37
37
  export const FORMS_LIST_RESOURCE = "forms";
@@ -1,5 +1,5 @@
1
1
  import { createElement } from 'react';
2
- import { RowMenu } from '../../../react/index.js';
2
+ import { RowMenu } from '../../../react/components/RowMenu/RowMenu.js';
3
3
  import { redirectRowMenuItems } from '../../rules.js';
4
4
  import { HitCountCell } from '../components/HitCountCell.js';
5
5
  import { useRedirectsOptions } from './RedirectsOptions.hooks.js';
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { DataTable, ConfirmDialog } from '../../../react/index.js';
2
+ import { DataTable } from '../../../react/components/DataTable.js';
3
+ import { ConfirmDialog } from '../../../react/components/ConfirmDialog/ConfirmDialog.js';
3
4
  import { agentHandle } from '@jini-ai/agentic';
4
5
  import { describeApiError } from '../../../core/transport/errors.js';
5
6
  import { ImportRedirectsForm } from '../components/ImportRedirectsForm.js';
@@ -22,8 +22,8 @@ export declare const KEYS: {
22
22
  };
23
23
  /** This screen's name on the host refresh bus — see `taxonomy/rules.ts`'s `TAXONOMY_RESOURCE`
24
24
  * for why this is a plain colocated constant rather than a shared registry. Agent-writable via
25
- * `redirects_create`/`redirects_update`/`redirects_tombstone` (`apps/website/src/features/redirects/
26
- * agent-tools.ts`) — `redirects_import` is excluded from that catalog entirely, but the three wired
25
+ * `redirects_create`/`redirects_update`/`redirects_tombstone` (the host's redirect agent tools) —
26
+ * `redirects_import` is excluded from that catalog entirely, but the three wired
27
27
  * single-row writes are enough to make this list go stale the same way Posts/Media did. */
28
28
  export declare const REDIRECTS_RESOURCE = "redirects";
29
29
  /**
@@ -18,8 +18,8 @@ export const KEYS = {
18
18
  };
19
19
  /** This screen's name on the host refresh bus — see `taxonomy/rules.ts`'s `TAXONOMY_RESOURCE`
20
20
  * for why this is a plain colocated constant rather than a shared registry. Agent-writable via
21
- * `redirects_create`/`redirects_update`/`redirects_tombstone` (`apps/website/src/features/redirects/
22
- * agent-tools.ts`) — `redirects_import` is excluded from that catalog entirely, but the three wired
21
+ * `redirects_create`/`redirects_update`/`redirects_tombstone` (the host's redirect agent tools) —
22
+ * `redirects_import` is excluded from that catalog entirely, but the three wired
23
23
  * single-row writes are enough to make this list go stale the same way Posts/Media did. */
24
24
  export const REDIRECTS_RESOURCE = "redirects";
25
25
  /**
@@ -49,7 +49,7 @@ function applySeoPatch(meta, patch) {
49
49
  };
50
50
  }
51
51
  /** One of the three site-wide scalars the server registers as `nullable: true` (`SEO_DEFINITIONS`,
52
- * `apps/website/src/features/seo/settings.ts`), under the real write's own rule: an omitted key
52
+ * `features/seo/settings.ts`), under the real write's own rule: an omitted key
53
53
  * (`undefined`) keeps the current value, `null` clears it, and a string overwrites. */
54
54
  function settingOrCleared(patchValue, currentValue) {
55
55
  if (patchValue === undefined)
@@ -64,7 +64,7 @@ export function SitemapModal({ locale, sitemapEnabled, regenerating, onRegenerat
64
64
  const { title } = useSitemapPresentation({ locale, modal });
65
65
  const { handleRegenerate } = useSitemapModalRegenerate({ onRegenerate });
66
66
  // Native modality keeps Tab from reaching the background, as MediaPickerDialog now does too.
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" }, {
67
+ return (_jsxs(Dialog, { open: true, title: title, onClose: () => onClose(), className: "settings-dialog jini-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",
69
69
  label: "Rebuild the cached sitemap and refresh this view",
70
70
  }), children: actionLabel({ pending: regenerating, pendingLabel: t({ locale: locale, key: "Working…" }), idleLabel: t({ locale: locale, key: "Regenerate sitemap" }) }) }), _jsx("button", { type: "button", className: "btn-secondary", onClick: onClose, "data-jini-autofocus": "", ...agentHandle({ handle: "seo-sitemap-modal-close" }, { role: "button", label: "Close the sitemap viewer" }), children: t({ locale: locale, key: "Close" }) })] }) })] }));
@@ -20,7 +20,7 @@ import { useWiredSeo } from "../hooks/use-seo.hooks.js";
20
20
  * still a minimal textarea-per-rule form (unchanged from the original disclosed scope note).
21
21
  *
22
22
  * Copy comes from the module's injected t function. Legacy locale props remain render-contract
23
- * metadata only; Tovu retains its dictionaries and passes the bound translator through the module.
23
+ * metadata only; the host retains its dictionaries and passes the bound translator through the module.
24
24
  * Host publish behavior is injected as headerActions, and media selection uses its existing slot
25
25
  * or the shared mediaPickerToken service. The original rationale is archived in SOURCE-RATIONALE.
26
26
  *
@@ -14,7 +14,7 @@ export declare function sortIssuesBySeverity({ issues }: {
14
14
  /**
15
15
  * One edited per-entry override field's outgoing value. An emptied text/URL box means "remove this
16
16
  * override so the entry falls back to the site default", which on the wire is `null` — the clear
17
- * sentinel `SeoExtFieldsPatch` documents (`apps/website/src/features/seo/types.ts`) and
17
+ * sentinel `SeoExtFieldsPatch` documents (`features/seo/types.ts`) and
18
18
  * `setEntrySeoOverrides` implements by `delete`-ing the key.
19
19
  *
20
20
  * It is NOT `""`. `write-service.ts` stores `""` as a genuine override and `seo.ts` resolves
@@ -47,7 +47,7 @@ export declare function actionLabel({ pending, pendingLabel, idleLabel }: {
47
47
  idleLabel: string;
48
48
  }, _optional?: Record<string, never>): string;
49
49
  /**
50
- * One `<url>` entry from a parsed sitemap. Only the fields `apps/website`'s
50
+ * One `<url>` entry from a parsed sitemap. Only the fields the host's
51
51
  * `registerSeoSitemapRoute` (`server/inbound/public-http/routes/site/sitemap.ts`) actually emits —
52
52
  * `<loc>` always, `<lastmod>` when the entry has one — verified by reading that route rather than
53
53
  * guessed: it never writes `changefreq`/`priority`, so `SitemapModal.tsx`'s table has no columns
@@ -88,7 +88,7 @@ export declare function isStoredMediaRef({ value }: {
88
88
  /** Builds the `{slug}:public` reference `MediaRefField` writes into the field on selection —
89
89
  * prefers the asset's readable slug over its id (readable-slugs S5b, 2026-09-23), matching
90
90
  * `resolveSeoImageRef`'s own id-or-slug lookup server-side (`findMediaByIdOrSlug`,
91
- * `apps/website/src/features/seo/media.ts`, S4) so a saved override reads as a slug, not an opaque
91
+ * `features/seo/media.ts`, S4) so a saved override reads as a slug, not an opaque
92
92
  * uuid. Falls back to `id` when `slug` is empty — defensive only; a real upload always derives one
93
93
  * (`deriveUniqueMediaSlug`). Same `transformName: "public"` `EmbedInsertControl.tsx` already writes
94
94
  * for an inserted image node. An old stored `{assetId}:public` value keeps resolving regardless
@@ -110,7 +110,7 @@ export declare function buildMediaRef({ item }: {
110
110
  * "string"` also resolves to `null` rather than throwing — `MediaRefField`'s `value` prop is a
111
111
  * `fieldValue(key, resolved) ?? ""` fallback chain (`react/pages/SeoPage.tsx`), where `??` does not replace a
112
112
  * non-nullish-but-wrong-typed result from a misbehaving caller; matches `resolveSeoImageRef`'s own
113
- * "never throws over one bad reference" contract (`apps/website/src/features/seo/media.ts`).
113
+ * "never throws over one bad reference" contract (`features/seo/media.ts`).
114
114
  *
115
115
  * @complexity O(1) — string parsing only, no I/O (the returned URL is a template; the browser
116
116
  * performs the actual fetch only once it is used as an `<img src>`). */
package/dist/seo/rules.js CHANGED
@@ -23,7 +23,7 @@ export function sortIssuesBySeverity({ issues }, _optional = {}) {
23
23
  /**
24
24
  * One edited per-entry override field's outgoing value. An emptied text/URL box means "remove this
25
25
  * override so the entry falls back to the site default", which on the wire is `null` — the clear
26
- * sentinel `SeoExtFieldsPatch` documents (`apps/website/src/features/seo/types.ts`) and
26
+ * sentinel `SeoExtFieldsPatch` documents (`features/seo/types.ts`) and
27
27
  * `setEntrySeoOverrides` implements by `delete`-ing the key.
28
28
  *
29
29
  * It is NOT `""`. `write-service.ts` stores `""` as a genuine override and `seo.ts` resolves
@@ -89,10 +89,10 @@ export function filterSitemapEntries({ entries, query }, _optional = {}) {
89
89
  * `MediaRefField`'s ref-building/preview logic (`ogImage`/`twitterImage`/`defaultOgImage` all
90
90
  * share this shape — SPEC intent: "make the OG image selectable, with a preview"). The reference
91
91
  * format itself is NOT reinvented here — it is verified, not guessed, against the one function that
92
- * actually parses it server-side: `resolveSeoImageRef` (`apps/website/src/features/seo/media.ts`)
92
+ * actually parses it server-side: `resolveSeoImageRef` (`features/seo/media.ts`)
93
93
  * accepts either an already-absolute URL, passed through unchanged, or a bare `{assetId}:{transformName}`
94
94
  * pair split on the FIRST `:`. `isAbsoluteMediaRef`/`parseMediaRefAssetId` below duplicate that
95
- * parsing rather than import across the app boundary (apps/admin has no dependency on apps/website)
95
+ * parsing rather than import across the app boundary (the admin UI has no dependency on the host server)
96
96
  * — kept in lockstep by this doc comment naming the source of truth; a change to that split MUST
97
97
  * update both.
98
98
  */
@@ -101,7 +101,7 @@ export function filterSitemapEntries({ entries, query }, _optional = {}) {
101
101
  * `transformName: "public"` for the identical `{assetId}:{transformName}` shape it writes into a
102
102
  * post body's image node. */
103
103
  const SEO_IMAGE_TRANSFORM = "public";
104
- /** Mirrors `resolveSeoImageRef`'s own `isAbsoluteUrl` (`apps/website/src/features/seo/media.ts`) —
104
+ /** Mirrors `resolveSeoImageRef`'s own `isAbsoluteUrl` (`features/seo/media.ts`) —
105
105
  * see this section's file-header doc for why this is a duplicate, not an import. */
106
106
  const ABSOLUTE_URL_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i;
107
107
  function isAbsoluteMediaRef(value) {
@@ -129,7 +129,7 @@ export function isStoredMediaRef({ value }, _optional = {}) {
129
129
  /** Builds the `{slug}:public` reference `MediaRefField` writes into the field on selection —
130
130
  * prefers the asset's readable slug over its id (readable-slugs S5b, 2026-09-23), matching
131
131
  * `resolveSeoImageRef`'s own id-or-slug lookup server-side (`findMediaByIdOrSlug`,
132
- * `apps/website/src/features/seo/media.ts`, S4) so a saved override reads as a slug, not an opaque
132
+ * `features/seo/media.ts`, S4) so a saved override reads as a slug, not an opaque
133
133
  * uuid. Falls back to `id` when `slug` is empty — defensive only; a real upload always derives one
134
134
  * (`deriveUniqueMediaSlug`). Same `transformName: "public"` `EmbedInsertControl.tsx` already writes
135
135
  * for an inserted image node. An old stored `{assetId}:public` value keeps resolving regardless
@@ -149,7 +149,7 @@ export function buildMediaRef({ item }, _optional = {}) {
149
149
  * "string"` also resolves to `null` rather than throwing — `MediaRefField`'s `value` prop is a
150
150
  * `fieldValue(key, resolved) ?? ""` fallback chain (`react/pages/SeoPage.tsx`), where `??` does not replace a
151
151
  * non-nullish-but-wrong-typed result from a misbehaving caller; matches `resolveSeoImageRef`'s own
152
- * "never throws over one bad reference" contract (`apps/website/src/features/seo/media.ts`).
152
+ * "never throws over one bad reference" contract (`features/seo/media.ts`).
153
153
  *
154
154
  * @complexity O(1) — string parsing only, no I/O (the returned URL is a template; the browser
155
155
  * performs the actual fetch only once it is used as an `<img src>`). */
@@ -205,7 +205,7 @@ export function shareImageWarning({ value, previewUrl, width, height, byteSize,
205
205
  /** One `FormData` field as a string, with the caller's fallback for an absent field. Its own named
206
206
  * function purely so {@link buildSeoSettingsPatch} below stays readable at a glance: written
207
207
  * inline, the seven fields' `?? `/`||` chains scored the patch builder at the ESLint ceiling
208
- * (`apps/admin` enforces 9/9 as a hard error) for no gain in clarity. */
208
+ * (the admin lint policy enforces 9/9 as a hard error) for no gain in clarity. */
209
209
  function formString(form, key, fallback = "") {
210
210
  return String(form.get(key) ?? fallback);
211
211
  }
@@ -214,7 +214,7 @@ function formString(form, key, fallback = "") {
214
214
  *
215
215
  * `null` is what CLEARS a site default: the server registers these three (`default_description`,
216
216
  * `default_og_image`, `twitter_site`) as `nullable: true` and `buildScalarWrites`
217
- * (`apps/website/src/features/seo/settings.ts`) normalizes `null` to the `""` absent-sentinel that
217
+ * (`features/seo/settings.ts`) normalizes `null` to the `""` absent-sentinel that
218
218
  * `getSeoSettings` reads back as `undefined`.
219
219
  *
220
220
  * It used to return `undefined`, under a comment claiming "an empty box means 'no site default'".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/admin",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
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": {
@@ -79,6 +79,12 @@
79
79
  "./consent/react": "browser"
80
80
  },
81
81
  "isolation": {
82
+ "shared": [
83
+ "src/react/components/DataTable.tsx",
84
+ "src/react/components/ConfirmDialog",
85
+ "src/react/components/RowMenu",
86
+ "src/react/types.ts"
87
+ ],
82
88
  "forbidden": {
83
89
  "universal": [
84
90
  "node:",
@@ -476,12 +482,12 @@
476
482
  "react": "^19.2.0",
477
483
  "react-dom": "^19.2.0",
478
484
  "@testing-library/user-event": "^14.6.1",
479
- "@jini-ai/cms": "^0.5.11",
480
- "@jini-ai/ui-kit": "^0.1.4",
481
- "@jini-ai/core": "^0.4.5",
482
- "@jini-ai/ui": "^0.5.1",
485
+ "@jini-ai/agent-plugins": "^0.4.6",
483
486
  "@jini-ai/agentic": "^0.4.4",
484
- "@jini-ai/agent-plugins": "^0.4.6"
487
+ "@jini-ai/ui-kit": "^0.1.4",
488
+ "@jini-ai/cms": "^0.5.12",
489
+ "@jini-ai/core": "^0.4.6",
490
+ "@jini-ai/ui": "^0.5.3"
485
491
  },
486
492
  "typesVersions": {
487
493
  "*": {