mechanica 2.0.3 → 2.1.0-beta.2

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 (31) hide show
  1. package/dist/chunks/{editor-JV0Nhfw4.js → editor-MStotuaz.js} +276 -106
  2. package/dist/chunks/node-adapter-4eD9p0iP.js +699 -0
  3. package/dist/chunks/page-assets-DsJXmoDs.js +70 -0
  4. package/dist/chunks/{page-assets-CM_MCuoa.js → rich-text-codec--Nssj0Nn.js} +71 -117
  5. package/dist/chunks/site-DR_X2rzc.js +62 -0
  6. package/dist/cli.js +59 -23
  7. package/dist/composer.js +16 -65
  8. package/dist/editor.css +112 -112
  9. package/dist/editor.js +69 -123
  10. package/dist/export.js +8 -5
  11. package/dist/plugin.js +69 -656
  12. package/dist/server.js +4 -0
  13. package/dist/types/cli/build.d.ts +3 -1
  14. package/dist/types/editor/lib/backend.d.ts +90 -0
  15. package/dist/types/editor/lib/page-list.d.ts +5 -20
  16. package/dist/types/{vite/dev → server}/assets-store.d.ts +2 -2
  17. package/dist/types/server/editor-page.d.ts +23 -0
  18. package/dist/types/server/index.d.ts +11 -0
  19. package/dist/types/server/node-adapter.d.ts +11 -0
  20. package/dist/types/{vite/dev → server}/pages-store.d.ts +5 -5
  21. package/dist/types/{vite → server}/rich-text-codec.d.ts +6 -0
  22. package/dist/types/server/service.d.ts +53 -0
  23. package/dist/types/server/site.d.ts +29 -0
  24. package/dist/types/vite/dev/middleware.d.ts +6 -28
  25. package/package.json +6 -2
  26. /package/dist/types/{vite/dev → server}/composed-store.d.ts +0 -0
  27. /package/dist/types/{vite/dev → server}/data-store.d.ts +0 -0
  28. /package/dist/types/{vite/dev → server}/fs-utils.d.ts +0 -0
  29. /package/dist/types/{vite/dev → server}/image-preview.d.ts +0 -0
  30. /package/dist/types/{vite/dev → server}/page-state.d.ts +0 -0
  31. /package/dist/types/{vite/dev → server}/query-dev.d.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  import { a as renderCrop, c as useDialog, d as _plugin_vue_export_helper_default, i as readImageSize, l as VIcon_default, n as VDialog_default, r as analyzeImageFile, s as dialogKey, t as ImagePickerDialog_default } from "./ImagePickerDialog-DJGhtW3r.js";
2
2
  import { Fragment, Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, inject, mergeProps, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onErrorCaptured, onMounted, openBlock, provide, reactive, ref, renderList, renderSlot, resolveDynamicComponent, shallowRef, toDisplayString, unref, vShow, watch, withCtx, withDirectives, withKeys, withModifiers } from "vue";
3
- import { deepEqual, getDefaultValue } from "mechanica-shared";
3
+ import { EDITOR_API_BASE, UPLOADS_PREFIX, deepEqual, getDefaultValue } from "mechanica-shared";
4
4
  import { unfoldSchema } from "compact-json-schema";
5
5
  import { TextEditor, uid } from "vuewrite";
6
6
  import { blocksToMarkdown, markdownToBlocks } from "vuewrite/markdown";
@@ -540,8 +540,6 @@ var ColorField_default = /* @__PURE__ */ defineComponent({
540
540
  });
541
541
  //#endregion
542
542
  //#region src/editor/lib/image-crop.ts
543
- /** URL prefix uploaded assets are served under (mirrors the dev assets-store). */
544
- var UPLOADS_PREFIX = "/@mechanica/assets/";
545
543
  var positive = (n) => typeof n === "number" && isFinite(n) && n > 0 ? n : void 0;
546
544
  /**
547
545
  * Resolve a field's `crop` annotation into cropping intent, or `null` when the
@@ -670,7 +668,7 @@ function derivativeName(src, rect, target) {
670
668
  /** A readable, filename-safe stem for an asset src (`hero` from `…/hero.png`). */
671
669
  function baseName(src) {
672
670
  if (!src.startsWith(UPLOADS_PREFIX)) return "image";
673
- let file = src.slice(19);
671
+ let file = src.slice(UPLOADS_PREFIX.length);
674
672
  try {
675
673
  file = decodeURIComponent(file);
676
674
  } catch {}
@@ -1047,6 +1045,275 @@ var ImageField_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__P
1047
1045
  }
1048
1046
  }), [["__scopeId", "data-v-664f68c6"]]);
