@openpresentation/opf-editor 0.26.4 → 0.26.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.
@@ -0,0 +1,30 @@
1
+ // The worker entry of the source pane's text checks (`createSourcePane(el, { worker })`). A host bundles this file as its own worker script
2
+ // (`@openpresentation/opf-editor/source-worker`), for example `new Worker(new URL("@openpresentation/opf-editor/source-worker", import.meta.url), { type: "module" })`
3
+ // with a bundler that understands that form, or builds it as an extra entry point and serves it next to the page. It does the same work as the pane's
4
+ // own inline path (`./source-check.js`), off the main thread. Requests run in order, but when several checks of one kind are waiting only the newest runs
5
+ // (the older ones are answered with `skipped`): the pane drops stale replies anyway.
6
+ import { createCheckService } from "./source-check.js";
7
+
8
+ const service = createCheckService();
9
+ let queue = [];
10
+ let scheduled = false;
11
+
12
+ function drain() {
13
+ scheduled = false;
14
+ const work = queue;
15
+ queue = [];
16
+ // The last check and the last syntax request win; catalogs messages always run, in order, and before the checks that follow them.
17
+ const newest = new Map();
18
+ work.forEach((message, index) => { if (message.type === "check" || message.type === "syntax") newest.set(message.type, index); });
19
+ work.forEach((message, index) => {
20
+ if ((message.type === "check" || message.type === "syntax") && newest.get(message.type) !== index) { self.postMessage({ id: message.id, ok: true, skipped: true }); return; }
21
+ const reply = service.handle(message);
22
+ try { self.postMessage(reply); } catch (error) { self.postMessage({ id: message.id, ok: false, error: `The result could not be sent: ${error?.message ?? error}` }); }
23
+ });
24
+ }
25
+
26
+ self.onmessage = (event) => {
27
+ queue.push(event.data);
28
+ // Messages that arrive while a check runs queue behind it; draining on the next task lets everything already waiting be seen together.
29
+ if (!scheduled) { scheduled = true; setTimeout(drain, 0); }
30
+ };
@@ -220,16 +220,40 @@ function inComment(line, column) {
220
220
  return false;
221
221
  }
222
222
 
223
+ /** The quote character of a quoted scalar that `value` opens and does not close, or null (a quoted scalar may run over several lines). */
224
+ export function openQuote(value) {
225
+ const text = value.trimStart();
226
+ const quote = text[0];
227
+ if (quote !== '"' && quote !== "'") return null;
228
+ return closesQuote(text.slice(1), quote) ? null : quote;
229
+ }
230
+
231
+ /** Whether `text` (the rest of a quoted scalar) holds its closing quote. */
232
+ function closesQuote(text, quote) {
233
+ for (let index = 0; index < text.length; index++) {
234
+ if (quote === '"' && text[index] === "\\") { index++; continue; }
235
+ if (text[index] === quote) {
236
+ if (quote === "'" && text[index + 1] === "'") { index++; continue; }
237
+ return true;
238
+ }
239
+ }
240
+ return false;
241
+ }
242
+
223
243
  /**
224
- * Walk the lines before `stop` (a line index), applying each to `stack`. Lines inside a block scalar or a flow collection that spans lines, blank
225
- * lines, comments and document markers change nothing. Returns `{ block, flow }`: the column a block scalar's text must be indented past (or -1) and
226
- * the depth of an unclosed flow collection (0 when none) at line `stop`, so the caller knows whether that line is inside either.
244
+ * Walk the lines before `stop` (a line index), applying each to `stack`. Lines inside a block scalar, a flow collection or a quoted scalar that span
245
+ * lines, blank lines, comments and document markers change nothing. Returns `{ block, flow, quote, open }`: the column a block scalar's text must be
246
+ * indented past (or -1), the depth of an unclosed flow collection (0 when none), the quote of an unclosed quoted scalar (or null) at line `stop`, so
247
+ * the caller knows whether that line is inside any of them, and for a flow collection `open`: the line it began on and where it sits in the stack.
227
248
  */
