@jini-ai/admin 0.4.12 → 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 (151) hide show
  1. package/CHANGELOG.md +4 -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/routing/rules.js +9 -4
  38. package/dist/core/transport/http.js +37 -20
  39. package/dist/forms/adapters/memory.js +22 -11
  40. package/dist/forms/conformance/forms-api.conformance.d.ts +6 -4
  41. package/dist/forms/conformance/forms-api.conformance.js +42 -46
  42. package/dist/forms/field-settings.js +150 -115
  43. package/dist/forms/html-rules.js +45 -28
  44. package/dist/forms/react/components/FieldSettingsRows.d.ts +22 -0
  45. package/dist/forms/react/components/FieldSettingsRows.js +29 -0
  46. package/dist/forms/react/components/FieldSettingsSection.d.ts +1 -1
  47. package/dist/forms/react/components/FieldSettingsSection.js +5 -6
  48. package/dist/forms/react/components/FormEditorFieldsBody.d.ts +1 -1
  49. package/dist/forms/react/components/FormEditorFieldsBody.js +4 -15
  50. package/dist/forms/react/components/FormEditorFieldsBodyParts.d.ts +18 -0
  51. package/dist/forms/react/components/FormEditorFieldsBodyParts.js +32 -0
  52. package/dist/forms/react/components/FormSubmissions.d.ts +1 -1
  53. package/dist/forms/react/components/FormSubmissions.js +4 -20
  54. package/dist/forms/react/components/FormSubmissionsTable.d.ts +19 -0
  55. package/dist/forms/react/components/FormSubmissionsTable.js +22 -0
  56. package/dist/media/adapters/memory.js +31 -26
  57. package/dist/media/controllers/edit-media.controller.js +2 -1
  58. package/dist/media/controllers/library.controller.js +48 -35
  59. package/dist/media/controllers/providers.controller.js +2 -1
  60. package/dist/media/html-attributes.js +29 -15
  61. package/dist/media/index.d.ts +1 -1
  62. package/dist/media/index.js +1 -1
  63. package/dist/media/react/components/MediaIcons.d.ts +0 -3
  64. package/dist/media/react/components/MediaIcons.js +0 -9
  65. package/dist/media/react/components/MediaLightbox.d.ts +2 -1
  66. package/dist/media/react/components/MediaLightbox.js +11 -5
  67. package/dist/media/react/hooks/LibraryTab.hooks.d.ts +55 -45
  68. package/dist/media/react/hooks/LibraryTab.hooks.js +107 -70
  69. package/dist/media/react/hooks/MediaCard.hooks.d.ts +27 -31
  70. package/dist/media/react/hooks/MediaCard.hooks.js +52 -26
  71. package/dist/media/react/hooks/MediaGrid.hooks.d.ts +1 -1
  72. package/dist/media/react/hooks/MediaLightbox.hooks.d.ts +10 -14
  73. package/dist/media/react/hooks/MediaLightbox.hooks.js +9 -56
  74. package/dist/media/react/hooks/MediaPage.hooks.d.ts +6 -0
  75. package/dist/media/react/hooks/MediaPage.hooks.js +29 -12
  76. package/dist/media/react/hooks/ProvidersTab.hooks.d.ts +33 -33
  77. package/dist/media/react/hooks/ProvidersTab.hooks.js +119 -79
  78. package/dist/media/react/hooks/UploadButton.hooks.d.ts +7 -5
  79. package/dist/media/react/hooks/UploadButton.hooks.js +36 -24
  80. package/dist/media/react/index.js +4 -0
  81. package/dist/media/react/tabs/LibraryTab.d.ts +1 -1
  82. package/dist/media/react/tabs/LibraryTab.js +2 -2
  83. package/dist/media/rules.d.ts +7 -0
  84. package/dist/media/rules.js +25 -2
  85. package/dist/react/components/ConfirmDialog/ConfirmDialog.js +5 -8
  86. package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.d.ts +20 -0
  87. package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.js +24 -0
  88. package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +16 -7
  89. package/dist/react/components/RowMenu/RowMenu.js +16 -10
  90. package/dist/react/components/Sidebar.js +32 -26
  91. package/dist/react/entities/EntityEdit.js +17 -14
  92. package/dist/react/entities/EntityField.d.ts +5 -2
  93. package/dist/react/entities/EntityField.js +38 -23
  94. package/dist/react/entities/EntityList.js +6 -5
  95. package/dist/react/entities/RelationTruncationNotice.d.ts +9 -0
  96. package/dist/react/entities/RelationTruncationNotice.js +9 -0
  97. package/dist/react/entities/use-entity-edit.d.ts +4 -2
  98. package/dist/react/entities/use-entity-edit.js +52 -33
  99. package/dist/react/shell/AdminShell.js +40 -23
  100. package/dist/react/shell/model.d.ts +4 -1
  101. package/dist/react/shell/model.js +3 -2
  102. package/dist/react/shell/use-admin-shell-session.js +25 -26
  103. package/dist/redirects/adapters/memory.js +9 -2
  104. package/dist/redirects/conformance/redirects-api.conformance.js +66 -36
  105. package/dist/redirects/react/hooks/use-import-redirects-form.hooks.d.ts +1 -1
  106. package/dist/redirects/react/hooks/use-import-redirects-form.hooks.js +10 -3
  107. package/dist/redirects/react/hooks/wired.hooks.d.ts +1 -1
  108. package/dist/security/adapters/http.js +25 -10
  109. package/dist/security/adapters/memory.js +19 -5
  110. package/dist/security/controllers/access-tokens.controller.js +27 -26
  111. package/dist/security/controllers/other-credentials.controller.js +4 -2
  112. package/dist/security/controllers/root-key.controller.js +17 -9
  113. package/dist/security/react/hooks/AccessTokensTab.hooks.d.ts +40 -38
  114. package/dist/security/react/hooks/AccessTokensTab.hooks.js +36 -9
  115. package/dist/security/react/hooks/RootKeyTab.hooks.d.ts +14 -14
  116. package/dist/security/react/hooks/RootKeyTab.hooks.js +12 -3
  117. package/dist/security/react/tabs/RootKeyTab.d.ts +1 -1
  118. package/dist/security/react/tabs/RootKeyTab.js +12 -3
  119. package/dist/security/rules.js +45 -25
  120. package/dist/seo/controllers/seo.controller.js +4 -2
  121. package/dist/seo/react/components/MediaRefField.d.ts +1 -1
  122. package/dist/seo/react/components/MediaRefField.js +19 -2
  123. package/dist/seo/react/hooks/MediaRefField.hooks.js +3 -1
  124. package/dist/seo/react/hooks/SeoPage.hooks.d.ts +1 -1
  125. package/dist/seo/react/hooks/use-share-image-warning.hooks.js +48 -34
  126. package/dist/seo/react/pages/SeoPage.d.ts +4 -4
  127. package/dist/seo/react/pages/SeoPage.js +26 -6
  128. package/dist/seo/rules.js +39 -10
  129. package/dist/source-control/conformance/source-control-api.conformance.js +68 -38
  130. package/dist/source-control/controllers/source-control.controller.js +20 -8
  131. package/dist/source-control/react/hooks/ProvidersTab.hooks.d.ts +2 -1
  132. package/dist/source-control/react/hooks/ProvidersTab.hooks.js +18 -11
  133. package/dist/widgets/conformance/widgets-api.conformance.js +15 -1
  134. package/dist/widgets/controllers/instance-editor.controller.js +33 -24
  135. package/dist/widgets/controllers/library.controller.js +18 -12
  136. package/dist/widgets/controllers/region-editor.controller.js +6 -5
  137. package/dist/widgets/react/hooks/page-views.hooks.d.ts +1 -0
  138. package/dist/widgets/react/hooks/page-views.hooks.js +3 -2
  139. package/dist/widgets/react/hooks/use-widget-regions.hooks.d.ts +1 -1
  140. package/dist/widgets/react/hooks/use-widget-regions.hooks.js +9 -3
  141. package/dist/widgets/react/hooks/use-widgets-library.hooks.js +9 -2
  142. package/dist/widgets/react/pages/WidgetInstanceEditor.d.ts +1 -1
  143. package/dist/widgets/react/pages/WidgetInstanceEditor.js +7 -8
  144. package/dist/widgets/rules.d.ts +14 -2
  145. package/dist/widgets/rules.js +9 -6
  146. package/package.json +17 -8
  147. package/src/themes/default/admin.css +51 -0
  148. package/src/themes/default/chat.css +21 -0
  149. package/src/themes/default/manifest.json +29 -0
  150. package/dist/media/styles.d.ts +0 -4
  151. package/dist/media/styles.js +0 -5
