@openpresentation/opf-editor 0.30.0 → 0.31.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.
package/dist/element.d.ts CHANGED
@@ -38,7 +38,7 @@ export interface OpfEditorElementDefaults {
38
38
  /**
39
39
  * `<opf-editor>`: the editor in one tag. The editor mounts in a `div` inside the element (light DOM), once the element is connected and the
40
40
  * page has parsed its content, and is destroyed one microtask after the element leaves the document (unless it came back: a move in the DOM
41
- * keeps the editor). Attributes `layout`, `toolbar`, `readonly`, `theme`, `density`, `slide` and `src` are observed; see docs/web-component.md.
41
+ * keeps the editor). Attributes `layout`, `toolbar`, `readonly`, `theme`, `density`, `slide`, `src` and `fonts` are observed; see docs/web-component.md.
42
42
  */
43
43
  export interface OpfEditorElement extends HTMLElement {
44
44
  /** The editor handle once it is created, else `null` (before mount, after removal, or when it could not be created). */
@@ -53,7 +53,10 @@ export interface OpfEditorElement extends HTMLElement {
53
53
  document: unknown;
54
54
  /** More `createOpfEditor` options. Read when the editor mounts; setting a different object on a mounted element remounts it, keeping the document. */
55
55
  options: Partial<OpfEditorOptions>;
56
- /** A fonts handle (or what `OpfEditorFontsOption` allows), or the URL of a font root; the `fonts` attribute is a font root. Default: `defineOpfEditorElement`'s. */
56
+ /**
57
+ * A fonts handle (or what `OpfEditorFontsOption` allows), or the URL of a font root. Default: `defineOpfEditorElement`'s, then `options.fonts`, then
58
+ * the `fonts` attribute (a font root; observed: changing it remounts the editor). Setting a different value on a mounted element remounts it.
59
+ */
57
60
  fonts: OpfEditorFontsOption | string | undefined;
58
61
  catalogs: readonly Catalog[] | undefined;
59
62
  /** The attributes as properties. `readOnly` is the `readonly` attribute. */
package/dist/styles.css CHANGED
@@ -7531,6 +7531,11 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
7531
7531
  gap: 4px;
7532
7532
  padding: 0 12px 8px;
7533
7533
  }
7534
+ /* CE-30: the slide row is no wider than the editor; the thumbnail strip scrolls inside it (a narrow box, a phone). */
7535
+ .opf-ws-dock > .opf-ws-navigator,
7536
+ .opf-ws-dock > .opf-ws-thumbnails {
7537
+ max-width: 100%;
7538
+ }
7534
7539
  .opf-ws-status {
7535
7540
  flex: none;
7536
7541
  margin: 0;
@@ -7618,27 +7623,29 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
7618
7623
  flex: 1 1 0;
7619
7624
  min-height: 0;
7620
7625
  }
7621
- /* A phone: the parts stack, the source above the slide, the side panel below. */
7626
+ /* A phone: the parts stack, the source above the slide, the side panel below. CE-30: only in the layouts that have several parts to stack;
7627
+ `embed`, `viewer` and `canvas` are the slide alone and fit it to their box at any width (a stage of 40vh made a short embed overflow).
7628
+ `:where()` keeps the specificity of the plain class. */
7622
7629
  @media (max-width: 760px) {
7623
- .opf-ws-body {
7630
+ :where(.opf-workspace:not([data-opf-layout="embed"], [data-opf-layout="viewer"], [data-opf-layout="canvas"])) .opf-ws-body {
7624
7631
  flex-direction: column;
7625
7632
  overflow: auto;
7626
7633
  }
7627
- .opf-ws-source {
7634
+ :where(.opf-workspace:not([data-opf-layout="embed"], [data-opf-layout="viewer"], [data-opf-layout="canvas"])) .opf-ws-source {
7628
7635
  flex: none;
7629
7636
  min-width: 0;
7630
7637
  height: 40vh;
7631
7638
  border-right: 0;
7632
7639
  border-bottom: 1px solid var(--opf-ui-border);
7633
7640
  }
7634
- .opf-ws-main {
7641
+ :where(.opf-workspace:not([data-opf-layout="embed"], [data-opf-layout="viewer"], [data-opf-layout="canvas"])) .opf-ws-main {
7635
7642
  flex: none;
7636
7643
  }
7637
- .opf-ws-stage {
7644
+ :where(.opf-workspace:not([data-opf-layout="embed"], [data-opf-layout="viewer"], [data-opf-layout="canvas"])) .opf-ws-stage {
7638
7645
  flex: none;
7639
7646
  min-height: 40vh;
7640
7647
  }
7641
- .opf-ws-side {
7648
+ :where(.opf-workspace:not([data-opf-layout="embed"], [data-opf-layout="viewer"], [data-opf-layout="canvas"])) .opf-ws-side {
7642
7649
  flex: none;
7643
7650
  border-left: 0;
7644
7651
  border-top: 1px solid var(--opf-ui-border);
@@ -4038,38 +4038,17 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
4038
4038
  /* ---------------------------------------------------------------- JSON editor (CodeMirror, opf-json / opf-code) */
4039
4039
  /* The editor ships no CSS of its own. Put it in an `.opf-ui`, `.opf-json` or `.opf-code` element (or the component that holds it) to get this look. */
4040
4040
 
4041
- :is(.opf-ui, .opf-json, .opf-code) .cm-editor {
4042
- font: 12.5px/1.65 var(--opf-ui-mono);
4043
- color: var(--opf-ui-fg);
4044
- background: var(--opf-ui-code-bg);
4045
- border: 1px solid var(--opf-ui-code-border);
4046
- border-radius: var(--opf-ui-radius-md);
4047
- }
4048
4041
 
4049
- :is(.opf-ui, .opf-json, .opf-code) .cm-editor.cm-focused {
4050
- outline: 2px solid var(--opf-ui-ring);
4051
- outline-offset: 2px;
4052
- }
4053
4042
 
4054
- :is(.opf-ui, .opf-json, .opf-code) .cm-scroller,
4055
- :is(.opf-ui, .opf-json, .opf-code) .cm-content {
4056
- font-family: var(--opf-ui-mono);
4057
- }
4058
4043
 
4059
- :is(.opf-ui, .opf-json, .opf-code) .cm-content {
4060
- caret-color: var(--opf-ui-fg);
4061
- }
4062
4044
 
4063
- :is(.opf-ui, .opf-json, .opf-code) .cm-cursor,
4064
- :is(.opf-ui, .opf-json, .opf-code) .cm-dropCursor {
4065
- border-left-color: var(--opf-ui-fg);
4066
- }
4067
4045
 
4068
- :is(.opf-ui, .opf-json, .opf-code) .cm-gutters {
4069
- color: var(--opf-ui-muted-fg);
4070
- background: var(--opf-ui-code-bg);
4071
- border-right: 1px solid var(--opf-ui-code-border);
4072
- }
4046
+
4047
+
4048
+
4049
+
4050
+
4051
+
4073
4052
 
4074
4053
  /* A wrapped line continues under its own indentation (src/source-wrap.js, VS Code's wrappingIndent "indent"): `opf-hang-<n>` says how many
4075
4054
  columns, the first line keeps its place (a negative text-indent of the same width) and every continuation line starts n columns in.
@@ -4077,66 +4056,52 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
4077
4056
  from the measured width, because a percentage here makes style and layout of every line very slow (replacing the text of an 848-line
4078
4057
  document took 8.5 s instead of 0.56 s). Text inside an inline widget does not inherit the negative indent. `--opf-line-start` is the line's own
4079
4058
  left padding (the source pane sets it to 2px). */
4080
- .cm-line.opf-hang-0 { --opf-hang: 0; }
4081
- .cm-line.opf-hang-1 { --opf-hang: 1; }
4082
- .cm-line.opf-hang-2 { --opf-hang: 2; }
4083
- .cm-line.opf-hang-3 { --opf-hang: 3; }
4084
- .cm-line.opf-hang-4 { --opf-hang: 4; }
4085
- .cm-line.opf-hang-5 { --opf-hang: 5; }
4086
- .cm-line.opf-hang-6 { --opf-hang: 6; }
4087
- .cm-line.opf-hang-7 { --opf-hang: 7; }
4088
- .cm-line.opf-hang-8 { --opf-hang: 8; }
4089
- .cm-line.opf-hang-9 { --opf-hang: 9; }
4090
- .cm-line.opf-hang-10 { --opf-hang: 10; }
4091
- .cm-line.opf-hang-11 { --opf-hang: 11; }
4092
- .cm-line.opf-hang-12 { --opf-hang: 12; }
4093
- .cm-line.opf-hang-13 { --opf-hang: 13; }
4094
- .cm-line.opf-hang-14 { --opf-hang: 14; }
4095
- .cm-line.opf-hang-15 { --opf-hang: 15; }
4096
- .cm-line.opf-hang-16 { --opf-hang: 16; }
4097
- .cm-line.opf-hang-17 { --opf-hang: 17; }
4098
- .cm-line.opf-hang-18 { --opf-hang: 18; }
4099
- .cm-line.opf-hang-19 { --opf-hang: 19; }
4100
- .cm-line.opf-hang-20 { --opf-hang: 20; }
4101
- .cm-line.opf-hang-21 { --opf-hang: 21; }
4102
- .cm-line.opf-hang-22 { --opf-hang: 22; }
4103
- .cm-line.opf-hang-23 { --opf-hang: 23; }
4104
- .cm-line.opf-hang-24 { --opf-hang: 24; }
4105
- .cm-line.opf-hang-25 { --opf-hang: 25; }
4106
- .cm-line.opf-hang-26 { --opf-hang: 26; }
4107
- .cm-line.opf-hang-27 { --opf-hang: 27; }
4108
- .cm-line.opf-hang-28 { --opf-hang: 28; }
4109
- .cm-line.opf-hang-29 { --opf-hang: 29; }
4110
- .cm-line.opf-hang-30 { --opf-hang: 30; }
4111
- .cm-line.opf-hang-31 { --opf-hang: 31; }
4112
- .cm-line.opf-hang-32 { --opf-hang: 32; }
4113
- :is(.opf-ui, .opf-json, .opf-code) .cm-line {
4114
- padding-left: calc(var(--opf-line-start, 6px) + var(--opf-hang, 0) * 1ch);
4115
- text-indent: calc(-1 * var(--opf-hang, 0) * 1ch);
4116
- }
4117
4059
 
4118
- :is(.opf-ui, .opf-json, .opf-code) .cm-line > * {
4119
- text-indent: 0;
4120
- }
4121
4060
 
4122
- :is(.opf-ui, .opf-json, .opf-code) .cm-activeLine,
4123
- :is(.opf-ui, .opf-json, .opf-code) .cm-line.is-hover {
4124
- background: color-mix(in srgb, var(--opf-ui-accent) 8%, transparent);
4125
- }
4126
4061
 
4127
- :is(.opf-ui, .opf-json, .opf-code) .cm-activeLineGutter {
4128
- color: var(--opf-ui-fg);
4129
- background: color-mix(in srgb, var(--opf-ui-accent) 8%, transparent);
4130
- }
4062
+
4063
+
4064
+
4065
+
4066
+
4067
+
4068
+
4069
+
4070
+
4071
+
4072
+
4073
+
4074
+
4075
+
4076
+
4077
+
4078
+
4079
+
4080
+
4081
+
4082
+
4083
+
4084
+
4085
+
4086
+
4087
+
4088
+
4089
+
4090
+
4091
+
4092
+
4093
+
4094
+
4095
+
4096
+
4097
+
4098
+
4131
4099
 
4132
4100
  /* The selection is a layer behind the text, so the text keeps its own colors: the tint is light enough that every token color (test/selection-contrast-browser.mjs
4133
4101
  measures each) keeps 4.5:1 against it, focused or not, in the light and the dark scheme. The focused rule of CodeMirror's base theme
4134
4102
  (`&light.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground`, `#d7d4f0` for light and `#233` for dark) has five classes, so
4135
4103
  the second selector has six and wins whichever stylesheet loads last; src/source-color-scheme.js flags the view dark so the rest of the base theme follows. */
4136
- :is(.opf-ui, .opf-json, .opf-code) .cm-editor .cm-selectionLayer .cm-selectionBackground,
4137
- :is(.opf-ui, .opf-json, .opf-code) .cm-editor.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground {
4138
- background: color-mix(in srgb, var(--opf-ui-accent) 16%, transparent);
4139
- }
4104
+
4140
4105
 
4141
4106
  /* A selection that is not drawn by CodeMirror (the handoff prompt, the data grid's text areas, the numbering preview) uses the browser's own, and a browser's
4142
4107
  default highlight is not made for our surfaces: the same tint, with the text color the surface already has, is 4.5:1 or better in both schemes. */
@@ -4148,15 +4113,9 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
4148
4113
 
4149
4114
  /* The other occurrences of the selected word: a 16% tint, so the lightest code text on it (a key in the dark scheme) keeps 4.5:1 (25% gave 4.2:1);
4150
4115
  the underline marks them at a glance. test/selection-contrast-browser.mjs measures it in both schemes. */
4151
- :is(.opf-ui, .opf-json, .opf-code) .cm-selectionMatch {
4152
- background: color-mix(in srgb, var(--opf-ui-warn) 16%, transparent);
4153
- box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--opf-ui-warn) 70%, transparent);
4154
- }
4155
4116
 
4156
- :is(.opf-ui, .opf-json, .opf-code) .cm-matchingBracket {
4157
- background: color-mix(in srgb, var(--opf-ui-accent) 16%, transparent);
4158
- outline: 1px solid var(--opf-ui-accent);
4159
- }
4117
+
4118
+
4160
4119
 
4161
4120
  :is(.opf-ui, .opf-json, .opf-code) .tok-key {
4162
4121
  color: var(--opf-ui-accent-strong);
@@ -4180,79 +4139,27 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
4180
4139
  border-radius: var(--opf-ui-radius-sm);
4181
4140
  }
4182
4141
 
4183
- :is(.opf-ui, .opf-json, .opf-code) .cm-panels {
4184
- color: var(--opf-ui-fg);
4185
- background: var(--opf-ui-surface);
4186
- border-color: var(--opf-ui-border);
4187
- font-family: var(--opf-ui-font);
4188
- }
4189
4142
 
4190
- :is(.opf-ui, .opf-json, .opf-code) .cm-panels input,
4191
- :is(.opf-ui, .opf-json, .opf-code) .cm-panels button {
4192
- font: 12px var(--opf-ui-font);
4193
- color: var(--opf-ui-fg);
4194
- background: var(--opf-ui-surface);
4195
- border: 1px solid var(--opf-ui-border);
4196
- border-radius: var(--opf-ui-radius-sm);
4197
- }
4198
4143
 
4199
- :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip {
4200
- color: var(--opf-ui-fg);
4201
- background: var(--opf-ui-surface);
4202
- border: 1px solid var(--opf-ui-border);
4203
- border-radius: var(--opf-ui-radius-md);
4204
- box-shadow: var(--opf-ui-shadow-pop);
4205
- }
4206
4144
 
4207
- :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip.cm-tooltip-autocomplete > ul {
4208
- font-family: var(--opf-ui-mono);
4209
- font-size: 12px;
4210
- }
4211
4145
 
4212
- :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip.cm-tooltip-autocomplete > ul > li {
4213
- padding: 2px 8px;
4214
- color: var(--opf-ui-fg);
4215
- }
4216
4146
 
4217
- :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected] {
4218
- color: var(--opf-ui-accent-fg);
4219
- background: var(--opf-ui-accent);
4220
- }
4221
4147
 
4222
- :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip-autocomplete .cm-completionDetail {
4223
- margin-left: 12px;
4224
- font-family: var(--opf-ui-font);
4225
- font-style: normal;
4226
- color: var(--opf-ui-muted-fg);
4227
- }
4228
4148
 
4229
- :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip-autocomplete > ul > li[aria-selected] .cm-completionDetail {
4230
- color: inherit;
4231
- opacity: 0.85;
4232
- }
4233
4149
 
4234
- :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip-autocomplete > .opf-completion-footer {
4235
- padding: 4px 8px;
4236
- font: 12px/1.4 var(--opf-ui-font);
4237
- color: var(--opf-ui-muted-fg);
4238
- border-top: 1px solid var(--opf-ui-border);
4239
- }
4240
4150
 
4241
- :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip-autocomplete .cm-completionMatchedText {
4242
- font-weight: 600;
4243
- text-decoration: none;
4244
- }
4245
4151
 
4246
- :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip .cm-completionInfo {
4247
- max-width: 280px;
4248
- padding: 6px 8px;
4249
- font: 12px/1.45 var(--opf-ui-font);
4250
- white-space: pre-wrap;
4251
- color: var(--opf-ui-fg);
4252
- background: var(--opf-ui-surface);
4253
- border: 1px solid var(--opf-ui-border);
4254
- border-radius: var(--opf-ui-radius-md);
4255
- }
4152
+
4153
+
4154
+
4155
+
4156
+
4157
+
4158
+
4159
+
4160
+
4161
+
4162
+
4256
4163
 
4257
4164
  /* The live region next to the code: read by screen readers, never seen. */
4258
4165
  .opf-source-live {
@@ -4268,16 +4175,7 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
4268
4175
  }
4269
4176
 
4270
4177
  /* The documentation cards: beside the selected suggestion and over a key or a value. */
4271
- :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip .opf-doc,
4272
- .opf-source-code .cm-tooltip .opf-doc {
4273
- display: flex;
4274
- flex-direction: column;
4275
- gap: 4px;
4276
- max-width: 340px;
4277
- padding: 2px 0;
4278
- font: 12px/1.45 var(--opf-ui-font);
4279
- white-space: normal;
4280
- }
4178
+
4281
4179
  .opf-doc-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; }
4282
4180
  .opf-doc-title { font-family: var(--opf-ui-mono); font-weight: 600; }
4283
4181
  .opf-doc-type { color: var(--opf-ui-muted-fg); }
@@ -4289,18 +4187,18 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
4289
4187
  .opf-doc-more { color: var(--opf-ui-muted-fg); font-size: 11px; }
4290
4188
 
4291
4189
  /* A problem's tooltip and the quick fix list. */
4292
- .cm-tooltip-lint .opf-problem { display: flex; flex-direction: column; gap: 2px; max-width: 360px; white-space: normal; }
4190
+
4293
4191
  .opf-problem-next { color: var(--opf-ui-muted-fg); }
4294
- .cm-tooltip-lint .cm-diagnosticAction { margin: 4px 6px 0 0; padding: 1px 8px; border: 1px solid var(--opf-ui-border); border-radius: var(--opf-ui-radius-sm); background: var(--opf-ui-muted); color: var(--opf-ui-fg); font: 12px var(--opf-ui-font); cursor: pointer; }
4295
- .cm-tooltip-lint .cm-diagnosticSource { color: var(--opf-ui-muted-fg); }
4192
+
4193
+
4296
4194
  .opf-problem-hint { color: var(--opf-ui-muted-fg); font-size: 11px; }
4297
- .cm-tooltip .opf-fixes { display: flex; flex-direction: column; min-width: 180px; padding: 3px; font: 12px var(--opf-ui-font); }
4195
+
4298
4196
  .opf-fix { padding: 3px 8px; border-radius: var(--opf-ui-radius-sm); white-space: nowrap; }
4299
4197
  .opf-fix[aria-selected="true"] { background: var(--opf-ui-accent); color: var(--opf-ui-accent-fg); }
4300
- :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip .cm-completionInfo { white-space: normal; }
4198
+
4301
4199
 
4302
4200
  /* 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. */
4303
- .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; }
4201
+
4304
4202
  .opf-hint-head { display: flex; justify-content: space-between; gap: 24px; padding: 2px 12px 5px; border-bottom: 1px solid var(--opf-ui-border); }
4305
4203
  .opf-hint-title { font: 600 12px var(--opf-ui-mono); }
4306
4204
  .opf-hint-aside { color: var(--opf-ui-muted-fg); font-size: 11.5px; white-space: nowrap; }
@@ -4315,20 +4213,14 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
4315
4213
  .opf-hint-row[data-state="next"] .opf-hint-state { color: var(--opf-ui-accent-strong); font-weight: 600; }
4316
4214
  .opf-hint-row[data-state="next"] .opf-hint-state::before { content: "\25B6\00a0"; }
4317
4215
  /* No animation either way; the rule is explicit so a later one cannot add motion for someone who asked for less. */
4318
- @media (prefers-reduced-motion: reduce) {
4319
- .cm-tooltip.opf-hint, .cm-tooltip.opf-hint * { animation: none !important; transition: none !important; }
4320
- }
4216
+
4321
4217
 
4322
4218
  /* Ctrl+Space with the list open hides the documentation panel (and shows it again). */
4323
- .cm-editor.opf-docs-off .cm-completionInfo { display: none; }
4324
4219
 
4325
- :is(.opf-ui, .opf-json, .opf-code) .cm-diagnostic-error {
4326
- border-left-color: var(--opf-ui-danger);
4327
- }
4328
4220
 
4329
- :is(.opf-ui, .opf-json, .opf-code) .cm-diagnostic-warning {
4330
- border-left-color: var(--opf-ui-warn);
4331
- }
4221
+
4222
+
4223
+
4332
4224
 
4333
4225
  /* The field-options popover. Its inline style uses system colors; color-scheme follows the theme, and these keep it on the tokens. */
4334
4226
  :is(.opf-ui, .opf-json, .opf-code) .json-field-menu {
@@ -5058,18 +4950,18 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
5058
4950
  /* Code */
5059
4951
  .opf-source-code { position: relative; flex: 1; min-height: 0; overflow: hidden; display: flex; }
5060
4952
  /* The pane frame draws the edge: the editor inside it has no border or radius of its own (panels.css gives a standalone editor both). */
5061
- .opf-source-code > .cm-editor { flex: 1; min-width: 0; height: 100%; background: transparent; color: var(--opf-ui-fg); border: 0; border-radius: 0; }
5062
- .opf-source-code .cm-editor.cm-focused { outline: none; }
5063
- .opf-source-code .cm-scroller { overflow: auto; font: 400 13.5px/22px var(--opf-ui-mono); }
5064
- .opf-source[data-density="touch"] .opf-source-code .cm-scroller { font-size: 13px; line-height: 21px; }
5065
- .opf-source-code .cm-content { padding: 16px 16px 16px 4px; caret-color: var(--opf-ui-fg); }
5066
- .opf-source-code .cm-line { --opf-line-start: 2px; padding-top: 0; padding-right: 2px; padding-bottom: 0; }
5067
- .opf-source-code .cm-gutters { border: 0; background: transparent; color: var(--opf-ui-muted-fg); }
5068
- .opf-source-code .cm-lineNumbers .cm-gutterElement { min-width: 30px; padding: 0 12px 0 0; text-align: right; font-size: 11px; }
5069
- .opf-source-code .cm-activeLine { background: transparent; }
5070
- /* The selection color is the shared one in panels.css (JSON editor section). */
5071
- .opf-source-code .cm-cursor { border-left-color: var(--opf-ui-fg); }
5072
- .opf-source-code .tok-key { color: var(--opf-ui-accent-strong); }
4953
+
4954
+
4955
+
4956
+
4957
+
4958
+
4959
+
4960
+
4961
+
4962
+ /* The selection color is the shared one in panels.css (JSON editor section). */
4963
+
4964
+ .opf-source-code .tok-key { color: var(--opf-ui-accent-strong); }
5073
4965
  .opf-source-code .tok-str { color: var(--opf-ui-fg); }
5074
4966
  .opf-source-code .tok-num { color: var(--opf-ui-warn); }
5075
4967
  .opf-source-code .tok-punct,
@@ -5079,7 +4971,7 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
5079
4971
  .opf-source-code .tok-head { color: var(--opf-ui-accent-strong); font-weight: 600; }
5080
4972
  .opf-source-code .tok-comment { font-style: italic; }
5081
4973
  .opf-source-code .code-key.is-active { background: color-mix(in srgb, var(--opf-ui-accent) 10%, transparent); border-radius: var(--opf-ui-radius-sm); }
5082
- .opf-source-code .cm-line.is-hover { background: color-mix(in srgb, var(--opf-ui-accent) 6%, transparent); }
4974
+
5083
4975
  /* CE-17: decorations a host draws (setDecorations) and the flash of revealPath; the host styles any other kind as .opf-source-decoration-<kind>. */
5084
4976
  .opf-source-code .opf-source-decoration { background: color-mix(in srgb, var(--opf-ui-accent) 16%, transparent); border-radius: var(--opf-ui-radius-sm); }
5085
4977
  .opf-source-code .opf-source-decoration-error { background: color-mix(in srgb, var(--opf-ui-danger) 18%, transparent); }
@@ -5087,12 +4979,12 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
5087
4979
  .opf-source-code .opf-source-decoration-info { background: color-mix(in srgb, var(--opf-ui-accent) 10%, transparent); }
5088
4980
  .opf-source-code .opf-source-decoration-added { background: color-mix(in srgb, var(--opf-ui-success) 20%, transparent); }
5089
4981
  .opf-source-code .opf-source-decoration-changed { background: color-mix(in srgb, var(--opf-ui-warn) 14%, transparent); }
5090
- .opf-source-code .cm-line.opf-source-flash { animation: opf-source-flash 1.2s ease-out; }
4982
+
5091
4983
  @keyframes opf-source-flash { from { background: color-mix(in srgb, var(--opf-ui-accent) 32%, transparent); } to { background: transparent; } }
5092
- @media (prefers-reduced-motion: reduce) { .opf-source-code .cm-line.opf-source-flash { animation: none; background: color-mix(in srgb, var(--opf-ui-accent) 20%, transparent); } }
5093
- .opf-source-code .cm-panels { background: var(--opf-ui-surface); color: var(--opf-ui-fg); border-color: var(--opf-ui-border); }
5094
- .opf-source-code .cm-tooltip { background: var(--opf-ui-surface); border: 1px solid var(--opf-ui-border); border-radius: var(--opf-ui-radius); color: var(--opf-ui-fg); }
5095
- .opf-source-code .cm-lintRange-error { background-image: none; text-decoration: underline wavy var(--opf-ui-danger); }
4984
+
4985
+
4986
+
4987
+
5096
4988
 
5097
4989
  /* Findings under the code */
5098
4990
  .opf-source-findings {
@@ -5230,16 +5122,14 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
5230
5122
  .opf-source-confirm-accept { font-weight: 600; }
5231
5123
 
5232
5124
  /* Fold gutter (src/source-folding.js) */
5233
- .opf-source-code .cm-foldGutter { width: 16px; }
5234
- .opf-source-code .cm-foldGutter .cm-gutterElement { display: flex; align-items: center; justify-content: center; padding: 0; cursor: pointer; }
5235
- .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); }
5236
- .opf-source-code .cm-foldGutter .opf-fold-marker.is-folded,
5237
- .opf-source-code:hover .cm-foldGutter .opf-fold-marker,
5238
- .opf-source-code .cm-foldGutter .opf-fold-marker:hover { opacity: 1; }
5239
- .opf-source-code .cm-foldGutter .opf-fold-marker:hover { color: var(--opf-ui-fg); }
5240
- .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; }
5241
- @media (prefers-reduced-motion: reduce) { .opf-source-code .cm-foldGutter .opf-fold-marker { transition: none; } }
5242
- @media (forced-colors: active) { .opf-source-code .cm-foldGutter .opf-fold-marker { opacity: 1; } }
5125
+
5126
+
5127
+
5128
+
5129
+
5130
+
5131
+
5132
+
5243
5133
 
5244
5134
  /* Outline panel (opt-in `outline` option) */
5245
5135
  .opf-source-body { display: flex; flex: 1; min-height: 0; min-width: 0; }
@@ -5258,16 +5148,13 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
5258
5148
 
5259
5149
  /* 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. */
5260
5150
  @media (forced-colors: active) {
5261
- .opf-source-code .cm-tooltip-autocomplete > ul > li[aria-selected],
5262
- .cm-tooltip .opf-fix[aria-selected="true"] { outline: 2px solid Highlight; outline-offset: -2px; forced-color-adjust: none; background: Canvas; color: CanvasText; }
5263
- .opf-source-code .cm-lintRange-error { text-decoration: underline wavy CanvasText; }
5264
- .opf-source-code .cm-lintRange-warning { text-decoration: underline dashed CanvasText; }
5265
- .opf-source-code .cm-lintRange-info { text-decoration: underline dotted CanvasText; }
5151
+
5152
+
5153
+
5154
+
5266
5155
  .opf-source-finding { border: 1px solid CanvasText; }
5267
5156
  }
5268
- @media (prefers-reduced-motion: reduce) {
5269
- .cm-tooltip, .cm-tooltip * { animation: none !important; transition: none !important; }
5270
- }
5157
+
5271
5158
 
5272
5159
  /* studio.css */
5273
5160
  /* CE-20: the Studio (`createOpfEditor(el, { layout: "studio" | "playground" })`). Only --opf-ui-* tokens. The arrangement follows data
@@ -7122,67 +7009,20 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
7122
7009
  }
7123
7010
 
7124
7011
  /* ---- CodeMirror's lists and panels (completions, problems, quick fixes, search) ---------------------------------------------- */
7125
- :root :is(.opf-ui, .opf-source, .opf-json, .opf-code) .cm-tooltip.cm-tooltip-autocomplete > ul > li {
7126
- display: flex;
7127
- align-items: center;
7128
- gap: 4px;
7129
- min-height: 44px;
7130
- padding: 2px 12px;
7131
- }
7132
- :root :is(.opf-ui, .opf-source, .opf-json, .opf-code) .cm-tooltip-autocomplete .cm-completionLabel {
7133
- min-width: 0;
7134
- overflow: hidden;
7135
- text-overflow: ellipsis;
7136
- }
7137
- .cm-tooltip .opf-fix {
7138
- display: flex;
7139
- align-items: center;
7140
- min-height: 44px;
7141
- padding: 3px 12px;
7142
- }
7143
- .cm-tooltip-lint .cm-diagnosticAction {
7144
- min-height: 44px;
7145
- padding: 0 14px;
7146
- }
7147
- :root .cm-panels .cm-panel.cm-search :is(input, button, label) {
7148
- min-height: 44px;
7149
- }
7150
- :root .cm-panels .cm-panel.cm-search :is(input[type="text"], input:not([type])) {
7151
- font-size: 16px;
7152
- }
7012
+
7013
+
7014
+
7015
+
7016
+
7017
+
7153
7018
  /* The find and replace fields take a row of their own each, the buttons and options wrap beneath, the close button stays in the corner. */
7154
- :root .cm-panels .cm-panel.cm-search {
7155
- display: flex;
7156
- flex-wrap: wrap;
7157
- align-items: center;
7158
- gap: 6px;
7159
- padding: 6px 52px 6px 8px;
7160
- }
7161
- :root .cm-panels .cm-panel.cm-search br {
7162
- display: none;
7163
- }
7164
- :root .cm-panels .cm-panel.cm-search input[type="text"],
7165
- :root .cm-panels .cm-panel.cm-search input:not([type]) {
7166
- flex: 1 1 100%;
7167
- min-width: 0;
7168
- margin: 0;
7169
- }
7170
- :root .cm-panels .cm-panel.cm-search button,
7171
- :root .cm-panels .cm-panel.cm-search label {
7172
- margin: 0;
7173
- }
7174
- :root .cm-panels .cm-panel.cm-search label {
7175
- display: inline-flex;
7176
- align-items: center;
7177
- gap: 6px;
7178
- }
7179
- :root .cm-panels .cm-panel.cm-search button {
7180
- min-width: 44px;
7181
- }
7182
- :root .cm-panels .cm-panel.cm-search button[name="close"] {
7183
- top: 0;
7184
- right: 4px;
7185
- }
7019
+
7020
+
7021
+
7022
+
7023
+
7024
+
7025
+
7186
7026
 
7187
7027
  /* ---- menus, dialogs and the compact controls of the panels --------------------------------------------------------------------- */
7188
7028
  .opf-export .opf-export-close {
@@ -7532,6 +7372,11 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
7532
7372
  gap: 4px;
7533
7373
  padding: 0 12px 8px;
7534
7374
  }
7375
+ /* CE-30: the slide row is no wider than the editor; the thumbnail strip scrolls inside it (a narrow box, a phone). */
7376
+ .opf-ws-dock > .opf-ws-navigator,
7377
+ .opf-ws-dock > .opf-ws-thumbnails {
7378
+ max-width: 100%;
7379
+ }
7535
7380
  .opf-ws-status {
7536
7381
  flex: none;
7537
7382
  margin: 0;
@@ -7619,27 +7464,29 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
7619
7464
  flex: 1 1 0;
7620
7465
  min-height: 0;
7621
7466
  }
7622
- /* A phone: the parts stack, the source above the slide, the side panel below. */
7467
+ /* A phone: the parts stack, the source above the slide, the side panel below. CE-30: only in the layouts that have several parts to stack;
7468
+ `embed`, `viewer` and `canvas` are the slide alone and fit it to their box at any width (a stage of 40vh made a short embed overflow).
7469
+ `:where()` keeps the specificity of the plain class. */
7623
7470
  @media (max-width: 760px) {
7624
- .opf-ws-body {
7471
+ :where(.opf-workspace:not([data-opf-layout="embed"], [data-opf-layout="viewer"], [data-opf-layout="canvas"])) .opf-ws-body {
7625
7472
  flex-direction: column;
7626
7473
  overflow: auto;
7627
7474
  }
7628
- .opf-ws-source {
7475
+ :where(.opf-workspace:not([data-opf-layout="embed"], [data-opf-layout="viewer"], [data-opf-layout="canvas"])) .opf-ws-source {
7629
7476
  flex: none;
7630
7477
  min-width: 0;
7631
7478
  height: 40vh;
7632
7479
  border-right: 0;
7633
7480
  border-bottom: 1px solid var(--opf-ui-border);
7634
7481
  }
7635
- .opf-ws-main {
7482
+ :where(.opf-workspace:not([data-opf-layout="embed"], [data-opf-layout="viewer"], [data-opf-layout="canvas"])) .opf-ws-main {
7636
7483
  flex: none;
7637
7484
  }
7638
- .opf-ws-stage {
7485
+ :where(.opf-workspace:not([data-opf-layout="embed"], [data-opf-layout="viewer"], [data-opf-layout="canvas"])) .opf-ws-stage {
7639
7486
  flex: none;
7640
7487
  min-height: 40vh;
7641
7488
  }
7642
- .opf-ws-side {
7489
+ :where(.opf-workspace:not([data-opf-layout="embed"], [data-opf-layout="viewer"], [data-opf-layout="canvas"])) .opf-ws-side {
7643
7490
  flex: none;
7644
7491
  border-left: 0;
7645
7492
  border-top: 1px solid var(--opf-ui-border);
@@ -7651,3 +7498,285 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
7651
7498
  }
7652
7499
  }
7653
7500
  }
7501
+
7502
+ /* Outside the layer: the rules for CodeMirror's elements, which CodeMirror's own unlayered theme would otherwise beat. */
7503
+ :is(.opf-ui, .opf-json, .opf-code) .cm-editor {
7504
+ font: 12.5px/1.65 var(--opf-ui-mono);
7505
+ color: var(--opf-ui-fg);
7506
+ background: var(--opf-ui-code-bg);
7507
+ border: 1px solid var(--opf-ui-code-border);
7508
+ border-radius: var(--opf-ui-radius-md);
7509
+ }
7510
+ :is(.opf-ui, .opf-json, .opf-code) .cm-editor.cm-focused {
7511
+ outline: 2px solid var(--opf-ui-ring);
7512
+ outline-offset: 2px;
7513
+ }
7514
+ :is(.opf-ui, .opf-json, .opf-code) .cm-scroller,
7515
+ :is(.opf-ui, .opf-json, .opf-code) .cm-content {
7516
+ font-family: var(--opf-ui-mono);
7517
+ }
7518
+ :is(.opf-ui, .opf-json, .opf-code) .cm-content {
7519
+ caret-color: var(--opf-ui-fg);
7520
+ }
7521
+ :is(.opf-ui, .opf-json, .opf-code) .cm-cursor,
7522
+ :is(.opf-ui, .opf-json, .opf-code) .cm-dropCursor {
7523
+ border-left-color: var(--opf-ui-fg);
7524
+ }
7525
+ :is(.opf-ui, .opf-json, .opf-code) .cm-gutters {
7526
+ color: var(--opf-ui-muted-fg);
7527
+ background: var(--opf-ui-code-bg);
7528
+ border-right: 1px solid var(--opf-ui-code-border);
7529
+ }
7530
+ .cm-line.opf-hang-0 { --opf-hang: 0; }
7531
+ .cm-line.opf-hang-1 { --opf-hang: 1; }
7532
+ .cm-line.opf-hang-2 { --opf-hang: 2; }
7533
+ .cm-line.opf-hang-3 { --opf-hang: 3; }
7534
+ .cm-line.opf-hang-4 { --opf-hang: 4; }
7535
+ .cm-line.opf-hang-5 { --opf-hang: 5; }
7536
+ .cm-line.opf-hang-6 { --opf-hang: 6; }
7537
+ .cm-line.opf-hang-7 { --opf-hang: 7; }
7538
+ .cm-line.opf-hang-8 { --opf-hang: 8; }
7539
+ .cm-line.opf-hang-9 { --opf-hang: 9; }
7540
+ .cm-line.opf-hang-10 { --opf-hang: 10; }
7541
+ .cm-line.opf-hang-11 { --opf-hang: 11; }
7542
+ .cm-line.opf-hang-12 { --opf-hang: 12; }
7543
+ .cm-line.opf-hang-13 { --opf-hang: 13; }
7544
+ .cm-line.opf-hang-14 { --opf-hang: 14; }
7545
+ .cm-line.opf-hang-15 { --opf-hang: 15; }
7546
+ .cm-line.opf-hang-16 { --opf-hang: 16; }
7547
+ .cm-line.opf-hang-17 { --opf-hang: 17; }
7548
+ .cm-line.opf-hang-18 { --opf-hang: 18; }
7549
+ .cm-line.opf-hang-19 { --opf-hang: 19; }
7550
+ .cm-line.opf-hang-20 { --opf-hang: 20; }
7551
+ .cm-line.opf-hang-21 { --opf-hang: 21; }
7552
+ .cm-line.opf-hang-22 { --opf-hang: 22; }
7553
+ .cm-line.opf-hang-23 { --opf-hang: 23; }
7554
+ .cm-line.opf-hang-24 { --opf-hang: 24; }
7555
+ .cm-line.opf-hang-25 { --opf-hang: 25; }
7556
+ .cm-line.opf-hang-26 { --opf-hang: 26; }
7557
+ .cm-line.opf-hang-27 { --opf-hang: 27; }
7558
+ .cm-line.opf-hang-28 { --opf-hang: 28; }
7559
+ .cm-line.opf-hang-29 { --opf-hang: 29; }
7560
+ .cm-line.opf-hang-30 { --opf-hang: 30; }
7561
+ .cm-line.opf-hang-31 { --opf-hang: 31; }
7562
+ .cm-line.opf-hang-32 { --opf-hang: 32; }
7563
+ :is(.opf-ui, .opf-json, .opf-code) .cm-line {
7564
+ padding-left: calc(var(--opf-line-start, 6px) + var(--opf-hang, 0) * 1ch);
7565
+ text-indent: calc(-1 * var(--opf-hang, 0) * 1ch);
7566
+ }
7567
+ :is(.opf-ui, .opf-json, .opf-code) .cm-line > * {
7568
+ text-indent: 0;
7569
+ }
7570
+ :is(.opf-ui, .opf-json, .opf-code) .cm-activeLine,
7571
+ :is(.opf-ui, .opf-json, .opf-code) .cm-line.is-hover {
7572
+ background: color-mix(in srgb, var(--opf-ui-accent) 8%, transparent);
7573
+ }
7574
+ :is(.opf-ui, .opf-json, .opf-code) .cm-activeLineGutter {
7575
+ color: var(--opf-ui-fg);
7576
+ background: color-mix(in srgb, var(--opf-ui-accent) 8%, transparent);
7577
+ }
7578
+ :is(.opf-ui, .opf-json, .opf-code) .cm-editor .cm-selectionLayer .cm-selectionBackground,
7579
+ :is(.opf-ui, .opf-json, .opf-code) .cm-editor.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground {
7580
+ background: color-mix(in srgb, var(--opf-ui-accent) 16%, transparent);
7581
+ }
7582
+ :is(.opf-ui, .opf-json, .opf-code) .cm-selectionMatch {
7583
+ background: color-mix(in srgb, var(--opf-ui-warn) 16%, transparent);
7584
+ box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--opf-ui-warn) 70%, transparent);
7585
+ }
7586
+ :is(.opf-ui, .opf-json, .opf-code) .cm-matchingBracket {
7587
+ background: color-mix(in srgb, var(--opf-ui-accent) 16%, transparent);
7588
+ outline: 1px solid var(--opf-ui-accent);
7589
+ }
7590
+ :is(.opf-ui, .opf-json, .opf-code) .cm-panels {
7591
+ color: var(--opf-ui-fg);
7592
+ background: var(--opf-ui-surface);
7593
+ border-color: var(--opf-ui-border);
7594
+ font-family: var(--opf-ui-font);
7595
+ }
7596
+ :is(.opf-ui, .opf-json, .opf-code) .cm-panels input,
7597
+ :is(.opf-ui, .opf-json, .opf-code) .cm-panels button {
7598
+ font: 12px var(--opf-ui-font);
7599
+ color: var(--opf-ui-fg);
7600
+ background: var(--opf-ui-surface);
7601
+ border: 1px solid var(--opf-ui-border);
7602
+ border-radius: var(--opf-ui-radius-sm);
7603
+ }
7604
+ :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip {
7605
+ color: var(--opf-ui-fg);
7606
+ background: var(--opf-ui-surface);
7607
+ border: 1px solid var(--opf-ui-border);
7608
+ border-radius: var(--opf-ui-radius-md);
7609
+ box-shadow: var(--opf-ui-shadow-pop);
7610
+ }
7611
+ :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip.cm-tooltip-autocomplete > ul {
7612
+ font-family: var(--opf-ui-mono);
7613
+ font-size: 12px;
7614
+ }
7615
+ :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip.cm-tooltip-autocomplete > ul > li {
7616
+ padding: 2px 8px;
7617
+ color: var(--opf-ui-fg);
7618
+ }
7619
+ :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected] {
7620
+ color: var(--opf-ui-accent-fg);
7621
+ background: var(--opf-ui-accent);
7622
+ }
7623
+ :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip-autocomplete .cm-completionDetail {
7624
+ margin-left: 12px;
7625
+ font-family: var(--opf-ui-font);
7626
+ font-style: normal;
7627
+ color: var(--opf-ui-muted-fg);
7628
+ }
7629
+ :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip-autocomplete > ul > li[aria-selected] .cm-completionDetail {
7630
+ color: inherit;
7631
+ opacity: 0.85;
7632
+ }
7633
+ :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip-autocomplete > .opf-completion-footer {
7634
+ padding: 4px 8px;
7635
+ font: 12px/1.4 var(--opf-ui-font);
7636
+ color: var(--opf-ui-muted-fg);
7637
+ border-top: 1px solid var(--opf-ui-border);
7638
+ }
7639
+ :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip-autocomplete .cm-completionMatchedText {
7640
+ font-weight: 600;
7641
+ text-decoration: none;
7642
+ }
7643
+ :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip .cm-completionInfo {
7644
+ max-width: 280px;
7645
+ padding: 6px 8px;
7646
+ font: 12px/1.45 var(--opf-ui-font);
7647
+ white-space: pre-wrap;
7648
+ color: var(--opf-ui-fg);
7649
+ background: var(--opf-ui-surface);
7650
+ border: 1px solid var(--opf-ui-border);
7651
+ border-radius: var(--opf-ui-radius-md);
7652
+ }
7653
+ :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip .opf-doc,
7654
+ .opf-source-code .cm-tooltip .opf-doc {
7655
+ display: flex;
7656
+ flex-direction: column;
7657
+ gap: 4px;
7658
+ max-width: 340px;
7659
+ padding: 2px 0;
7660
+ font: 12px/1.45 var(--opf-ui-font);
7661
+ white-space: normal;
7662
+ }
7663
+ .cm-tooltip-lint .opf-problem { display: flex; flex-direction: column; gap: 2px; max-width: 360px; white-space: normal; }
7664
+ .cm-tooltip-lint .cm-diagnosticAction { margin: 4px 6px 0 0; padding: 1px 8px; border: 1px solid var(--opf-ui-border); border-radius: var(--opf-ui-radius-sm); background: var(--opf-ui-muted); color: var(--opf-ui-fg); font: 12px var(--opf-ui-font); cursor: pointer; }
7665
+ .cm-tooltip-lint .cm-diagnosticSource { color: var(--opf-ui-muted-fg); }
7666
+ .cm-tooltip .opf-fixes { display: flex; flex-direction: column; min-width: 180px; padding: 3px; font: 12px var(--opf-ui-font); }
7667
+ :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip .cm-completionInfo { white-space: normal; }
7668
+ .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; }
7669
+ @media (prefers-reduced-motion: reduce) {
7670
+ .cm-tooltip.opf-hint, .cm-tooltip.opf-hint * { animation: none !important; transition: none !important; }
7671
+ }
7672
+ .cm-editor.opf-docs-off .cm-completionInfo { display: none; }
7673
+ :is(.opf-ui, .opf-json, .opf-code) .cm-diagnostic-error {
7674
+ border-left-color: var(--opf-ui-danger);
7675
+ }
7676
+ :is(.opf-ui, .opf-json, .opf-code) .cm-diagnostic-warning {
7677
+ border-left-color: var(--opf-ui-warn);
7678
+ }
7679
+ .opf-source-code > .cm-editor { flex: 1; min-width: 0; height: 100%; background: transparent; color: var(--opf-ui-fg); border: 0; border-radius: 0; }
7680
+ .opf-source-code .cm-editor.cm-focused { outline: none; }
7681
+ .opf-source-code .cm-scroller { overflow: auto; font: 400 13.5px/22px var(--opf-ui-mono); }
7682
+ .opf-source[data-density="touch"] .opf-source-code .cm-scroller { font-size: 13px; line-height: 21px; }
7683
+ .opf-source-code .cm-content { padding: 16px 16px 16px 4px; caret-color: var(--opf-ui-fg); }
7684
+ .opf-source-code .cm-line { --opf-line-start: 2px; padding-top: 0; padding-right: 2px; padding-bottom: 0; }
7685
+ .opf-source-code .cm-gutters { border: 0; background: transparent; color: var(--opf-ui-muted-fg); }
7686
+ .opf-source-code .cm-lineNumbers .cm-gutterElement { min-width: 30px; padding: 0 12px 0 0; text-align: right; font-size: 11px; }
7687
+ .opf-source-code .cm-activeLine { background: transparent; }
7688
+ .opf-source-code .cm-cursor { border-left-color: var(--opf-ui-fg); }
7689
+ .opf-source-code .cm-line.is-hover { background: color-mix(in srgb, var(--opf-ui-accent) 6%, transparent); }
7690
+ .opf-source-code .cm-line.opf-source-flash { animation: opf-source-flash 1.2s ease-out; }
7691
+ @media (prefers-reduced-motion: reduce) {
7692
+ .opf-source-code .cm-line.opf-source-flash { animation: none; background: color-mix(in srgb, var(--opf-ui-accent) 20%, transparent); }
7693
+ }
7694
+ .opf-source-code .cm-panels { background: var(--opf-ui-surface); color: var(--opf-ui-fg); border-color: var(--opf-ui-border); }
7695
+ .opf-source-code .cm-tooltip { background: var(--opf-ui-surface); border: 1px solid var(--opf-ui-border); border-radius: var(--opf-ui-radius); color: var(--opf-ui-fg); }
7696
+ .opf-source-code .cm-lintRange-error { background-image: none; text-decoration: underline wavy var(--opf-ui-danger); }
7697
+ .opf-source-code .cm-foldGutter { width: 16px; }
7698
+ .opf-source-code .cm-foldGutter .cm-gutterElement { display: flex; align-items: center; justify-content: center; padding: 0; cursor: pointer; }
7699
+ .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); }
7700
+ .opf-source-code .cm-foldGutter .opf-fold-marker.is-folded,
7701
+ .opf-source-code:hover .cm-foldGutter .opf-fold-marker,
7702
+ .opf-source-code .cm-foldGutter .opf-fold-marker:hover { opacity: 1; }
7703
+ .opf-source-code .cm-foldGutter .opf-fold-marker:hover { color: var(--opf-ui-fg); }
7704
+ .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; }
7705
+ @media (prefers-reduced-motion: reduce) {
7706
+ .opf-source-code .cm-foldGutter .opf-fold-marker { transition: none; }
7707
+ }
7708
+ @media (forced-colors: active) {
7709
+ .opf-source-code .cm-foldGutter .opf-fold-marker { opacity: 1; }
7710
+ }
7711
+ @media (forced-colors: active) {
7712
+ .opf-source-code .cm-tooltip-autocomplete > ul > li[aria-selected],
7713
+ .cm-tooltip .opf-fix[aria-selected="true"] { outline: 2px solid Highlight; outline-offset: -2px; forced-color-adjust: none; background: Canvas; color: CanvasText; }
7714
+ .opf-source-code .cm-lintRange-error { text-decoration: underline wavy CanvasText; }
7715
+ .opf-source-code .cm-lintRange-warning { text-decoration: underline dashed CanvasText; }
7716
+ .opf-source-code .cm-lintRange-info { text-decoration: underline dotted CanvasText; }
7717
+ }
7718
+ @media (prefers-reduced-motion: reduce) {
7719
+ .cm-tooltip, .cm-tooltip * { animation: none !important; transition: none !important; }
7720
+ }
7721
+ @media (pointer: coarse) {
7722
+ :root :is(.opf-ui, .opf-source, .opf-json, .opf-code) .cm-tooltip.cm-tooltip-autocomplete > ul > li {
7723
+ display: flex;
7724
+ align-items: center;
7725
+ gap: 4px;
7726
+ min-height: 44px;
7727
+ padding: 2px 12px;
7728
+ }
7729
+ :root :is(.opf-ui, .opf-source, .opf-json, .opf-code) .cm-tooltip-autocomplete .cm-completionLabel {
7730
+ min-width: 0;
7731
+ overflow: hidden;
7732
+ text-overflow: ellipsis;
7733
+ }
7734
+ .cm-tooltip .opf-fix {
7735
+ display: flex;
7736
+ align-items: center;
7737
+ min-height: 44px;
7738
+ padding: 3px 12px;
7739
+ }
7740
+ .cm-tooltip-lint .cm-diagnosticAction {
7741
+ min-height: 44px;
7742
+ padding: 0 14px;
7743
+ }
7744
+ :root .cm-panels .cm-panel.cm-search :is(input, button, label) {
7745
+ min-height: 44px;
7746
+ }
7747
+ :root .cm-panels .cm-panel.cm-search :is(input[type="text"], input:not([type])) {
7748
+ font-size: 16px;
7749
+ }
7750
+ :root .cm-panels .cm-panel.cm-search {
7751
+ display: flex;
7752
+ flex-wrap: wrap;
7753
+ align-items: center;
7754
+ gap: 6px;
7755
+ padding: 6px 52px 6px 8px;
7756
+ }
7757
+ :root .cm-panels .cm-panel.cm-search br {
7758
+ display: none;
7759
+ }
7760
+ :root .cm-panels .cm-panel.cm-search input[type="text"],
7761
+ :root .cm-panels .cm-panel.cm-search input:not([type]) {
7762
+ flex: 1 1 100%;
7763
+ min-width: 0;
7764
+ margin: 0;
7765
+ }
7766
+ :root .cm-panels .cm-panel.cm-search button,
7767
+ :root .cm-panels .cm-panel.cm-search label {
7768
+ margin: 0;
7769
+ }
7770
+ :root .cm-panels .cm-panel.cm-search label {
7771
+ display: inline-flex;
7772
+ align-items: center;
7773
+ gap: 6px;
7774
+ }
7775
+ :root .cm-panels .cm-panel.cm-search button {
7776
+ min-width: 44px;
7777
+ }
7778
+ :root .cm-panels .cm-panel.cm-search button[name="close"] {
7779
+ top: 0;
7780
+ right: 4px;
7781
+ }
7782
+ }
package/dist/toolbar.d.ts CHANGED
@@ -26,11 +26,21 @@ export type ToolbarItemName =
26
26
  | "spacer"
