smartrte-react 0.3.5 → 1.0.0-beta.10

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 (147) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +205 -14
  3. package/dist/adapters/domChecklistCommandBridge.js +1 -1
  4. package/dist/adapters/domCommandBridge.d.ts +2 -2
  5. package/dist/adapters/domCommandBridge.js +2 -2
  6. package/dist/adapters/domInlineAtomCommandBridge.d.ts +10 -9
  7. package/dist/adapters/domInlineAtomCommandBridge.js +154 -50
  8. package/dist/adapters/domInlineImageCommandBridge.d.ts +7 -7
  9. package/dist/adapters/domInlineImageCommandBridge.js +6 -72
  10. package/dist/adapters/domMoveCommandBridge.js +1 -1
  11. package/dist/adapters/domSelectionBridge.d.ts +3 -3
  12. package/dist/adapters/domSmartDocument.d.ts +4 -4
  13. package/dist/adapters/domSmartDocument.js +2 -2
  14. package/dist/adapters/domTableCommandBridge.d.ts +42 -16
  15. package/dist/adapters/domTableCommandBridge.js +203 -173
  16. package/dist/adapters/internalFlags.d.ts +0 -8
  17. package/dist/adapters/internalFlags.js +0 -24
  18. package/dist/adapters/legacyListShadowComparator.d.ts +1 -1
  19. package/dist/adapters/legacyListShadowComparator.js +1 -1
  20. package/dist/adapters/pdfPrint.d.ts +8 -0
  21. package/dist/adapters/pdfPrint.js +22 -0
  22. package/dist/adapters/shadowMode.d.ts +3 -3
  23. package/dist/adapters/shadowMode.js +1 -1
  24. package/dist/canonicalEditorRuntime.d.ts +65 -1
  25. package/dist/canonicalEditorRuntime.js +63 -5
  26. package/dist/capabilityPresets.d.ts +27 -0
  27. package/dist/capabilityPresets.js +21 -0
  28. package/dist/collabTransport.d.ts +66 -0
  29. package/dist/collabTransport.js +1 -0
  30. package/dist/commentProvider.d.ts +24 -0
  31. package/dist/commentProvider.js +1 -0
  32. package/dist/components/CanonicalAuthorityEditor.d.ts +56 -1
  33. package/dist/components/CanonicalAuthorityEditor.js +1304 -116
  34. package/dist/components/ClassicEditorAuthority.d.ts +93 -22
  35. package/dist/components/ClassicEditorAuthority.js +11 -43
  36. package/dist/components/ColorPickerPopover.d.ts +39 -0
  37. package/dist/components/ColorPickerPopover.js +287 -0
  38. package/dist/components/CommentMarkers.d.ts +25 -0
  39. package/dist/components/CommentMarkers.js +105 -0
  40. package/dist/components/CommentThreadPanel.d.ts +26 -0
  41. package/dist/components/CommentThreadPanel.js +47 -0
  42. package/dist/components/ContextMenu.d.ts +23 -0
  43. package/dist/components/ContextMenu.js +141 -0
  44. package/dist/components/FormulaLibraryPopover.d.ts +16 -0
  45. package/dist/components/FormulaLibraryPopover.js +107 -0
  46. package/dist/components/LinkEditorPopover.d.ts +3 -1
  47. package/dist/components/LinkEditorPopover.js +22 -6
  48. package/dist/components/MediaDetailsPopover.d.ts +35 -0
  49. package/dist/components/MediaDetailsPopover.js +136 -0
  50. package/dist/components/MediaManager.d.ts +4 -20
  51. package/dist/components/MediaManager.js +40 -11
  52. package/dist/components/MediaOverlay.d.ts +27 -0
  53. package/dist/components/MediaOverlay.js +228 -0
  54. package/dist/components/SpecialCharacterPopover.d.ts +18 -0
  55. package/dist/components/SpecialCharacterPopover.js +99 -0
  56. package/dist/components/StructuralSuggestionMarkers.d.ts +20 -0
  57. package/dist/components/StructuralSuggestionMarkers.js +56 -0
  58. package/dist/components/SuggestionPanel.d.ts +26 -0
  59. package/dist/components/SuggestionPanel.js +33 -0
  60. package/dist/components/TableBorderPopover.d.ts +59 -0
  61. package/dist/components/TableBorderPopover.js +149 -0
  62. package/dist/components/TableResizeHandles.d.ts +25 -0
  63. package/dist/components/TableResizeHandles.js +323 -0
  64. package/dist/components/TableSizePickerPopover.d.ts +16 -0
  65. package/dist/components/TableSizePickerPopover.js +126 -0
  66. package/dist/components/ToolbarPrimitives.d.ts +161 -0
  67. package/dist/components/ToolbarPrimitives.js +237 -0
  68. package/dist/components/VersionHistoryPanel.d.ts +9 -0
  69. package/dist/components/VersionHistoryPanel.js +157 -0
  70. package/dist/components/fixedPositioning.d.ts +25 -0
  71. package/dist/components/fixedPositioning.js +42 -0
  72. package/dist/formulaLibrary.d.ts +31 -0
  73. package/dist/formulaLibrary.js +90 -0
  74. package/dist/index.d.ts +25 -24
  75. package/dist/index.js +15 -11
  76. package/dist/mediaManagerAdapter.d.ts +12 -0
  77. package/dist/mediaManagerAdapter.js +34 -0
  78. package/dist/mediaProvider.d.ts +21 -1
  79. package/dist/specialCharacters.d.ts +22 -0
  80. package/dist/specialCharacters.js +86 -0
  81. package/dist/standalone/classic-editor-embed.js +19 -12
  82. package/dist/suggestionProvider.d.ts +17 -0
  83. package/dist/suggestionProvider.js +1 -0
  84. package/dist/theme.d.ts +1 -1
  85. package/dist/theme.js +219 -30
  86. package/dist/toolbarTools.d.ts +95 -0
  87. package/dist/toolbarTools.js +17 -0
  88. package/dist/versionProvider.d.ts +29 -0
  89. package/dist/versionProvider.js +1 -0
  90. package/package.json +28 -14
  91. package/dist/adapters/canonicalInlineCommandBridge.d.ts +0 -15
  92. package/dist/adapters/canonicalInlineCommandBridge.js +0 -269
  93. package/dist/adapters/canonicalListCommandBridge.d.ts +0 -26
  94. package/dist/adapters/canonicalListCommandBridge.js +0 -430
  95. package/dist/adapters/coreBold.d.ts +0 -5
  96. package/dist/adapters/coreBold.js +0 -3
  97. package/dist/adapters/documentFormats.d.ts +0 -26
  98. package/dist/adapters/documentFormats.js +0 -49
  99. package/dist/adapters/docxFormat.d.ts +0 -5
  100. package/dist/adapters/docxFormat.js +0 -272
  101. package/dist/adapters/domBlockCommandBridge.d.ts +0 -19
  102. package/dist/adapters/domBlockCommandBridge.js +0 -79
  103. package/dist/adapters/inlineMarkCoreExecution.d.ts +0 -9
  104. package/dist/adapters/inlineMarkCoreExecution.js +0 -25
  105. package/dist/adapters/pdfFormat.d.ts +0 -23
  106. package/dist/adapters/pdfFormat.js +0 -204
  107. package/dist/adapters/portableDocxAtoms.d.ts +0 -4
  108. package/dist/adapters/portableDocxAtoms.js +0 -58
  109. package/dist/adapters/styledDocxFormat.d.ts +0 -8
  110. package/dist/adapters/styledDocxFormat.js +0 -190
  111. package/dist/builtInFormatDefinitions.d.ts +0 -5
  112. package/dist/builtInFormatDefinitions.js +0 -82
  113. package/dist/canonicalAuthorityFlag.d.ts +0 -32
  114. package/dist/canonicalAuthorityFlag.js +0 -49
  115. package/dist/canonicalClipboardRuntime.d.ts +0 -15
  116. package/dist/canonicalClipboardRuntime.js +0 -76
  117. package/dist/components/ClassicEditor.d.ts +0 -76
  118. package/dist/components/ClassicEditor.js +0 -6729
  119. package/dist/editorController.d.ts +0 -78
  120. package/dist/editorController.js +0 -298
  121. package/dist/formatFidelity.d.ts +0 -14
  122. package/dist/formatFidelity.js +0 -107
  123. package/dist/formatRuntime.d.ts +0 -50
  124. package/dist/formatRuntime.js +0 -20
  125. package/dist/pluginRuntime.d.ts +0 -68
  126. package/dist/pluginRuntime.js +0 -78
  127. package/dist/standalone/editor.js +0 -1068
  128. package/dist/test-harness/atomShadowComparator.d.ts +0 -14
  129. package/dist/test-harness/atomShadowComparator.js +0 -63
  130. package/dist/test-harness/blockShadowComparator.d.ts +0 -30
  131. package/dist/test-harness/blockShadowComparator.js +0 -140
  132. package/dist/test-harness/clipboardShadowComparator.d.ts +0 -12
  133. package/dist/test-harness/clipboardShadowComparator.js +0 -46
  134. package/dist/test-harness/inlineShadowComparator.d.ts +0 -32
  135. package/dist/test-harness/inlineShadowComparator.js +0 -152
  136. package/dist/test-harness/legacyAtomEngine.d.ts +0 -10
  137. package/dist/test-harness/legacyAtomEngine.js +0 -15
  138. package/dist/test-harness/legacyBlockEngine.d.ts +0 -31
  139. package/dist/test-harness/legacyBlockEngine.js +0 -31
  140. package/dist/test-harness/legacyClipboardEngine.d.ts +0 -14
  141. package/dist/test-harness/legacyClipboardEngine.js +0 -67
  142. package/dist/test-harness/legacyInlineEngine.d.ts +0 -19
  143. package/dist/test-harness/legacyInlineEngine.js +0 -49
  144. package/dist/test-harness/legacyTableEngine.d.ts +0 -12
  145. package/dist/test-harness/legacyTableEngine.js +0 -25
  146. package/dist/test-harness/tableShadowComparator.d.ts +0 -29
  147. package/dist/test-harness/tableShadowComparator.js +0 -101