@@ -28,11 +28,22 @@ export function formatFormAnswer({ value, options, type }, display = {}) {
28
28
  if (Array.isArray(value))
29
29
  return value.map(item => formatFormAnswer({ value: item, options, type }, display)).join(", ");
30
30
  if (typeof value === "boolean")
31
- return value ? display.yes ?? "Yes" : display.no ?? "No";
32
- if (typeof value === "string" && type !== undefined && type in TEMPORAL_STYLES)
33
- return formatTemporal({ value, type }, display);
31
+ return formatBoolean({ value }, display);
34
32
  if (typeof value === "string")
35
- return options?.find(option => option.value === value)?.label ?? value;
33
+ return formatText({ value, options, type }, display);
34
+ return formatOther({ value });
35
+ }
36
+ function formatBoolean({ value }, { yes = "Yes", no = "No" }) {
37
+ return value ? yes : no;
38
+ }
39
+ /** A temporal field's answer as a date; any other string as its choice label, or raw. */
40
+ function formatText({ value, options, type }, display) {
41
+ if (type !== undefined && type in TEMPORAL_STYLES)
42
+ return formatTemporal({ value, type }, display);
43
+ return options?.find(option => option.value === value)?.label ?? value;
44
+ }
45
+ /** A file reference as its name and size; any other value as JSON or its string form. */
46
+ function formatOther({ value }) {
36
47
  const file = formAnswerFile({ value });
37
48
  if (file)
38
49
  return file.byteSize === undefined ? file.filename : `${file.filename} (${formatByteSize({ bytes: file.byteSize })})`;
@@ -92,32 +103,38 @@ function starterAttributes(field, kind) {
92
103
  /** One field's starter markup; every control comes from the registry kind, as the public render does. */
93
104
  function starterField(field) {
94
105
  const kind = kindOf(field.type);
95
- const label = escapeHtml(field.label);
96
- const attrs = starterAttributes(field, kind);
97
- const options = field.options ?? [];
98
- if (kind.control === "group") {
99
- // One box per option under a shared name; the CMS collects them back into this choice field.
100
- // Native `required` on every checkbox would demand ALL of them, so a checkbox group leaves it
101
- // off (the server still enforces one-or-more); a radio group's `required` means "pick one".
102
- const boxAttrs = kind.multiple ? starterAttributes({ ...field, required: false }, kind) : attrs;
103
- const boxes = options.map((option) => ` <label><input type="${kind.htmlType}" ${boxAttrs} value="${escapeHtml(option.value)}"${isDefault(field, option.value) ? " checked" : ""}> ${escapeHtml(option.label)}</label>`);
104
- return `<fieldset>\n <legend>${label}</legend>\n${boxes.join("\n")}\n</fieldset>`;
105
- }
106
- if (kind.control === "select") {
107
- const empty = kind.multiple ? [] : [` <option value="">${escapeHtml(field.placeholder ?? "Choose an option")}</option>`];
108
- const choices = options.map((option) => ` <option value="${escapeHtml(option.value)}"${isDefault(field, option.value) ? " selected" : ""}>${escapeHtml(option.label)}</option>`);
109
- return `<label>${label}\n <select ${attrs}${kind.multiple ? " multiple" : ""}>\n${[...empty, ...choices].join("\n")}\n </select>\n</label>`;
110
- }
106
+ return starterRendererFor({ kind })({ field, kind, label: escapeHtml(field.label), attrs: starterAttributes(field, kind) });
107
+ }
108
+ function starterRendererFor({ kind }) {
109
+ if (kind.control === "group")
110
+ return starterGroup;
111
+ if (kind.control === "select")
112
+ return starterSelect;
111
113
  if (kind.control === "textarea")
112
- return `<label>${label}\n <textarea ${attrs}></textarea>\n</label>`;
114
+ return ({ label, attrs }) => `<label>${label}\n <textarea ${attrs}></textarea>\n</label>`;
113
115
  if (kind.htmlType === "hidden")
114
- return `<input type="hidden" ${attrs} value="${escapeHtml(String(field.default ?? ""))}" aria-label="${label}">`;
115
- if (kind.htmlType === "checkbox") {
116
- const value = field.checkboxValue !== undefined ? ` value="${escapeHtml(field.checkboxValue)}"` : "";
117
- const checked = field.default === true || (field.checkboxValue !== undefined && field.default === field.checkboxValue) ? " checked" : "";
118
- return `<label><input type="checkbox"${field.type === "toggle" ? ' role="switch"' : ""} ${attrs}${value}${checked}> ${label}</label>`;
119
- }
120
- return `<label>${label}\n <input type="${kind.htmlType}" ${attrs}>\n</label>`;
116
+ return ({ field, label, attrs }) => `<input type="hidden" ${attrs} value="${escapeHtml(String(field.default ?? ""))}" aria-label="${label}">`;
117
+ if (kind.htmlType === "checkbox")
118
+ return starterCheckbox;
119
+ return ({ label, attrs }) => `<label>${label}\n <input type="${kind.htmlType}" ${attrs}>\n</label>`;
120
+ }
121
+ function starterGroup({ field, kind, label, attrs }) {
122
+ // One box per option under a shared name; the CMS collects them back into this choice field.
123
+ // Native `required` on every checkbox would demand ALL of them, so a checkbox group leaves it
124
+ // off (the server still enforces one-or-more); a radio group's `required` means "pick one".
125
+ const boxAttrs = kind.multiple ? starterAttributes({ ...field, required: false }, kind) : attrs;
126
+ const boxes = (field.options ?? []).map((option) => ` <label><input type="${kind.htmlType}" ${boxAttrs} value="${escapeHtml(option.value)}"${isDefault(field, option.value) ? " checked" : ""}> ${escapeHtml(option.label)}</label>`);
127
+ return `<fieldset>\n <legend>${label}</legend>\n${boxes.join("\n")}\n</fieldset>`;
128
+ }
129
+ function starterSelect({ field, kind, label, attrs }) {
130
+ const empty = kind.multiple ? [] : [` <option value="">${escapeHtml(field.placeholder ?? "Choose an option")}</option>`];
131
+ const choices = (field.options ?? []).map((option) => ` <option value="${escapeHtml(option.value)}"${isDefault(field, option.value) ? " selected" : ""}>${escapeHtml(option.label)}</option>`);
132
+ return `<label>${label}\n <select ${attrs}${kind.multiple ? " multiple" : ""}>\n${[...empty, ...choices].join("\n")}\n </select>\n</label>`;
133
+ }
134
+ function starterCheckbox({ field, label, attrs }) {
135
+ const value = field.checkboxValue !== undefined ? ` value="${escapeHtml(field.checkboxValue)}"` : "";
136
+ const checked = field.default === true || (field.checkboxValue !== undefined && field.default === field.checkboxValue) ? " checked" : "";
137
+ return `<label><input type="checkbox"${field.type === "toggle" ? ' role="switch"' : ""} ${attrs}${value}${checked}> ${label}</label>`;
121
138
  }
122
139
  /**
123
140
  * Editable form BODY only: the server supplies the transport and spam-protection wrapper. Each
@@ -0,0 +1,22 @@
1
+ import type { useFieldSettingsView } from '../hooks/forms-view.hooks.js';
2
+ /**
3
+ * @file The row groups `FieldSettingsSection` stacks, split out so each group's show/hide branches
4
+ * are counted on their own. Markup only: every value and handler comes from `useFieldSettingsView`.
5
+ */
6
+ type FieldSettingsView = ReturnType<typeof useFieldSettingsView>;
7
+ export interface FieldSettingsRowsProps {
8
+ view: FieldSettingsView;
9
+ t: (key: string) => string;
10
+ }
11
+ /** Placeholder and help-text rows. */
12
+ export declare function FieldSettingsTextRows({ view, t }: FieldSettingsRowsProps): import("react").JSX.Element;
13
+ /** The option list editor of a choice field. */
14
+ export declare function FieldSettingsOptionsRow({ view, t }: FieldSettingsRowsProps): import("react").JSX.Element;
15
+ /** The default-value row: a box, a single-choice select, or a typed input, whichever the type takes. */
16
+ export declare function FieldSettingsDefaultRows({ view, t }: FieldSettingsRowsProps): import("react").JSX.Element;
17
+ /** Length, pattern and range rows. */
18
+ export declare function FieldSettingsLimitRows({ view, t }: FieldSettingsRowsProps): import("react").JSX.Element;
19
+ /** Accepted file types and max size of an upload field. */
20
+ export declare function FieldSettingsUploadRows({ view, t }: FieldSettingsRowsProps): import("react").JSX.Element;
21
+ export {};
22
+ //# sourceMappingURL=FieldSettingsRows.d.ts.map
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { agentHandle } from '@jini-ai/agentic';
3
+ import { InfoTip } from '@jini-ai/ui/admin-widgets';
4
+ import { MAX_UPLOAD_MB } from '../../field-settings.js';
5
+ /** Placeholder and help-text rows. */
6
+ export function FieldSettingsTextRows({ view, t }) {
7
+ const { settings, form } = view;
8
+ return (_jsxs(_Fragment, { children: [settings.placeholder ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-placeholder", children: t('Placeholder') }), _jsx("input", { id: "field-settings-placeholder", "data-jini-autofocus": "", value: form.placeholder, onChange: view.onPlaceholder, ...agentHandle({ handle: 'form-field-settings-placeholder' }, { role: 'field', label: 'Hint text shown inside the empty control' }) })] })) : null, settings.helpText ? (_jsxs("div", { className: "field-settings-row", children: [_jsxs("span", { className: "field-settings-label", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-help", children: t('Help text') }), _jsx(InfoTip, { label: t('Shown under the field and read out by screen readers.') })] }), _jsx("input", { id: "field-settings-help", "data-jini-autofocus": "", value: form.helpText, onChange: view.onHelpText, ...agentHandle({ handle: 'form-field-settings-help' }, { role: 'field', label: 'Help text shown under this field' }) })] })) : null] }));
9
+ }
10
+ /** The option list editor of a choice field. */
11
+ export function FieldSettingsOptionsRow({ view, t }) {
12
+ return (_jsxs("div", { className: "field-settings-row field-settings-row--top", children: [_jsxs("span", { className: "field-settings-label", children: [_jsx("span", { className: "field-label", id: "field-settings-options-label", children: t('Options') }), _jsx(InfoTip, { label: t('The label is what visitors see. The value is what the submission stores; it follows the label until you edit it.') })] }), _jsxs("div", { className: "field-options", role: "group", "aria-labelledby": "field-settings-options-label", children: [_jsx("ol", { className: "field-options-list", children: view.options.map((row) => (_jsxs("li", { className: "field-option-row", children: [_jsx("input", { className: "field-option-label", "aria-label": row.labelAria, placeholder: t('Label'), value: row.label, onChange: row.onLabel, ...agentHandle({ handle: `${row.handle}-label` }, { role: 'field', label: 'Option text visitors see' }) }), _jsx("input", { className: "field-option-value", "aria-label": row.valueAria, placeholder: t('Value'), value: row.value, onChange: row.onValue, ...agentHandle({ handle: `${row.handle}-value` }, { role: 'field', label: 'Option value stored with each submission' }) }), view.settings.multiple ? (_jsx("label", { className: "field-option-default", children: _jsx("input", { type: "checkbox", checked: row.isDefault, disabled: !row.value, onChange: row.onDefault, "aria-label": row.defaultAria }) })) : null, _jsxs("span", { className: "field-option-actions", children: [_jsx("button", { type: "button", className: "btn-ghost field-option-btn", disabled: row.isFirst, onClick: row.onUp, "aria-label": row.upAria, children: "\u2191" }), _jsx("button", { type: "button", className: "btn-ghost field-option-btn", disabled: row.isLast, onClick: row.onDown, "aria-label": row.downAria, children: "\u2193" }), _jsx("button", { type: "button", className: "btn-ghost field-option-btn", onClick: row.onRemove, "aria-label": row.removeAria, ...agentHandle({ handle: `${row.handle}-remove` }, { role: 'button', label: 'Remove this option' }), children: "\u00D7" })] })] }, row._rowId))) }), _jsx("button", { type: "button", className: "btn-secondary field-settings-add", onClick: view.addOption, ...agentHandle({ handle: 'form-field-options-add' }, { role: 'button', label: 'Add another option to this choice field' }), children: t('Add option') })] })] }));
13
+ }
14
+ /** The default-value row: a box, a single-choice select, or a typed input, whichever the type takes. */
15
+ export function FieldSettingsDefaultRows({ view, t }) {
16
+ const { settings, form } = view;
17
+ return (_jsxs(_Fragment, { children: [settings.defaultKind === 'boolean' ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("span", { className: "field-label", children: t('Default') }), _jsxs("label", { className: "form-checkbox-field", children: [_jsx("input", { type: "checkbox", checked: form.defaultChecked, onChange: view.onDefaultChecked, ...agentHandle({ handle: 'form-field-settings-default-checked' }, { role: 'checkbox', label: 'Whether this box starts checked' }) }), t('Checked by default')] })] })) : null, settings.defaultKind === 'choice' ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-default", children: t('Default') }), _jsxs("select", { id: "field-settings-default", className: "field-settings-compact", value: form.defaultChoices[0] ?? '', onChange: view.onDefaultChoice, ...agentHandle({ handle: 'form-field-settings-default' }, { role: 'field', label: 'Which option starts selected — set with page.select_option' }), children: [_jsx("option", { value: "", children: t('None') }), view.defaultChoices.map((choice) => _jsx("option", { value: choice.value, children: choice.label }, choice.key))] })] })) : null, view.defaultInputType ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-default", children: t('Default') }), _jsx("input", { id: "field-settings-default", "data-jini-autofocus": "", className: view.defaultClass, type: view.defaultInputType, value: form.defaultText, onChange: view.onDefaultText, ...agentHandle({ handle: 'form-field-settings-default' }, { role: 'field', label: 'The value this field starts with' }) })] })) : null] }));
18
+ }
19
+ /** Length, pattern and range rows. */
20
+ export function FieldSettingsLimitRows({ view, t }) {
21
+ const { settings, form } = view;
22
+ return (_jsxs(_Fragment, { children: [settings.length ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("span", { className: "field-label", id: "field-settings-length-label", children: t('Length') }), _jsxs("span", { className: "field-settings-pair", role: "group", "aria-labelledby": "field-settings-length-label", children: [_jsx("input", { type: "number", min: 0, className: "field-settings-num", "aria-label": t('Min length'), placeholder: t('Min'), value: form.minLength, onChange: view.onMinLength, ...agentHandle({ handle: 'form-field-settings-min-length' }, { role: 'field', label: 'Fewest characters accepted' }) }), _jsx("span", { "aria-hidden": "true", children: "\u2013" }), _jsx("input", { type: "number", min: 1, className: "field-settings-num", "aria-label": t('Max length'), placeholder: t('Max'), value: form.maxLength, onChange: view.onMaxLength, ...agentHandle({ handle: 'form-field-settings-max-length' }, { role: 'field', label: 'Most characters accepted; blank means the default limit' }) })] })] })) : null, settings.pattern ? (_jsxs("div", { className: "field-settings-row", children: [_jsxs("span", { className: "field-settings-label", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-pattern", children: t('Pattern') }), _jsx(InfoTip, { label: t('Optional. The whole answer must match it, e.g. [0-9]+ for digits only. Groups, | and repeating more than once are not supported.') })] }), _jsx("input", { id: "field-settings-pattern", className: "field-settings-code", value: form.pattern, onChange: view.onPattern, spellCheck: false, ...agentHandle({ handle: 'form-field-settings-pattern' }, { role: 'field', label: 'Regular expression the whole answer must match' }) })] })) : null, settings.bounds ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("span", { className: "field-label", id: "field-settings-bounds-label", children: t('Range') }), _jsxs("span", { className: "field-settings-pair", role: "group", "aria-labelledby": "field-settings-bounds-label", children: [_jsx("input", { type: view.boundsType, className: view.boundsClass, "aria-label": t('Min'), placeholder: t('Min'), value: form.min, onChange: view.onMin, ...agentHandle({ handle: 'form-field-settings-min' }, { role: 'field', label: 'Lowest value or earliest date/time accepted' }) }), _jsx("span", { "aria-hidden": "true", children: "\u2013" }), _jsx("input", { type: view.boundsType, className: view.boundsClass, "aria-label": t('Max'), placeholder: t('Max'), value: form.max, onChange: view.onMax, ...agentHandle({ handle: 'form-field-settings-max' }, { role: 'field', label: 'Highest value or latest date/time accepted' }) }), settings.step ? (_jsx("input", { type: "number", min: 0, step: "any", className: "field-settings-num", "aria-label": t('Step'), placeholder: t('Step'), value: form.step, onChange: view.onStep, ...agentHandle({ handle: 'form-field-settings-step' }, { role: 'field', label: 'Increment between accepted values' }) })) : null] })] })) : null] }));
23
+ }
24
+ /** Accepted file types and max size of an upload field. */
25
+ export function FieldSettingsUploadRows({ view, t }) {
26
+ const { form } = view;
27
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "field-settings-row", children: [_jsxs("span", { className: "field-settings-label", children: [_jsx("span", { className: "field-label", id: "field-settings-files-label", children: t('Files') }), _jsx(InfoTip, { label: t('Leave all unticked to accept every safe type: images, video and PDF.') })] }), _jsx("span", { className: "field-settings-checks", role: "group", "aria-labelledby": "field-settings-files-label", children: view.uploads.map((choice) => (_jsxs("label", { className: "form-checkbox-field", children: [_jsx("input", { type: "checkbox", checked: choice.checked, onChange: choice.onToggle, ...agentHandle({ handle: choice.handle }, { role: 'checkbox', label: `Accept ${choice.label} uploads` }) }), t(choice.label)] }, choice.value))) })] }), _jsxs("div", { className: "field-settings-row", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-max-size", children: t('Max size (MB)') }), _jsx("input", { id: "field-settings-max-size", type: "number", min: 0, max: MAX_UPLOAD_MB, step: "any", className: "field-settings-num", placeholder: String(MAX_UPLOAD_MB), value: form.maxFileSizeMb, onChange: view.onMaxFileSize, ...agentHandle({ handle: 'form-field-settings-max-size' }, { role: 'field', label: 'Largest upload accepted, in megabytes' }) })] })] }));
28
+ }
29
+ //# sourceMappingURL=FieldSettingsRows.js.map
@@ -15,5 +15,5 @@ export interface FieldSettingsSectionProps {
15
15
  * placeholder (slider, file…) focused the Help text ⓘ, whose tip opened half behind the dialog
16
16
  * (Chrome check, 2026-10-11). Focus must land on an input, never on an ⓘ.
17
17
  */
18
- export declare function FieldSettingsSection({ field, controller, t }: FieldSettingsSectionProps, _optional?: Record<string, never>): import("react").JSX.Element;
18
+ export declare function FieldSettingsSection({ field, controller, t }: FieldSettingsSectionProps): import("react").JSX.Element;
19
19
  //# sourceMappingURL=FieldSettingsSection.d.ts.map
@@ -1,8 +1,7 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { agentHandle } from '@jini-ai/agentic';
3
- import { InfoTip } from '@jini-ai/ui/admin-widgets';
4
- import { MAX_UPLOAD_MB } from '../../field-settings.js';
5
3
  import { useFieldSettingsView } from '../hooks/forms-view.hooks.js';
4
+ import { FieldSettingsDefaultRows, FieldSettingsLimitRows, FieldSettingsOptionsRow, FieldSettingsTextRows, FieldSettingsUploadRows } from './FieldSettingsRows.js';
6
5
  /**
7
6
  * The settings half of the field dialog: one label-beside-control row per setting the field's
8
7
  * registry kind supports (`field-settings.ts` decides which). Markup only; every handler comes from
@@ -13,9 +12,9 @@ import { useFieldSettingsView } from '../hooks/forms-view.hooks.js';
13
12
  * placeholder (slider, file…) focused the Help text ⓘ, whose tip opened half behind the dialog
14
13
  * (Chrome check, 2026-10-11). Focus must land on an input, never on an ⓘ.
15
14
  */
16
- export function FieldSettingsSection({ field, controller, t }, _optional = {}) {
15
+ export function FieldSettingsSection({ field, controller, t }) {
17
16
  const view = useFieldSettingsView({ field, controller, t });
18
- const { settings, form } = view;
19
- return (_jsxs("div", { className: "field-settings", ...agentHandle({ handle: 'form-field-settings' }, { role: 'region', label: "This field's type-specific settings" }), children: [settings.placeholder ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-placeholder", children: t('Placeholder') }), _jsx("input", { id: "field-settings-placeholder", "data-jini-autofocus": "", value: form.placeholder, onChange: view.onPlaceholder, ...agentHandle({ handle: 'form-field-settings-placeholder' }, { role: 'field', label: 'Hint text shown inside the empty control' }) })] })) : null, settings.helpText ? (_jsxs("div", { className: "field-settings-row", children: [_jsxs("span", { className: "field-settings-label", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-help", children: t('Help text') }), _jsx(InfoTip, { label: t('Shown under the field and read out by screen readers.') })] }), _jsx("input", { id: "field-settings-help", "data-jini-autofocus": "", value: form.helpText, onChange: view.onHelpText, ...agentHandle({ handle: 'form-field-settings-help' }, { role: 'field', label: 'Help text shown under this field' }) })] })) : null, settings.options ? (_jsxs("div", { className: "field-settings-row field-settings-row--top", children: [_jsxs("span", { className: "field-settings-label", children: [_jsx("span", { className: "field-label", id: "field-settings-options-label", children: t('Options') }), _jsx(InfoTip, { label: t('The label is what visitors see. The value is what the submission stores; it follows the label until you edit it.') })] }), _jsxs("div", { className: "field-options", role: "group", "aria-labelledby": "field-settings-options-label", children: [_jsx("ol", { className: "field-options-list", children: view.options.map((row) => (_jsxs("li", { className: "field-option-row", children: [_jsx("input", { className: "field-option-label", "aria-label": row.labelAria, placeholder: t('Label'), value: row.label, onChange: row.onLabel, ...agentHandle({ handle: `${row.handle}-label` }, { role: 'field', label: 'Option text visitors see' }) }), _jsx("input", { className: "field-option-value", "aria-label": row.valueAria, placeholder: t('Value'), value: row.value, onChange: row.onValue, ...agentHandle({ handle: `${row.handle}-value` }, { role: 'field', label: 'Option value stored with each submission' }) }), view.settings.multiple ? (_jsx("label", { className: "field-option-default", children: _jsx("input", { type: "checkbox", checked: row.isDefault, disabled: !row.value, onChange: row.onDefault, "aria-label": row.defaultAria }) })) : null, _jsxs("span", { className: "field-option-actions", children: [_jsx("button", { type: "button", className: "btn-ghost field-option-btn", disabled: row.isFirst, onClick: row.onUp, "aria-label": row.upAria, children: "\u2191" }), _jsx("button", { type: "button", className: "btn-ghost field-option-btn", disabled: row.isLast, onClick: row.onDown, "aria-label": row.downAria, children: "\u2193" }), _jsx("button", { type: "button", className: "btn-ghost field-option-btn", onClick: row.onRemove, "aria-label": row.removeAria, ...agentHandle({ handle: `${row.handle}-remove` }, { role: 'button', label: 'Remove this option' }), children: "\u00D7" })] })] }, row._rowId))) }), _jsx("button", { type: "button", className: "btn-secondary field-settings-add", onClick: view.addOption, ...agentHandle({ handle: 'form-field-options-add' }, { role: 'button', label: 'Add another option to this choice field' }), children: t('Add option') })] })] })) : null, settings.defaultKind === 'boolean' ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("span", { className: "field-label", children: t('Default') }), _jsxs("label", { className: "form-checkbox-field", children: [_jsx("input", { type: "checkbox", checked: form.defaultChecked, onChange: view.onDefaultChecked, ...agentHandle({ handle: 'form-field-settings-default-checked' }, { role: 'checkbox', label: 'Whether this box starts checked' }) }), t('Checked by default')] })] })) : null, settings.defaultKind === 'choice' ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-default", children: t('Default') }), _jsxs("select", { id: "field-settings-default", className: "field-settings-compact", value: form.defaultChoices[0] ?? '', onChange: view.onDefaultChoice, ...agentHandle({ handle: 'form-field-settings-default' }, { role: 'field', label: 'Which option starts selected — set with page.select_option' }), children: [_jsx("option", { value: "", children: t('None') }), view.defaultChoices.map((choice) => _jsx("option", { value: choice.value, children: choice.label }, choice.key))] })] })) : null, view.defaultInputType ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-default", children: t('Default') }), _jsx("input", { id: "field-settings-default", "data-jini-autofocus": "", className: view.defaultClass, type: view.defaultInputType, value: form.defaultText, onChange: view.onDefaultText, ...agentHandle({ handle: 'form-field-settings-default' }, { role: 'field', label: 'The value this field starts with' }) })] })) : null, settings.length ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("span", { className: "field-label", id: "field-settings-length-label", children: t('Length') }), _jsxs("span", { className: "field-settings-pair", role: "group", "aria-labelledby": "field-settings-length-label", children: [_jsx("input", { type: "number", min: 0, className: "field-settings-num", "aria-label": t('Min length'), placeholder: t('Min'), value: form.minLength, onChange: view.onMinLength, ...agentHandle({ handle: 'form-field-settings-min-length' }, { role: 'field', label: 'Fewest characters accepted' }) }), _jsx("span", { "aria-hidden": "true", children: "\u2013" }), _jsx("input", { type: "number", min: 1, className: "field-settings-num", "aria-label": t('Max length'), placeholder: t('Max'), value: form.maxLength, onChange: view.onMaxLength, ...agentHandle({ handle: 'form-field-settings-max-length' }, { role: 'field', label: 'Most characters accepted; blank means the default limit' }) })] })] })) : null, settings.pattern ? (_jsxs("div", { className: "field-settings-row", children: [_jsxs("span", { className: "field-settings-label", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-pattern", children: t('Pattern') }), _jsx(InfoTip, { label: t('Optional. The whole answer must match it, e.g. [0-9]+ for digits only. Groups, | and repeating more than once are not supported.') })] }), _jsx("input", { id: "field-settings-pattern", className: "field-settings-code", value: form.pattern, onChange: view.onPattern, spellCheck: false, ...agentHandle({ handle: 'form-field-settings-pattern' }, { role: 'field', label: 'Regular expression the whole answer must match' }) })] })) : null, settings.bounds ? (_jsxs("div", { className: "field-settings-row", children: [_jsx("span", { className: "field-label", id: "field-settings-bounds-label", children: t('Range') }), _jsxs("span", { className: "field-settings-pair", role: "group", "aria-labelledby": "field-settings-bounds-label", children: [_jsx("input", { type: view.boundsType, className: view.boundsClass, "aria-label": t('Min'), placeholder: t('Min'), value: form.min, onChange: view.onMin, ...agentHandle({ handle: 'form-field-settings-min' }, { role: 'field', label: 'Lowest value or earliest date/time accepted' }) }), _jsx("span", { "aria-hidden": "true", children: "\u2013" }), _jsx("input", { type: view.boundsType, className: view.boundsClass, "aria-label": t('Max'), placeholder: t('Max'), value: form.max, onChange: view.onMax, ...agentHandle({ handle: 'form-field-settings-max' }, { role: 'field', label: 'Highest value or latest date/time accepted' }) }), settings.step ? (_jsx("input", { type: "number", min: 0, step: "any", className: "field-settings-num", "aria-label": t('Step'), placeholder: t('Step'), value: form.step, onChange: view.onStep, ...agentHandle({ handle: 'form-field-settings-step' }, { role: 'field', label: 'Increment between accepted values' }) })) : null] })] })) : null, settings.upload ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "field-settings-row", children: [_jsxs("span", { className: "field-settings-label", children: [_jsx("span", { className: "field-label", id: "field-settings-files-label", children: t('Files') }), _jsx(InfoTip, { label: t('Leave all unticked to accept every safe type: images, video and PDF.') })] }), _jsx("span", { className: "field-settings-checks", role: "group", "aria-labelledby": "field-settings-files-label", children: view.uploads.map((choice) => (_jsxs("label", { className: "form-checkbox-field", children: [_jsx("input", { type: "checkbox", checked: choice.checked, onChange: choice.onToggle, ...agentHandle({ handle: choice.handle }, { role: 'checkbox', label: `Accept ${choice.label} uploads` }) }), t(choice.label)] }, choice.value))) })] }), _jsxs("div", { className: "field-settings-row", children: [_jsx("label", { className: "field-label", htmlFor: "field-settings-max-size", children: t('Max size (MB)') }), _jsx("input", { id: "field-settings-max-size", type: "number", min: 0, max: MAX_UPLOAD_MB, step: "any", className: "field-settings-num", placeholder: String(MAX_UPLOAD_MB), value: form.maxFileSizeMb, onChange: view.onMaxFileSize, ...agentHandle({ handle: 'form-field-settings-max-size' }, { role: 'field', label: 'Largest upload accepted, in megabytes' }) })] })] })) : null] }));
17
+ const { settings } = view;
18
+ return (_jsxs("div", { className: "field-settings", ...agentHandle({ handle: 'form-field-settings' }, { role: 'region', label: "This field's type-specific settings" }), children: [_jsx(FieldSettingsTextRows, { view: view, t: t }), settings.options ? _jsx(FieldSettingsOptionsRow, { view: view, t: t }) : null, _jsx(FieldSettingsDefaultRows, { view: view, t: t }), _jsx(FieldSettingsLimitRows, { view: view, t: t }), settings.upload ? _jsx(FieldSettingsUploadRows, { view: view, t: t }) : null] }));
20
19
  }
21
20
  //# sourceMappingURL=FieldSettingsSection.js.map
@@ -21,7 +21,7 @@ export interface FormEditorFieldsBodyProps {
21
21
  onCopyEmbed: () => void;
22
22
  copyFeedback: string | null;
23
23
  }
24
- export declare function FormEditorFieldsBody(props: FormEditorFieldsBodyProps, _optional?: {}): import("react").JSX.Element;
24
+ export declare function FormEditorFieldsBody(props: FormEditorFieldsBodyProps): import("react").JSX.Element;
25
25
  /** The page-header title/description text — split out because `FormEditor`'s isNew-dependent copy
26
26
  * (two ternaries, one with a `||` fallback) is otherwise indistinguishable, in the complexity
27
27
  * count, from the branches that actually decide what's on screen. */
@@ -1,24 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useFormFieldsBodyView } from "../hooks/forms-view.hooks.js";
3
3
  import { agentHandle } from '@jini-ai/agentic';
4
- import { FormFieldsEditor } from "./FormFieldsEditor.js";
5
- export function FormEditorFieldsBody(props, _optional = {}) {
4
+ import { FormEditorActions, FormEditorModeSwitch } from "./FormEditorFieldsBodyParts.js";
5
+ export function FormEditorFieldsBody(props) {
6
6
  const handlers = useFormFieldsBodyView(props);
7
- const { name, onNameChange, slug, onSlugChange, isNew, fields, existingFieldIds, onFieldsChange, form, saving, onStatusToggle, onSave, t, mode, onModeChange, html, onHtmlChange, onCopyEmbed, copyFeedback, } = props;
7
+ const { name, slug, isNew, t } = props;
8
8
  return (_jsxs(_Fragment, { children: [_jsx("div", { className: "field-group", children: _jsxs("div", { className: "field-row", children: [_jsxs("div", { className: "field", children: [_jsx("label", { className: "field-label", htmlFor: "form-name", children: t("Name") }), _jsx("input", { id: "form-name", value: name, onChange: handlers.onName, ...agentHandle({ handle: "form-editor-name" }, { role: "field", label: "This form's display name" }) })] }), _jsxs("div", { className: "field", children: [_jsx("label", { className: "field-label", htmlFor: "form-slug", children: t("Slug") }), _jsx("input", { id: "form-slug", value: slug, disabled: !isNew, onChange: handlers.onSlug, ...agentHandle({ handle: "form-editor-slug" }, {
9
9
  role: "field",
10
10
  label: "This form's URL slug — only editable while creating a new form",
11
- }) })] })] }) }), _jsxs("div", { className: "field-group", children: [_jsxs("div", { className: "segmented", role: "group", "aria-label": t("Form authoring mode"), children: [_jsx("button", { type: "button", className: mode === "builder" ? "is-active" : "", "aria-pressed": mode === "builder", onClick: handlers.onBuilder, children: t("Builder") }), _jsx("button", { type: "button", className: mode === "html" ? "is-active" : "", "aria-pressed": mode === "html", onClick: handlers.onHtmlMode, children: t("HTML") })] }), mode === "html" ? (_jsx("textarea", { className: "page-html-source", value: html, onChange: handlers.onHtml, spellCheck: false, "aria-label": t("Form HTML") })) : (_jsx("div", { className: "table-scroll", children: _jsx(FormFieldsEditor, { fields: fields, existingFieldIds: existingFieldIds, onChange: onFieldsChange, t: t }) }))] }), _jsxs("div", { className: "editor-actions form-actions", children: [form ? _jsx("button", { type: "button", className: "btn-secondary", onClick: onCopyEmbed, children: t("Copy HTML embed") }) : null, copyFeedback ? _jsx("span", { role: "status", children: copyFeedback }) : null, !isNew && form ? (
12
- // Reversible-but-access-affecting (turns off the live site's ability to accept
13
- // submissions through this form) — `.btn-warning`, not `.btn-danger`: nothing is
14
- // deleted, and the same control flips right back to "Enable". Re-enabling is the safe
15
- // direction, so it stays `.btn-secondary` rather than inheriting the warning look.
16
- _jsx("button", { type: "button", className: form.status === "active" ? "btn-warning" : "btn-secondary", disabled: saving, onClick: onStatusToggle, ...agentHandle({ handle: "form-editor-status-toggle" }, {
17
- role: "button",
18
- label: "Disable or re-enable this form — disabling stops it accepting new submissions without deleting it",
19
- }), children: form.status === "active" ? t("Disable") : t("Enable") })) : null, _jsx("button", { type: "button", disabled: saving, onClick: onSave, ...agentHandle({ handle: "form-editor-save" }, {
20
- role: "button",
21
- label: "Save this form's name, slug, fields and notification settings",
22
- }), children: isNew ? t("Create form") : t("Save") })] })] }));
11
+ }) })] })] }) }), _jsx("div", { className: "field-group", children: _jsx(FormEditorModeSwitch, { props: props, handlers: handlers }) }), _jsx(FormEditorActions, { props: props })] }));
23
12
  }