1049
1047
  //#endregion
1048
+ //#region src/editor/lib/save-queue.ts
1049
+ /** Throw from `send` to signal a version conflict (page changed on disk). */
1050
+ var SaveConflictError = class extends Error {
1051
+ constructor(message = "Page changed on disk") {
1052
+ super(message);
1053
+ this.name = "SaveConflictError";
1054
+ }
1055
+ };
1056
+ /**
1057
+ * Debounced, status-tracking save pipeline. Only the newest snapshot is ever
1058
+ * sent; a failure keeps it around (status `error`) until `retry()` or a newer
1059
+ * edit re-triggers the send. No automatic retry loop — the UI surfaces the
1060
+ * failure instead.
1061
+ */
1062
+ function createSaveQueue(options) {
1063
+ const delay = options.delay ?? 500;
1064
+ const state = reactive({ status: "saved" });
1065
+ let timer = null;
1066
+ let last = null;
1067
+ /** A snapshot exists that the server hasn't confirmed yet. */
1068
+ let unsaved = false;
1069
+ /** The save request currently in flight, if any. */
1070
+ let inflight = null;
1071
+ const cancelTimer = () => {
1072
+ if (timer != null) clearTimeout(timer);
1073
+ timer = null;
1074
+ };
1075
+ function send() {
1076
+ if (inflight) return inflight;
1077
+ if (last == null) return Promise.resolve();
1078
+ cancelTimer();
1079
+ state.status = "saving";
1080
+ const snapshot = last;
1081
+ inflight = options.send(snapshot).then(() => {
1082
+ if (last === snapshot) {
1083
+ unsaved = false;
1084
+ state.status = "saved";
1085
+ } else {
1086
+ state.status = "pending";
1087
+ schedule();
1088
+ }
1089
+ }).catch((error) => {
1090
+ state.status = error instanceof SaveConflictError ? "conflict" : "error";
1091
+ }).finally(() => {
1092
+ inflight = null;
1093
+ });
1094
+ return inflight;
1095
+ }
1096
+ const schedule = () => {
1097
+ cancelTimer();
1098
+ timer = setTimeout(() => void send(), delay);
1099
+ };
1100
+ return {
1101
+ get status() {
1102
+ return state.status;
1103
+ },
1104
+ push(snapshot) {
1105
+ last = snapshot;
1106
+ unsaved = true;
1107
+ if (!inflight) state.status = "pending";
1108
+ schedule();
1109
+ },
1110
+ hasUnsaved: () => unsaved || inflight != null,
1111
+ async flush() {
1112
+ cancelTimer();
1113
+ if (inflight) await inflight;
1114
+ if (unsaved && state.status !== "error" && state.status !== "conflict") await send();
1115
+ return !unsaved;
1116
+ },
1117
+ retry() {
1118
+ if (state.status !== "error" && state.status !== "conflict") return;
1119
+ send();
1120
+ },
1121
+ discard() {
1122
+ cancelTimer();
1123
+ last = null;
1124
+ unsaved = false;
1125
+ state.status = "saved";
1126
+ },
1127
+ flushOnUnload() {
1128
+ if (!unsaved && !inflight) return true;
1129
+ if (state.status === "conflict") return false;
1130
+ if (!options.beacon || last == null) return false;
1131
+ const queued = options.beacon(last);
1132
+ if (queued) {
1133
+ unsaved = false;
1134
+ cancelTimer();
1135
+ state.status = "saved";
1136
+ }
1137
+ return queued;
1138
+ },
1139
+ dispose: cancelTimer
1140
+ };
1141
+ }
1142
+ //#endregion
1143
+ //#region src/editor/lib/backend.ts
1144
+ /** A failed request; `message` is fit for showing to the user. */
1145
+ var BackendError = class extends Error {
1146
+ status;
1147
+ constructor(message, status) {
1148
+ super(message);
1149
+ this.status = status;
1150
+ this.name = "BackendError";
1151
+ }
1152
+ };
1153
+ var errorMessage = (body, field) => typeof body?.error === "object" ? body.error[field] : body?.error;
1154
+ /** Slug shared with the thumbs CLI's `pageSlug`: `/` → `index`, `/docs/api` → `docs-api`. */
1155
+ function pageSlug(path) {
1156
+ const trimmed = path.replace(/^\/+|\/+$/g, "");
1157
+ return trimmed ? trimmed.replace(/\//g, "-") : "index";
1158
+ }
1159
+ var JSON_HEADERS = { "content-type": "application/json" };
1160
+ /** Build a backend over HTTP for a host config (all defaults = the dev server). */
1161
+ function createEditorBackend(config = {}) {
1162
+ const base = (config.base ?? EDITOR_API_BASE).replace(/\/+$/, "");
1163
+ const hostHeaders = config.headers ?? {};
1164
+ const hasHostHeaders = Object.keys(hostHeaders).length > 0;
1165
+ const url = (route, params) => {
1166
+ const query = new URLSearchParams();
1167
+ for (const [key, value] of Object.entries(params ?? {})) if (value != null) query.set(key, value);
1168
+ const search = query.toString();
1169
+ return `${base}${route}${search ? `?${search}` : ""}`;
1170
+ };
1171
+ const request = (target, init) => {
1172
+ if (!hasHostHeaders) return init ? fetch(target, init) : fetch(target);
1173
+ return fetch(target, {
1174
+ ...init,
1175
+ headers: {
1176
+ ...hostHeaders,
1177
+ ...init?.headers
1178
+ }
1179
+ });
1180
+ };
1181
+ const postJson = (target, body) => request(target, {
1182
+ method: "POST",
1183
+ headers: JSON_HEADERS,
1184
+ body: JSON.stringify(body)
1185
+ });
1186
+ const readJson = (response) => response.json().catch(() => null);
1187
+ const writePage = async (target, input) => {
1188
+ const response = await postJson(target, input);
1189
+ const data = await readJson(response);
1190
+ if (!response.ok) return {
1191
+ ok: false,
1192
+ error: errorMessage(data, "path") ?? "Could not save the page"
1193
+ };
1194
+ return {
1195
+ ok: true,
1196
+ path: data?.path
1197
+ };
1198
+ };
1199
+ const saveUrl = (target) => url("/save", {
1200
+ path: target.path,
1201
+ locale: target.locale
1202
+ });
1203
+ const upload = async (body, name, derived) => {
1204
+ const response = await request(url("/upload"), {
1205
+ method: "POST",
1206
+ headers: {
1207
+ "x-file-name": encodeURIComponent(name),
1208
+ ...derived ? { "x-derived-asset": "1" } : {}
1209
+ },
1210
+ body
1211
+ });
1212
+ if (!response.ok) throw new BackendError(`Upload failed (${response.status})`, response.status);
1213
+ return await response.json();
1214
+ };
1215
+ const writeComposed = async (target, body, verb) => {
1216
+ const response = await postJson(target, body);
1217
+ if (response.status === 409) throw new SaveConflictError();
1218
+ if (!response.ok) throw new BackendError(errorMessage(await readJson(response), "id") ?? `${verb} failed (${response.status})`, response.status);
1219
+ return { version: (await readJson(response))?.version ?? null };
1220
+ };
1221
+ return {
1222
+ capabilities: {
1223
+ composer: true,
1224
+ ...config.capabilities
1225
+ },
1226
+ pages: {
1227
+ list: async () => (await request(url("/pages"))).json(),
1228
+ create: (input) => writePage(url("/pages"), input),
1229
+ update: (path, input) => writePage(url("/pages", { path }), input),
1230
+ duplicate: (path, input) => writePage(url("/pages/duplicate", { path }), input),
1231
+ remove: async (path) => (await request(url("/pages", { path }), { method: "DELETE" })).ok,
1232
+ setDraft: async (path, draft) => (await postJson(url("/pages/draft", { path }), { draft })).ok,
1233
+ createTranslation: async (path, locale) => {
1234
+ const response = await request(url("/pages/translation", {
1235
+ path,
1236
+ locale
1237
+ }), { method: "POST" });
1238
+ if (!response.ok && response.status !== 400) throw new BackendError(`Create failed (${response.status})`, response.status);
1239
+ },
1240
+ state: async (path) => {
1241
+ const response = await request(url("/state", { path }));
1242
+ return response.ok ? await response.json() : null;
1243
+ },
1244
+ save: async (target, body) => {
1245
+ const response = await postJson(saveUrl(target), body);
1246
+ if (response.status === 409) throw new SaveConflictError();
1247
+ if (!response.ok) throw new BackendError(`Save failed (${response.status})`, response.status);
1248
+ return {
1249
+ success: true,
1250
+ version: (await readJson(response))?.version ?? null
1251
+ };
1252
+ },
1253
+ saveOnUnload: (target, body) => {
1254
+ const payload = JSON.stringify(body);
1255
+ if (hasHostHeaders) {
1256
+ request(saveUrl(target), {
1257
+ method: "POST",
1258
+ headers: JSON_HEADERS,
1259
+ body: payload,
1260
+ keepalive: true
1261
+ });
1262
+ return true;
1263
+ }
1264
+ return navigator.sendBeacon(saveUrl(target), new Blob([payload], { type: "application/json" }));
1265
+ }
1266
+ },
1267
+ assets: {
1268
+ upload: async (file) => {
1269
+ const { src, previewSrc, width, height } = await upload(file, file.name, false);
1270
+ return {
1271
+ src,
1272
+ previewSrc,
1273
+ width,
1274
+ height
1275
+ };
1276
+ },
1277
+ uploadDerived: async (blob, name) => ({ src: (await upload(blob, name, true)).src }),
1278
+ images: async () => {
1279
+ const response = await request(url("/images"));
1280
+ return response.ok ? await response.json() : [];
1281
+ }
1282
+ },
1283
+ composed: {
1284
+ get: async (id) => {
1285
+ const response = await request(url("/composed/get", { id }));
1286
+ return response.ok ? await response.json() : null;
1287
+ },
1288
+ create: (def) => writeComposed(url("/composed/create"), def, "Create"),
1289
+ save: (id, def, version) => writeComposed(url("/composed/save", { id }), {
1290
+ def,
1291
+ version
1292
+ }, "Save"),
1293
+ remove: async (id) => {
1294
+ await request(url("/composed/delete", { id }), { method: "POST" });
1295
+ }
1296
+ },
1297
+ urls: {
1298
+ pageThumb: (path) => `${base}/thumbs/${pageSlug(path)}.png`,
1299
+ blockThumb: (blockId) => `${base}/thumbs/blocks/${encodeURIComponent(blockId)}.png`,
1300
+ composer: (blockId) => `${base}/composer/${encodeURIComponent(blockId)}`
1301
+ },
1302
+ onStoreChange(listener) {
1303
+ return () => {};
1304
+ }
1305
+ };
1306
+ }
1307
+ var current = null;
1308
+ /**
1309
+ * The backend for this page — created on first use from the host's
1310
+ * `window.__MECHANICA_EDITOR__` config (absent → the dev server).
1311
+ */
1312
+ function editorBackend() {
1313
+ if (!current) current = createEditorBackend(typeof window === "undefined" ? void 0 : window.__MECHANICA_EDITOR__);
1314
+ return current;
1315
+ }
1316
+ //#endregion
1050
1317
  //#region src/editor/lib/page-list.ts
1051
1318
  var pagesPromise = null;
1052
1319
  /**
@@ -1054,17 +1321,15 @@ var pagesPromise = null;
1054
1321
  * many link fields share one request. Resolves to `[]` when unavailable.
1055
1322
  */
1056
1323
  function fetchPages() {
1057
- if (!pagesPromise) pagesPromise = fetch("/@mechanica/pages").then((response) => response.json()).catch(() => []);
1324
+ if (!pagesPromise) pagesPromise = editorBackend().pages.list().catch(() => []);
1058
1325
  return pagesPromise;
1059
1326
  }
1060
1327
  /**
1061
- * The dev-server URL of a page's thumbnail (written by `mechanica thumbs` into
1062
- * `.mech/thumbs/`). Slug mirrors the CLI's `pageSlug`: `/` → `index`,
1063
- * `/docs/api` → `docs-api`. The file may not exist — callers need a fallback.
1328
+ * The URL of a page's thumbnail (written by `mechanica thumbs` into
1329
+ * `.mech/thumbs/`). The file may not exist — callers need a fallback.
1064
1330
  */
1065
1331
  function pageThumbUrl(path) {
1066
- const trimmed = path.replace(/^\/+|\/+$/g, "");
1067
- return `/@mechanica/thumbs/${trimmed ? trimmed.replace(/\//g, "-") : "index"}.png`;
1332
+ return editorBackend().urls.pageThumb(path);
1068
1333
  }
1069
1334
  /**
1070
1335
  * Derive a URL path from a page name: a lowercase ASCII slug, prefixed with
@@ -3217,99 +3482,4 @@ function normalizeTemplate(template) {
3217
3482
  return [createRootFrame(template.slice())];
3218
3483
  }
3219
3484
  //#endregion
3220
- //#region src/editor/lib/save-queue.ts
3221
- /** Throw from `send` to signal a version conflict (page changed on disk). */
3222
- var SaveConflictError = class extends Error {
3223
- constructor(message = "Page changed on disk") {
3224
- super(message);
3225
- this.name = "SaveConflictError";
3226
- }
3227
- };
3228
- /**
3229
- * Debounced, status-tracking save pipeline. Only the newest snapshot is ever
3230
- * sent; a failure keeps it around (status `error`) until `retry()` or a newer
3231
- * edit re-triggers the send. No automatic retry loop — the UI surfaces the
3232
- * failure instead.
3233
- */
3234
- function createSaveQueue(options) {
3235
- const delay = options.delay ?? 500;
3236
- const state = reactive({ status: "saved" });
3237
- let timer = null;
3238
- let last = null;
3239
- /** A snapshot exists that the server hasn't confirmed yet. */
3240
- let unsaved = false;
3241
- /** The save request currently in flight, if any. */
3242
- let inflight = null;
3243
- const cancelTimer = () => {
3244
- if (timer != null) clearTimeout(timer);
3245
- timer = null;
3246
- };
3247
- function send() {
3248
- if (inflight) return inflight;
3249
- if (last == null) return Promise.resolve();
3250
- cancelTimer();
3251
- state.status = "saving";
3252
- const snapshot = last;
3253
- inflight = options.send(snapshot).then(() => {
3254
- if (last === snapshot) {
3255
- unsaved = false;
3256
- state.status = "saved";
3257
- } else {
3258
- state.status = "pending";
3259
- schedule();
3260
- }
3261
- }).catch((error) => {
3262
- state.status = error instanceof SaveConflictError ? "conflict" : "error";
3263
- }).finally(() => {
3264
- inflight = null;
3265
- });
3266
- return inflight;
3267
- }
3268
- const schedule = () => {
3269
- cancelTimer();
3270
- timer = setTimeout(() => void send(), delay);
3271
- };
3272
- return {
3273
- get status() {
3274
- return state.status;
3275
- },
3276
- push(snapshot) {
3277
- last = snapshot;
3278
- unsaved = true;
3279
- if (!inflight) state.status = "pending";
3280
- schedule();
3281
- },
3282
- hasUnsaved: () => unsaved || inflight != null,
3283
- async flush() {
3284
- cancelTimer();
3285
- if (inflight) await inflight;
3286
- if (unsaved && state.status !== "error" && state.status !== "conflict") await send();
3287
- return !unsaved;
3288
- },
3289
- retry() {
3290
- if (state.status !== "error" && state.status !== "conflict") return;
3291
- send();
3292
- },
3293
- discard() {
3294
- cancelTimer();
3295
- last = null;
3296
- unsaved = false;
3297
- state.status = "saved";
3298
- },
3299
- flushOnUnload() {
3300
- if (!unsaved && !inflight) return true;
3301
- if (state.status === "conflict") return false;
3302
- if (!options.beacon || last == null) return false;
3303
- const queued = options.beacon(last);
3304
- if (queued) {
3305
- unsaved = false;
3306
- cancelTimer();
3307
- state.status = "saved";
3308
- }
3309
- return queued;
3310
- },
3311
- dispose: cancelTimer
3312
- };
3313
- }
3314
- //#endregion
3315
- export { createContextMenu as A, placeBlock as C, registerBuiltinFieldEditors as D, uid$1 as E, groupPagesByFolder as F, pageThumbUrl as I, pathFromName as L, SmartLinkField_default as M, fetchPages as N, registerRichTextWidgets as O, filterPages as P, VSelect_default as R, moveBlock as S, removeBlock as T, duplicateBlock as _, SchemaForm_default as a, findParentList as b, shortcutChar as c, compareBlocks as d, createContentBlock as f, cloneBlock as g, humanize as h, normalizeTemplate as i, VSegmented_default as j, contextMenuKey as k, blockAvailableForLayout as l, toBlockMeta as m, createSaveQueue as n, SchemaField_default as o, isUnderFolder as p, createRootFrame as r, FieldControl_default as s, SaveConflictError as t, blockAvailableIn as u, ensureSlotList as v, relocateBlock as w, findParentSlot as x, findBlock as y, VPopover_default as z };
3485
+ export { SmartLinkField_default as A, removeBlock as C, contextMenuKey as D, registerRichTextWidgets as E, pathFromName as F, editorBackend as I, createSaveQueue as L, filterPages as M, groupPagesByFolder as N, createContextMenu as O, pageThumbUrl as P, VSelect_default as R, relocateBlock as S, registerBuiltinFieldEditors as T, findBlock as _, FieldControl_default as a, moveBlock as b, blockAvailableIn as c, isUnderFolder as d, toBlockMeta as f, ensureSlotList as g, duplicateBlock as h, SchemaField_default as i, fetchPages as j, VSegmented_default as k, compareBlocks as l, cloneBlock as m, normalizeTemplate as n, shortcutChar as o, humanize as p, SchemaForm_default as r, blockAvailableForLayout as s, createRootFrame as t, createContentBlock as u, findParentList as v, uid$1 as w, placeBlock as x, findParentSlot as y, VPopover_default as z };