@docmentis/udoc-editor 0.0.0-stage → 0.8.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.
Files changed (207) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +47 -2
  4. package/dist/arrange.d.ts +37 -0
  5. package/dist/arrange.d.ts.map +1 -0
  6. package/dist/arrange.js +431 -0
  7. package/dist/arrange.js.map +1 -0
  8. package/dist/clipboard.d.ts +12 -0
  9. package/dist/clipboard.d.ts.map +1 -0
  10. package/dist/clipboard.js +101 -0
  11. package/dist/clipboard.js.map +1 -0
  12. package/dist/commands.d.ts +12 -0
  13. package/dist/commands.d.ts.map +1 -0
  14. package/dist/commands.js +225 -0
  15. package/dist/commands.js.map +1 -0
  16. package/dist/components/ComboBox.d.ts +38 -0
  17. package/dist/components/ComboBox.d.ts.map +1 -0
  18. package/dist/components/ComboBox.js +57 -0
  19. package/dist/components/ComboBox.js.map +1 -0
  20. package/dist/components/ModeSwitch.d.ts +15 -0
  21. package/dist/components/ModeSwitch.d.ts.map +1 -0
  22. package/dist/components/ModeSwitch.js +94 -0
  23. package/dist/components/ModeSwitch.js.map +1 -0
  24. package/dist/components/NewSlideModal.d.ts +41 -0
  25. package/dist/components/NewSlideModal.d.ts.map +1 -0
  26. package/dist/components/NewSlideModal.js +136 -0
  27. package/dist/components/NewSlideModal.js.map +1 -0
  28. package/dist/components/PageControls.d.ts +78 -0
  29. package/dist/components/PageControls.d.ts.map +1 -0
  30. package/dist/components/PageControls.js +201 -0
  31. package/dist/components/PageControls.js.map +1 -0
  32. package/dist/components/PageSettingsModal.d.ts +27 -0
  33. package/dist/components/PageSettingsModal.d.ts.map +1 -0
  34. package/dist/components/PageSettingsModal.js +273 -0
  35. package/dist/components/PageSettingsModal.js.map +1 -0
  36. package/dist/components/PaintOptionsModal.d.ts +19 -0
  37. package/dist/components/PaintOptionsModal.d.ts.map +1 -0
  38. package/dist/components/PaintOptionsModal.js +103 -0
  39. package/dist/components/PaintOptionsModal.js.map +1 -0
  40. package/dist/components/TextControls.d.ts +49 -0
  41. package/dist/components/TextControls.d.ts.map +1 -0
  42. package/dist/components/TextControls.js +452 -0
  43. package/dist/components/TextControls.js.map +1 -0
  44. package/dist/context-menu.d.ts +46 -0
  45. package/dist/context-menu.d.ts.map +1 -0
  46. package/dist/context-menu.js +144 -0
  47. package/dist/context-menu.js.map +1 -0
  48. package/dist/edit-client.d.ts +121 -0
  49. package/dist/edit-client.d.ts.map +1 -0
  50. package/dist/edit-client.js +154 -0
  51. package/dist/edit-client.js.map +1 -0
  52. package/dist/edit-command.d.ts +7 -0
  53. package/dist/edit-command.d.ts.map +1 -0
  54. package/dist/edit-command.js +18 -0
  55. package/dist/edit-command.js.map +1 -0
  56. package/dist/edit-mode.d.ts +19 -0
  57. package/dist/edit-mode.d.ts.map +1 -0
  58. package/dist/edit-mode.js +214 -0
  59. package/dist/edit-mode.js.map +1 -0
  60. package/dist/editor.d.ts +101 -0
  61. package/dist/editor.d.ts.map +1 -0
  62. package/dist/editor.js +101 -0
  63. package/dist/editor.js.map +1 -0
  64. package/dist/fonts/faces.d.ts +28 -0
  65. package/dist/fonts/faces.d.ts.map +1 -0
  66. package/dist/fonts/faces.js +38 -0
  67. package/dist/fonts/faces.js.map +1 -0
  68. package/dist/fonts/preview.d.ts +98 -0
  69. package/dist/fonts/preview.d.ts.map +1 -0
  70. package/dist/fonts/preview.js +149 -0
  71. package/dist/fonts/preview.js.map +1 -0
  72. package/dist/formatting-marks.d.ts +23 -0
  73. package/dist/formatting-marks.d.ts.map +1 -0
  74. package/dist/formatting-marks.js +55 -0
  75. package/dist/formatting-marks.js.map +1 -0
  76. package/dist/grips.d.ts +14 -0
  77. package/dist/grips.d.ts.map +1 -0
  78. package/dist/grips.js +459 -0
  79. package/dist/grips.js.map +1 -0
  80. package/dist/i18n/ar.d.ts +4 -0
  81. package/dist/i18n/ar.d.ts.map +1 -0
  82. package/dist/i18n/ar.js +175 -0
  83. package/dist/i18n/ar.js.map +1 -0
  84. package/dist/i18n/de.d.ts +4 -0
  85. package/dist/i18n/de.d.ts.map +1 -0
  86. package/dist/i18n/de.js +175 -0
  87. package/dist/i18n/de.js.map +1 -0
  88. package/dist/i18n/en.d.ts +5 -0
  89. package/dist/i18n/en.d.ts.map +1 -0
  90. package/dist/i18n/en.js +176 -0
  91. package/dist/i18n/en.js.map +1 -0
  92. package/dist/i18n/es.d.ts +4 -0
  93. package/dist/i18n/es.d.ts.map +1 -0
  94. package/dist/i18n/es.js +175 -0
  95. package/dist/i18n/es.js.map +1 -0
  96. package/dist/i18n/fr.d.ts +4 -0
  97. package/dist/i18n/fr.d.ts.map +1 -0
  98. package/dist/i18n/fr.js +175 -0
  99. package/dist/i18n/fr.js.map +1 -0
  100. package/dist/i18n/index.d.ts +21 -0
  101. package/dist/i18n/index.d.ts.map +1 -0
  102. package/dist/i18n/index.js +42 -0
  103. package/dist/i18n/index.js.map +1 -0
  104. package/dist/i18n/ja.d.ts +4 -0
  105. package/dist/i18n/ja.d.ts.map +1 -0
  106. package/dist/i18n/ja.js +175 -0
  107. package/dist/i18n/ja.js.map +1 -0
  108. package/dist/i18n/ko.d.ts +4 -0
  109. package/dist/i18n/ko.d.ts.map +1 -0
  110. package/dist/i18n/ko.js +175 -0
  111. package/dist/i18n/ko.js.map +1 -0
  112. package/dist/i18n/messages.d.ts +316 -0
  113. package/dist/i18n/messages.d.ts.map +1 -0
  114. package/dist/i18n/messages.js +2 -0
  115. package/dist/i18n/messages.js.map +1 -0
  116. package/dist/i18n/pt-BR.d.ts +4 -0
  117. package/dist/i18n/pt-BR.d.ts.map +1 -0
  118. package/dist/i18n/pt-BR.js +175 -0
  119. package/dist/i18n/pt-BR.js.map +1 -0
  120. package/dist/i18n/ru.d.ts +4 -0
  121. package/dist/i18n/ru.d.ts.map +1 -0
  122. package/dist/i18n/ru.js +175 -0
  123. package/dist/i18n/ru.js.map +1 -0
  124. package/dist/i18n/zh-CN.d.ts +4 -0
  125. package/dist/i18n/zh-CN.d.ts.map +1 -0
  126. package/dist/i18n/zh-CN.js +175 -0
  127. package/dist/i18n/zh-CN.js.map +1 -0
  128. package/dist/i18n/zh-TW.d.ts +4 -0
  129. package/dist/i18n/zh-TW.d.ts.map +1 -0
  130. package/dist/i18n/zh-TW.js +175 -0
  131. package/dist/i18n/zh-TW.js.map +1 -0
  132. package/dist/icons.d.ts +97 -0
  133. package/dist/icons.d.ts.map +1 -0
  134. package/dist/icons.js +103 -0
  135. package/dist/icons.js.map +1 -0
  136. package/dist/index.d.ts +9 -0
  137. package/dist/index.d.ts.map +1 -0
  138. package/dist/index.js +14 -0
  139. package/dist/index.js.map +1 -0
  140. package/dist/insert.d.ts +21 -0
  141. package/dist/insert.d.ts.map +1 -0
  142. package/dist/insert.js +129 -0
  143. package/dist/insert.js.map +1 -0
  144. package/dist/menu-action.d.ts +73 -0
  145. package/dist/menu-action.d.ts.map +1 -0
  146. package/dist/menu-action.js +89 -0
  147. package/dist/menu-action.js.map +1 -0
  148. package/dist/mode.d.ts +21 -0
  149. package/dist/mode.d.ts.map +1 -0
  150. package/dist/mode.js +17 -0
  151. package/dist/mode.js.map +1 -0
  152. package/dist/new-slide.d.ts +21 -0
  153. package/dist/new-slide.d.ts.map +1 -0
  154. package/dist/new-slide.js +63 -0
  155. package/dist/new-slide.js.map +1 -0
  156. package/dist/page-controls.d.ts +20 -0
  157. package/dist/page-controls.d.ts.map +1 -0
  158. package/dist/page-controls.js +99 -0
  159. package/dist/page-controls.js.map +1 -0
  160. package/dist/paint.d.ts +52 -0
  161. package/dist/paint.d.ts.map +1 -0
  162. package/dist/paint.js +262 -0
  163. package/dist/paint.js.map +1 -0
  164. package/dist/ruler.d.ts +10 -0
  165. package/dist/ruler.d.ts.map +1 -0
  166. package/dist/ruler.js +97 -0
  167. package/dist/ruler.js.map +1 -0
  168. package/dist/state.d.ts +85 -0
  169. package/dist/state.d.ts.map +1 -0
  170. package/dist/state.js +28 -0
  171. package/dist/state.js.map +1 -0
  172. package/dist/styles.d.ts +2 -0
  173. package/dist/styles.d.ts.map +1 -0
  174. package/dist/styles.js +758 -0
  175. package/dist/styles.js.map +1 -0
  176. package/dist/tabs.d.ts +11 -0
  177. package/dist/tabs.d.ts.map +1 -0
  178. package/dist/tabs.js +153 -0
  179. package/dist/tabs.js.map +1 -0
  180. package/dist/text-format.d.ts +114 -0
  181. package/dist/text-format.d.ts.map +1 -0
  182. package/dist/text-format.js +315 -0
  183. package/dist/text-format.js.map +1 -0
  184. package/dist/tools.d.ts +35 -0
  185. package/dist/tools.d.ts.map +1 -0
  186. package/dist/tools.js +41 -0
  187. package/dist/tools.js.map +1 -0
  188. package/dist/version.d.ts +2 -0
  189. package/dist/version.d.ts.map +1 -0
  190. package/dist/version.js +7 -0
  191. package/dist/version.js.map +1 -0
  192. package/dist/worker/handlers.d.ts +11 -0
  193. package/dist/worker/handlers.d.ts.map +1 -0
  194. package/dist/worker/handlers.js +127 -0
  195. package/dist/worker/handlers.js.map +1 -0
  196. package/dist/worker/protocol.d.ts +190 -0
  197. package/dist/worker/protocol.d.ts.map +1 -0
  198. package/dist/worker/protocol.js +2 -0
  199. package/dist/worker/protocol.js.map +1 -0
  200. package/dist/worker/worker-source.d.ts +3 -0
  201. package/dist/worker/worker-source.d.ts.map +1 -0
  202. package/dist/worker/worker-source.js +11 -0
  203. package/dist/worker/worker.d.ts +2 -0
  204. package/dist/worker/worker.d.ts.map +1 -0
  205. package/dist/worker/worker.js +6 -0
  206. package/dist/worker/worker.js.map +1 -0
  207. package/package.json +69 -5
