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.
- package/CHANGELOG.md +55 -0
- package/README.md +205 -14
- package/dist/adapters/domChecklistCommandBridge.js +1 -1
- package/dist/adapters/domCommandBridge.d.ts +2 -2
- package/dist/adapters/domCommandBridge.js +2 -2
- package/dist/adapters/domInlineAtomCommandBridge.d.ts +10 -9
- package/dist/adapters/domInlineAtomCommandBridge.js +154 -50
- package/dist/adapters/domInlineImageCommandBridge.d.ts +7 -7
- package/dist/adapters/domInlineImageCommandBridge.js +6 -72
- package/dist/adapters/domMoveCommandBridge.js +1 -1
- package/dist/adapters/domSelectionBridge.d.ts +3 -3
- package/dist/adapters/domSmartDocument.d.ts +4 -4
- package/dist/adapters/domSmartDocument.js +2 -2
- package/dist/adapters/domTableCommandBridge.d.ts +42 -16
- package/dist/adapters/domTableCommandBridge.js +203 -173
- package/dist/adapters/internalFlags.d.ts +0 -8
- package/dist/adapters/internalFlags.js +0 -24
- package/dist/adapters/legacyListShadowComparator.d.ts +1 -1
- package/dist/adapters/legacyListShadowComparator.js +1 -1
- package/dist/adapters/pdfPrint.d.ts +8 -0
- package/dist/adapters/pdfPrint.js +22 -0
- package/dist/adapters/shadowMode.d.ts +3 -3
- package/dist/adapters/shadowMode.js +1 -1
- package/dist/canonicalEditorRuntime.d.ts +65 -1
- package/dist/canonicalEditorRuntime.js +63 -5
- package/dist/capabilityPresets.d.ts +27 -0
- package/dist/capabilityPresets.js +21 -0
- package/dist/collabTransport.d.ts +66 -0
- package/dist/collabTransport.js +1 -0
- package/dist/commentProvider.d.ts +24 -0
- package/dist/commentProvider.js +1 -0
- package/dist/components/CanonicalAuthorityEditor.d.ts +56 -1
- package/dist/components/CanonicalAuthorityEditor.js +1304 -116
- package/dist/components/ClassicEditorAuthority.d.ts +93 -22
- package/dist/components/ClassicEditorAuthority.js +11 -43
- package/dist/components/ColorPickerPopover.d.ts +39 -0
- package/dist/components/ColorPickerPopover.js +287 -0
- package/dist/components/CommentMarkers.d.ts +25 -0
- package/dist/components/CommentMarkers.js +105 -0
- package/dist/components/CommentThreadPanel.d.ts +26 -0
- package/dist/components/CommentThreadPanel.js +47 -0
- package/dist/components/ContextMenu.d.ts +23 -0
- package/dist/components/ContextMenu.js +141 -0
- package/dist/components/FormulaLibraryPopover.d.ts +16 -0
- package/dist/components/FormulaLibraryPopover.js +107 -0
- package/dist/components/LinkEditorPopover.d.ts +3 -1
- package/dist/components/LinkEditorPopover.js +22 -6
- package/dist/components/MediaDetailsPopover.d.ts +35 -0
- package/dist/components/MediaDetailsPopover.js +136 -0
- package/dist/components/MediaManager.d.ts +4 -20
- package/dist/components/MediaManager.js +40 -11
- package/dist/components/MediaOverlay.d.ts +27 -0
- package/dist/components/MediaOverlay.js +228 -0
- package/dist/components/SpecialCharacterPopover.d.ts +18 -0
- package/dist/components/SpecialCharacterPopover.js +99 -0
- package/dist/components/StructuralSuggestionMarkers.d.ts +20 -0
- package/dist/components/StructuralSuggestionMarkers.js +56 -0
- package/dist/components/SuggestionPanel.d.ts +26 -0
- package/dist/components/SuggestionPanel.js +33 -0
- package/dist/components/TableBorderPopover.d.ts +59 -0
- package/dist/components/TableBorderPopover.js +149 -0
- package/dist/components/TableResizeHandles.d.ts +25 -0
- package/dist/components/TableResizeHandles.js +323 -0
- package/dist/components/TableSizePickerPopover.d.ts +16 -0
- package/dist/components/TableSizePickerPopover.js +126 -0
- package/dist/components/ToolbarPrimitives.d.ts +161 -0
- package/dist/components/ToolbarPrimitives.js +237 -0
- package/dist/components/VersionHistoryPanel.d.ts +9 -0
- package/dist/components/VersionHistoryPanel.js +157 -0
- package/dist/components/fixedPositioning.d.ts +25 -0
- package/dist/components/fixedPositioning.js +42 -0
- package/dist/formulaLibrary.d.ts +31 -0
- package/dist/formulaLibrary.js +90 -0
- package/dist/index.d.ts +25 -24
- package/dist/index.js +15 -11
- package/dist/mediaManagerAdapter.d.ts +12 -0
- package/dist/mediaManagerAdapter.js +34 -0
- package/dist/mediaProvider.d.ts +21 -1
- package/dist/specialCharacters.d.ts +22 -0
- package/dist/specialCharacters.js +86 -0
- package/dist/standalone/classic-editor-embed.js +19 -12
- package/dist/suggestionProvider.d.ts +17 -0
- package/dist/suggestionProvider.js +1 -0
- package/dist/theme.d.ts +1 -1
- package/dist/theme.js +219 -30
- package/dist/toolbarTools.d.ts +95 -0
- package/dist/toolbarTools.js +17 -0
- package/dist/versionProvider.d.ts +29 -0
- package/dist/versionProvider.js +1 -0
- package/package.json +28 -14
- package/dist/adapters/canonicalInlineCommandBridge.d.ts +0 -15
- package/dist/adapters/canonicalInlineCommandBridge.js +0 -269
- package/dist/adapters/canonicalListCommandBridge.d.ts +0 -26
- package/dist/adapters/canonicalListCommandBridge.js +0 -430
- package/dist/adapters/coreBold.d.ts +0 -5
- package/dist/adapters/coreBold.js +0 -3
- package/dist/adapters/documentFormats.d.ts +0 -26
- package/dist/adapters/documentFormats.js +0 -49
- package/dist/adapters/docxFormat.d.ts +0 -5
- package/dist/adapters/docxFormat.js +0 -272
- package/dist/adapters/domBlockCommandBridge.d.ts +0 -19
- package/dist/adapters/domBlockCommandBridge.js +0 -79
- package/dist/adapters/inlineMarkCoreExecution.d.ts +0 -9
- package/dist/adapters/inlineMarkCoreExecution.js +0 -25
- package/dist/adapters/pdfFormat.d.ts +0 -23
- package/dist/adapters/pdfFormat.js +0 -204
- package/dist/adapters/portableDocxAtoms.d.ts +0 -4
- package/dist/adapters/portableDocxAtoms.js +0 -58
- package/dist/adapters/styledDocxFormat.d.ts +0 -8
- package/dist/adapters/styledDocxFormat.js +0 -190
- package/dist/builtInFormatDefinitions.d.ts +0 -5
- package/dist/builtInFormatDefinitions.js +0 -82
- package/dist/canonicalAuthorityFlag.d.ts +0 -32
- package/dist/canonicalAuthorityFlag.js +0 -49
- package/dist/canonicalClipboardRuntime.d.ts +0 -15
- package/dist/canonicalClipboardRuntime.js +0 -76
- package/dist/components/ClassicEditor.d.ts +0 -76
- package/dist/components/ClassicEditor.js +0 -6729
- package/dist/editorController.d.ts +0 -78
- package/dist/editorController.js +0 -298
- package/dist/formatFidelity.d.ts +0 -14
- package/dist/formatFidelity.js +0 -107
- package/dist/formatRuntime.d.ts +0 -50
- package/dist/formatRuntime.js +0 -20
- package/dist/pluginRuntime.d.ts +0 -68
- package/dist/pluginRuntime.js +0 -78
- package/dist/standalone/editor.js +0 -1068
- package/dist/test-harness/atomShadowComparator.d.ts +0 -14
- package/dist/test-harness/atomShadowComparator.js +0 -63
- package/dist/test-harness/blockShadowComparator.d.ts +0 -30
- package/dist/test-harness/blockShadowComparator.js +0 -140
- package/dist/test-harness/clipboardShadowComparator.d.ts +0 -12
- package/dist/test-harness/clipboardShadowComparator.js +0 -46
- package/dist/test-harness/inlineShadowComparator.d.ts +0 -32
- package/dist/test-harness/inlineShadowComparator.js +0 -152
- package/dist/test-harness/legacyAtomEngine.d.ts +0 -10
- package/dist/test-harness/legacyAtomEngine.js +0 -15
- package/dist/test-harness/legacyBlockEngine.d.ts +0 -31
- package/dist/test-harness/legacyBlockEngine.js +0 -31
- package/dist/test-harness/legacyClipboardEngine.d.ts +0 -14
- package/dist/test-harness/legacyClipboardEngine.js +0 -67
- package/dist/test-harness/legacyInlineEngine.d.ts +0 -19
- package/dist/test-harness/legacyInlineEngine.js +0 -49
- package/dist/test-harness/legacyTableEngine.d.ts +0 -12
- package/dist/test-harness/legacyTableEngine.js +0 -25
- package/dist/test-harness/tableShadowComparator.d.ts +0 -29
- 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
|
+
];
|