@kubohiroya/turbowarp-extension-data-editor 0.1.0 → 0.2.1

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 (123) hide show
  1. package/README.md +30 -6
  2. package/dist/build-info.json +7 -2
  3. package/dist/descendants.js +11 -2
  4. package/dist/fixed-input-branches.d.ts +12 -0
  5. package/dist/fixed-input-branches.js +180 -0
  6. package/dist/fixed-input-editor.d.ts +39 -0
  7. package/dist/fixed-input-editor.js +538 -0
  8. package/dist/fixed-input-schema.d.ts +15 -0
  9. package/dist/fixed-input-schema.js +21 -0
  10. package/dist/fixed-input-snapshot.d.ts +7 -0
  11. package/dist/fixed-input-snapshot.js +180 -0
  12. package/dist/fixed-input-types.d.ts +138 -0
  13. package/dist/fixed-input-types.js +23 -0
  14. package/dist/fixed-input-view.d.ts +14 -0
  15. package/dist/fixed-input-view.js +168 -0
  16. package/dist/index.d.ts +7 -0
  17. package/dist/index.js +4 -0
  18. package/dist/json-value.d.ts +4 -0
  19. package/dist/json-value.js +12 -0
  20. package/dist/prepared-save.d.ts +5 -0
  21. package/dist/prepared-save.js +47 -0
  22. package/dist/prepared-types.d.ts +13 -0
  23. package/dist/prepared-types.js +1 -0
  24. package/dist/react.css +42 -0
  25. package/dist/react.d.ts +3 -0
  26. package/dist/react.js +10 -1
  27. package/dist/registry.d.ts +6 -0
  28. package/dist/registry.js +98 -48
  29. package/dist/rjsf.d.ts +48 -1
  30. package/dist/rjsf.js +92 -8
  31. package/dist/schema-compose.d.ts +6 -0
  32. package/dist/schema-compose.js +103 -0
  33. package/dist/schema-engine.d.ts +18 -0
  34. package/dist/schema-engine.js +298 -0
  35. package/dist/schema-json.d.ts +10 -0
  36. package/dist/schema-json.js +68 -0
  37. package/dist/schema-types.d.ts +161 -0
  38. package/dist/schema-types.js +2 -0
  39. package/dist/schema-view.d.ts +7 -0
  40. package/dist/schema-view.js +102 -0
  41. package/dist/schema.d.ts +10 -0
  42. package/dist/schema.js +87 -0
  43. package/dist/session.d.ts +3 -1
  44. package/dist/session.js +220 -9
  45. package/dist/source-document.d.ts +3 -0
  46. package/dist/source-document.js +415 -0
  47. package/dist/source-editor.d.ts +13 -0
  48. package/dist/source-editor.js +235 -0
  49. package/dist/source-schema-hints.d.ts +3 -0
  50. package/dist/source-schema-hints.js +13 -0
  51. package/dist/source-types.d.ts +47 -0
  52. package/dist/source-types.js +1 -0
  53. package/dist/source-view.d.ts +3 -0
  54. package/dist/source-view.js +318 -0
  55. package/dist/turbowarp/bridge.d.ts +2 -1
  56. package/dist/turbowarp/bridge.js +34 -5
  57. package/dist/turbowarp/fixed-fields.d.ts +82 -0
  58. package/dist/turbowarp/fixed-fields.js +311 -0
  59. package/dist/turbowarp/fixed-transaction.d.ts +18 -0
  60. package/dist/turbowarp/fixed-transaction.js +460 -0
  61. package/dist/turbowarp/host.d.ts +3 -8
  62. package/dist/turbowarp/host.js +68 -1
  63. package/dist/turbowarp.d.ts +4 -0
  64. package/dist/turbowarp.js +1 -1
  65. package/dist/types.d.ts +7 -4
  66. package/dist/view.js +4 -0
  67. package/dist/webpack4/index.js +24 -5
  68. package/dist/webpack4/metafile.json +9886 -8710
  69. package/dist/webpack4/react.js +11 -2
  70. package/dist/webpack4/react.js.map +2 -2
  71. package/dist/webpack4/rjsf.js +15205 -20951
  72. package/dist/webpack4/rjsf.js.map +4 -4
  73. package/dist/webpack4/schema.js +20 -0
  74. package/dist/webpack4/shared-75QIWLZY.js +8375 -0
  75. package/dist/webpack4/shared-75QIWLZY.js.map +7 -0
  76. package/dist/webpack4/shared-BKTRZUZH.js +42 -0
  77. package/dist/webpack4/{shared-KIFRVTSM.js.map → shared-BKTRZUZH.js.map} +1 -1
  78. package/dist/webpack4/shared-FYUILHED.js +1 -0
  79. package/dist/webpack4/shared-FYUILHED.js.map +7 -0
  80. package/dist/webpack4/shared-GQLPPRT2.js +210 -0
  81. package/dist/webpack4/shared-GQLPPRT2.js.map +7 -0
  82. package/dist/webpack4/shared-P2JBRZFP.js +2206 -0
  83. package/dist/webpack4/shared-P2JBRZFP.js.map +7 -0
  84. package/dist/webpack4/{shared-KIFRVTSM.js → shared-RN7GOT65.js} +2 -41
  85. package/dist/webpack4/shared-RN7GOT65.js.map +7 -0
  86. package/dist/webpack4/turbowarp.js +912 -9
  87. package/dist/webpack4/turbowarp.js.map +4 -4
  88. package/docs/README.md +1 -0
  89. package/docs/block-editor-e2e.md +98 -16
  90. package/docs/ci-success-gate.md +84 -0
  91. package/docs/editor/issue-12-public-validation/README.md +61 -0
  92. package/docs/editor/issue-12-public-validation/dark-invalid.png +0 -0
  93. package/docs/editor/issue-12-public-validation/dark-styles.json +173 -0
  94. package/docs/editor/issue-12-public-validation/dark-valid.png +0 -0
  95. package/docs/editor/issue-12-public-validation/deployment-target.json +5 -0
  96. package/docs/editor/issue-12-public-validation/deployment-wait.json +7 -0
  97. package/docs/editor/issue-12-public-validation/light-invalid.png +0 -0
  98. package/docs/editor/issue-12-public-validation/light-styles.json +173 -0
  99. package/docs/editor/issue-12-public-validation/light-valid.png +0 -0
  100. package/docs/editor/issue-12-public-validation/provenance.json +88 -0
  101. package/docs/editor/issue-12-public-validation/results.json +677 -0
  102. package/docs/editor/issue-12-public-validation/summary.json +206 -0
  103. package/docs/fixed-input-contracts.md +77 -0
  104. package/docs/github-pages.md +48 -41
  105. package/docs/package-distribution.md +2 -2
  106. package/docs/schema-stage1-browser.json +10 -0
  107. package/docs/schema-stage1-contracts.json +242 -0
  108. package/docs/schema-stage1-dependencies.json +202 -0
  109. package/docs/schema-stage1-git-artifact-20261018.ja.md +17 -0
  110. package/docs/schema-stage1-profile-compat.json +30 -0
  111. package/docs/schema-stage1-review-fixes-20261018.ja.md +36 -0
  112. package/docs/schema-stage1-verification.json +541 -0
  113. package/docs/schema-stage1.ja.md +80 -0
  114. package/docs/schema.md +43 -0
  115. package/docs/source-forms.ja.md +19 -0
  116. package/docs/stage-2-publication.ja.md +35 -0
  117. package/docs/template-fixture-separation.ja.md +43 -0
  118. package/package.json +18 -3
  119. package/webpack4.cjs +1 -1
  120. package/dist/webpack4/shared-CQB3NG2A.js +0 -1
  121. package/dist/webpack4/shared-WHOAW2VF.js +0 -957
  122. package/dist/webpack4/shared-WHOAW2VF.js.map +0 -7
  123. /package/dist/webpack4/{shared-CQB3NG2A.js.map → schema.js.map} +0 -0