228
249
  function walk(lines, stop, stack) {
229
250
  let block = -1;
230
251
  let flow = 0;
252
+ let quote = null;
253
+ let open = null;
231
254
  for (let number = 0; number < stop; number++) {
232
255
  const text = lines[number].text;
256
+ if (quote) { if (closesQuote(text, quote)) quote = null; continue; }
233
257
  if (text.trim() === "") continue;
234
258
  const indent = text.length - text.trimStart().length;
235
259
  if (block >= 0) {
@@ -244,11 +268,14 @@ function walk(lines, stop, stack) {
244
268
  const value = entry ? parsed.rest.slice(entry.valueStart) : parsed.rest;
245
269
  const shape = entry ? shapeOfValue(value) : (parsed.markers.length ? shapeOfValue(value) : undefined);
246
270
  if (!entry && !parsed.markers.length) continue;
247
- enter(stack, parsed, entry, shape);
271
+ const placed = enter(stack, parsed, entry, shape);
248
272
  if (shape?.block) block = entry ? parsed.restStart + entry.keyStart : parsed.markers.at(-1);
249
- else if (shape?.flow) flow = flowDepth(value, 0);
273
+ else if (shape?.flow) {
274
+ flow = flowDepth(value, 0);
275
+ open = flow > 0 ? { number, parsed, entry, value, placed } : null;
276
+ } else if (shape && "scalar" in shape) quote = openQuote(value);
250
277
  }
251
- return { block, flow };
278
+ return { block, flow, quote, open };
252
279
  }
253
280
 
254
281
  // ---- flow collections on one line --------------------------------------------------------------------------------------------------
@@ -401,7 +428,13 @@ function keyToken(text, line, restStart, position) {
401
428
  * colon yet. Options: `explicit` (Ctrl+Space also takes an empty place), `prefix` (the path of the node the text is the body of, for YAML in a fence of
402
429
  * a Markdown slide), `exclude` (keys never offered, the `slides` of front matter).
403
430
  */
404
- export function yamlContextAt(text, position, { explicit = false, prefix = [], exclude = [] } = {}) {
431
+ export function yamlContextAt(text, position, options = {}) {
432
+ const context = readContext(text, position, options);
433
+ // The text the context was read from, and the path it is the body of: the references the value lists offer are looked up in it.
434
+ return context ? { ...context, sourceText: text, prefix: options.prefix ?? [] } : null;
435
+ }
436
+
437
+ function readContext(text, position, { explicit = false, prefix = [], exclude = [] }) {
405
438
  const lines = linesOf(text);
406
439
  const number = lineIndex(lines, position);
407
440
  const line = lines[number];
@@ -414,8 +447,10 @@ export function yamlContextAt(text, position, { explicit = false, prefix = [], e
414
447
  const state = walk(lines, number, stack);
415
448
  const lineIndent = lineText.length - lineText.trimStart().length;
416
449
  if (state.block >= 0 && (trimmedLine === "" ? column > state.block : lineIndent > state.block)) return null;
417
- if (state.flow > 0) return null;
418
450
  const before = lineText.slice(0, column);
451
+ // A line inside a quoted scalar that began above: text, in which only `{{` of a variable is completed.
452
+ if (state.quote) return continuationContext({ text, line, position, before, explicit });
453
+ if (state.flow > 0) return flowContinuation({ text, lines, number, line, position, column, explicit, prefix, exclude, open: state.open });
419
454
  const parsed = readMarkers(before);
420
455
  // The key is read from the whole line: a colon after the caret that ends the key (`top:le|ft: x`) is not the colon before it.
421
456
  const whole = readMarkers(lineText);
@@ -425,6 +460,53 @@ export function yamlContextAt(text, position, { explicit = false, prefix = [], e
425
460
  return entry ? valueContext({ ...shared, entry }) : keyOrItemContext(shared);
426
461
  }
427
462
 
463
+ const VARIABLE_OPEN = /\{\{[A-Za-z0-9._-]*$/;
464
+
465
+ /**
466
+ * A line of text that continues a scalar begun on a line above (a quoted scalar left open, or a sentence that runs on): nothing to complete in it
467
+ * except the name of a variable after `{{`. The context is a value in the text sense (`multiline`), with no path.
468
+ */
469
+ function continuationContext({ text, line, position, before, explicit }) {
470
+ if (!VARIABLE_OPEN.test(before)) return null;
471
+ let end = position;
472
+ while (end < line.end && /[A-Za-z0-9._-]/.test(text[end])) end++;
473
+ const start = line.start + (line.text.length - line.text.trimStart().length);
474
+ return { format: "yaml", mode: "value", multiline: true, token: { quoted: true, closed: false, start, end, from: start, to: end }, path: [], key: undefined, existing: new Set(), document: {}, itemStart: false, flow: false, needsSpace: false, column: 0, indent: 0, explicit };
475
+ }
476
+
477
+ // The text of a line without its comment (a `#` after a space, outside quotes).
478
+ function withoutComment(text) {
479
+ let quote = null;
480
+ for (let index = 0; index < text.length; index++) {
481
+ const character = text[index];
482
+ if (quote) {
483
+ if (quote === '"' && character === "\\") index++;
484
+ else if (character === quote) quote = null;
485
+ } else if (character === '"' || character === "'") {
486
+ if (index === 0 || /[\s[{,:-]/.test(text[index - 1])) quote = character;
487
+ } else if (character === "#" && (index === 0 || isSpace(text[index - 1]))) return text.slice(0, index);
488
+ }
489
+ return text;
490
+ }
491
+
492
+ /**
493
+ * A caret on a line inside a flow collection that began on an earlier line (`tags: [a,` / ` b, |]`, `{theme: minimal,` / ` |}`): the lines from
494
+ * the opening one to the caret are read as one text, so the containers open at the caret and the keys and items before it are known as on one line.
495
+ */
496
+ function flowContinuation({ text, lines, number, line, position, column, explicit, prefix, exclude, open }) {
497
+ if (!open) return null;
498
+ const first = lines[open.number].text;
499
+ const start = open.entry ? open.parsed.restStart + open.entry.valueStart : open.parsed.restStart;
500
+ const middle = lines.slice(open.number + 1, number).map((item) => withoutComment(item.text).trim());
501
+ const joined = [withoutComment(first.slice(start)).trimStart(), ...middle, line.text.slice(0, column).trimStart()].join(" ");
502
+ const flow = flowAt(joined, joined.length);
503
+ if (!flow) return null;
504
+ const { map, seq } = open.placed;
505
+ const frame = open.entry ? map : seq;
506
+ const base = open.entry ? [...map.path, open.entry.key] : [...seq.path, seq.count - 1];
507
+ return flowContext({ text, line, position, flow, base, frame, lines, explicit, prefix, exclude });
508
+ }
509
+
428
510
  function valueContext({ text, lines, line, position, parsed, entry, explicit, prefix, stack, lineIndent, exclude }) {
429
511
  const lineText = line.text;
430
512
  let valueFrom = line.start + parsed.restStart + entry.valueStart;
@@ -465,7 +547,10 @@ function keyOrItemContext({ text, lines, number, line, position, parsed, explici
465
547
  }
466
548
  const quoteOpen = rest[0] === '"' || rest[0] === "'";
467
549
  // The word before the caret is a plain key (or the start of a scalar item) or an unfinished quoted key; anything else (a sentence) is prose.
468
- if (!quoteOpen && rest !== "" && !/^[A-Za-z0-9_.$+:-]+$/.test(rest)) return null;
550
+ if (!quoteOpen && rest !== "" && !/^[A-Za-z0-9_.$+:-]+$/.test(rest)) {
551
+ // A sentence that runs on from a scalar above: only a variable (`{{`) is completed in it.
552
+ return parsed.markers.length ? null : continuationContext({ text, line, position, before: lineText.slice(0, position - line.start), explicit });
553
+ }
469
554
  let seq = null;
470
555
  for (const col of parsed.markers) { seq = stack.seqAt(col); seq.items[seq.count - 1] = { open: true }; }
471
556
  const frame = stack.mapAt(parsed.restStart);
package/dist/styles.css CHANGED
@@ -3639,6 +3639,13 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
3639
3639
  opacity: 0.85;
3640
3640
  }
3641
3641
 
3642
+ :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip-autocomplete > .opf-completion-footer {
3643
+ padding: 4px 8px;
3644
+ font: 12px/1.4 var(--opf-ui-font);
3645
+ color: var(--opf-ui-muted-fg);
3646
+ border-top: 1px solid var(--opf-ui-border);
3647
+ }
3648
+
3642
3649
  :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip-autocomplete .cm-completionMatchedText {
3643
3650
  font-weight: 600;
3644
3651
  text-decoration: none;
@@ -3700,6 +3707,26 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
3700
3707
  .opf-fix[aria-selected="true"] { background: var(--opf-ui-accent); color: var(--opf-ui-accent-fg); }
3701
3708
  :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip .cm-completionInfo { white-space: normal; }
3702
3709
 
3710
+ /* The shape hint (Cmd/Ctrl+Shift+Space): the fields of the object around the caret, each set, next or optional. The state is a word and a mark, not only a color. */
3711
+ .cm-tooltip.opf-hint { display: flex; flex-direction: column; min-width: 22em; max-width: 38em; padding: 4px 0; font: 12.5px var(--opf-ui-font); white-space: normal; }
3712
+ .opf-hint-head { display: flex; justify-content: space-between; gap: 24px; padding: 2px 12px 5px; border-bottom: 1px solid var(--opf-ui-border); }
3713
+ .opf-hint-title { font: 600 12px var(--opf-ui-mono); }
3714
+ .opf-hint-aside { color: var(--opf-ui-muted-fg); font-size: 11.5px; white-space: nowrap; }
3715
+ .opf-hint-rows { display: flex; flex-direction: column; margin: 0; padding: 3px 0 0; list-style: none; }
3716
+ .opf-hint-row { display: grid; grid-template-columns: 8em minmax(0, 1fr) 5.5em; gap: 12px; align-items: baseline; padding: 1px 12px; }
3717
+ .opf-hint-key { font: 12px var(--opf-ui-mono); overflow-wrap: anywhere; }
3718
+ .opf-hint-description { color: var(--opf-ui-muted-fg); }
3719
+ .opf-hint-state { text-align: right; color: var(--opf-ui-muted-fg); font-size: 11.5px; }
3720
+ .opf-hint-row[data-state="set"] .opf-hint-state::before { content: "\2713\00a0"; }
3721
+ .opf-hint-row[data-state="next"] { background: color-mix(in srgb, var(--opf-ui-accent) 12%, transparent); box-shadow: inset 2px 0 0 var(--opf-ui-accent); }
3722
+ .opf-hint-row[data-state="next"] .opf-hint-key,
3723
+ .opf-hint-row[data-state="next"] .opf-hint-state { color: var(--opf-ui-accent-strong); font-weight: 600; }
3724
+ .opf-hint-row[data-state="next"] .opf-hint-state::before { content: "\25B6\00a0"; }
3725
+ /* No animation either way; the rule is explicit so a later one cannot add motion for someone who asked for less. */
3726
+ @media (prefers-reduced-motion: reduce) {
3727
+ .cm-tooltip.opf-hint, .cm-tooltip.opf-hint * { animation: none !important; transition: none !important; }
3728
+ }
3729
+
3703
3730
  /* Ctrl+Space with the list open hides the documentation panel (and shows it again). */
3704
3731
  .cm-editor.opf-docs-off .cm-completionInfo { display: none; }
3705
3732
 
@@ -4007,6 +4034,208 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
4007
4034
  .opf-present-button { transition: none; }
4008
4035
  }
4009
4036
 
4037
+ /* shortcuts.css */
4038
+ /* The keyboard shortcuts dialog (src/source-shortcuts.js). Only --opf-ui-* tokens, no color literals (test/styles.mjs). */
4039
+ .opf-shortcuts,
4040
+ .opf-shortcuts *,
4041
+ .opf-shortcuts *::before,
4042
+ .opf-shortcuts *::after { box-sizing: border-box; }
4043
+ .opf-shortcuts [hidden] { display: none !important; }
4044
+ .opf-shortcuts {
4045
+ position: fixed;
4046
+ inset: 0;
4047
+ z-index: 70;
4048
+ display: grid;
4049
+ grid-template-columns: minmax(0, 640px);
4050
+ justify-content: center;
4051
+ align-content: center;
4052
+ padding: 16px;
4053
+ background: color-mix(in srgb, var(--opf-ui-fg) 32%, transparent);
4054
+ font-family: var(--opf-ui-font);
4055
+ font-size: 13px;
4056
+ line-height: 1.5;
4057
+ color: var(--opf-ui-fg);
4058
+ }
4059
+ .opf-shortcuts .opf-shortcuts-dialog {
4060
+ display: flex;
4061
+ flex-direction: column;
4062
+ gap: 8px;
4063
+ width: 100%;
4064
+ max-height: min(720px, calc(100vh - 32px));
4065
+ padding: 16px 18px 12px;
4066
+ overflow: hidden;
4067
+ background: var(--opf-ui-surface);
4068
+ border: 1px solid var(--opf-ui-border);
4069
+ border-radius: var(--opf-ui-radius-lg);
4070
+ box-shadow: var(--opf-ui-shadow-pop);
4071
+ }
4072
+ .opf-shortcuts .opf-shortcuts-dialog:focus { outline: none; }
4073
+ .opf-shortcuts .opf-shortcuts-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
4074
+ .opf-shortcuts .opf-shortcuts-title { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }
4075
+ .opf-shortcuts .opf-shortcuts-help { margin: 0; color: var(--opf-ui-muted-fg); }
4076
+ .opf-shortcuts .opf-shortcuts-close {
4077
+ display: grid;
4078
+ place-items: center;
4079
+ width: 30px;
4080
+ height: 30px;
4081
+ border: 0;
4082
+ border-radius: var(--opf-ui-radius);
4083
+ background: transparent;
4084
+ color: var(--opf-ui-muted-fg);
4085
+ font: inherit;
4086
+ font-size: 20px;
4087
+ line-height: 1;
4088
+ cursor: pointer;
4089
+ }
4090
+ .opf-shortcuts .opf-shortcuts-close:hover { background: var(--opf-ui-muted); color: var(--opf-ui-fg); }
4091
+ .opf-shortcuts .opf-shortcuts-search {
4092
+ width: 100%;
4093
+ min-height: 34px;
4094
+ padding: 4px 10px;
4095
+ border: 1px solid var(--opf-ui-border);
4096
+ border-radius: var(--opf-ui-radius-md);
4097
+ background: var(--opf-ui-bg);
4098
+ color: inherit;
4099
+ font: inherit;
4100
+ }
4101
+ .opf-shortcuts button:focus-visible,
4102
+ .opf-shortcuts input:focus-visible,
4103
+ .opf-shortcuts .opf-shortcuts-list:focus-visible { outline: 2px solid var(--opf-ui-ring); outline-offset: 2px; }
4104
+ .opf-shortcuts .opf-shortcuts-status { min-height: 18px; color: var(--opf-ui-muted-fg); font-size: 12px; }
4105
+ .opf-shortcuts .opf-shortcuts-list { flex: 1; min-height: 120px; overflow: auto; padding-right: 4px; }
4106
+ .opf-shortcuts .opf-shortcuts-group { margin: 0 0 12px; }
4107
+ .opf-shortcuts .opf-shortcuts-group-title { margin: 8px 0 4px; font-size: 11px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--opf-ui-muted-fg); }
4108
+ .opf-shortcuts .opf-shortcuts-row { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; padding: 4px 0; border-bottom: 1px solid var(--opf-ui-border); }
4109
+ .opf-shortcuts .opf-shortcuts-name { min-width: 0; }
4110
+ .opf-shortcuts .opf-shortcuts-when { color: var(--opf-ui-muted-fg); font-size: 12px; }
4111
+ .opf-shortcuts .opf-shortcuts-keys { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 4px; flex: none; max-width: 55%; }
4112
+ .opf-shortcuts .opf-shortcuts-or { color: var(--opf-ui-muted-fg); font-size: 11px; padding: 0 2px; align-self: center; }
4113
+ .opf-shortcuts .opf-shortcuts-key {
4114
+ display: inline-block;
4115
+ min-width: 22px;
4116
+ padding: 1px 6px;
4117
+ border: 1px solid var(--opf-ui-border-strong);
4118
+ border-bottom-width: 2px;
4119
+ border-radius: var(--opf-ui-radius-sm);
4120
+ background: var(--opf-ui-muted);
4121
+ font: 12px var(--opf-ui-mono);
4122
+ text-align: center;
4123
+ white-space: nowrap;
4124
+ }
4125
+ .opf-shortcuts .opf-shortcuts-none { color: var(--opf-ui-muted-fg); }
4126
+ @media (forced-colors: active) {
4127
+ .opf-shortcuts .opf-shortcuts-dialog,
4128
+ .opf-shortcuts .opf-shortcuts-key { border-color: CanvasText; }
4129
+ }
4130
+
4131
+ /* The symbol picker (src/source-outline.js) */
4132
+ .opf-quickpick {
4133
+ position: absolute;
4134
+ top: 6px;
4135
+ left: 50%;
4136
+ z-index: 40;
4137
+ width: min(480px, calc(100% - 24px));
4138
+ transform: translateX(-50%);
4139
+ display: flex;
4140
+ flex-direction: column;
4141
+ gap: 2px;
4142
+ padding: 6px;
4143
+ background: var(--opf-ui-surface);
4144
+ color: var(--opf-ui-fg);
4145
+ border: 1px solid var(--opf-ui-border);
4146
+ border-radius: var(--opf-ui-radius-lg);
4147
+ box-shadow: var(--opf-ui-shadow-pop);
4148
+ font: 13px var(--opf-ui-font);
4149
+ }
4150
+ .opf-quickpick-input { width: 100%; min-height: 30px; padding: 3px 8px; border: 1px solid var(--opf-ui-border); border-radius: var(--opf-ui-radius-md); background: var(--opf-ui-bg); color: inherit; font: inherit; }
4151
+ .opf-quickpick-input:focus-visible { outline: 2px solid var(--opf-ui-ring); outline-offset: 1px; }
4152
+ .opf-quickpick-list { margin: 0; padding: 2px 0 0; list-style: none; max-height: 280px; overflow: auto; }
4153
+ .opf-quickpick-item { display: flex; align-items: baseline; gap: 8px; min-height: 26px; padding: 3px 10px; border-radius: var(--opf-ui-radius-sm); cursor: pointer; }
4154
+ .opf-quickpick-item.is-selected { background: var(--opf-ui-accent); color: var(--opf-ui-accent-fg); }
4155
+ .opf-quickpick-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
4156
+ .opf-quickpick-detail { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; opacity: 0.75; }
4157
+ .opf-quickpick-kind { margin-left: auto; font-size: 11px; opacity: 0.75; flex: none; }
4158
+ .opf-quickpick-more { padding: 4px 10px; color: var(--opf-ui-muted-fg); font-size: 12px; }
4159
+ .opf-quickpick-status { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
4160
+ @media (forced-colors: active) { .opf-quickpick-item.is-selected { outline: 2px solid Highlight; } }
4161
+
4162
+ /* source-navigation.css */
4163
+ /*
4164
+ * Go to definition, peek, references and rename (source-navigation.js): one block widget under the line of the symbol. Tokens only. The declaration is
4165
+ * marked by a word, an arrow and a heavier line as well as by color, and nothing animates.
4166
+ */
4167
+ .opf-sym {
4168
+ box-sizing: border-box;
4169
+ margin: 4px 0 6px 12px;
4170
+ padding: 6px 10px 8px;
4171
+ border: 1px solid var(--opf-ui-border-strong);
4172
+ border-left: 3px solid var(--opf-ui-accent);
4173
+ border-radius: var(--opf-ui-radius);
4174
+ background: var(--opf-ui-surface);
4175
+ color: var(--opf-ui-fg);
4176
+ box-shadow: var(--opf-ui-shadow-pop);
4177
+ font: 12px/1.45 var(--opf-ui-font);
4178
+ text-align: start;
4179
+ white-space: normal;
4180
+ cursor: default;
4181
+ }
4182
+ .opf-sym:focus-visible,
4183
+ .opf-sym-list:focus-visible { outline: 2px solid var(--opf-ui-ring); outline-offset: 1px; }
4184
+ .opf-sym-head { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
4185
+ .opf-sym-title { flex: 1 1 auto; min-width: 0; font-weight: 600; overflow-wrap: anywhere; }
4186
+ .opf-sym-where { color: var(--opf-ui-muted-fg); }
4187
+ .opf-sym-close {
4188
+ flex: none;
4189
+ min-height: 24px;
4190
+ padding: 0 8px;
4191
+ border: 1px solid var(--opf-ui-border);
4192
+ border-radius: var(--opf-ui-radius-sm);
4193
+ background: var(--opf-ui-muted-2);
4194
+ color: var(--opf-ui-fg);
4195
+ font: inherit;
4196
+ cursor: pointer;
4197
+ }
4198
+ .opf-sym-close:hover { background: var(--opf-ui-muted); }
4199
+ .opf-sym-close:focus-visible,
4200
+ .opf-sym-input:focus-visible { outline: 2px solid var(--opf-ui-ring); outline-offset: 1px; }
4201
+ .opf-sym-lines, .opf-sym-list { margin: 6px 0 0; padding: 0; list-style: none; max-height: 260px; overflow: auto; }
4202
+ .opf-sym-line, .opf-sym-ref { display: flex; align-items: baseline; gap: 8px; min-width: 0; padding: 1px 4px; border-radius: var(--opf-ui-radius-sm); }
4203
+ .opf-sym-no { flex: none; min-width: 3ch; color: var(--opf-ui-muted-fg); font-family: var(--opf-ui-mono); text-align: end; }
4204
+ .opf-sym-mark { flex: none; width: 1ch; color: var(--opf-ui-accent-strong); }
4205
+ .opf-sym-text { flex: 1 1 auto; min-width: 0; font-family: var(--opf-ui-mono); white-space: pre-wrap; overflow-wrap: anywhere; }
4206
+ .opf-sym-line.is-target { background: var(--opf-ui-muted-2); font-weight: 600; }
4207
+ .opf-sym-ref { cursor: pointer; }
4208
+ .opf-sym-ref:hover { background: var(--opf-ui-muted-2); }
4209
+ .opf-sym-ref[aria-selected="true"] { background: var(--opf-ui-accent); color: var(--opf-ui-accent-fg); }
4210
+ .opf-sym-ref[aria-selected="true"] .opf-sym-no,
4211
+ .opf-sym-ref[aria-selected="true"] .opf-sym-badge { color: inherit; }
4212
+ .opf-sym-badge { flex: none; min-width: 6.5em; color: var(--opf-ui-muted-fg); font-size: 11px; }
4213
+ .opf-sym-ref.is-declaration .opf-sym-badge { font-weight: 600; }
4214
+ .opf-sym-hint { margin: 6px 0 0; color: var(--opf-ui-muted-fg); font-size: 11px; }
4215
+ .opf-sym-field { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
4216
+ .opf-sym-label { flex: none; color: var(--opf-ui-muted-fg); }
4217
+ .opf-sym-input {
4218
+ flex: 1 1 auto;
4219
+ min-width: 0;
4220
+ min-height: var(--opf-ui-control-h);
4221
+ padding: 0 8px;
4222
+ border: 1px solid var(--opf-ui-border-strong);
4223
+ border-radius: var(--opf-ui-radius-sm);
4224
+ background: var(--opf-ui-bg);
4225
+ color: var(--opf-ui-fg);
4226
+ font: 13px var(--opf-ui-mono);
4227
+ }
4228
+ .opf-sym-input[aria-invalid="true"] { border-color: var(--opf-ui-danger); }
4229
+ .opf-sym-message { min-height: 1.45em; margin-top: 4px; }
4230
+ .opf-sym-message.is-error { color: var(--opf-ui-danger); font-weight: 600; }
4231
+ .opf-sym-message.is-error::before { content: "Error: "; }
4232
+ .opf-sym-preview { margin-top: 6px; }
4233
+ .opf-sym-image { display: block; max-width: 100%; max-height: 160px; border: 1px solid var(--opf-ui-border); border-radius: var(--opf-ui-radius-sm); }
4234
+ @media (forced-colors: active) {
4235
+ .opf-sym { border: 1px solid CanvasText; }
4236
+ .opf-sym-ref[aria-selected="true"] { outline: 2px solid Highlight; }
4237
+ }
4238
+
4010
4239
  /* source-pane.css */
4011
4240
  /* Source pane (RR-98). Only --opf-ui-* tokens (src/styles/tokens.css). */
4012
4241
  .opf-source {
@@ -4268,6 +4497,25 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
4268
4497
  cursor: pointer;
4269
4498
  }
4270
4499
  .opf-source-finding-fix:hover { background: var(--opf-ui-muted); }
4500
+ .opf-source-finding-details {
4501
+ min-height: 22px;
4502
+ padding: 0 8px;
4503
+ border: 1px solid transparent;
4504
+ border-radius: var(--opf-ui-radius-sm);
4505
+ background: transparent;
4506
+ color: var(--opf-ui-muted-fg);
4507
+ font-size: 12px;
4508
+ text-decoration: underline;
4509
+ cursor: pointer;
4510
+ }
4511
+ .opf-source-finding-details:hover { background: var(--opf-ui-muted); }
4512
+ .opf-source-finding-details[aria-expanded="true"] { text-decoration: none; background: var(--opf-ui-muted); }
4513
+ .opf-source-finding-explain { display: flex; flex-direction: column; gap: 4px; margin: 0 6px 6px 28px; padding: 6px 8px; border-left: 2px solid var(--opf-ui-border); color: var(--opf-ui-fg); font-size: 12px; }
4514
+ .opf-source-finding-explain[hidden] { display: none; }
4515
+ .opf-source-finding-explain-line { margin: 0; overflow-wrap: anywhere; }
4516
+ .opf-source-finding-explain-example { font: 11px var(--opf-ui-mono); overflow-wrap: anywhere; white-space: pre-wrap; }
4517
+ .opf-source-finding-explain-example code { padding: 1px 4px; border-radius: var(--opf-ui-radius-sm); background: var(--opf-ui-muted); }
4518
+ .opf-source-finding-explain-link { color: var(--opf-ui-muted-fg); }
4271
4519
  .opf-source-finding-more { padding: 3px 6px; color: var(--opf-ui-muted-fg); font-size: 12px; }
4272
4520
 
4273
4521
  .opf-source-status { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
@@ -4300,6 +4548,46 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
4300
4548
  .opf-source-confirm button:hover { background: var(--opf-ui-muted); }
4301
4549
  .opf-source-confirm-accept { font-weight: 600; }
4302
4550
 
4551
+ /* Fold gutter (src/source-folding.js) */
4552
+ .opf-source-code .cm-foldGutter { width: 16px; }
4553
+ .opf-source-code .cm-foldGutter .cm-gutterElement { display: flex; align-items: center; justify-content: center; padding: 0; cursor: pointer; }
4554
+ .opf-source-code .cm-foldGutter .opf-fold-marker { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 100%; color: var(--opf-ui-muted-fg); opacity: 0; transition: opacity var(--opf-ui-ease); }
4555
+ .opf-source-code .cm-foldGutter .opf-fold-marker.is-folded,
4556
+ .opf-source-code:hover .cm-foldGutter .opf-fold-marker,
4557
+ .opf-source-code .cm-foldGutter .opf-fold-marker:hover { opacity: 1; }
4558
+ .opf-source-code .cm-foldGutter .opf-fold-marker:hover { color: var(--opf-ui-fg); }
4559
+ .opf-source-code .cm-foldPlaceholder { border: 0; background: var(--opf-ui-muted); color: var(--opf-ui-muted-fg); border-radius: var(--opf-ui-radius-sm); padding: 0 5px; margin: 0 2px; cursor: pointer; }
4560
+ @media (prefers-reduced-motion: reduce) { .opf-source-code .cm-foldGutter .opf-fold-marker { transition: none; } }
4561
+ @media (forced-colors: active) { .opf-source-code .cm-foldGutter .opf-fold-marker { opacity: 1; } }
4562
+
4563
+ /* Outline panel (opt-in `outline` option) */
4564
+ .opf-source-body { display: flex; flex: 1; min-height: 0; min-width: 0; }
4565
+ .opf-source-body > .opf-source-code { flex: 1; min-width: 0; }
4566
+ .opf-source-outline { flex: none; width: 220px; max-width: 40%; overflow: auto; border-right: 1px solid var(--opf-ui-code-border); padding: 4px 0; }
4567
+ .opf-source-outline-list { margin: 0; padding: 0; list-style: none; }
4568
+ .opf-source-outline-item { display: flex; align-items: baseline; gap: 6px; width: 100%; min-height: 26px; padding: 2px 8px; border: 0; background: transparent; color: var(--opf-ui-fg); text-align: left; font: inherit; cursor: pointer; }
4569
+ .opf-source-outline-item:hover { background: var(--opf-ui-muted); }
4570
+ .opf-source-outline-item:focus-visible { outline: 2px solid var(--opf-ui-ring); outline-offset: -2px; }
4571
+ .opf-source-outline-item[aria-current="true"] { background: var(--opf-ui-muted); font-weight: 600; }
4572
+ .opf-source-outline-item.is-section { color: var(--opf-ui-muted-fg); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
4573
+ .opf-source-outline-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
4574
+ .opf-source-outline-detail { margin-left: auto; flex: none; max-width: 45%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; color: var(--opf-ui-muted-fg); }
4575
+ .opf-source-outline-empty { padding: 8px 10px; color: var(--opf-ui-muted-fg); }
4576
+ @media (forced-colors: active) { .opf-source-outline-item[aria-current="true"] { outline: 1px solid Highlight; } }
4577
+
4578
+ /* Forced colors (Windows high contrast): the selection of a list is an outline and a problem keeps its underline and a different shape per severity. */
4579
+ @media (forced-colors: active) {
4580
+ .opf-source-code .cm-tooltip-autocomplete > ul > li[aria-selected],
4581
+ .cm-tooltip .opf-fix[aria-selected="true"] { outline: 2px solid Highlight; outline-offset: -2px; forced-color-adjust: none; background: Canvas; color: CanvasText; }
4582
+ .opf-source-code .cm-lintRange-error { text-decoration: underline wavy CanvasText; }
4583
+ .opf-source-code .cm-lintRange-warning { text-decoration: underline dashed CanvasText; }
4584
+ .opf-source-code .cm-lintRange-info { text-decoration: underline dotted CanvasText; }
4585
+ .opf-source-finding { border: 1px solid CanvasText; }
4586
+ }
4587
+ @media (prefers-reduced-motion: reduce) {
4588
+ .cm-tooltip, .cm-tooltip * { animation: none !important; transition: none !important; }
4589
+ }
4590
+
4303
4591
  /* thumbnails.css */
4304
4592
  /* Thumbnail strip (RR-98). Uses only --opf-ui-* tokens. */
4305
4593
  .opf-thumbs {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-editor",
3
- "version": "0.26.4",
3
+ "version": "0.26.6",
4
4
  "description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -231,6 +231,10 @@
231
231
  "types": "./dist/annotations.d.ts",
232
232
  "default": "./dist/annotations.js"
233
233
  },
234
+ "./source-worker": {
235
+ "types": "./dist/source-worker.d.ts",
236
+ "default": "./dist/source-worker.js"
237
+ },
234
238
  "./source-pane": {
235
239
  "types": "./dist/source-pane.d.ts",
236
240
  "default": "./dist/source-pane.js"
@@ -283,10 +287,10 @@
283
287
  "@lezer/highlight": "1.2.5",
284
288
  "jsonc-parser": "3.3.1",
285
289
  "yaml": "^2.9.1",
286
- "@openpresentation/opf": "^0.26.4"
290
+ "@openpresentation/opf": "^0.26.6"
287
291
  },
288
292
  "peerDependencies": {
289
- "@openpresentation/opf-render": "^0.26.4"
293
+ "@openpresentation/opf-render": "^0.26.6"
290
294
  },
291
295
  "peerDependenciesMeta": {
292
296
  "@openpresentation/opf-render": {
@@ -345,9 +349,9 @@
345
349
  "pdf-lib": "1.17.1",
346
350
  "pdfjs-dist": "6.4.299",
347
351
  "playwright": "1.64.0",
348
- "@openpresentation/gallery": "0.26.4",
349
- "@openpresentation/opf-pptx": "0.26.4",
350
- "@openpresentation/opf-render": "0.26.4"
352
+ "@openpresentation/opf-pptx": "0.26.6",
353
+ "@openpresentation/opf-render": "0.26.6",
354
+ "@openpresentation/gallery": "0.26.6"
351
355
  },
352
356
  "scripts": {
353
357
  "build": "node scripts/build.mjs",
@@ -388,6 +392,7 @@
388
392
  "test:source-keyboard-json-browser": "node test/source-keyboard-json-browser.mjs",
389
393
  "test:source-keyboard-yaml-browser": "node test/source-keyboard-yaml-browser.mjs",
390
394
  "test:source-keyboard-markdown-browser": "node test/source-keyboard-markdown-browser.mjs",
395
+ "test:source-diagnostics-browser": "node test/source-diagnostics-browser.mjs",
391
396
  "test:layout-editor-browser": "node test/layout-editor-browser.mjs",
392
397
  "test:export-license-browser": "node test/export-license-browser.mjs",
393
398
  "test:chart-options-browser": "node test/chart-options-browser.mjs",
@@ -397,5 +402,5 @@
397
402
  "test:export-menu-browser": "node test/export-menu-browser.mjs",
398
403
  "test:agent-handoff-browser": "node test/agent-handoff-browser.mjs"
399
404
  },
400
- "gitHead": "8809122a74a43aa99cdd383a856839ad6f018af6"
405
+ "gitHead": "66937576155a3c10ec86ee3327921b51f0c141ae"
401
406
  }