@@ -0,0 +1,19 @@
1
+ import type { PaintChannel } from "@docmentis/udoc-ui/internal";
2
+ import type { I18n } from "../i18n/index.js";
3
+ /** What `open` needs — which channel's options to show. */
4
+ export interface PaintOptionsContext {
5
+ channel: PaintChannel;
6
+ }
7
+ /**
8
+ * Build the paint-options modal. The returned handle is mounted once by the
9
+ * shell; `open` re-seeds it for whichever channel's row was clicked.
10
+ */
11
+ export declare function createPaintOptionsModal(i18n: I18n): {
12
+ el: HTMLDivElement;
13
+ mount: (parent: HTMLElement) => void;
14
+ open: (ctx: PaintOptionsContext) => void;
15
+ close: () => void;
16
+ destroy: () => void;
17
+ };
18
+ export type PaintOptionsModal = ReturnType<typeof createPaintOptionsModal>;
19
+ //# sourceMappingURL=PaintOptionsModal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PaintOptionsModal.d.ts","sourceRoot":"","sources":["../../src/components/PaintOptionsModal.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,IAAI,EAAc,MAAM,kBAAkB,CAAC;AAEzD,2DAA2D;AAC3D,MAAM,WAAW,mBAAmB;IAChC,OAAO,EAAE,YAAY,CAAC;CACzB;AAgBD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,IAAI;;oBAmEvB,WAAW,KAAG,IAAI;gBAftB,mBAAmB,KAAG,IAAI;iBAS3B,IAAI;mBAkBF,IAAI;EAM3B;AAED,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
@@ -0,0 +1,103 @@
1
+ // The advanced Fill / Line options dialog, opened from a paint panel's "More
2
+ // Fill/Line Options…" row. A modal — backdrop + centered dialog — that will
3
+ // host the richer authoring surface the dropdown can't: a custom colour, and
4
+ // the non-solid fills (gradient / pattern / picture) for Fill; compound lines,
5
+ // caps / joins, and arrow ends for Stroke.
6
+ //
7
+ // Storeless and imperative, matching {@link createPageSettingsModal}: mounted
8
+ // once by the shell and reused for whichever channel's row was clicked.
9
+ // `open({ channel })` seeds the title; the body is a placeholder until the
10
+ // surface is designed — the goal here is the dialog + its seam, not its content.
11
+ import { ICON_CLEAR, on } from "@docmentis/udoc-ui/internal";
12
+ const TITLE = {
13
+ fill: "paintOptions.fillTitle",
14
+ stroke: "paintOptions.lineTitle",
15
+ text: "paintOptions.textColorTitle",
16
+ highlight: "paintOptions.highlightTitle",
17
+ };
18
+ const BLURB = {
19
+ fill: "paintOptions.fillBlurb",
20
+ stroke: "paintOptions.lineBlurb",
21
+ text: "paintOptions.textColorBlurb",
22
+ highlight: "paintOptions.highlightBlurb",
23
+ };
24
+ /**
25
+ * Build the paint-options modal. The returned handle is mounted once by the
26
+ * shell; `open` re-seeds it for whichever channel's row was clicked.
27
+ */
28
+ export function createPaintOptionsModal(i18n) {
29
+ const el = document.createElement("div");
30
+ el.className = "udoc-paint-modal";
31
+ el.style.display = "none";
32
+ const backdrop = document.createElement("div");
33
+ backdrop.className = "udoc-paint-modal__backdrop";
34
+ const dialog = document.createElement("div");
35
+ dialog.className = "udoc-paint-modal__dialog";
36
+ dialog.setAttribute("role", "dialog");
37
+ dialog.setAttribute("aria-modal", "true");
38
+ // --- header ---
39
+ const header = document.createElement("div");
40
+ header.className = "udoc-paint-modal__header";
41
+ const heading = document.createElement("h2");
42
+ heading.className = "udoc-paint-modal__heading";
43
+ const closeBtn = document.createElement("button");
44
+ closeBtn.type = "button";
45
+ closeBtn.className = "udoc-paint-modal__close";
46
+ closeBtn.setAttribute("aria-label", i18n.t("common.close"));
47
+ closeBtn.title = i18n.t("common.close");
48
+ closeBtn.innerHTML = ICON_CLEAR;
49
+ header.append(heading, closeBtn);
50
+ // --- body (placeholder until the authoring surface is designed) ---
51
+ const body = document.createElement("div");
52
+ body.className = "udoc-paint-modal__body";
53
+ const placeholder = document.createElement("p");
54
+ placeholder.className = "udoc-paint-modal__placeholder";
55
+ body.appendChild(placeholder);
56
+ // --- footer ---
57
+ const footer = document.createElement("div");
58
+ footer.className = "udoc-paint-modal__footer";
59
+ const doneBtn = document.createElement("button");
60
+ doneBtn.type = "button";
61
+ doneBtn.className = "udoc-paint-modal__done";
62
+ doneBtn.textContent = i18n.t("common.done");
63
+ footer.appendChild(doneBtn);
64
+ dialog.append(header, body, footer);
65
+ el.append(backdrop, dialog);
66
+ let isOpen = false;
67
+ const unsubEvents = [];
68
+ on(closeBtn, "click", () => close());
69
+ on(doneBtn, "click", () => close());
70
+ on(backdrop, "click", () => close());
71
+ function open(ctx) {
72
+ heading.textContent = i18n.t(TITLE[ctx.channel]);
73
+ dialog.setAttribute("aria-label", i18n.t(TITLE[ctx.channel]));
74
+ placeholder.textContent = i18n.t(BLURB[ctx.channel]);
75
+ isOpen = true;
76
+ el.style.display = "";
77
+ doneBtn.focus();
78
+ }
79
+ function close() {
80
+ if (!isOpen)
81
+ return;
82
+ isOpen = false;
83
+ el.style.display = "none";
84
+ }
85
+ function mount(parent) {
86
+ parent.appendChild(el);
87
+ const onKeydown = (e) => {
88
+ if (isOpen && e.key === "Escape") {
89
+ e.stopPropagation();
90
+ close();
91
+ }
92
+ };
93
+ document.addEventListener("keydown", onKeydown);
94
+ unsubEvents.push(() => document.removeEventListener("keydown", onKeydown));
95
+ }
96
+ function destroy() {
97
+ for (const off of unsubEvents)
98
+ off();
99
+ el.remove();
100
+ }
101
+ return { el, mount, open, close, destroy };
102
+ }
103
+ //# sourceMappingURL=PaintOptionsModal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PaintOptionsModal.js","sourceRoot":"","sources":["../../src/components/PaintOptionsModal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import type { RibbonControl, RibbonControlEnv } from "@docmentis/udoc-ui/internal";
2
+ import type { ListKind, ListStyle, TextFormatProperty } from "../text-format.js";
3
+ import type { EditTools } from "../tools.js";
4
+ /** The em an engine-drawn specimen is rasterized at, in CSS px — matched to the
5
+ * row's own text size so a masked row and a live-text row read at the same
6
+ * weight beside each other. */
7
+ export declare const FONT_SPECIMEN_EM = 14;
8
+ /**
9
+ * The Text tab's font picker: a typeable combobox showing the *selection's*
10
+ * resolved font. The list offers the catalog (opened full, virtualized), Enter
11
+ * applies any name — one outside the list included — and, while the list is
12
+ * open, typing filters it (the input doubles as a search field). The input
13
+ * mirrors the selection; each render keeps it in sync.
14
+ */
15
+ export declare function fontFamilyControl(env: RibbonControlEnv, id: string, tools: EditTools): RibbonControl;
16
+ /**
17
+ * The Text tab's size field: a typeable combobox showing the selection's
18
+ * resolved point size, its list the preset sizes (rebuilt on open so the check
19
+ * tracks the selection). Enter applies any point size in range — one outside
20
+ * the presets included. Mirror-only like the font picker.
21
+ */
22
+ export declare function fontSizeControl(env: RibbonControlEnv, id: string, tools: EditTools): RibbonControl;
23
+ /**
24
+ * A character toggle (Bold / Italic / Underline / …): an icon button
25
+ * reflecting whether the selection carries that format (on / off / mixed). A
26
+ * mixed / off selection turns the format on, a fully-on one turns it off (the
27
+ * Office gesture).
28
+ */
29
+ export declare function textFormatControl(env: RibbonControlEnv, command: {
30
+ id: string;
31
+ icon: string;
32
+ property: TextFormatProperty;
33
+ }, tools: EditTools): RibbonControl;
34
+ /**
35
+ * A Bullets / Numbering split button: the main region toggles the selection's
36
+ * paragraphs into the last-picked style (or out of the list when it's already
37
+ * lit), the caret opens the style gallery. A mirror like the character toggles
38
+ * — the lit state re-derives from the selection each render, not from the click.
39
+ */
40
+ export declare function listControl(env: RibbonControlEnv, command: {
41
+ id: string;
42
+ icon: string;
43
+ list: ListKind;
44
+ }, tools: EditTools): RibbonControl;
45
+ /** The style each list button applies when its main region is clicked — the
46
+ * gallery's first entry until one is picked, that pick from then on (the
47
+ * split-button contract: the caret chooses, the main repeats). */
48
+ export declare function createLastListStyle(): Record<ListKind, ListStyle>;
49
+ //# sourceMappingURL=TextControls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TextControls.d.ts","sourceRoot":"","sources":["../../src/components/TextControls.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAmB,MAAM,6BAA6B,CAAC;AAKpG,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAiB7C;;gCAEgC;AAChC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAqHnC;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,gBAAgB,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,aAAa,CAkLpG;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,gBAAgB,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,aAAa,CAiDlG;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC7B,GAAG,EAAE,gBAAgB,EACrB,OAAO,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,kBAAkB,CAAA;CAAE,EACnE,KAAK,EAAE,SAAS,GACjB,aAAa,CAcf;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACvB,GAAG,EAAE,gBAAgB,EACrB,OAAO,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,EACrD,KAAK,EAAE,SAAS,GACjB,aAAa,CA8Df;AAED;;mEAEmE;AACnE,wBAAgB,mBAAmB,IAAI,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,CAEjE"}
@@ -0,0 +1,452 @@
1
+ import { createButton, createSplitButton } from "@docmentis/udoc-ui/internal";
2
+ import { cssFontFamily, isEngineDrawn } from "../fonts/preview.js";
3
+ import { editState } from "../state.js";
4
+ import { listStyles } from "../text-format.js";
5
+ import { createComboBox } from "./ComboBox.js";
6
+ /** The point sizes the Text tab's size dropdown offers — the common Office set.
7
+ * The selection's own size still shows on the trigger even when it isn't one of
8
+ * these (a document can carry any size); the list is just the quick picks. */
9
+ const SIZE_PRESETS = [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36, 40, 48, 60, 72];
10
+ // Font dropdown virtualization knobs. FONT_ROW_H must match the fixed
11
+ // `.udoc-font-item` height in styles.ts; FONT_MENU_ROWS is how many rows
12
+ // the menu's (height-capped) max-height shows. The number of selectable
13
+ // fonts is unbounded — the list scrolls the whole catalog.
14
+ const FONT_ROW_H = 30;
15
+ const FONT_MENU_ROWS = 10;
16
+ const FONT_OVERSCAN = 4;
17
+ /** The em an engine-drawn specimen is rasterized at, in CSS px — matched to the
18
+ * row's own text size so a masked row and a live-text row read at the same
19
+ * weight beside each other. */
20
+ export const FONT_SPECIMEN_EM = 14;
21
+ /** A text-format toggle's `aria-pressed` for the tri-state selection format —
22
+ * `"mixed"` for a split selection, else the plain boolean string. The active
23
+ * tint lights for both `"on"` and `"mixed"` (the format is present); the aria
24
+ * value is what distinguishes them for assistive tech. */
25
+ function textFormatAria(state) {
26
+ return state === "on" ? "true" : state === "mixed" ? "mixed" : "false";
27
+ }
28
+ /**
29
+ * Whether a family answers what has been typed into the font box.
30
+ *
31
+ * **Prefix, not substring.** Across ~2000 families a match anywhere in the name
32
+ * buries what you meant: `ar` used to return 227 rows — Akshar, Almarai and
33
+ * Amaranth ahead of Arimo — where it now returns 31, and `oto` matched Roboto
34
+ * without you having typed its start. Type-ahead is for a name you already know,
35
+ * so the match has to begin where the name does.
36
+ *
37
+ * Every **word** is a starting point, not just the first, because a family's
38
+ * distinguishing word is often not the one it leads with — `sans` has to keep
39
+ * finding Open Sans, Noto Sans and PT Sans, and `mono` Roboto Mono. That is
40
+ * still a prefix rule: it drops the mid-word matches and keeps the ones a reader
41
+ * would call "starts with".
42
+ *
43
+ * Splitting on spaces is exhaustive for this catalog — every name in it is
44
+ * letters, digits and spaces, with no hyphens or punctuation to miss.
45
+ */
46
+ function matchesFontQuery(name, query) {
47
+ const lower = name.toLowerCase();
48
+ return lower.startsWith(query) || lower.split(" ").some((word) => word.startsWith(query));
49
+ }
50
+ /**
51
+ * Put a font-picker row's family into its label — as its own typeface where we
52
+ * can, and as plain text where we can't yet.
53
+ *
54
+ * Three states, and the row is legible in all of them. A family the browser can
55
+ * load is **live text** with `font-family` set to it: the name shows in the UI
56
+ * face until the web font arrives and in its own from then on, with no work here
57
+ * to make the swap happen. A family only the engine can draw shows the plain
58
+ * name until its specimen lands, then the mask replaces it. A specimen the
59
+ * engine had nothing to draw for stays plain text, which is exactly what the
60
+ * picker looked like before specimens existed.
61
+ *
62
+ * The engine specimen is a `background: currentColor` box with the PNG as its
63
+ * mask, so it takes the row's text color: the hovered row, the dark theme and
64
+ * the forced-colors mode all come out right without a second raster. `width`
65
+ * and `height` are the CSS box the engine sized it for, and `bleed` the
66
+ * transparent margin inside it that a negative offset takes back out.
67
+ */
68
+ function fillFontLabel(label, family, loader) {
69
+ const specimen = isEngineDrawn(family.sourceType) ? loader.specimen(family.name) : undefined;
70
+ if (specimen) {
71
+ const ink = document.createElement("span");
72
+ ink.className = "udoc-font-specimen";
73
+ ink.style.setProperty("--udoc-font-specimen", `url("${specimen.url}")`);
74
+ ink.style.width = `${specimen.width}px`;
75
+ ink.style.height = `${specimen.height}px`;
76
+ // Pull the raster back by its bleed so the specimen's pen origin lands
77
+ // where a live-text row's does; without it every engine row sits an
78
+ // eighth of an em right of its neighbours.
79
+ ink.style.marginLeft = `${-specimen.bleed}px`;
80
+ label.replaceChildren(ink);
81
+ return;
82
+ }
83
+ label.textContent = family.name;
84
+ // An engine-drawn family keeps the UI face while it waits: naming it to CSS
85
+ // would let a same-named OS font answer for a face the document isn't using.
86
+ if (!isEngineDrawn(family.sourceType))
87
+ label.style.fontFamily = cssFontFamily(family.name);
88
+ }
89
+ // Reflect a value in a combobox's input. Skipped while the input is focused so
90
+ // it never stomps a value the user is typing (the incoming selection report
91
+ // re-mirrors on blur).
92
+ //
93
+ // A font name shows in the UI font, not its own typeface. We deliberately don't
94
+ // preview: the engine shapes font formats the browser can't load as web fonts
95
+ // (Type1, ...), and bundled / embedded faces never reach the DOM at all — an
96
+ // honest name beats a preview that only works for the subset a `font-family`
97
+ // CSS lookup happens to resolve.
98
+ function mirrorField(field, value) {
99
+ if (field === document.activeElement)
100
+ return;
101
+ field.value = value;
102
+ }
103
+ // Commit a typed value on Enter, abandon it on Escape / blur (the mirror then
104
+ // restores the selection's value), open the list on ArrowDown, and keep the
105
+ // caret-movement keys for in-field editing — stopping them from reaching the
106
+ // ribbon's roving handler, which would otherwise jump focus to an adjacent
107
+ // tool. `mirror` re-reads the live selection each blur.
108
+ function wireComboInput(combo, commit, mirror) {
109
+ combo.input.addEventListener("keydown", (e) => {
110
+ switch (e.key) {
111
+ case "Enter":
112
+ commit(combo.input.value);
113
+ combo.input.blur();
114
+ break;
115
+ case "Escape":
116
+ combo.input.blur();
117
+ break;
118
+ case "ArrowDown":
119
+ e.preventDefault();
120
+ combo.controller.open();
121
+ break;
122
+ case "ArrowLeft":
123
+ case "ArrowRight":
124
+ case "Home":
125
+ case "End":
126
+ e.stopPropagation();
127
+ break;
128
+ }
129
+ });
130
+ combo.input.addEventListener("focus", () => combo.input.select());
131
+ combo.input.addEventListener("blur", mirror);
132
+ }
133
+ /**
134
+ * The Text tab's font picker: a typeable combobox showing the *selection's*
135
+ * resolved font. The list offers the catalog (opened full, virtualized), Enter
136
+ * applies any name — one outside the list included — and, while the list is
137
+ * open, typing filters it (the input doubles as a search field). The input
138
+ * mirrors the selection; each render keeps it in sync.
139
+ */
140
+ export function fontFamilyControl(env, id, tools) {
141
+ const { store, label } = env;
142
+ const { i18n } = tools;
143
+ const selectedFont = () => editState(store.getState()).selectionText.font;
144
+ // Re-render the open font list. Set while a list is open, so a specimen
145
+ // landing from the engine can put itself on screen; cleared on close, so a
146
+ // late arrival for a closed list does nothing.
147
+ let refreshFontMenu = null;
148
+ const unlisten = tools.onSpecimens(() => refreshFontMenu?.());
149
+ // Render the font combobox's list (a listbox) — one row per family name,
150
+ // the current pick checked. Every listed family is pickable whether or not
151
+ // its bytes are here yet: the engine shapes a still-downloading family in a
152
+ // stand-in and re-shapes when it lands, so the row has no load state to
153
+ // show. The catalog can run to ~2000 families, so the list is **filtered by
154
+ // `query` and virtualized**: a full-height sizer the menu scrolls, with only
155
+ // the visible rows (plus overscan) mounted, each absolutely positioned.
156
+ //
157
+ // The list is in **two sections**: the families already loaded, then a
158
+ // divider, then the rest of the catalog. Both sections are filtered by the
159
+ // same query, so a section can empty out and the divider goes with it. The
160
+ // divider is a border on the first row below it rather than a row of its
161
+ // own — a row of its own would have to carry a height through the virtual
162
+ // list's index-to-offset arithmetic, and this needs nothing from it.
163
+ const renderFontMenu = (dd, query, keepScroll = false) => {
164
+ const state = editState(store.getState());
165
+ // A full rebuild replaces the scroll viewport, so where the list was has
166
+ // to be carried across by hand. `keepScroll` marks the renders that are
167
+ // *not* a new list — a specimen arriving mid-scroll — where jumping back
168
+ // to the selection would yank the rows out from under the pointer.
169
+ const wasAt = dd.menu.querySelector(".udoc-font-scroll")?.scrollTop ?? 0;
170
+ const q = query.trim().toLowerCase();
171
+ const matches = q
172
+ ? state.fontFamilies.filter((family) => matchesFontQuery(family.name, q))
173
+ : state.fontFamilies;
174
+ // A Set because this runs over the whole ~2000-family list.
175
+ const here = new Set(state.loadedFontFamilyNames);
176
+ const loaded = matches.filter((family) => here.has(family.name));
177
+ const rest = matches.filter((family) => !here.has(family.name));
178
+ // Every match — the virtual list mounts only the visible rows, so the
179
+ // whole (unfiltered) ~2000-family catalog scrolls just as cheaply.
180
+ const shown = [...loaded, ...rest];
181
+ // Where the second section starts, so its first row draws the divider.
182
+ // -1 when either section is empty: nothing to divide.
183
+ const dividerAt = loaded.length > 0 && rest.length > 0 ? loaded.length : -1;
184
+ const selected = state.selectionText.font;
185
+ dd.menu.classList.add("udoc-font-menu");
186
+ dd.menu.innerHTML = "";
187
+ if (shown.length === 0) {
188
+ const empty = document.createElement("div");
189
+ empty.className = "udoc-menu__empty";
190
+ empty.textContent = i18n.t("ribbon.noMatchingFonts");
191
+ dd.menu.appendChild(empty);
192
+ return;
193
+ }
194
+ // The scroll viewport is a child of the panel, not the panel — the
195
+ // panel's rounded corners have to clip the native scrollbar. See
196
+ // udoc-font-scroll in styles.ts.
197
+ const viewport = document.createElement("div");
198
+ viewport.className = "udoc-font-scroll";
199
+ const sizer = document.createElement("div");
200
+ sizer.className = "udoc-font-sizer";
201
+ sizer.style.height = `${shown.length * FONT_ROW_H}px`;
202
+ viewport.appendChild(sizer);
203
+ dd.menu.appendChild(viewport);
204
+ // Mount only the rows in view (+ overscan); recycle on scroll.
205
+ const loader = tools.fontPreviews;
206
+ const renderWindow = () => {
207
+ const scrollTop = viewport.scrollTop;
208
+ const viewH = viewport.clientHeight || FONT_ROW_H * FONT_MENU_ROWS;
209
+ const first = Math.max(0, Math.floor(scrollTop / FONT_ROW_H) - FONT_OVERSCAN);
210
+ const last = Math.min(shown.length, Math.ceil((scrollTop + viewH) / FONT_ROW_H) + FONT_OVERSCAN);
211
+ sizer.replaceChildren();
212
+ for (let i = first; i < last; i++) {
213
+ const family = shown[i];
214
+ const name = family.name;
215
+ const item = document.createElement("button");
216
+ item.className =
217
+ i === dividerAt
218
+ ? "udoc-menu__item udoc-font-item udoc-font-item--section"
219
+ : "udoc-menu__item udoc-font-item";
220
+ item.setAttribute("role", "option");
221
+ // The label is the family drawn in its own face, so the row's
222
+ // accessible name has to be stated rather than read off the text
223
+ // — for an engine specimen there is no text to read.
224
+ item.setAttribute("aria-label", name);
225
+ item.title = name;
226
+ item.style.top = `${i * FONT_ROW_H}px`;
227
+ const isSelected = name === selected;
228
+ item.classList.toggle("udoc-menu__item--selected", isSelected);
229
+ item.setAttribute("aria-selected", String(isSelected));
230
+ item.innerHTML = `<span class="udoc-menu__item-label"></span>`;
231
+ fillFontLabel(item.lastElementChild, family, loader);
232
+ item.addEventListener("click", (e) => {
233
+ e.stopPropagation();
234
+ // The trigger mirrors the selection, so it updates when the
235
+ // resulting report lands.
236
+ void tools.text.fontFamilySelection(name);
237
+ dd.controller.close();
238
+ });
239
+ sizer.appendChild(item);
240
+ }
241
+ // Ask for the window's specimens once the rows are up, so the list
242
+ // is on screen before any of this starts. Families already worked
243
+ // on are skipped inside, which is what makes calling this on every
244
+ // scroll step cheap.
245
+ loader.request(shown.slice(first, last).map((f) => ({ family: f.name, sourceType: f.sourceType })));
246
+ };
247
+ viewport.onscroll = renderWindow;
248
+ // On open (no filter), reveal the current selection; on a filter, top.
249
+ const selectedIndex = q ? -1 : shown.findIndex((family) => family.name === selected);
250
+ viewport.scrollTop = keepScroll
251
+ ? wasAt
252
+ : selectedIndex > 0
253
+ ? Math.max(0, selectedIndex * FONT_ROW_H - (FONT_ROW_H * FONT_MENU_ROWS) / 2)
254
+ : 0;
255
+ renderWindow();
256
+ };
257
+ // What the open list is filtered by — deliberately *not* the input's value.
258
+ // The input mirrors the selection, so on open it holds the font already
259
+ // applied; filtering by that would narrow the list to the one row you
260
+ // started from. It becomes a query only once typed into, and opening clears
261
+ // it again.
262
+ let fontQuery = "";
263
+ const combo = createComboBox({ label, className: "udoc-font-select" }, {
264
+ menuLabel: label,
265
+ onOpen: () => {
266
+ fontQuery = "";
267
+ // Held while the list is up so a specimen arriving from the
268
+ // engine can put itself on screen (it lands a round trip after
269
+ // the row was mounted).
270
+ refreshFontMenu = () => renderFontMenu(combo, fontQuery, true);
271
+ // Render from what the store already holds, then again when the
272
+ // refreshed loaded set lands — rendering only on the reply would
273
+ // blank the list for a round trip, and never re-rendering would
274
+ // leave a font loaded since the last open below the divider.
275
+ renderFontMenu(combo, fontQuery);
276
+ void tools.refreshLoadedFontFamilies().then(() => {
277
+ if (combo.controller.isOpen)
278
+ renderFontMenu(combo, fontQuery);
279
+ });
280
+ },
281
+ onClose: () => {
282
+ refreshFontMenu = null;
283
+ },
284
+ });
285
+ combo.input.dataset.command = id;
286
+ mirrorField(combo.input, selectedFont());
287
+ wireComboInput(combo, (raw) => {
288
+ const name = raw.trim();
289
+ if (name)
290
+ void tools.text.fontFamilySelection(name);
291
+ }, () => mirrorField(combo.input, selectedFont()));
292
+ // Type-to-filter: narrow the open list as the user types (closed, typing
293
+ // just edits the value the mirror will restore).
294
+ combo.input.addEventListener("input", () => {
295
+ if (!combo.controller.isOpen)
296
+ return;
297
+ fontQuery = combo.input.value;
298
+ renderFontMenu(combo, fontQuery);
299
+ });
300
+ return {
301
+ el: combo.el,
302
+ sync: (state) => mirrorField(combo.input, editState(state).selectionText.font),
303
+ close: () => combo.controller.close(),
304
+ destroy: () => {
305
+ unlisten();
306
+ combo.destroy();
307
+ },
308
+ };
309
+ }
310
+ /**
311
+ * The Text tab's size field: a typeable combobox showing the selection's
312
+ * resolved point size, its list the preset sizes (rebuilt on open so the check
313
+ * tracks the selection). Enter applies any point size in range — one outside
314
+ * the presets included. Mirror-only like the font picker.
315
+ */
316
+ export function fontSizeControl(env, id, tools) {
317
+ const { store, label } = env;
318
+ const selectedSize = () => editState(store.getState()).selectionText.size;
319
+ // Fill the list (a listbox) — one option per preset point size, the one
320
+ // matching the selection marked selected. Like the font picker the input is
321
+ // a mirror, so it updates when the resulting selection report lands, not on
322
+ // the click.
323
+ const fillSizes = (dd) => {
324
+ dd.menu.innerHTML = "";
325
+ for (const size of SIZE_PRESETS) {
326
+ const item = document.createElement("button");
327
+ item.className = "udoc-menu__item udoc-size-item";
328
+ item.setAttribute("role", "option");
329
+ const selected = String(size) === selectedSize();
330
+ item.classList.toggle("udoc-menu__item--selected", selected);
331
+ item.setAttribute("aria-selected", String(selected));
332
+ item.innerHTML = `<span class="udoc-menu__item-label">${size}</span>`;
333
+ item.addEventListener("click", (e) => {
334
+ e.stopPropagation();
335
+ void tools.text.fontSizeSelection(size);
336
+ dd.controller.close();
337
+ });
338
+ dd.menu.appendChild(item);
339
+ }
340
+ };
341
+ const combo = createComboBox({ label, className: "udoc-size-select", inputMode: "decimal" }, { menuLabel: label, onOpen: () => fillSizes(combo) });
342
+ combo.input.dataset.command = id;
343
+ mirrorField(combo.input, selectedSize());
344
+ wireComboInput(combo, (raw) => {
345
+ const size = parseFloat(raw);
346
+ // Office's point-size range; an out-of-range or non-numeric entry is
347
+ // dropped and the mirror restores the real value.
348
+ if (!isNaN(size) && size >= 1 && size <= 4000)
349
+ void tools.text.fontSizeSelection(size);
350
+ }, () => mirrorField(combo.input, selectedSize()));
351
+ return {
352
+ el: combo.el,
353
+ sync: (state) => mirrorField(combo.input, editState(state).selectionText.size),
354
+ close: () => combo.controller.close(),
355
+ destroy: () => combo.destroy(),
356
+ };
357
+ }
358
+ /**
359
+ * A character toggle (Bold / Italic / Underline / …): an icon button
360
+ * reflecting whether the selection carries that format (on / off / mixed). A
361
+ * mixed / off selection turns the format on, a fully-on one turns it off (the
362
+ * Office gesture).
363
+ */
364
+ export function textFormatControl(env, command, tools) {
365
+ const { property } = command;
366
+ const btn = createButton({ icon: command.icon, label: env.label });
367
+ btn.dataset.command = command.id;
368
+ const sync = (state) => {
369
+ const format = editState(state).selectionText[property];
370
+ btn.classList.toggle("udoc-btn--selected", format !== "off");
371
+ btn.setAttribute("aria-pressed", textFormatAria(format));
372
+ };
373
+ sync(env.store.getState());
374
+ btn.addEventListener("click", () => {
375
+ void tools.text.formatTextSelection(property, editState(env.store.getState()).selectionText[property] !== "on");
376
+ });
377
+ return { el: btn, sync };
378
+ }
379
+ /**
380
+ * A Bullets / Numbering split button: the main region toggles the selection's
381
+ * paragraphs into the last-picked style (or out of the list when it's already
382
+ * lit), the caret opens the style gallery. A mirror like the character toggles
383
+ * — the lit state re-derives from the selection each render, not from the click.
384
+ */
385
+ export function listControl(env, command, tools) {
386
+ const { i18n, lastListStyle } = tools;
387
+ const { list } = command;
388
+ const listed = () => editState(env.store.getState()).selectionText[list];
389
+ // Fill the gallery — one row per marker style. No row is marked: the
390
+ // selection reports only whether its paragraphs are in a list, not which
391
+ // style, and the last pick the main region re-applies is not a value the
392
+ // selection holds. The row's sample is the markers themselves ("1. 2. 3."),
393
+ // not a glyph: the gallery's whole question is what the marker *looks
394
+ // like*, which a drawn icon would only paraphrase. Picking applies the
395
+ // style and remembers it for the main region; the button's lit state still
396
+ // comes from the selection report that follows.
397
+ const fillListStyles = () => {
398
+ split.menu.innerHTML = "";
399
+ for (const style of listStyles(list)) {
400
+ const item = document.createElement("button");
401
+ item.className = "udoc-menu__item udoc-list-item";
402
+ item.setAttribute("role", "menuitem");
403
+ const label = i18n.t(style.label);
404
+ item.setAttribute("aria-label", label);
405
+ item.title = label;
406
+ item.innerHTML = `<span class="udoc-menu__item-label udoc-list-sample">${style.preview}</span>`;
407
+ item.addEventListener("click", (e) => {
408
+ e.stopPropagation();
409
+ lastListStyle[list] = style;
410
+ void tools.text.listSelection(style);
411
+ split.controller.close();
412
+ });
413
+ split.menu.appendChild(item);
414
+ }
415
+ };
416
+ const split = createSplitButton({
417
+ mainLabel: env.label,
418
+ mainIcon: command.icon,
419
+ caretLabel: i18n.t("ribbon.listOptions", { label: env.label }),
420
+ menuLabel: i18n.t("ribbon.listStyles", { label: env.label }),
421
+ onOpen: fillListStyles,
422
+ });
423
+ split.main.dataset.command = command.id;
424
+ split.caret.dataset.command = command.id;
425
+ split.setActive(listed() !== "off");
426
+ split.main.addEventListener("click", () => {
427
+ // Lit means every touched paragraph is already in this kind of list, so
428
+ // the button's job is to take them out; mixed and off both put them in
429
+ // (the Office gesture, matching Bold).
430
+ const on = listed() === "on";
431
+ void tools.text.listSelection(on ? null : lastListStyle[list]);
432
+ });
433
+ return {
434
+ el: split.el,
435
+ // Both halves carry the command id (so each greys with it), but the lit
436
+ // state belongs to the pair — set it on the wrapper they share.
437
+ sync: (state) => {
438
+ const format = editState(state).selectionText[list];
439
+ split.el.classList.toggle("udoc-split--selected", format !== "off");
440
+ split.main.setAttribute("aria-pressed", textFormatAria(format));
441
+ },
442
+ close: () => split.controller.close(),
443
+ destroy: () => split.destroy(),
444
+ };
445
+ }
446
+ /** The style each list button applies when its main region is clicked — the
447
+ * gallery's first entry until one is picked, that pick from then on (the
448
+ * split-button contract: the caret chooses, the main repeats). */
449
+ export function createLastListStyle() {
450
+ return { bulleted: listStyles("bulleted")[0], numbered: listStyles("numbered")[0] };
451
+ }
452
+ //# sourceMappingURL=TextControls.js.map