@openleaf-editor/ui 0.1.0-beta.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/styles.js ADDED
@@ -0,0 +1,532 @@
1
+ /**
2
+ * Toolbar styles, and the two ways they can reach the page.
3
+ *
4
+ * ## Why this is harder than it should be
5
+ *
6
+ * OpenLeaf deliberately does not use Shadow DOM: the *content* area must
7
+ * inherit the host site's typography for editing to be WYSIWYG against their
8
+ * theme. The price is that these rules share a cascade with a stylesheet we
9
+ * have never seen -- Bootstrap, Tailwind preflight, a 2009 WordPress theme, a
10
+ * Drupal admin theme. So every property a host `button {}` rule or reset might
11
+ * touch is set explicitly below, even where the browser default would do.
12
+ *
13
+ * `all: unset` is NOT used. It looks like the obvious answer and it is a trap:
14
+ * it destroys the inheritance we want (font, colour) and wipes the default
15
+ * focus behaviour we are obliged to keep.
16
+ *
17
+ * No `!important` either. It wins today and makes the toolbar unthemeable
18
+ * tomorrow, which defeats the point of the custom-property API.
19
+ *
20
+ * ## Delivery and CSP
21
+ *
22
+ * Government and enterprise integrators -- the users who most need a free
23
+ * editor -- commonly run `style-src 'self'` with no `'unsafe-inline'`, which
24
+ * blocks an injected `<style>` element. So there are exactly two paths:
25
+ *
26
+ * 1. A constructable `CSSStyleSheet` added to `document.adoptedStyleSheets`.
27
+ * CSP gates resources *parsed as style*; a CSSOM object attached this way
28
+ * never passes through that gate, by design rather than by loophole.
29
+ * 2. The integrator links `@openleaf-editor/ui/openleaf.css` themselves and calls
30
+ * `markStylesExternal()`.
31
+ *
32
+ * There is deliberately NO `<style>` injection fallback. It reads as a safety
33
+ * net and is the opposite: it is blocked by exactly the strict-CSP setups that
34
+ * would need it, and it fails *silently* -- an unstyled toolbar with no signal
35
+ * an integrator can act on. A console warning naming the stylesheet is more
36
+ * useful than a mechanism that quietly does nothing.
37
+ */
38
+ /**
39
+ * The dark palette, as fallbacks for the same public tokens the light one uses.
40
+ *
41
+ * Written once and applied from three selectors below rather than pasted three
42
+ * times, because the failure mode of a pasted palette is one selector quietly
43
+ * missing a line -- and the line it is missing is a colour nobody looks at until
44
+ * it is the wrong one.
45
+ */
46
+ const DARK_TOKENS = `
47
+ --ol-text: var(--openleaf-color-text, #e6edf3);
48
+ --ol-text-muted: var(--openleaf-color-text-muted, #9198a1);
49
+ --ol-surface: var(--openleaf-color-surface, #0d1117);
50
+ --ol-surface-hover: var(--openleaf-color-surface-hover, #21262d);
51
+ --ol-surface-active: var(--openleaf-color-surface-active, #1f3a5f);
52
+ --ol-border: var(--openleaf-color-border, #3d444d);
53
+ --ol-accent: var(--openleaf-color-accent, #79c0ff);
54
+ --ol-focus: var(--openleaf-color-focus, #79c0ff);
55
+ `;
56
+ export const CSS = `
57
+ .ol-editor {
58
+ /* Two families, named before either is public API. A singular
59
+ \`--openleaf-font\` implies "the" font, and source view already has a
60
+ monospace surface -- bolting a mono token on beside a singular name later
61
+ is the awkward outcome worth avoiding now. \`--openleaf-font\` is still
62
+ honoured as a fallback. */
63
+ --ol-font: var(--openleaf-font-ui, var(--openleaf-font, system-ui, -apple-system, "Segoe UI", sans-serif));
64
+ --ol-font-mono: var(--openleaf-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
65
+ --ol-font-size: var(--openleaf-font-size, 14px);
66
+ --ol-radius: var(--openleaf-radius, 4px);
67
+ --ol-text: var(--openleaf-color-text, #1f2328);
68
+ --ol-text-muted: var(--openleaf-color-text-muted, #59636e);
69
+ --ol-surface: var(--openleaf-color-surface, #ffffff);
70
+ --ol-surface-hover: var(--openleaf-color-surface-hover, #f0f1f3);
71
+ --ol-surface-active: var(--openleaf-color-surface-active, #dbe9ff);
72
+ --ol-border: var(--openleaf-color-border, #d1d9e0);
73
+ --ol-accent: var(--openleaf-color-accent, #0550ae);
74
+ --ol-focus: var(--openleaf-color-focus, #0969da);
75
+ --ol-button-size: var(--openleaf-button-size, 32px);
76
+ --ol-icon-size: var(--openleaf-icon-size, 16px);
77
+ --ol-gap: var(--openleaf-gap, 2px);
78
+ /* A 2009 WordPress admin bar sits at z-index 99999 and Drupal's toolbar plays
79
+ similar games. Without a sanctioned escape hatch integrators fix a toolbar
80
+ rendered behind a sticky host header with !important -- the exact thing the
81
+ token API exists to prevent. */
82
+ --ol-z: var(--openleaf-z-index, 1);
83
+ /* Thickness and offset, not just colour: "you can change the colour but not
84
+ the thickness" is a poor answer to a Section 508 team citing WCAG 2.2
85
+ Focus Appearance. */
86
+ --ol-focus-width: var(--openleaf-focus-width, 2px);
87
+ --ol-focus-offset: var(--openleaf-focus-offset, 1px);
88
+
89
+ display: block;
90
+ color: var(--ol-text);
91
+ }
92
+
93
+ /* Three ways into the dark palette, and one way out of it.
94
+ \`data-ol-scheme\` is set from the applied skin's declared scheme, and a skin
95
+ that declares one outranks both the attribute and the system -- it has to,
96
+ because its tokens have already replaced the palette outright. Without the
97
+ \`:not()\` guards a light skin on a dark machine would take these fallbacks for
98
+ every token it did not itself set, which is a light surface carrying dark-mode
99
+ muted text. */
100
+ @media (prefers-color-scheme: dark) {
101
+ .ol-editor:not([data-ol-theme="light"]):not([data-ol-scheme]) {${DARK_TOKENS}}
102
+ }
103
+
104
+ .ol-editor[data-ol-theme="dark"]:not([data-ol-scheme]) {${DARK_TOKENS}}
105
+
106
+ .ol-editor[data-ol-scheme="dark"] {${DARK_TOKENS}}
107
+
108
+ /* Native widget chrome -- the popup a \`select\` opens, scrollbars, the caret,
109
+ form control backgrounds -- is painted by the browser from \`color-scheme\`,
110
+ not from any property we can hand an integrator. Left alone it follows the
111
+ page, which is right while the editor's own palette also follows the page and
112
+ wrong the moment either attribute pins the editor to one world. So it is set
113
+ exactly when the editor stops following along, and inherited from the host
114
+ otherwise. */
115
+ .ol-editor[data-ol-theme="light"] { color-scheme: light; }
116
+ .ol-editor[data-ol-theme="dark"] { color-scheme: dark; }
117
+ .ol-editor[data-ol-scheme="light"] { color-scheme: light; }
118
+ .ol-editor[data-ol-scheme="dark"] { color-scheme: dark; }
119
+
120
+ /* Wrapping, not scrolling and not an overflow menu. Both of those hide
121
+ controls -- one off-screen, one behind a click -- and a formatting control
122
+ the author cannot see is a control they do not have. */
123
+ .ol-editor .ol-toolbar {
124
+ display: flex;
125
+ flex-wrap: wrap;
126
+ align-items: center;
127
+ gap: var(--ol-gap);
128
+ box-sizing: border-box;
129
+ margin: 0;
130
+ padding: 4px;
131
+ border: 1px solid var(--ol-border);
132
+ border-bottom: 0;
133
+ border-radius: var(--ol-radius) var(--ol-radius) 0 0;
134
+ background: var(--ol-surface);
135
+ font: inherit;
136
+ position: relative;
137
+ z-index: var(--ol-z);
138
+ }
139
+
140
+ .ol-editor .ol-group {
141
+ display: flex;
142
+ flex-wrap: wrap;
143
+ align-items: center;
144
+ gap: var(--ol-gap);
145
+ }
146
+
147
+ /* The divider is a BORDER on the group, not a standalone flex item.
148
+ As its own element it could wrap onto the trailing edge of a row with nothing
149
+ after it -- a stranded line floating at the end of a row. Wrapping begins
150
+ around 690-740px, well inside ordinary desktop widths (a CMS sidebar, a
151
+ half-width split pane), so this was not a 360px-only edge case.
152
+ border-inline-start rather than border-left, so it flips under RTL. */
153
+ .ol-editor .ol-group + .ol-group {
154
+ border-inline-start: 1px solid var(--ol-border);
155
+ padding-inline-start: 5px;
156
+ margin-inline-start: 1px;
157
+ }
158
+
159
+ /* Every property a host reset or \`button {}\` rule is likely to touch is set
160
+ here explicitly. Specificity is (0,2,0) via the descendant selector, which
161
+ beats a bare element or single-class host rule without resorting to
162
+ !important. */
163
+ .ol-editor .ol-btn {
164
+ box-sizing: border-box;
165
+ display: inline-flex;
166
+ align-items: center;
167
+ justify-content: center;
168
+ flex: 0 0 auto;
169
+ width: var(--ol-button-size);
170
+ height: var(--ol-button-size);
171
+ min-width: var(--ol-button-size);
172
+ min-height: var(--ol-button-size);
173
+ max-width: none;
174
+ padding: 0;
175
+ margin: 0;
176
+ border: 1px solid transparent;
177
+ border-radius: var(--ol-radius);
178
+ background: transparent;
179
+ color: var(--ol-text);
180
+ font: inherit;
181
+ font-family: var(--ol-font);
182
+ font-size: var(--ol-font-size);
183
+ font-weight: 400;
184
+ line-height: 1;
185
+ letter-spacing: normal;
186
+ text-transform: none;
187
+ text-align: center;
188
+ text-decoration: none;
189
+ text-shadow: none;
190
+ box-shadow: none;
191
+ opacity: 1;
192
+ cursor: pointer;
193
+ appearance: none;
194
+ -webkit-appearance: none;
195
+ outline-offset: var(--ol-focus-offset);
196
+ -webkit-tap-highlight-color: transparent;
197
+ transition: background-color 120ms ease, border-color 120ms ease;
198
+ }
199
+
200
+ .ol-editor .ol-btn:hover {
201
+ background: var(--ol-surface-hover);
202
+ }
203
+
204
+ /* :focus-visible only, so a mouse click does not leave a ring behind, but the
205
+ ring is never removed for keyboard users. */
206
+ .ol-editor .ol-btn:focus-visible {
207
+ outline: var(--ol-focus-width) solid var(--ol-focus);
208
+ outline-offset: var(--ol-focus-offset);
209
+ }
210
+
211
+ /* Pressed state is distinguished from hover by THREE signals, not just colour:
212
+ a filled background, a visible border, and an inset shadow that reads as
213
+ physically depressed. Colour alone would fail for a colour-blind author and
214
+ would be invisible in forced-colours mode. */
215
+ .ol-editor .ol-btn[aria-pressed="true"] {
216
+ background: var(--ol-surface-active);
217
+ border-color: var(--ol-accent);
218
+ color: var(--ol-accent);
219
+ box-shadow: inset 0 1px 2px rgb(0 0 0 / 12%);
220
+ }
221
+
222
+ .ol-editor .ol-btn[aria-pressed="true"]:hover {
223
+ background: var(--ol-surface-active);
224
+ border-color: var(--ol-accent);
225
+ }
226
+
227
+ /* aria-disabled rather than the disabled attribute: a disabled button is
228
+ removed from the roving tabindex and cannot be reached or announced, so an
229
+ author using a screen reader cannot discover that the control exists. */
230
+ .ol-editor .ol-btn[aria-disabled="true"] {
231
+ opacity: 0.4;
232
+ cursor: default;
233
+ }
234
+
235
+ .ol-editor .ol-btn[aria-disabled="true"]:hover {
236
+ background: transparent;
237
+ }
238
+
239
+ .ol-editor .ol-icon {
240
+ width: var(--ol-icon-size);
241
+ height: var(--ol-icon-size);
242
+ display: block;
243
+ pointer-events: none;
244
+ flex: 0 0 auto;
245
+ }
246
+
247
+ /* Block-type select. Sized and coloured to sit level with the icon buttons so
248
+ it does not read as bolted on, but it stays a native <select> -- a custom
249
+ listbox is a large amount of ARIA that would then owe real screen reader
250
+ testing to be worth anything. */
251
+ .ol-editor .ol-select {
252
+ box-sizing: border-box;
253
+ height: var(--ol-button-size);
254
+ max-width: 11em;
255
+ padding: 0 22px 0 6px;
256
+ margin: 0;
257
+ border: 1px solid var(--ol-border);
258
+ border-radius: var(--ol-radius);
259
+ background-color: transparent;
260
+ background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
261
+ linear-gradient(135deg, currentColor 50%, transparent 50%);
262
+ background-position: right 10px center, right 6px center;
263
+ background-size: 4px 4px, 4px 4px;
264
+ background-repeat: no-repeat;
265
+ color: var(--ol-text);
266
+ font-family: var(--ol-font);
267
+ font-size: var(--ol-font-size);
268
+ font-weight: 400;
269
+ line-height: 1;
270
+ text-transform: none;
271
+ letter-spacing: normal;
272
+ box-shadow: none;
273
+ cursor: pointer;
274
+ appearance: none;
275
+ -webkit-appearance: none;
276
+ }
277
+
278
+ .ol-editor .ol-select:hover {
279
+ background-color: var(--ol-surface-hover);
280
+ }
281
+
282
+ .ol-editor .ol-select:focus-visible {
283
+ outline: var(--ol-focus-width) solid var(--ol-focus);
284
+ outline-offset: var(--ol-focus-offset);
285
+ }
286
+
287
+ /* The option list is painted by the OS, which does not inherit our tokens, so
288
+ give it explicit colours or dark mode shows black text on black. */
289
+ .ol-editor .ol-select option {
290
+ background: var(--ol-surface);
291
+ color: var(--ol-text);
292
+ }
293
+
294
+ .ol-editor .ol-content {
295
+ box-sizing: border-box;
296
+ border: 1px solid var(--ol-border);
297
+ border-radius: 0 0 var(--ol-radius) var(--ol-radius);
298
+ background: var(--ol-surface);
299
+ }
300
+
301
+ /* Deliberately minimal: the content area inherits the host's typography, which
302
+ is the entire reason this project does not use Shadow DOM. Only padding and
303
+ the focus ring are ours. */
304
+ .ol-editor .ol-content .ProseMirror {
305
+ padding: 12px;
306
+ min-height: 8rem;
307
+ outline: none;
308
+ }
309
+
310
+ .ol-editor .ol-content:focus-within {
311
+ outline: 2px solid var(--ol-focus);
312
+ outline-offset: -1px;
313
+ }
314
+
315
+ /* Tables.
316
+ These styles live in core, not in the opt-in table plugin, because table
317
+ NODES live in core: every deployment reads and renders tables even where
318
+ editing them is switched off. Unstyled tables would render as runs of
319
+ undelimited text. */
320
+ .ol-editor .ol-content .ProseMirror table {
321
+ border-collapse: collapse;
322
+ table-layout: fixed;
323
+ width: 100%;
324
+ margin: 0 0 1em;
325
+ overflow: hidden;
326
+ }
327
+
328
+ .ol-editor .ol-content .ProseMirror td,
329
+ .ol-editor .ol-content .ProseMirror th {
330
+ border: 1px solid var(--ol-border);
331
+ padding: 6px 8px;
332
+ vertical-align: top;
333
+ /* A zero-width cell cannot be clicked into, so it cannot be repaired. */
334
+ min-width: 2em;
335
+ position: relative;
336
+ }
337
+
338
+ .ol-editor .ol-content .ProseMirror th {
339
+ background: var(--ol-surface-hover);
340
+ font-weight: 600;
341
+ text-align: start;
342
+ }
343
+
344
+ /* prosemirror-tables marks cells in a rectangular selection with this class.
345
+ An ::after overlay rather than a background so it composes with a cell that
346
+ already has one, and so a header cell still reads as a header. */
347
+ .ol-editor .ol-content .ProseMirror .selectedCell::after {
348
+ content: "";
349
+ position: absolute;
350
+ inset: 0;
351
+ background: var(--ol-surface-active);
352
+ opacity: 0.4;
353
+ pointer-events: none;
354
+ }
355
+
356
+ /* The column resize handle, drawn by prosemirror-tables' columnResizing. */
357
+ .ol-editor .ol-content .ProseMirror .column-resize-handle {
358
+ position: absolute;
359
+ right: -2px;
360
+ top: 0;
361
+ bottom: 0;
362
+ width: 4px;
363
+ background: var(--ol-accent);
364
+ pointer-events: none;
365
+ z-index: 20;
366
+ }
367
+
368
+ .ol-editor .ol-content .ProseMirror.resize-cursor {
369
+ cursor: col-resize;
370
+ }
371
+
372
+ /* Preserved-but-unrecognised markup, surfaced rather than hidden. */
373
+ .ol-editor .ol-content .ProseMirror-selectednode {
374
+ outline: 2px solid var(--ol-focus);
375
+ outline-offset: 2px;
376
+ border-radius: 2px;
377
+ }
378
+
379
+ .ol-editor .ol-source {
380
+ box-sizing: border-box;
381
+ display: block;
382
+ width: 100%;
383
+ min-height: 12rem;
384
+ padding: 12px;
385
+ border: 1px solid var(--ol-border);
386
+ border-radius: 0 0 var(--ol-radius) var(--ol-radius);
387
+ background: var(--ol-surface);
388
+ color: var(--ol-text);
389
+ font-family: var(--ol-font-mono);
390
+ font-size: 13px;
391
+ line-height: 1.5;
392
+ resize: vertical;
393
+ white-space: pre-wrap;
394
+ }
395
+
396
+ /* Directional icons under RTL.
397
+ Group order reverses for free with flexbox, but a curved undo arrow does not:
398
+ in an RTL document "back" points the other way. Only genuinely directional
399
+ icons are flipped -- bold and italic must not be mirrored. */
400
+ .ol-editor[dir="rtl"] .ol-icon--directional,
401
+ [dir="rtl"] .ol-editor .ol-icon--directional {
402
+ transform: scaleX(-1);
403
+ }
404
+
405
+ /* The announcement region. Visually hidden but not display:none, which would
406
+ remove it from the accessibility tree and silence it. */
407
+ .ol-editor .ol-live {
408
+ position: absolute;
409
+ width: 1px;
410
+ height: 1px;
411
+ margin: -1px;
412
+ padding: 0;
413
+ overflow: hidden;
414
+ clip: rect(0 0 0 0);
415
+ clip-path: inset(50%);
416
+ white-space: nowrap;
417
+ border: 0;
418
+ }
419
+
420
+ /* Coarse pointers get a larger target. WCAG 2.2 SC 2.5.8 asks for 24x24 CSS px
421
+ minimum; 32 clears that on a mouse and 40 is comfortable on a thumb. */
422
+ @media (pointer: coarse) {
423
+ .ol-editor {
424
+ --ol-button-size: var(--openleaf-button-size, 40px);
425
+ --ol-gap: var(--openleaf-gap, 4px);
426
+ }
427
+ }
428
+
429
+ @media (prefers-reduced-motion: reduce) {
430
+ .ol-editor .ol-btn {
431
+ transition: none;
432
+ }
433
+ }
434
+
435
+ /* Forced colours (Windows high contrast) replaces our palette wholesale. The
436
+ pressed state must not depend on a background we no longer control, so it is
437
+ re-expressed as a border, which the mode preserves. */
438
+ @media (forced-colors: active) {
439
+ .ol-editor .ol-btn {
440
+ border-color: ButtonBorder;
441
+ color: ButtonText;
442
+ }
443
+ .ol-editor .ol-btn[aria-pressed="true"] {
444
+ border-color: Highlight;
445
+ color: Highlight;
446
+ box-shadow: none;
447
+ }
448
+ .ol-editor .ol-btn:focus-visible {
449
+ outline-color: Highlight;
450
+ }
451
+ .ol-editor .ol-btn[aria-disabled="true"] {
452
+ color: GrayText;
453
+ opacity: 1;
454
+ }
455
+ }
456
+ `;
457
+ let externallyProvided = false;
458
+ /**
459
+ * Declare that the integrator has linked `openleaf.css` themselves, so no
460
+ * injection is attempted. Call before the first editor is created.
461
+ */
462
+ export function markStylesExternal() {
463
+ externallyProvided = true;
464
+ }
465
+ const injected = new WeakSet();
466
+ const registered = new Map();
467
+ let warned = false;
468
+ /**
469
+ * Attach a stylesheet through the CSP-safe path.
470
+ *
471
+ * Public because plugins need it. The highlighting plugin previously
472
+ * hand-rolled this -- the same constructable-stylesheet dance, the same
473
+ * fallback, the same warning -- which meant the CSP reasoning lived in two
474
+ * places and only one of them would get fixed.
475
+ *
476
+ * Deduplicated per document by the CSS text itself, so calling it twice from a
477
+ * bundle loaded twice is harmless.
478
+ */
479
+ export function registerStyles(css, target) {
480
+ const doc = target ?? (typeof document !== 'undefined' ? document : undefined);
481
+ if (!doc)
482
+ return 'unavailable';
483
+ let seen = registered.get(doc);
484
+ if (!seen) {
485
+ seen = new Set();
486
+ registered.set(doc, seen);
487
+ }
488
+ if (seen.has(css))
489
+ return 'already';
490
+ // CSP gates resources *parsed as style* -- <style> elements, style attributes,
491
+ // linked stylesheets. A CSSOM object attached through adoptedStyleSheets never
492
+ // passes through that gate, by design rather than by loophole.
493
+ if (typeof CSSStyleSheet !== 'undefined' && 'adoptedStyleSheets' in Document.prototype) {
494
+ try {
495
+ const sheet = new CSSStyleSheet();
496
+ sheet.replaceSync(css);
497
+ doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, sheet];
498
+ seen.add(css);
499
+ return 'adopted';
500
+ }
501
+ catch {
502
+ /* fall through to the warning */
503
+ }
504
+ }
505
+ if (!warned) {
506
+ warned = true;
507
+ console.warn('@openleaf-editor/ui: this browser has no adoptedStyleSheets support, so styles ' +
508
+ 'were not injected. Link the stylesheet instead:\n' +
509
+ ' <link rel="stylesheet" href=".../@openleaf-editor/ui/openleaf.css">\n' +
510
+ 'then call markStylesExternal() to silence this warning.');
511
+ }
512
+ return 'unavailable';
513
+ }
514
+ /**
515
+ * Ensure the editor's own stylesheet is present. Safe to call repeatedly.
516
+ *
517
+ * There is deliberately **no `<style>` injection fallback**. It looks like a
518
+ * safety net and is the opposite: it fails under exactly the strict-CSP
519
+ * configurations it would be needed for, and it fails *silently* -- a blocked
520
+ * injection leaves an unstyled toolbar and no signal an integrator can act on.
521
+ */
522
+ export function ensureStyles(doc) {
523
+ if (externallyProvided)
524
+ return 'external';
525
+ if (injected.has(doc))
526
+ return 'already';
527
+ const outcome = registerStyles(CSS, doc);
528
+ if (outcome === 'adopted')
529
+ injected.add(doc);
530
+ return outcome;
531
+ }
532
+ //# sourceMappingURL=styles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.js","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The toolbar.
3
+ *
4
+ * A plain class rather than a custom element: the editor is already a custom
5
+ * element, buttons are just DOM, and per-button custom elements would cost
6
+ * bundle size and upgrade-timing complexity for no benefit.
7
+ *
8
+ * ## Keyboard model
9
+ *
10
+ * `role="toolbar"` with a roving tabindex, so the whole bar is ONE tab stop.
11
+ * Without that, Tab from the editable region walks a keyboard user through
12
+ * twenty buttons before they reach their content.
13
+ *
14
+ * Two details that are easy to get wrong and both matter:
15
+ *
16
+ * 1. **Arrow-key roving is applied only to `<button>` elements.** The block-type
17
+ * control is a native `<select>`, and when focus is on it, Left/Right have two
18
+ * competing owners -- the roving handler wants to move to the next item, the
19
+ * select natively wants to change its value. Intercepting those keys breaks
20
+ * the select; not intercepting them breaks the toolbar contract. The
21
+ * resolution is that the select owns all of its own key events and is a
22
+ * genuine second tab stop rather than part of the roving scheme.
23
+ *
24
+ * 2. **Escape returns focus and the selection to the content.** Preventing mouse
25
+ * clicks from stealing focus solves the mouse case and leaves the keyboard
26
+ * case undefined: once a screen reader user deliberately enters the toolbar,
27
+ * focus really is on a button, and without a return path their only way out
28
+ * is blind-Tabbing through the rest of the host's form. `Alt+F10` enters,
29
+ * Escape leaves, matching TinyMCE and CKEditor 5 so muscle memory transfers.
30
+ */
31
+ import type { EditorState, Transaction } from 'prosemirror-state';
32
+ import type { EditorView } from 'prosemirror-view';
33
+ export interface ToolbarOptions {
34
+ /** Accessible name for the toolbar landmark. */
35
+ label?: string;
36
+ /** Space-separated item ids, `|` for a separator. */
37
+ layout?: string;
38
+ }
39
+ export declare class Toolbar {
40
+ #private;
41
+ readonly el: HTMLDivElement;
42
+ constructor(host: HTMLElement, doc: Document, options?: ToolbarOptions);
43
+ /** Attach to a view and build the controls. */
44
+ mount(view: EditorView): void;
45
+ destroy(): void;
46
+ /** The live region element, which the host mounts once. */
47
+ get liveRegion(): HTMLDivElement;
48
+ /**
49
+ * Reflect the editor state onto the controls.
50
+ *
51
+ * Deliberately synchronous, not batched into an animation frame. Twenty cheap
52
+ * predicates plus a diffed attribute write is sub-millisecond work, and
53
+ * batching would trade a perceptible frame of lag between pressing Bold and
54
+ * the button lighting up for a performance problem that does not exist.
55
+ *
56
+ * `tr` is passed so announcements can be gated on a real formatting change.
57
+ */
58
+ update(state: EditorState, tr?: Transaction): void;
59
+ /**
60
+ * Push a state a predicate cannot derive.
61
+ *
62
+ * `isActive`/`isEnabled` assume everything follows from the document and
63
+ * selection. Real plugin state does not: "upload in progress", "collab lock
64
+ * held by another user", "rewrite running" all live outside the document, and
65
+ * a pull-on-transaction model cannot see them.
66
+ */
67
+ setItemState(id: string, state: {
68
+ active?: boolean;
69
+ enabled?: boolean;
70
+ }): void;
71
+ /** Move focus into the toolbar. Bound to Alt+F10 by the host. */
72
+ focusToolbar(): void;
73
+ /** Return focus and the prior selection to the editable region. */
74
+ returnFocusToContent(): void;
75
+ }
76
+ //# sourceMappingURL=toolbar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolbar.d.ts","sourceRoot":"","sources":["../src/toolbar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAUlD,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAiDD,qBAAa,OAAO;;IAClB,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAA;gBAef,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,GAAE,cAAmB;IA8B1E,+CAA+C;IAC/C,KAAK,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IAM7B,OAAO,IAAI,IAAI;IAQf,2DAA2D;IAC3D,IAAI,UAAU,IAAI,cAAc,CAE/B;IAwPD;;;;;;;;;OASG;IACH,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,CAAC,EAAE,WAAW,GAAG,IAAI;IAmDlD;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;IA8F9E,iEAAiE;IACjE,YAAY,IAAI,IAAI;IAYpB,mEAAmE;IACnE,oBAAoB,IAAI,IAAI;CAG7B"}