mechanica 2.0.0-alpha.4 → 2.0.0-alpha.6

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.
package/bin/mechanica.js CHANGED
File without changes
@@ -1,4 +1,11 @@
1
1
  import { Fragment, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, mergeProps, normalizeClass, onMounted, openBlock, ref, renderList, renderSlot, shallowReactive, toDisplayString, unref, withCtx, withModifiers } from "vue";
2
+ //#region \0plugin-vue:export-helper
3
+ var _plugin_vue_export_helper_default = (sfc, props) => {
4
+ const target = sfc.__vccOpts || sfc;
5
+ for (const [key, val] of props) target[key] = val;
6
+ return target;
7
+ };
8
+ //#endregion
2
9
  //#region \0svg-glob:C:\Users\Den\Desktop\CURVES\mechanica\packages\mechanica\src\editor\icons
3
10
  var contents = {
4
11
  "align-center": `<path d="M4 6h16M7 12h10M6 18h12"/>`,
@@ -543,13 +550,6 @@ var VIcon_default = /* @__PURE__ */ defineComponent({
543
550
  }
544
551
  });
545
552
  //#endregion
546
- //#region \0plugin-vue:export-helper
547
- var _plugin_vue_export_helper_default = (sfc, props) => {
548
- const target = sfc.__vccOpts || sfc;
549
- for (const [key, val] of props) target[key] = val;
550
- return target;
551
- };
552
- //#endregion
553
553
  //#region src/editor/ui/dialog.ts
554
554
  var dialogKey = Symbol("mech-dialog");
555
555
  /** Create the reactive dialog store (provided once by the editor shell). */
@@ -869,4 +869,4 @@ var ImagePickerDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(
869
869
  }
870
870
  }), [["__scopeId", "data-v-4ef9dd0d"]]);
871
871
  //#endregion
872
- export { renderCrop as a, useDialog as c, contents as d, readImageSize as i, _plugin_vue_export_helper_default as l, VDialog_default as n, createDialogStore as o, analyzeImageFile as r, dialogKey as s, ImagePickerDialog_default as t, VIcon_default as u };
872
+ export { renderCrop as a, useDialog as c, _plugin_vue_export_helper_default as d, readImageSize as i, VIcon_default as l, VDialog_default as n, createDialogStore as o, analyzeImageFile as r, dialogKey as s, ImagePickerDialog_default as t, contents as u };
@@ -1,4 +1,4 @@
1
- import { a as renderCrop, c as useDialog, i as readImageSize, l as _plugin_vue_export_helper_default, n as VDialog_default, r as analyzeImageFile, s as dialogKey, t as ImagePickerDialog_default, u as VIcon_default } from "./ImagePickerDialog-BfjNtRoR.js";
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-CY-mn2UP.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
3
  import { deepEqual, getDefaultValue } from "mechanica-shared";
4
4
  import { unfoldSchema } from "compact-json-schema";
@@ -19,9 +19,146 @@ function resolveFieldEditor(schema) {
19
19
  if (schema.type && editors.has(schema.type)) return editors.get(schema.type);
20
20
  return editors.get("string");
21
21
  }