27
27
  | "|";
28
28
 
29
- /** An item by name, or with its own priority (higher stays visible longer) and whether it may move into the more menu. */
30
- export type ToolbarItemSpec = ToolbarItemName | { id: ToolbarItemName; priority?: number; collapsible?: boolean };
29
+ /** An item name, or the name of a host's own item. */
30
+ export type ToolbarItemId = ToolbarItemName | (string & {});
31
+
32
+ /**
33
+ * An item by name, or with its own priority (higher stays visible longer) and whether it may move into the more menu. A host's own item is
34
+ * `{ id, priority }` with a name that is not one of the above (lowercase letters, digits and dashes): its slot starts empty, the host fills it
35
+ * (`slot(id)`) and registers what it offers in the more menu with `mount(id, part)`. A bare unknown name throws, so a misspelt item is caught.
36
+ */
37
+ export type ToolbarItemSpec =
38
+ | ToolbarItemName
39
+ | { id: ToolbarItemName; priority?: number; collapsible?: boolean }
40
+ | { id: string; priority: number; collapsible?: boolean };
31
41
 
32
42
  export interface ToolbarItem {
33
- id: ToolbarItemName;
43
+ id: ToolbarItemId;
34
44
  priority: number;
35
45
  collapsible: boolean;
36
46
  }
