@jxsuite/studio 0.37.1 → 1.0.0

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 (38) hide show
  1. package/dist/studio.js +77654 -76269
  2. package/dist/studio.js.map +128 -119
  3. package/package.json +44 -44
  4. package/src/account-status.ts +39 -0
  5. package/src/browse/browse.ts +10 -14
  6. package/src/canvas/iframe-host.ts +1 -1
  7. package/src/editor/context-menu.ts +1 -1
  8. package/src/editor/repeater-scope.ts +8 -13
  9. package/src/files/files.ts +3 -0
  10. package/src/format/format-host.ts +63 -1
  11. package/src/new-project/add-repo-modal.ts +183 -0
  12. package/src/new-project/new-project-modal.ts +22 -3
  13. package/src/page-params.ts +34 -8
  14. package/src/panels/ai-chat/chat-markdown.ts +2 -2
  15. package/src/panels/ai-panel.ts +61 -4
  16. package/src/panels/data-grid.ts +619 -0
  17. package/src/panels/signals-panel.ts +102 -437
  18. package/src/panels/statusbar.ts +1 -1
  19. package/src/panels/welcome-screen.ts +50 -0
  20. package/src/platform-errors.ts +30 -0
  21. package/src/platforms/cloud.ts +172 -89
  22. package/src/platforms/devserver.ts +172 -0
  23. package/src/services/context-resolver.ts +73 -0
  24. package/src/services/data-service.ts +155 -0
  25. package/src/services/monaco-setup.ts +75 -26
  26. package/src/settings/contributed-section.ts +406 -0
  27. package/src/settings/extension-sections.ts +145 -0
  28. package/src/settings/schema-field-ui.ts +4 -2
  29. package/src/settings/settings-modal.ts +101 -42
  30. package/src/site-context.ts +10 -1
  31. package/src/studio.ts +24 -0
  32. package/src/tabs/transact.ts +1 -1
  33. package/src/types.ts +120 -1
  34. package/src/ui/form-controls.ts +322 -0
  35. package/src/ui/progress-modal.ts +2 -2
  36. package/src/ui/schema-form.ts +524 -0
  37. package/src/utils/studio-utils.ts +4 -3
  38. package/src/settings/content-types-editor.ts +0 -599