22
+ /**
23
+ * The non-breaking character a keyboard event asks for, or `null`. Matches Word's
24
+ * muscle memory: Ctrl/Cmd+Shift+Space → nbsp, Ctrl/Cmd+Shift+Minus → nb-hyphen.
25
+ * Keyed off `event.code` (physical key) so it fires on any keyboard layout, the
26
+ * same way the editor's other shortcuts stay layout-independent.
27
+ */
28
+ function typographyCharForEvent(event) {
29
+ if (!(event.ctrlKey || event.metaKey) || !event.shiftKey || event.altKey) return null;
30
+ if (event.code === "Space") return "\xA0";
31
+ if (event.code === "Minus") return "‑";
32
+ return null;
33
+ }
34
+ /**
35
+ * Insert `char` at the caret of an `<input>`/`<textarea>`, replacing any
36
+ * selection, and leave the caret after it. Returns the field's new value so the
37
+ * caller can emit it. `setRangeText` keeps native undo working.
38
+ */
39
+ function insertAtCursor(el, char) {
40
+ const start = el.selectionStart ?? el.value.length;
41
+ const end = el.selectionEnd ?? start;
42
+ el.setRangeText(char, start, end, "end");
43
+ return el.value;
44
+ }
45
+ var HTML_ESCAPES = {
46
+ "&": "&amp;",
47
+ "<": "&lt;",
48
+ ">": "&gt;"
49
+ };
50
+ function escapeHtml(text) {
51
+ return text.replace(/[&<>]/g, (ch) => HTML_ESCAPES[ch] ?? ch);
52
+ }
53
+ /**
54
+ * HTML that mirrors `text` for the plain-field marker overlay: every character
55
+ * keeps its exact glyph advance (so the layer lines up with the real field), but
56
+ * the two non-breaking characters are wrapped in marker spans the CSS lights up.
57
+ * The characters themselves stay in the spans — that's what preserves the width.
58
+ */
59
+ function highlightTypographyHtml(text) {
60
+ return escapeHtml(text).replaceAll("\xA0", `<span class="mech-typo__nbsp"> </span>`).replaceAll("‑", `<span class="mech-typo__nbhyphen">‑</span>`);
61
+ }
62
+ /**
63
+ * vuewrite `parser`: scans a block's text and emits a one-character style span
64
+ * over every non-breaking character. vuewrite merges these at *render* time only
65
+ * — they never enter the stored model, so the Markdown on disk stays clean while
66
+ * the editor still shows where the glue is.
67
+ */
68
+ function typographyParser(text) {
69
+ const spans = [];
70
+ for (let i = 0; i < text.length; i++) if (text[i] === "\xA0") spans.push({
71
+ start: i,
72
+ end: i + 1,
73
+ style: "nbsp"
74
+ });
75
+ else if (text[i] === "‑") spans.push({
76
+ start: i,
77
+ end: i + 1,
78
+ style: "nbHyphen"
79
+ });
80
+ return spans;
81
+ }
82
+ //#endregion
83
+ //#region src/editor/components/TypographyInput.vue?vue&type=script&setup=true&lang.ts
84
+ var _hoisted_1$19 = { class: "mech-typo" };
85
+ var _hoisted_2$15 = [
86
+ "value",
87
+ "placeholder",
88
+ "rows"
89
+ ];
90
+ var _hoisted_3$12 = ["value", "placeholder"];
91
+ var _hoisted_4$9 = ["innerHTML"];
22
92
  //#endregion
23
- //#region src/editor/fields/editors/StringField.vue?vue&type=script&setup=true&lang.ts
24
- var _hoisted_1$20 = ["value", "placeholder"];
93
+ //#region src/editor/components/TypographyInput.vue
94
+ var TypographyInput_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
95
+ __name: "TypographyInput",
96
+ props: {
97
+ modelValue: {},
98
+ multiline: { type: Boolean },
99
+ placeholder: {},
100
+ rows: { default: 3 }
101
+ },
102
+ emits: ["update:modelValue"],
103
+ setup(__props, { emit: __emit }) {
104
+ const props = __props;
105
+ const emit = __emit;
106
+ const fieldRef = ref();
107
+ const overlayRef = ref();
108
+ const highlighted = computed(() => highlightTypographyHtml(props.modelValue ?? ""));
109
+ function onInput(event) {
110
+ emit("update:modelValue", event.target.value);
111
+ nextTick(syncScroll);
112
+ }
113
+ function onKeyDown(event) {
114
+ const char = typographyCharForEvent(event);
115
+ if (!char) return;
116
+ event.preventDefault();
117
+ const el = fieldRef.value;
118
+ if (!el) return;
119
+ emit("update:modelValue", insertAtCursor(el, char));
120
+ nextTick(syncScroll);
121
+ }
122
+ function syncScroll() {
123
+ const el = fieldRef.value;
124
+ const overlay = overlayRef.value;
125
+ if (!el || !overlay) return;
126
+ overlay.scrollTop = el.scrollTop;
127
+ overlay.scrollLeft = el.scrollLeft;
128
+ }
129
+ return (_ctx, _cache) => {
130
+ return openBlock(), createElementBlock("div", _hoisted_1$19, [__props.multiline ? (openBlock(), createElementBlock("textarea", {
131
+ key: 0,
132
+ ref_key: "fieldRef",
133
+ ref: fieldRef,
134
+ class: "mech-input mech-textarea",
135
+ value: __props.modelValue,
136
+ placeholder: __props.placeholder,
137
+ rows: __props.rows,
138
+ onInput,
139
+ onKeydown: onKeyDown,
140
+ onScroll: syncScroll
141
+ }, null, 40, _hoisted_2$15)) : (openBlock(), createElementBlock("input", {
142
+ key: 1,
143
+ ref_key: "fieldRef",
144
+ ref: fieldRef,
145
+ class: "mech-input",
146
+ type: "text",
147
+ value: __props.modelValue,
148
+ placeholder: __props.placeholder,
149
+ onInput,
150
+ onKeydown: onKeyDown,
151
+ onScroll: syncScroll
152
+ }, null, 40, _hoisted_3$12)), createElementVNode("div", {
153
+ ref_key: "overlayRef",
154
+ ref: overlayRef,
155
+ class: normalizeClass(["mech-typo__overlay", __props.multiline ? "is-multiline" : "is-single"]),
156
+ "aria-hidden": "true",
157
+ innerHTML: highlighted.value
158
+ }, null, 10, _hoisted_4$9)]);
159
+ };
160
+ }
161
+ }), [["__scopeId", "data-v-0086c808"]]);
25
162
  //#endregion