@@ -38,7 +48,7 @@ export interface ToolbarItem {
38
48
  /** The defaults of every item: `more` and `title` stay (the title shrinks instead), `spacer` and `|` are layout. */
39
49
  export declare const TOOLBAR_ITEMS: Readonly<Record<ToolbarItemName, Readonly<{ priority: number; collapsible: boolean }>>>;
40
50
 
41
- /** The items with their priorities. Throws on a name that is not an item, and on a named item given twice (`spacer` and `|` may repeat). */
51
+ /** The items with their priorities. Throws on a name that is not an item (or a host item), and on a named item given twice (`spacer` and `|` may repeat). */
42
52
  export declare function normalizeToolbarItems(items: readonly ToolbarItemSpec[]): ToolbarItem[];
43
53
 
44
54
  /** One entry of the more menu. `pressed` makes it a checkbox item (`aria-checked`); a `disabled` item is listed but does not run. */
@@ -65,7 +75,7 @@ export interface ToolbarOptions {
65
75
  /** Replace the ⋯ glyph: `more` (`.opf-tb-more-icon`). See `OpfIcon`. */
66
76
  icons?: OpfIcons<"more">;
67
77
  /** The set of collapsed items changed. */
68
- onCollapse?(detail: { collapsed: ToolbarItemName[] }): void;
78
+ onCollapse?(detail: { collapsed: ToolbarItemId[] }): void;
69
79
  /** A part's `menuItems()` or an item's `run()` threw. */
70
80
  onError?(error: unknown): void;
71
81
  }
@@ -74,20 +84,20 @@ export interface Toolbar {
74
84
  /** The row (`.opf-tb`), appended to the container. */
75
85
  readonly element: HTMLElement;
76
86
  readonly items: ToolbarItem[];
77
- /** One element per named item (`data-opf-toolbar-item`), in bar order. Mount a part in it, then `mount(name, part)`. */
78
- readonly slots: Partial<Record<ToolbarItemName, HTMLElement>>;
79
- slot(name: ToolbarItemName): HTMLElement | undefined;
87
+ /** One element per named item (`data-opf-toolbar-item`), in bar order, host items included. Mount a part in it, then `mount(name, part)`. */
88
+ readonly slots: Partial<Record<ToolbarItemId, HTMLElement>>;
89
+ slot(name: ToolbarItemId): HTMLElement | undefined;
80
90
  /** The items in the more menu now, in bar order. */
81
- readonly collapsed: ToolbarItemName[];
91
+ readonly collapsed: ToolbarItemId[];
82
92
  readonly menuOpen: boolean;
83
93
  /** Change an item's priority or whether it collapses; false for an item that is not in the bar (or `more`, `spacer`, `|`). */
84
- setItem(name: ToolbarItemName, change: { priority?: number; collapsible?: boolean }): boolean;
94
+ setItem(name: ToolbarItemId, change: { priority?: number; collapsible?: boolean }): boolean;
85
95
  /** Register the part mounted in a slot, for its menu items. Returns the function that unregisters it. */
86
- mount(name: ToolbarItemName, part: ToolbarPart): () => void;
96
+ mount(name: ToolbarItemId, part: ToolbarPart): () => void;
87
97
  /** What the more menu would list now, one group per collapsed part. */
88
- menuItems(): { id: ToolbarItemName; items: ToolbarMenuItem[] }[];
98
+ menuItems(): { id: ToolbarItemId; items: ToolbarMenuItem[] }[];
89
99
  /** Fit the row now (it also fits itself on the next frame after a resize). Returns the collapsed items. */
90
- update(): ToolbarItemName[];
100
+ update(): ToolbarItemId[];
91
101
  /** Open the more menu and focus its first (or last) item. False when there is nothing to list. */
92
102
  openMenu(which?: "first" | "last"): boolean;
93
103
  closeMenu(options?: { restoreFocus?: boolean }): void;
package/dist/toolbar.js CHANGED
@@ -31,10 +31,16 @@ export const TOOLBAR_ITEMS = Object.freeze({
31
31
  });
32
32
  const REPEATABLE = new Set(["spacer", "|"]);
33
33
  const LABELS = Object.freeze({ more: "More", menu: "More actions" });
34
+ // A host's own item: `{ id, priority }` with a name of its own (lowercase letters, digits and dashes). Its slot starts empty; the host fills it and
35
+ // registers what it offers in ⋯ with `mount(id, part)`, like any item. A bare unknown name still throws, so a misspelt item is caught.
36
+ const HOST_ITEM = /^[a-z][a-z0-9-]*$/;
37
+
38
+ const isHostItem = (id) => typeof id === "string" && !Object.hasOwn(TOOLBAR_ITEMS, id) && HOST_ITEM.test(id);
34
39
 
35
40
  /**
36
41
  * Turn the `items` option (names, or `{ id, priority, collapsible }`) into the list the toolbar builds, with each item's priority and whether
37
- * it collapses. Throws on a name that is not an item and on a named item given twice (`spacer` and `|` may repeat).
42
+ * it collapses. Throws on a name that is not an item (a host's own item is `{ id, priority }` with a name of its own) and on a named item given
43
+ * twice (`spacer` and `|` may repeat).
38
44
  */
39
45
  export function normalizeToolbarItems(items) {
40
46
  if (!Array.isArray(items)) throw new TypeError("toolbar items must be an array of item names or { id, priority } objects.");
@@ -42,8 +48,9 @@ export function normalizeToolbarItems(items) {
42
48
  return items.map((item, index) => {
43
49
  const spec = typeof item === "string" ? { id: item } : item;
44
50
  if (!spec || typeof spec !== "object" || typeof spec.id !== "string") throw new TypeError(`toolbar item ${index} must be an item name or { id, priority }.`);
45
- const known = TOOLBAR_ITEMS[spec.id];
46
- if (!known) throw new RangeError(`"${spec.id}" is not a toolbar item (${Object.keys(TOOLBAR_ITEMS).join(", ")}).`);
51
+ const host = typeof item === "object" && spec.priority !== undefined && isHostItem(spec.id);
52
+ const known = host ? { priority: spec.priority, collapsible: true } : Object.hasOwn(TOOLBAR_ITEMS, spec.id) ? TOOLBAR_ITEMS[spec.id] : undefined;
53
+ if (!known) throw new RangeError(`"${spec.id}" is not a toolbar item (${Object.keys(TOOLBAR_ITEMS).join(", ")}; an item of your own is { id, priority }).`);
47
54
  if (!REPEATABLE.has(spec.id)) {
48
55
  if (seen.has(spec.id)) throw new RangeError(`the toolbar item "${spec.id}" is given twice.`);
49
56
  seen.add(spec.id);
@@ -274,7 +281,8 @@ export function createToolbar(container, options = {}) {
274
281
  }
275
282
  const before = collapsed.join(",");
276
283
  bar.setAttribute("data-measuring", "");
277
- for (const entry of entries) if (entry.collapsible) setCollapsed(entry, false);
284
+ // Every item shows again first, one made non-collapsible (`setItem`) while it was in ⋯ included.
285
+ for (const entry of entries) setCollapsed(entry, false);
278
286
  if (moreEntry) moreEntry.element.hidden = true;
279
287
  syncDividers();
280
288
  const next = [];
@@ -274,6 +274,15 @@ export interface OpfEditorSourceOptions {
274
274
  onDownload?(file: SourceFile): void | Promise<void>;
275
275
  }
276
276
 
277
+ /** CE-30: `variables` as an object: the canvas draws `{{variables}}` resolved, as with `"resolved"`. */
278
+ export interface OpfEditorVariablesOptions {
279
+ /**
280
+ * Called when a press on the slide lands on text that has no field of its own (a value the deck generates), in place of focusing the editor's
281
+ * source pane: focus your own code editor. A throw is reported as an `error` event.
282
+ */
283
+ focusSource?(): void;
284
+ }
285
+
277
286
  export interface OpfEditorOptions {
278
287
  /** The document to edit: an OPF object, or its JSON text (then the pane keeps its spelling). Creates the session. */
279
288
  document?: unknown;
@@ -289,8 +298,12 @@ export interface OpfEditorOptions {
289
298
  imageResolver?: ToSvgOptions["imageResolver"];
290
299
  /** Stores an uploaded image yourself and returns its reference (see `@openpresentation/opf-editor/assets`). Used by the design controls. */
291
300
  onAddAsset?: DesignControlsOptions["onAddAsset"];
292
- /** `"authored"` (default): the canvas shows `{{variables}}` as written. `"resolved"`: filled in, except in a template deck; a press on filled-in text opens its properties form. */
293
- variables?: "authored" | "resolved";
301
+ /**
302
+ * `"authored"` (default): the canvas shows `{{variables}}` as written. `"resolved"`: filled in, except in a template deck; a press on filled-in text
303
+ * opens its properties form, and a press on text without a field of its own (a generated value) focuses the source pane. CE-30: an object is
304
+ * `"resolved"` too, and its `focusSource` is called instead of focusing the source pane: for a host with its own code editor beside the slide.
305
+ */
306
+ variables?: "authored" | "resolved" | OpfEditorVariablesOptions;
294
307
 
295
308
  /**
296
309
  * `"full"` (default), `"canvas"`, `"embed"` (CE-28) or `"source"`: the editor builds the arrangement (ignored when `slots` names a part; slots for the
package/dist/workspace.js CHANGED
@@ -164,10 +164,11 @@ function resolveParts(options, layout, slots, asked = []) {
164
164
  }
165
165
  // The slide is the editor; a source-only layout has none.
166
166
  parts.canvas = layout === "source" ? false : panels.canvas !== false;
167
- // CE-28 wave 2: an item the toolbar lists brings the part it shows: Export in the viewer, and Review (run without its panel where the layout
168
- // has none, so the pill has counts) wherever it is listed.
167
+ // CE-28 wave 2: an item the toolbar lists brings the part it shows: Export in the viewer, Present (CE-30: also where the host arranges the
168
+ // parts with `slots`), and Review (run without its panel where the layout has none, so the pill has counts) wherever it is listed.
169
169
  const listed = (id) => asked.some((item) => item.id === id) || Boolean(slots[itemSlot(id)]);
170
170
  if (panels.export === undefined && listed("export")) parts.export = true;
171
+ if (panels.present === undefined && listed("present")) parts.present = true;
171
172
  if (panels.review === undefined && listed("review") && !parts.review) {
172
173
  parts.review = true;
173
174
  // A layout without a Review panel gets none: the review runs headless for the pill.
@@ -203,7 +204,9 @@ function resolveToolbar(options, layout, parts, views, given, items) {
203
204
  spacer: true,
204
205
  "|": true,
205
206
  };
206
- return items.filter((item) => !RESERVED_ITEMS.has(item.id) && available[item.id] && !(ITEM_SLOT_NAMES.has(item.id) && given[itemSlot(item.id)]));
207
+ // A host's own item (`{ id, priority }` with a name of its own) is always built: its slot is the host's to fill (`editor.toolbar.slot(id)`).
208
+ const host = (id) => !Object.hasOwn(TOOLBAR_ITEMS, id);
209
+ return items.filter((item) => !RESERVED_ITEMS.has(item.id) && (host(item.id) || available[item.id]) && !(ITEM_SLOT_NAMES.has(item.id) && given[itemSlot(item.id)]));
207
210
  }
208
211
 
209
212
  /**
@@ -422,7 +425,10 @@ export function createOpfEditor(element, options = {}) {
422
425
  }
423
426
  let frameSize = { width: 0, height: 0 };
424
427
  const draftMemory = createSourceMemory();
425
- const variablesMode = options.variables === "resolved" ? "resolved" : "authored";
428
+ // CE-30: `variables: { focusSource }` is "resolved" with the host's own code editor: a press on text without a field of its own focuses that.
429
+ const variablesObject = options.variables !== null && typeof options.variables === "object" ? options.variables : undefined;
430
+ const variablesMode = options.variables === "resolved" || variablesObject ? "resolved" : "authored";
431
+ if (variablesObject && variablesObject.focusSource !== undefined && typeof variablesObject.focusSource !== "function") throw new TypeError("variables.focusSource must be a function.");
426
432
  const fitMode = studioLayout ? false : options.fit === undefined ? (layout === "slots" ? false : "contain") : options.fit;
427
433
 
428
434
  const say = (message) => {
@@ -944,7 +950,14 @@ export function createOpfEditor(element, options = {}) {
944
950
  cleanups.push(
945
951
  installCanvasVariableEditing(slots.canvas, ui, canvas, {
946
952
  resolved: () => !authoredTemplate,
947
- focusSource: () => pane?.editor?.api?.focus?.(),
953
+ focusSource: () => {
954
+ if (!variablesObject?.focusSource) return pane?.editor?.api?.focus?.();
955
+ try {
956
+ variablesObject.focusSource();
957
+ } catch (error) {
958
+ report(error);
959
+ }
960
+ },
948
961
  }),
949
962
  );
950
963
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-editor",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -348,12 +348,12 @@
348
348
  "@lezer/highlight": "^1.2.5",
349
349
  "jsonc-parser": "3.3.1",
350
350
  "yaml": "^2.9.1",
351
- "@openpresentation/opf": "^0.30.0"
351
+ "@openpresentation/opf": "^0.31.0"
352
352
  },
353
353
  "peerDependencies": {
354
354
  "react": ">=18",
355
355
  "react-dom": ">=18",
356
- "@openpresentation/opf-render": "^0.30.0"
356
+ "@openpresentation/opf-render": "^0.31.0"
357
357
  },
358
358
  "peerDependenciesMeta": {
359
359
  "@openpresentation/opf-render": {
@@ -422,9 +422,9 @@
422
422
  "react-dom": "19.3.1",
423
423
  "react-18": "npm:react@18.3.1",
424
424
  "react-dom-18": "npm:react-dom@18.3.1",
425
- "@openpresentation/gallery": "0.30.0",
426
- "@openpresentation/opf-pptx": "0.30.0",
427
- "@openpresentation/opf-render": "0.30.0"
425
+ "@openpresentation/opf-pptx": "0.31.0",
426
+ "@openpresentation/gallery": "0.31.0",
427
+ "@openpresentation/opf-render": "0.31.0"
428
428
  },
429
429
  "scripts": {
430
430
  "build": "node scripts/build.mjs",
@@ -479,5 +479,5 @@
479
479
  "test:workspace-react-browser": "node test/workspace-react-browser.mjs",
480
480
  "test:element-browser": "node test/element-browser.mjs"
481
481
  },
482
- "gitHead": "d82530c652b01fd6da204913c70636bfdf898642"
482
+ "gitHead": "d68561fad7b2b319cd7cdb8d7e75fefdd1dffa4c"
483
483
  }