24
13
  //# sourceMappingURL=FormEditorFieldsBody.js.map
@@ -0,0 +1,18 @@
1
+ import type { useFormFieldsBodyView } from "../hooks/forms-view.hooks.js";
2
+ import type { FormEditorFieldsBodyProps } from "./FormEditorFieldsBody.js";
3
+ /**
4
+ * @file The two halves of `FormEditorFieldsBody` that branch on its props — the Builder/HTML switch
5
+ * and the action row — split out so each one's branches are counted on their own. Markup only.
6
+ */
7
+ type FieldsBodyHandlers = ReturnType<typeof useFormFieldsBodyView>;
8
+ /** Builder/HTML switch plus whichever editor the current mode shows. */
9
+ export declare function FormEditorModeSwitch({ props, handlers }: {
10
+ props: FormEditorFieldsBodyProps;
11
+ handlers: FieldsBodyHandlers;
12
+ }): import("react").JSX.Element;
13
+ /** Copy embed, copy feedback, Disable/Enable (saved forms only) and Save/Create. */
14
+ export declare function FormEditorActions({ props }: {
15
+ props: FormEditorFieldsBodyProps;
16
+ }): import("react").JSX.Element;
17
+ export {};
18
+ //# sourceMappingURL=FormEditorFieldsBodyParts.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { agentHandle } from '@jini-ai/agentic';
3
+ import { FormFieldsEditor } from "./FormFieldsEditor.js";
4
+ /** Builder/HTML switch plus whichever editor the current mode shows. */
5
+ export function FormEditorModeSwitch({ props, handlers }) {
6
+ const { fields, existingFieldIds, onFieldsChange, t, mode, html } = props;
7
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "segmented", role: "group", "aria-label": t("Form authoring mode"), children: [_jsx("button", { type: "button", className: mode === "builder" ? "is-active" : "", "aria-pressed": mode === "builder", onClick: handlers.onBuilder, children: t("Builder") }), _jsx("button", { type: "button", className: mode === "html" ? "is-active" : "", "aria-pressed": mode === "html", onClick: handlers.onHtmlMode, children: t("HTML") })] }), mode === "html" ? (_jsx("textarea", { className: "page-html-source", value: html, onChange: handlers.onHtml, spellCheck: false, "aria-label": t("Form HTML") })) : (_jsx("div", { className: "table-scroll", children: _jsx(FormFieldsEditor, { fields: fields, existingFieldIds: existingFieldIds, onChange: onFieldsChange, t: t }) }))] }));
8
+ }
9
+ /** Copy embed, copy feedback, Disable/Enable (saved forms only) and Save/Create. */
10
+ export function FormEditorActions({ props }) {
11
+ const { isNew, form, saving, onStatusToggle, onSave, t, onCopyEmbed, copyFeedback } = props;
12
+ /* `form-actions` is a spacing-only hook layered on top of the shared `.editor-actions`
13
+ flex row. `.editor-actions` sets direction/gap/alignment but no top margin, and this row
14
+ is not a `.field-group`, so the `.field-group + .field-group` rhythm that separates every
15
+ other block on this screen skips it — leaving Disable/Save flush against the Recipients
16
+ input. Fixed here rather than by adding a margin to `.editor-actions` itself, because that
17
+ class is shared with the other editor screens and a global change would shift spacing on
18
+ screens nobody has looked at yet. */
19
+ return (_jsxs("div", { className: "editor-actions form-actions", children: [form ? _jsx("button", { type: "button", className: "btn-secondary", onClick: onCopyEmbed, children: t("Copy HTML embed") }) : null, copyFeedback ? _jsx("span", { role: "status", children: copyFeedback }) : null, !isNew && form ? (
20
+ // Reversible-but-access-affecting (turns off the live site's ability to accept
21
+ // submissions through this form) — `.btn-warning`, not `.btn-danger`: nothing is
22
+ // deleted, and the same control flips right back to "Enable". Re-enabling is the safe
23
+ // direction, so it stays `.btn-secondary` rather than inheriting the warning look.
24
+ _jsx("button", { type: "button", className: form.status === "active" ? "btn-warning" : "btn-secondary", disabled: saving, onClick: onStatusToggle, ...agentHandle({ handle: "form-editor-status-toggle" }, {
25
+ role: "button",
26
+ label: "Disable or re-enable this form — disabling stops it accepting new submissions without deleting it",
27
+ }), children: form.status === "active" ? t("Disable") : t("Enable") })) : null, _jsx("button", { type: "button", disabled: saving, onClick: onSave, ...agentHandle({ handle: "form-editor-save" }, {
28
+ role: "button",
29
+ label: "Save this form's name, slug, fields and notification settings",
30
+ }), children: isNew ? t("Create form") : t("Save") })] }));
31
+ }
32
+ //# sourceMappingURL=FormEditorFieldsBodyParts.js.map
@@ -8,5 +8,5 @@ export interface FormSubmissionsProps {
8
8
  t: (key: string) => string;
9
9
  answerColumns?: FormAnswerColumn[];
10
10
  }