26
163
  //#region src/editor/fields/editors/StringField.vue
27
164
  var StringField_default = /* @__PURE__ */ defineComponent({
@@ -33,22 +170,16 @@ var StringField_default = /* @__PURE__ */ defineComponent({
33
170
  emits: ["update:modelValue"],
34
171
  setup(__props, { emit: __emit }) {
35
172
  const emit = __emit;
36
- const onInput = (event) => emit("update:modelValue", event.target.value);
37
173
  return (_ctx, _cache) => {
38
- return openBlock(), createElementBlock("input", {
39
- class: "mech-input",
40
- type: "text",
41
- value: __props.modelValue,
174
+ return openBlock(), createBlock(TypographyInput_default, {
175
+ "model-value": __props.modelValue,
42
176
  placeholder: __props.schema.placeholder,
43
- onInput
44
- }, null, 40, _hoisted_1$20);
177
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => emit("update:modelValue", $event))
178
+ }, null, 8, ["model-value", "placeholder"]);
45
179
  };
46
180
  }
47
181
  });
48
182
  //#endregion
49
- //#region src/editor/fields/editors/TextField.vue?vue&type=script&setup=true&lang.ts
50
- var _hoisted_1$19 = ["value", "placeholder"];
51
- //#endregion
52
183
  //#region src/editor/fields/editors/TextField.vue
53
184
  var TextField_default = /* @__PURE__ */ defineComponent({
54
185
  __name: "TextField",
@@ -59,15 +190,14 @@ var TextField_default = /* @__PURE__ */ defineComponent({
59
190
  emits: ["update:modelValue"],
60
191
  setup(__props, { emit: __emit }) {
61
192
  const emit = __emit;
62
- const onInput = (event) => emit("update:modelValue", event.target.value);
63
193
  return (_ctx, _cache) => {
64
- return openBlock(), createElementBlock("textarea", {
65
- class: "mech-input mech-textarea",
66
- value: __props.modelValue,
194
+ return openBlock(), createBlock(TypographyInput_default, {
195
+ multiline: "",
196
+ "model-value": __props.modelValue,
67
197
  placeholder: __props.schema.placeholder,
68
- rows: "3",
69
- onInput
70
- }, null, 40, _hoisted_1$19);
198
+ rows: 3,
199
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => emit("update:modelValue", $event))
200
+ }, null, 8, ["model-value", "placeholder"]);
71
201
  };
72
202
  }
73
203
  });
@@ -1477,6 +1607,14 @@ var decorator = (style) => {
1477
1607
  }
1478
1608
  };
1479
1609
  case "color": return { style: `color: ${style.meta?.color};` };
1610
+ case "nbsp": return {
1611
+ tag: "span",
1612
+ class: "rt-nbsp"
1613
+ };
1614
+ case "nbHyphen": return {
1615
+ tag: "span",
1616
+ class: "rt-nbhyphen"
1617
+ };
1480
1618
  default: return;
1481
1619
  }
1482
1620
  };