@@ -0,0 +1,237 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ import { AlignCenter, AlignJustify, AlignLeft, AlignRight, AlignVerticalSpaceAround, ALargeSmall, Baseline, Bold, CheckSquare, ChevronDown, Code, Columns3, Download, Eye, FileJson, History, Image as ImageIcon, IndentDecrease, IndentIncrease, Italic, Link2, ListChecks, ListOrdered, ListRestart, List as ListIcon, MessageSquare, MessageSquarePlus, MoreHorizontal, MoveDown, MoveLeft, MoveRight, MoveUp, Music, Omega, PanelTop, PenLine, Pilcrow, Quote, Redo2, Rows3, ScissorsLineDashed, SeparatorHorizontal, Square, Sigma, Strikethrough, Subscript, Superscript, Table2, Trash2, Type, Underline, Undo2, Unlink2, Upload, Video, Pencil, ZoomIn, ZoomOut, } from "lucide-react";
4
+ import { getFixedPositioningOrigin } from "./fixedPositioning.js";
5
+ /**
6
+ * The toolbar's icon set (Phase: Direction B toolbar redesign, 2026-08-29).
7
+ * Lucide, per the design proposal's §11 recommendation - MIT-licensed,
8
+ * tree-shakeable (only these ~40 icons ship), a consistent stroke style
9
+ * that reads at the toolbar's small render size. Two icons here
10
+ * (mergeCells/splitCell) have no Lucide equivalent and are hand-drawn to
11
+ * match Lucide's own stroke weight/corner radius - the "grid + directional
12
+ * arrow" concept the owner confirmed from the proposal's two alternatives.
13
+ */
14
+ const stroke = { width: 17, height: 17, strokeWidth: 1.9 };
15
+ const MergeCellsIcon = () => (_jsxs("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.9", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }), _jsx("line", { x1: "15", y1: "4", x2: "15", y2: "20" }), _jsx("path", { d: "M6 12h5" }), _jsx("polyline", { points: "9 9 12 12 9 15" })] }));
16
+ const SplitCellIcon = () => (_jsxs("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.9", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }), _jsx("line", { x1: "9", y1: "4", x2: "9", y2: "20" }), _jsx("path", { d: "M18 9v6" }), _jsx("polyline", { points: "20 10.5 18 12 20 13.5" }), _jsx("polyline", { points: "16 10.5 18 12 16 13.5" })] }));
17
+ export const toolbarIcons = {
18
+ bold: _jsx(Bold, { ...stroke }),
19
+ italic: _jsx(Italic, { ...stroke }),
20
+ underline: _jsx(Underline, { ...stroke }),
21
+ strikethrough: _jsx(Strikethrough, { ...stroke }),
22
+ code: _jsx(Code, { ...stroke }),
23
+ superscript: _jsx(Superscript, { ...stroke }),
24
+ subscript: _jsx(Subscript, { ...stroke }),
25
+ textColor: _jsx(Baseline, { ...stroke }),
26
+ backgroundColor: _jsx(PenLine, { ...stroke }),
27
+ fontSize: _jsx(ALargeSmall, { ...stroke }),
28
+ fontFamily: _jsx(Type, { ...stroke }),
29
+ paragraphStyle: _jsx(Pilcrow, { ...stroke }),
30
+ alignLeft: _jsx(AlignLeft, { ...stroke }),
31
+ alignCenter: _jsx(AlignCenter, { ...stroke }),
32
+ alignRight: _jsx(AlignRight, { ...stroke }),
33
+ alignJustify: _jsx(AlignJustify, { ...stroke }),
34
+ quote: _jsx(Quote, { ...stroke }),
35
+ moveUp: _jsx(MoveUp, { ...stroke }),
36
+ moveDown: _jsx(MoveDown, { ...stroke }),
37
+ moveLeft: _jsx(MoveLeft, { ...stroke }),
38
+ moveRight: _jsx(MoveRight, { ...stroke }),
39
+ indent: _jsx(IndentIncrease, { ...stroke }),
40
+ outdent: _jsx(IndentDecrease, { ...stroke }),
41
+ link: _jsx(Link2, { ...stroke }),
42
+ unlink: _jsx(Unlink2, { ...stroke }),
43
+ bulletedList: _jsx(ListIcon, { ...stroke }),
44
+ numberedList: _jsx(ListOrdered, { ...stroke }),
45
+ checklist: _jsx(ListChecks, { ...stroke }),
46
+ checkSquare: _jsx(CheckSquare, { ...stroke }),
47
+ restart: _jsx(ListRestart, { ...stroke }),
48
+ continueNumbering: _jsx(Rows3, { ...stroke }),
49
+ table: _jsx(Table2, { ...stroke }),
50
+ mergeCells: _jsx(MergeCellsIcon, {}),
51
+ splitCell: _jsx(SplitCellIcon, {}),
52
+ addRow: _jsx(Rows3, { ...stroke }),
53
+ addColumn: _jsx(Columns3, { ...stroke }),
54
+ headerRow: _jsx(PanelTop, { ...stroke }),
55
+ deleteTable: _jsx(Trash2, { ...stroke }),
56
+ image: _jsx(ImageIcon, { ...stroke }),
57
+ video: _jsx(Video, { ...stroke }),
58
+ audio: _jsx(Music, { ...stroke }),
59
+ formula: _jsx(Sigma, { ...stroke }),
60
+ specialChar: _jsx(Omega, { ...stroke }),
61
+ cellBorder: _jsx(Square, { ...stroke }),
62
+ edit: _jsx(Pencil, { ...stroke }),
63
+ zoomIn: _jsx(ZoomIn, { ...stroke }),
64
+ zoomOut: _jsx(ZoomOut, { ...stroke }),
65
+ delete: _jsx(Trash2, { ...stroke }),
66
+ import: _jsx(Upload, { ...stroke }),
67
+ saveCopy: _jsx(Download, { ...stroke }),
68
+ json: _jsx(FileJson, { ...stroke }),
69
+ history: _jsx(History, { ...stroke }),
70
+ addComment: _jsx(MessageSquarePlus, { ...stroke }),
71
+ comments: _jsx(MessageSquare, { ...stroke }),
72
+ suggest: _jsx(PenLine, { ...stroke }),
73
+ suggestions: _jsx(MessageSquare, { ...stroke }),
74
+ showEdits: _jsx(Eye, { ...stroke }),
75
+ undo: _jsx(Undo2, { ...stroke }),
76
+ redo: _jsx(Redo2, { ...stroke }),
77
+ more: _jsx(MoreHorizontal, { ...stroke }),
78
+ divider: _jsx(SeparatorHorizontal, { ...stroke }),
79
+ pageBreak: _jsx(ScissorsLineDashed, { ...stroke }),
80
+ lineHeight: _jsx(AlignVerticalSpaceAround, { ...stroke }),
81
+ };
82
+ /** A single always-visible toolbar control: icon + plain-language label together, per the redesign's core principle. */
83
+ export function ToolbarButton({ icon, label, ariaLabel, pressed, disabled, title, onClick, iconOnly, widePromote }) {
84
+ return _jsxs("button", { type: "button", className: "srte-tool-button", "aria-label": ariaLabel ?? label, "aria-pressed": pressed, title: title ?? ariaLabel ?? label, disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: onClick, ...(widePromote ? { "data-srte-wide-promote": "true" } : {}), children: [toolbarIcons[icon], !iconOnly && _jsx("span", { children: label })] });
85
+ }
86
+ /** One row inside an open dropdown menu - closes the dropdown after acting, matching ContextMenu.tsx's own dismiss-on-action convention. */
87
+ export function ToolbarMenuItem({ icon, label, ariaLabel, pressed, disabled, title, onClick, widePromote }) {
88
+ return _jsxs("button", { type: "button", className: "srte-menu-item", role: "menuitem", "aria-label": ariaLabel ?? label, "aria-pressed": pressed, title: title, disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: (event) => {
89
+ onClick(event);
90
+ event.currentTarget.closest("details")?.removeAttribute("open");
91
+ }, ...(widePromote ? { "data-srte-wide-promote": "true" } : {}), children: [icon ? toolbarIcons[icon] : null, _jsx("span", { children: label }), pressed === true && _jsx("span", { className: "srte-menu-check", "aria-hidden": "true", children: "\u2713" }), pressed === "mixed" && _jsx("span", { className: "srte-menu-check", "aria-hidden": "true", children: "~" })] });
92
+ }
93
+ /**
94
+ * Closes an open `<details>` when the user clicks anywhere outside it -
95
+ * native `<details>` does *not* do this on its own (confirmed directly:
96
+ * unlike a real modal, it only ever closes via its own `<summary>` or a
97
+ * script setting `open = false`), so a dropdown left open and then clicked
98
+ * past straight into the editor previously just stayed open. Same
99
+ * pointerdown+mousedown, capture-phase, ref-read pattern as
100
+ * ContextMenu.tsx/ColorPickerPopover.tsx's own outside-click dismiss -
101
+ * mounted once for the element's lifetime regardless of open/closed state,
102
+ * since `<details>` (unlike those components) isn't unmounted when closed.
103
+ */
104
+ const useDismissDetailsOnOutsideClick = (ref) => {
105
+ useEffect(() => {
106
+ const dismissIfOutside = (event) => {
107
+ const el = ref.current;
108
+ if (el?.open && !el.contains(event.target))
109
+ el.removeAttribute("open");
110
+ };
111
+ window.addEventListener("pointerdown", dismissIfOutside, true);
112
+ window.addEventListener("mousedown", dismissIfOutside, true);
113
+ return () => {
114
+ window.removeEventListener("pointerdown", dismissIfOutside, true);
115
+ window.removeEventListener("mousedown", dismissIfOutside, true);
116
+ };
117
+ }, [ref]);
118
+ };
119
+ /**
120
+ * A labeled dropdown for a group's long-tail tools - wires up the
121
+ * `.srte-toolbar-menu`/`.srte-menu` CSS `theme.ts` already defines but the
122
+ * pre-redesign toolbar never used. `<details>/<summary>` gives free
123
+ * keyboard support (Enter/Space to open, native focus handling) without a
124
+ * new open/close state to manage; outside-click dismiss is handled by
125
+ * useDismissDetailsOnOutsideClick above.
126
+ *
127
+ * The panel itself renders `position: fixed` with JS-measured coordinates
128
+ * (docs/bugs/toolbar-dropdown-clipped-by-host-overflow-hidden.md), the same
129
+ * pattern ColorPickerPopover/TableBorderPopover already use - a real host
130
+ * can (and, reported live, does) wrap the editor in an `overflow: hidden`
131
+ * container for its own layout reasons; a `position: absolute` panel
132
+ * anchored inside that container gets visually clipped the moment it needs
133
+ * to extend past the container's own bounds, which is exactly when a
134
+ * narrow host is most likely to need the dropdown in the first place.
135
+ * `position: fixed`'s containing block is normally the viewport, so this
136
+ * escapes ANY ancestor's overflow clipping regardless of DOM depth - unless
137
+ * an ancestor has its own transform/perspective/filter, which becomes the
138
+ * containing block instead (see fixedPositioning.ts, and
139
+ * docs/bugs/toolbar-menu-misplaced-inside-transformed-ancestor.md); the
140
+ * placement math below accounts for that case too.
141
+ */
142
+ export function ToolbarDropdown({ icon, label, priority, children }) {
143
+ const detailsRef = useRef(null);
144
+ useDismissDetailsOnOutsideClick(detailsRef);
145
+ const [placement, setPlacement] = useState(null);
146
+ useLayoutEffect(() => {
147
+ const details = detailsRef.current;
148
+ if (!details)
149
+ return;
150
+ const recompute = () => {
151
+ if (!details.open) {
152
+ setPlacement(null);
153
+ return;
154
+ }
155
+ const margin = 8;
156
+ const triggerRect = details.getBoundingClientRect();
157
+ const menu = details.querySelector(":scope > .srte-menu");
158
+ const menuWidth = menu?.offsetWidth ?? 210;
159
+ const menuHeight = menu?.offsetHeight ?? 0;
160
+ const viewportWidth = window.innerWidth;
161
+ const viewportHeight = window.innerHeight;
162
+ const left = Math.min(Math.max(margin, triggerRect.left), Math.max(margin, viewportWidth - menuWidth - margin));
163
+ const overflowsBottom = triggerRect.bottom + 6 + menuHeight > viewportHeight - margin;
164
+ const top = overflowsBottom ? Math.max(margin, triggerRect.top - menuHeight - 6) : triggerRect.bottom + 6;
165
+ const origin = getFixedPositioningOrigin(details);
166
+ setPlacement({ left: left - origin.left, top: top - origin.top });
167
+ };
168
+ recompute();
169
+ details.addEventListener("toggle", recompute);
170
+ return () => details.removeEventListener("toggle", recompute);
171
+ }, []);
172
+ return _jsxs("details", { ref: detailsRef, className: "srte-toolbar-menu", ...(priority ? { "data-srte-priority": priority } : {}), children: [_jsxs("summary", { className: "srte-tool-button srte-menu-trigger", onMouseDown: (event) => event.preventDefault(), children: [icon ? toolbarIcons[icon] : null, _jsx("span", { children: label }), _jsx(ChevronDown, { width: 13, height: 13, strokeWidth: 2, "aria-hidden": "true" })] }), _jsx("div", { className: "srte-menu", role: "menu", "data-srte-menu-fixed": "true", style: { left: placement?.left ?? 0, top: placement?.top ?? 0, visibility: placement ? "visible" : "hidden" }, children: children })] });
173
+ }
174
+ /** A visually-separated cluster of controls, optionally collapsing as a whole unit on narrow viewports (theme.ts's `.srte-toolbar-group[data-srte-priority="3"]`). */
175
+ export function ToolbarGroup({ priority, children }) {
176
+ return _jsx("span", { className: "srte-toolbar-group", ...(priority ? { "data-srte-priority": priority } : {}), children: children });
177
+ }
178
+ /**
179
+ * The narrow-viewport overflow trigger - `.srte-mobile-more` is CSS-hidden
180
+ * except under theme.ts's `max-width: 639px` rules, which also hide every
181
+ * `[data-srte-priority="2"]`/`[data-srte-priority="3"]` element. Its own
182
+ * menu statically lists every tool that lives inside one of those
183
+ * collapsing dropdowns/groups, so nothing becomes unreachable once the
184
+ * primary row starts shedding groups - this is plain CSS visibility
185
+ * toggling of pre-rendered content, not JS media-query logic, matching how
186
+ * the rest of this responsive system already works.
187
+ *
188
+ * The panel itself renders `position: fixed` with JS-measured coordinates,
189
+ * the same fix and reasoning as `ToolbarDropdown` (docs/bugs/
190
+ * toolbar-dropdown-clipped-by-host-overflow-hidden.md) - this component was
191
+ * NOT covered by that fix at the time (incorrectly assumed to be
192
+ * unaffected since it already had its own separate mobile CSS), but it has
193
+ * the exact same `position: absolute` vulnerability to a host's own
194
+ * `overflow: hidden` container, and since this is the ONLY toolbar
195
+ * overflow affordance on narrow viewports (every ToolbarDropdown hides
196
+ * entirely below the mobile breakpoint), a clipped host is more likely to
197
+ * matter here, not less. Confirmed live: a real narrow host wrapping the
198
+ * editor in `overflow: hidden` computed this panel at `left: -214px` -
199
+ * mostly off-screen, functionally invisible despite being technically
200
+ * present in the DOM.
201
+ */
202
+ export function MobileMoreMenu({ children }) {
203
+ const detailsRef = useRef(null);
204
+ useDismissDetailsOnOutsideClick(detailsRef);
205
+ const [placement, setPlacement] = useState(null);
206
+ useLayoutEffect(() => {
207
+ const details = detailsRef.current;
208
+ if (!details)
209
+ return;
210
+ const recompute = () => {
211
+ if (!details.open) {
212
+ setPlacement(null);
213
+ return;
214
+ }
215
+ const margin = 8;
216
+ const triggerRect = details.getBoundingClientRect();
217
+ const menu = details.querySelector(":scope > .srte-menu");
218
+ const menuWidth = menu?.offsetWidth ?? Math.min(280, window.innerWidth - 16);
219
+ const menuHeight = menu?.offsetHeight ?? 0;
220
+ const viewportWidth = window.innerWidth;
221
+ const viewportHeight = window.innerHeight;
222
+ // Right-aligned to the trigger, matching this menu's original
223
+ // CSS-only `right: 0` intent - clamped into the viewport instead of
224
+ // being allowed to run off it or get clipped by an ancestor.
225
+ const preferredLeft = triggerRect.right - menuWidth;
226
+ const left = Math.min(Math.max(margin, preferredLeft), Math.max(margin, viewportWidth - menuWidth - margin));
227
+ const overflowsBottom = triggerRect.bottom + 6 + menuHeight > viewportHeight - margin;
228
+ const top = overflowsBottom ? Math.max(margin, triggerRect.top - menuHeight - 6) : triggerRect.bottom + 6;
229
+ const origin = getFixedPositioningOrigin(details);
230
+ setPlacement({ left: left - origin.left, top: top - origin.top });
231
+ };
232
+ recompute();
233
+ details.addEventListener("toggle", recompute);
234
+ return () => details.removeEventListener("toggle", recompute);
235
+ }, []);
236
+ return _jsxs("details", { ref: detailsRef, className: "srte-toolbar-menu srte-mobile-more", children: [_jsx("summary", { className: "srte-tool-button srte-menu-trigger", "aria-label": "More tools", onMouseDown: (event) => event.preventDefault(), children: toolbarIcons.more }), _jsx("div", { className: "srte-menu", role: "menu", "data-srte-menu-fixed": "true", style: { left: placement?.left ?? 0, top: placement?.top ?? 0, visibility: placement ? "visible" : "hidden" }, children: children })] });
237
+ }
@@ -0,0 +1,9 @@
1
+ import type { VersionProvider } from "../versionProvider.js";
2
+ import type { CanonicalEditorRuntime } from "../canonicalEditorRuntime.js";
3
+ export declare function VersionHistoryPanel(props: {
4
+ open: boolean;
5
+ onClose: () => void;
6
+ runtime: CanonicalEditorRuntime;
7
+ versionProvider: VersionProvider;
8
+ authorId?: string;
9
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,157 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { diffVersions, foundationSchema } from "smartrte-core/foundation";
4
+ const formatTimestamp = (createdAt) => new Date(createdAt).toLocaleString();
5
+ const summarize = (diff) => {
6
+ const parts = [
7
+ diff.added.length ? `${diff.added.length} added` : null,
8
+ diff.removed.length ? `${diff.removed.length} removed` : null,
9
+ diff.changed.length ? `${diff.changed.length} edited` : null,
10
+ ].filter((part) => Boolean(part));
11
+ return parts.length ? parts.join(", ") : "No differences";
12
+ };
13
+ export function VersionHistoryPanel(props) {
14
+ const { open, onClose, runtime, versionProvider, authorId } = props;
15
+ const [versions, setVersions] = useState([]);
16
+ const [loading, setLoading] = useState(false);
17
+ const [error, setError] = useState(null);
18
+ const [label, setLabel] = useState("");
19
+ const [saving, setSaving] = useState(false);
20
+ const [busyId, setBusyId] = useState(null);
21
+ // Selecting two entries computes a diff summary between them - order is
22
+ // the order they were selected in, not list order, so "swap" reads
23
+ // naturally when a user picks the later one first.
24
+ const [compareIds, setCompareIds] = useState([]);
25
+ const [diffSummary, setDiffSummary] = useState(null);
26
+ const [diffLoading, setDiffLoading] = useState(false);
27
+ const refresh = async () => {
28
+ setLoading(true);
29
+ setError(null);
30
+ try {
31
+ setVersions([...(await versionProvider.list())].sort((a, b) => b.createdAt - a.createdAt));
32
+ }
33
+ catch {
34
+ setError("Failed to load version history.");
35
+ }
36
+ finally {
37
+ setLoading(false);
38
+ }
39
+ };
40
+ useEffect(() => {
41
+ if (!open)
42
+ return;
43
+ setError(null);
44
+ setCompareIds([]);
45
+ setDiffSummary(null);
46
+ void refresh();
47
+ // eslint-disable-next-line react-hooks/exhaustive-deps
48
+ }, [open]);
49
+ useEffect(() => {
50
+ if (compareIds.length !== 2) {
51
+ setDiffSummary(null);
52
+ return;
53
+ }
54
+ let cancelled = false;
55
+ setDiffLoading(true);
56
+ (async () => {
57
+ try {
58
+ const [a, b] = await Promise.all(compareIds.map((id) => versionProvider.load(id)));
59
+ if (cancelled)
60
+ return;
61
+ setDiffSummary(summarize(diffVersions(a, b, foundationSchema)));
62
+ }
63
+ catch {
64
+ if (!cancelled)
65
+ setError("Failed to compare the selected versions.");
66
+ }
67
+ finally {
68
+ if (!cancelled)
69
+ setDiffLoading(false);
70
+ }
71
+ })();
72
+ return () => { cancelled = true; };
73
+ // eslint-disable-next-line react-hooks/exhaustive-deps
74
+ }, [compareIds]);
75
+ const handleSave = async () => {
76
+ setSaving(true);
77
+ setError(null);
78
+ try {
79
+ const version = runtime.saveVersion({ ...(label.trim() ? { label: label.trim() } : {}), ...(authorId ? { authorId } : {}) });
80
+ await versionProvider.save(version);
81
+ setLabel("");
82
+ await refresh();
83
+ }
84
+ catch {
85
+ setError("Failed to save this version.");
86
+ }
87
+ finally {
88
+ setSaving(false);
89
+ }
90
+ };
91
+ const handleRestore = async (entry) => {
92
+ setBusyId(entry.id);
93
+ setError(null);
94
+ try {
95
+ const target = await versionProvider.load(entry.id);
96
+ runtime.restoreVersion(target);
97
+ // Non-destructive restore (Phase 12a spec's explicit recommendation):
98
+ // record the restoration as a new, forward-moving version rather
99
+ // than silently rewriting/discarding anything - the version list
100
+ // only ever grows. Nothing between the current state and the
101
+ // restore target is ever deleted; every prior version, including
102
+ // whatever was live immediately before this restore, is still
103
+ // reachable by restoring to it directly.
104
+ const recorded = runtime.saveVersion({
105
+ label: `Restored: ${entry.label || formatTimestamp(entry.createdAt)}`,
106
+ ...(authorId ? { authorId } : {}),
107
+ });
108
+ await versionProvider.save(recorded);
109
+ await refresh();
110
+ }
111
+ catch {
112
+ setError("Failed to restore this version.");
113
+ }
114
+ finally {
115
+ setBusyId(null);
116
+ }
117
+ };
118
+ const handleDelete = async (entry) => {
119
+ setBusyId(entry.id);
120
+ setError(null);
121
+ try {
122
+ await versionProvider.remove(entry.id);
123
+ setCompareIds((current) => current.filter((id) => id !== entry.id));
124
+ await refresh();
125
+ }
126
+ catch {
127
+ setError("Failed to delete this version.");
128
+ }
129
+ finally {
130
+ setBusyId(null);
131
+ }
132
+ };
133
+ const toggleCompare = (id) => {
134
+ setCompareIds((current) => {
135
+ if (current.includes(id))
136
+ return current.filter((existing) => existing !== id);
137
+ if (current.length < 2)
138
+ return [...current, id];
139
+ return [current[1], id];
140
+ });
141
+ };
142
+ if (!open)
143
+ return null;
144
+ return (_jsx("div", { style: {
145
+ position: "fixed", inset: 0, background: "var(--srte-modal-backdrop)",
146
+ backdropFilter: "var(--srte-modal-backdrop-filter)", WebkitBackdropFilter: "var(--srte-modal-backdrop-filter)",
147
+ display: "flex", alignItems: "center", justifyContent: "center", zIndex: 80,
148
+ }, onClick: onClose, children: _jsxs("div", { role: "dialog", "aria-label": "Version history", "data-srte-version-history": "true", style: {
149
+ background: "var(--srte-modal-bg)", color: "var(--srte-modal-text)",
150
+ width: 560, maxWidth: "90vw", maxHeight: "86vh",
151
+ borderRadius: 10, boxShadow: "var(--srte-menu-shadow)",
152
+ display: "flex", flexDirection: "column",
153
+ }, onClick: (event) => event.stopPropagation(), children: [_jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", padding: "10px 14px", borderBottom: "1px solid var(--srte-border-light)" }, children: [_jsx("div", { style: { fontWeight: 600 }, children: "Version history" }), _jsx("button", { type: "button", "aria-label": "Close version history", onClick: onClose, children: "\u2715" })] }), error && _jsx("div", { style: { color: "var(--srte-danger)", padding: "8px 14px" }, children: error }), _jsxs("div", { style: { display: "flex", gap: 8, padding: "12px 14px", borderBottom: "1px solid var(--srte-border-light)" }, children: [_jsx("input", { value: label, onChange: (event) => setLabel(event.target.value), placeholder: "Label this version (optional)", style: { flex: 1, padding: "6px 8px", border: "1px solid var(--srte-border)", borderRadius: 6, background: "var(--srte-input-bg)", color: "var(--srte-input-text)" } }), _jsx("button", { type: "button", "data-srte-version-save": "true", disabled: saving, onClick: () => void handleSave(), children: saving ? "Saving…" : "Save version" })] }), compareIds.length === 2 && (_jsx("div", { style: { padding: "8px 14px", fontSize: 13, color: "var(--srte-text-muted)", borderBottom: "1px solid var(--srte-border-light)" }, "data-srte-version-diff-summary": "true", children: diffLoading ? "Comparing…" : diffSummary })), _jsxs("div", { style: { overflowY: "auto", padding: "8px 14px 14px" }, children: [loading && _jsx("div", { style: { color: "var(--srte-text-muted)", padding: "8px 0" }, children: "Loading\u2026" }), !loading && versions.length === 0 && _jsx("div", { style: { color: "var(--srte-text-muted)", padding: "8px 0" }, children: "No saved versions yet." }), versions.map((entry) => (_jsxs("div", { "data-srte-version-entry": entry.id, style: {
154
+ display: "flex", alignItems: "center", gap: 8, padding: "8px 0",
155
+ borderBottom: "1px solid var(--srte-border-light)",
156
+ }, children: [_jsx("input", { type: "checkbox", "aria-label": `Select ${entry.label || formatTimestamp(entry.createdAt)} for comparison`, checked: compareIds.includes(entry.id), onChange: () => toggleCompare(entry.id) }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: entry.label || "Untitled version" }), _jsx("div", { style: { fontSize: 11, color: "var(--srte-text-muted)" }, children: formatTimestamp(entry.createdAt) })] }), _jsx("button", { type: "button", "data-srte-version-restore": entry.id, disabled: busyId === entry.id, onClick: () => void handleRestore(entry), children: "Restore" }), _jsx("button", { type: "button", "data-srte-version-delete": entry.id, disabled: busyId === entry.id, onClick: () => void handleDelete(entry), style: { color: "var(--srte-danger)" }, children: "Delete" })] }, entry.id)))] })] }) }));
157
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A CSS `transform`, `perspective`, `filter`, `backdrop-filter`, or a
3
+ * `will-change` naming one of those, on an ancestor makes that ancestor the
4
+ * containing block for `position: fixed` descendants instead of the
5
+ * viewport (CSS Transforms spec's rendering model) - most commonly a
6
+ * Radix/shadcn `Dialog`'s centering transform (`translate-x-[-50%]
7
+ * translate-y-[-50%]`), which any host may legitimately wrap this editor
8
+ * in. When that happens, `left`/`top` set on a fixed-positioned menu/popover
9
+ * are resolved against that ancestor's border box (as rendered,
10
+ * post-transform) instead of the viewport, silently producing coordinates
11
+ * that are internally consistent but land far from the trigger with no
12
+ * error - confirmed live via a Sootr host rendering the editor inside such
13
+ * a dialog.
14
+ *
15
+ * `getBoundingClientRect()` always returns viewport-relative coordinates
16
+ * regardless of transforms, so every placement calculation in this codebase
17
+ * computes coordinates in viewport space first, then subtracts this
18
+ * origin - the real containing block's own viewport-relative top-left - so
19
+ * the applied `left`/`top` land correctly under either containing block.
20
+ */
21
+ export declare function findFixedPositioningContainer(el: HTMLElement): HTMLElement | null;
22
+ export declare function getFixedPositioningOrigin(el: HTMLElement): {
23
+ left: number;
24
+ top: number;
25
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * A CSS `transform`, `perspective`, `filter`, `backdrop-filter`, or a
3
+ * `will-change` naming one of those, on an ancestor makes that ancestor the
4
+ * containing block for `position: fixed` descendants instead of the
5
+ * viewport (CSS Transforms spec's rendering model) - most commonly a
6
+ * Radix/shadcn `Dialog`'s centering transform (`translate-x-[-50%]
7
+ * translate-y-[-50%]`), which any host may legitimately wrap this editor
8
+ * in. When that happens, `left`/`top` set on a fixed-positioned menu/popover
9
+ * are resolved against that ancestor's border box (as rendered,
10
+ * post-transform) instead of the viewport, silently producing coordinates
11
+ * that are internally consistent but land far from the trigger with no
12
+ * error - confirmed live via a Sootr host rendering the editor inside such
13
+ * a dialog.
14
+ *
15
+ * `getBoundingClientRect()` always returns viewport-relative coordinates
16
+ * regardless of transforms, so every placement calculation in this codebase
17
+ * computes coordinates in viewport space first, then subtracts this
18
+ * origin - the real containing block's own viewport-relative top-left - so
19
+ * the applied `left`/`top` land correctly under either containing block.
20
+ */
21
+ export function findFixedPositioningContainer(el) {
22
+ let node = el.parentElement;
23
+ while (node) {
24
+ const style = window.getComputedStyle(node);
25
+ if ((style.transform && style.transform !== "none") ||
26
+ (style.perspective && style.perspective !== "none") ||
27
+ (style.filter && style.filter !== "none") ||
28
+ (style.backdropFilter && style.backdropFilter !== "none") ||
29
+ (style.willChange && /transform|perspective|filter/.test(style.willChange))) {
30
+ return node;
31
+ }
32
+ node = node.parentElement;
33
+ }
34
+ return null;
35
+ }
36
+ export function getFixedPositioningOrigin(el) {
37
+ const container = findFixedPositioningContainer(el);
38
+ if (!container)
39
+ return { left: 0, top: 0 };
40
+ const rect = container.getBoundingClientRect();
41
+ return { left: rect.left, top: rect.top };
42
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Formula library (Direction B "Small" scope, per the owner-approved
3
+ * proposal - see the "Formula Library & Special Character Picker" scoping
4
+ * artifact): a curated set of common science/math formulas, browsable by
5
+ * domain and searchable by name, replacing the raw `window.prompt`
6
+ * formula-insert flow.
7
+ *
8
+ * Every entry's `latex` is plain LaTeX, verified against KaTeX 0.18's
9
+ * supported command set (the actual rendering ceiling - see
10
+ * `surface/renderer.ts`'s `KATEX_OPTIONS`, `{ trust: false, strict: "error"
11
+ * }`). Chemistry entries use the `mhchem` extension's `\ce{...}` macro,
12
+ * registered in `surface/renderer.ts` (core) for real document rendering
13
+ * and in `FormulaLibraryPopover.tsx` for this library's own previews.
14
+ *
15
+ * `notation` is always `"latex"` - the schema also accepts `"mathml"`, but
16
+ * the renderer never implements a MathML render path
17
+ * (docs/bugs/formula-mathml-notation-not-rendered.md, open) - so no library
18
+ * entry may use it.
19
+ */
20
+ export type FormulaDomain = "algebra" | "geometry" | "calculus" | "physics" | "chemistry" | "statistics";
21
+ export interface FormulaLibraryEntry {
22
+ readonly id: string;
23
+ readonly domain: FormulaDomain;
24
+ readonly name: string;
25
+ readonly latex: string;
26
+ }
27
+ export declare const FORMULA_DOMAINS: readonly {
28
+ readonly id: FormulaDomain;
29
+ readonly label: string;
30
+ }[];
31
+ export declare const FORMULA_LIBRARY: readonly FormulaLibraryEntry[];
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Formula library (Direction B "Small" scope, per the owner-approved
3
+ * proposal - see the "Formula Library & Special Character Picker" scoping
4
+ * artifact): a curated set of common science/math formulas, browsable by
5
+ * domain and searchable by name, replacing the raw `window.prompt`
6
+ * formula-insert flow.
7
+ *
8
+ * Every entry's `latex` is plain LaTeX, verified against KaTeX 0.18's
9
+ * supported command set (the actual rendering ceiling - see
10
+ * `surface/renderer.ts`'s `KATEX_OPTIONS`, `{ trust: false, strict: "error"
11
+ * }`). Chemistry entries use the `mhchem` extension's `\ce{...}` macro,
12
+ * registered in `surface/renderer.ts` (core) for real document rendering
13
+ * and in `FormulaLibraryPopover.tsx` for this library's own previews.
14
+ *
15
+ * `notation` is always `"latex"` - the schema also accepts `"mathml"`, but
16
+ * the renderer never implements a MathML render path
17
+ * (docs/bugs/formula-mathml-notation-not-rendered.md, open) - so no library
18
+ * entry may use it.
19
+ */
20
+ export const FORMULA_DOMAINS = [
21
+ { id: "algebra", label: "Algebra" },
22
+ { id: "geometry", label: "Geometry" },
23
+ { id: "calculus", label: "Calculus" },
24
+ { id: "physics", label: "Physics" },
25
+ { id: "chemistry", label: "Chemistry" },
26
+ { id: "statistics", label: "Statistics" },
27
+ ];
28
+ export const FORMULA_LIBRARY = [
29
+ // Algebra
30
+ { id: "algebra-quadratic", domain: "algebra", name: "Quadratic formula", latex: "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}" },
31
+ { id: "algebra-binomial-sum-sq", domain: "algebra", name: "Square of a sum", latex: "(a+b)^2 = a^2 + 2ab + b^2" },
32
+ { id: "algebra-binomial-diff-sq", domain: "algebra", name: "Square of a difference", latex: "(a-b)^2 = a^2 - 2ab + b^2" },
33
+ { id: "algebra-diff-of-squares", domain: "algebra", name: "Difference of squares", latex: "a^2 - b^2 = (a+b)(a-b)" },
34
+ { id: "algebra-exponent-product", domain: "algebra", name: "Exponent product rule", latex: "a^m \\cdot a^n = a^{m+n}" },
35
+ { id: "algebra-exponent-power", domain: "algebra", name: "Exponent power rule", latex: "(a^m)^n = a^{mn}" },
36
+ { id: "algebra-log-product", domain: "algebra", name: "Logarithm product rule", latex: "\\log_a(xy) = \\log_a x + \\log_a y" },
37
+ { id: "algebra-log-quotient", domain: "algebra", name: "Logarithm quotient rule", latex: "\\log_a\\left(\\frac{x}{y}\\right) = \\log_a x - \\log_a y" },
38
+ { id: "algebra-binomial-theorem", domain: "algebra", name: "Binomial theorem", latex: "(x+y)^n = \\sum_{k=0}^{n} \\binom{n}{k} x^{n-k} y^k" },
39
+ { id: "algebra-vertex", domain: "algebra", name: "Vertex of a parabola", latex: "x = -\\frac{b}{2a}" },
40
+ // Geometry
41
+ { id: "geometry-circle-area", domain: "geometry", name: "Circle area", latex: "A = \\pi r^2" },
42
+ { id: "geometry-circle-circumference", domain: "geometry", name: "Circle circumference", latex: "C = 2\\pi r" },
43
+ { id: "geometry-triangle-area", domain: "geometry", name: "Triangle area", latex: "A = \\frac{1}{2}bh" },
44
+ { id: "geometry-sphere-volume", domain: "geometry", name: "Sphere volume", latex: "V = \\frac{4}{3}\\pi r^3" },
45
+ { id: "geometry-sphere-surface", domain: "geometry", name: "Sphere surface area", latex: "A = 4\\pi r^2" },
46
+ { id: "geometry-cylinder-volume", domain: "geometry", name: "Cylinder volume", latex: "V = \\pi r^2 h" },
47
+ { id: "geometry-cone-volume", domain: "geometry", name: "Cone volume", latex: "V = \\frac{1}{3}\\pi r^2 h" },
48
+ { id: "geometry-pythagorean", domain: "geometry", name: "Pythagorean theorem", latex: "a^2 + b^2 = c^2" },
49
+ { id: "geometry-box-volume", domain: "geometry", name: "Rectangular box volume", latex: "V = lwh" },
50
+ { id: "geometry-trapezoid-area", domain: "geometry", name: "Trapezoid area", latex: "A = \\frac{1}{2}(b_1+b_2)h" },
51
+ // Calculus
52
+ { id: "calculus-power-rule", domain: "calculus", name: "Power rule (derivative)", latex: "\\frac{d}{dx}x^n = nx^{n-1}" },
53
+ { id: "calculus-product-rule", domain: "calculus", name: "Product rule", latex: "\\frac{d}{dx}[f(x)g(x)] = f'(x)g(x) + f(x)g'(x)" },
54
+ { id: "calculus-quotient-rule", domain: "calculus", name: "Quotient rule", latex: "\\frac{d}{dx}\\left[\\frac{f(x)}{g(x)}\\right] = \\frac{f'(x)g(x) - f(x)g'(x)}{g(x)^2}" },
55
+ { id: "calculus-chain-rule", domain: "calculus", name: "Chain rule", latex: "\\frac{d}{dx}f(g(x)) = f'(g(x))\\,g'(x)" },
56
+ { id: "calculus-ftc", domain: "calculus", name: "Fundamental theorem of calculus", latex: "\\int_a^b f'(x)\\,dx = f(b) - f(a)" },
57
+ { id: "calculus-power-rule-integral", domain: "calculus", name: "Power rule (integral)", latex: "\\int x^n\\,dx = \\frac{x^{n+1}}{n+1} + C" },
58
+ { id: "calculus-derivative-exp", domain: "calculus", name: "Derivative of eˣ", latex: "\\frac{d}{dx}e^x = e^x" },
59
+ { id: "calculus-derivative-ln", domain: "calculus", name: "Derivative of ln(x)", latex: "\\frac{d}{dx}\\ln x = \\frac{1}{x}" },
60
+ { id: "calculus-derivative-def", domain: "calculus", name: "Definition of the derivative", latex: "f'(x) = \\lim_{h \\to 0} \\frac{f(x+h) - f(x)}{h}" },
61
+ { id: "calculus-taylor-series", domain: "calculus", name: "Taylor series", latex: "f(x) = \\sum_{n=0}^{\\infty} \\frac{f^{(n)}(a)}{n!}(x-a)^n" },
62
+ // Physics
63
+ { id: "physics-kinematics-v", domain: "physics", name: "Kinematics: velocity", latex: "v = u + at" },
64
+ { id: "physics-kinematics-s", domain: "physics", name: "Kinematics: displacement", latex: "s = ut + \\frac{1}{2}at^2" },
65
+ { id: "physics-kinematics-v2", domain: "physics", name: "Kinematics: velocity²", latex: "v^2 = u^2 + 2as" },
66
+ { id: "physics-newton-second", domain: "physics", name: "Newton's second law", latex: "F = ma" },
67
+ { id: "physics-kinetic-energy", domain: "physics", name: "Kinetic energy", latex: "E_k = \\frac{1}{2}mv^2" },
68
+ { id: "physics-potential-energy", domain: "physics", name: "Gravitational potential energy", latex: "E_p = mgh" },
69
+ { id: "physics-ohms-law", domain: "physics", name: "Ohm's law", latex: "V = IR" },
70
+ { id: "physics-power", domain: "physics", name: "Power", latex: "P = \\frac{W}{t}" },
71
+ { id: "physics-gravitation", domain: "physics", name: "Newton's law of gravitation", latex: "F = G\\frac{m_1 m_2}{r^2}" },
72
+ { id: "physics-momentum", domain: "physics", name: "Momentum", latex: "p = mv" },
73
+ { id: "physics-wave-equation", domain: "physics", name: "Wave equation", latex: "v = f\\lambda" },
74
+ { id: "physics-mass-energy", domain: "physics", name: "Mass-energy equivalence", latex: "E = mc^2" },
75
+ // Chemistry (needs the mhchem KaTeX extension - see this file's own doc comment)
76
+ { id: "chemistry-ideal-gas", domain: "chemistry", name: "Ideal gas law", latex: "PV = nRT" },
77
+ { id: "chemistry-molarity", domain: "chemistry", name: "Molarity", latex: "M = \\frac{n}{V}" },
78
+ { id: "chemistry-ph", domain: "chemistry", name: "pH", latex: "\\text{pH} = -\\log_{10}[\\text{H}^+]" },
79
+ { id: "chemistry-density", domain: "chemistry", name: "Density", latex: "\\rho = \\frac{m}{V}" },
80
+ { id: "chemistry-combined-gas-law", domain: "chemistry", name: "Combined gas law", latex: "\\frac{P_1 V_1}{T_1} = \\frac{P_2 V_2}{T_2}" },
81
+ { id: "chemistry-percent-yield", domain: "chemistry", name: "Percent yield", latex: "\\%\\,\\text{yield} = \\frac{\\text{actual}}{\\text{theoretical}} \\times 100\\%" },
82
+ { id: "chemistry-water-formation", domain: "chemistry", name: "Combustion: hydrogen + oxygen", latex: "\\ce{2 H2 + O2 -> 2 H2O}" },
83
+ // Statistics
84
+ { id: "statistics-mean", domain: "statistics", name: "Mean", latex: "\\bar{x} = \\frac{1}{n}\\sum_{i=1}^{n} x_i" },
85
+ { id: "statistics-variance", domain: "statistics", name: "Variance", latex: "\\sigma^2 = \\frac{1}{n}\\sum_{i=1}^{n}(x_i - \\bar{x})^2" },
86
+ { id: "statistics-std-dev", domain: "statistics", name: "Standard deviation", latex: "\\sigma = \\sqrt{\\frac{1}{n}\\sum_{i=1}^{n}(x_i - \\bar{x})^2}" },
87
+ { id: "statistics-normal-pdf", domain: "statistics", name: "Normal distribution", latex: "f(x) = \\frac{1}{\\sigma\\sqrt{2\\pi}}\\, e^{-\\frac{(x-\\mu)^2}{2\\sigma^2}}" },
88
+ { id: "statistics-z-score", domain: "statistics", name: "Z-score", latex: "z = \\frac{x - \\mu}{\\sigma}" },
89
+ { id: "statistics-binomial-probability", domain: "statistics", name: "Binomial probability", latex: "P(X=k) = \\binom{n}{k} p^k (1-p)^{n-k}" },
90
+ ];