@jini-ai/admin 0.4.12 → 0.6.10

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 +70 -2
  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 +76 -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 +23 -14
  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
@@ -7,6 +7,23 @@ export async function runFormsApiConformance({ api }, { slug = 'forms-conformanc
7
7
  passed++;
8
8
  else
9
9
  failures.push(name); };
10
+ const created = await checkCreateAndRead({ api, slug, check });
11
+ await checkUpdates({ api, created, check });
12
+ if (submission)
13
+ await checkSubmissions({ api, created, submission, check });
14
+ return { passed, failures };
15
+ }
16
+ /** Resolves true when the operation is refused; only that probe's rejection is absorbed. */
17
+ async function rejects({ operation }) {
18
+ try {
19
+ await operation();
20
+ return false;
21
+ }
22
+ catch {
23
+ return true;
24
+ }
25
+ }
26
+ async function checkCreateAndRead({ api, slug, check }) {
10
27
  const field = { id: 'email', label: 'Email', type: 'email', required: true, className: 'wide', attributes: { 'aria-label': 'Email' } };
11
28
  const created = await api.createFormDefinition({ name: 'Contact', slug, fields: [field] }, { notify: { enabled: true, recipients: ['owner@example.com'] } });
12
29
  check('create returns requested definition', created.name === 'Contact' && created.slug === slug && created.status === 'active');
@@ -16,56 +33,35 @@ export async function runFormsApiConformance({ api }, { slug = 'forms-conformanc
16
33
  const snapshot = await api.getFormDefinition({ id: created.id });
17
34
  Object.assign(snapshot.fields[0], { label: 'Changed only in the read snapshot' });
18
35
  check('read snapshots cannot mutate stored fields', (await api.getFormDefinition({ id: created.id })).fields[0]?.label === 'Email');
19
- const updated = await api.updateFormDefinition({ id: created.id }, { name: 'Renamed', fields: [...created.fields, { id: 'message', label: 'Message', type: 'textarea', required: false }] });
36
+ return created;
37
+ }
38
+ async function checkUpdates({ api, created, check }) {
39
+ const { id } = created;
40
+ const updated = await api.updateFormDefinition({ id }, { name: 'Renamed', fields: [...created.fields, { id: 'message', label: 'Message', type: 'textarea', required: false }] });
20
41
  check('name patch and added field persist', updated.name === 'Renamed' && updated.fields.length === 2);
21
42
  check('untouched notify survives update', updated.notify.enabled && updated.notify.recipients[0] === 'owner@example.com');
22
- let refused = false;
23
- try {
24
- await api.updateFormDefinition({ id: created.id }, { fields: [] });
25
- }
26
- catch {
27
- refused = true;
28
- }
29
- check('existing fields cannot be removed', refused);
30
- check('rejected removal leaves fields intact', (await api.getFormDefinition({ id: created.id })).fields.length === 2);
31
- const disabled = await api.updateFormDefinition({ id: created.id }, { status: 'disabled' });
32
- check('definition disable is reversible', disabled.status === 'disabled' && (await api.updateFormDefinition({ id: created.id }, { status: 'active' })).status === 'active');
33
- const html = await api.updateFormDefinition({ id: created.id }, { mode: 'html', html: '<input name="email">' });
43
+ check('existing fields cannot be removed', await rejects({ operation: () => api.updateFormDefinition({ id }, { fields: [] }) }));
44
+ check('rejected removal leaves fields intact', (await api.getFormDefinition({ id })).fields.length === 2);
45
+ const disabled = await api.updateFormDefinition({ id }, { status: 'disabled' });
46
+ check('definition disable is reversible', disabled.status === 'disabled' && (await api.updateFormDefinition({ id }, { status: 'active' })).status === 'active');
47
+ const html = await api.updateFormDefinition({ id }, { mode: 'html', html: '<input name="email">' });
34
48
  check('HTML authoring survives update', html.mode === 'html' && html.html === '<input name="email">');
35
- check('builder switch is explicit', (await api.updateFormDefinition({ id: created.id }, { mode: 'builder' })).mode === 'builder');
36
- check('empty submission page has null cursor', (await api.listFormSubmissions({ formId: created.id })).nextCursor === null);
49
+ check('builder switch is explicit', (await api.updateFormDefinition({ id }, { mode: 'builder' })).mode === 'builder');
50
+ check('empty submission page has null cursor', (await api.listFormSubmissions({ formId: id })).nextCursor === null);
37
51
  check('definition has no delete operation', !('deleteFormDefinition' in api));
38
- if (submission) {
39
- const page = await api.listFormSubmissions({ formId: submission.formId }, { limit: 1 });
40
- check('submission page is form scoped', page.items.every(item => item.formDefinitionId === submission.formId));
41
- check('submission detail is scoped', (await api.getFormSubmission(submission)).formDefinitionId === submission.formId);
42
- let wrongFormRejected = false;
43
- try {
44
- await api.getFormSubmission({ formId: created.id, submissionId: submission.submissionId });
45
- }
46
- catch {
47
- wrongFormRejected = true;
48
- }
49
- check('a submission of another form is unavailable', wrongFormRejected);
50
- let wrongFormDeleteRejected = false;
51
- try {
52
- await api.deleteFormSubmission({ formId: created.id, submissionId: submission.submissionId });
53
- }
54
- catch {
55
- wrongFormDeleteRejected = true;
56
- }
57
- check('cross-form trash is rejected without removing the submission', wrongFormDeleteRejected && (await api.getFormSubmission(submission)).id === submission.submissionId);
52
+ }
53
+ async function checkSubmissions({ api, created, submission, check }) {
54
+ const crossForm = { formId: created.id, submissionId: submission.submissionId };
55
+ const page = await api.listFormSubmissions({ formId: submission.formId }, { limit: 1 });
56
+ check('submission page is form scoped', page.items.every(item => item.formDefinitionId === submission.formId));
57
+ check('submission detail is scoped', (await api.getFormSubmission(submission)).formDefinitionId === submission.formId);
58
+ check('a submission of another form is unavailable', await rejects({ operation: () => api.getFormSubmission(crossForm) }));
59
+ const wrongFormDeleteRejected = await rejects({ operation: () => api.deleteFormSubmission(crossForm) });
60
+ const submissionRetained = await api.getFormSubmission(submission).then(item => item.id === submission.submissionId, () => false);
61
+ check('cross-form trash is rejected without removing the submission', wrongFormDeleteRejected && submissionRetained);
62
+ if (submissionRetained)
58
63
  await api.deleteFormSubmission(submission);
59
- let missing = false;
60
- try {
61
- await api.getFormSubmission(submission);
62
- }
63
- catch {
64
- missing = true;
65
- }
66
- check('trashed submission detail is unavailable', missing);
67
- check('trashed submission is absent from active pages', !(await api.listFormSubmissions({ formId: submission.formId })).items.some(item => item.id === submission.submissionId));
68
- }
69
- return { passed, failures };
64
+ check('trashed submission detail is unavailable', await rejects({ operation: () => api.getFormSubmission(submission) }));
65
+ check('trashed submission is absent from active pages', !(await api.listFormSubmissions({ formId: submission.formId })).items.some(item => item.id === submission.submissionId));
70
66
  }
71
67
  //# sourceMappingURL=forms-api.conformance.js.map
@@ -23,6 +23,20 @@ function defaultKindOf(kind) {
23
23
  return "temporal";
24
24
  return kind.control === "textarea" ? "textarea" : "text";
25
25
  }
26
+ const isNumeric = ({ kind }) => kind.htmlType === "number" || kind.htmlType === "range";
27
+ /** Native input type for a kind's min/max; number and range share numeric bounds. */
28
+ function boundsOf({ kind }) {
29
+ if (!BOUNDS.has(kind.htmlType))
30
+ return null;
31
+ return isNumeric({ kind }) ? "number" : kind.htmlType;
32
+ }
33
+ /** Whether a (non-hidden) kind shows a placeholder. */
34
+ function takesPlaceholder({ kind }) {
35
+ // A single-choice select shows its placeholder as the empty first option.
36
+ if (kind.control === "select")
37
+ return !kind.multiple;
38
+ return kind.textual === true || kind.htmlType === "number";
39
+ }
26
40
  /**
27
41
  * Settings for one type, from its registry kind. Unknown (legacy or host-added) types get help
28
42
  * text only, so the editor never offers a setting the server would refuse.
@@ -33,15 +47,13 @@ export function fieldSettingsFor({ type }, _optional = {}) {
33
47
  if (!kind)
34
48
  return NO_SETTINGS;
35
49
  const hidden = kind.htmlType === "hidden";
36
- const numeric = kind.htmlType === "number" || kind.htmlType === "range";
37
- const bounds = BOUNDS.has(kind.htmlType) ? (numeric ? "number" : kind.htmlType) : null;
50
+ const numeric = isNumeric({ kind });
38
51
  return {
39
- // A single-choice select shows its placeholder as the empty first option.
40
- placeholder: !hidden && ((kind.textual === true && kind.control !== "select") || kind.htmlType === "number" || (kind.control === "select" && !kind.multiple)),
52
+ placeholder: !hidden && takesPlaceholder({ kind }),
41
53
  helpText: !hidden,
42
54
  length: kind.textual === true,
43
55
  pattern: kind.textual === true && !hidden,
44
- bounds,
56
+ bounds: boundsOf({ kind }),
45
57
  step: numeric,
46
58
  options: kind.choices === true,
47
59
  multiple: kind.multiple === true,
@@ -96,53 +108,45 @@ const SEED_OPTIONS = [{ value: "Option 1", label: "Option 1" }, { value: "Option
96
108
  export function retypeField({ field, type }, _optional = {}) {
97
109
  const before = fieldSettingsFor({ type: field.type });
98
110
  const after = fieldSettingsFor({ type });
99
- const next = { id: field.id, label: field.label, type, required: after.helpText ? field.required : false };
100
- if (after.helpText && field.helpText !== undefined)
101
- next.helpText = field.helpText;
102
- if (field.className !== undefined)
103
- next.className = field.className;
104
- if (field.attributes !== undefined)
105
- next.attributes = { ...field.attributes };
106
- if (after.placeholder && field.placeholder !== undefined)
107
- next.placeholder = field.placeholder;
108
- if (after.length) {
109
- if (field.maxLength != null)
110
- next.maxLength = field.maxLength;
111
- if (field.minLength !== undefined)
112
- next.minLength = field.minLength;
113
- }
114
- if (after.pattern && field.pattern !== undefined)
115
- next.pattern = field.pattern;
116
- if (after.bounds && after.bounds === before.bounds) {
117
- if (field.min !== undefined)
118
- next.min = field.min;
119
- if (field.max !== undefined)
120
- next.max = field.max;
121
- }
122
- if (after.step && field.step !== undefined)
123
- next.step = field.step;
124
- if (after.options)
125
- next.options = field.options?.length ? field.options.map((option) => ({ ...option })) : SEED_OPTIONS.map((option) => ({ ...option }));
126
- if (after.upload) {
127
- if (field.accept !== undefined)
128
- next.accept = [...field.accept];
129
- if (field.maxFileSize !== undefined)
130
- next.maxFileSize = field.maxFileSize;
111
+ return {
112
+ id: field.id, label: field.label, type, required: after.helpText ? field.required : false,
113
+ ...setKeys({ field, keys: retainedKeys({ field, before, after }) }),
114
+ ...(after.options ? { options: (field.options?.length ? field.options : SEED_OPTIONS).map((option) => ({ ...option })) } : {}),
115
+ ...setKeys({ field, keys: after.upload ? ["accept", "maxFileSize"] : [] }),
116
+ // A default only survives when its shape is unchanged (text to text, one choice to one choice).
117
+ ...(keepsDefault({ before, after }) && field.default !== undefined ? { default: field.default } : {}),
118
+ };
119
+ }
120
+ const when = (applies, keys) => (applies ? keys : []);
121
+ /** The settings before the options that a retype keeps, in the field's key order. */
122
+ function retainedKeys({ field, before, after }) {
123
+ return [
124
+ ...when(after.helpText, ["helpText"]), "className", "attributes", ...when(after.placeholder, ["placeholder"]),
125
+ // A null max length (the wire's "cleared") is dropped rather than carried.
126
+ ...when(after.length && field.maxLength != null, ["maxLength"]), ...when(after.length, ["minLength"]),
127
+ ...when(after.pattern, ["pattern"]),
128
+ ...when(after.bounds !== null && after.bounds === before.bounds, ["min", "max"]),
129
+ ...when(after.step, ["step"]),
130
+ ];
131
+ }
132
+ const keepsDefault = ({ before, after }) => after.defaultKind !== null && after.defaultKind === before.defaultKind && after.bounds === before.bounds;
133
+ /** The listed keys the field has set, in order; attribute maps and accept lists are copied, not shared. */
134
+ function setKeys({ field, keys }) {
135
+ const kept = {};
136
+ for (const key of keys) {
137
+ const value = field[key];
138
+ if (value === undefined)
139
+ continue;
140
+ Object.assign(kept, { [key]: Array.isArray(value) ? [...value] : typeof value === "object" && value !== null ? { ...value } : value });
131
141
  }
132
- // A default only survives when its shape is unchanged (text to text, one choice to one choice).
133
- if (field.default !== undefined && after.defaultKind !== null && after.defaultKind === before.defaultKind && after.bounds === before.bounds)
134
- next.default = field.default;
135
- return next;
142
+ return kept;
136
143
  }
137
144
  const text = (value) => (value === undefined || value === null ? "" : String(value));
138
145
  /** Seed the draft from a stored field. @complexity O(o) in options. */
139
146
  export function settingsFormFromField({ field }, _optional = {}) {
140
- const defaults = field.default;
141
147
  return {
142
148
  placeholder: text(field.placeholder), helpText: text(field.helpText),
143
- defaultText: typeof defaults === "boolean" || Array.isArray(defaults) ? "" : text(defaults),
144
- defaultChecked: defaults === true || (field.checkboxValue !== undefined && defaults === field.checkboxValue),
145
- defaultChoices: Array.isArray(defaults) ? [...defaults] : typeof defaults === "string" && fieldSettingsFor({ type: field.type }).options ? [defaults] : [],
149
+ ...defaultDraft({ field }),
146
150
  minLength: text(field.minLength), maxLength: text(field.maxLength), pattern: text(field.pattern),
147
151
  min: text(field.min), max: text(field.max), step: text(field.step),
148
152
  options: (field.options ?? []).map((option, index) => ({ _rowId: index, value: option.value, label: option.label })),
@@ -150,6 +154,21 @@ export function settingsFormFromField({ field }, _optional = {}) {
150
154
  maxFileSizeMb: field.maxFileSize === undefined ? "" : String(Math.round((field.maxFileSize / 1024 / 1024) * 100) / 100),
151
155
  };
152
156
  }
157
+ /** The stored default as the dialog's three default controls. */
158
+ function defaultDraft({ field }) {
159
+ const defaults = field.default;
160
+ return {
161
+ defaultText: typeof defaults === "boolean" || Array.isArray(defaults) ? "" : text(defaults),
162
+ defaultChecked: defaults === true || (field.checkboxValue !== undefined && defaults === field.checkboxValue),
163
+ defaultChoices: defaultChoicesOf({ field }),
164
+ };
165
+ }
166
+ function defaultChoicesOf({ field }) {
167
+ const defaults = field.default;
168
+ if (Array.isArray(defaults))
169
+ return [...defaults];
170
+ return typeof defaults === "string" && fieldSettingsFor({ type: field.type }).options ? [defaults] : [];
171
+ }
153
172
  /**
154
173
  * Edit one option row. A value that still mirrors its label follows label edits, so typing labels
155
174
  * is enough for most forms; an edited value stays put.
@@ -199,59 +218,74 @@ function optionalNumber(value, error, { integer = false } = {}) {
199
218
  return Number.isFinite(number) && (!integer || (Number.isInteger(number) && number >= 0)) ? { ok: true, value: number } : { ok: false, error };
200
219
  }
201
220
  /** Text length, pattern and placeholder settings. */
202
- function textPatch(form, settings) {
203
- const patch = { placeholder: settings.placeholder && !isBlank(form.placeholder) ? form.placeholder : undefined, pattern: undefined, minLength: undefined };
204
- if (settings.length) {
205
- const min = optionalNumber(form.minLength, "Length must be a whole number.", { integer: true });
206
- const max = optionalNumber(form.maxLength, "Length must be a whole number.", { integer: true });
207
- if (!min.ok)
208
- return min;
209
- if (!max.ok)
210
- return max;
211
- if (min.value !== undefined && max.value !== undefined && min.value > max.value)
212
- return { ok: false, error: "Minimum must not be above maximum." };
213
- patch.minLength = min.value;
214
- // The wire contract clears a text length with null; unsupported types omit the key entirely.
215
- patch.maxLength = max.value ?? null;
216
- }
217
- if (settings.pattern && !isBlank(form.pattern)) {
218
- // Browser-safe too: a `v`-flag-invalid pattern is silently ignored by browsers (2026-10-11).
219
- if (!isBrowserSafeFieldPattern({ pattern: form.pattern }))
220
- return { ok: false, error: "That pattern isn't supported." };
221
- patch.pattern = form.pattern;
222
- }
223
- return { ok: true, value: patch };
221
+ function textPatch({ form, settings }) {
222
+ const length = settings.length ? lengthPatch({ form }) : { ok: true, value: {} };
223
+ if (!length.ok)
224
+ return length;
225
+ const pattern = settings.pattern ? patternOf({ form }) : { ok: true, value: undefined };
226
+ if (!pattern.ok)
227
+ return pattern;
228
+ return { ok: true, value: {
229
+ placeholder: settings.placeholder && !isBlank(form.placeholder) ? form.placeholder : undefined,
230
+ pattern: pattern.value, minLength: undefined, ...length.value,
231
+ } };
232
+ }
233
+ const MIN_ABOVE_MAX = { ok: false, error: "Minimum must not be above maximum." };
234
+ const isAbove = (min, max) => min !== undefined && max !== undefined && min > max;
235
+ function lengthPatch({ form }) {
236
+ const min = optionalNumber(form.minLength, "Length must be a whole number.", { integer: true });
237
+ const max = optionalNumber(form.maxLength, "Length must be a whole number.", { integer: true });
238
+ if (!min.ok)
239
+ return min;
240
+ if (!max.ok)
241
+ return max;
242
+ if (isAbove(min.value, max.value))
243
+ return MIN_ABOVE_MAX;
244
+ // The wire contract clears a text length with null; unsupported types omit the key entirely.
245
+ return { ok: true, value: { minLength: min.value, maxLength: max.value ?? null } };
246
+ }
247
+ function patternOf({ form }) {
248
+ if (isBlank(form.pattern))
249
+ return { ok: true, value: undefined };
250
+ // Browser-safe too: a `v`-flag-invalid pattern is silently ignored by browsers (2026-10-11).
251
+ if (!isBrowserSafeFieldPattern({ pattern: form.pattern }))
252
+ return { ok: false, error: "That pattern isn't supported." };
253
+ return { ok: true, value: form.pattern };
224
254
  }
225
255
  /** Numeric or date/time bounds, plus a numeric step. Native date/time strings compare as text. */
226
- function boundsPatch(form, settings) {
227
- const patch = { min: undefined, max: undefined, step: undefined };
228
- if (settings.bounds === "number") {
229
- const min = optionalNumber(form.min, "Enter numbers for min, max and step.");
230
- const max = optionalNumber(form.max, "Enter numbers for min, max and step.");
231
- const step = optionalNumber(form.step, "Enter numbers for min, max and step.");
232
- if (!min.ok)
233
- return min;
234
- if (!max.ok)
235
- return max;
236
- if (!step.ok)
237
- return step;
238
- if (step.value !== undefined && step.value <= 0)
239
- return { ok: false, error: "Step must be above 0." };
240
- if (min.value !== undefined && max.value !== undefined && min.value > max.value)
241
- return { ok: false, error: "Minimum must not be above maximum." };
242
- return { ok: true, value: { min: min.value, max: max.value, step: step.value } };
243
- }
244
- if (settings.bounds) {
245
- const min = isBlank(form.min) ? undefined : form.min;
246
- const max = isBlank(form.max) ? undefined : form.max;
247
- if (min !== undefined && max !== undefined && min > max)
248
- return { ok: false, error: "Minimum must not be above maximum." };
249
- return { ok: true, value: { min, max } };
250
- }
251
- return { ok: true, value: patch };
256
+ function boundsPatch({ form, settings }) {
257
+ if (settings.bounds === "number")
258
+ return numberBoundsPatch({ form });
259
+ if (settings.bounds)
260
+ return textBoundsPatch({ form });
261
+ return { ok: true, value: { min: undefined, max: undefined, step: undefined } };
262
+ }
263
+ function numberBoundsPatch({ form }) {
264
+ const error = "Enter numbers for min, max and step.";
265
+ const min = optionalNumber(form.min, error);
266
+ const max = optionalNumber(form.max, error);
267
+ const step = optionalNumber(form.step, error);
268
+ if (!min.ok)
269
+ return min;
270
+ if (!max.ok)
271
+ return max;
272
+ if (!step.ok)
273
+ return step;
274
+ if (step.value !== undefined && step.value <= 0)
275
+ return { ok: false, error: "Step must be above 0." };
276
+ if (isAbove(min.value, max.value))
277
+ return MIN_ABOVE_MAX;
278
+ return { ok: true, value: { min: min.value, max: max.value, step: step.value } };
279
+ }
280
+ function textBoundsPatch({ form }) {
281
+ const min = isBlank(form.min) ? undefined : form.min;
282
+ const max = isBlank(form.max) ? undefined : form.max;
283
+ if (isAbove(min, max))
284
+ return MIN_ABOVE_MAX;
285
+ return { ok: true, value: { min, max } };
252
286
  }
253
287
  /** Choice options: blank rows are skipped; the rest need a label and a unique value. */
254
- function optionsPatch(form, settings) {
288
+ function optionsPatch({ form, settings }) {
255
289
  if (!settings.options)
256
290
  return { ok: true, value: undefined };
257
291
  const options = form.options.filter((row) => !(isBlank(row.value) && isBlank(row.label))).map((row) => ({ value: row.value.trim(), label: row.label.trim() }));
@@ -262,26 +296,27 @@ function optionsPatch(form, settings) {
262
296
  }
263
297
  return { ok: true, value: options };
264
298
  }
299
+ const typedDefault = ({ form }) => ({ ok: true, value: isBlank(form.defaultText) ? undefined : form.defaultText });
300
+ const DEFAULT_READERS = {
301
+ // A box with an authored value submits that value, so its "checked" default is that value too.
302
+ boolean: ({ form, checkboxValue }) => ({ ok: true, value: form.defaultChecked ? checkboxValue ?? true : undefined }),
303
+ choice: ({ form, offered }) => ({ ok: true, value: form.defaultChoices.find((value) => offered.has(value)) }),
304
+ choices: ({ form, offered }) => {
305
+ const chosen = form.defaultChoices.filter((value) => offered.has(value));
306
+ return { ok: true, value: chosen.length ? chosen : undefined };
307
+ },
308
+ number: ({ form }) => optionalNumber(form.defaultText, "Default must be a number."),
309
+ text: typedDefault, textarea: typedDefault, temporal: typedDefault,
310
+ };
265
311
  /** The default in the stored shape for its kind; choices drop values no longer offered. */
266
- function defaultValue(form, settings, { options, checkboxValue }) {
312
+ function defaultValue({ form, settings }, { options, checkboxValue }) {
313
+ if (settings.defaultKind === null)
314
+ return { ok: true, value: undefined };
267
315
  const offered = new Set((options ?? []).map((option) => option.value));
268
- switch (settings.defaultKind) {
269
- // A box with an authored value submits that value, so its "checked" default is that value too.
270
- case "boolean": return { ok: true, value: form.defaultChecked ? checkboxValue ?? true : undefined };
271
- case "choice": return { ok: true, value: form.defaultChoices.find((value) => offered.has(value)) };
272
- case "choices": {
273
- const chosen = form.defaultChoices.filter((value) => offered.has(value));
274
- return { ok: true, value: chosen.length ? chosen : undefined };
275
- }
276
- case "number": return optionalNumber(form.defaultText, "Default must be a number.");
277
- case "text":
278
- case "textarea":
279
- case "temporal": return { ok: true, value: isBlank(form.defaultText) ? undefined : form.defaultText };
280
- default: return { ok: true, value: undefined };
281
- }
316
+ return DEFAULT_READERS[settings.defaultKind]({ form, offered, checkboxValue });
282
317
  }
283
318
  /** Upload limits: no boxes ticked means every safe type; size is whole megabytes up to the cap. */
284
- function uploadPatch(form, settings) {
319
+ function uploadPatch({ form, settings }) {
285
320
  if (!settings.upload)
286
321
  return { ok: true, value: {} };
287
322
  const size = optionalNumber(form.maxFileSizeMb, "Max size must be up to 10 MB.");
@@ -302,19 +337,19 @@ function uploadPatch(form, settings) {
302
337
  */
303
338
  export function buildFieldSettingsPatch({ form, field }, _optional = {}) {
304
339
  const settings = fieldSettingsFor({ type: field.type });
305
- const textual = textPatch(form, settings);
340
+ const textual = textPatch({ form, settings });
306
341
  if (!textual.ok)
307
342
  return textual;
308
- const bounds = boundsPatch(form, settings);
343
+ const bounds = boundsPatch({ form, settings });
309
344
  if (!bounds.ok)
310
345
  return bounds;
311
- const options = optionsPatch(form, settings);
346
+ const options = optionsPatch({ form, settings });
312
347
  if (!options.ok)
313
348
  return options;
314
- const fallback = defaultValue(form, settings, { options: options.value, checkboxValue: field.checkboxValue });
349
+ const fallback = defaultValue({ form, settings }, { options: options.value, checkboxValue: field.checkboxValue });
315
350
  if (!fallback.ok)
316
351
  return fallback;
317
- const upload = uploadPatch(form, settings);
352
+ const upload = uploadPatch({ form, settings });
318
353
  if (!upload.ok)
319
354
  return upload;
320
355
  return { ok: true, patch: {
@@ -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