@@ -2065,9 +2203,28 @@ var RichTextEditor_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/*
2065
2203
  autosizeMarkdown();
2066
2204
  nextTick(() => fromMarkdown = false);
2067
2205
  }
2206
+ /** The nbsp / non-breaking-hyphen shortcuts in the raw Markdown view too. */
2207
+ function onMarkdownKeyDown(event) {
2208
+ const char = typographyCharForEvent(event);
2209
+ if (!char || !markdownRef.value) return;
2210
+ event.preventDefault();
2211
+ const md = insertAtCursor(markdownRef.value, char);
2212
+ markdown.value = md;
2213
+ fromMarkdown = true;
2214
+ model.value = markdownToBlocks(md, model.value, { softBreaks: true });
2215
+ autosizeMarkdown();
2216
+ nextTick(() => fromMarkdown = false);
2217
+ }
2068
2218
  function onKeyDown(event) {
2069
2219
  const editor = editorRef.value;
2070
2220
  if (!editor) return;
2221
+ const nb = typographyCharForEvent(event);
2222
+ if (nb) {
2223
+ event.preventDefault();
2224
+ editor.insertText(nb);
2225
+ editor.pushHistory("insertText");
2226
+ return;
2227
+ }
2071
2228
  if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
2072
2229
  const style = {
2073
2230
  KeyB: "bold",
@@ -2192,6 +2349,7 @@ var RichTextEditor_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/*
2192
2349
  class: "mech-rte__surface",
2193
2350
  renderer: unref(renderer),
2194
2351
  decorator: unref(decorator),
2352
+ parser: unref(typographyParser),
2195
2353
  "html-parser": unref(htmlParser),
2196
2354
  onKeydown: onKeyDown,
2197
2355
  onContextmenu: onContextMenu
@@ -2213,6 +2371,7 @@ var RichTextEditor_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/*
2213
2371
  "modelValue",
2214
2372
  "renderer",
2215
2373
  "decorator",
2374
+ "parser",
2216
2375
  "html-parser"
2217
2376
  ])) : (openBlock(), createElementBlock("textarea", {
2218
2377
  key: 2,
@@ -2222,11 +2381,12 @@ var RichTextEditor_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/*
2222
2381
  value: markdown.value,
2223
2382
  spellcheck: "false",
2224
2383
  placeholder: __props.placeholder,
2225
- onInput: onMarkdownInput
2384
+ onInput: onMarkdownInput,
2385
+ onKeydown: onMarkdownKeyDown
2226
2386
  }, null, 40, _hoisted_8))])], 2);
2227
2387
  };
2228
2388
  }
2229
- }), [["__scopeId", "data-v-f118319c"]]);
2389
+ }), [["__scopeId", "data-v-6729e5a2"]]);
2230
2390
  //#endregion
2231
2391
  //#region src/editor/dialogs/RichTextDialog.vue
2232
2392
  var RichTextDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
package/dist/cli.js CHANGED
@@ -204,6 +204,34 @@ function breadcrumbsFor(path, names) {
204
204
  }));
205
205
  }
206
206
  /**
207
+ * Map a baked {@link VirtualPage} to an {@link ExportPage}. The provider emits
208
+ * one per (logical page × locale) and self-declares `locale`/`locales`, so the
209
+ * mapping just localizes the served `path` and carries the alternates — it never
210
+ * goes through the file-based `readTranslation`.
211
+ */
212
+ function virtualToExportPage(v, config) {
213
+ const isDefault = !config || !v.locale || v.locale === config.default;
214
+ const locale = config ? v.locale ?? config.default : void 0;
215
+ const path = isDefault ? v.path : localePath(v.path, v.locale, config);
216
+ const name = typeof v.meta?.title === "string" ? v.meta.title : void 0;
217
+ return {
218
+ path,
219
+ logicalPath: v.path,
220
+ locale,
221
+ translations: v.locales,
222
+ content: v.content ?? [],
223
+ data: v.data ?? {},
224
+ name,
225
+ lastmod: v.lastmod,
226
+ page: {
227
+ meta: v.meta ?? {},
228
+ locale,
229
+ locales: v.locales,
230
+ path: v.path
231
+ }
232
+ };
233
+ }
234
+ /**
207
235
  * Load the pieces needed to preload per-page block chunks: the client build's
208
236
  * Vite manifest and the plugin's block map. Older builds (or builds outside
209
237
  * `mechanica build`) may not have them — links are simply skipped then.
@@ -328,6 +356,15 @@ async function exportProject(cwd, ssr, options = {}) {
328
356
  }
329
357
  for (const [locale, paths] of Object.entries(missing)) if (paths.length) warn(`[mechanica] locale "${locale}" is missing ${paths.length} translation(s) (rendered only in ${config.default}): ${paths.join(", ")}`);
330
358
  }
359
+ if (ssr.generatedPages?.length) {
360
+ const taken = new Set(pages.map((page) => page.path));
361
+ for (const vp of ssr.generatedPages) {
362
+ const generated = virtualToExportPage(vp, config);
363
+ if (taken.has(generated.path)) throw new Error(`[mechanica] generatePages produced "${generated.path}", but a page already exists there — change the generated path or remove the conflicting page.`);
364
+ taken.add(generated.path);
365
+ pages.push(generated);
366
+ }
367
+ }
331
368
  setPageCodec(richText);
332
369
  const querySource = {
333
370
  listPages: (opts) => listPages(join(cwd, ".mech"), opts),
@@ -421,6 +458,7 @@ async function exportProject(cwd, ssr, options = {}) {
421
458
  };
422
459
  const templateHandlesPagination = index.includes("page.pagination");
423
460
  const pageNames = new Map(defaultPages.map((page) => [page.logicalPath, page.name ?? (page.logicalPath === "/" ? "Home" : page.logicalPath.split("/").pop())]));
461
+ for (const vp of ssr.generatedPages ?? []) pageNames.set(vp.path, typeof vp.meta?.title === "string" ? vp.meta.title : vp.path.split("/").pop());
424
462
  /** hreflang alternates for a translated page (each locale + x-default), at page N. */
425
463
  const alternatesFor = (source, pageNum = 1) => {
426
464
  if (!config || !source.translations || source.translations.length < 2) return void 0;
package/dist/composer.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import './editor.css';
2
2
  import { c as ANCHOR_H, i as renderBlocks, l as ANCHOR_V, t as createMechanica } from "./chunks/create-mechanica-CnBFiyfJ.js";
3
- import { C as registerBuiltinFieldEditors, N as VSelect_default, P as VPopover_default, S as uid, _ as findParentSlot, a as FieldControl_default, b as relocateBlock, d as humanize, f as cloneBlock, g as findParentList, h as findBlock, i as SchemaField_default, m as ensureSlotList, n as createSaveQueue, o as shortcutChar, p as duplicateBlock, t as SaveConflictError, u as toBlockMeta, v as moveBlock, x as removeBlock, y as placeBlock } from "./chunks/editor-BYMvxQTM.js";
4
- import { d as contents, l as _plugin_vue_export_helper_default, u as VIcon_default } from "./chunks/ImagePickerDialog-BfjNtRoR.js";
3
+ import { C as registerBuiltinFieldEditors, N as VSelect_default, P as VPopover_default, S as uid, _ as findParentSlot, a as FieldControl_default, b as relocateBlock, d as humanize, f as cloneBlock, g as findParentList, h as findBlock, i as SchemaField_default, m as ensureSlotList, n as createSaveQueue, o as shortcutChar, p as duplicateBlock, t as SaveConflictError, u as toBlockMeta, v as moveBlock, x as removeBlock, y as placeBlock } from "./chunks/editor-9tquvDcS.js";
4
+ import { d as _plugin_vue_export_helper_default, l as VIcon_default, u as contents } from "./chunks/ImagePickerDialog-CY-mn2UP.js";
5
5
  import { Fragment, Teleport, computed, createApp, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, onScopeDispose, openBlock, provide, reactive, ref, renderList, renderSlot, resolveComponent, toDisplayString, unref, vModelText, watch, withCtx, withDirectives, withKeys, withModifiers } from "vue";
6
6
  import { buildPreviewData, getDefaultValue, isBinding, registerFieldSchemas, resolveBindings, walkTree } from "mechanica-shared";
7
7
  import { unfoldSchema } from "compact-json-schema";
package/dist/editor.css CHANGED
@@ -467,6 +467,43 @@
467
467
  padding: 8px 0;
468
468
  color: var(--mech-muted);
469
469
  font-size: 13px;
470
+ }.mech-typo[data-v-0086c808] {
471
+ position: relative;
472
+ display: block;
473
+ width: 100%;
474
+ }
475
+ .mech-typo__overlay[data-v-0086c808] {
476
+ position: absolute;
477
+ inset: 0;
478
+ box-sizing: border-box;
479
+ border: 1px solid transparent;
480
+ z-index: 1;
481
+ overflow: hidden;
482
+ pointer-events: none;
483
+ color: transparent;
484
+ font: inherit;
485
+ font-size: 13.5px;
486
+ }
487
+ .mech-typo__overlay.is-single[data-v-0086c808] {
488
+ padding: 0 12px;
489
+ line-height: 36px;
490
+ white-space: pre;
491
+ }
492
+ .mech-typo__overlay.is-multiline[data-v-0086c808] {
493
+ padding: 9px 12px;
494
+ line-height: 1.45;
495
+ white-space: pre-wrap;
496
+ overflow-wrap: break-word;
497
+ }
498
+ .mech-typo__overlay[data-v-0086c808] .mech-typo__nbsp,
499
+ .mech-typo__overlay[data-v-0086c808] .mech-typo__nbhyphen {
500
+ border-radius: 3px;
501
+ background-color: color-mix(in srgb, var(--mech-accent) 15%, transparent);
502
+ }
503
+ .mech-typo__overlay[data-v-0086c808] .mech-typo__nbsp {
504
+ background-image: radial-gradient(circle at center, var(--mech-accent) 0 1px, transparent 1.4px);
505
+ background-repeat: no-repeat;
506
+ background-position: center;
470
507
  }.mech-popover[data-v-4884cec4] {
471
508
  position: fixed;
472
509
  z-index: 2147483680;
@@ -1124,60 +1161,60 @@ from {
1124
1161
  }
1125
1162
  .mech-rt-table[data-v-0b304924] .vw-table-cell {
1126
1163
  font-size: 13.5px;
1127
- }.mech-rte[data-v-f118319c] {
1164
+ }.mech-rte[data-v-6729e5a2] {
1128
1165
  display: flex;
1129
1166
  flex-direction: column;
1130
1167
  gap: 6px;
1131
1168
  }
1132
- .mech-rte__switchbar[data-v-f118319c] {
1169
+ .mech-rte__switchbar[data-v-6729e5a2] {
1133
1170
  display: flex;
1134
1171
  justify-content: flex-end;
1135
1172
  margin-top: -25px;
1136
1173
  }
1137
- .mech-rte__box[data-v-f118319c] {
1174
+ .mech-rte__box[data-v-6729e5a2] {
1138
1175
  display: flex;
1139
1176
  flex-direction: column;
1140
1177
  border-radius: var(--mech-radius);
1141
1178
  transition: background 0.12s, border-color 0.12s, box-shadow 0.12s;
1142
1179
  }
1143
- .mech-rte--full .mech-rte__box[data-v-f118319c] {
1180
+ .mech-rte--full .mech-rte__box[data-v-6729e5a2] {
1144
1181
  border: 1px solid var(--mech-input-border);
1145
1182
  background: var(--mech-bg);
1146
1183
  overflow: hidden;
1147
1184
  }
1148
- .mech-rte--full .mech-rte__box[data-v-f118319c]:focus-within {
1185
+ .mech-rte--full .mech-rte__box[data-v-6729e5a2]:focus-within {
1149
1186
  border-color: var(--mech-accent);
1150
1187
  box-shadow: 0 0 0 3px var(--mech-ring);
1151
1188
  }
1152
- .mech-rte--minimal .mech-rte__box[data-v-f118319c] {
1189
+ .mech-rte--minimal .mech-rte__box[data-v-6729e5a2] {
1153
1190
  border: 1px solid transparent;
1154
1191
  background: var(--mech-field-bg);
1155
1192
  }
1156
- .mech-rte--minimal .mech-rte__box[data-v-f118319c]:hover {
1193
+ .mech-rte--minimal .mech-rte__box[data-v-6729e5a2]:hover {
1157
1194
  background: var(--mech-field-bg-hover);
1158
1195
  }
1159
- .mech-rte--minimal .mech-rte__box[data-v-f118319c]:focus-within {
1196
+ .mech-rte--minimal .mech-rte__box[data-v-6729e5a2]:focus-within {
1160
1197
  background: var(--mech-bg);
1161
1198
  border-color: var(--mech-accent);
1162
1199
  box-shadow: 0 0 0 3px var(--mech-ring);
1163
1200
  }
1164
- .mech-rte__bar[data-v-f118319c] {
1201
+ .mech-rte__bar[data-v-6729e5a2] {
1165
1202
  display: flex;
1166
1203
  align-items: center;
1167
1204
  gap: 2px;
1168
1205
  padding: 5px 6px;
1169
1206
  border-bottom: 1px solid var(--mech-border);
1170
1207
  }
1171
- .mech-rte__toggle[data-v-f118319c] {
1208
+ .mech-rte__toggle[data-v-6729e5a2] {
1172
1209
  margin-left: auto;
1173
1210
  }
1174
- .mech-rte__divider[data-v-f118319c] {
1211
+ .mech-rte__divider[data-v-6729e5a2] {
1175
1212
  width: 1px;
1176
1213
  align-self: stretch;
1177
1214
  margin: 4px 4px;
1178
1215
  background: var(--mech-border);
1179
1216
  }
1180
- .mech-rte__insert-item[data-v-f118319c] {
1217
+ .mech-rte__insert-item[data-v-6729e5a2] {
1181
1218
  display: flex;
1182
1219
  align-items: center;
1183
1220
  gap: 8px;
@@ -1194,26 +1231,26 @@ from {
1194
1231
  text-align: left;
1195
1232
  cursor: pointer;
1196
1233
  }
1197
- .mech-rte__insert-item[data-v-f118319c]:hover {
1234
+ .mech-rte__insert-item[data-v-6729e5a2]:hover {
1198
1235
  background: var(--mech-hover);
1199
1236
  }
1200
- .mech-rte__insert-icon[data-v-f118319c] {
1237
+ .mech-rte__insert-icon[data-v-6729e5a2] {
1201
1238
  width: 16px;
1202
1239
  height: 16px;
1203
1240
  flex: none;
1204
1241
  color: var(--mech-muted);
1205
1242
  }
1206
- .mech-rte__insert-icon[data-v-f118319c] svg {
1243
+ .mech-rte__insert-icon[data-v-6729e5a2] svg {
1207
1244
  display: block;
1208
1245
  width: 100%;
1209
1246
  height: 100%;
1210
1247
  }
1211
- .mech-rte__surface[data-v-f118319c] .mech-rte__menu-target {
1248
+ .mech-rte__surface[data-v-6729e5a2] .mech-rte__menu-target {
1212
1249
  border-radius: var(--mech-radius-sm);
1213
1250
  background: var(--mech-accent-soft);
1214
1251
  box-shadow: 0 0 0 3px var(--mech-accent-soft);
1215
1252
  }
1216
- .mech-rte__surface[data-v-f118319c] {
1253
+ .mech-rte__surface[data-v-6729e5a2] {
1217
1254
  padding: 11px 13px;
1218
1255
  min-height: 64px;
1219
1256
  line-height: 1.6;
@@ -1223,56 +1260,56 @@ from {
1223
1260
  white-space: pre-wrap;
1224
1261
  overflow-y: auto;
1225
1262
  }
1226
- .mech-rte__surface[data-v-f118319c] p {
1263
+ .mech-rte__surface[data-v-6729e5a2] p {
1227
1264
  margin: 0 0 0.6em;
1228
1265
  }
1229
- .mech-rte__surface[data-v-f118319c] h1,
1230
- .mech-rte__surface[data-v-f118319c] h2,
1231
- .mech-rte__surface[data-v-f118319c] h3 {
1266
+ .mech-rte__surface[data-v-6729e5a2] h1,
1267
+ .mech-rte__surface[data-v-6729e5a2] h2,
1268
+ .mech-rte__surface[data-v-6729e5a2] h3 {
1232
1269
  font-weight: 700;
1233
1270
  margin: 0.85em 0 0.35em;
1234
1271
  }
1235
- .mech-rte__surface[data-v-f118319c] h1 {
1272
+ .mech-rte__surface[data-v-6729e5a2] h1 {
1236
1273
  font-size: 1.5em;
1237
1274
  }
1238
- .mech-rte__surface[data-v-f118319c] h2 {
1275
+ .mech-rte__surface[data-v-6729e5a2] h2 {
1239
1276
  font-size: 1.3em;
1240
1277
  }
1241
- .mech-rte__surface[data-v-f118319c] h3 {
1278
+ .mech-rte__surface[data-v-6729e5a2] h3 {
1242
1279
  font-size: 1.1em;
1243
1280
  }
1244
- .mech-rte__surface[data-v-f118319c] ul,
1245
- .mech-rte__surface[data-v-f118319c] ol {
1281
+ .mech-rte__surface[data-v-6729e5a2] ul,
1282
+ .mech-rte__surface[data-v-6729e5a2] ol {
1246
1283
  margin: 0 0 0.6em;
1247
1284
  padding-left: 0;
1248
1285
  }
1249
- .mech-rte__surface[data-v-f118319c] li {
1286
+ .mech-rte__surface[data-v-6729e5a2] li {
1250
1287
  margin: 0.15em 0 0.15em 1.7em;
1251
1288
  list-style: disc;
1252
1289
  }
1253
- .mech-rte__surface[data-v-f118319c] li.ol {
1290
+ .mech-rte__surface[data-v-6729e5a2] li.ol {
1254
1291
  list-style: decimal;
1255
1292
  }
1256
- .mech-rte__surface[data-v-f118319c] :last-child {
1293
+ .mech-rte__surface[data-v-6729e5a2] :last-child {
1257
1294
  margin-bottom: 0;
1258
1295
  }
1259
- .mech-rte__surface[data-v-f118319c] b {
1296
+ .mech-rte__surface[data-v-6729e5a2] b {
1260
1297
  font-weight: 700;
1261
1298
  }
1262
- .mech-rte__surface[data-v-f118319c] i {
1299
+ .mech-rte__surface[data-v-6729e5a2] i {
1263
1300
  font-style: italic;
1264
1301
  }
1265
- .mech-rte__surface[data-v-f118319c] u {
1302
+ .mech-rte__surface[data-v-6729e5a2] u {
1266
1303
  text-decoration: underline;
1267
1304
  }
1268
- .mech-rte__surface[data-v-f118319c] s {
1305
+ .mech-rte__surface[data-v-6729e5a2] s {
1269
1306
  text-decoration: line-through;
1270
1307
  }
1271
- .mech-rte__surface[data-v-f118319c] a {
1308
+ .mech-rte__surface[data-v-6729e5a2] a {
1272
1309
  color: var(--mech-accent);
1273
1310
  text-decoration: underline;
1274
1311
  }
1275
- .mech-rte__surface[data-v-f118319c] code {
1312
+ .mech-rte__surface[data-v-6729e5a2] code {
1276
1313
  font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
1277
1314
  font-size: 0.88em;
1278
1315
  background: var(--mech-field-bg);
@@ -1280,7 +1317,17 @@ from {
1280
1317
  border-radius: 4px;
1281
1318
  border: 1px solid var(--mech-border);
1282
1319
  }
1283
- .mech-rte__surface[data-v-f118319c] .rt-callout {
1320
+ .mech-rte__surface[data-v-6729e5a2] .rt-nbsp,
1321
+ .mech-rte__surface[data-v-6729e5a2] .rt-nbhyphen {
1322
+ border-radius: 3px;
1323
+ background-color: color-mix(in srgb, var(--mech-accent) 15%, transparent);
1324
+ }
1325
+ .mech-rte__surface[data-v-6729e5a2] .rt-nbsp {
1326
+ background-image: radial-gradient(circle at center, var(--mech-accent) 0 1px, transparent 1.4px);
1327
+ background-repeat: no-repeat;
1328
+ background-position: center;
1329
+ }
1330
+ .mech-rte__surface[data-v-6729e5a2] .rt-callout {
1284
1331
  margin: 0.6em 0;
1285
1332
  padding: 9px 13px;
1286
1333
  border: 1px solid var(--c-border);
@@ -1290,24 +1337,24 @@ from {
1290
1337
  --c-border: #b9c8f5;
1291
1338
  --c-soft: #eef2fe;
1292
1339
  }
1293
- .mech-rte__surface[data-v-f118319c] .rt-callout--tip {
1340
+ .mech-rte__surface[data-v-6729e5a2] .rt-callout--tip {
1294
1341
  --c-border: #aee0c4;
1295
1342
  --c-soft: #eaf8f0;
1296
1343
  }
1297
- .mech-rte__surface[data-v-f118319c] .rt-callout--warning {
1344
+ .mech-rte__surface[data-v-6729e5a2] .rt-callout--warning {
1298
1345
  --c-border: #f4d39a;
1299
1346
  --c-soft: #fdf4e3;
1300
1347
  }
1301
- .mech-rte--minimal .mech-rte__surface[data-v-f118319c] {
1348
+ .mech-rte--minimal .mech-rte__surface[data-v-6729e5a2] {
1302
1349
  min-height: 52px;
1303
1350
  max-height: 220px;
1304
1351
  }
1305
- .mech-rte--minimal .mech-rte__markdown[data-v-f118319c] {
1352
+ .mech-rte--minimal .mech-rte__markdown[data-v-6729e5a2] {
1306
1353
  min-height: 52px;
1307
1354
  max-height: 220px;
1308
1355
  resize: none;
1309
1356
  }
1310
- .mech-rte__markdown[data-v-f118319c] {
1357
+ .mech-rte__markdown[data-v-6729e5a2] {
1311
1358
  border: none;
1312
1359
  resize: none;
1313
1360
  font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
@@ -1318,7 +1365,7 @@ from {
1318
1365
  width: 100%;
1319
1366
  box-sizing: border-box;
1320
1367
  }
1321
- .mech-rte__code[data-v-f118319c] {
1368
+ .mech-rte__code[data-v-6729e5a2] {
1322
1369
  margin: 0.4em 0;
1323
1370
  padding: 10px 12px;
1324
1371
  background: var(--mech-field-bg);
@@ -1328,7 +1375,7 @@ from {
1328
1375
  font-size: 12.5px;
1329
1376
  overflow-x: auto;
1330
1377
  }
1331
- .mech-rte__placeholder[data-v-f118319c] {
1378
+ .mech-rte__placeholder[data-v-6729e5a2] {
1332
1379
  position: absolute;
1333
1380
  color: var(--mech-placeholder);
1334
1381
  pointer-events: none;