@@ -0,0 +1,13 @@
1
+ import { Ajv } from 'ajv';
2
+ import { Ajv2020 } from 'ajv/dist/2020.js';
3
+ import formats from 'ajv-formats';
4
+ import { prepare } from './schema-engine.js';
5
+ import { snapshotJson } from './schema-json.js';
6
+ /** Resolve refs on a private view using the same URI/anchor registry as canonical validation. */
7
+ export function prepareSourceVariant(variant) {
8
+ const view = snapshotJson(variant);
9
+ const ajv = view.schemaBundle.dialect === 'draft-07' ? new Ajv({ strict: false }) : new Ajv2020({ strict: false });
10
+ formats.default(ajv);
11
+ prepare(view.schemaBundle, ajv);
12
+ return view;
13
+ }
@@ -0,0 +1,47 @@
1
+ import type { SchemaBundle, SchemaJsonObject, SchemaJsonValue, SchemaValidationContext } from './schema-types.js';
2
+ /** Source strings remain authoritative; decoded values are only an editing view. */
3
+ export interface SourceDocument {
4
+ readonly source: string;
5
+ readonly format: 'json' | 'yaml';
6
+ readonly variantId: string;
7
+ readonly data: SchemaJsonValue;
8
+ }
9
+ export interface SourceVariant {
10
+ readonly variantId: string;
11
+ readonly format: 'json' | 'yaml';
12
+ readonly schemaBundle: SchemaBundle;
13
+ readonly uiSchema?: SchemaJsonObject;
14
+ }
15
+ export interface SourceValidation {
16
+ readonly source: string;
17
+ readonly result: {
18
+ readonly kind: 'valid';
19
+ readonly diagnostics: readonly [];
20
+ } | {
21
+ readonly kind: 'invalid' | 'execution-error' | 'schema-error';
22
+ readonly diagnostics: readonly {
23
+ readonly code: string;
24
+ readonly dataPath?: string;
25
+ }[];
26
+ };
27
+ }
28
+ export interface SourceBinding<C = null> {
29
+ readonly argument: string;
30
+ readonly label?: string;
31
+ readonly variants: readonly SourceVariant[];
32
+ /** Explicit owner-defined source-only cases, for example a valid empty scene. */
33
+ readonly sourceOnlyVariants?: readonly {
34
+ readonly variantId: string;
35
+ readonly format: 'json' | 'yaml';
36
+ }[];
37
+ decode(source: string): SourceDocument;
38
+ validateSource(source: string, context: SchemaValidationContext<C>): SourceValidation;
39
+ }
40
+ export interface SourceEditorOptions<C = null> {
41
+ readonly apiVersion: 1;
42
+ readonly extensionId: string;
43
+ readonly editorId: string;
44
+ readonly opcodes: readonly string[];
45
+ readonly bindings: readonly SourceBinding<C>[];
46
+ readonly validationContext: () => SchemaValidationContext<C>;
47
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { CapturedBinding } from './source-editor.js';
2
+ import type { EditorViewContext } from './types.js';
3
+ export declare function mountSourceView<C>(container: HTMLElement, context: EditorViewContext, bindings: readonly CapturedBinding<C>[]): () => void;
@@ -0,0 +1,318 @@
1
+ import * as React from 'react';
2
+ import * as ReactDOM from 'react-dom';
3
+ import { createSourceCandidate, patchDecodedSource, readSourceDocument } from './source-editor.js';
4
+ import { cloneJson } from './data.js';
5
+ const record = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
6
+ const pointer = (path, key) => `${path}/${key.replace(/~/g, '~0').replace(/\//g, '~1')}`;
7
+ const typeOf = (v) => v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v;
8
+ const initial = (type) => {
9
+ switch (type) {
10
+ case 'object': return {};
11
+ case 'array': return [];
12
+ case 'string': return '';
13
+ case 'number': return 0;
14
+ case 'boolean': return false;
15
+ default: return null;
16
+ }
17
+ };
18
+ function mergeHints(left, right) {
19
+ const result = { ...left, ...right };
20
+ for (const field of ['properties', 'patternProperties']) {
21
+ if (!record(left[field]) && !record(right[field]))
22
+ continue;
23
+ const a = record(left[field]) ? left[field] : {};
24
+ const b = record(right[field]) ? right[field] : {};
25
+ const combined = { ...a };
26
+ for (const [key, child] of Object.entries(b))
27
+ Object.defineProperty(combined, key, {
28
+ value: Object.hasOwn(a, key) ? { allOf: [a[key], child] } : child, enumerable: true
29
+ });
30
+ result[field] = combined;
31
+ }
32
+ if (Array.isArray(left.enum) && Array.isArray(right.enum)) {
33
+ result.enum = left.enum.filter(value => right.enum.some(other => JSON.stringify(value) === JSON.stringify(other)));
34
+ }
35
+ return result;
36
+ }
37
+ function propertyHint(hints, key) {
38
+ const matches = [];
39
+ if (record(hints.properties) && Object.hasOwn(hints.properties, key))
40
+ matches.push(hints.properties[key]);
41
+ if (record(hints.patternProperties))
42
+ for (const [pattern, schema] of Object.entries(hints.patternProperties)) {
43
+ try {
44
+ if (new RegExp(pattern, 'u').test(key))
45
+ matches.push(schema);
46
+ }
47
+ catch ( /* The canonical compiler rejects invalid patterns. */_a) { /* The canonical compiler rejects invalid patterns. */ }
48
+ }
49
+ return matches.length > 1 ? { allOf: matches } : matches.length ? matches[0] : hints.additionalProperties;
50
+ }
51
+ /** Presentation hints never replace canonical validation or remove schema constraints. */
52
+ function resolveHints(variant, schema, data, seen = new Set()) {
53
+ var _a;
54
+ if (!record(schema) || seen.has(schema))
55
+ return {};
56
+ seen.add(schema);
57
+ let result = { ...schema };
58
+ if (typeof schema.$ref === 'string') {
59
+ const ref = schema.$ref;
60
+ const hash = ref.indexOf('#');
61
+ const uri = hash < 0 ? ref : ref.slice(0, hash);
62
+ const resources = [variant.schemaBundle.root, ...variant.schemaBundle.resources];
63
+ const resource = uri ? resources.find(r => r.retrievalUri === uri || (record(r.document) && r.document.$id === uri)) : variant.schemaBundle.root;
64
+ let target = resource === null || resource === void 0 ? void 0 : resource.document;
65
+ const fragment = hash < 0 ? '' : decodeURIComponent(ref.slice(hash + 1));
66
+ if (fragment.startsWith('/'))
67
+ for (const part of fragment.slice(1).split('/')) {
68
+ target = record(target) || Array.isArray(target) ? target[part.replace(/~1/g, '/').replace(/~0/g, '~')] : undefined;
69
+ }
70
+ if (!fragment || fragment.startsWith('/'))
71
+ result = mergeHints(resolveHints(variant, target, data, seen), result);
72
+ }
73
+ if (Array.isArray(schema.allOf))
74
+ for (const part of schema.allOf) {
75
+ const next = resolveHints(variant, part, data, seen);
76
+ result = mergeHints(result, next);
77
+ }
78
+ const alternatives = (_a = schema.oneOf) !== null && _a !== void 0 ? _a : schema.anyOf;
79
+ if (Array.isArray(alternatives)) {
80
+ const choices = alternatives.filter(part => {
81
+ if (!record(part))
82
+ return false;
83
+ if (typeof part.type === 'string' && part.type !== typeOf(data) && !(part.type === 'integer' && typeof data === 'number'))
84
+ return false;
85
+ if (record(part.properties) && record(data))
86
+ for (const [key, child] of Object.entries(part.properties)) {
87
+ if (record(child) && Object.hasOwn(child, 'const') && data[key] !== child.const)
88
+ return false;
89
+ }
90
+ return true;
91
+ });
92
+ if (choices.length === 1)
93
+ result = mergeHints(result, resolveHints(variant, choices[0], data, seen));
94
+ }
95
+ if (!Array.isArray(result.enum)) {
96
+ if (Object.hasOwn(result, 'const'))
97
+ result.enum = [result.const];
98
+ else if (Array.isArray(alternatives)) {
99
+ const enums = alternatives.map(part => resolveHints(variant, part, data, new Set(seen)));
100
+ if (enums.length && enums.every(part => Array.isArray(part.enum))) {
101
+ const values = enums.flatMap(part => part.enum);
102
+ result.enum = [...new Map(values.map(value => [JSON.stringify(value), value])).values()];
103
+ }
104
+ }
105
+ }
106
+ return result;
107
+ }
108
+ function Tree({ value, path, schema, ui, variant, invalidNumbers, change: onChange }) {
109
+ var _a;
110
+ const hints = resolveHints(variant, schema, value);
111
+ const uiHints = record(ui) ? ui : {};
112
+ const [newKey, setNewKey] = React.useState('');
113
+ const change = (next, edit = { path }) => onChange(next, edit);
114
+ const choices = Array.isArray(hints.enum) ? hints.enum : Array.isArray(uiHints['ui:enum']) ? uiHints['ui:enum'] : undefined;
115
+ const label = typeof hints.title === 'string' ? hints.title : path || '/';
116
+ const props = { 'aria-label': label };
117
+ const type = typeOf(value);
118
+ let field;
119
+ if (choices) {
120
+ const serialized = JSON.stringify(value);
121
+ const options = choices.map(item => JSON.stringify(item));
122
+ if (!options.includes(serialized))
123
+ options.unshift(serialized);
124
+ field = React.createElement("select", { ...props, value: serialized, onChange: e => change(JSON.parse(e.target.value)) }, options.map(item => React.createElement("option", { key: item, value: item }, item)));
125
+ }
126
+ else if (typeof value === 'string') {
127
+ field = React.createElement("textarea", { ...props, value: value, onChange: e => change(e.target.value) });
128
+ }
129
+ else if (typeof value === 'number') {
130
+ field = React.createElement("input", { ...props, type: "text", inputMode: "decimal", value: (_a = invalidNumbers[path]) !== null && _a !== void 0 ? _a : String(value), onChange: e => {
131
+ const text = e.target.value;
132
+ const number = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/.test(text) ? Number(text) : NaN;
133
+ change(Number.isFinite(number) ? number : value, { path, ...(Number.isFinite(number) ? {} : { invalidText: text }) });
134
+ } });
135
+ }
136
+ else if (typeof value === 'boolean') {
137
+ field = React.createElement("input", { ...props, type: "checkbox", checked: value, onChange: e => change(e.target.checked) });
138
+ }
139
+ else if (Array.isArray(value)) {
140
+ field = React.createElement("div", null,
141
+ value.map((child, index) => {
142
+ var _a;
143
+ return React.createElement("div", { key: index },
144
+ React.createElement(Tree, { invalidNumbers: invalidNumbers, value: child, path: pointer(path, String(index)), variant: variant, schema: Array.isArray(hints.prefixItems) ? (_a = hints.prefixItems[index]) !== null && _a !== void 0 ? _a : hints.items : Array.isArray(hints.items) ? hints.items[index] : hints.items, ui: uiHints.items, change: (next, edit) => { const copy = [...value]; copy[index] = next; change(copy, edit); } }),
145
+ React.createElement("button", { type: "button", "aria-label": `Remove ${pointer(path, String(index))}`, onClick: () => change(value.filter((_, i) => i !== index), { path, removeIndex: index }) }, "Remove"),
146
+ React.createElement("button", { type: "button", disabled: index === 0, "aria-label": `Move up ${pointer(path, String(index))}`, onClick: () => {
147
+ const copy = [...value];
148
+ [copy[index - 1], copy[index]] = [copy[index], copy[index - 1]];
149
+ change(copy, { path, swap: [index - 1, index] });
150
+ } }, "\u2191"));
151
+ }),
152
+ React.createElement("button", { type: "button", "aria-label": `Add item ${path || '/'}`, onClick: () => change([...value, null], { path: pointer(path, String(value.length)) }) }, "Add item"));
153
+ }
154
+ else if (value !== null && typeof value === 'object') {
155
+ const properties = record(hints.properties) ? hints.properties : {};
156
+ field = React.createElement("div", null,
157
+ Object.entries(value).map(([key, child]) => React.createElement("div", { key: key },
158
+ React.createElement(Tree, { invalidNumbers: invalidNumbers, value: child, path: pointer(path, key), variant: variant, schema: propertyHint(hints, key), ui: uiHints[key], change: (next, edit) => {
159
+ const copy = cloneJson(value);
160
+ Object.defineProperty(copy, key, { value: next, enumerable: true, writable: true, configurable: true });
161
+ change(copy, edit);
162
+ } }),
163
+ React.createElement("button", { type: "button", "aria-label": `Remove ${pointer(path, key)}`, onClick: () => {
164
+ const copy = cloneJson(value);
165
+ delete copy[key];
166
+ change(copy, { path: pointer(path, key) });
167
+ } }, "Remove"))),
168
+ React.createElement("label", null,
169
+ "Property ",
170
+ React.createElement("input", { "aria-label": `New property ${path || '/'}`, value: newKey, onChange: e => setNewKey(e.target.value) })),
171
+ React.createElement("select", { "aria-label": `Known property ${path || '/'}`, value: "", onChange: e => setNewKey(e.target.value) },
172
+ React.createElement("option", { value: "" }, "Choose property"),
173
+ Object.keys(properties).filter(key => !Object.hasOwn(value, key)).map(key => React.createElement("option", { key: key }, key))),
174
+ React.createElement("button", { type: "button", disabled: !newKey || Object.hasOwn(value, newKey), onClick: () => {
175
+ const copy = cloneJson(value);
176
+ Object.defineProperty(copy, newKey, { value: null, enumerable: true, writable: true, configurable: true });
177
+ change(copy, { path: pointer(path, newKey) });
178
+ setNewKey('');
179
+ } }, "Add property"));
180
+ }
181
+ else
182
+ field = React.createElement("span", null, "null");
183
+ return React.createElement("fieldset", { "data-source-path": path },
184
+ React.createElement("legend", null, label),
185
+ React.createElement("select", { "aria-label": `Type ${path || '/'}`, value: type, onChange: e => change(initial(e.target.value)) }, ['object', 'array', 'string', 'number', 'boolean', 'null'].map(t => React.createElement("option", { key: t }, t))),
186
+ field);
187
+ }
188
+ function liveIssues(draft, bindings) {
189
+ const issues = [];
190
+ const sources = draft.sources;
191
+ for (const binding of bindings) {
192
+ const entry = sources[binding.argument];
193
+ try {
194
+ if (entry.invalidForm === true)
195
+ throw new TypeError();
196
+ const doc = record(entry.creating)
197
+ ? { ...entry.creating, data: entry.data }
198
+ : readSourceDocument(binding, entry.source);
199
+ const validator = binding.validators.get(JSON.stringify([doc.format, doc.variantId]));
200
+ if (validator && validator.validate((entry.mode === 'form' ? entry.data : doc.data)).kind !== 'valid') {
201
+ issues.push({ message: 'Value does not satisfy the schema.', path: `/${binding.argument}` });
202
+ }
203
+ }
204
+ catch (_a) {
205
+ issues.push({ message: 'Repair the invalid source or form value.', path: `/${binding.argument}` });
206
+ }
207
+ }
208
+ return issues;
209
+ }
210
+ function SourceView({ context, bindings }) {
211
+ const [, redraw] = React.useState(0);
212
+ const [creating, selectCreateVariant] = React.useState({});
213
+ React.useEffect(() => {
214
+ const stop = context.subscribe(() => redraw(n => n + 1));
215
+ context.setValidationIssues(liveIssues(context.getDraft(), bindings));
216
+ return stop;
217
+ }, [context, bindings]);
218
+ const state = context.getState();
219
+ if (state.status !== 'editing' && state.status !== 'saving')
220
+ return React.createElement(React.Fragment, null);
221
+ const draft = context.getDraft();
222
+ const sources = draft.sources;
223
+ const disabled = context.getState().status !== 'editing';
224
+ const update = (argument, entry) => {
225
+ const next = context.getDraft();
226
+ const nextSources = next.sources;
227
+ Object.defineProperty(nextSources, argument, { value: entry, enumerable: true, configurable: true, writable: true });
228
+ context.setDraft(next);
229
+ context.setValidationIssues(liveIssues(next, bindings));
230
+ };
231
+ return React.createElement("div", { className: "block-editor-source-forms" }, bindings.map(binding => {
232
+ var _a, _b;
233
+ const entry = sources[binding.argument];
234
+ const selectedCreation = Object.hasOwn(creating, binding.argument) ? creating[binding.argument] : '';
235
+ let document;
236
+ let variant;
237
+ try {
238
+ document = readSourceDocument(binding, entry.source);
239
+ variant = binding.presentationVariants.find(v => v.format === document.format && v.variantId === document.variantId);
240
+ }
241
+ catch ( /* The Source textarea remains available for repair. */_c) { /* The Source textarea remains available for repair. */ }
242
+ if (record(entry.creating)) {
243
+ const creation = entry.creating;
244
+ variant = binding.presentationVariants.find(v => v.format === creation.format && v.variantId === creation.variantId);
245
+ }
246
+ return React.createElement("fieldset", { key: binding.argument, disabled: disabled },
247
+ React.createElement("legend", null, (_a = binding.label) !== null && _a !== void 0 ? _a : binding.argument),
248
+ React.createElement("button", { type: "button", "aria-pressed": entry.mode === 'source', onClick: () => {
249
+ try {
250
+ if (entry.invalidForm === true)
251
+ throw new TypeError();
252
+ const source = record(entry.creating)
253
+ ? createSourceCandidate(entry.creating.format, entry.data)
254
+ : entry.mode === 'form' && document && Object.hasOwn(entry, 'data')
255
+ ? patchDecodedSource(document, entry.data, source => readSourceDocument(binding, source)) : entry.source;
256
+ const next = { ...entry, mode: 'source', source, invalidForm: false, invalidNumbers: {} };
257
+ delete next.creating;
258
+ update(binding.argument, next);
259
+ }
260
+ catch (_a) {
261
+ context.setValidationIssues([{ message: 'This edit cannot preserve the source. Repair the Source explicitly.' }]);
262
+ }
263
+ } }, "Source"),
264
+ React.createElement("select", { "aria-label": `${binding.argument} New format and variant`, value: selectedCreation, onChange: e => selectCreateVariant({ ...creating, [binding.argument]: e.target.value }) },
265
+ React.createElement("option", { value: "" }, "Choose new format and variant"),
266
+ binding.presentationVariants.map(v => React.createElement("option", { key: JSON.stringify([v.format, v.variantId]), value: JSON.stringify([v.format, v.variantId]) },
267
+ v.format,
268
+ " / ",
269
+ v.variantId))),
270
+ React.createElement("button", { type: "button", disabled: !selectedCreation, onClick: () => {
271
+ const [format, variantId] = JSON.parse(selectedCreation);
272
+ const chosen = binding.presentationVariants.find(v => v.format === format && v.variantId === variantId);
273
+ const root = chosen.schemaBundle.root.document;
274
+ const type = record(root) && typeof root.type === 'string' ? root.type : 'null';
275
+ update(binding.argument, { mode: 'form', source: entry.source, sourceEdited: true, creating: { format, variantId }, data: initial(type) });
276
+ } }, "Create document (replace source)"),
277
+ React.createElement("button", { type: "button", disabled: !variant, "aria-pressed": entry.mode === 'form', onClick: () => {
278
+ if (document || entry.creating)
279
+ update(binding.argument, { ...entry, mode: 'form', data: Object.hasOwn(entry, 'data') ? entry.data : document.data });
280
+ } }, "Form"),
281
+ entry.mode === 'form' && React.createElement("button", { type: "button", onClick: () => { var _a; return update(binding.argument, { mode: 'source', source: entry.source, sourceEdited: (_a = entry.sourceEdited) !== null && _a !== void 0 ? _a : false }); } }, "Discard form edits"),
282
+ entry.mode === 'form' && variant && Object.hasOwn(entry, 'data')
283
+ ? React.createElement(Tree, { invalidNumbers: ((_b = entry.invalidNumbers) !== null && _b !== void 0 ? _b : {}), value: entry.data, path: "", schema: variant.schemaBundle.root.document, ui: variant.uiSchema, variant: variant, change: (data, edit = { path: '' }) => {
284
+ var _a;
285
+ const pending = ((_a = entry.invalidNumbers) !== null && _a !== void 0 ? _a : {});
286
+ const invalidNumbers = {};
287
+ const prefix = `${edit.path}/`;
288
+ for (const [path, text] of Object.entries(pending)) {
289
+ let target = path;
290
+ if (edit.removeIndex !== undefined || edit.swap) {
291
+ if (path.startsWith(prefix)) {
292
+ const [head, ...tail] = path.slice(prefix.length).split('/');
293
+ let index = Number(head);
294
+ if (index === edit.removeIndex)
295
+ continue;
296
+ if (edit.removeIndex !== undefined && index > edit.removeIndex)
297
+ index--;
298
+ if (edit.swap)
299
+ index = index === edit.swap[0] ? edit.swap[1] : index === edit.swap[1] ? edit.swap[0] : index;
300
+ target = `${prefix}${index}${tail.length ? '/' + tail.join('/') : ''}`;
301
+ }
302
+ }
303
+ else if (path === edit.path || path.startsWith(prefix))
304
+ continue;
305
+ Object.defineProperty(invalidNumbers, target, { value: text, enumerable: true });
306
+ }
307
+ if (edit.invalidText !== undefined)
308
+ Object.defineProperty(invalidNumbers, edit.path, { value: edit.invalidText, enumerable: true });
309
+ update(binding.argument, { ...entry, data, invalidNumbers, invalidForm: Object.keys(invalidNumbers).length > 0 });
310
+ } })
311
+ : React.createElement("textarea", { "aria-label": `${binding.argument} Source`, value: entry.source, onChange: e => update(binding.argument, { mode: 'source', source: e.target.value, sourceEdited: true }) }),
312
+ entry.invalidForm === true && React.createElement("p", { role: "alert" }, "Enter a valid JSON number."));
313
+ }));
314
+ }
315
+ export function mountSourceView(container, context, bindings) {
316
+ ReactDOM.render(React.createElement(SourceView, { context: context, bindings: bindings }), container);
317
+ return () => { ReactDOM.unmountComponentAtNode(container); };
318
+ }
@@ -1,5 +1,5 @@
1
1
  export default createBlockEditorBridge;
2
- declare function createBlockEditorBridge({ ScratchBlocks, workspace, vm, service, label, canOpen, onOpen, onError, descendants }: {
2
+ declare function createBlockEditorBridge({ ScratchBlocks, workspace, vm, service, label, canOpen, onOpen, onError, descendants, fixedInputs }: {
3
3
  ScratchBlocks: any;
4
4
  workspace: any;
5
5
  vm: any;
@@ -9,6 +9,7 @@ declare function createBlockEditorBridge({ ScratchBlocks, workspace, vm, service
9
9
  onOpen: any;
10
10
  onError: any;
11
11
  descendants?: boolean | undefined;
12
+ fixedInputs?: boolean | undefined;
12
13
  }): {
13
14
  refresh: () => void;
14
15
  dispose: () => void;
@@ -1,22 +1,32 @@
1
1
  import createBlockEditorHost from './host.js';
2
2
  import bindBlockEditorIcon from './icon.js';
3
3
  import readBlockEditorAppearance from './appearance.js';
4
- import { supportsDescendantEditing } from '../index.js';
4
+ import { supportsDescendantEditing, supportsFixedInputEditing } from '../index.js';
5
5
  import createBlockEditorErrorSession from './error-session.js';
6
- const openBlockEditorSession = ({ registry, reference, host, descendants }) => (descendants && supportsDescendantEditing(host) ?
7
- registry.openDescendants(reference, host) : registry.open(reference, host));
8
- const createBlockEditorBridge = ({ ScratchBlocks, workspace, vm, service, label, canOpen, onOpen, onError, descendants = false }) => {
6
+ const fixedDeclaration = (registry, snapshot) => snapshot && typeof registry.resolveFixed === 'function' &&
7
+ registry.resolveFixed(snapshot.opcode);
8
+ const openBlockEditorSession = ({ registry, reference, host, descendants }) => {
9
+ const snapshot = host.readBlock ? host.readBlock(reference) : null;
10
+ // A fixed registration owns only this block; legacy aggregate traversal stays fail-closed.
11
+ if (fixedDeclaration(registry, snapshot))
12
+ return supportsFixedInputEditing(host) ? registry.open(reference, host) : undefined;
13
+ return descendants && supportsDescendantEditing(host) ?
14
+ registry.openDescendants(reference, host) : registry.open(reference, host);
15
+ };
16
+ const createBlockEditorBridge = ({ ScratchBlocks, workspace, vm, service, label, canOpen, onOpen, onError, descendants = false, fixedInputs = false }) => {
9
17
  const host = createBlockEditorHost({ ScratchBlocks,
10
18
  workspace,
11
19
  vm,
12
20
  registry: service.registry,
13
21
  enableDescendants: descendants,
22
+ enableFixedInputs: fixedInputs,
14
23
  onError });
15
24
  const decorations = new Map();
16
25
  let disposed = false;
17
26
  let timer = null;
18
27
  let activeSession = null;
19
28
  let openingSnapshot = null;
29
+ let openingFixedDeclaration = null;
20
30
  let stopWaiting = null;
21
31
  const endSession = () => {
22
32
  const session = activeSession;
@@ -41,6 +51,7 @@ const createBlockEditorBridge = ({ ScratchBlocks, workspace, vm, service, label,
41
51
  }
42
52
  };
43
53
  const checkSession = () => {
54
+ var _a;
44
55
  // The host owns rollback. Its synchronous transaction must retain the draft on failure.
45
56
  if (host.isCommitting())
46
57
  return;
@@ -49,6 +60,14 @@ const createBlockEditorBridge = ({ ScratchBlocks, workspace, vm, service, label,
49
60
  return;
50
61
  const current = typeof activeSession.getNodes === 'function' ?
51
62
  host.descendants.readSubtree(activeSession.reference) : host.readBlock(activeSession.reference);
63
+ if (openingFixedDeclaration) {
64
+ // Same-target external edits conflict at save while the session retains its draft.
65
+ // Project/target/registration replacement ends the session and drops that draft.
66
+ if (!current || ((_a = current.fixedContext) === null || _a === void 0 ? void 0 : _a.generation) !== openingSnapshot.fixedContext.generation ||
67
+ fixedDeclaration(service.registry, current) !== openingFixedDeclaration)
68
+ endSession();
69
+ return;
70
+ }
52
71
  if (!current || JSON.stringify(current) !== JSON.stringify(openingSnapshot))
53
72
  endSession();
54
73
  };
@@ -56,6 +75,8 @@ const createBlockEditorBridge = ({ ScratchBlocks, workspace, vm, service, label,
56
75
  if (disposed || !vm.editingTarget || !canOpen() || !service.registry.resolve(block.type))
57
76
  return false;
58
77
  const snapshot = host.readBlock({ targetId: vm.editingTarget.id, blockId: block.id });
78
+ if (fixedDeclaration(service.registry, snapshot))
79
+ return supportsFixedInputEditing(host);
59
80
  return snapshot && Object.values(snapshot.arguments).every(slot => slot.kind === 'literal');
60
81
  };
61
82
  const open = block => {
@@ -71,6 +92,7 @@ const createBlockEditorBridge = ({ ScratchBlocks, workspace, vm, service, label,
71
92
  activeSession = session;
72
93
  openingSnapshot = typeof session.getNodes === 'function' ?
73
94
  host.descendants.readSubtree(session.reference) : host.readBlock(session.reference);
95
+ openingFixedDeclaration = fixedDeclaration(service.registry, openingSnapshot) || null;
74
96
  const labels = {};
75
97
  const appearances = Object.create(null);
76
98
  if (typeof session.getNodes === 'function') {
@@ -88,6 +110,7 @@ const createBlockEditorBridge = ({ ScratchBlocks, workspace, vm, service, label,
88
110
  onError(error);
89
111
  activeSession = createBlockEditorErrorSession(reference, error);
90
112
  openingSnapshot = null;
113
+ openingFixedDeclaration = null;
91
114
  onOpen(activeSession);
92
115
  }
93
116
  };
@@ -137,7 +160,13 @@ const createBlockEditorBridge = ({ ScratchBlocks, workspace, vm, service, label,
137
160
  };
138
161
  const unregister = service.subscribe(() => {
139
162
  // Aggregate sessions observe relevant registry changes themselves.
140
- if (!activeSession || typeof activeSession.getNodes !== 'function') {
163
+ if (openingFixedDeclaration && activeSession) {
164
+ if (service.registry.resolveFixed(openingSnapshot.opcode) !== openingFixedDeclaration) {
165
+ host.invalidate();
166
+ endSession();
167
+ }
168
+ }
169
+ else if (!activeSession || typeof activeSession.getNodes !== 'function') {
141
170
  host.invalidate();
142
171
  endSession();
143
172
  }
@@ -0,0 +1,82 @@
1
+ /** Freeze native method identities once when the host attaches, before opening editors. */
2
+ export function initializeFixedFields(ScratchBlocks: any): void;
3
+ /** Native objects stay local; only primitive metadata is returned in snapshots. */
4
+ export function inspectFixedArgument({ ScratchBlocks, block, vmBlock, target, argument, role, identify, extensionId }: {
5
+ ScratchBlocks: any;
6
+ block: any;
7
+ vmBlock: any;
8
+ target: any;
9
+ argument: any;
10
+ role: any;
11
+ identify: any;
12
+ extensionId: any;
13
+ }): {
14
+ slot: {
15
+ kind: string;
16
+ value: string | number | boolean;
17
+ } | {
18
+ kind: string;
19
+ value?: never;
20
+ };
21
+ metadata: {
22
+ role: any;
23
+ source: any;
24
+ identity: string;
25
+ reason: any;
26
+ };
27
+ signatureParts: any[];
28
+ } | {
29
+ slot: {
30
+ kind: string;
31
+ value?: never;
32
+ };
33
+ metadata: {
34
+ role: any;
35
+ source: string;
36
+ identity: string;
37
+ field?: never;
38
+ };
39
+ signatureParts: any[];
40
+ } | {
41
+ signatureParts: any[];
42
+ binding?: {
43
+ restoreState?: {
44
+ colors: any[];
45
+ hsv: any[][];
46
+ blockProperties: any;
47
+ };
48
+ block: any;
49
+ field: any;
50
+ fieldName: any;
51
+ value: string;
52
+ vmBlock: any;
53
+ kind: string;
54
+ Class: any;
55
+ prototype: any;
56
+ allowedValues: readonly string[] | null | undefined;
57
+ restoreValue: any;
58
+ rawState: any;
59
+ restoreHandles: any;
60
+ restoreDom: any;
61
+ };
62
+ slot: {
63
+ kind: string;
64
+ value: string;
65
+ };
66
+ metadata: {
67
+ role: any;
68
+ source: string;
69
+ identity: string;
70
+ field: {
71
+ blockId: any;
72
+ name: any;
73
+ type: string | undefined;
74
+ };
75
+ };
76
+ } | null;
77
+ /** Validate the exact saved primitive; native setters must not silently normalize it. */
78
+ export function prepareFixedChange(binding: any, value: any): any;
79
+ /** Capture a later external value without invoking replacement field methods. */
80
+ export function captureFixedFieldState(binding: any): any;
81
+ /** Restore raw text and the standard color field's additional block/UI state. */
82
+ export function restoreFixedField(binding: any): void;