11
- export declare function FormSubmissions({ formId, useFormSubmissionsHook, t, answerColumns }: FormSubmissionsProps, _optional?: Record<string, never>): import("react").JSX.Element;
11
+ export declare function FormSubmissions({ formId, useFormSubmissionsHook, t, answerColumns }: FormSubmissionsProps): import("react").JSX.Element;
12
12
  //# sourceMappingURL=FormSubmissions.d.ts.map
@@ -1,12 +1,10 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useSubmissionsView } from "../hooks/forms-view.hooks.js";
3
- import { DataTable } from '../../../react/components/DataTable.js';
4
- import { agentHandle } from '@jini-ai/agentic';
5
3
  import { useWiredFormSubmissions } from '../hooks/use-form-submissions.hooks.js';
6
4
  import { useFormSubmissionDate } from '../hooks/use-form-submission-date.hooks.js';
7
5
  import { FormSubmissionDetail } from "./FormSubmissionDetail.js";
8
- import { FormSubmissionDate } from "./FormSubmissionDate.js";
9
- export function FormSubmissions({ formId, useFormSubmissionsHook = useWiredFormSubmissions, t, answerColumns = [] }, _optional = {}) {
6
+ import { FormSubmissionsTable } from "./FormSubmissionsTable.js";
7
+ export function FormSubmissions({ formId, useFormSubmissionsHook = useWiredFormSubmissions, t, answerColumns = [] }) {
10
8
  const formatDate = useFormSubmissionDate({});
11
9
  const controller = useFormSubmissionsHook({ formId });
12
10
  const { submissions, nextCursor, error, selectedId, loadingMore } = controller;
@@ -27,20 +25,6 @@ export function FormSubmissions({ formId, useFormSubmissionsHook = useWiredFormS
27
25
  // own post-early-return computation. `submissions` grows only on an explicit "Load more" click
28
26
  // (`use-form-submissions.hooks.ts`'s cursor-append), so this O(n) pass tracks real data changes,
29
27
  // not incidental re-renders.
30
- return (_jsxs("div", { children: [error ? _jsx("div", { className: "notice error", children: error }) : null, _jsx(DataTable, { rows: rows, rowKey: (s) => s.id, columns: [
31
- { key: "submitted-at", header: t("Submitted at"), cell: (s) => _jsx(FormSubmissionDate, { date: formatDate({ iso: s.submittedAt }) }) },
32
- { key: "source-ip", header: t("Source IP"), cell: (s) => s.sourceIp },
33
- ...answerColumns,
34
- {
35
- key: "view",
36
- cell: (s, index) => (_jsx("button", { type: "button", onClick: s.view, ...agentHandle({ handle: s.handle }, {
37
- role: "button",
38
- label: "Open this submission's full details",
39
- }), children: t("View") })),
40
- },
41
- ] }), nextCursor ? (_jsx("button", { type: "button", className: "btn-secondary", disabled: loadingMore, onClick: loadMore, ...agentHandle({ handle: "form-submissions-load-more" }, {
42
- role: "button",
43
- label: "Load the next page of submissions",
44
- }), children: loadingMore ? t("Loading…") : t("Load more") })) : null] }));
28
+ return (_jsx(FormSubmissionsTable, { rows: rows, loadMore: loadMore, error: error, nextCursor: nextCursor, loadingMore: loadingMore, formatDate: formatDate, answerColumns: answerColumns, t: t }));
45
29
  }
46
30
  //# sourceMappingURL=FormSubmissions.js.map
@@ -0,0 +1,19 @@
1
+ import type { FormAnswerColumn } from '../../html-rules.js';
2
+ import type { useSubmissionsView } from "../hooks/forms-view.hooks.js";
3
+ import type { useFormSubmissionDate } from '../hooks/use-form-submission-date.hooks.js';
4
+ /**
5
+ * @file The loaded-list half of `FormSubmissions`: an optional error above the table, the table
6
+ * itself, and "Load more" while a next page exists. Markup only.
7
+ */
8
+ export interface FormSubmissionsTableProps {
9
+ rows: ReturnType<typeof useSubmissionsView>["rows"];
10
+ loadMore: () => void;
11
+ error: string | null;
12
+ nextCursor: string | null;
13
+ loadingMore: boolean;
14
+ formatDate: ReturnType<typeof useFormSubmissionDate>;
15
+ answerColumns: FormAnswerColumn[];
16
+ t: (key: string) => string;
17
+ }
18
+ export declare function FormSubmissionsTable({ rows, loadMore, error, nextCursor, loadingMore, formatDate, answerColumns, t }: FormSubmissionsTableProps): import("react").JSX.Element;
19
+ //# sourceMappingURL=FormSubmissionsTable.d.ts.map
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { DataTable } from '../../../react/components/DataTable.js';
3
+ import { agentHandle } from '@jini-ai/agentic';
4
+ import { FormSubmissionDate } from "./FormSubmissionDate.js";
5
+ export function FormSubmissionsTable({ rows, loadMore, error, nextCursor, loadingMore, formatDate, answerColumns, t }) {
6
+ return (_jsxs("div", { children: [error ? _jsx("div", { className: "notice error", children: error }) : null, _jsx(DataTable, { rows: rows, rowKey: (s) => s.id, columns: [
7
+ { key: "submitted-at", header: t("Submitted at"), cell: (s) => _jsx(FormSubmissionDate, { date: formatDate({ iso: s.submittedAt }) }) },
8
+ { key: "source-ip", header: t("Source IP"), cell: (s) => s.sourceIp },
9
+ ...answerColumns,
10
+ {
11
+ key: "view",
12
+ cell: (s) => (_jsx("button", { type: "button", onClick: s.view, ...agentHandle({ handle: s.handle }, {
13
+ role: "button",
14
+ label: "Open this submission's full details",
15
+ }), children: t("View") })),
16
+ },
17
+ ] }), nextCursor ? (_jsx("button", { type: "button", className: "btn-secondary", disabled: loadingMore, onClick: loadMore, ...agentHandle({ handle: "form-submissions-load-more" }, {
18
+ role: "button",
19
+ label: "Load the next page of submissions",
20
+ }), children: loadingMore ? t("Loading…") : t("Load more") })) : null] }));
21
+ }
22
+ //# sourceMappingURL=FormSubmissionsTable.js.map
@@ -1,4 +1,14 @@
1
1
  import { queryMedia } from '../rules.js';
2
+ const METADATA_KEYS = ['title', 'slug', 'alt', 'caption', 'credit', 'width', 'height', 'cssClass', 'htmlAttributes'];
3
+ /** Only metadata keys with a defined value; null stays, so a patch can clear a nullable field. */
4
+ function pickMetadata({ patch }) {
5
+ return Object.fromEntries(Object.entries(patch).filter(([key, value]) => METADATA_KEYS.includes(key) && value !== undefined));
6
+ }
7
+ /** Decoded length of a base64 string: 3 bytes per 4 chars, minus trailing padding. */
8
+ function base64ByteSize({ dataBase64 }) {
9
+ const padding = dataBase64.match(/=+$/)?.[0].length ?? 0;
10
+ return Math.max(0, Math.floor(dataBase64.length * 3 / 4) - padding);
11
+ }
2
12
  export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().toISOString() } = {}) {
3
13
  const items = new Map(assets.map((item) => [item.id, Object.freeze({ ...item })]));
4
14
  let sequence = 0;
@@ -20,7 +30,7 @@ export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().t
20
30
  // The REAL server ignores the declared string and stores sniffContentType(bytes) instead.
21
31
  // This fake has no bytes to sniff — its input is an opaque base64 string a test made up —
22
32
  // so it echoes what the caller declared. Tests needing disagreement must seed assets directly.
23
- return { byteSize: Math.max(0, Math.floor(input.dataBase64.length * 3 / 4) - (input.dataBase64.match(/=+$/)?.[0].length ?? 0)), contentType: input.contentType, sha256: `memory-content-${++sequence}` };
33
+ return { byteSize: base64ByteSize(input), contentType: input.contentType, sha256: `memory-content-${++sequence}` };
24
34
  }
25
35
  return {
26
36
  replaceSupported: true,
@@ -38,15 +48,16 @@ export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().t
38
48
  const timestamp = now();
39
49
  return put({
40
50
  id,
41
- title: options.title ?? input.filename,
42
- alt: options.alt ?? '',
43
- caption: options.caption ?? '',
44
- credit: options.credit ?? '',
45
- slug: options.slug ?? id,
46
- width: options.width ?? null,
47
- height: options.height ?? null,
48
- cssClass: options.cssClass ?? null,
49
- htmlAttributes: options.htmlAttributes ?? null,
51
+ title: input.filename,
52
+ alt: '',
53
+ caption: '',
54
+ credit: '',
55
+ slug: id,
56
+ width: null,
57
+ height: null,
58
+ cssClass: null,
59
+ htmlAttributes: null,
60
+ ...pickMetadata({ patch: options }),
50
61
  status: 'active',
51
62
  publicUrl: null,
52
63
  createdAt: timestamp,
@@ -58,14 +69,13 @@ export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().t
58
69
  async update({ id, patch }, options = {}) {
59
70
  check(options);
60
71
  const previous = get(id);
61
- const metadata = Object.fromEntries(Object.entries(patch).filter(([key, value]) => ['title', 'slug', 'alt', 'caption', 'credit', 'width', 'height', 'cssClass', 'htmlAttributes'].includes(key) && value !== undefined));
62
- return put({ ...previous, ...metadata, version: previous.version + 1, updatedAt: now() });
72
+ return put({ ...previous, ...pickMetadata({ patch }), version: previous.version + 1, updatedAt: now() });
63
73
  },
64
74
  async replace({ id, upload }, options = {}) {
65
75
  check(options);
66
76
  const previous = get(id);
67
77
  if (previous.status !== 'active')
68
- throw new Error('Cannot replace trashed media');
78
+ throw new Error(`Cannot replace trashed media: ${id} (restore it first)`);
69
79
  return put({
70
80
  ...previous,
71
81
  ...content(upload),
@@ -92,7 +102,7 @@ export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().t
92
102
  async delete({ id }, options = {}) {
93
103
  check(options);
94
104
  if (get(id).status !== 'trashed')
95
- throw new Error('Trash media before permanent deletion');
105
+ throw new Error(`Trash media before permanent deletion: ${id}`);
96
106
  items.delete(id);
97
107
  return { purged: true };
98
108
  },
@@ -102,11 +112,11 @@ export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().t
102
112
  }
103
113
  export function createMemoryMediaProviders({ providers = [] }, _optional = {}) {
104
114
  const items = new Map(providers.map((item) => [item.id, Object.freeze({ ...item })]));
105
- function write(id, configured) {
115
+ function write(id, patch) {
106
116
  const previous = items.get(id);
107
117
  if (!previous)
108
- throw new Error('Unknown provider');
109
- const next = Object.freeze({ ...previous, configured });
118
+ throw new Error(`Unknown media provider: ${id}`);
119
+ const next = Object.freeze({ ...previous, ...patch });
110
120
  items.set(id, next);
111
121
  return next;
112
122
  }
@@ -117,22 +127,17 @@ export function createMemoryMediaProviders({ providers = [] }, _optional = {}) {
117
127
  },
118
128
  async saveSettings({ id, baseUrl, model }, options = {}) {
119
129
  options.signal?.throwIfAborted();
120
- const previous = items.get(id);
121
- if (!previous)
122
- throw new Error('Unknown provider');
123
- const next = Object.freeze({ ...previous, baseUrl, model });
124
- items.set(id, next);
125
- return next;
130
+ return write(id, { baseUrl, model });
126
131
  },
127
132
  async saveCredential({ id, credential }, options = {}) {
128
133
  options.signal?.throwIfAborted();
129
134
  if (!credential.trim())
130
- throw new Error('Credential is required');
131
- return write(id, true);
135
+ throw new Error(`Credential is required for media provider: ${id}`);
136
+ return write(id, { configured: true });
132
137
  },
133
138
  async removeCredential({ id }, options = {}) {
134
139
  options.signal?.throwIfAborted();
135
- return write(id, false);
140
+ return write(id, { configured: false });
136
141
  },
137
142
  };
138
143
  }
@@ -31,11 +31,12 @@ export function createEditMediaController({ api, item }, _optional = {}) {
31
31
  store.set({
32
32
  patch: { error: error instanceof Error ? error.message : 'Failed to save media metadata' },
33
33
  });
34
- return false;
35
34
  }
36
35
  finally {
37
36
  store.set({ patch: { saving: false } });
38
37
  }
38
+ // Reached only through the catch above: the failure is already recorded in `error`.
39
+ return false;
39
40
  }
40
41
  return {
41
42
  getSnapshot: store.getSnapshot,