@@ -0,0 +1,406 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Contributed settings section — the generic renderer behind `$studio.settings`
4
+ * (specs/extensions.md §9.1). An extension's `project` class declares a settings section and this
5
+ * module renders it over `projectConfig[key]`: layout "form" is one schema form over the whole
6
+ * section value; layout "map" is master-detail (key list left, entry form right) for `type: object`
7
+ * + `additionalProperties` sections. Persistence mirrors the content-types editor: mutate
8
+ * projectState.projectConfig and rewrite project.json through the platform.
9
+ */
10
+
11
+ import { html, nothing, render as litRender } from "lit-html";
12
+ import { getPlatform } from "../platform";
13
+ import { projectState } from "../store";
14
+ import { renderForm } from "../ui/schema-form";
15
+ import { resolveContextPointer } from "../services/context-resolver";
16
+ import { deriveSecretEnvName } from "../services/data-service";
17
+
18
+ import type { TemplateResult } from "lit-html";
19
+ import type { JsonSchema, SchemaFormContext } from "../ui/schema-form";
20
+ import type { ProjectConfig } from "@jxsuite/schema/types";
21
+
22
+ // ─── Contribution shape ───────────────────────────────────────────────────────
23
+
24
+ /** A `$studio.settings` contribution paired with the entry schema from the project fragment. */
25
+ export interface SettingsContribution {
26
+ /** The project.json top-level property the section owns. */
27
+ key: string;
28
+ /** Section heading; falls back to the key. */
29
+ title?: string | undefined;
30
+ /** The `$studio.settings` block from the class descriptor. */
31
+ settings: {
32
+ layout?: "map" | "form" | undefined;
33
+ entry?:
34
+ | {
35
+ ui?: Record<string, { control?: string; enum?: unknown }> | undefined;
36
+ newEntry?: Record<string, unknown> | undefined;
37
+ }
38
+ | undefined;
39
+ };
40
+ /** JSON Schema for one entry (map layout) or the whole section value (form layout). */
41
+ entrySchema: JsonSchema;
42
+ }
43
+
44
+ /** Context handed to a host-provided section actions renderer. */
45
+ export interface SectionActionsContext {
46
+ sectionKey: string;
47
+ /** The selected entry key (map layout), or null. */
48
+ selected: string | null;
49
+ rerender: () => void;
50
+ }
51
+
52
+ /** Host options threaded to the schema-form context. */
53
+ export interface ContributedSectionOptions {
54
+ /** Registered formats backing the `$formats` virtual root. */
55
+ formats?: { name: string }[] | undefined;
56
+ /**
57
+ * Optional actions row rendered under the section title — the hook domain modules use to surface
58
+ * section-scoped operations (e.g. the data surface's Test/Push actions) without the generic
59
+ * renderer knowing any extension.
60
+ */
61
+ actions?: ((ctx: SectionActionsContext) => TemplateResult) | undefined;
62
+ }
63
+
64
+ // ─── Module state ─────────────────────────────────────────────────────────────
65
+
66
+ /** Selected entry key per section (map layout). */
67
+ const selectedEntries = new Map<string, string | null>();
68
+ /** Sections whose new-entry form is open (map layout). */
69
+ const newEntryOpen = new Set<string>();
70
+ /** Pending new-entry names per section (map layout). */
71
+ const newEntryNames = new Map<string, string>();
72
+
73
+ /** Reset contributed-section ephemeral UI state (test hook). */
74
+ export function resetContributedSectionState(): void {
75
+ selectedEntries.clear();
76
+ newEntryOpen.clear();
77
+ newEntryNames.clear();
78
+ }
79
+
80
+ // ─── Persistence ──────────────────────────────────────────────────────────────
81
+
82
+ async function saveProjectConfig() {
83
+ const platform = getPlatform();
84
+ const config = (projectState as { projectConfig: ProjectConfig }).projectConfig;
85
+ await platform.writeFile("project.json", JSON.stringify(config, null, "\t"));
86
+ }
87
+
88
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
89
+
90
+ /** Apply a schema-form patch onto a record in place; `undefined` values unset the key. */
91
+ function applyPatch(target: Record<string, unknown>, patch: Record<string, unknown>) {
92
+ for (const [key, value] of Object.entries(patch)) {
93
+ if (value === undefined) {
94
+ delete target[key];
95
+ } else {
96
+ target[key] = value;
97
+ }
98
+ }
99
+ }
100
+
101
+ /** Slugify a user-entered entry name — same rules as new content-type names. */
102
+ function slugify(name: string): string {
103
+ return name
104
+ .toLowerCase()
105
+ .replaceAll(/\s+/g, "-")
106
+ .replaceAll(/[^a-z0-9-]/g, "");
107
+ }
108
+
109
+ /** Instantiate a newEntry template, substituting `${key}` in every string value. */
110
+ function instantiateNewEntry(
111
+ template: Record<string, unknown> | undefined,
112
+ key: string,
113
+ ): Record<string, unknown> {
114
+ if (!template) {
115
+ return {};
116
+ }
117
+ const substitute = (value: unknown): unknown => {
118
+ if (typeof value === "string") {
119
+ return value.replaceAll("${key}", key);
120
+ }
121
+ if (Array.isArray(value)) {
122
+ return value.map((item) => substitute(item));
123
+ }
124
+ if (value && typeof value === "object") {
125
+ const out: Record<string, unknown> = {};
126
+ for (const [k, v] of Object.entries(value)) {
127
+ out[k] = substitute(v);
128
+ }
129
+ return out;
130
+ }
131
+ return value;
132
+ };
133
+ return substitute(template) as Record<string, unknown>;
134
+ }
135
+
136
+ /**
137
+ * Schema-form context resolving `#/$context/…` pointers over the live project config. When the
138
+ * platform has a secrets surface, `commitSecret` backs the "secret" control: the VALUE goes to
139
+ * platform.setSecrets under a derived env name; the returned NAME is what lands in project.json.
140
+ */
141
+ function buildContext(
142
+ sectionKey: string,
143
+ opts: ContributedSectionOptions,
144
+ entryKey: string | null = null,
145
+ ): SchemaFormContext {
146
+ const platform = getPlatform();
147
+ return {
148
+ fieldKeyPrefix: `$settings.${sectionKey}`,
149
+ resolvePointer: (pointer, scope) =>
150
+ resolveContextPointer(pointer, {
151
+ projectConfig: (projectState?.projectConfig ?? {}) as Record<string, unknown>,
152
+ ...(scope !== undefined && { scope }),
153
+ ...(opts.formats !== undefined && { formats: opts.formats }),
154
+ }),
155
+ ...(typeof platform.setSecrets === "function"
156
+ ? {
157
+ commitSecret: async (key: string, value: string) => {
158
+ const envName = deriveSecretEnvName(sectionKey, entryKey, key);
159
+ await platform.setSecrets!({ set: { [envName]: value } });
160
+ return envName;
161
+ },
162
+ }
163
+ : {}),
164
+ };
165
+ }
166
+
167
+ /** The section's value object in the project config, created on demand. */
168
+ function sectionValue(key: string): Record<string, unknown> | null {
169
+ const config = projectState?.projectConfig as Record<string, unknown> | null | undefined;
170
+ if (!config) {
171
+ return null;
172
+ }
173
+ const existing = config[key];
174
+ if (existing && typeof existing === "object" && !Array.isArray(existing)) {
175
+ return existing as Record<string, unknown>;
176
+ }
177
+ const fresh: Record<string, unknown> = {};
178
+ config[key] = fresh;
179
+ return fresh;
180
+ }
181
+
182
+ // ─── Render ───────────────────────────────────────────────────────────────────
183
+
184
+ /**
185
+ * Render a contributed settings section into a settings-modal content container.
186
+ *
187
+ * @param {HTMLElement} container
188
+ * @param {SettingsContribution} contribution
189
+ * @param {ContributedSectionOptions} [opts]
190
+ */
191
+ export function renderContributedSection(
192
+ container: HTMLElement,
193
+ contribution: SettingsContribution,
194
+ opts: ContributedSectionOptions = {},
195
+ ) {
196
+ const rerender = () => renderContributedSection(container, contribution, opts);
197
+ const title = contribution.title ?? contribution.key;
198
+ const layout = contribution.settings.layout ?? "form";
199
+
200
+ const body =
201
+ layout === "map"
202
+ ? renderMapLayout(contribution, opts, rerender)
203
+ : renderFormLayout(contribution, opts, rerender);
204
+
205
+ const selected = layout === "map" ? (selectedEntries.get(contribution.key) ?? null) : null;
206
+ const actions = opts.actions
207
+ ? opts.actions({ rerender, sectionKey: contribution.key, selected })
208
+ : nothing;
209
+
210
+ const tpl = html`
211
+ <div class="settings-section contributed-section">
212
+ <h3 class="settings-section-title">${title}</h3>
213
+ ${actions}${body}
214
+ </div>
215
+ `;
216
+
217
+ litRender(tpl, container);
218
+ }
219
+
220
+ /** Layout "form" — one schema form over the whole section value. */
221
+ function renderFormLayout(
222
+ contribution: SettingsContribution,
223
+ opts: ContributedSectionOptions,
224
+ rerender: () => void,
225
+ ): TemplateResult {
226
+ const value = sectionValue(contribution.key) ?? {};
227
+ const ui = contribution.settings.entry?.ui;
228
+
229
+ return html`
230
+ <div class="settings-form-panel">
231
+ ${renderForm(contribution.entrySchema, value, {
232
+ context: buildContext(contribution.key, opts, null),
233
+ onChange: (patch) => {
234
+ const target = sectionValue(contribution.key);
235
+ if (!target) {
236
+ return;
237
+ }
238
+ applyPatch(target, patch);
239
+ rerender();
240
+ void saveProjectConfig();
241
+ },
242
+ rerender,
243
+ ...(ui !== undefined && { ui }),
244
+ })}
245
+ </div>
246
+ `;
247
+ }
248
+
249
+ /** Layout "map" — master-detail: entry key list left, entry form right. */
250
+ function renderMapLayout(
251
+ contribution: SettingsContribution,
252
+ opts: ContributedSectionOptions,
253
+ rerender: () => void,
254
+ ): TemplateResult {
255
+ const { key: sectionKey } = contribution;
256
+ const entries = sectionValue(sectionKey) ?? {};
257
+ const entryKeys = Object.keys(entries);
258
+ const selected = selectedEntries.get(sectionKey) ?? null;
259
+ const ui = contribution.settings.entry?.ui;
260
+
261
+ const handleCreate = () => {
262
+ const slug = slugify(newEntryNames.get(sectionKey) ?? "");
263
+ const target = sectionValue(sectionKey);
264
+ if (!slug || !target || target[slug]) {
265
+ return;
266
+ }
267
+ target[slug] = instantiateNewEntry(contribution.settings.entry?.newEntry, slug);
268
+ selectedEntries.set(sectionKey, slug);
269
+ newEntryOpen.delete(sectionKey);
270
+ newEntryNames.delete(sectionKey);
271
+ rerender();
272
+ void saveProjectConfig();
273
+ };
274
+
275
+ const handleDelete = () => {
276
+ const target = sectionValue(sectionKey);
277
+ if (!selected || !target?.[selected]) {
278
+ return;
279
+ }
280
+ delete target[selected];
281
+ selectedEntries.set(sectionKey, null);
282
+ rerender();
283
+ void saveProjectConfig();
284
+ };
285
+
286
+ const handleRename = (newName: string) => {
287
+ const target = sectionValue(sectionKey);
288
+ const slug = slugify(newName);
289
+ if (!selected || !target || !slug || slug === selected || target[slug]) {
290
+ return;
291
+ }
292
+ // Rebuild the map to preserve entry order under the new key
293
+ const next: Record<string, unknown> = {};
294
+ for (const [k, v] of Object.entries(target)) {
295
+ next[k === selected ? slug : k] = v;
296
+ delete target[k];
297
+ }
298
+ Object.assign(target, next);
299
+ selectedEntries.set(sectionKey, slug);
300
+ rerender();
301
+ void saveProjectConfig();
302
+ };
303
+
304
+ // Left column — entry key list
305
+ const listTpl = html`
306
+ <div class="settings-list-panel">
307
+ ${entryKeys.map(
308
+ (name) => html`
309
+ <sp-action-button
310
+ size="s"
311
+ ?selected=${selected === name}
312
+ @click=${() => {
313
+ selectedEntries.set(sectionKey, name);
314
+ rerender();
315
+ }}
316
+ >
317
+ ${name}
318
+ </sp-action-button>
319
+ `,
320
+ )}
321
+ ${newEntryOpen.has(sectionKey)
322
+ ? html`
323
+ <div class="settings-inline-form">
324
+ <sp-textfield
325
+ size="s"
326
+ placeholder="entry-name"
327
+ .value=${newEntryNames.get(sectionKey) ?? ""}
328
+ @input=${(e: Event) => {
329
+ newEntryNames.set(sectionKey, (e.target as HTMLInputElement).value);
330
+ }}
331
+ @keydown=${(e: KeyboardEvent) => {
332
+ if (e.key === "Enter") {
333
+ handleCreate();
334
+ }
335
+ if (e.key === "Escape") {
336
+ newEntryOpen.delete(sectionKey);
337
+ rerender();
338
+ }
339
+ }}
340
+ ></sp-textfield>
341
+ <sp-action-button size="s" @click=${handleCreate}>Create</sp-action-button>
342
+ </div>
343
+ `
344
+ : html`
345
+ <sp-action-button
346
+ size="s"
347
+ quiet
348
+ @click=${() => {
349
+ newEntryOpen.add(sectionKey);
350
+ rerender();
351
+ }}
352
+ >
353
+ <sp-icon-add slot="icon"></sp-icon-add> New Entry
354
+ </sp-action-button>
355
+ `}
356
+ </div>
357
+ `;
358
+
359
+ // Right column — entry form
360
+ const selectedEntry = selected ? entries[selected] : undefined;
361
+ const editorTpl: TemplateResult =
362
+ selected && selectedEntry && typeof selectedEntry === "object"
363
+ ? html`
364
+ <div class="settings-editor-panel">
365
+ <div class="settings-editor-header">
366
+ <sp-textfield
367
+ size="s"
368
+ quiet
369
+ class="entry-name-input"
370
+ value=${selected}
371
+ @change=${(e: Event) => {
372
+ const target = e.target as HTMLInputElement;
373
+ handleRename(target.value.trim());
374
+ target.value = selectedEntries.get(sectionKey) ?? selected;
375
+ }}
376
+ @keydown=${(e: KeyboardEvent) => {
377
+ const target = e.target as HTMLInputElement;
378
+ if (e.key === "Enter") {
379
+ target.blur();
380
+ }
381
+ if (e.key === "Escape") {
382
+ target.value = selected;
383
+ target.blur();
384
+ }
385
+ }}
386
+ ></sp-textfield>
387
+ <sp-action-button size="xs" quiet title="Delete entry" @click=${handleDelete}>
388
+ <sp-icon-delete slot="icon"></sp-icon-delete>
389
+ </sp-action-button>
390
+ </div>
391
+ ${renderForm(contribution.entrySchema, selectedEntry as Record<string, unknown>, {
392
+ context: buildContext(sectionKey, opts, selected),
393
+ onChange: (patch) => {
394
+ applyPatch(selectedEntry as Record<string, unknown>, patch);
395
+ rerender();
396
+ void saveProjectConfig();
397
+ },
398
+ rerender,
399
+ ...(ui !== undefined && { ui }),
400
+ })}
401
+ </div>
402
+ `
403
+ : html`<div class="settings-empty-state">Select or create an entry</div>`;
404
+
405
+ return html` <div class="settings-two-col">${listTpl} ${editorTpl}</div> `;
406
+ }
@@ -0,0 +1,145 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Extension settings sections — bridges the extensions payload (platform `listExtensions`) into the
4
+ * settings-modal registry. Every project-section contribution whose class declares a
5
+ * `$studio.settings` block (specs/extensions.md §9.1) gets a section rendered generically by
6
+ * renderContributedSection; sections vanish again when their extension is disabled. The studio
7
+ * hard-codes nothing per extension — parser's Content Types section arrives through this exact
8
+ * path.
9
+ */
10
+
11
+ import { getFormats, loadExtensions, loadFormats } from "../format/format-host";
12
+ import { registerSettingsSection, unregisterSettingsSection } from "./settings-modal";
13
+ import { renderContributedSection } from "./contributed-section";
14
+ import { dataSectionActions } from "../panels/data-grid";
15
+ import type { ExtensionContributionInfo } from "../types";
16
+ import type { JsonSchema } from "../ui/schema-form";
17
+ import type { SettingsContribution } from "./contributed-section";
18
+
19
+ /** The `$studio.settings` block shape consumed here (nav metadata + the renderer inputs). */
20
+ interface StudioSettingsBlock {
21
+ icon?: string;
22
+ label?: string;
23
+ order?: number;
24
+ layout?: "map" | "form";
25
+ entry?: SettingsContribution["settings"]["entry"];
26
+ }
27
+
28
+ /** A contribution resolved into settings-modal registration inputs. */
29
+ export interface DerivedSettingsSection {
30
+ key: string;
31
+ label: string;
32
+ icon?: string | undefined;
33
+ order: number;
34
+ contribution: SettingsContribution;
35
+ }
36
+
37
+ /** Default sort position for contributed sections without an explicit `order`. */
38
+ const DEFAULT_SECTION_ORDER = 100;
39
+
40
+ /**
41
+ * Derive the settings-modal registration for one contribution, or null when its class declares no
42
+ * `$studio.settings` block. The wire `entrySchema` is the SECTION value schema (`properties[<key>]`
43
+ * of the project fragment); map layouts edit one entry at a time, so their form schema is that
44
+ * section schema's `additionalProperties`.
45
+ *
46
+ * @param {ExtensionContributionInfo} info
47
+ * @returns {DerivedSettingsSection | null}
48
+ */
49
+ export function deriveSettingsSection(
50
+ info: ExtensionContributionInfo,
51
+ ): DerivedSettingsSection | null {
52
+ const studio = info.studio as { settings?: StudioSettingsBlock } | null | undefined;
53
+ const settings = studio?.settings;
54
+ if (!settings) {
55
+ return null;
56
+ }
57
+ const { key } = info.project;
58
+ const layout = settings.layout ?? "form";
59
+ const sectionSchema = (info.entrySchema ?? {}) as JsonSchema & {
60
+ additionalProperties?: JsonSchema | boolean;
61
+ };
62
+ const entrySchema =
63
+ layout === "map"
64
+ ? typeof sectionSchema.additionalProperties === "object"
65
+ ? sectionSchema.additionalProperties
66
+ : {}
67
+ : (sectionSchema as JsonSchema);
68
+ const label = settings.label ?? info.project.title ?? key;
69
+ return {
70
+ contribution: {
71
+ entrySchema,
72
+ key,
73
+ settings: {
74
+ ...(settings.entry === undefined ? {} : { entry: settings.entry }),
75
+ ...(settings.layout === undefined ? {} : { layout: settings.layout }),
76
+ },
77
+ title: label,
78
+ },
79
+ icon: settings.icon,
80
+ key,
81
+ label,
82
+ order: settings.order ?? DEFAULT_SECTION_ORDER,
83
+ };
84
+ }
85
+
86
+ /** Section keys this module registered, so stale ones unregister on project/extension change. */
87
+ const registeredKeys = new Set<string>();
88
+
89
+ /**
90
+ * Load the extensions payload and (re)register a settings section per `$studio.settings`
91
+ * contribution, unregistering sections whose extension is no longer enabled. Formats load alongside
92
+ * so the `$formats` context root has data at render time. Call on project activation and after
93
+ * project.json `extensions` changes; loadExtensions caches, so repeat calls are cheap.
94
+ */
95
+ export async function syncExtensionSettingsSections(): Promise<void> {
96
+ const [extensions] = await Promise.all([loadExtensions(), loadFormats()]);
97
+ const next = new Set<string>();
98
+ for (const ext of extensions) {
99
+ for (const info of ext.contributions) {
100
+ const derived = deriveSettingsSection(info);
101
+ if (!derived) {
102
+ continue;
103
+ }
104
+ const { contribution } = derived;
105
+ registerSettingsSection({
106
+ icon: derived.icon,
107
+ key: derived.key,
108
+ label: derived.label,
109
+ order: derived.order,
110
+ render: (container) => {
111
+ // Data-domain sections get the Test/Push/Data-grid actions slot when the platform
112
+ // Implements the protocol's data routes; every other section renders actions-free.
113
+ const actions = dataSectionActions(derived.key);
114
+ renderContributedSection(container, contribution, {
115
+ formats: getFormats(),
116
+ ...(actions === null ? {} : { actions }),
117
+ });
118
+ },
119
+ });
120
+ next.add(derived.key);
121
+ }
122
+ }
123
+ for (const key of registeredKeys) {
124
+ if (!next.has(key)) {
125
+ unregisterSettingsSection(key);
126
+ }
127
+ }
128
+ registeredKeys.clear();
129
+ for (const key of next) {
130
+ registeredKeys.add(key);
131
+ }
132
+ }
133
+
134
+ /** Unregister every section this module added and forget them (project close / tests). */
135
+ export function resetExtensionSettingsSections(): void {
136
+ for (const key of registeredKeys) {
137
+ unregisterSettingsSection(key);
138
+ }
139
+ registeredKeys.clear();
140
+ }
141
+
142
+ /** The section keys currently registered by this module (diagnostics/tests). */
143
+ export function extensionSectionKeys(): string[] {
144
+ return [...registeredKeys];
145
+ }
@@ -87,7 +87,9 @@ export function fieldCardTpl(
87
87
  const isRef = type === "reference";
88
88
  const nestedProps = fieldSchema.properties || {};
89
89
  const nestedRequired = fieldSchema.required || [];
90
- const refTarget = fieldSchema.$ref ? fieldSchema.$ref.replace("#/contentTypes/", "") : "";
90
+ // Reference targets live under a referenceable project section (e.g. `#/content/<type>`); strip
91
+ // The section prefix generically so any section's targets round-trip.
92
+ const refTarget = fieldSchema.$ref ? fieldSchema.$ref.replace(/^#\/[^/]+\//, "") : "";
91
93
 
92
94
  return html`
93
95
  <div class="schema-field-card">
@@ -452,7 +454,7 @@ export function schemaForType(type: string, format?: string) {
452
454
  return { properties: {}, required: [], type: "object" };
453
455
  }
454
456
  case "reference": {
455
- return { $ref: "#/contentTypes/" };
457
+ return { $ref: "#/content/" };
456
458
  }
457
459
  default: {
458
460
  return format ? { format, type: "string" } : { type: "string" };